Improve performance of "Evaluate build tags as true and false" feature (#2058)

This PR provides an alternative way of implementing #1938 (i.e.
evaluating both `tag` and `!tag` as true when evaluating build
constraints).

The way it was implemented originally has exponential time complexity
with the number of provided build tags. This made gazelle unusable on
projects with a relatively large number of tags, as the run would never
end.

This implementation leverages the existing `dropNegationForIgnoredTags`
to manipulate the tag expression.

Fixes #1262 in a more efficient way than what #1938 implemented.
diff --git a/language/go/build_constraints.go b/language/go/build_constraints.go
index 8bae3ce..4d681aa 100644
--- a/language/go/build_constraints.go
+++ b/language/go/build_constraints.go
@@ -52,7 +52,7 @@
 			return nil, err
 		}
 
-		return newBuildTags(x)
+		return newBuildTags(x), nil
 	}
 
 	var fullConstraint constraint.Expr
@@ -88,7 +88,7 @@
 		return nil, nil
 	}
 
-	return newBuildTags(fullConstraint)
+	return newBuildTags(fullConstraint), nil
 }
 
 // buildTags represents the build tags specified in a file.
@@ -103,21 +103,14 @@
 
 // newBuildTags will return a new buildTags structure with any
 // ignored tags filtered out from the provided constraints.
-func newBuildTags(x constraint.Expr) (*buildTags, error) {
-	modified, err := dropNegationForIgnoredTags(pushNot(x, false))
-	if err != nil {
-		return nil, err
-	}
-
-	rawTags, err := collectTags(modified)
-	if err != nil {
-		return nil, err
-	}
+func newBuildTags(x constraint.Expr) *buildTags {
+	modified := dropNegationForIgnoredTags(pushNot(x, false), isDefaultIgnoredTag)
+	rawTags := collectTags(modified)
 
 	return &buildTags{
 		expr:    modified,
 		rawTags: rawTags,
-	}, nil
+	}
 }
 
 func (b *buildTags) tags() []string {
@@ -149,16 +142,16 @@
 // without having to worry that the result will be negated later on. Ignored tags should always
 // evaluate to true, regardless of whether they are negated or not leaving the final evaluation
 // to happen at compile time by the compiler.
-func dropNegationForIgnoredTags(expr constraint.Expr) (constraint.Expr, error) {
+func dropNegationForIgnoredTags(expr constraint.Expr, isIgnoredTag func(tag string) bool) constraint.Expr {
 	if expr == nil {
-		return nil, nil
+		return nil
 	}
 
 	switch x := expr.(type) {
 	case *constraint.TagExpr:
 		return &constraint.TagExpr{
 			Tag: x.Tag,
-		}, nil
+		}
 
 	case *constraint.NotExpr:
 		var toRet constraint.Expr
@@ -168,58 +161,40 @@
 				Tag: tag.Tag,
 			}
 		} else {
-			fixed, err := dropNegationForIgnoredTags(x.X)
-			if err != nil {
-				return nil, err
-			}
+			fixed := dropNegationForIgnoredTags(x.X, isIgnoredTag)
 			toRet = &constraint.NotExpr{X: fixed}
 		}
 
-		return toRet, nil
+		return toRet
 
 	case *constraint.AndExpr:
-		a, err := dropNegationForIgnoredTags(x.X)
-		if err != nil {
-			return nil, err
-		}
-
-		b, err := dropNegationForIgnoredTags(x.Y)
-		if err != nil {
-			return nil, err
-		}
+		a := dropNegationForIgnoredTags(x.X, isIgnoredTag)
+		b := dropNegationForIgnoredTags(x.Y, isIgnoredTag)
 
 		return &constraint.AndExpr{
 			X: a,
 			Y: b,
-		}, nil
+		}
 
 	case *constraint.OrExpr:
-		a, err := dropNegationForIgnoredTags(x.X)
-		if err != nil {
-			return nil, err
-		}
-
-		b, err := dropNegationForIgnoredTags(x.Y)
-		if err != nil {
-			return nil, err
-		}
-
+		a := dropNegationForIgnoredTags(x.X, isIgnoredTag)
+		b := dropNegationForIgnoredTags(x.Y, isIgnoredTag)
 		return &constraint.OrExpr{
 			X: a,
 			Y: b,
-		}, nil
+		}
 
 	default:
-		return nil, fmt.Errorf("unknown constraint type: %T", x)
+		panic(fmt.Errorf("unknown constraint type: %T", x))
 	}
 }
 
-// filterTags will traverse the provided constraint.Expr, recursively, and call
+// visitTags will traverse the provided constraint.Expr, recursively, and call
 // the user provided ok func on concrete constraint.TagExpr structures. If the provided
 // func returns true, the tag in question is kept, otherwise it is filtered out.
-func visitTags(expr constraint.Expr, visit func(string)) (err error) {
+func visitTags(expr constraint.Expr, visit func(string)) {
 	if expr == nil {
-		return nil
+		return
 	}
 
 	switch x := expr.(type) {
@@ -227,37 +202,29 @@
 		visit(x.Tag)
 
 	case *constraint.NotExpr:
-		err = visitTags(x.X, visit)
+		visitTags(x.X, visit)
 
 	case *constraint.AndExpr:
-		err = visitTags(x.X, visit)
-		if err == nil {
-			err = visitTags(x.Y, visit)
-		}
+		visitTags(x.X, visit)
+		visitTags(x.Y, visit)
 
 	case *constraint.OrExpr:
-		err = visitTags(x.X, visit)
-		if err == nil {
-			err = visitTags(x.Y, visit)
-		}
+		visitTags(x.X, visit)
+		visitTags(x.Y, visit)
 
 	default:
-		return fmt.Errorf("unknown constraint type: %T", x)
+		panic(fmt.Errorf("unknown constraint type: %T", x))
 	}
 
 	return
 }
 
-func collectTags(expr constraint.Expr) ([]string, error) {
+func collectTags(expr constraint.Expr) []string {
 	var tags []string
-	err := visitTags(expr, func(tag string) {
+	visitTags(expr, func(tag string) {
 		tags = append(tags, tag)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return tags, err
+	return tags
 }
 
 // cgoTagsAndOpts contains compile or link options which should only be applied
@@ -304,15 +271,15 @@
 		return nil, err
 	}
 
-	return newBuildTags(x)
+	return newBuildTags(x), nil
 }
 
-// isIgnoredTag returns whether the tag is "cgo", "purego", "race", "msan"  or is a release tag.
+// isDefaultIgnoredTag returns whether the tag is "cgo", "purego", "race", "msan"  or is a release tag.
 // Release tags match the pattern "go[0-9]\.[0-9]+".
 // Gazelle won't consider whether an ignored tag is satisfied when evaluating
 // build constraints for a file and will instead defer to the compiler at compile
 // time.
-func isIgnoredTag(tag string) bool {
+func isDefaultIgnoredTag(tag string) bool {
 	if tag == "cgo" || tag == "purego" || tag == "race" || tag == "msan" {
 		return true
 	}
diff --git a/language/go/build_constraints_test.go b/language/go/build_constraints_test.go
index fc31182..680b7ab 100644
--- a/language/go/build_constraints_test.go
+++ b/language/go/build_constraints_test.go
@@ -60,10 +60,7 @@
 		},
 	} {
 		t.Run(tc.desc, func(t *testing.T) {
-			bt, err := newBuildTags(tc.input)
-			if err != nil {
-				t.Fatal(err)
-			}
+			bt := newBuildTags(tc.input)
 			if diff := cmp.Diff(tc.want, bt.expr); diff != "" {
 				t.Errorf("(-want, +got): %s", diff)
 			}
diff --git a/language/go/config.go b/language/go/config.go
index 89ac3f4..ac7f089 100644
--- a/language/go/config.go
+++ b/language/go/config.go
@@ -41,16 +41,6 @@
 
 var minimumRulesGoVersion = version.Version{0, 29, 0}
 
-type tagSet map[string]struct{}
-
-func (ts tagSet) clone() tagSet {
-	c := make(tagSet, len(ts))
-	for k, v := range ts {
-		c[k] = v
-	}
-	return c
-}
-
 // goConfig contains configuration values related to Go rules.
 type goConfig struct {
 	// The name under which the rules_go repository can be referenced from the
@@ -63,7 +53,7 @@
 
 	// genericTags is a set of tags that Gazelle considers to be true. Set with
 	// -build_tags or # gazelle:build_tags. Some tags, like gc, are always on.
-	genericTags []tagSet
+	genericTags map[string]bool
 
 	// prefix is a prefix of an import path, used to generate importpath
 	// attributes. Set with -go_prefix or # gazelle:prefix.
@@ -188,10 +178,12 @@
 		goProtoCompilers: defaultGoProtoCompilers,
 		goGrpcCompilers:  defaultGoGrpcCompilers,
 		goGenerateProto:  true,
-		genericTags: []tagSet{
-			{"gc": struct{}{}},
-		},
 	}
+	if gc.genericTags == nil {
+		gc.genericTags = make(map[string]bool)
+	}
+	// Add default tags
+	gc.genericTags["gc"] = true
 	return gc
 }
 
@@ -201,9 +193,9 @@
 
 func (gc *goConfig) clone() *goConfig {
 	gcCopy := *gc
-	gcCopy.genericTags = make([]tagSet, 0, len(gc.genericTags))
-	for _, ts := range gc.genericTags {
-		gcCopy.genericTags = append(gcCopy.genericTags, ts.clone())
+	gcCopy.genericTags = make(map[string]bool)
+	for k, v := range gc.genericTags {
+		gcCopy.genericTags[k] = v
 	}
 	gcCopy.goProtoCompilers = gc.goProtoCompilers[:len(gc.goProtoCompilers):len(gc.goProtoCompilers)]
 	gcCopy.goGrpcCompilers = gc.goGrpcCompilers[:len(gc.goGrpcCompilers):len(gc.goGrpcCompilers)]
@@ -221,13 +213,7 @@
 		if strings.HasPrefix(t, "!") {
 			return fmt.Errorf("build tags can't be negated: %s", t)
 		}
-		var newSets []tagSet
-		for _, ts := range gc.genericTags {
-			c := ts.clone()
-			c[t] = struct{}{}
-			newSets = append(newSets, c)
-		}
-		gc.genericTags = append(gc.genericTags, newSets...)
+		gc.genericTags[t] = true
 	}
 	return nil
 }
@@ -594,6 +580,7 @@
 			case "build_tags":
 				if err := gc.setBuildTags(d.Value); err != nil {
 					log.Print(err)
+					continue
 				}
 
 			case "go_generate_proto":
diff --git a/language/go/config_test.go b/language/go/config_test.go
index 3ac54d8..e239c11 100644
--- a/language/go/config_test.go
+++ b/language/go/config_test.go
@@ -59,21 +59,6 @@
 	return c, langs, cexts
 }
 
-func newTagSet(tags ...string) tagSet {
-	ts := make(tagSet)
-	for _, t := range tags {
-		ts[t] = struct{}{}
-	}
-	return ts
-}
-
-var expectedBuildTags = []tagSet{
-	newTagSet("gc"),
-	newTagSet("gc", "foo"),
-	newTagSet("gc", "bar"),
-	newTagSet("gc", "foo", "bar"),
-}
-
 func TestCommandLine(t *testing.T) {
 	c, _, _ := testConfig(
 		t,
@@ -83,8 +68,10 @@
 		"-external=vendored",
 		"-repo_root=.")
 	gc := getGoConfig(c)
-	if diff := cmp.Diff(expectedBuildTags, gc.genericTags); diff != "" {
-		t.Errorf("(-want, +got): %s", diff)
+	for _, tag := range []string{"foo", "bar", "gc"} {
+		if !gc.genericTags[tag] {
+			t.Errorf("expected tag %q to be set", tag)
+		}
 	}
 	if gc.prefix != "example.com/repo" {
 		t.Errorf(`got prefix %q; want "example.com/repo"`, gc.prefix)
@@ -114,8 +101,10 @@
 		cext.Configure(c, "test", f)
 	}
 	gc := getGoConfig(c)
-	if diff := cmp.Diff(expectedBuildTags, gc.genericTags); diff != "" {
-		t.Errorf("(-want, +got): %s", diff)
+	for _, tag := range []string{"foo", "bar", "gc"} {
+		if !gc.genericTags[tag] {
+			t.Errorf("expected tag %q to be set", tag)
+		}
 	}
 	if gc.prefix != "y" {
 		t.Errorf(`got prefix %q; want "y"`, gc.prefix)
diff --git a/language/go/fileinfo.go b/language/go/fileinfo.go
index 1d5d7c9..a4c75f9 100644
--- a/language/go/fileinfo.go
+++ b/language/go/fileinfo.go
@@ -569,8 +569,19 @@
 	}
 
 	goConf := getGoConfig(c)
-	checker := func(tag string, ts tagSet) bool {
-		if isIgnoredTag(tag) {
+
+	if tags != nil {
+		// Treat provided generic tags as "ignored tags", meaning that both
+		// `tag` and `!tag` are considered true when evaluating build constraints
+		isIgnoredTag := func(tag string) bool {
+			return goConf.genericTags[tag]
+		}
+
+		tags = newBuildTags(dropNegationForIgnoredTags(tags.expr, isIgnoredTag))
+	}
+
+	checker := func(tag string) bool {
+		if isDefaultIgnoredTag(tag) {
 			return true
 		}
 		if _, ok := rule.KnownOSSet[tag]; ok || tag == "unix" {
@@ -585,19 +596,13 @@
 				return false
 			}
 			return arch == tag
+
 		}
 
-		_, ok := ts[tag]
-		return ok
+		return goConf.genericTags[tag]
 	}
 
-	for _, ts := range goConf.genericTags {
-		c := func(tag string) bool { return checker(tag, ts) }
-		if tags.eval(c) && cgoTags.eval(c) {
-			return true
-		}
-	}
-	return false
+	return tags.eval(checker) && cgoTags.eval(checker)
 }
 
 // rulesGoSupportsOS returns whether the os tag is recognized by the version of
diff --git a/language/go/fileinfo_test.go b/language/go/fileinfo_test.go
index 6c89ac6..9aac4af 100644
--- a/language/go/fileinfo_test.go
+++ b/language/go/fileinfo_test.go
@@ -386,7 +386,7 @@
 	defer os.RemoveAll(dir)
 	for _, tc := range []struct {
 		desc                        string
-		genericTags                 string
+		genericTags                 map[string]bool
 		os, arch, filename, content string
 		want                        bool
 	}{
@@ -466,12 +466,12 @@
 			want:     false,
 		}, {
 			desc:        "tags all satisfied",
-			genericTags: "a,b",
+			genericTags: map[string]bool{"a": true, "b": true},
 			content:     "// +build a,b\n\npackage foo",
 			want:        true,
 		}, {
 			desc:        "tags some satisfied",
-			genericTags: "a",
+			genericTags: map[string]bool{"a": true},
 			content:     "// +build a,b\n\npackage foo",
 			want:        false,
 		}, {
@@ -480,7 +480,7 @@
 			want:    true,
 		}, {
 			desc:        "tag satisfied negated",
-			genericTags: "a",
+			genericTags: map[string]bool{"a": true},
 			content:     "// +build !a\n\npackage foo",
 			want:        true,
 		}, {
@@ -489,27 +489,27 @@
 			want:    false,
 		}, {
 			desc:        "tag group and satisfied",
-			genericTags: "foo,bar",
+			genericTags: map[string]bool{"foo": true, "bar": true},
 			content:     "// +build foo,bar\n\npackage foo",
 			want:        true,
 		}, {
 			desc:        "tag group and unsatisfied",
-			genericTags: "foo",
+			genericTags: map[string]bool{"foo": true},
 			content:     "// +build foo,bar\n\npackage foo",
 			want:        false,
 		}, {
 			desc:        "tag line or satisfied",
-			genericTags: "foo",
+			genericTags: map[string]bool{"foo": true},
 			content:     "// +build foo bar\n\npackage foo",
 			want:        true,
 		}, {
 			desc:        "tag line or unsatisfied",
-			genericTags: "foo",
+			genericTags: map[string]bool{"foo": true},
 			content:     "// +build !foo bar\n\npackage foo",
 			want:        true,
 		}, {
 			desc:        "tag lines and satisfied",
-			genericTags: "foo,bar",
+			genericTags: map[string]bool{"foo": true, "bar": true},
 			content: `
 // +build foo
 // +build bar
@@ -518,7 +518,7 @@
 			want: true,
 		}, {
 			desc:        "tag lines and unsatisfied",
-			genericTags: "foo",
+			genericTags: map[string]bool{"foo": true},
 			content: `
 // +build foo
 // +build bar
@@ -528,7 +528,7 @@
 		}, {
 			desc:        "cgo tags satisfied",
 			os:          "linux",
-			genericTags: "foo",
+			genericTags: map[string]bool{"foo": true},
 			content: `
 // +build foo
 
@@ -581,8 +581,9 @@
 		t.Run(tc.desc, func(t *testing.T) {
 			c, _, _ := testConfig(t)
 			gc := getGoConfig(c)
-			if err := gc.setBuildTags(tc.genericTags); err != nil {
-				t.Errorf("error setting build tags %q", tc.genericTags)
+			gc.genericTags = tc.genericTags
+			if gc.genericTags == nil {
+				gc.genericTags = map[string]bool{"gc": true}
 			}
 			filename := tc.filename
 			if filename == "" {