go/analysis/passes/buildtag: update check for //go:build

Part of //go:build change (#41184).
See https://golang.org/design/draft-gobuild

Report misplaced and malformed //go:build lines,
like we've always done for // +build lines.

Report when the // +build lines do not say the same thing as the //go:build line.
(Running gofmt will always bring them back in sync.)

For Go 1.17 vet.

Change-Id: Ifd22c6e8edcfeedc37851ba514712c5c057ad4b8
Reviewed-on: https://go-review.googlesource.com/c/tools/+/293834
Trust: Russ Cox <rsc@golang.org>
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go
index 5e99afe..8447244 100644
--- a/go/analysis/analysistest/analysistest.go
+++ b/go/analysis/analysistest/analysistest.go
@@ -354,13 +354,13 @@
 		// Any comment starting with "want" is treated
 		// as an expectation, even without following whitespace.
 		if rest := strings.TrimPrefix(text, "want"); rest != text {
-			expects, err := parseExpectations(rest)
+			lineDelta, expects, err := parseExpectations(rest)
 			if err != nil {
 				t.Errorf("%s:%d: in 'want' comment: %s", filename, linenum, err)
 				return
 			}
 			if expects != nil {
-				want[key{filename, linenum}] = expects
+				want[key{filename, linenum + lineDelta}] = expects
 			}
 		}
 	}
@@ -526,13 +526,13 @@
 // parseExpectations parses the content of a "// want ..." comment
 // and returns the expectations, a mixture of diagnostics ("rx") and
 // facts (name:"rx").
-func parseExpectations(text string) ([]expectation, error) {
+func parseExpectations(text string) (lineDelta int, expects []expectation, err error) {
 	var scanErr string
 	sc := new(scanner.Scanner).Init(strings.NewReader(text))
 	sc.Error = func(s *scanner.Scanner, msg string) {
 		scanErr = msg // e.g. bad string escape
 	}
-	sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings
+	sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanInts
 
 	scanRegexp := func(tok rune) (*regexp.Regexp, error) {
 		if tok != scanner.String && tok != scanner.RawString {
@@ -543,14 +543,19 @@
 		return regexp.Compile(pattern)
 	}
 
-	var expects []expectation
 	for {
 		tok := sc.Scan()
 		switch tok {
+		case '+':
+			tok = sc.Scan()
+			if tok != scanner.Int {
+				return 0, nil, fmt.Errorf("got +%s, want +Int", scanner.TokenString(tok))
+			}
+			lineDelta, _ = strconv.Atoi(sc.TokenText())
 		case scanner.String, scanner.RawString:
 			rx, err := scanRegexp(tok)
 			if err != nil {
-				return nil, err
+				return 0, nil, err
 			}
 			expects = append(expects, expectation{"diagnostic", "", rx})
 
@@ -558,24 +563,24 @@
 			name := sc.TokenText()
 			tok = sc.Scan()
 			if tok != ':' {
-				return nil, fmt.Errorf("got %s after %s, want ':'",
+				return 0, nil, fmt.Errorf("got %s after %s, want ':'",
 					scanner.TokenString(tok), name)
 			}
 			tok = sc.Scan()
 			rx, err := scanRegexp(tok)
 			if err != nil {
-				return nil, err
+				return 0, nil, err
 			}
 			expects = append(expects, expectation{"fact", name, rx})
 
 		case scanner.EOF:
 			if scanErr != "" {
-				return nil, fmt.Errorf("%s", scanErr)
+				return 0, nil, fmt.Errorf("%s", scanErr)
 			}
-			return expects, nil
+			return lineDelta, expects, nil
 
 		default:
-			return nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok))
+			return 0, nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok))
 		}
 	}
 }
diff --git a/go/analysis/passes/buildtag/buildtag.go b/go/analysis/passes/buildtag/buildtag.go
index 841b928..c4407ad 100644
--- a/go/analysis/passes/buildtag/buildtag.go
+++ b/go/analysis/passes/buildtag/buildtag.go
@@ -2,14 +2,17 @@
 // 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 (
-	"bytes"
-	"fmt"
 	"go/ast"
+	"go/build/constraint"
 	"go/parser"
+	"go/token"
 	"strings"
 	"unicode"
 
@@ -52,118 +55,313 @@
 }
 
 func checkGoFile(pass *analysis.Pass, f *ast.File) {
-	pastCutoff := false
+	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 {
-			pastCutoff = true
+			check.plusBuildOK = false
 		}
-
-		// "+build" is ignored within or after a /*...*/ comment.
-		if !strings.HasPrefix(group.List[0].Text, "//") {
-			pastCutoff = true
-			continue
+		// 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 {
-			if !strings.Contains(c.Text, "+build") {
-				continue
+			// "+build" is ignored within or after a /*...*/ comment.
+			if !strings.HasPrefix(c.Text, "//") {
+				check.plusBuildOK = false
 			}
-			if err := checkLine(c.Text, pastCutoff); err != nil {
-				pass.Reportf(c.Pos(), "%s", err)
-			}
+			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
 	}
 
-	// We must look at the raw lines, as build tags may appear in non-Go
-	// files such as assembly files.
-	lines := bytes.SplitAfter(content, nl)
+	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 cutoff int
-	for i, line := range lines {
-		line = bytes.TrimSpace(line)
-		if !bytes.HasPrefix(line, slashSlash) {
-			if len(line) > 0 {
-				break
+	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
 			}
-			cutoff = i
+			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
 		}
 	}
-
-	for i, line := range lines {
-		line = bytes.TrimSpace(line)
-		if !bytes.HasPrefix(line, slashSlash) {
-			continue
-		}
-		if !bytes.Contains(line, []byte("+build")) {
-			continue
-		}
-		if err := checkLine(string(line), i >= cutoff); err != nil {
-			pass.Reportf(analysisutil.LineStart(tf, i+1), "%s", err)
-			continue
-		}
-	}
-	return nil
 }
 
-// checkLine checks a line that starts with "//" and contains "+build".
-func checkLine(line string, pastCutoff bool) error {
-	line = strings.TrimPrefix(line, "//")
-	line = strings.TrimSpace(line)
+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
+		}
+	}
+}
 
-	if strings.HasPrefix(line, "+build") {
-		fields := strings.Fields(line)
-		if fields[0] != "+build" {
-			// Comment is something like +buildasdf not +build.
-			return fmt.Errorf("possible malformed +build comment")
+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)")
 		}
-		if pastCutoff {
-			return fmt.Errorf("+build comment must appear before package clause and be followed by a blank line")
-		}
-		if err := checkArguments(fields); err != nil {
-			return err
-		}
+		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 {
-		// Comment with +build but not at beginning.
-		if !pastCutoff {
-			return fmt.Errorf("possible malformed +build comment")
-		}
+		check.pass.Reportf(pos, "unexpected extra //go:build line")
+		check.crossCheck = false
 	}
-	return nil
+
+	// 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 checkArguments(fields []string) error {
+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, "!!") {
-				return fmt.Errorf("invalid double negative in build constraint: %s", arg)
+				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 != '.' {
-					return fmt.Errorf("invalid non-alphanumeric build constraint: %s", arg)
+					check.pass.Reportf(pos, "invalid non-alphanumeric build constraint: %s", arg)
+					check.crossCheck = false
+					break
 				}
 			}
 		}
 	}
-	return nil
+
+	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}
+		}
+	}
 }
 
-var (
-	nl         = []byte("\n")
-	slashSlash = []byte("//")
-)
+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
+	}
+}
diff --git a/go/analysis/passes/buildtag/buildtag_old.go b/go/analysis/passes/buildtag/buildtag_old.go
new file mode 100644
index 0000000..e923492
--- /dev/null
+++ b/go/analysis/passes/buildtag/buildtag_old.go
@@ -0,0 +1,174 @@
+// 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.
+
+// TODO(rsc): Delete this file once Go 1.17 comes out and we can retire Go 1.15 support.
+
+//go:build !go1.16
+// +build !go1.16
+
+// Package buildtag defines an Analyzer that checks build tags.
+package buildtag
+
+import (
+	"bytes"
+	"fmt"
+	"go/ast"
+	"go/parser"
+	"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) {
+	pastCutoff := false
+	for _, group := range f.Comments {
+		// A +build comment is ignored after or adjoining the package declaration.
+		if group.End()+1 >= f.Package {
+			pastCutoff = true
+		}
+
+		// "+build" is ignored within or after a /*...*/ comment.
+		if !strings.HasPrefix(group.List[0].Text, "//") {
+			pastCutoff = true
+			continue
+		}
+
+		// Check each line of a //-comment.
+		for _, c := range group.List {
+			if !strings.Contains(c.Text, "+build") {
+				continue
+			}
+			if err := checkLine(c.Text, pastCutoff); err != nil {
+				pass.Reportf(c.Pos(), "%s", err)
+			}
+		}
+	}
+}
+
+func checkOtherFile(pass *analysis.Pass, filename string) error {
+	content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
+	if err != nil {
+		return err
+	}
+
+	// We must look at the raw lines, as build tags may appear in non-Go
+	// files such as assembly files.
+	lines := bytes.SplitAfter(content, nl)
+
+	// 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 cutoff int
+	for i, line := range lines {
+		line = bytes.TrimSpace(line)
+		if !bytes.HasPrefix(line, slashSlash) {
+			if len(line) > 0 {
+				break
+			}
+			cutoff = i
+		}
+	}
+
+	for i, line := range lines {
+		line = bytes.TrimSpace(line)
+		if !bytes.HasPrefix(line, slashSlash) {
+			continue
+		}
+		if !bytes.Contains(line, []byte("+build")) {
+			continue
+		}
+		if err := checkLine(string(line), i >= cutoff); err != nil {
+			pass.Reportf(analysisutil.LineStart(tf, i+1), "%s", err)
+			continue
+		}
+	}
+	return nil
+}
+
+// checkLine checks a line that starts with "//" and contains "+build".
+func checkLine(line string, pastCutoff bool) error {
+	line = strings.TrimPrefix(line, "//")
+	line = strings.TrimSpace(line)
+
+	if strings.HasPrefix(line, "+build") {
+		fields := strings.Fields(line)
+		if fields[0] != "+build" {
+			// Comment is something like +buildasdf not +build.
+			return fmt.Errorf("possible malformed +build comment")
+		}
+		if pastCutoff {
+			return fmt.Errorf("+build comment must appear before package clause and be followed by a blank line")
+		}
+		if err := checkArguments(fields); err != nil {
+			return err
+		}
+	} else {
+		// Comment with +build but not at beginning.
+		if !pastCutoff {
+			return fmt.Errorf("possible malformed +build comment")
+		}
+	}
+	return nil
+}
+
+func checkArguments(fields []string) error {
+	for _, arg := range fields[1:] {
+		for _, elem := range strings.Split(arg, ",") {
+			if strings.HasPrefix(elem, "!!") {
+				return fmt.Errorf("invalid double negative in build constraint: %s", arg)
+			}
+			elem = strings.TrimPrefix(elem, "!")
+			for _, c := range elem {
+				if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
+					return fmt.Errorf("invalid non-alphanumeric build constraint: %s", arg)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+var (
+	nl         = []byte("\n")
+	slashSlash = []byte("//")
+)
diff --git a/go/analysis/passes/buildtag/buildtag_test.go b/go/analysis/passes/buildtag/buildtag_test.go
index 110343c..163e8e3 100644
--- a/go/analysis/passes/buildtag/buildtag_test.go
+++ b/go/analysis/passes/buildtag/buildtag_test.go
@@ -5,6 +5,8 @@
 package buildtag_test
 
 import (
+	"runtime"
+	"strings"
 	"testing"
 
 	"golang.org/x/tools/go/analysis"
@@ -13,6 +15,9 @@
 )
 
 func Test(t *testing.T) {
+	if strings.HasPrefix(runtime.Version(), "go1.") && runtime.Version() < "go1.16" {
+		t.Skipf("skipping on %v", runtime.Version())
+	}
 	analyzer := *buildtag.Analyzer
 	analyzer.Run = func(pass *analysis.Pass) (interface{}, error) {
 		defer func() {
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag.go b/go/analysis/passes/buildtag/testdata/src/a/buildtag.go
index dcc980c..5bc5d3c 100644
--- a/go/analysis/passes/buildtag/testdata/src/a/buildtag.go
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag.go
@@ -4,15 +4,19 @@
 
 // This file contains tests for the buildtag checker.
 
-// +builder // want `possible malformed \+build comment`
-// +build !ignore
+// want +1 `possible malformed \+build comment`
+// +builder
+// +build ignore
 
 // Mention +build // want `possible malformed \+build comment`
 
-// +build nospace // want "build comment must appear before package clause and be followed by a blank line"
+// want +1 `misplaced \+build comment`
+// +build nospace
+//go:build ok
 package a
 
-// +build toolate // want "build comment must appear before package clause and be followed by a blank line$"
+// want +1 `misplaced \+build comment`
+// +build toolate
 
 var _ = 3
 
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag2.go b/go/analysis/passes/buildtag/testdata/src/a/buildtag2.go
index 3b71ca5..453cbea 100644
--- a/go/analysis/passes/buildtag/testdata/src/a/buildtag2.go
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag2.go
@@ -6,4 +6,12 @@
 
 package a
 
-// +build toolate // want "build comment must appear before package clause and be followed by a blank line$"
+// want +1 `misplaced \+build comment`
+// +build toolate
+
+// want +1 `misplaced //go:build comment`
+//go:build toolate
+
+var _ = `
+// +build notacomment
+`
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag3.go b/go/analysis/passes/buildtag/testdata/src/a/buildtag3.go
new file mode 100644
index 0000000..0e81c49
--- /dev/null
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag3.go
@@ -0,0 +1,14 @@
+// Copyright 2020 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.
+
+// want +3 `[+]build lines do not match //go:build condition`
+
+//go:build good
+// +build bad
+
+package a
+
+var _ = `
+// +build notacomment
+`
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag4.go b/go/analysis/passes/buildtag/testdata/src/a/buildtag4.go
new file mode 100644
index 0000000..2651130
--- /dev/null
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag4.go
@@ -0,0 +1,9 @@
+// Copyright 2020 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 !(bad || worse)
+// +build !bad
+// +build !worse
+
+package a
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag5.go b/go/analysis/passes/buildtag/testdata/src/a/buildtag5.go
new file mode 100644
index 0000000..bd5e039
--- /dev/null
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag5.go
@@ -0,0 +1,11 @@
+// Copyright 2020 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 !(bad || worse)
+// +build !bad,!worse
+
+package a
+
+//want +1 `misplaced \+build comment`
+// +build other
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag6.s b/go/analysis/passes/buildtag/testdata/src/a/buildtag6.s
new file mode 100644
index 0000000..40fe14c
--- /dev/null
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag6.s
@@ -0,0 +1,9 @@
+// Copyright 2020 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.
+
+#include "go_asm.h"
+
+// ok because we cannot parse assembly files.
+// +build no
+
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag7.s b/go/analysis/passes/buildtag/testdata/src/a/buildtag7.s
new file mode 100644
index 0000000..b622d48
--- /dev/null
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag7.s
@@ -0,0 +1,11 @@
+// Copyright 2020 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.
+
+// +build ignore
+
+#include "go_asm.h"
+
+// ok because we cannot parse assembly files
+// the assembler would complain if we did assemble this file.
+//go:build no
diff --git a/go/analysis/passes/buildtag/testdata/src/a/buildtag8.s b/go/analysis/passes/buildtag/testdata/src/a/buildtag8.s
new file mode 100644
index 0000000..2f4edd3
--- /dev/null
+++ b/go/analysis/passes/buildtag/testdata/src/a/buildtag8.s
@@ -0,0 +1,14 @@
+// Copyright 2020 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.
+
+// want +3 `\+build lines do not match //go:build condition`
+
+//go:build something
+// +build ignore
+
+#include "go_asm.h"
+
+// ok because we cannot parse assembly files
+// the assembler would complain if we did assemble this file.
+//go:build no