| // Copyright 2024 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package golang | |
| // This file defines a simple HTML rendering of package documentation | |
| // in imitation of the style of pkg.go.dev. | |
| // | |
| // The current implementation is just a starting point and a | |
| // placeholder for a more sophisticated one. | |
| // | |
| // TODO(adonovan): | |
| // - rewrite using html/template. No, really, let's do this. | |
| // - emit breadcrumbs for parent + sibling packages. | |
| // - list promoted methods---we have type information! (golang/go#67158) | |
| // - gather Example tests, following go/doc and pkgsite. | |
| // - add option for doc.AllDecls: show non-exported symbols too. | |
| // - style the <li> bullets in the index as invisible. | |
| // - add push notifications such as didChange -> reload. | |
| // - there appears to be a maximum file size beyond which the | |
| // "source.doc" code action is not offered. Remove that. | |
| // - modify JS httpGET function to give a transient visual indication | |
| // when clicking a source link that the editor is being navigated | |
| // (in case it doesn't raise itself, like VS Code). | |
| // - move this into a new package, golang/web, and then | |
| // split out the various helpers without fear of polluting | |
| // the golang package namespace? | |
| // - show "Deprecated" chip when appropriate. | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "go/ast" | |
| "go/doc" | |
| "go/doc/comment" | |
| "go/format" | |
| "go/token" | |
| "go/types" | |
| "html/template" | |
| "path/filepath" | |
| "slices" | |
| "strings" | |
| "golang.org/x/tools/gopls/internal/cache" | |
| "golang.org/x/tools/gopls/internal/cache/parsego" | |
| "golang.org/x/tools/gopls/internal/protocol" | |
| "golang.org/x/tools/gopls/internal/util/bug" | |
| "golang.org/x/tools/gopls/internal/util/cursorutil" | |
| "golang.org/x/tools/internal/astutil" | |
| "golang.org/x/tools/internal/stdlib" | |
| "golang.org/x/tools/internal/typesinternal" | |
| ) | |
| // DocFragment finds the package and (optionally) symbol identified by | |
| // the current selection, and returns the package path and the | |
| // optional symbol URL fragment (e.g. "#Buffer.Len") for a symbol, | |
| // along with a title for the code action. | |
| // | |
| // It is called once to offer the code action, and again when the | |
| // command is executed. This is slightly inefficient but ensures that | |
| // the title and package/symbol logic are consistent in all cases. | |
| // | |
| // It returns zeroes if there is nothing to see here (e.g. reference to a builtin). | |
| func DocFragment(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (pkgpath PackagePath, fragment, title string) { | |
| thing := thingAtPoint(pkg, pgf, start, end) | |
| makeTitle := func(kind string, imp *types.Package, name string) string { | |
| title := "Browse documentation for " + kind + " " | |
| if imp != nil && imp != pkg.Types() { | |
| title += imp.Name() + "." | |
| } | |
| return title + name | |
| } | |
| wholePackage := func(pkg *types.Package) (PackagePath, string, string) { | |
| // External test packages don't have /pkg doc pages, | |
| // so instead show the doc for the package under test. | |
| // (This named-based heuristic is imperfect.) | |
| if forTest, ok := strings.CutSuffix(pkg.Path(), "_test"); ok { | |
| return PackagePath(forTest), "", makeTitle("package", nil, filepath.Base(forTest)) | |
| } | |
| return PackagePath(pkg.Path()), "", makeTitle("package", nil, pkg.Name()) | |
| } | |
| // Conceptually, we check cases in the order: | |
| // 1. symbol | |
| // 2. package | |
| // 3. enclosing | |
| // but the logic of cases 1 and 3 are identical, hence the odd factoring. | |
| // Imported package? | |
| if thing.pkg != nil && thing.symbol == nil { | |
| return wholePackage(thing.pkg) | |
| } | |
| // Symbol? | |
| var sym types.Object | |
| if thing.symbol != nil { | |
| sym = thing.symbol // reference to a symbol | |
| } else if thing.enclosing != nil { | |
| sym = thing.enclosing // selection is within a declaration of a symbol | |
| } | |
| if sym == nil { | |
| return wholePackage(pkg.Types()) // no symbol | |
| } | |
| // Built-in (error.Error, append or unsafe). | |
| // TODO(adonovan): handle builtins in /pkg viewer. | |
| if sym.Pkg() == nil { | |
| return "", "", "" // nothing to see here | |
| } | |
| pkgpath = PackagePath(sym.Pkg().Path()) | |
| // Unexported? Show enclosing type or package. | |
| if !sym.Exported() { | |
| // Unexported method of exported type? | |
| if fn, ok := sym.(*types.Func); ok { | |
| _, named := typesinternal.RecvBase(fn) | |
| if named != nil && named.Obj().Exported() { | |
| sym = named.Obj() | |
| goto below | |
| } | |
| } | |
| return wholePackage(sym.Pkg()) | |
| below: | |
| } | |
| // Reference to symbol in external test package? | |
| // Short-circuit: see comment in wholePackage. | |
| if strings.HasSuffix(string(pkgpath), "_test") { | |
| return wholePackage(pkg.Types()) | |
| } | |
| // package-level symbol? | |
| if typesinternal.IsPackageLevel(sym) { | |
| return pkgpath, sym.Name(), makeTitle(objectKind(sym), sym.Pkg(), sym.Name()) | |
| } | |
| // Inv: sym is field or method, or local. | |
| switch sym := sym.(type) { | |
| case *types.Func: // => method | |
| isPtr, named := typesinternal.RecvBase(sym) | |
| if named != nil { | |
| if !named.Obj().Exported() { | |
| return wholePackage(sym.Pkg()) // exported method of unexported type | |
| } | |
| name := fmt.Sprintf("(%s%s).%s", | |
| strings.Repeat("*", btoi(isPtr)), // for *T | |
| named.Obj().Name(), | |
| sym.Name()) | |
| fragment := named.Obj().Name() + "." + sym.Name() | |
| return pkgpath, fragment, makeTitle("method", sym.Pkg(), name) | |
| } | |
| case *types.Var: | |
| if sym.IsField() { | |
| // TODO(adonovan): support fields. | |
| // The Var symbol doesn't include the struct | |
| // type, so we need to use the logic from | |
| // Hover. (This isn't important for | |
| // DocFragment as fields don't have fragments, | |
| // but it matters to the grand unification of | |
| // Hover/Definition/DocFragment. | |
| } | |
| } | |
| // Field, non-exported method, or local declaration: | |
| // just show current package. | |
| return wholePackage(pkg.Types()) | |
| } | |
| // thing describes the package or symbol denoted by a selection. | |
| // | |
| // TODO(adonovan): Hover, Definition, and References all start by | |
| // identifying the selected object. Let's achieve a better factoring | |
| // of the common parts using this structure, including uniform | |
| // treatment of doc links, linkname, and suchlike. | |
| type thing struct { | |
| // At most one of these fields is set. | |
| // (The 'enclosing' field is a fallback for when neither | |
| // of the first two is set.) | |
| symbol types.Object // referenced symbol | |
| pkg *types.Package // referenced package | |
| enclosing types.Object // package-level symbol or method decl enclosing selection | |
| } | |
| func thingAtPoint(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) thing { | |
| cur, _, _, _ := astutil.Select(pgf.Cursor(), start, end) | |
| // In an import spec? | |
| if spec, _ := cursorutil.FirstEnclosing[*ast.ImportSpec](cur); spec != nil { | |
| if pkgname := pkg.TypesInfo().PkgNameOf(spec); pkgname != nil { | |
| return thing{pkg: pkgname.Imported()} | |
| } | |
| } | |
| // Definition or reference to symbol? | |
| var obj types.Object | |
| if id, ok := cur.Node().(*ast.Ident); ok { | |
| obj = pkg.TypesInfo().ObjectOf(id) | |
| // Treat use to PkgName like ImportSpec. | |
| if pkgname, ok := obj.(*types.PkgName); ok { | |
| return thing{pkg: pkgname.Imported()} | |
| } | |
| } else if sel, ok := cur.Node().(*ast.SelectorExpr); ok { | |
| // e.g. selection is "fmt.Println" or just a portion ("mt.Prin") | |
| obj = pkg.TypesInfo().Uses[sel.Sel] | |
| } | |
| if obj != nil { | |
| return thing{symbol: obj} | |
| } | |
| // Find outermost enclosing declaration. | |
| var id *ast.Ident | |
| for cur := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.ValueSpec)(nil), (*ast.TypeSpec)(nil)) { | |
| switch n := cur.Node().(type) { | |
| case *ast.FuncDecl: // function or method | |
| id = n.Name | |
| case *ast.ValueSpec: | |
| id = n.Names[0] // var, const: use first name | |
| case *ast.TypeSpec: | |
| id = n.Name | |
| } | |
| } | |
| if obj := pkg.TypesInfo().Defs[id]; obj != nil { | |
| return thing{enclosing: obj} | |
| } | |
| return thing{} // nothing to see here | |
| } | |
| // Web is an abstraction of gopls' web server. | |
| type Web interface { | |
| // PkgURL forms URLs of package or symbol documentation. | |
| PkgURL(viewID string, path PackagePath, fragment string) protocol.URI | |
| // SrcURL forms URLs that cause the editor to open a file at a specific position. | |
| SrcURL(filename string, line, col8 int) protocol.URI | |
| } | |
| // PackageDocHTML formats the package documentation page. | |
| // | |
| // The posURL function returns a URL that when visited, has the side | |
| // effect of causing gopls to direct the client editor to navigate to | |
| // the specified file/line/column position, in UTF-8 coordinates. | |
| // | |
| // TODO(adonovan): this function could use some unit tests; we | |
| // shouldn't have to use integration tests to cover microdetails of | |
| // HTML rendering. (It is tempting to abstract this function so that | |
| // it depends only on FileSet/File/Types/TypeInfo/etc, but we should | |
| // bend the tests to the production interfaces, not the other way | |
| // around.) | |
| func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error) { | |
| // We can't use doc.NewFromFiles (even with doc.PreserveAST | |
| // mode) as it calls ast.NewPackage which assumes that each | |
| // ast.File has an ast.Scope and resolves identifiers to | |
| // (deprecated) ast.Objects. (This is golang/go#66290.) | |
| // But doc.New only requires pkg.{Name,Files}, | |
| // so we just boil it down. | |
| // | |
| // The only loss is doc.classifyExamples. | |
| // TODO(adonovan): simulate that too. | |
| fileMap := make(map[string]*ast.File) | |
| for _, f := range pkg.Syntax() { | |
| fileMap[pkg.FileSet().File(f.FileStart).Name()] = f | |
| } | |
| astpkg := &ast.Package{ | |
| Name: pkg.Types().Name(), | |
| Files: fileMap, | |
| } | |
| // PreserveAST mode only half works (golang/go#66449): it still | |
| // mutates ASTs when filtering out non-exported symbols. | |
| // As a workaround, enable AllDecls to suppress filtering, | |
| // and do it ourselves. | |
| mode := doc.PreserveAST | doc.AllDecls | |
| docpkg := doc.New(astpkg, pkg.Types().Path(), mode) | |
| // Discard non-exported symbols. | |
| // TODO(adonovan): do this conditionally, and expose option in UI. | |
| const showUnexported = false | |
| if !showUnexported { | |
| var ( | |
| unexported = func(name string) bool { return !token.IsExported(name) } | |
| filterValues = func(slice *[]*doc.Value) { | |
| delValue := func(v *doc.Value) bool { | |
| v.Names = slices.DeleteFunc(v.Names, unexported) | |
| return len(v.Names) == 0 | |
| } | |
| *slice = slices.DeleteFunc(*slice, delValue) | |
| } | |
| filterFuncs = func(funcs *[]*doc.Func) { | |
| *funcs = slices.DeleteFunc(*funcs, func(v *doc.Func) bool { | |
| return unexported(v.Name) | |
| }) | |
| } | |
| ) | |
| filterValues(&docpkg.Consts) | |
| filterValues(&docpkg.Vars) | |
| filterFuncs(&docpkg.Funcs) | |
| docpkg.Types = slices.DeleteFunc(docpkg.Types, func(t *doc.Type) bool { | |
| filterValues(&t.Consts) | |
| filterValues(&t.Vars) | |
| filterFuncs(&t.Funcs) | |
| filterFuncs(&t.Methods) | |
| if unexported(t.Name) { | |
| // If an unexported type has an exported constructor function, | |
| // treat the constructor as an ordinary standalone function. | |
| // We will sort Funcs again below. | |
| docpkg.Funcs = append(docpkg.Funcs, t.Funcs...) | |
| return true // delete this type | |
| } | |
| return false // keep this type | |
| }) | |
| slices.SortFunc(docpkg.Funcs, func(x, y *doc.Func) int { | |
| return strings.Compare(x.Name, y.Name) | |
| }) | |
| } | |
| // docHTML renders the doc comment as Markdown. | |
| // The fileNode is used to deduce the enclosing file | |
| // for the correct import mapping. | |
| // | |
| // It is not concurrency-safe. | |
| var docHTML func(fileNode ast.Node, comment string) []byte | |
| { | |
| // Adapt doc comment parser and printer | |
| // to our representation of Go packages | |
| // so that doc links (e.g. "[fmt.Println]") | |
| // become valid links. | |
| printer := &comment.Printer{ | |
| DocLinkURL: func(link *comment.DocLink) string { | |
| path := pkg.Metadata().PkgPath | |
| if link.ImportPath != "" { | |
| path = PackagePath(link.ImportPath) | |
| } | |
| fragment := link.Name | |
| if link.Recv != "" { | |
| fragment = link.Recv + "." + link.Name | |
| } | |
| return web.PkgURL(viewID, path, fragment) | |
| }, | |
| } | |
| parse := newDocCommentParser(pkg) | |
| docHTML = func(fileNode ast.Node, comment string) []byte { | |
| doc := parse(fileNode, comment) | |
| return printer.HTML(doc) | |
| } | |
| } | |
| scope := pkg.Types().Scope() | |
| escape := template.HTMLEscapeString | |
| title := fmt.Sprintf("%s package - %s - Gopls packages", | |
| pkg.Types().Name(), escape(pkg.Types().Path())) | |
| var buf bytes.Buffer | |
| buf.WriteString(`<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>` + title + `</title> | |
| <link rel="stylesheet" href="/assets/common.css"> | |
| <script src="/assets/common.js"></script> | |
| <style> | |
| .lit { color: darkgreen; } | |
| header { | |
| position: sticky; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| padding: 0.3em; | |
| } | |
| .Documentation-sinceVersion { | |
| font-weight: normal; | |
| color: #808080; | |
| float: right; | |
| } | |
| #pkgsite { height: 1.5em; } | |
| #hdr-Selector { | |
| margin-right: 0.3em; | |
| float: right; | |
| min-width: 25em; | |
| padding: 0.3em; | |
| } | |
| </style> | |
| <script type='text/javascript'> | |
| window.addEventListener('load', function() { | |
| // Hook up the navigation selector. | |
| document.getElementById('hdr-Selector').onchange = (e) => { | |
| window.location.href = e.target.value; | |
| }; | |
| }); | |
| </script> | |
| </head> | |
| <body> | |
| <header> | |
| <select id='hdr-Selector'> | |
| <optgroup label="Documentation"> | |
| <option label="Overview" value="#hdr-Overview"/> | |
| <option label="Index" value="#hdr-Index"/> | |
| <option label="Constants" value="#hdr-Constants"/> | |
| <option label="Variables" value="#hdr-Variables"/> | |
| <option label="Functions" value="#hdr-Functions"/> | |
| <option label="Types" value="#hdr-Types"/> | |
| <option label="Source Files" value="#hdr-SourceFiles"/> | |
| </optgroup> | |
| `) | |
| // -- header select element -- | |
| // option emits an <option> for the specified symbol. | |
| // | |
| // recvType is the apparent receiver type, which may | |
| // differ from RecvBase(obj).Name for promoted methods. | |
| option := func(obj types.Object, recvType string) { | |
| // Render functions/methods as "(recv) Method(p1, ..., pN)". | |
| fragment := obj.Name() | |
| // format parameter names (p1, ..., pN) | |
| label := obj.Name() // for a type | |
| if fn, ok := obj.(*types.Func); ok { | |
| var buf strings.Builder | |
| sig := fn.Signature() | |
| if sig.Recv() != nil { | |
| fmt.Fprintf(&buf, "(%s) ", sig.Recv().Name()) | |
| fragment = recvType + "." + fn.Name() | |
| } | |
| fmt.Fprintf(&buf, "%s(", fn.Name()) | |
| for i := 0; i < sig.Params().Len(); i++ { | |
| if i > 0 { | |
| buf.WriteString(", ") | |
| } | |
| name := sig.Params().At(i).Name() | |
| if name == "" { | |
| name = "_" | |
| } | |
| buf.WriteString(name) | |
| } | |
| buf.WriteByte(')') | |
| label = buf.String() | |
| } | |
| fmt.Fprintf(&buf, " <option label='%s' value='#%s'/>\n", label, fragment) | |
| } | |
| // index of functions | |
| fmt.Fprintf(&buf, "<optgroup label='Functions'>\n") | |
| for _, fn := range docpkg.Funcs { | |
| option(scope.Lookup(fn.Name), "") | |
| } | |
| fmt.Fprintf(&buf, "</optgroup>\n") | |
| // index of types | |
| fmt.Fprintf(&buf, "<optgroup label='Types'>\n") | |
| for _, doctype := range docpkg.Types { | |
| option(scope.Lookup(doctype.Name), "") | |
| } | |
| fmt.Fprintf(&buf, "</optgroup>\n") | |
| // index of constructors and methods of each type | |
| for _, doctype := range docpkg.Types { | |
| tname := scope.Lookup(doctype.Name).(*types.TypeName) | |
| if len(doctype.Funcs)+len(doctype.Methods) > 0 { | |
| fmt.Fprintf(&buf, "<optgroup label='type %s'>\n", doctype.Name) | |
| for _, docfn := range doctype.Funcs { | |
| option(scope.Lookup(docfn.Name), "") | |
| } | |
| for _, docmethod := range doctype.Methods { | |
| method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) | |
| option(method, doctype.Name) | |
| } | |
| fmt.Fprintf(&buf, "</optgroup>\n") | |
| } | |
| } | |
| fmt.Fprintf(&buf, "</select>\n") | |
| fmt.Fprintf(&buf, "</header>\n") | |
| // -- main element -- | |
| // nodeHTML returns HTML markup for a syntax tree. | |
| // It replaces referring identifiers with links, | |
| // and adds style spans for strings and comments. | |
| nodeHTML := func(n ast.Node) string { | |
| // linkify returns the appropriate URL (if any) for an identifier. | |
| linkify := func(id *ast.Ident) protocol.URI { | |
| if obj, ok := pkg.TypesInfo().Uses[id]; ok && obj.Pkg() != nil { | |
| // imported package name? | |
| if pkgname, ok := obj.(*types.PkgName); ok { | |
| // TODO(adonovan): do this for Defs of PkgName too. | |
| return web.PkgURL(viewID, PackagePath(pkgname.Imported().Path()), "") | |
| } | |
| // package-level symbol? | |
| if obj.Parent() == obj.Pkg().Scope() { | |
| if obj.Pkg() == pkg.Types() { | |
| return "#" + obj.Name() // intra-package ref | |
| } else { | |
| return web.PkgURL(viewID, PackagePath(obj.Pkg().Path()), obj.Name()) | |
| } | |
| } | |
| // method of package-level named type? | |
| if fn, ok := obj.(*types.Func); ok { | |
| sig := fn.Signature() | |
| if sig.Recv() != nil { | |
| _, named := typesinternal.RecvBase(fn) | |
| if named != nil { | |
| fragment := named.Obj().Name() + "." + fn.Name() | |
| return web.PkgURL(viewID, PackagePath(fn.Pkg().Path()), fragment) | |
| } | |
| } | |
| return "" | |
| } | |
| // TODO(adonovan): field of package-level named struct type. | |
| // (Requires an index, since there's no way to | |
| // get from Var to Named.) | |
| } | |
| return "" | |
| } | |
| // Splice spans into HTML-escaped segments of the | |
| // original source buffer (which is usually but not | |
| // necessarily formatted). | |
| // | |
| // (For expedience we don't use the more sophisticated | |
| // approach taken by cmd/godoc and pkgsite's render | |
| // package, which emit the text, spans, and comments | |
| // in one traversal of the syntax tree.) | |
| // | |
| // TODO(adonovan): splice styled spans around comments too. | |
| // | |
| // TODO(adonovan): pkgsite prints specs from grouped | |
| // type decls like "type ( T1; T2 )" to make them | |
| // appear as separate decls. We should too. | |
| var buf bytes.Buffer | |
| for _, file := range pkg.CompiledGoFiles() { | |
| if astutil.NodeContainsPos(file.File, n.Pos()) { | |
| pos := n.Pos() | |
| // emit emits source in the interval [pos:to] and updates pos. | |
| emit := func(to token.Pos) { | |
| // Ident and BasicLit always have a valid pos. | |
| // (Failure means the AST has been corrupted.) | |
| if !to.IsValid() { | |
| bug.Reportf("invalid Pos") | |
| } | |
| text, err := file.PosText(pos, to) | |
| if err != nil { | |
| bug.Reportf("invalid pos range: %v", err) | |
| return | |
| } | |
| buf.WriteString(escape(string(text))) | |
| pos = to | |
| } | |
| ast.Inspect(n, func(n ast.Node) bool { | |
| switch n := n.(type) { | |
| case *ast.Ident: | |
| emit(n.Pos()) | |
| pos = n.End() | |
| if url := linkify(n); url != "" { | |
| fmt.Fprintf(&buf, "<a class='id' href='%s'>%s</a>", escape(url), escape(n.Name)) | |
| } else { | |
| buf.WriteString(escape(n.Name)) // plain | |
| } | |
| case *ast.BasicLit: | |
| emit(n.Pos()) | |
| pos = n.End() | |
| fmt.Fprintf(&buf, "<span class='lit'>%s</span>", escape(n.Value)) | |
| } | |
| return true | |
| }) | |
| emit(n.End()) | |
| return buf.String() | |
| } | |
| } | |
| // Original source not found. | |
| // Format the node without adornments. | |
| if err := format.Node(&buf, pkg.FileSet(), n); err != nil { | |
| // e.g. BadDecl? | |
| buf.Reset() | |
| fmt.Fprintf(&buf, "formatting error: %v", err) | |
| } | |
| return escape(buf.String()) | |
| } | |
| // fnString is like fn.String() except that it: | |
| // - shows the receiver name; | |
| // - uses space "(T) M()" not dot "(T).M()" after receiver; | |
| // - doesn't bother with the special case for interface receivers | |
| // since it is unreachable for the methods in go/doc. | |
| // - elides parameters after the first three: f(a, b, c, ...). | |
| fnString := func(fn *types.Func) string { | |
| pkgRelative := typesinternal.NameRelativeTo(pkg.Types()) | |
| sig := fn.Signature() | |
| // Emit "func (recv T) F". | |
| var buf bytes.Buffer | |
| buf.WriteString("func ") | |
| if recv := sig.Recv(); recv != nil { | |
| buf.WriteByte('(') | |
| if recv.Name() != "" { | |
| buf.WriteString(recv.Name()) | |
| buf.WriteByte(' ') | |
| } | |
| types.WriteType(&buf, recv.Type(), pkgRelative) | |
| buf.WriteByte(')') | |
| buf.WriteByte(' ') // (ObjectString uses a '.' here) | |
| } else if pkg := fn.Pkg(); pkg != nil { | |
| if s := pkgRelative(pkg); s != "" { | |
| buf.WriteString(s) | |
| buf.WriteByte('.') | |
| } | |
| } | |
| buf.WriteString(fn.Name()) | |
| // Emit signature. | |
| // | |
| // Elide parameters after the third one. | |
| // WriteSignature is too complex to fork, so we replace | |
| // parameters 4+ with "invalid type", format, | |
| // then post-process the string. | |
| if sig.Params().Len() > 3 { | |
| // Clone each TypeParam as NewSignatureType modifies them (#67294). | |
| cloneTparams := func(seq *types.TypeParamList) []*types.TypeParam { | |
| slice := make([]*types.TypeParam, seq.Len()) | |
| for i := range slice { | |
| tparam := seq.At(i) | |
| slice[i] = types.NewTypeParam(tparam.Obj(), tparam.Constraint()) | |
| } | |
| return slice | |
| } | |
| sig = types.NewSignatureType( | |
| sig.Recv(), | |
| cloneTparams(sig.RecvTypeParams()), | |
| cloneTparams(sig.TypeParams()), | |
| types.NewTuple(append( | |
| slices.Collect(sig.Params().Variables())[:3], | |
| types.NewParam(0, nil, "", types.Typ[types.Invalid]))...), | |
| sig.Results(), | |
| false) // any final ...T parameter is truncated | |
| } | |
| types.WriteSignature(&buf, sig, pkgRelative) | |
| return strings.ReplaceAll(buf.String(), ", invalid type)", ", ...)") | |
| } | |
| fmt.Fprintf(&buf, "<main>\n") | |
| // package name | |
| fmt.Fprintf(&buf, "<h1 id='hdr-Overview'>Package %s</h1>\n", pkg.Types().Name()) | |
| // import path | |
| fmt.Fprintf(&buf, "<pre class='code'>import %q</pre>\n", pkg.Types().Path()) | |
| // link to same package in pkg.go.dev | |
| fmt.Fprintf(&buf, "<div><a href=%q title='View in pkg.go.dev'><img id='pkgsite' src='/assets/go-logo-blue.svg'/></a>\n", | |
| "https://pkg.go.dev/"+string(pkg.Types().Path())) | |
| // package doc | |
| for _, f := range pkg.Syntax() { | |
| if f.Doc != nil { | |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(f.Doc, docpkg.Doc)) | |
| break | |
| } | |
| } | |
| // symbol index | |
| fmt.Fprintf(&buf, "<h2 id='hdr-Index'>Index</h2>\n") | |
| fmt.Fprintf(&buf, "<ul>\n") | |
| if len(docpkg.Consts) > 0 { | |
| fmt.Fprintf(&buf, "<li><a href='#hdr-Constants'>Constants</a></li>\n") | |
| } | |
| if len(docpkg.Vars) > 0 { | |
| fmt.Fprintf(&buf, "<li><a href='#hdr-Variables'>Variables</a></li>\n") | |
| } | |
| for _, fn := range docpkg.Funcs { | |
| obj := scope.Lookup(fn.Name).(*types.Func) | |
| fmt.Fprintf(&buf, "<li><a href='#%s'>%s</a></li>\n", | |
| obj.Name(), escape(fnString(obj))) | |
| } | |
| for _, doctype := range docpkg.Types { | |
| tname := scope.Lookup(doctype.Name).(*types.TypeName) | |
| fmt.Fprintf(&buf, "<li><a href='#%[1]s'>type %[1]s</a></li>\n", | |
| tname.Name()) | |
| if len(doctype.Funcs)+len(doctype.Methods) > 0 { | |
| fmt.Fprintf(&buf, "<ul>\n") | |
| // constructors | |
| for _, docfn := range doctype.Funcs { | |
| obj := scope.Lookup(docfn.Name).(*types.Func) | |
| fmt.Fprintf(&buf, "<li><a href='#%s'>%s</a></li>\n", | |
| docfn.Name, escape(fnString(obj))) | |
| } | |
| // methods | |
| for _, docmethod := range doctype.Methods { | |
| method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) | |
| fmt.Fprintf(&buf, "<li><a href='#%s.%s'>%s</a></li>\n", | |
| doctype.Name, | |
| docmethod.Name, | |
| escape(fnString(method.(*types.Func)))) | |
| } | |
| fmt.Fprintf(&buf, "</ul>\n") | |
| } | |
| } | |
| // TODO(adonovan): add index of Examples here. | |
| fmt.Fprintf(&buf, "</ul>\n") | |
| // constants and variables | |
| values := func(vals []*doc.Value) { | |
| for _, v := range vals { | |
| // anchors | |
| for _, name := range v.Names { | |
| fmt.Fprintf(&buf, "<a id='%s'></a>\n", escape(name)) | |
| } | |
| // declaration | |
| decl2 := *v.Decl // shallow copy | |
| decl2.Doc = nil | |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2)) | |
| // comment (if any) | |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(v.Decl, v.Doc)) | |
| } | |
| } | |
| fmt.Fprintf(&buf, "<h2 id='hdr-Constants'>Constants</h2>\n") | |
| if len(docpkg.Consts) == 0 { | |
| fmt.Fprintf(&buf, "<div>(no constants)</div>\n") | |
| } else { | |
| values(docpkg.Consts) | |
| } | |
| fmt.Fprintf(&buf, "<h2 id='hdr-Variables'>Variables</h2>\n") | |
| if len(docpkg.Vars) == 0 { | |
| fmt.Fprintf(&buf, "<div>(no variables)</div>\n") | |
| } else { | |
| values(docpkg.Vars) | |
| } | |
| // addedInHTML returns an HTML division containing the Go release version at | |
| // which this obj became available. | |
| addedInHTML := func(obj types.Object) string { | |
| if sym := StdSymbolOf(obj); sym != nil && sym.Version != stdlib.Version(0) { | |
| return fmt.Sprintf("<span class='Documentation-sinceVersion'>added in %v</span>", sym.Version) | |
| } | |
| return "" | |
| } | |
| // package-level functions | |
| fmt.Fprintf(&buf, "<h2 id='hdr-Functions'>Functions</h2>\n") | |
| // funcs emits a list of package-level functions, | |
| // possibly organized beneath the type they construct. | |
| funcs := func(funcs []*doc.Func) { | |
| for _, docfn := range funcs { | |
| obj := scope.Lookup(docfn.Name).(*types.Func) | |
| fmt.Fprintf(&buf, "<h3 id='%s'>func %s %s</h3>\n", | |
| docfn.Name, objHTML(pkg.FileSet(), web, obj), addedInHTML(obj)) | |
| // decl: func F(params) results | |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", | |
| nodeHTML(docfn.Decl.Type)) | |
| // comment (if any) | |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(docfn.Decl, docfn.Doc)) | |
| } | |
| } | |
| funcs(docpkg.Funcs) | |
| // types and their subelements | |
| fmt.Fprintf(&buf, "<h2 id='hdr-Types'>Types</h2>\n") | |
| for _, doctype := range docpkg.Types { | |
| tname := scope.Lookup(doctype.Name).(*types.TypeName) | |
| // title and source link | |
| fmt.Fprintf(&buf, "<h3 id='%s'>type %s %s</h3>\n", | |
| doctype.Name, objHTML(pkg.FileSet(), web, tname), addedInHTML(tname)) | |
| // declaration | |
| // TODO(adonovan): excise non-exported struct fields somehow. | |
| decl2 := *doctype.Decl // shallow copy | |
| decl2.Doc = nil | |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", nodeHTML(&decl2)) | |
| // comment (if any) | |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", docHTML(doctype.Decl, doctype.Doc)) | |
| // subelements | |
| values(doctype.Consts) // constants of type T | |
| values(doctype.Vars) // vars of type T | |
| funcs(doctype.Funcs) // constructors of T | |
| // methods on T | |
| for _, docmethod := range doctype.Methods { | |
| method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) | |
| fmt.Fprintf(&buf, "<h4 id='%s.%s'>func (%s) %s %s</h4>\n", | |
| doctype.Name, docmethod.Name, | |
| docmethod.Orig, // T or *T | |
| objHTML(pkg.FileSet(), web, method), addedInHTML(method)) | |
| // decl: func (x T) M(params) results | |
| fmt.Fprintf(&buf, "<pre class='code'>%s</pre>\n", | |
| nodeHTML(docmethod.Decl.Type)) | |
| // comment (if any) | |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", | |
| docHTML(docmethod.Decl, docmethod.Doc)) | |
| } | |
| } | |
| // source files | |
| fmt.Fprintf(&buf, "<h2 id='hdr-SourceFiles'>Source files</h2>\n") | |
| for _, filename := range docpkg.Filenames { | |
| fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", | |
| sourceLinkHTML( | |
| template.HTML(template.HTMLEscapeString(filepath.Base(filename))), | |
| web.SrcURL(filename, 1, 1))) | |
| } | |
| fmt.Fprintf(&buf, "</main>\n") | |
| fmt.Fprintf(&buf, "</body>\n") | |
| fmt.Fprintf(&buf, "</html>\n") | |
| return buf.Bytes(), nil | |
| } |