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.
8 // Package buildtag defines an Analyzer that checks build tags.
19 "golang.org/x/tools/go/analysis"
20 "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
23 const Doc = "check that +build tags are well-formed and correctly located"
25 var Analyzer = &analysis.Analyzer{
31 func runBuildTag(pass *analysis.Pass) (interface{}, error) {
32 for _, f := range pass.Files {
35 for _, name := range pass.OtherFiles {
36 if err := checkOtherFile(pass, name); err != nil {
40 for _, name := range pass.IgnoredFiles {
41 if strings.HasSuffix(name, ".go") {
42 f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments)
44 // Not valid Go source code - not our job to diagnose, so ignore.
49 if err := checkOtherFile(pass, name); err != nil {
57 func checkGoFile(pass *analysis.Pass, f *ast.File) {
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
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
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
79 check.comment(c.Slash, c.Text)
84 func checkOtherFile(pass *analysis.Pass, filename string) error {
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)
96 check.file(token.Pos(tf.Base()), string(content))
100 type checker struct {
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
112 func (check *checker) init(pass *analysis.Pass) {
114 check.goBuildOK = true
115 check.plusBuildOK = true
116 check.crossCheck = true
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
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
129 i := strings.Index(text, "\n")
135 offset := len(fullText) - len(text)
138 line = strings.TrimSpace(line)
139 if !strings.HasPrefix(line, "//") && line != "" {
143 plusBuildCutoff = offset
147 // Process each line.
148 // Must stop once we hit goBuildOK == false
152 i := strings.Index(text, "\n")
158 offset := len(fullText) - len(text)
161 check.plusBuildOK = offset < plusBuildCutoff
163 if strings.HasPrefix(line, "//") {
164 check.comment(pos+token.Pos(offset), line)
168 // Keep looking for the point at which //go:build comments
169 // stop being allowed. Skip over, cut out any /* */ comments.
171 line = strings.TrimSpace(line)
173 i := strings.Index(line, "*/")
178 line = line[i+len("*/"):]
182 if strings.HasPrefix(line, "/*") {
184 line = line[len("/*"):]
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.
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)
207 if strings.Contains(text, "//go:build") {
208 check.goBuildLine(pos, text)
211 if strings.HasPrefix(text, "/*") {
212 if i := strings.Index(text, "\n"); i >= 0 {
213 // multiline /* */ comment - process interior lines
219 i := strings.Index(text, "\n")
226 if strings.HasPrefix(line, "//") {
227 check.comment(pos, line)
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)")
244 if !check.goBuildOK || check.inStar {
245 check.pass.Reportf(pos, "misplaced //go:build comment")
246 check.crossCheck = false
250 if check.goBuildPos == token.NoPos {
251 check.goBuildPos = pos
253 check.pass.Reportf(pos, "unexpected extra //go:build line")
254 check.crossCheck = false
257 // testing hack: stop at // ERROR
258 if i := strings.Index(line, " // ERROR "); i >= 0 {
262 x, err := constraint.Parse(line)
264 check.pass.Reportf(pos, "%v", err)
265 check.crossCheck = false
269 if check.goBuild == nil {
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")
284 if !check.plusBuildOK { // inStar implies !plusBuildOK
285 check.pass.Reportf(pos, "misplaced +build comment")
286 check.crossCheck = false
289 if check.plusBuildPos == token.NoPos {
290 check.plusBuildPos = pos
293 // testing hack: stop at // ERROR
294 if i := strings.Index(line, " // ERROR "); i >= 0 {
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
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
318 if check.crossCheck {
319 y, err := constraint.Parse(line)
321 // Should never happen - constraint.Parse never rejects a // +build line.
322 // Also, we just checked the syntax above.
324 check.pass.Reportf(pos, "%v", err)
325 check.crossCheck = false
328 if check.plusBuild == nil {
331 check.plusBuild = &constraint.AndExpr{X: check.plusBuild, Y: y}
336 func (check *checker) finish() {
337 if !check.crossCheck || check.plusBuildPos == token.NoPos || check.goBuildPos == token.NoPos {
341 // Have both //go:build and // +build,
342 // with no errors found (crossCheck still true).
344 var want constraint.Expr
345 lines, err := constraint.PlusBuildLines(check.goBuild)
347 check.pass.Reportf(check.goBuildPos, "%v", err)
350 for _, line := range lines {
351 y, err := constraint.Parse(line)
353 // Definitely should not happen, but not the user's fault.
360 want = &constraint.AndExpr{X: want, Y: y}
363 if want.String() != check.plusBuild.String() {
364 check.pass.Reportf(check.plusBuildPos, "+build lines do not match //go:build condition")