| // Copyright 2020 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 analysisinternal provides gopls' internal analyses with a |
| // number of helper functions that operate on typed syntax trees. |
| package analysisinternal |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/scanner" |
| "go/token" |
| "go/types" |
| "os" |
| pathpkg "path" |
| |
| "golang.org/x/tools/go/analysis" |
| ) |
| |
| func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos { |
| // Get the end position for the type error. |
| file := fset.File(start) |
| if file == nil { |
| return start |
| } |
| if offset := file.PositionFor(start, false).Offset; offset > len(src) { |
| return start |
| } else { |
| src = src[offset:] |
| } |
| |
| // Attempt to find a reasonable end position for the type error. |
| // |
| // TODO(rfindley): the heuristic implemented here is unclear. It looks like |
| // it seeks the end of the primary operand starting at start, but that is not |
| // quite implemented (for example, given a func literal this heuristic will |
| // return the range of the func keyword). |
| // |
| // We should formalize this heuristic, or deprecate it by finally proposing |
| // to add end position to all type checker errors. |
| // |
| // Nevertheless, ensure that the end position at least spans the current |
| // token at the cursor (this was golang/go#69505). |
| end := start |
| { |
| var s scanner.Scanner |
| fset := token.NewFileSet() |
| f := fset.AddFile("", fset.Base(), len(src)) |
| s.Init(f, src, nil /* no error handler */, scanner.ScanComments) |
| pos, tok, lit := s.Scan() |
| if tok != token.SEMICOLON && token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) { |
| off := file.Offset(pos) + len(lit) |
| src = src[off:] |
| end += token.Pos(off) |
| } |
| } |
| |
| // Look for bytes that might terminate the current operand. See note above: |
| // this is imprecise. |
| if width := bytes.IndexAny(src, " \n,():;[]+-*/"); width > 0 { |
| end += token.Pos(width) |
| } |
| return end |
| } |
| |
| // StmtToInsertVarBefore returns the ast.Stmt before which we can |
| // safely insert a new var declaration, or nil if the path denotes a |
| // node outside any statement. |
| // |
| // Basic Example: |
| // |
| // z := 1 |
| // y := z + x |
| // |
| // If x is undeclared, then this function would return `y := z + x`, so that we |
| // can insert `x := ` on the line before `y := z + x`. |
| // |
| // If stmt example: |
| // |
| // if z == 1 { |
| // } else if z == y {} |
| // |
| // If y is undeclared, then this function would return `if z == 1 {`, because we cannot |
| // insert a statement between an if and an else if statement. As a result, we need to find |
| // the top of the if chain to insert `y := ` before. |
| func StmtToInsertVarBefore(path []ast.Node) ast.Stmt { |
| enclosingIndex := -1 |
| for i, p := range path { |
| if _, ok := p.(ast.Stmt); ok { |
| enclosingIndex = i |
| break |
| } |
| } |
| if enclosingIndex == -1 { |
| return nil // no enclosing statement: outside function |
| } |
| enclosingStmt := path[enclosingIndex] |
| switch enclosingStmt.(type) { |
| case *ast.IfStmt: |
| // The enclosingStmt is inside of the if declaration, |
| // We need to check if we are in an else-if stmt and |
| // get the base if statement. |
| // TODO(adonovan): for non-constants, it may be preferable |
| // to add the decl as the Init field of the innermost |
| // enclosing ast.IfStmt. |
| return baseIfStmt(path, enclosingIndex) |
| case *ast.CaseClause: |
| // Get the enclosing switch stmt if the enclosingStmt is |
| // inside of the case statement. |
| for i := enclosingIndex + 1; i < len(path); i++ { |
| if node, ok := path[i].(*ast.SwitchStmt); ok { |
| return node |
| } else if node, ok := path[i].(*ast.TypeSwitchStmt); ok { |
| return node |
| } |
| } |
| } |
| if len(path) <= enclosingIndex+1 { |
| return enclosingStmt.(ast.Stmt) |
| } |
| // Check if the enclosing statement is inside another node. |
| switch expr := path[enclosingIndex+1].(type) { |
| case *ast.IfStmt: |
| // Get the base if statement. |
| return baseIfStmt(path, enclosingIndex+1) |
| case *ast.ForStmt: |
| if expr.Init == enclosingStmt || expr.Post == enclosingStmt { |
| return expr |
| } |
| case *ast.SwitchStmt, *ast.TypeSwitchStmt: |
| return expr.(ast.Stmt) |
| } |
| return enclosingStmt.(ast.Stmt) |
| } |
| |
| // baseIfStmt walks up the if/else-if chain until we get to |
| // the top of the current if chain. |
| func baseIfStmt(path []ast.Node, index int) ast.Stmt { |
| stmt := path[index] |
| for i := index + 1; i < len(path); i++ { |
| if node, ok := path[i].(*ast.IfStmt); ok && node.Else == stmt { |
| stmt = node |
| continue |
| } |
| break |
| } |
| return stmt.(ast.Stmt) |
| } |
| |
| // WalkASTWithParent walks the AST rooted at n. The semantics are |
| // similar to ast.Inspect except it does not call f(nil). |
| func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) { |
| var ancestors []ast.Node |
| ast.Inspect(n, func(n ast.Node) (recurse bool) { |
| if n == nil { |
| ancestors = ancestors[:len(ancestors)-1] |
| return false |
| } |
| |
| var parent ast.Node |
| if len(ancestors) > 0 { |
| parent = ancestors[len(ancestors)-1] |
| } |
| ancestors = append(ancestors, n) |
| return f(n, parent) |
| }) |
| } |
| |
| // MatchingIdents finds the names of all identifiers in 'node' that match any of the given types. |
| // 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within |
| // the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that |
| // is unrecognized. |
| func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string { |
| |
| // Initialize matches to contain the variable types we are searching for. |
| matches := make(map[types.Type][]string) |
| for _, typ := range typs { |
| if typ == nil { |
| continue // TODO(adonovan): is this reachable? |
| } |
| matches[typ] = nil // create entry |
| } |
| |
| seen := map[types.Object]struct{}{} |
| ast.Inspect(node, func(n ast.Node) bool { |
| if n == nil { |
| return false |
| } |
| // Prevent circular definitions. If 'pos' is within an assignment statement, do not |
| // allow any identifiers in that assignment statement to be selected. Otherwise, |
| // we could do the following, where 'x' satisfies the type of 'f0': |
| // |
| // x := fakeStruct{f0: x} |
| // |
| if assign, ok := n.(*ast.AssignStmt); ok && pos > assign.Pos() && pos <= assign.End() { |
| return false |
| } |
| if n.End() > pos { |
| return n.Pos() <= pos |
| } |
| ident, ok := n.(*ast.Ident) |
| if !ok || ident.Name == "_" { |
| return true |
| } |
| obj := info.Defs[ident] |
| if obj == nil || obj.Type() == nil { |
| return true |
| } |
| if _, ok := obj.(*types.TypeName); ok { |
| return true |
| } |
| // Prevent duplicates in matches' values. |
| if _, ok = seen[obj]; ok { |
| return true |
| } |
| seen[obj] = struct{}{} |
| // Find the scope for the given position. Then, check whether the object |
| // exists within the scope. |
| innerScope := pkg.Scope().Innermost(pos) |
| if innerScope == nil { |
| return true |
| } |
| _, foundObj := innerScope.LookupParent(ident.Name, pos) |
| if foundObj != obj { |
| return true |
| } |
| // The object must match one of the types that we are searching for. |
| // TODO(adonovan): opt: use typeutil.Map? |
| if names, ok := matches[obj.Type()]; ok { |
| matches[obj.Type()] = append(names, ident.Name) |
| } else { |
| // If the object type does not exactly match |
| // any of the target types, greedily find the first |
| // target type that the object type can satisfy. |
| for typ := range matches { |
| if equivalentTypes(obj.Type(), typ) { |
| matches[typ] = append(matches[typ], ident.Name) |
| } |
| } |
| } |
| return true |
| }) |
| return matches |
| } |
| |
| func equivalentTypes(want, got types.Type) bool { |
| if types.Identical(want, got) { |
| return true |
| } |
| // Code segment to help check for untyped equality from (golang/go#32146). |
| if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 { |
| if lhs, ok := got.Underlying().(*types.Basic); ok { |
| return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType |
| } |
| } |
| return types.AssignableTo(want, got) |
| } |
| |
| // MakeReadFile returns a simple implementation of the Pass.ReadFile function. |
| func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) { |
| return func(filename string) ([]byte, error) { |
| if err := CheckReadable(pass, filename); err != nil { |
| return nil, err |
| } |
| return os.ReadFile(filename) |
| } |
| } |
| |
| // CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass]. |
| func CheckReadable(pass *analysis.Pass, filename string) error { |
| if slicesContains(pass.OtherFiles, filename) || |
| slicesContains(pass.IgnoredFiles, filename) { |
| return nil |
| } |
| for _, f := range pass.Files { |
| if pass.Fset.File(f.FileStart).Name() == filename { |
| return nil |
| } |
| } |
| return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename) |
| } |
| |
| // TODO(adonovan): use go1.21 slices.Contains. |
| func slicesContains[S ~[]E, E comparable](slice S, x E) bool { |
| for _, elem := range slice { |
| if elem == x { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // AddImport checks whether this file already imports pkgpath and |
| // that import is in scope at pos. If so, it returns the name under |
| // which it was imported and a zero edit. Otherwise, it adds a new |
| // import of pkgpath, using a name derived from the preferred name, |
| // and returns the chosen name along with the edit for the new import. |
| // |
| // It does not mutate its arguments. |
| func AddImport(info *types.Info, file *ast.File, pos token.Pos, pkgpath, preferredName string) (name string, newImport []analysis.TextEdit) { |
| // Find innermost enclosing lexical block. |
| scope := info.Scopes[file].Innermost(pos) |
| if scope == nil { |
| panic("no enclosing lexical block") |
| } |
| |
| // Is there an existing import of this package? |
| // If so, are we in its scope? (not shadowed) |
| for _, spec := range file.Imports { |
| pkgname, ok := importedPkgName(info, spec) |
| if ok && pkgname.Imported().Path() == pkgpath { |
| if _, obj := scope.LookupParent(pkgname.Name(), pos); obj == pkgname { |
| return pkgname.Name(), nil |
| } |
| } |
| } |
| |
| // We must add a new import. |
| // Ensure we have a fresh name. |
| newName := preferredName |
| for i := 0; ; i++ { |
| if _, obj := scope.LookupParent(newName, pos); obj == nil { |
| break // fresh |
| } |
| newName = fmt.Sprintf("%s%d", preferredName, i) |
| } |
| |
| // For now, keep it real simple: create a new import |
| // declaration before the first existing declaration (which |
| // must exist), including its comments, and let goimports tidy it up. |
| // |
| // Use a renaming import whenever the preferred name is not |
| // available, or the chosen name does not match the last |
| // segment of its path. |
| newText := fmt.Sprintf("import %q\n\n", pkgpath) |
| if newName != preferredName || newName != pathpkg.Base(pkgpath) { |
| newText = fmt.Sprintf("import %s %q\n\n", newName, pkgpath) |
| } |
| decl0 := file.Decls[0] |
| var before ast.Node = decl0 |
| switch decl0 := decl0.(type) { |
| case *ast.GenDecl: |
| if decl0.Doc != nil { |
| before = decl0.Doc |
| } |
| case *ast.FuncDecl: |
| if decl0.Doc != nil { |
| before = decl0.Doc |
| } |
| } |
| return newName, []analysis.TextEdit{{ |
| Pos: before.Pos(), |
| End: before.Pos(), |
| NewText: []byte(newText), |
| }} |
| } |
| |
| // importedPkgName returns the PkgName object declared by an ImportSpec. |
| // TODO(adonovan): use go1.22's Info.PkgNameOf. |
| func importedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) { |
| var obj types.Object |
| if imp.Name != nil { |
| obj = info.Defs[imp.Name] |
| } else { |
| obj = info.Implicits[imp] |
| } |
| pkgname, ok := obj.(*types.PkgName) |
| return pkgname, ok |
| } |