honnef.co/go/tools@v0.5.0-0.dev.0.20240520180541-dcae280a5e87/analysis/code/code.go (about) 1 // Package code answers structural and type questions about Go code. 2 package code 3 4 import ( 5 "flag" 6 "fmt" 7 "go/ast" 8 "go/build/constraint" 9 "go/constant" 10 "go/token" 11 "go/types" 12 "path/filepath" 13 "strconv" 14 "strings" 15 16 "honnef.co/go/tools/analysis/facts/generated" 17 "honnef.co/go/tools/analysis/facts/purity" 18 "honnef.co/go/tools/analysis/facts/tokenfile" 19 "honnef.co/go/tools/analysis/lint" 20 "honnef.co/go/tools/go/ast/astutil" 21 "honnef.co/go/tools/go/types/typeutil" 22 "honnef.co/go/tools/knowledge" 23 "honnef.co/go/tools/pattern" 24 25 "golang.org/x/tools/go/analysis" 26 ) 27 28 type Positioner interface { 29 Pos() token.Pos 30 } 31 32 func IsOfStringConvertibleByteSlice(pass *analysis.Pass, expr ast.Expr) bool { 33 typ, ok := pass.TypesInfo.TypeOf(expr).Underlying().(*types.Slice) 34 if !ok { 35 return false 36 } 37 elem := types.Unalias(typ.Elem()) 38 if LanguageVersion(pass, expr) >= 18 { 39 // Before Go 1.18, one could not directly convert from []T (where 'type T byte') 40 // to string. See also https://github.com/golang/go/issues/23536. 41 elem = elem.Underlying() 42 } 43 return types.Identical(elem, types.Typ[types.Byte]) 44 } 45 46 func IsOfPointerToTypeWithName(pass *analysis.Pass, expr ast.Expr, name string) bool { 47 ptr, ok := types.Unalias(pass.TypesInfo.TypeOf(expr)).(*types.Pointer) 48 if !ok { 49 return false 50 } 51 return typeutil.IsTypeWithName(ptr.Elem(), name) 52 } 53 54 func IsOfTypeWithName(pass *analysis.Pass, expr ast.Expr, name string) bool { 55 return typeutil.IsTypeWithName(pass.TypesInfo.TypeOf(expr), name) 56 } 57 58 func IsInTest(pass *analysis.Pass, node Positioner) bool { 59 // FIXME(dh): this doesn't work for global variables with 60 // initializers 61 f := pass.Fset.File(node.Pos()) 62 return f != nil && strings.HasSuffix(f.Name(), "_test.go") 63 } 64 65 // IsMain reports whether the package being processed is a package 66 // main. 67 func IsMain(pass *analysis.Pass) bool { 68 return pass.Pkg.Name() == "main" 69 } 70 71 // IsMainLike reports whether the package being processed is a 72 // main-like package. A main-like package is a package that is 73 // package main, or that is intended to be used by a tool framework 74 // such as cobra to implement a command. 75 // 76 // Note that this function errs on the side of false positives; it may 77 // return true for packages that aren't main-like. IsMainLike is 78 // intended for analyses that wish to suppress diagnostics for 79 // main-like packages to avoid false positives. 80 func IsMainLike(pass *analysis.Pass) bool { 81 if pass.Pkg.Name() == "main" { 82 return true 83 } 84 for _, imp := range pass.Pkg.Imports() { 85 if imp.Path() == "github.com/spf13/cobra" { 86 return true 87 } 88 } 89 return false 90 } 91 92 func SelectorName(pass *analysis.Pass, expr *ast.SelectorExpr) string { 93 info := pass.TypesInfo 94 sel := info.Selections[expr] 95 if sel == nil { 96 if x, ok := expr.X.(*ast.Ident); ok { 97 pkg, ok := info.ObjectOf(x).(*types.PkgName) 98 if !ok { 99 // This shouldn't happen 100 return fmt.Sprintf("%s.%s", x.Name, expr.Sel.Name) 101 } 102 return fmt.Sprintf("%s.%s", pkg.Imported().Path(), expr.Sel.Name) 103 } 104 panic(fmt.Sprintf("unsupported selector: %v", expr)) 105 } 106 if v, ok := sel.Obj().(*types.Var); ok && v.IsField() { 107 return fmt.Sprintf("(%s).%s", typeutil.DereferenceR(sel.Recv()), sel.Obj().Name()) 108 } else { 109 return fmt.Sprintf("(%s).%s", sel.Recv(), sel.Obj().Name()) 110 } 111 } 112 113 func IsNil(pass *analysis.Pass, expr ast.Expr) bool { 114 return pass.TypesInfo.Types[expr].IsNil() 115 } 116 117 func BoolConst(pass *analysis.Pass, expr ast.Expr) bool { 118 val := pass.TypesInfo.ObjectOf(expr.(*ast.Ident)).(*types.Const).Val() 119 return constant.BoolVal(val) 120 } 121 122 func IsBoolConst(pass *analysis.Pass, expr ast.Expr) bool { 123 // We explicitly don't support typed bools because more often than 124 // not, custom bool types are used as binary enums and the explicit 125 // comparison is desired. We err on the side of false negatives and 126 // treat aliases like other custom types. 127 128 ident, ok := expr.(*ast.Ident) 129 if !ok { 130 return false 131 } 132 obj := pass.TypesInfo.ObjectOf(ident) 133 c, ok := obj.(*types.Const) 134 if !ok { 135 return false 136 } 137 basic, ok := c.Type().(*types.Basic) 138 if !ok { 139 return false 140 } 141 if basic.Kind() != types.UntypedBool && basic.Kind() != types.Bool { 142 return false 143 } 144 return true 145 } 146 147 func ExprToInt(pass *analysis.Pass, expr ast.Expr) (int64, bool) { 148 tv := pass.TypesInfo.Types[expr] 149 if tv.Value == nil { 150 return 0, false 151 } 152 if tv.Value.Kind() != constant.Int { 153 return 0, false 154 } 155 return constant.Int64Val(tv.Value) 156 } 157 158 func ExprToString(pass *analysis.Pass, expr ast.Expr) (string, bool) { 159 val := pass.TypesInfo.Types[expr].Value 160 if val == nil { 161 return "", false 162 } 163 if val.Kind() != constant.String { 164 return "", false 165 } 166 return constant.StringVal(val), true 167 } 168 169 func CallName(pass *analysis.Pass, call *ast.CallExpr) string { 170 // See the comment in typeutil.FuncName for why this doesn't require special handling 171 // of aliases. 172 173 fun := astutil.Unparen(call.Fun) 174 175 // Instantiating a function cannot return another generic function, so doing this once is enough 176 switch idx := fun.(type) { 177 case *ast.IndexExpr: 178 fun = idx.X 179 case *ast.IndexListExpr: 180 fun = idx.X 181 } 182 183 // (foo)[T] is not a valid instantiationg, so no need to unparen again. 184 185 switch fun := fun.(type) { 186 case *ast.SelectorExpr: 187 fn, ok := pass.TypesInfo.ObjectOf(fun.Sel).(*types.Func) 188 if !ok { 189 return "" 190 } 191 return typeutil.FuncName(fn) 192 case *ast.Ident: 193 obj := pass.TypesInfo.ObjectOf(fun) 194 switch obj := obj.(type) { 195 case *types.Func: 196 return typeutil.FuncName(obj) 197 case *types.Builtin: 198 return obj.Name() 199 default: 200 return "" 201 } 202 default: 203 return "" 204 } 205 } 206 207 func IsCallTo(pass *analysis.Pass, node ast.Node, name string) bool { 208 // See the comment in typeutil.FuncName for why this doesn't require special handling 209 // of aliases. 210 211 call, ok := node.(*ast.CallExpr) 212 if !ok { 213 return false 214 } 215 return CallName(pass, call) == name 216 } 217 218 func IsCallToAny(pass *analysis.Pass, node ast.Node, names ...string) bool { 219 // See the comment in typeutil.FuncName for why this doesn't require special handling 220 // of aliases. 221 222 call, ok := node.(*ast.CallExpr) 223 if !ok { 224 return false 225 } 226 q := CallName(pass, call) 227 for _, name := range names { 228 if q == name { 229 return true 230 } 231 } 232 return false 233 } 234 235 func File(pass *analysis.Pass, node Positioner) *ast.File { 236 m := pass.ResultOf[tokenfile.Analyzer].(map[*token.File]*ast.File) 237 return m[pass.Fset.File(node.Pos())] 238 } 239 240 // BuildConstraints returns the build constraints for file f. It considers both //go:build lines as well as 241 // GOOS and GOARCH in file names. 242 func BuildConstraints(pass *analysis.Pass, f *ast.File) (constraint.Expr, bool) { 243 var expr constraint.Expr 244 for _, cmt := range f.Comments { 245 if len(cmt.List) == 0 { 246 continue 247 } 248 for _, el := range cmt.List { 249 if el.Pos() > f.Package { 250 break 251 } 252 if line := el.Text; strings.HasPrefix(line, "//go:build") { 253 var err error 254 expr, err = constraint.Parse(line) 255 if err != nil { 256 expr = nil 257 } 258 break 259 } 260 } 261 } 262 263 name := pass.Fset.PositionFor(f.Pos(), false).Filename 264 oexpr := constraintsFromName(name) 265 if oexpr != nil { 266 if expr == nil { 267 expr = oexpr 268 } else { 269 expr = &constraint.AndExpr{X: expr, Y: oexpr} 270 } 271 } 272 273 return expr, expr != nil 274 } 275 276 func constraintsFromName(name string) constraint.Expr { 277 name = filepath.Base(name) 278 name = strings.TrimSuffix(name, ".go") 279 name = strings.TrimSuffix(name, "_test") 280 var goos, goarch string 281 switch strings.Count(name, "_") { 282 case 0: 283 // No GOOS or GOARCH in the file name. 284 case 1: 285 _, c, _ := strings.Cut(name, "_") 286 if _, ok := knowledge.KnownGOOS[c]; ok { 287 goos = c 288 } else if _, ok := knowledge.KnownGOARCH[c]; ok { 289 goarch = c 290 } 291 default: 292 n := strings.LastIndex(name, "_") 293 if _, ok := knowledge.KnownGOOS[name[n+1:]]; ok { 294 // The file name is *_stuff_GOOS.go 295 goos = name[n+1:] 296 } else if _, ok := knowledge.KnownGOARCH[name[n+1:]]; ok { 297 // The file name is *_GOOS_GOARCH.go or *_stuff_GOARCH.go 298 goarch = name[n+1:] 299 _, c, _ := strings.Cut(name[:n], "_") 300 if _, ok := knowledge.KnownGOOS[c]; ok { 301 // The file name is *_GOOS_GOARCH.go 302 goos = c 303 } 304 } else { 305 // The file name could also be something like foo_windows_nonsense.go — and because nonsense 306 // isn't a known GOARCH, "windows" won't be interpreted as a GOOS, either. 307 } 308 } 309 310 var expr constraint.Expr 311 if goos != "" { 312 expr = &constraint.TagExpr{Tag: goos} 313 } 314 if goarch != "" { 315 if expr == nil { 316 expr = &constraint.TagExpr{Tag: goarch} 317 } else { 318 expr = &constraint.AndExpr{X: expr, Y: &constraint.TagExpr{Tag: goarch}} 319 } 320 } 321 return expr 322 } 323 324 // IsGenerated reports whether pos is in a generated file. It ignores 325 // //line directives. 326 func IsGenerated(pass *analysis.Pass, pos token.Pos) bool { 327 _, ok := Generator(pass, pos) 328 return ok 329 } 330 331 // Generator returns the generator that generated the file containing 332 // pos. It ignores //line directives. 333 func Generator(pass *analysis.Pass, pos token.Pos) (generated.Generator, bool) { 334 file := pass.Fset.PositionFor(pos, false).Filename 335 m := pass.ResultOf[generated.Analyzer].(map[string]generated.Generator) 336 g, ok := m[file] 337 return g, ok 338 } 339 340 // MayHaveSideEffects reports whether expr may have side effects. If 341 // the purity argument is nil, this function implements a purely 342 // syntactic check, meaning that any function call may have side 343 // effects, regardless of the called function's body. Otherwise, 344 // purity will be consulted to determine the purity of function calls. 345 func MayHaveSideEffects(pass *analysis.Pass, expr ast.Expr, purity purity.Result) bool { 346 switch expr := expr.(type) { 347 case *ast.BadExpr: 348 return true 349 case *ast.Ellipsis: 350 return MayHaveSideEffects(pass, expr.Elt, purity) 351 case *ast.FuncLit: 352 // the literal itself cannot have side effects, only calling it 353 // might, which is handled by CallExpr. 354 return false 355 case *ast.ArrayType, *ast.StructType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.ChanType: 356 // types cannot have side effects 357 return false 358 case *ast.BasicLit: 359 return false 360 case *ast.BinaryExpr: 361 return MayHaveSideEffects(pass, expr.X, purity) || MayHaveSideEffects(pass, expr.Y, purity) 362 case *ast.CallExpr: 363 if purity == nil { 364 return true 365 } 366 switch obj := typeutil.Callee(pass.TypesInfo, expr).(type) { 367 case *types.Func: 368 if _, ok := purity[obj]; !ok { 369 return true 370 } 371 case *types.Builtin: 372 switch obj.Name() { 373 case "len", "cap": 374 default: 375 return true 376 } 377 default: 378 return true 379 } 380 for _, arg := range expr.Args { 381 if MayHaveSideEffects(pass, arg, purity) { 382 return true 383 } 384 } 385 return false 386 case *ast.CompositeLit: 387 if MayHaveSideEffects(pass, expr.Type, purity) { 388 return true 389 } 390 for _, elt := range expr.Elts { 391 if MayHaveSideEffects(pass, elt, purity) { 392 return true 393 } 394 } 395 return false 396 case *ast.Ident: 397 return false 398 case *ast.IndexExpr: 399 return MayHaveSideEffects(pass, expr.X, purity) || MayHaveSideEffects(pass, expr.Index, purity) 400 case *ast.IndexListExpr: 401 // In theory, none of the checks are necessary, as IndexListExpr only involves types. But there is no harm in 402 // being safe. 403 if MayHaveSideEffects(pass, expr.X, purity) { 404 return true 405 } 406 for _, idx := range expr.Indices { 407 if MayHaveSideEffects(pass, idx, purity) { 408 return true 409 } 410 } 411 return false 412 case *ast.KeyValueExpr: 413 return MayHaveSideEffects(pass, expr.Key, purity) || MayHaveSideEffects(pass, expr.Value, purity) 414 case *ast.SelectorExpr: 415 return MayHaveSideEffects(pass, expr.X, purity) 416 case *ast.SliceExpr: 417 return MayHaveSideEffects(pass, expr.X, purity) || 418 MayHaveSideEffects(pass, expr.Low, purity) || 419 MayHaveSideEffects(pass, expr.High, purity) || 420 MayHaveSideEffects(pass, expr.Max, purity) 421 case *ast.StarExpr: 422 return MayHaveSideEffects(pass, expr.X, purity) 423 case *ast.TypeAssertExpr: 424 return MayHaveSideEffects(pass, expr.X, purity) 425 case *ast.UnaryExpr: 426 if MayHaveSideEffects(pass, expr.X, purity) { 427 return true 428 } 429 return expr.Op == token.ARROW || expr.Op == token.AND 430 case *ast.ParenExpr: 431 return MayHaveSideEffects(pass, expr.X, purity) 432 case nil: 433 return false 434 default: 435 panic(fmt.Sprintf("internal error: unhandled type %T", expr)) 436 } 437 } 438 439 func LanguageVersion(pass *analysis.Pass, node Positioner) int { 440 // As of Go 1.21, two places can specify the minimum Go version: 441 // - 'go' directives in go.mod and go.work files 442 // - individual files by using '//go:build' 443 // 444 // Individual files can upgrade to a higher version than the module version. Individual files 445 // can also downgrade to a lower version, but only if the module version is at least Go 1.21. 446 // 447 // The restriction on downgrading doesn't matter to us. All language changes before Go 1.22 will 448 // not type-check on versions that are too old, and thus never reach our analyzes. In practice, 449 // such ineffective downgrading will always be useless, as the compiler will not restrict the 450 // language features used, and doesn't ever rely on minimum versions to restrict the use of the 451 // standard library. However, for us, both choices (respecting or ignoring ineffective 452 // downgrading) have equal complexity, but only respecting it has a non-zero chance of reducing 453 // noisy positives. 454 // 455 // The minimum Go versions are exposed via go/ast.File.GoVersion and go/types.Package.GoVersion. 456 // ast.File's version is populated by the parser, whereas types.Package's version is populated 457 // from the Go version specified in the types.Config, which is set by our package loader, based 458 // on the module information provided by go/packages, via 'go list -json'. 459 // 460 // As of Go 1.21, standard library packages do not present themselves as modules, and thus do 461 // not have a version set on their types.Package. In this case, we fall back to the version 462 // provided by our '-go' flag. In most cases, '-go' defaults to 'module', which falls back to 463 // the Go version that Staticcheck was built with when no module information exists. In the 464 // future, the standard library will hopefully be a proper module (see 465 // https://github.com/golang/go/issues/61174#issuecomment-1622471317). In that case, the version 466 // of standard library packages will match that of the used Go version. At that point, 467 // Staticcheck will refuse to work with Go versions that are too new, to avoid misinterpreting 468 // code due to language changes. 469 // 470 // We also lack module information when building in GOPATH mode. In this case, the implied 471 // language version is at most Go 1.21, as per https://github.com/golang/go/issues/60915. We 472 // don't handle this yet, and it will not matter until Go 1.22. 473 // 474 // It is not clear how per-file downgrading behaves in GOPATH mode. On the one hand, no module 475 // version at all is provided, which should preclude per-file downgrading. On the other hand, 476 // https://github.com/golang/go/issues/60915 suggests that the language version is at most 1.21 477 // in GOPATH mode, which would allow per-file downgrading. Again it doesn't affect us, as all 478 // relevant language changes before Go 1.22 will lead to type-checking failures and never reach 479 // us. 480 // 481 // It is not clear if per-file upgrading is possible in GOPATH mode. This needs clarification. 482 483 f := File(pass, node) 484 var n int 485 if v := f.GoVersion; v != "" { 486 var ok bool 487 n, ok = lint.ParseGoVersion(v) 488 if !ok { 489 panic(fmt.Sprintf("unexpected failure parsing version %q", v)) 490 } 491 } else if v := pass.Pkg.GoVersion(); v != "" { 492 var ok bool 493 n, ok = lint.ParseGoVersion(v) 494 if !ok { 495 panic(fmt.Sprintf("unexpected failure parsing version %q", v)) 496 } 497 } else { 498 v, ok := pass.Analyzer.Flags.Lookup("go").Value.(flag.Getter) 499 if !ok { 500 panic("requested Go version, but analyzer has no version flag") 501 } 502 n = v.Get().(int) 503 } 504 505 return n 506 } 507 508 func StdlibVersion(pass *analysis.Pass, node Positioner) int { 509 var n int 510 if v := pass.Pkg.GoVersion(); v != "" { 511 var ok bool 512 n, ok = lint.ParseGoVersion(v) 513 if !ok { 514 panic(fmt.Sprintf("unexpected failure parsing version %q", v)) 515 } 516 } else { 517 v, ok := pass.Analyzer.Flags.Lookup("go").Value.(flag.Getter) 518 if !ok { 519 panic("requested Go version, but analyzer has no version flag") 520 } 521 n = v.Get().(int) 522 } 523 524 f := File(pass, node) 525 if f == nil { 526 panic(fmt.Sprintf("no file found for node with position %s", pass.Fset.PositionFor(node.Pos(), false))) 527 } 528 529 if v := f.GoVersion; v != "" { 530 nf, err := strconv.Atoi(strings.TrimPrefix(v, "go1.")) 531 if err != nil { 532 panic(fmt.Sprintf("unexpected error: %s", err)) 533 } 534 535 if n < 21 { 536 // Before Go 1.21, the Go version set in go.mod specified the maximum language version 537 // available to the module. It wasn't uncommon to set the version to Go 1.20 but only 538 // use 1.20 functionality (both language and stdlib) in files tagged for 1.20, and 539 // supporting a lower version overall. As such, a file tagged lower than the module 540 // version couldn't expect to have access to the standard library of the version set in 541 // go.mod. 542 // 543 // While Go 1.21's behavior has been backported to 1.19.11 and 1.20.6, users' 544 // expectations have not. 545 n = nf 546 } else { 547 // Go 1.21 and newer refuse to build modules that depend on versions newer than the Go 548 // version. This means that in a 1.22 module with a file tagged as 1.17, the file can 549 // expect to have access to 1.22's standard library. 550 // 551 // Do note that strictly speaking we're conflating the Go version and the module version in 552 // our check. Nothing is stopping a user from using Go 1.17 to build a Go 1.22 module, in 553 // which case the 1.17 file will not have acces to the 1.22 standard library. However, we 554 // believe that if a module requires 1.21 or newer, then the author clearly expects the new 555 // behavior, and doesn't care for the old one. Otherwise they would've specified an older 556 // version. 557 // 558 // In other words, the module version also specifies what it itself actually means, with 559 // >=1.21 being a minimum version for the toolchain, and <1.21 being a maximum version for 560 // the language. 561 562 if nf > n { 563 n = nf 564 } 565 } 566 } 567 568 return n 569 } 570 571 var integerLiteralQ = pattern.MustParse(`(IntegerLiteral tv)`) 572 573 func IntegerLiteral(pass *analysis.Pass, node ast.Node) (types.TypeAndValue, bool) { 574 m, ok := Match(pass, integerLiteralQ, node) 575 if !ok { 576 return types.TypeAndValue{}, false 577 } 578 return m.State["tv"].(types.TypeAndValue), true 579 } 580 581 func IsIntegerLiteral(pass *analysis.Pass, node ast.Node, value constant.Value) bool { 582 tv, ok := IntegerLiteral(pass, node) 583 if !ok { 584 return false 585 } 586 return constant.Compare(tv.Value, token.EQL, value) 587 } 588 589 // IsMethod reports whether expr is a method call of a named method with signature meth. 590 // If name is empty, it is not checked. 591 // For now, method expressions (Type.Method(recv, ..)) are not considered method calls. 592 func IsMethod(pass *analysis.Pass, expr *ast.SelectorExpr, name string, meth *types.Signature) bool { 593 if name != "" && expr.Sel.Name != name { 594 return false 595 } 596 sel, ok := pass.TypesInfo.Selections[expr] 597 if !ok || sel.Kind() != types.MethodVal { 598 return false 599 } 600 return types.Identical(sel.Type(), meth) 601 } 602 603 func RefersTo(pass *analysis.Pass, expr ast.Expr, ident types.Object) bool { 604 found := false 605 fn := func(node ast.Node) bool { 606 ident2, ok := node.(*ast.Ident) 607 if !ok { 608 return true 609 } 610 if ident == pass.TypesInfo.ObjectOf(ident2) { 611 found = true 612 return false 613 } 614 return true 615 } 616 ast.Inspect(expr, fn) 617 return found 618 }