src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

sa4032.go (6645B)


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