sa4032.go raw

   1  package sa4032
   2  
   3  import (
   4  	"fmt"
   5  	"go/ast"
   6  	"go/build/constraint"
   7  	"go/constant"
   8  
   9  	"golang.org/x/tools/go/analysis"
  10  	"golang.org/x/tools/go/analysis/passes/inspect"
  11  	"honnef.co/go/tools/analysis/code"
  12  	"honnef.co/go/tools/analysis/lint"
  13  	"honnef.co/go/tools/analysis/report"
  14  	"honnef.co/go/tools/knowledge"
  15  	"honnef.co/go/tools/pattern"
  16  )
  17  
  18  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
  19  	Analyzer: &analysis.Analyzer{
  20  		Name:     "SA4032",
  21  		Run:      CheckImpossibleGOOSGOARCH,
  22  		Requires: []*analysis.Analyzer{inspect.Analyzer},
  23  	},
  24  	Doc: &lint.RawDocumentation{
  25  		Title:    `Comparing \'runtime.GOOS\' or \'runtime.GOARCH\' against impossible value`,
  26  		Since:    "2024.1",
  27  		Severity: lint.SeverityWarning,
  28  		MergeIf:  lint.MergeIfAny,
  29  	},
  30  })
  31  
  32  var Analyzer = SCAnalyzer.Analyzer
  33  
  34  var (
  35  	goosComparisonQ   = pattern.MustParse(`(BinaryExpr (Symbol "runtime.GOOS") op@(Or "==" "!=") lit@(BasicLit "STRING" _))`)
  36  	goarchComparisonQ = pattern.MustParse(`(BinaryExpr (Symbol "runtime.GOARCH") op@(Or "==" "!=") lit@(BasicLit "STRING" _))`)
  37  )
  38  
  39  func CheckImpossibleGOOSGOARCH(pass *analysis.Pass) (any, error) {
  40  	// TODO(dh): validate GOOS and GOARCH together. that is,
  41  	// given '(linux && amd64) || (windows && mips)',
  42  	// flag 'if runtime.GOOS == "linux" && runtime.GOARCH == "mips"'
  43  	//
  44  	// We can't use our IR for the control flow graph, because go/types constant folds constant comparisons, so
  45  	// 'runtime.GOOS == "windows"' will just become 'false'. We can't use the AST-based CFG builder from x/tools,
  46  	// because it doesn't model branch conditions.
  47  
  48  	for _, f := range pass.Files {
  49  		expr, ok := code.BuildConstraints(pass, f)
  50  		if !ok {
  51  			continue
  52  		}
  53  
  54  		ast.Inspect(f, func(node ast.Node) bool {
  55  			if m, ok := code.Match(pass, goosComparisonQ, node); ok {
  56  				tv := pass.TypesInfo.Types[m.State["lit"].(ast.Expr)]
  57  				goos := constant.StringVal(tv.Value)
  58  
  59  				if _, ok := knowledge.KnownGOOS[goos]; !ok {
  60  					// Don't try to reason about GOOS values we don't know about. Maybe the user is using a newer
  61  					// version of Go that supports a new target, or maybe they run a fork of Go.
  62  					return true
  63  				}
  64  				sat, ok := validateGOOSComparison(expr, goos)
  65  				if !ok {
  66  					return true
  67  				}
  68  				if !sat {
  69  					// Note that we do not have to worry about constraints that can never be satisfied, such as 'linux
  70  					// && windows'. Packages with such files will not be passed to Staticcheck in the first place,
  71  					// precisely because the constraints aren't satisfiable.
  72  					report.Report(pass, node,
  73  						fmt.Sprintf("due to the file's build constraints, runtime.GOOS will never equal %q", goos))
  74  				}
  75  			} else if m, ok := code.Match(pass, goarchComparisonQ, node); ok {
  76  				tv := pass.TypesInfo.Types[m.State["lit"].(ast.Expr)]
  77  				goarch := constant.StringVal(tv.Value)
  78  
  79  				if _, ok := knowledge.KnownGOARCH[goarch]; !ok {
  80  					// Don't try to reason about GOARCH values we don't know about. Maybe the user is using a newer
  81  					// version of Go that supports a new target, or maybe they run a fork of Go.
  82  					return true
  83  				}
  84  				sat, ok := validateGOARCHComparison(expr, goarch)
  85  				if !ok {
  86  					return true
  87  				}
  88  				if !sat {
  89  					// Note that we do not have to worry about constraints that can never be satisfied, such as 'amd64
  90  					// && mips'. Packages with such files will not be passed to Staticcheck in the first place,
  91  					// precisely because the constraints aren't satisfiable.
  92  					report.Report(pass, node,
  93  						fmt.Sprintf("due to the file's build constraints, runtime.GOARCH will never equal %q", goarch))
  94  				}
  95  			}
  96  			return true
  97  		})
  98  	}
  99  
 100  	return nil, nil
 101  }
 102  func validateGOOSComparison(expr constraint.Expr, goos string) (sat bool, didCheck bool) {
 103  	matchGoosTag := func(tag string, goos string) (ok bool, goosTag bool) {
 104  		switch tag {
 105  		case "aix",
 106  			"android",
 107  			"dragonfly",
 108  			"freebsd",
 109  			"hurd",
 110  			"illumos",
 111  			"ios",
 112  			"js",
 113  			"netbsd",
 114  			"openbsd",
 115  			"plan9",
 116  			"wasip1",
 117  			"windows":
 118  			return goos == tag, true
 119  		case "darwin":
 120  			return (goos == "darwin" || goos == "ios"), true
 121  		case "linux":
 122  			return (goos == "linux" || goos == "android"), true
 123  		case "solaris":
 124  			return (goos == "solaris" || goos == "illumos"), true
 125  		case "unix":
 126  			return (goos == "aix" ||
 127  				goos == "android" ||
 128  				goos == "darwin" ||
 129  				goos == "dragonfly" ||
 130  				goos == "freebsd" ||
 131  				goos == "hurd" ||
 132  				goos == "illumos" ||
 133  				goos == "ios" ||
 134  				goos == "linux" ||
 135  				goos == "netbsd" ||
 136  				goos == "openbsd" ||
 137  				goos == "solaris"), true
 138  		default:
 139  			return false, false
 140  		}
 141  	}
 142  
 143  	return validateTagComparison(expr, func(tag string) (matched bool, special bool) {
 144  		return matchGoosTag(tag, goos)
 145  	})
 146  }
 147  
 148  func validateGOARCHComparison(expr constraint.Expr, goarch string) (sat bool, didCheck bool) {
 149  	matchGoarchTag := func(tag string, goarch string) (ok bool, goosTag bool) {
 150  		switch tag {
 151  		case "386",
 152  			"amd64",
 153  			"arm",
 154  			"arm64",
 155  			"loong64",
 156  			"mips",
 157  			"mipsle",
 158  			"mips64",
 159  			"mips64le",
 160  			"ppc64",
 161  			"ppc64le",
 162  			"riscv64",
 163  			"s390x",
 164  			"sparc64",
 165  			"wasm":
 166  			return goarch == tag, true
 167  		default:
 168  			return false, false
 169  		}
 170  	}
 171  
 172  	return validateTagComparison(expr, func(tag string) (matched bool, special bool) {
 173  		return matchGoarchTag(tag, goarch)
 174  	})
 175  }
 176  
 177  func validateTagComparison(expr constraint.Expr, matchSpecialTag func(tag string) (matched bool, special bool)) (sat bool, didCheck bool) {
 178  	otherTags := map[string]int{}
 179  	// Collect all tags that aren't known architecture-based tags
 180  	b := expr.Eval(func(tag string) bool {
 181  		ok, special := matchSpecialTag(tag)
 182  		if !special {
 183  			// Assign an ID to this tag, but only if we haven't seen it before. For the expression 'foo && foo', this
 184  			// callback will be called twice for the 'foo' tag.
 185  			if _, ok := otherTags[tag]; !ok {
 186  				otherTags[tag] = len(otherTags)
 187  			}
 188  		}
 189  		return ok
 190  	})
 191  
 192  	if b || len(otherTags) == 0 {
 193  		// We're done. Either the formula can be satisfied regardless of the values of non-special tags, if any,
 194  		// or there aren't any non-special tags and the formula cannot be satisfied.
 195  		return b, true
 196  	}
 197  
 198  	if len(otherTags) > 10 {
 199  		// We have to try 2**len(otherTags) combinations of tags. 2**10 is about the worst we're willing to try.
 200  		return false, false
 201  	}
 202  
 203  	// Try all permutations of otherTags. If any evaluates to true, then the expression is satisfiable.
 204  	for bits := 0; bits < 1<<len(otherTags); bits++ {
 205  		b := expr.Eval(func(tag string) bool {
 206  			ok, special := matchSpecialTag(tag)
 207  			if special {
 208  				return ok
 209  			}
 210  			return bits&(1<<otherTags[tag]) != 0
 211  		})
 212  		if b {
 213  			return true, true
 214  		}
 215  	}
 216  
 217  	return false, true
 218  }
 219