--- /dev/null
+// 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
+ }
+}