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