golang.org/x/tools/gopls@v0.15.3/internal/analysis/undeclaredname/undeclared.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package undeclaredname 6 7 import ( 8 "bytes" 9 _ "embed" 10 "fmt" 11 "go/ast" 12 "go/format" 13 "go/token" 14 "go/types" 15 "strings" 16 "unicode" 17 18 "golang.org/x/tools/go/analysis" 19 "golang.org/x/tools/go/ast/astutil" 20 "golang.org/x/tools/gopls/internal/util/safetoken" 21 "golang.org/x/tools/internal/analysisinternal" 22 ) 23 24 //go:embed doc.go 25 var doc string 26 27 var Analyzer = &analysis.Analyzer{ 28 Name: "undeclaredname", 29 Doc: analysisinternal.MustExtractDoc(doc, "undeclaredname"), 30 Requires: []*analysis.Analyzer{}, 31 Run: run, 32 RunDespiteErrors: true, 33 URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname", 34 } 35 36 // The prefix for this error message changed in Go 1.20. 37 var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} 38 39 func run(pass *analysis.Pass) (interface{}, error) { 40 for _, err := range pass.TypeErrors { 41 runForError(pass, err) 42 } 43 return nil, nil 44 } 45 46 func runForError(pass *analysis.Pass, err types.Error) { 47 // Extract symbol name from error. 48 var name string 49 for _, prefix := range undeclaredNamePrefixes { 50 if !strings.HasPrefix(err.Msg, prefix) { 51 continue 52 } 53 name = strings.TrimPrefix(err.Msg, prefix) 54 } 55 if name == "" { 56 return 57 } 58 59 // Find file enclosing error. 60 var file *ast.File 61 for _, f := range pass.Files { 62 if f.Pos() <= err.Pos && err.Pos < f.End() { 63 file = f 64 break 65 } 66 } 67 if file == nil { 68 return 69 } 70 71 // Find path to identifier in the error. 72 path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) 73 if len(path) < 2 { 74 return 75 } 76 ident, ok := path[0].(*ast.Ident) 77 if !ok || ident.Name != name { 78 return 79 } 80 81 // Skip selector expressions because it might be too complex 82 // to try and provide a suggested fix for fields and methods. 83 if _, ok := path[1].(*ast.SelectorExpr); ok { 84 return 85 } 86 87 // Undeclared quick fixes only work in function bodies. 88 inFunc := false 89 for i := range path { 90 if _, inFunc = path[i].(*ast.FuncDecl); inFunc { 91 if i == 0 { 92 return 93 } 94 if _, isBody := path[i-1].(*ast.BlockStmt); !isBody { 95 return 96 } 97 break 98 } 99 } 100 if !inFunc { 101 return 102 } 103 104 // Offer a fix. 105 noun := "variable" 106 if isCallPosition(path) { 107 noun = "function" 108 } 109 pass.Report(analysis.Diagnostic{ 110 Pos: err.Pos, 111 End: err.Pos + token.Pos(len(name)), 112 Message: err.Msg, 113 Category: FixCategory, 114 SuggestedFixes: []analysis.SuggestedFix{{ 115 Message: fmt.Sprintf("Create %s %q", noun, name), 116 // No TextEdits => computed by a gopls command 117 }}, 118 }) 119 } 120 121 const FixCategory = "undeclaredname" // recognized by gopls ApplyFix 122 123 // SuggestedFix computes the edits for the lazy (no-edits) fix suggested by the analyzer. 124 func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { 125 pos := start // don't use the end 126 path, _ := astutil.PathEnclosingInterval(file, pos, pos) 127 if len(path) < 2 { 128 return nil, nil, fmt.Errorf("no expression found") 129 } 130 ident, ok := path[0].(*ast.Ident) 131 if !ok { 132 return nil, nil, fmt.Errorf("no identifier found") 133 } 134 135 // Check for a possible call expression, in which case we should add a 136 // new function declaration. 137 if isCallPosition(path) { 138 return newFunctionDeclaration(path, file, pkg, info, fset) 139 } 140 141 // Get the place to insert the new statement. 142 insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) 143 if insertBeforeStmt == nil { 144 return nil, nil, fmt.Errorf("could not locate insertion point") 145 } 146 147 insertBefore := safetoken.StartPosition(fset, insertBeforeStmt.Pos()).Offset 148 149 // Get the indent to add on the line after the new statement. 150 // Since this will have a parse error, we can not use format.Source(). 151 contentBeforeStmt, indent := content[:insertBefore], "\n" 152 if nl := bytes.LastIndex(contentBeforeStmt, []byte("\n")); nl != -1 { 153 indent = string(contentBeforeStmt[nl:]) 154 } 155 156 // Create the new local variable statement. 157 newStmt := fmt.Sprintf("%s := %s", ident.Name, indent) 158 return fset, &analysis.SuggestedFix{ 159 Message: fmt.Sprintf("Create variable %q", ident.Name), 160 TextEdits: []analysis.TextEdit{{ 161 Pos: insertBeforeStmt.Pos(), 162 End: insertBeforeStmt.Pos(), 163 NewText: []byte(newStmt), 164 }}, 165 }, nil 166 } 167 168 func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, info *types.Info, fset *token.FileSet) (*token.FileSet, *analysis.SuggestedFix, error) { 169 if len(path) < 3 { 170 return nil, nil, fmt.Errorf("unexpected set of enclosing nodes: %v", path) 171 } 172 ident, ok := path[0].(*ast.Ident) 173 if !ok { 174 return nil, nil, fmt.Errorf("no name for function declaration %v (%T)", path[0], path[0]) 175 } 176 call, ok := path[1].(*ast.CallExpr) 177 if !ok { 178 return nil, nil, fmt.Errorf("no call expression found %v (%T)", path[1], path[1]) 179 } 180 181 // Find the enclosing function, so that we can add the new declaration 182 // below. 183 var enclosing *ast.FuncDecl 184 for _, n := range path { 185 if n, ok := n.(*ast.FuncDecl); ok { 186 enclosing = n 187 break 188 } 189 } 190 // TODO(rstambler): Support the situation when there is no enclosing 191 // function. 192 if enclosing == nil { 193 return nil, nil, fmt.Errorf("no enclosing function found: %v", path) 194 } 195 196 pos := enclosing.End() 197 198 var paramNames []string 199 var paramTypes []types.Type 200 // keep track of all param names to later ensure uniqueness 201 nameCounts := map[string]int{} 202 for _, arg := range call.Args { 203 typ := info.TypeOf(arg) 204 if typ == nil { 205 return nil, nil, fmt.Errorf("unable to determine type for %s", arg) 206 } 207 208 switch t := typ.(type) { 209 // this is the case where another function call returning multiple 210 // results is used as an argument 211 case *types.Tuple: 212 n := t.Len() 213 for i := 0; i < n; i++ { 214 name := typeToArgName(t.At(i).Type()) 215 nameCounts[name]++ 216 217 paramNames = append(paramNames, name) 218 paramTypes = append(paramTypes, types.Default(t.At(i).Type())) 219 } 220 221 default: 222 // does the argument have a name we can reuse? 223 // only happens in case of a *ast.Ident 224 var name string 225 if ident, ok := arg.(*ast.Ident); ok { 226 name = ident.Name 227 } 228 229 if name == "" { 230 name = typeToArgName(typ) 231 } 232 233 nameCounts[name]++ 234 235 paramNames = append(paramNames, name) 236 paramTypes = append(paramTypes, types.Default(typ)) 237 } 238 } 239 240 for n, c := range nameCounts { 241 // Any names we saw more than once will need a unique suffix added 242 // on. Reset the count to 1 to act as the suffix for the first 243 // occurrence of that name. 244 if c >= 2 { 245 nameCounts[n] = 1 246 } else { 247 delete(nameCounts, n) 248 } 249 } 250 251 params := &ast.FieldList{} 252 253 for i, name := range paramNames { 254 if suffix, repeats := nameCounts[name]; repeats { 255 nameCounts[name]++ 256 name = fmt.Sprintf("%s%d", name, suffix) 257 } 258 259 // only worth checking after previous param in the list 260 if i > 0 { 261 // if type of parameter at hand is the same as the previous one, 262 // add it to the previous param list of identifiers so to have: 263 // (s1, s2 string) 264 // and not 265 // (s1 string, s2 string) 266 if paramTypes[i] == paramTypes[i-1] { 267 params.List[len(params.List)-1].Names = append(params.List[len(params.List)-1].Names, ast.NewIdent(name)) 268 continue 269 } 270 } 271 272 params.List = append(params.List, &ast.Field{ 273 Names: []*ast.Ident{ 274 ast.NewIdent(name), 275 }, 276 Type: analysisinternal.TypeExpr(file, pkg, paramTypes[i]), 277 }) 278 } 279 280 decl := &ast.FuncDecl{ 281 Name: ast.NewIdent(ident.Name), 282 Type: &ast.FuncType{ 283 Params: params, 284 // TODO(golang/go#47558): Also handle result 285 // parameters here based on context of CallExpr. 286 }, 287 Body: &ast.BlockStmt{ 288 List: []ast.Stmt{ 289 &ast.ExprStmt{ 290 X: &ast.CallExpr{ 291 Fun: ast.NewIdent("panic"), 292 Args: []ast.Expr{ 293 &ast.BasicLit{ 294 Value: `"unimplemented"`, 295 }, 296 }, 297 }, 298 }, 299 }, 300 }, 301 } 302 303 b := bytes.NewBufferString("\n\n") 304 if err := format.Node(b, fset, decl); err != nil { 305 return nil, nil, err 306 } 307 return fset, &analysis.SuggestedFix{ 308 Message: fmt.Sprintf("Create function %q", ident.Name), 309 TextEdits: []analysis.TextEdit{{ 310 Pos: pos, 311 End: pos, 312 NewText: b.Bytes(), 313 }}, 314 }, nil 315 } 316 317 func typeToArgName(ty types.Type) string { 318 s := types.Default(ty).String() 319 320 switch t := ty.(type) { 321 case *types.Basic: 322 // use first letter in type name for basic types 323 return s[0:1] 324 case *types.Slice: 325 // use element type to decide var name for slices 326 return typeToArgName(t.Elem()) 327 case *types.Array: 328 // use element type to decide var name for arrays 329 return typeToArgName(t.Elem()) 330 case *types.Chan: 331 return "ch" 332 } 333 334 s = strings.TrimFunc(s, func(r rune) bool { 335 return !unicode.IsLetter(r) 336 }) 337 338 if s == "error" { 339 return "err" 340 } 341 342 // remove package (if present) 343 // and make first letter lowercase 344 a := []rune(s[strings.LastIndexByte(s, '.')+1:]) 345 a[0] = unicode.ToLower(a[0]) 346 return string(a) 347 } 348 349 // isCallPosition reports whether the path denotes the subtree in call position, f(). 350 func isCallPosition(path []ast.Node) bool { 351 return len(path) > 1 && 352 is[*ast.CallExpr](path[1]) && 353 path[1].(*ast.CallExpr).Fun == path[0] 354 } 355 356 func is[T any](x any) bool { 357 _, ok := x.(T) 358 return ok 359 }