| // 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 implements the "Browse free symbols" code action. |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/token" |
| "go/types" |
| "html" |
| "slices" |
| "sort" |
| "strings" |
| |
| "golang.org/x/tools/go/ast/astutil" |
| "golang.org/x/tools/gopls/internal/cache" |
| "golang.org/x/tools/gopls/internal/cache/metadata" |
| "golang.org/x/tools/gopls/internal/cache/parsego" |
| "golang.org/x/tools/gopls/internal/util/moremaps" |
| "golang.org/x/tools/gopls/internal/util/safetoken" |
| "golang.org/x/tools/internal/typesinternal" |
| ) |
| |
| // FreeSymbolsHTML returns an HTML document containing the report of |
| // free symbols referenced by the selection. |
| func FreeSymbolsHTML(viewID string, pkg *cache.Package, pgf *parsego.File, start, end token.Pos, web Web) []byte { |
| |
| // Compute free references. |
| refs := freeRefs(pkg.Types(), pkg.TypesInfo(), pgf.File, start, end) |
| |
| // -- model -- |
| |
| type Import struct { |
| Path metadata.PackagePath |
| Symbols []string |
| } |
| type Symbol struct { |
| Kind string |
| Type string |
| Refs []types.Object |
| } |
| var model struct { |
| Imported []Import |
| PkgLevel []Symbol |
| Local []Symbol |
| } |
| |
| qualifier := typesinternal.NameRelativeTo(pkg.Types()) |
| |
| // Populate model. |
| { |
| // List the refs in order of dotted paths. |
| sort.Slice(refs, func(i, j int) bool { |
| return refs[i].dotted < refs[j].dotted |
| }) |
| |
| // Inspect the references. |
| imported := make(map[string][]*freeRef) // refs to imported symbols, by package path |
| seen := make(map[string]bool) // to de-dup dotted paths |
| for _, ref := range refs { |
| if seen[ref.dotted] { |
| continue // de-dup |
| } |
| seen[ref.dotted] = true |
| |
| var symbols *[]Symbol |
| switch ref.scope { |
| case "file": |
| // imported symbol: group by package |
| if pkgname, ok := ref.objects[0].(*types.PkgName); ok { |
| path := pkgname.Imported().Path() |
| imported[path] = append(imported[path], ref) |
| } |
| continue |
| case "pkg": |
| symbols = &model.PkgLevel |
| case "local": |
| symbols = &model.Local |
| default: |
| panic(ref.scope) |
| } |
| |
| // Package and local symbols are presented the same way. |
| // We treat each dotted path x.y.z as a separate entity. |
| |
| // Compute kind and type of last object (y in obj.x.y). |
| typestr := " " + types.TypeString(ref.typ, qualifier) |
| var kind string |
| switch obj := ref.objects[len(ref.objects)-1].(type) { |
| case *types.Var: |
| kind = "var" |
| case *types.Func: |
| kind = "func" |
| case *types.TypeName: |
| if is[*types.TypeParam](obj.Type()) { |
| kind = "type parameter" |
| } else { |
| kind = "type" |
| } |
| typestr = "" // avoid "type T T" |
| case *types.Const: |
| kind = "const" |
| case *types.Label: |
| kind = "label" |
| typestr = "" // avoid "label L L" |
| } |
| |
| *symbols = append(*symbols, Symbol{ |
| Kind: kind, |
| Type: typestr, |
| Refs: ref.objects, |
| }) |
| } |
| |
| // Imported symbols. |
| // Produce one record per package, with a list of symbols. |
| for pkgPath, refs := range moremaps.Sorted(imported) { |
| var syms []string |
| for _, ref := range refs { |
| // strip package name (bytes.Buffer.Len -> Buffer.Len) |
| syms = append(syms, ref.dotted[len(ref.objects[0].Name())+len("."):]) |
| } |
| sort.Strings(syms) |
| const max = 4 |
| if len(syms) > max { |
| syms[max-1] = fmt.Sprintf("... (%d)", len(syms)) |
| syms = syms[:max] |
| } |
| |
| model.Imported = append(model.Imported, Import{ |
| Path: PackagePath(pkgPath), |
| Symbols: syms, |
| }) |
| } |
| } |
| |
| // -- presentation -- |
| |
| var buf bytes.Buffer |
| buf.WriteString(`<!DOCTYPE html> |
| <html> |
| <head> |
| <style> |
| .col-pkg { color: #2eb007 } |
| .col-file { color: #a10b15 } |
| .col-local { color: #0cb7c9 } |
| li { font-family: monospace; } |
| p { max-width: 6in; } |
| </style> |
| <script src="/assets/common.js"></script> |
| <link rel="stylesheet" href="/assets/common.css"> |
| </head> |
| <body> |
| <h1>Free symbols</h1> |
| <p> |
| The selected code contains references to these free* symbols: |
| </p> |
| `) |
| |
| // Present the refs in three sections: imported, same package, local. |
| |
| // -- imported symbols -- |
| |
| // Show one item per package, with a list of symbols. |
| fmt.Fprintf(&buf, "<h2><span class='col-file'>⬤</span> Imported symbols</h2>\n") |
| fmt.Fprintf(&buf, "<ul>\n") |
| for _, imp := range model.Imported { |
| fmt.Fprintf(&buf, "<li>import \"<a href='%s'>%s</a>\" // for %s</li>\n", |
| web.PkgURL(viewID, imp.Path, ""), |
| html.EscapeString(string(imp.Path)), |
| strings.Join(imp.Symbols, ", ")) |
| } |
| if len(model.Imported) == 0 { |
| fmt.Fprintf(&buf, "<li>(none)</li>\n") |
| } |
| buf.WriteString("</ul>\n") |
| |
| // -- package and local symbols -- |
| |
| showSymbols := func(scope, title string, symbols []Symbol) { |
| fmt.Fprintf(&buf, "<h2><span class='col-%s'>⬤</span> %s</h2>\n", scope, title) |
| fmt.Fprintf(&buf, "<ul>\n") |
| pre := buf.Len() |
| for _, sym := range symbols { |
| fmt.Fprintf(&buf, "<li>%s ", sym.Kind) // of rightmost symbol in dotted path |
| for i, obj := range sym.Refs { |
| if i > 0 { |
| buf.WriteByte('.') |
| } |
| buf.WriteString(objHTML(pkg.FileSet(), web, obj)) |
| } |
| fmt.Fprintf(&buf, " %s</li>\n", html.EscapeString(sym.Type)) |
| } |
| if buf.Len() == pre { |
| fmt.Fprintf(&buf, "<li>(none)</li>\n") |
| } |
| buf.WriteString("</ul>\n") |
| } |
| showSymbols("pkg", "Package-level symbols", model.PkgLevel) |
| showSymbols("local", "Local symbols", model.Local) |
| |
| // -- code selection -- |
| |
| // Print the selection, highlighting references to free symbols. |
| buf.WriteString("<hr/>\n") |
| sort.Slice(refs, func(i, j int) bool { |
| return refs[i].expr.Pos() < refs[j].expr.Pos() |
| }) |
| pos := start |
| emitTo := func(end token.Pos) { |
| if pos < end { |
| fileStart := pgf.File.FileStart |
| text := pgf.Mapper.Content[pos-fileStart : end-fileStart] |
| buf.WriteString(html.EscapeString(string(text))) |
| pos = end |
| } |
| } |
| buf.WriteString(`<pre>`) |
| for _, ref := range refs { |
| emitTo(ref.expr.Pos()) |
| fmt.Fprintf(&buf, `<b class='col-%s'>`, ref.scope) |
| emitTo(ref.expr.End()) |
| buf.WriteString(`</b>`) |
| } |
| emitTo(end) |
| buf.WriteString(`</pre> |
| <hr> |
| <p> |
| *A symbol is "free" if it is referenced within the selection but declared |
| outside of it. |
| |
| The free variables are approximately the set of parameters that |
| would be needed if the block were extracted into its own function in |
| the same package. |
| |
| Free identifiers may include local types and control labels as well. |
| |
| Even when you don't intend to extract a block into a new function, |
| this information can help you to tell at a glance what names a block |
| of code depends on. |
| </p> |
| <p> |
| Each dotted path of identifiers (such as file.Name.Pos) is reported |
| as a separate item, so that you can see which parts of a complex |
| type are actually needed. |
| |
| The free symbols referenced by the body of a function may |
| reveal that only a small part (a single field of a struct, say) of |
| one of the function's parameters is used, allowing you to simplify |
| and generalize the function by choosing a different type for that |
| parameter. |
| </p> |
| `) |
| return buf.Bytes() |
| } |
| |
| // A freeRef records a reference to a dotted path obj.x.y, |
| // where obj (=objects[0]) is a free symbol. |
| type freeRef struct { |
| objects []types.Object // [obj x y] |
| dotted string // "obj.x.y" (used as sort key) |
| scope string // scope of obj: pkg|file|local |
| expr ast.Expr // =*Ident|*SelectorExpr |
| typ types.Type // type of obj.x.y |
| } |
| |
| // freeRefs returns the list of references to free symbols (from |
| // within the selection to a symbol declared outside of it). |
| // It uses only info.{Scopes,Types,Uses}. |
| func freeRefs(pkg *types.Package, info *types.Info, file *ast.File, start, end token.Pos) []*freeRef { |
| // Keep us honest about which fields we access. |
| info = &types.Info{ |
| Scopes: info.Scopes, |
| Types: info.Types, |
| Uses: info.Uses, |
| } |
| |
| fileScope := info.Scopes[file] |
| pkgScope := fileScope.Parent() |
| |
| // id is called for the leftmost id x in each dotted chain such as (x.y).z. |
| // suffix is the reversed suffix of selections (e.g. [z y]). |
| id := func(n *ast.Ident, suffix []types.Object) *freeRef { |
| obj := info.Uses[n] |
| if obj == nil { |
| return nil // not a reference |
| } |
| if start <= obj.Pos() && obj.Pos() < end { |
| return nil // defined within selection => not free |
| } |
| parent := obj.Parent() |
| |
| // Compute dotted path. |
| objects := append(suffix, obj) |
| if obj.Pkg() != nil && obj.Pkg() != pkg && isPackageLevel(obj) { // dot import |
| // Synthesize the implicit PkgName. |
| pkgName := types.NewPkgName(token.NoPos, pkg, obj.Pkg().Name(), obj.Pkg()) |
| parent = fileScope |
| objects = append(objects, pkgName) |
| } |
| slices.Reverse(objects) |
| var dotted strings.Builder |
| for i, obj := range objects { |
| if obj == nil { |
| return nil // type error |
| } |
| if i > 0 { |
| dotted.WriteByte('.') |
| } |
| dotted.WriteString(obj.Name()) |
| } |
| |
| // Compute scope of base object. |
| var scope string |
| switch parent { |
| case nil: |
| return nil // interface method or struct field |
| case types.Universe: |
| return nil // built-in (not interesting) |
| case fileScope: |
| scope = "file" // defined at file scope (imported package) |
| case pkgScope: |
| scope = "pkg" // defined at package level |
| default: |
| scope = "local" // defined within current function |
| } |
| |
| return &freeRef{ |
| objects: objects, |
| dotted: dotted.String(), |
| scope: scope, |
| } |
| } |
| |
| // sel(x.y.z, []) calls sel(x.y, [z]) calls id(x, [z, y]). |
| sel := func(sel *ast.SelectorExpr, suffix []types.Object) *freeRef { |
| for { |
| suffix = append(suffix, info.Uses[sel.Sel]) |
| |
| switch x := astutil.Unparen(sel.X).(type) { |
| case *ast.Ident: |
| return id(x, suffix) |
| default: |
| return nil |
| case *ast.SelectorExpr: |
| sel = x |
| } |
| } |
| } |
| |
| // Visit all the identifiers in the selected ASTs. |
| var free []*freeRef |
| path, _ := astutil.PathEnclosingInterval(file, start, end) |
| var visit func(n ast.Node) bool |
| visit = func(n ast.Node) bool { |
| // Is this node contained within the selection? |
| // (freesymbols permits inexact selections, |
| // like two stmts in a block.) |
| if n != nil && start <= n.Pos() && n.End() <= end { |
| var ref *freeRef |
| switch n := n.(type) { |
| case *ast.Ident: |
| ref = id(n, nil) |
| case *ast.SelectorExpr: |
| ref = sel(n, nil) |
| } |
| |
| if ref != nil { |
| ref.expr = n.(ast.Expr) |
| ref.typ = info.Types[n.(ast.Expr)].Type |
| free = append(free, ref) |
| } |
| |
| // After visiting x.sel, don't descend into sel. |
| // Descend into x only if we didn't get a ref for x.sel. |
| if sel, ok := n.(*ast.SelectorExpr); ok { |
| if ref == nil { |
| ast.Inspect(sel.X, visit) |
| } |
| return false |
| } |
| } |
| |
| return true // descend |
| } |
| ast.Inspect(path[0], visit) |
| return free |
| } |
| |
| // objHTML returns HTML for obj.Name(), possibly marked up as a link |
| // to the web server that, when visited, opens the declaration in the |
| // client editor. |
| func objHTML(fset *token.FileSet, web Web, obj types.Object) string { |
| text := obj.Name() |
| if posn := safetoken.StartPosition(fset, obj.Pos()); posn.IsValid() { |
| url := web.SrcURL(posn.Filename, posn.Line, posn.Column) |
| return sourceLink(text, url) |
| } |
| return text |
| } |
| |
| // sourceLink returns HTML for a link to open a file in the client editor. |
| func sourceLink(text, url string) string { |
| // The /src URL returns nothing but has the side effect |
| // of causing the LSP client to open the requested file. |
| // So we use onclick to prevent the browser from navigating. |
| // We keep the href attribute as it causes the <a> to render |
| // as a link: blue, underlined, with URL hover information. |
| return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`, |
| html.EscapeString(url), text) |
| } |