github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa4032/sa4032.go (about) 1 package sa4032 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/build/constraint" 7 "go/constant" 8 9 "github.com/amarpal/go-tools/analysis/code" 10 "github.com/amarpal/go-tools/analysis/lint" 11 "github.com/amarpal/go-tools/analysis/report" 12 "github.com/amarpal/go-tools/knowledge" 13 "github.com/amarpal/go-tools/pattern" 14 "golang.org/x/tools/go/analysis" 15 "golang.org/x/tools/go/analysis/passes/inspect" 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.Documentation{ 25 Title: `Comparing \'runtime.GOOS\' or \'runtime.GOARCH\' against impossible value`, 26 Since: "Unreleased", 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 }