1 | // Copyright 2013 The Go Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style |
3 | // license that can be found in the LICENSE file. |
4 | |
5 | //go:build go1.16 |
6 | // +build go1.16 |
7 | |
8 | // Package buildtag defines an Analyzer that checks build tags. |
9 | package buildtag |
10 | |
11 | import ( |
12 | "go/ast" |
13 | "go/build/constraint" |
14 | "go/parser" |
15 | "go/token" |
16 | "strings" |
17 | "unicode" |
18 | |
19 | "golang.org/x/tools/go/analysis" |
20 | "golang.org/x/tools/go/analysis/passes/internal/analysisutil" |
21 | ) |
22 | |
23 | const Doc = "check that +build tags are well-formed and correctly located" |
24 | |
25 | var Analyzer = &analysis.Analyzer{ |
26 | Name: "buildtag", |
27 | Doc: Doc, |
28 | Run: runBuildTag, |
29 | } |
30 | |
31 | func runBuildTag(pass *analysis.Pass) (interface{}, error) { |
32 | for _, f := range pass.Files { |
33 | checkGoFile(pass, f) |
34 | } |
35 | for _, name := range pass.OtherFiles { |
36 | if err := checkOtherFile(pass, name); err != nil { |
37 | return nil, err |
38 | } |
39 | } |
40 | for _, name := range pass.IgnoredFiles { |
41 | if strings.HasSuffix(name, ".go") { |
42 | f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) |
43 | if err != nil { |
44 | // Not valid Go source code - not our job to diagnose, so ignore. |
45 | return nil, nil |
46 | } |
47 | checkGoFile(pass, f) |
48 | } else { |
49 | if err := checkOtherFile(pass, name); err != nil { |
50 | return nil, err |
51 | } |
52 | } |
53 | } |
54 | return nil, nil |
55 | } |
56 | |
57 | func checkGoFile(pass *analysis.Pass, f *ast.File) { |
58 | var check checker |
59 | check.init(pass) |
60 | defer check.finish() |
61 | |
62 | for _, group := range f.Comments { |
63 | // A +build comment is ignored after or adjoining the package declaration. |
64 | if group.End()+1 >= f.Package { |
65 | check.plusBuildOK = false |
66 | } |
67 | // A //go:build comment is ignored after the package declaration |
68 | // (but adjoining it is OK, in contrast to +build comments). |
69 | if group.Pos() >= f.Package { |
70 | check.goBuildOK = false |
71 | } |
72 | |
73 | // Check each line of a //-comment. |
74 | for _, c := range group.List { |
75 | // "+build" is ignored within or after a /*...*/ comment. |
76 | if !strings.HasPrefix(c.Text, "//") { |
77 | check.plusBuildOK = false |
78 | } |
79 | check.comment(c.Slash, c.Text) |
80 | } |
81 | } |
82 | } |
83 | |
84 | func checkOtherFile(pass *analysis.Pass, filename string) error { |
85 | var check checker |
86 | check.init(pass) |
87 | defer check.finish() |
88 | |
89 | // We cannot use the Go parser, since this may not be a Go source file. |
90 | // Read the raw bytes instead. |
91 | content, tf, err := analysisutil.ReadFile(pass.Fset, filename) |
92 | if err != nil { |
93 | return err |
94 | } |
95 | |
96 | check.file(token.Pos(tf.Base()), string(content)) |
97 | return nil |
98 | } |
99 | |
100 | type checker struct { |
101 | pass *analysis.Pass |
102 | plusBuildOK bool // "+build" lines still OK |
103 | goBuildOK bool // "go:build" lines still OK |
104 | crossCheck bool // cross-check go:build and +build lines when done reading file |
105 | inStar bool // currently in a /* */ comment |
106 | goBuildPos token.Pos // position of first go:build line found |
107 | plusBuildPos token.Pos // position of first "+build" line found |
108 | goBuild constraint.Expr // go:build constraint found |
109 | plusBuild constraint.Expr // AND of +build constraints found |
110 | } |
111 | |
112 | func (check *checker) init(pass *analysis.Pass) { |
113 | check.pass = pass |
114 | check.goBuildOK = true |
115 | check.plusBuildOK = true |
116 | check.crossCheck = true |
117 | } |
118 | |
119 | func (check *checker) file(pos token.Pos, text string) { |
120 | // Determine cutpoint where +build comments are no longer valid. |
121 | // They are valid in leading // comments in the file followed by |
122 | // a blank line. |
123 | // |
124 | // This must be done as a separate pass because of the |
125 | // requirement that the comment be followed by a blank line. |
126 | var plusBuildCutoff int |
127 | fullText := text |
128 | for text != "" { |
129 | i := strings.Index(text, "\n") |
130 | if i < 0 { |
131 | i = len(text) |
132 | } else { |
133 | i++ |
134 | } |
135 | offset := len(fullText) - len(text) |
136 | line := text[:i] |
137 | text = text[i:] |
138 | line = strings.TrimSpace(line) |
139 | if !strings.HasPrefix(line, "//") && line != "" { |
140 | break |
141 | } |
142 | if line == "" { |
143 | plusBuildCutoff = offset |
144 | } |
145 | } |
146 | |
147 | // Process each line. |
148 | // Must stop once we hit goBuildOK == false |
149 | text = fullText |
150 | check.inStar = false |
151 | for text != "" { |
152 | i := strings.Index(text, "\n") |
153 | if i < 0 { |
154 | i = len(text) |
155 | } else { |
156 | i++ |
157 | } |
158 | offset := len(fullText) - len(text) |
159 | line := text[:i] |
160 | text = text[i:] |
161 | check.plusBuildOK = offset < plusBuildCutoff |
162 | |
163 | if strings.HasPrefix(line, "//") { |
164 | check.comment(pos+token.Pos(offset), line) |
165 | continue |
166 | } |
167 | |
168 | // Keep looking for the point at which //go:build comments |
169 | // stop being allowed. Skip over, cut out any /* */ comments. |
170 | for { |
171 | line = strings.TrimSpace(line) |
172 | if check.inStar { |
173 | i := strings.Index(line, "*/") |
174 | if i < 0 { |
175 | line = "" |
176 | break |
177 | } |
178 | line = line[i+len("*/"):] |
179 | check.inStar = false |
180 | continue |
181 | } |
182 | if strings.HasPrefix(line, "/*") { |
183 | check.inStar = true |
184 | line = line[len("/*"):] |
185 | continue |
186 | } |
187 | break |
188 | } |
189 | if line != "" { |
190 | // Found non-comment non-blank line. |
191 | // Ends space for valid //go:build comments, |
192 | // but also ends the fraction of the file we can |
193 | // reliably parse. From this point on we might |
194 | // incorrectly flag "comments" inside multiline |
195 | // string constants or anything else (this might |
196 | // not even be a Go program). So stop. |
197 | break |
198 | } |
199 | } |
200 | } |
201 | |
202 | func (check *checker) comment(pos token.Pos, text string) { |
203 | if strings.HasPrefix(text, "//") { |
204 | if strings.Contains(text, "+build") { |
205 | check.plusBuildLine(pos, text) |
206 | } |
207 | if strings.Contains(text, "//go:build") { |
208 | check.goBuildLine(pos, text) |
209 | } |
210 | } |
211 | if strings.HasPrefix(text, "/*") { |
212 | if i := strings.Index(text, "\n"); i >= 0 { |
213 | // multiline /* */ comment - process interior lines |
214 | check.inStar = true |
215 | i++ |
216 | pos += token.Pos(i) |
217 | text = text[i:] |
218 | for text != "" { |
219 | i := strings.Index(text, "\n") |
220 | if i < 0 { |
221 | i = len(text) |
222 | } else { |
223 | i++ |
224 | } |
225 | line := text[:i] |
226 | if strings.HasPrefix(line, "//") { |
227 | check.comment(pos, line) |
228 | } |
229 | pos += token.Pos(i) |
230 | text = text[i:] |
231 | } |
232 | check.inStar = false |
233 | } |
234 | } |
235 | } |
236 | |
237 | func (check *checker) goBuildLine(pos token.Pos, line string) { |
238 | if !constraint.IsGoBuild(line) { |
239 | if !strings.HasPrefix(line, "//go:build") && constraint.IsGoBuild("//"+strings.TrimSpace(line[len("//"):])) { |
240 | check.pass.Reportf(pos, "malformed //go:build line (space between // and go:build)") |
241 | } |
242 | return |
243 | } |
244 | if !check.goBuildOK || check.inStar { |
245 | check.pass.Reportf(pos, "misplaced //go:build comment") |
246 | check.crossCheck = false |
247 | return |
248 | } |
249 | |
250 | if check.goBuildPos == token.NoPos { |
251 | check.goBuildPos = pos |
252 | } else { |
253 | check.pass.Reportf(pos, "unexpected extra //go:build line") |
254 | check.crossCheck = false |
255 | } |
256 | |
257 | // testing hack: stop at // ERROR |
258 | if i := strings.Index(line, " // ERROR "); i >= 0 { |
259 | line = line[:i] |
260 | } |
261 | |
262 | x, err := constraint.Parse(line) |
263 | if err != nil { |
264 | check.pass.Reportf(pos, "%v", err) |
265 | check.crossCheck = false |
266 | return |
267 | } |
268 | |
269 | if check.goBuild == nil { |
270 | check.goBuild = x |
271 | } |
272 | } |
273 | |
274 | func (check *checker) plusBuildLine(pos token.Pos, line string) { |
275 | line = strings.TrimSpace(line) |
276 | if !constraint.IsPlusBuild(line) { |
277 | // Comment with +build but not at beginning. |
278 | // Only report early in file. |
279 | if check.plusBuildOK && !strings.HasPrefix(line, "// want") { |
280 | check.pass.Reportf(pos, "possible malformed +build comment") |
281 | } |
282 | return |
283 | } |
284 | if !check.plusBuildOK { // inStar implies !plusBuildOK |
285 | check.pass.Reportf(pos, "misplaced +build comment") |
286 | check.crossCheck = false |
287 | } |
288 | |
289 | if check.plusBuildPos == token.NoPos { |
290 | check.plusBuildPos = pos |
291 | } |
292 | |
293 | // testing hack: stop at // ERROR |
294 | if i := strings.Index(line, " // ERROR "); i >= 0 { |
295 | line = line[:i] |
296 | } |
297 | |
298 | fields := strings.Fields(line[len("//"):]) |
299 | // IsPlusBuildConstraint check above implies fields[0] == "+build" |
300 | for _, arg := range fields[1:] { |
301 | for _, elem := range strings.Split(arg, ",") { |
302 | if strings.HasPrefix(elem, "!!") { |
303 | check.pass.Reportf(pos, "invalid double negative in build constraint: %s", arg) |
304 | check.crossCheck = false |
305 | continue |
306 | } |
307 | elem = strings.TrimPrefix(elem, "!") |
308 | for _, c := range elem { |
309 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { |
310 | check.pass.Reportf(pos, "invalid non-alphanumeric build constraint: %s", arg) |
311 | check.crossCheck = false |
312 | break |
313 | } |
314 | } |
315 | } |
316 | } |
317 | |
318 | if check.crossCheck { |
319 | y, err := constraint.Parse(line) |
320 | if err != nil { |
321 | // Should never happen - constraint.Parse never rejects a // +build line. |
322 | // Also, we just checked the syntax above. |
323 | // Even so, report. |
324 | check.pass.Reportf(pos, "%v", err) |
325 | check.crossCheck = false |
326 | return |
327 | } |
328 | if check.plusBuild == nil { |
329 | check.plusBuild = y |
330 | } else { |
331 | check.plusBuild = &constraint.AndExpr{X: check.plusBuild, Y: y} |
332 | } |
333 | } |
334 | } |
335 | |
336 | func (check *checker) finish() { |
337 | if !check.crossCheck || check.plusBuildPos == token.NoPos || check.goBuildPos == token.NoPos { |
338 | return |
339 | } |
340 | |
341 | // Have both //go:build and // +build, |
342 | // with no errors found (crossCheck still true). |
343 | // Check they match. |
344 | var want constraint.Expr |
345 | lines, err := constraint.PlusBuildLines(check.goBuild) |
346 | if err != nil { |
347 | check.pass.Reportf(check.goBuildPos, "%v", err) |
348 | return |
349 | } |
350 | for _, line := range lines { |
351 | y, err := constraint.Parse(line) |
352 | if err != nil { |
353 | // Definitely should not happen, but not the user's fault. |
354 | // Do not report. |
355 | return |
356 | } |
357 | if want == nil { |
358 | want = y |
359 | } else { |
360 | want = &constraint.AndExpr{X: want, Y: y} |
361 | } |
362 | } |
363 | if want.String() != check.plusBuild.String() { |
364 | check.pass.Reportf(check.plusBuildPos, "+build lines do not match //go:build condition") |
365 | return |
366 | } |
367 | } |
368 |
Members