Stop using select in srcs (#152)

Platform-specfic srcs, separated with select expressions, have caused
a few problems. If the goos and goarch attributes of go_binary are
used, the wrong set of sources may be compiled. rules like go_path
will get filtered lists of sources without any way to get the
unfiltered list.

Separating sources is largely unnecessary, since rules_go also filters
sources at execution time. Therefore, this separation is being removed
by Gazelle.

* Rules generated by gazelle will now have flat srcs lists.
* Existing srcs lists will be flattened by "gazelle update" before
  merging. Sources that appear multiple times will be de-duplicated,
  and comments will be consolidated.

Note that Gazelle still filters sources, it just doesn't generate select
expresions for them anymore. Unbuildable sources (e.g., with
+build ignore) will not appear in any list. Dependencies and flags
will still be platform-specific.

Fixes #134
diff --git a/cmd/gazelle/integration_test.go b/cmd/gazelle/integration_test.go
index f9af161..cff354b 100644
--- a/cmd/gazelle/integration_test.go
+++ b/cmd/gazelle/integration_test.go
@@ -159,10 +159,10 @@
     name = "go_default_library",
     srcs = select({
         "@io_bazel_rules_go//go/platform:linux": [
-						# top comment
-						"foo.go",  # side comment
-						# bar comment
-						"bar.go",
+            # foo comment
+            "foo.go",  # side comment
+            # bar comment
+            "bar.go",
         ],
         "//conditions:default": [],
     }),
@@ -198,15 +198,12 @@
 
 go_library(
     name = "go_default_library",
-    srcs = select({
-        "@io_bazel_rules_go//go/platform:linux": [
-            # top comment
-            # bar comment
-            "bar.go",
-            "foo.go",  # side comment
-        ],
-        "//conditions:default": [],
-    }),
+    srcs = [
+        # bar comment
+        "bar.go",
+        # foo comment
+        "foo.go",  # side comment
+    ],
     importpath = "example.com/foo",
     visibility = ["//visibility:public"],
     deps = select({
diff --git a/internal/merger/fix.go b/internal/merger/fix.go
index 1c548c0..969eaa3 100644
--- a/internal/merger/fix.go
+++ b/internal/merger/fix.go
@@ -43,6 +43,7 @@
 	migrateLibraryEmbed(c, f)
 	migrateGrpcCompilers(c, f)
 	removeBinaryImportPath(c, f)
+	flattenSrcs(c, f)
 	squashCgoLibrary(c, f)
 	removeLegacyProto(c, f)
 }
@@ -346,6 +347,114 @@
 	f.Stmt = deleteIndices(f.Stmt, deletedIndices)
 }
 
+// flattenSrcs transforms srcs attributes structured as concatenations of
+// lists and selects (generated from PlatformStrings; see
+// extractPlatformStringsExprs for matching details) into a sorted,
+// de-duplicated list. Comments are accumulated and de-duplicated across
+// duplicate expressions.
+func flattenSrcs(c *config.Config, f *bf.File) {
+	for _, stmt := range f.Stmt {
+		call, ok := stmt.(*bf.CallExpr)
+		if !ok {
+			continue
+		}
+		rule := bf.Rule{Call: call}
+		if !isGoRule(rule.Kind()) {
+			continue
+		}
+		oldSrcs := rule.Attr("srcs")
+		if oldSrcs == nil {
+			continue
+		}
+		flatSrcs := flattenSrcsExpr(oldSrcs)
+		rule.SetAttr("srcs", flatSrcs)
+	}
+}
+
+func flattenSrcsExpr(oldSrcs bf.Expr) bf.Expr {
+	oldExprs, err := extractPlatformStringsExprs(oldSrcs)
+	if err != nil {
+		return oldSrcs
+	}
+
+	unique := make(map[string]*bf.StringExpr)
+	type elemComment struct{ elem, com string }
+	seenComments := make(map[elemComment]bool)
+	addElem := func(e bf.Expr) bool {
+		s, ok := e.(*bf.StringExpr)
+		if !ok {
+			return false
+		}
+		sCopy, ok := unique[s.Value]
+		if !ok {
+			// Make a copy of s. We may modify it when we consolidate comments from
+			// duplicate strings. We don't want to modify the original in case this
+			// function fails (due to a later failed pattern match).
+			sCopy = new(bf.StringExpr)
+			*sCopy = *s
+			sCopy.Comments.Before = make([]bf.Comment, 0, len(s.Comments.Before))
+			sCopy.Comments.Suffix = make([]bf.Comment, 0, len(s.Comments.Suffix))
+			unique[s.Value] = sCopy
+		}
+		for _, c := range s.Comment().Before {
+			if key := (elemComment{s.Value, c.Token}); !seenComments[key] {
+				sCopy.Comments.Before = append(sCopy.Comments.Before, c)
+				seenComments[key] = true
+			}
+		}
+		for _, c := range s.Comment().Suffix {
+			if key := (elemComment{s.Value, c.Token}); !seenComments[key] {
+				sCopy.Comments.Suffix = append(sCopy.Comments.Suffix, c)
+				seenComments[key] = true
+			}
+		}
+		return true
+	}
+	addList := func(e bf.Expr) bool {
+		l, ok := e.(*bf.ListExpr)
+		if !ok {
+			return false
+		}
+		for _, elem := range l.List {
+			if !addElem(elem) {
+				return false
+			}
+		}
+		return true
+	}
+	addDict := func(d *bf.DictExpr) bool {
+		for _, kv := range d.List {
+			if !addList(kv.(*bf.KeyValueExpr).Value) {
+				return false
+			}
+		}
+		return true
+	}
+
+	if oldExprs.generic != nil {
+		if !addList(oldExprs.generic) {
+			return oldSrcs
+		}
+	}
+	for _, d := range []*bf.DictExpr{oldExprs.os, oldExprs.arch, oldExprs.platform} {
+		if d == nil {
+			continue
+		}
+		if !addDict(d) {
+			return oldSrcs
+		}
+	}
+
+	sortedExprs := make([]bf.Expr, 0, len(unique))
+	for _, e := range unique {
+		sortedExprs = append(sortedExprs, e)
+	}
+	sort.Slice(sortedExprs, func(i, j int) bool {
+		return sortedExprs[i].(*bf.StringExpr).Value < sortedExprs[j].(*bf.StringExpr).Value
+	})
+	return &bf.ListExpr{List: sortedExprs}
+}
+
 // FixLoads removes loads of unused go rules and adds loads of newly used rules.
 // This should be called after FixFile and MergeFile, since symbols
 // may be introduced that aren't loaded.
diff --git a/internal/merger/fix_test.go b/internal/merger/fix_test.go
index 9b5a901..125777d 100644
--- a/internal/merger/fix_test.go
+++ b/internal/merger/fix_test.go
@@ -123,6 +123,40 @@
 )
 `,
 		},
+		// flattenSrcs tests
+		{
+			desc: "flatten srcs",
+			old: `load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "gen.go",
+    ] + select({
+        "@io_bazel_rules_go//platform:darwin_amd64": [
+            # darwin
+            "foo.go", # keep
+        ],
+        "@io_bazel_rules_go//platform:linux_amd64": [
+            # linux
+            "foo.go", # keep
+        ],
+    }),
+)
+`,
+			want: `load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        # darwin
+        # linux
+        "foo.go",  # keep
+        "gen.go",
+    ],
+)
+`,
+		},
 		// squashCgoLibrary tests
 		{
 			desc: "no cgo_library",
diff --git a/internal/packages/package.go b/internal/packages/package.go
index 1e38d37..852fb77 100644
--- a/internal/packages/package.go
+++ b/internal/packages/package.go
@@ -129,6 +129,34 @@
 	return len(ps.Generic) == 0 && len(ps.OS) == 0 && len(ps.Arch) == 0 && len(ps.Platform) == 0
 }
 
+func (ps *PlatformStrings) Flat() []string {
+	unique := make(map[string]struct{})
+	for _, s := range ps.Generic {
+		unique[s] = struct{}{}
+	}
+	for _, ss := range ps.OS {
+		for _, s := range ss {
+			unique[s] = struct{}{}
+		}
+	}
+	for _, ss := range ps.Arch {
+		for _, s := range ss {
+			unique[s] = struct{}{}
+		}
+	}
+	for _, ss := range ps.Platform {
+		for _, s := range ss {
+			unique[s] = struct{}{}
+		}
+	}
+	flat := make([]string, 0, len(unique))
+	for s := range unique {
+		flat = append(flat, s)
+	}
+	sort.Strings(flat)
+	return flat
+}
+
 func (ps *PlatformStrings) firstGoFile() string {
 	for _, f := range ps.Generic {
 		if strings.HasSuffix(f, ".go") {
diff --git a/internal/rules/generator.go b/internal/rules/generator.go
index 6f15ba8..76e7641 100644
--- a/internal/rules/generator.go
+++ b/internal/rules/generator.go
@@ -223,7 +223,7 @@
 func (g *Generator) commonAttrs(pkgRel, name, visibility string, target packages.GoTarget) []KeyValue {
 	attrs := []KeyValue{{"name", name}}
 	if !target.Sources.IsEmpty() {
-		attrs = append(attrs, KeyValue{"srcs", target.Sources})
+		attrs = append(attrs, KeyValue{"srcs", target.Sources.Flat()})
 	}
 	if target.Cgo {
 		attrs = append(attrs, KeyValue{"cgo", true})
diff --git a/internal/rules/generator_test.go b/internal/rules/generator_test.go
index 4bcc607..2b71ff4 100644
--- a/internal/rules/generator_test.go
+++ b/internal/rules/generator_test.go
@@ -75,36 +75,30 @@
 	}
 
 	for _, dir := range dirs {
-		rel, err := filepath.Rel(repoRoot, dir)
-		if err != nil {
-			t.Fatal(err)
-		}
+		rel, _ := filepath.Rel(repoRoot, dir)
+		t.Run(rel, func(t *testing.T) {
+			c, pkg, oldFile := packageFromDir(c, dir)
+			g := rules.NewGenerator(c, l, oldFile)
+			rs, _, err := g.GenerateRules(pkg)
+			if err != nil {
+				t.Fatal(err)
+			}
+			f := &bf.File{Stmt: rs}
+			rules.SortLabels(f)
+			merger.FixLoads(f)
+			got := string(bf.Format(f))
 
-		c, pkg, oldFile := packageFromDir(c, dir)
-		if pkg == nil {
-			t.Fatalf("%s: no package produced", dir)
-		}
-		g := rules.NewGenerator(c, l, oldFile)
-		rs, _, err := g.GenerateRules(pkg)
-		if err != nil {
-			t.Fatal(err)
-		}
-		f := &bf.File{Stmt: rs}
-		rules.SortLabels(f)
-		merger.FixLoads(f)
-		got := string(bf.Format(f))
+			wantPath := filepath.Join(pkg.Dir, "BUILD.want")
+			wantBytes, err := ioutil.ReadFile(wantPath)
+			if err != nil {
+				t.Fatalf("error reading %s: %v", wantPath, err)
+			}
+			want := string(wantBytes)
 
-		wantPath := filepath.Join(pkg.Dir, "BUILD.want")
-		wantBytes, err := ioutil.ReadFile(wantPath)
-		if err != nil {
-			t.Errorf("error reading %s: %v", wantPath, err)
-			continue
-		}
-		want := string(wantBytes)
-
-		if got != want {
-			t.Errorf("g.Generate(%q, %#v) = %s; want %s", rel, pkg, got, want)
-		}
+			if got != want {
+				t.Errorf("g.Generate(%q, %#v) = %s; want %s", rel, pkg, got, want)
+			}
+		})
 	}
 }
 
diff --git a/internal/rules/testdata/repo/cgolib_with_build_tags/BUILD.want b/internal/rules/testdata/repo/cgolib_with_build_tags/BUILD.want
index eda123d..cf48abb 100644
--- a/internal/rules/testdata/repo/cgolib_with_build_tags/BUILD.want
+++ b/internal/rules/testdata/repo/cgolib_with_build_tags/BUILD.want
@@ -3,66 +3,15 @@
 go_library(
     name = "go_default_library",
     srcs = [
+        "asm_linux.S",
+        "asm_other.S",
         "foo.go",
         "foo.h",
-    ] + select({
-        "@io_bazel_rules_go//go/platform:android": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:darwin": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:dragonfly": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:freebsd": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:linux": [
-            "asm_linux.S",
-            "foo_linux.c",
-            "pure_linux.go",
-        ],
-        "@io_bazel_rules_go//go/platform:nacl": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:netbsd": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:openbsd": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:plan9": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:solaris": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "@io_bazel_rules_go//go/platform:windows": [
-            "asm_other.S",
-            "foo_other.c",
-            "pure_other.go",
-        ],
-        "//conditions:default": [],
-    }),
+        "foo_linux.c",
+        "foo_other.c",
+        "pure_linux.go",
+        "pure_other.go",
+    ],
     _gazelle_imports = [
         "example.com/repo/lib",
         "fmt",
diff --git a/internal/rules/testdata/repo/gen_and_exclude/BUILD.want b/internal/rules/testdata/repo/gen_and_exclude/BUILD.want
index 226f45f..f6a3ea0 100644
--- a/internal/rules/testdata/repo/gen_and_exclude/BUILD.want
+++ b/internal/rules/testdata/repo/gen_and_exclude/BUILD.want
@@ -4,16 +4,10 @@
     name = "go_default_library",
     srcs = [
         "gen.go",
+        "gen_linux.go",
+        "gen_static_darwin.go",
         "static.go",
-    ] + select({
-        "@io_bazel_rules_go//go/platform:darwin": [
-            "gen_static_darwin.go",
-        ],
-        "@io_bazel_rules_go//go/platform:linux": [
-            "gen_linux.go",
-        ],
-        "//conditions:default": [],
-    }),
+    ],
     _gazelle_imports = select({
         "@io_bazel_rules_go//go/platform:darwin": [
             "github.com/jr_hacker/darwin",
diff --git a/internal/rules/testdata/repo/platforms/BUILD.want b/internal/rules/testdata/repo/platforms/BUILD.want
index 9f9a0f7..047e850 100644
--- a/internal/rules/testdata/repo/platforms/BUILD.want
+++ b/internal/rules/testdata/repo/platforms/BUILD.want
@@ -5,31 +5,19 @@
     srcs = [
         "cgo_generic.c",
         "cgo_generic.go",
+        "cgo_linux.c",
+        "cgo_linux.go",
         "generic.go",
         "no_cgo.go",
         "release.go",
-    ] + select({
-        "@io_bazel_rules_go//go/platform:darwin": [
-            "suffix_darwin.go",
-            "tag_d.go",
-        ],
-        "@io_bazel_rules_go//go/platform:linux": [
-            "cgo_linux.c",
-            "cgo_linux.go",
-            "suffix_linux.go",
-            "tag_l.go",
-        ],
-        "//conditions:default": [],
-    }) + select({
-        "@io_bazel_rules_go//go/platform:amd64": [
-            "suffix_amd64.go",
-            "tag_a.go",
-        ],
-        "@io_bazel_rules_go//go/platform:arm": [
-            "suffix_arm.go",
-        ],
-        "//conditions:default": [],
-    }),
+        "suffix_amd64.go",
+        "suffix_arm.go",
+        "suffix_darwin.go",
+        "suffix_linux.go",
+        "tag_a.go",
+        "tag_d.go",
+        "tag_l.go",
+    ],
     _gazelle_imports = [
         "example.com/repo/platforms/generic",
     ] + select({
@@ -58,10 +46,6 @@
     name = "go_default_xtest",
     srcs = [
         "generic_test.go",
-    ] + select({
-        "@io_bazel_rules_go//go/platform:linux": [
-            "suffix_linux_test.go",
-        ],
-        "//conditions:default": [],
-    }),
+        "suffix_linux_test.go",
+    ],
 )