1
2
3
4
5
6
7
8
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
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
64 if group.End()+1 >= f.Package {
65 check.plusBuildOK = false
66 }
67
68
69 if group.Pos() >= f.Package {
70 check.goBuildOK = false
71 }
72
73
74 for _, c := range group.List {
75
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
90
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
103 goBuildOK bool
104 crossCheck bool
105 inStar bool
106 goBuildPos token.Pos
107 plusBuildPos token.Pos
108 goBuild constraint.Expr
109 plusBuild constraint.Expr
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
121
122
123
124
125
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
148
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
169
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
191
192
193
194
195
196
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
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
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
278
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 {
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
294 if i := strings.Index(line, " // ERROR "); i >= 0 {
295 line = line[:i]
296 }
297
298 fields := strings.Fields(line[len("//"):])
299
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
322
323
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
342
343
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
354
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
View as plain text