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.
9package buildtag
10
11import (
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
23const Doc = "check that +build tags are well-formed and correctly located"
24
25var Analyzer = &analysis.Analyzer{
26	Name: "buildtag",
27	Doc:  Doc,
28	Run:  runBuildTag,
29}
30
31func 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
57func 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
84func 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
100type 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
112func (check *checker) init(pass *analysis.Pass) {
113	check.pass = pass
114	check.goBuildOK = true
115	check.plusBuildOK = true
116	check.crossCheck = true
117}
118
119func (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
202func (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
237func (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
274func (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
336func (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