diff options
| author | Taras Madan <tarasmadan@google.com> | 2024-09-10 12:16:33 +0200 |
|---|---|---|
| committer | Taras Madan <tarasmadan@google.com> | 2024-09-10 14:05:26 +0000 |
| commit | c97c816133b42257d0bcf1ee4bd178bb2a7a2b9e (patch) | |
| tree | 0bcbc2e540bbf8f62f6c17887cdd53b8c2cee637 /vendor/honnef.co/go/tools/analysis/code/code.go | |
| parent | 54e657429ab892ad06c90cd7c1a4eb33ba93a3dc (diff) | |
vendor: update
Diffstat (limited to 'vendor/honnef.co/go/tools/analysis/code/code.go')
| -rw-r--r-- | vendor/honnef.co/go/tools/analysis/code/code.go | 260 |
1 files changed, 248 insertions, 12 deletions
diff --git a/vendor/honnef.co/go/tools/analysis/code/code.go b/vendor/honnef.co/go/tools/analysis/code/code.go index f200363d9..7d20ea4c9 100644 --- a/vendor/honnef.co/go/tools/analysis/code/code.go +++ b/vendor/honnef.co/go/tools/analysis/code/code.go @@ -2,12 +2,14 @@ package code import ( - "flag" "fmt" "go/ast" + "go/build/constraint" "go/constant" "go/token" "go/types" + "go/version" + "path/filepath" "strings" "honnef.co/go/tools/analysis/facts/generated" @@ -15,6 +17,7 @@ import ( "honnef.co/go/tools/analysis/facts/tokenfile" "honnef.co/go/tools/go/ast/astutil" "honnef.co/go/tools/go/types/typeutil" + "honnef.co/go/tools/knowledge" "honnef.co/go/tools/pattern" "golang.org/x/tools/go/analysis" @@ -24,8 +27,30 @@ type Positioner interface { Pos() token.Pos } -func IsOfType(pass *analysis.Pass, expr ast.Expr, name string) bool { - return typeutil.IsType(pass.TypesInfo.TypeOf(expr), name) +func IsOfStringConvertibleByteSlice(pass *analysis.Pass, expr ast.Expr) bool { + typ, ok := pass.TypesInfo.TypeOf(expr).Underlying().(*types.Slice) + if !ok { + return false + } + elem := types.Unalias(typ.Elem()) + if version.Compare(LanguageVersion(pass, expr), "go1.18") >= 0 { + // Before Go 1.18, one could not directly convert from []T (where 'type T byte') + // to string. See also https://github.com/golang/go/issues/23536. + elem = elem.Underlying() + } + return types.Identical(elem, types.Typ[types.Byte]) +} + +func IsOfPointerToTypeWithName(pass *analysis.Pass, expr ast.Expr, name string) bool { + ptr, ok := types.Unalias(pass.TypesInfo.TypeOf(expr)).(*types.Pointer) + if !ok { + return false + } + return typeutil.IsTypeWithName(ptr.Elem(), name) +} + +func IsOfTypeWithName(pass *analysis.Pass, expr ast.Expr, name string) bool { + return typeutil.IsTypeWithName(pass.TypesInfo.TypeOf(expr), name) } func IsInTest(pass *analysis.Pass, node Positioner) bool { @@ -94,8 +119,9 @@ func BoolConst(pass *analysis.Pass, expr ast.Expr) bool { func IsBoolConst(pass *analysis.Pass, expr ast.Expr) bool { // We explicitly don't support typed bools because more often than - // not, custom bool types are used as binary enums and the - // explicit comparison is desired. + // not, custom bool types are used as binary enums and the explicit + // comparison is desired. We err on the side of false negatives and + // treat aliases like other custom types. ident, ok := expr.(*ast.Ident) if !ok { @@ -139,6 +165,9 @@ func ExprToString(pass *analysis.Pass, expr ast.Expr) (string, bool) { } func CallName(pass *analysis.Pass, call *ast.CallExpr) string { + // See the comment in typeutil.FuncName for why this doesn't require special handling + // of aliases. + fun := astutil.Unparen(call.Fun) // Instantiating a function cannot return another generic function, so doing this once is enough @@ -174,6 +203,9 @@ func CallName(pass *analysis.Pass, call *ast.CallExpr) string { } func IsCallTo(pass *analysis.Pass, node ast.Node, name string) bool { + // See the comment in typeutil.FuncName for why this doesn't require special handling + // of aliases. + call, ok := node.(*ast.CallExpr) if !ok { return false @@ -182,6 +214,9 @@ func IsCallTo(pass *analysis.Pass, node ast.Node, name string) bool { } func IsCallToAny(pass *analysis.Pass, node ast.Node, names ...string) bool { + // See the comment in typeutil.FuncName for why this doesn't require special handling + // of aliases. + call, ok := node.(*ast.CallExpr) if !ok { return false @@ -200,7 +235,91 @@ func File(pass *analysis.Pass, node Positioner) *ast.File { return m[pass.Fset.File(node.Pos())] } -// IsGenerated reports whether pos is in a generated file, It ignores +// BuildConstraints returns the build constraints for file f. It considers both //go:build lines as well as +// GOOS and GOARCH in file names. +func BuildConstraints(pass *analysis.Pass, f *ast.File) (constraint.Expr, bool) { + var expr constraint.Expr + for _, cmt := range f.Comments { + if len(cmt.List) == 0 { + continue + } + for _, el := range cmt.List { + if el.Pos() > f.Package { + break + } + if line := el.Text; strings.HasPrefix(line, "//go:build") { + var err error + expr, err = constraint.Parse(line) + if err != nil { + expr = nil + } + break + } + } + } + + name := pass.Fset.PositionFor(f.Pos(), false).Filename + oexpr := constraintsFromName(name) + if oexpr != nil { + if expr == nil { + expr = oexpr + } else { + expr = &constraint.AndExpr{X: expr, Y: oexpr} + } + } + + return expr, expr != nil +} + +func constraintsFromName(name string) constraint.Expr { + name = filepath.Base(name) + name = strings.TrimSuffix(name, ".go") + name = strings.TrimSuffix(name, "_test") + var goos, goarch string + switch strings.Count(name, "_") { + case 0: + // No GOOS or GOARCH in the file name. + case 1: + _, c, _ := strings.Cut(name, "_") + if _, ok := knowledge.KnownGOOS[c]; ok { + goos = c + } else if _, ok := knowledge.KnownGOARCH[c]; ok { + goarch = c + } + default: + n := strings.LastIndex(name, "_") + if _, ok := knowledge.KnownGOOS[name[n+1:]]; ok { + // The file name is *_stuff_GOOS.go + goos = name[n+1:] + } else if _, ok := knowledge.KnownGOARCH[name[n+1:]]; ok { + // The file name is *_GOOS_GOARCH.go or *_stuff_GOARCH.go + goarch = name[n+1:] + _, c, _ := strings.Cut(name[:n], "_") + if _, ok := knowledge.KnownGOOS[c]; ok { + // The file name is *_GOOS_GOARCH.go + goos = c + } + } else { + // The file name could also be something like foo_windows_nonsense.go — and because nonsense + // isn't a known GOARCH, "windows" won't be interpreted as a GOOS, either. + } + } + + var expr constraint.Expr + if goos != "" { + expr = &constraint.TagExpr{Tag: goos} + } + if goarch != "" { + if expr == nil { + expr = &constraint.TagExpr{Tag: goarch} + } else { + expr = &constraint.AndExpr{X: expr, Y: &constraint.TagExpr{Tag: goarch}} + } + } + return expr +} + +// IsGenerated reports whether pos is in a generated file. It ignores // //line directives. func IsGenerated(pass *analysis.Pass, pos token.Pos) bool { _, ok := Generator(pass, pos) @@ -315,13 +434,113 @@ func MayHaveSideEffects(pass *analysis.Pass, expr ast.Expr, purity purity.Result } } -func IsGoVersion(pass *analysis.Pass, minor int) bool { - f, ok := pass.Analyzer.Flags.Lookup("go").Value.(flag.Getter) - if !ok { - panic("requested Go version, but analyzer has no version flag") +// LanguageVersion returns the version of the Go language that node has access to. This +// might differ from the version of the Go standard library. +func LanguageVersion(pass *analysis.Pass, node Positioner) string { + // As of Go 1.21, two places can specify the minimum Go version: + // - 'go' directives in go.mod and go.work files + // - individual files by using '//go:build' + // + // Individual files can upgrade to a higher version than the module version. Individual files + // can also downgrade to a lower version, but only if the module version is at least Go 1.21. + // + // The restriction on downgrading doesn't matter to us. All language changes before Go 1.22 will + // not type-check on versions that are too old, and thus never reach our analyzes. In practice, + // such ineffective downgrading will always be useless, as the compiler will not restrict the + // language features used, and doesn't ever rely on minimum versions to restrict the use of the + // standard library. However, for us, both choices (respecting or ignoring ineffective + // downgrading) have equal complexity, but only respecting it has a non-zero chance of reducing + // noisy positives. + // + // The minimum Go versions are exposed via go/ast.File.GoVersion and go/types.Package.GoVersion. + // ast.File's version is populated by the parser, whereas types.Package's version is populated + // from the Go version specified in the types.Config, which is set by our package loader, based + // on the module information provided by go/packages, via 'go list -json'. + // + // As of Go 1.21, standard library packages do not present themselves as modules, and thus do + // not have a version set on their types.Package. In this case, we fall back to the version + // provided by our '-go' flag. In most cases, '-go' defaults to 'module', which falls back to + // the Go version that Staticcheck was built with when no module information exists. In the + // future, the standard library will hopefully be a proper module (see + // https://github.com/golang/go/issues/61174#issuecomment-1622471317). In that case, the version + // of standard library packages will match that of the used Go version. At that point, + // Staticcheck will refuse to work with Go versions that are too new, to avoid misinterpreting + // code due to language changes. + // + // We also lack module information when building in GOPATH mode. In this case, the implied + // language version is at most Go 1.21, as per https://github.com/golang/go/issues/60915. We + // don't handle this yet, and it will not matter until Go 1.22. + // + // It is not clear how per-file downgrading behaves in GOPATH mode. On the one hand, no module + // version at all is provided, which should preclude per-file downgrading. On the other hand, + // https://github.com/golang/go/issues/60915 suggests that the language version is at most 1.21 + // in GOPATH mode, which would allow per-file downgrading. Again it doesn't affect us, as all + // relevant language changes before Go 1.22 will lead to type-checking failures and never reach + // us. + // + // Per-file upgrading is permitted in GOPATH mode. + + // If the file has its own Go version, we will return that. Otherwise, we default to + // the type checker's GoVersion, which is populated from either the Go module, or from + // our '-go' flag. + return pass.TypesInfo.FileVersions[File(pass, node)] +} + +// StdlibVersion returns the version of the Go standard library that node can expect to +// have access to. This might differ from the language version for versions of Go older +// than 1.21. +func StdlibVersion(pass *analysis.Pass, node Positioner) string { + // The Go version as specified in go.mod or via the '-go' flag + n := pass.Pkg.GoVersion() + + f := File(pass, node) + if f == nil { + panic(fmt.Sprintf("no file found for node with position %s", pass.Fset.PositionFor(node.Pos(), false))) } - version := f.Get().(int) - return version >= minor + + if nf := f.GoVersion; nf != "" { + if version.Compare(n, "go1.21") == -1 { + // Before Go 1.21, the Go version set in go.mod specified the maximum language + // version available to the module. It wasn't uncommon to set the version to + // Go 1.20 but restrict usage of 1.20 functionality (both language and stdlib) + // to files tagged for 1.20, and supporting a lower version overall. As such, + // a file tagged lower than the module version couldn't expect to have access + // to the standard library of the version set in go.mod. + // + // At the same time, a file tagged higher than the module version, while not + // able to use newer language features, would still have been able to use a + // newer standard library. + // + // While Go 1.21's behavior has been backported to 1.19.11 and 1.20.6, users' + // expectations have not. + return nf + } else { + // Go 1.21 and newer refuse to build modules that depend on versions newer + // than the used version of the Go toolchain. This means that in a 1.22 module + // with a file tagged as 1.17, the file can expect to have access to 1.22's + // standard library (but not to 1.22 language features). A file tagged with a + // version higher than the minimum version has access to the newer standard + // library (and language features.) + // + // Do note that strictly speaking we're conflating the Go version and the + // module version in our check. Nothing is stopping a user from using Go 1.17 + // (which didn't implement the new rules for versions in go.mod) to build a Go + // 1.22 module, in which case a file tagged with go1.17 will not have acces to the 1.22 + // standard library. However, we believe that if a module requires 1.21 or + // newer, then the author clearly expects the new behavior, and doesn't care + // for the old one. Otherwise they would've specified an older version. + // + // In other words, the module version also specifies what it itself actually means, with + // >=1.21 being a minimum version for the toolchain, and <1.21 being a maximum version for + // the language. + + if version.Compare(nf, n) == 1 { + return nf + } + } + } + + return n } var integerLiteralQ = pattern.MustParse(`(IntegerLiteral tv)`) @@ -355,3 +574,20 @@ func IsMethod(pass *analysis.Pass, expr *ast.SelectorExpr, name string, meth *ty } return types.Identical(sel.Type(), meth) } + +func RefersTo(pass *analysis.Pass, expr ast.Expr, ident types.Object) bool { + found := false + fn := func(node ast.Node) bool { + ident2, ok := node.(*ast.Ident) + if !ok { + return true + } + if ident == pass.TypesInfo.ObjectOf(ident2) { + found = true + return false + } + return true + } + ast.Inspect(expr, fn) + return found +} |
