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 }