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  }