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