// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.16 // +build go1.16 // Package buildtag defines an Analyzer that checks build tags. package buildtag import ( "go/ast" "go/build/constraint" "go/parser" "go/token" "strings" "unicode" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" ) const Doc = "check that +build tags are well-formed and correctly located" var Analyzer = &analysis.Analyzer{ Name: "buildtag", Doc: Doc, Run: runBuildTag, } func runBuildTag(pass *analysis.Pass) (interface{}, error) { for _, f := range pass.Files { checkGoFile(pass, f) } for _, name := range pass.OtherFiles { if err := checkOtherFile(pass, name); err != nil { return nil, err } } for _, name := range pass.IgnoredFiles { if strings.HasSuffix(name, ".go") { f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) if err != nil { // Not valid Go source code - not our job to diagnose, so ignore. return nil, nil } checkGoFile(pass, f) } else { if err := checkOtherFile(pass, name); err != nil { return nil, err } } } return nil, nil } func checkGoFile(pass *analysis.Pass, f *ast.File) { var check checker check.init(pass) defer check.finish() for _, group := range f.Comments { // A +build comment is ignored after or adjoining the package declaration. if group.End()+1 >= f.Package { check.plusBuildOK = false } // A //go:build comment is ignored after the package declaration // (but adjoining it is OK, in contrast to +build comments). if group.Pos() >= f.Package { check.goBuildOK = false } // Check each line of a //-comment. for _, c := range group.List { // "+build" is ignored within or after a /*...*/ comment. if !strings.HasPrefix(c.Text, "//") { check.plusBuildOK = false } check.comment(c.Slash, c.Text) } } } func checkOtherFile(pass *analysis.Pass, filename string) error { var check checker check.init(pass) defer check.finish() // We cannot use the Go parser, since this may not be a Go source file. // Read the raw bytes instead. content, tf, err := analysisutil.ReadFile(pass.Fset, filename) if err != nil { return err } check.file(token.Pos(tf.Base()), string(content)) return nil } type checker struct { pass *analysis.Pass plusBuildOK bool // "+build" lines still OK goBuildOK bool // "go:build" lines still OK crossCheck bool // cross-check go:build and +build lines when done reading file inStar bool // currently in a /* */ comment goBuildPos token.Pos // position of first go:build line found plusBuildPos token.Pos // position of first "+build" line found goBuild constraint.Expr // go:build constraint found plusBuild constraint.Expr // AND of +build constraints found } func (check *checker) init(pass *analysis.Pass) { check.pass = pass check.goBuildOK = true check.plusBuildOK = true check.crossCheck = true } func (check *checker) file(pos token.Pos, text string) { // Determine cutpoint where +build comments are no longer valid. // They are valid in leading // comments in the file followed by // a blank line. // // This must be done as a separate pass because of the // requirement that the comment be followed by a blank line. var plusBuildCutoff int fullText := text for text != "" { i := strings.Index(text, "\n") if i < 0 { i = len(text) } else { i++ } offset := len(fullText) - len(text) line := text[:i] text = text[i:] line = strings.TrimSpace(line) if !strings.HasPrefix(line, "//") && line != "" { break } if line == "" { plusBuildCutoff = offset } } // Process each line. // Must stop once we hit goBuildOK == false text = fullText check.inStar = false for text != "" { i := strings.Index(text, "\n") if i < 0 { i = len(text) } else { i++ } offset := len(fullText) - len(text) line := text[:i] text = text[i:] check.plusBuildOK = offset < plusBuildCutoff if strings.HasPrefix(line, "//") { check.comment(pos+token.Pos(offset), line) continue } // Keep looking for the point at which //go:build comments // stop being allowed. Skip over, cut out any /* */ comments. for { line = strings.TrimSpace(line) if check.inStar { i := strings.Index(line, "*/") if i < 0 { line = "" break } line = line[i+len("*/"):] check.inStar = false continue } if strings.HasPrefix(line, "/*") { check.inStar = true line = line[len("/*"):] continue } break } if line != "" { // Found non-comment non-blank line. // Ends space for valid //go:build comments, // but also ends the fraction of the file we can // reliably parse. From this point on we might // incorrectly flag "comments" inside multiline // string constants or anything else (this might // not even be a Go program). So stop. break } } } func (check *checker) comment(pos token.Pos, text string) { if strings.HasPrefix(text, "//") { if strings.Contains(text, "+build") { check.plusBuildLine(pos, text) } if strings.Contains(text, "//go:build") { check.goBuildLine(pos, text) } } if strings.HasPrefix(text, "/*") { if i := strings.Index(text, "\n"); i >= 0 { // multiline /* */ comment - process interior lines check.inStar = true i++ pos += token.Pos(i) text = text[i:] for text != "" { i := strings.Index(text, "\n") if i < 0 { i = len(text) } else { i++ } line := text[:i] if strings.HasPrefix(line, "//") { check.comment(pos, line) } pos += token.Pos(i) text = text[i:] } check.inStar = false } } } func (check *checker) goBuildLine(pos token.Pos, line string) { if !constraint.IsGoBuild(line) { if !strings.HasPrefix(line, "//go:build") && constraint.IsGoBuild("//"+strings.TrimSpace(line[len("//"):])) { check.pass.Reportf(pos, "malformed //go:build line (space between // and go:build)") } return } if !check.goBuildOK || check.inStar { check.pass.Reportf(pos, "misplaced //go:build comment") check.crossCheck = false return } if check.goBuildPos == token.NoPos { check.goBuildPos = pos } else { check.pass.Reportf(pos, "unexpected extra //go:build line") check.crossCheck = false } // testing hack: stop at // ERROR if i := strings.Index(line, " // ERROR "); i >= 0 { line = line[:i] } x, err := constraint.Parse(line) if err != nil { check.pass.Reportf(pos, "%v", err) check.crossCheck = false return } if check.goBuild == nil { check.goBuild = x } } func (check *checker) plusBuildLine(pos token.Pos, line string) { line = strings.TrimSpace(line) if !constraint.IsPlusBuild(line) { // Comment with +build but not at beginning. // Only report early in file. if check.plusBuildOK && !strings.HasPrefix(line, "// want") { check.pass.Reportf(pos, "possible malformed +build comment") } return } if !check.plusBuildOK { // inStar implies !plusBuildOK check.pass.Reportf(pos, "misplaced +build comment") check.crossCheck = false } if check.plusBuildPos == token.NoPos { check.plusBuildPos = pos } // testing hack: stop at // ERROR if i := strings.Index(line, " // ERROR "); i >= 0 { line = line[:i] } fields := strings.Fields(line[len("//"):]) // IsPlusBuildConstraint check above implies fields[0] == "+build" for _, arg := range fields[1:] { for _, elem := range strings.Split(arg, ",") { if strings.HasPrefix(elem, "!!") { check.pass.Reportf(pos, "invalid double negative in build constraint: %s", arg) check.crossCheck = false continue } elem = strings.TrimPrefix(elem, "!") for _, c := range elem { if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { check.pass.Reportf(pos, "invalid non-alphanumeric build constraint: %s", arg) check.crossCheck = false break } } } } if check.crossCheck { y, err := constraint.Parse(line) if err != nil { // Should never happen - constraint.Parse never rejects a // +build line. // Also, we just checked the syntax above. // Even so, report. check.pass.Reportf(pos, "%v", err) check.crossCheck = false return } if check.plusBuild == nil { check.plusBuild = y } else { check.plusBuild = &constraint.AndExpr{X: check.plusBuild, Y: y} } } } func (check *checker) finish() { if !check.crossCheck || check.plusBuildPos == token.NoPos || check.goBuildPos == token.NoPos { return } // Have both //go:build and // +build, // with no errors found (crossCheck still true). // Check they match. var want constraint.Expr lines, err := constraint.PlusBuildLines(check.goBuild) if err != nil { check.pass.Reportf(check.goBuildPos, "%v", err) return } for _, line := range lines { y, err := constraint.Parse(line) if err != nil { // Definitely should not happen, but not the user's fault. // Do not report. return } if want == nil { want = y } else { want = &constraint.AndExpr{X: want, Y: y} } } if want.String() != check.plusBuild.String() { check.pass.Reportf(check.plusBuildPos, "+build lines do not match //go:build condition") return } }