Experimental: read and write build files in alternate directories (#286)

* The flags -experimental_{read,write}_build_files_dir may now be used
  to read and write build files to alternate directories, which may be
  outside of the repository root.
* When a build file is read from an alternate directory, the build
  file in the source directory is ignored.
* When a build file is written to an alternate directory, any existing
  build file in that directory is replaced. The build file in the
  source directory is not updated.
diff --git a/cmd/gazelle/diff.go b/cmd/gazelle/diff.go
index a5cd6d5..4e74fea 100644
--- a/cmd/gazelle/diff.go
+++ b/cmd/gazelle/diff.go
@@ -20,21 +20,18 @@
 	"io/ioutil"
 	"os"
 	"os/exec"
-
-	"github.com/bazelbuild/bazel-gazelle/internal/config"
-	bzl "github.com/bazelbuild/buildtools/build"
+	"path/filepath"
 )
 
-func diffFile(c *config.Config, file *bzl.File, path string) error {
-	oldContents, err := ioutil.ReadFile(file.Path)
+func diffFile(path string, newContents []byte) error {
+	oldContents, err := ioutil.ReadFile(path)
 	if err != nil {
 		oldContents = nil
 	}
-	newContents := bzl.Format(file)
 	if bytes.Equal(oldContents, newContents) {
 		return nil
 	}
-	f, err := ioutil.TempFile("", c.DefaultBuildFileName())
+	f, err := ioutil.TempFile("", filepath.Base(path))
 	if err != nil {
 		return err
 	}
diff --git a/cmd/gazelle/fix-update.go b/cmd/gazelle/fix-update.go
index 60d15e2..a44994c 100644
--- a/cmd/gazelle/fix-update.go
+++ b/cmd/gazelle/fix-update.go
@@ -18,6 +18,7 @@
 import (
 	"flag"
 	"fmt"
+	"io/ioutil"
 	"log"
 	"os"
 	"path/filepath"
@@ -31,19 +32,17 @@
 	"github.com/bazelbuild/bazel-gazelle/internal/resolve"
 	"github.com/bazelbuild/bazel-gazelle/internal/rule"
 	"github.com/bazelbuild/bazel-gazelle/internal/walk"
-	bzl "github.com/bazelbuild/buildtools/build"
 )
 
 // updateConfig holds configuration information needed to run the fix and
 // update commands. This includes everything in config.Config, but it also
 // includes some additional fields that aren't relevant to other packages.
 type updateConfig struct {
-	emit              emitFunc
-	outDir, outSuffix string
-	repos             []repos.Repo
+	emit  emitFunc
+	repos []repos.Repo
 }
 
-type emitFunc func(*config.Config, *bzl.File, string) error
+type emitFunc func(path string, data []byte) error
 
 var modeFromName = map[string]emitFunc{
 	"print": printFile,
@@ -68,8 +67,6 @@
 	c.ShouldFix = cmd == "fix"
 
 	fs.StringVar(&ucr.mode, "mode", "fix", "print: prints all of the updated BUILD files\n\tfix: rewrites all of the BUILD files in place\n\tdiff: computes the rewrite but then just does a diff")
-	fs.StringVar(&uc.outDir, "experimental_out_dir", "", "write build files to an alternate directory tree")
-	fs.StringVar(&uc.outSuffix, "experimental_out_suffix", "", "extra suffix appended to build file names. Only used if -experimental_out_dir is also set.")
 }
 
 func (ucr *updateConfigurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error {
@@ -198,7 +195,7 @@
 
 		// Insert or merge rules into the build file.
 		if f == nil {
-			f = rule.EmptyFile(filepath.Join(dir, c.DefaultBuildFileName()))
+			f = rule.EmptyFile(filepath.Join(dir, c.DefaultBuildFileName()), rel)
 			for _, r := range gen {
 				r.Insert(f)
 			}
@@ -236,15 +233,9 @@
 	// Emit merged files.
 	for _, v := range visits {
 		merger.FixLoads(v.file, loads)
-		v.file.Sync()
-		bzl.Rewrite(v.file.File, nil) // have buildifier 'format' our rules.
-
-		path := v.file.Path
-		if uc.outDir != "" {
-			stem := filepath.Base(v.file.Path) + uc.outSuffix
-			path = filepath.Join(uc.outDir, v.pkgRel, stem)
-		}
-		if err := uc.emit(c, v.file.File, path); err != nil {
+		content := v.file.Format()
+		outputPath := findOutputPath(c, v.file)
+		if err := uc.emit(outputPath, content); err != nil {
 			log.Print(err)
 		}
 	}
@@ -269,7 +260,7 @@
 	if err := fs.Parse(args); err != nil {
 		if err == flag.ErrHelp {
 			fixUpdateUsage(fs)
-			os.Exit(0)
+			return nil, err
 		}
 		// flag already prints the error; don't print it again.
 		log.Fatal("Try -help for more information.")
@@ -283,7 +274,7 @@
 
 	uc := getUpdateConfig(c)
 	workspacePath := filepath.Join(c.RepoRoot, "WORKSPACE")
-	if workspace, err := rule.LoadFile(workspacePath); err != nil {
+	if workspace, err := rule.LoadFile(workspacePath, ""); err != nil {
 		if !os.IsNotExist(err) {
 			return nil, err
 		}
@@ -361,8 +352,7 @@
 	if err := merger.CheckGazelleLoaded(workspace); err != nil {
 		return err
 	}
-	workspace.Sync()
-	return uc.emit(c, workspace.File, workspace.Path)
+	return uc.emit(workspace.Path, workspace.Format())
 }
 
 func findWorkspaceName(f *rule.File) string {
@@ -384,3 +374,25 @@
 	}
 	return !strings.HasPrefix(rel, "..")
 }
+
+func findOutputPath(c *config.Config, f *rule.File) string {
+	if c.ReadBuildFilesDir == "" && c.WriteBuildFilesDir == "" {
+		return f.Path
+	}
+	baseDir := c.WriteBuildFilesDir
+	if c.WriteBuildFilesDir == "" {
+		baseDir = c.RepoRoot
+	}
+	outputDir := filepath.Join(baseDir, filepath.FromSlash(f.Pkg))
+	defaultOutputPath := filepath.Join(outputDir, c.DefaultBuildFileName())
+	files, err := ioutil.ReadDir(outputDir)
+	if err != nil {
+		// Ignore error. Directory probably doesn't exist.
+		return defaultOutputPath
+	}
+	outputPath := rule.MatchBuildFileName(outputDir, c.ValidBuildFileNames, files)
+	if outputPath == "" {
+		return defaultOutputPath
+	}
+	return outputPath
+}
diff --git a/cmd/gazelle/fix.go b/cmd/gazelle/fix.go
index af25cef..7a1c388 100644
--- a/cmd/gazelle/fix.go
+++ b/cmd/gazelle/fix.go
@@ -19,17 +19,11 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
-
-	"github.com/bazelbuild/bazel-gazelle/internal/config"
-	bzl "github.com/bazelbuild/buildtools/build"
 )
 
-func fixFile(c *config.Config, file *bzl.File, path string) error {
+func fixFile(path string, data []byte) error {
 	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
 		return err
 	}
-	if err := ioutil.WriteFile(path, bzl.Format(file), 0666); err != nil {
-		return err
-	}
-	return nil
+	return ioutil.WriteFile(path, data, 0666)
 }
diff --git a/cmd/gazelle/fix_test.go b/cmd/gazelle/fix_test.go
index b26e62a..c4fe25e 100644
--- a/cmd/gazelle/fix_test.go
+++ b/cmd/gazelle/fix_test.go
@@ -20,9 +20,9 @@
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 	"testing"
 
-	"github.com/bazelbuild/bazel-gazelle/internal/config"
 	bzl "github.com/bazelbuild/buildtools/build"
 )
 
@@ -63,9 +63,7 @@
 			},
 		},
 	}
-	c := &config.Config{}
-
-	if err := fixFile(c, stubFile, stubFile.Path); err != nil {
+	if err := fixFile(stubFile.Path, bzl.Format(stubFile)); err != nil {
 		t.Errorf("fixFile(%#v) failed with %v; want success", stubFile, err)
 		return
 	}
@@ -134,3 +132,157 @@
 		t.Errorf("BUILD.bazel should not exist")
 	}
 }
+
+func TestReadWriteDir(t *testing.T) {
+	buildInFile := fileSpec{
+		path: "in/BUILD.in",
+		content: `
+go_binary(
+    name = "hello",
+    pure = "on",
+)
+`,
+	}
+	buildSrcFile := fileSpec{
+		path:    "src/BUILD.bazel",
+		content: `# src build file`,
+	}
+	oldFiles := []fileSpec{
+		buildInFile,
+		buildSrcFile,
+		{
+			path: "src/hello.go",
+			content: `
+package main
+
+func main() {}
+`,
+		}, {
+			path:    "out/BUILD",
+			content: `this should get replaced`,
+		},
+	}
+
+	for _, tc := range []struct {
+		desc string
+		args []string
+		want []fileSpec
+	}{
+		{
+			desc: "read",
+			args: []string{
+				"-repo_root={{dir}}/src",
+				"-experimental_read_build_files_dir={{dir}}/in",
+				"-build_file_name=BUILD.bazel,BUILD,BUILD.in",
+				"-go_prefix=example.com/repo",
+				"{{dir}}/src",
+			},
+			want: []fileSpec{
+				buildInFile,
+				{
+					path: "src/BUILD.bazel",
+					content: `
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_binary(
+    name = "hello",
+    embed = [":go_default_library"],
+    pure = "on",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = ["hello.go"],
+    importpath = "example.com/repo",
+    visibility = ["//visibility:private"],
+)
+`,
+				},
+			},
+		}, {
+			desc: "write",
+			args: []string{
+				"-repo_root={{dir}}/src",
+				"-experimental_write_build_files_dir={{dir}}/out",
+				"-build_file_name=BUILD.bazel,BUILD,BUILD.in",
+				"-go_prefix=example.com/repo",
+				"{{dir}}/src",
+			},
+			want: []fileSpec{
+				buildInFile,
+				buildSrcFile,
+				{
+					path: "out/BUILD",
+					content: `
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+# src build file
+
+go_library(
+    name = "go_default_library",
+    srcs = ["hello.go"],
+    importpath = "example.com/repo",
+    visibility = ["//visibility:private"],
+)
+
+go_binary(
+    name = "repo",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
+`,
+				},
+			},
+		}, {
+			desc: "read_and_write",
+			args: []string{
+				"-repo_root={{dir}}/src",
+				"-experimental_read_build_files_dir={{dir}}/in",
+				"-experimental_write_build_files_dir={{dir}}/out",
+				"-build_file_name=BUILD.bazel,BUILD,BUILD.in",
+				"-go_prefix=example.com/repo",
+				"{{dir}}/src",
+			},
+			want: []fileSpec{
+				buildInFile,
+				{
+					path: "out/BUILD",
+					content: `
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_binary(
+    name = "hello",
+    embed = [":go_default_library"],
+    pure = "on",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = ["hello.go"],
+    importpath = "example.com/repo",
+    visibility = ["//visibility:private"],
+)
+`,
+				},
+			},
+		},
+	} {
+		t.Run(tc.desc, func(t *testing.T) {
+			dir, err := createFiles(oldFiles)
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer os.RemoveAll(dir)
+			replacer := strings.NewReplacer("{{dir}}", dir, "/", string(os.PathSeparator))
+			for i := range tc.args {
+				tc.args[i] = replacer.Replace(tc.args[i])
+			}
+			if err := run(tc.args); err != nil {
+				t.Error(err)
+			}
+			checkFiles(t, dir, tc.want)
+		})
+	}
+}
diff --git a/cmd/gazelle/gazelle.go b/cmd/gazelle/gazelle.go
index 2de75e4..fd44442 100644
--- a/cmd/gazelle/gazelle.go
+++ b/cmd/gazelle/gazelle.go
@@ -18,6 +18,7 @@
 package main
 
 import (
+	"flag"
 	"fmt"
 	"log"
 	"os"
@@ -76,7 +77,7 @@
 	case fixCmd, updateCmd:
 		return runFixUpdate(cmd, args)
 	case helpCmd:
-		help()
+		return help()
 	case updateReposCmd:
 		return updateRepos(args)
 	default:
@@ -85,7 +86,7 @@
 	return nil
 }
 
-func help() {
+func help() error {
 	fmt.Fprint(os.Stderr, `usage: gazelle <command> [args...]
 
 Gazelle is a BUILD file generator for Go projects. It can create new BUILD files
@@ -115,4 +116,5 @@
 without notice.
 
 `)
+	return flag.ErrHelp
 }
diff --git a/cmd/gazelle/integration_test.go b/cmd/gazelle/integration_test.go
index ae4b005..ae01e8f 100644
--- a/cmd/gazelle/integration_test.go
+++ b/cmd/gazelle/integration_test.go
@@ -21,6 +21,7 @@
 
 import (
 	"bytes"
+	"flag"
 	"io/ioutil"
 	"log"
 	"os"
@@ -83,19 +84,15 @@
 				t.Errorf("not a directory: %s", f.path)
 			}
 		} else {
-			want := f.content
-			if len(want) > 0 && want[0] == '\n' {
-				// Strip leading newline, added for readability.
-				want = want[1:]
-			}
+			want := strings.TrimSpace(f.content)
 			gotBytes, err := ioutil.ReadFile(filepath.Join(dir, f.path))
 			if err != nil {
 				t.Errorf("could not read %s: %v", f.path, err)
 				continue
 			}
-			got := string(gotBytes)
+			got := strings.TrimSpace(string(gotBytes))
 			if got != want {
-				t.Errorf("%s: got %s ; want %s", f.path, got, f.content)
+				t.Errorf("%s: got:\n%s\nwant:\n %s", f.path, gotBytes, f.content)
 			}
 		}
 	}
@@ -124,8 +121,10 @@
 		{"update-repos", "-h"},
 	} {
 		t.Run(args[0], func(t *testing.T) {
-			if err := runGazelle(".", args); err != nil {
-				t.Error(err)
+			if err := runGazelle(".", args); err == nil {
+				t.Errorf("%s: got success, want flag.ErrHelp", args[0])
+			} else if err != flag.ErrHelp {
+				t.Errorf("%s: got %v, want flag.ErrHelp", args[0], err)
 			}
 		})
 	}
diff --git a/cmd/gazelle/print.go b/cmd/gazelle/print.go
index 63ddb77..e7dfe13 100644
--- a/cmd/gazelle/print.go
+++ b/cmd/gazelle/print.go
@@ -17,12 +17,9 @@
 
 import (
 	"os"
-
-	"github.com/bazelbuild/bazel-gazelle/internal/config"
-	bzl "github.com/bazelbuild/buildtools/build"
 )
 
-func printFile(c *config.Config, f *bzl.File, _ string) error {
-	_, err := os.Stdout.Write(bzl.Format(f))
+func printFile(_ string, data []byte) error {
+	_, err := os.Stdout.Write(data)
 	return err
 }
diff --git a/cmd/gazelle/update-repos.go b/cmd/gazelle/update-repos.go
index 8c13002..2f38374 100644
--- a/cmd/gazelle/update-repos.go
+++ b/cmd/gazelle/update-repos.go
@@ -93,7 +93,7 @@
 	uc := getUpdateReposConfig(c)
 
 	workspacePath := filepath.Join(c.RepoRoot, "WORKSPACE")
-	f, err := rule.LoadFile(workspacePath)
+	f, err := rule.LoadFile(workspacePath, "")
 	if err != nil {
 		return fmt.Errorf("error loading %q: %v", workspacePath, err)
 	}
@@ -106,7 +106,7 @@
 	if err := merger.CheckGazelleLoaded(f); err != nil {
 		return err
 	}
-	if err := f.Save(); err != nil {
+	if err := f.Save(f.Path); err != nil {
 		return fmt.Errorf("error writing %q: %v", f.Path, err)
 	}
 	return nil
@@ -124,7 +124,7 @@
 	if err := fs.Parse(args); err != nil {
 		if err == flag.ErrHelp {
 			updateReposUsage(fs)
-			os.Exit(0)
+			return nil, err
 		}
 		// flag already prints the error; don't print it again.
 		return nil, errors.New("Try -help for more information")
diff --git a/internal/config/config.go b/internal/config/config.go
index 3b275d3..d959d78 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -47,6 +47,14 @@
 	// RepoName is the name of the repository.
 	RepoName string
 
+	// ReadBuildFilesDir is the absolute path to a directory where
+	// build files should be read from instead of RepoRoot.
+	ReadBuildFilesDir string
+
+	// WriteBuildFilesDir is the absolute path to a directory where
+	// build files should be written to instead of RepoRoot.
+	WriteBuildFilesDir string
+
 	// ValidBuildFileNames is a list of base names that are considered valid
 	// build files. Some repositories may have files named "BUILD" that are not
 	// used by Bazel and should be ignored. Must contain at least one string.
@@ -134,12 +142,14 @@
 // CommonConfigurer handles language-agnostic command-line flags and directives,
 // i.e., those that apply to Config itself and not to Config.Exts.
 type CommonConfigurer struct {
-	repoRoot, buildFileNames string
+	repoRoot, buildFileNames, readBuildFilesDir, writeBuildFilesDir string
 }
 
 func (cc *CommonConfigurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *Config) {
 	fs.StringVar(&cc.repoRoot, "repo_root", "", "path to a directory which corresponds to go_prefix, otherwise gazelle searches for it.")
 	fs.StringVar(&cc.buildFileNames, "build_file_name", strings.Join(DefaultValidBuildFileNames, ","), "comma-separated list of valid build file names.\nThe first element of the list is the name of output build files to generate.")
+	fs.StringVar(&cc.readBuildFilesDir, "experimental_read_build_files_dir", "", "path to a directory where build files should be read from (instead of -repo_root)")
+	fs.StringVar(&cc.writeBuildFilesDir, "experimental_write_build_files_dir", "", "path to a directory where build files should be written to (instead of -repo_root)")
 }
 
 func (cc *CommonConfigurer) CheckFlags(fs *flag.FlagSet, c *Config) error {
@@ -159,6 +169,19 @@
 		return fmt.Errorf("%s: failed to resolve symlinks: %v", cc.repoRoot, err)
 	}
 	c.ValidBuildFileNames = strings.Split(cc.buildFileNames, ",")
+	if cc.readBuildFilesDir != "" {
+		c.ReadBuildFilesDir, err = filepath.Abs(cc.readBuildFilesDir)
+		if err != nil {
+			return fmt.Errorf("%s: failed to find absolute path of -read_build_files_dir: %v", cc.readBuildFilesDir, err)
+		}
+	}
+	if cc.writeBuildFilesDir != "" {
+		c.WriteBuildFilesDir, err = filepath.Abs(cc.writeBuildFilesDir)
+		if err != nil {
+			return fmt.Errorf("%s: failed to find absolute path of -write_build_files_dir: %v", cc.writeBuildFilesDir, err)
+		}
+	}
+
 	return nil
 }
 
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 41baef0..b532f91 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -66,7 +66,7 @@
 	c := New()
 	cc := &CommonConfigurer{}
 	buildData := []byte(`# gazelle:build_file_name x,y`)
-	f, err := rule.LoadData("test", buildData)
+	f, err := rule.LoadData("test", "", buildData)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/language/go/config_test.go b/internal/language/go/config_test.go
index e7d9c1b..0ebbfc0 100644
--- a/internal/language/go/config_test.go
+++ b/internal/language/go/config_test.go
@@ -69,7 +69,7 @@
 # gazelle:importmap_prefix x
 # gazelle:prefix y
 `)
-	f, err := rule.LoadData(filepath.FromSlash("test/BUILD.bazel"), content)
+	f, err := rule.LoadData(filepath.FromSlash("test/BUILD.bazel"), "test", content)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -188,7 +188,7 @@
 			var f *rule.File
 			if tc.content != "" {
 				var err error
-				f, err = rule.LoadData(path.Join(tc.rel, "BUILD.bazel"), []byte(tc.content))
+				f, err = rule.LoadData(path.Join(tc.rel, "BUILD.bazel"), tc.rel, []byte(tc.content))
 				if err != nil {
 					t.Fatal(err)
 				}
@@ -243,7 +243,7 @@
 		},
 	} {
 		t.Run(tc.desc, func(t *testing.T) {
-			f, err := rule.LoadData("BUILD.bazel", []byte(tc.content))
+			f, err := rule.LoadData("BUILD.bazel", "", []byte(tc.content))
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/language/go/fix_test.go b/internal/language/go/fix_test.go
index b34b78a..c6b0b6d 100644
--- a/internal/language/go/fix_test.go
+++ b/internal/language/go/fix_test.go
@@ -632,7 +632,7 @@
 }
 
 func testFix(t *testing.T, tc fixTestCase, fix func(*rule.File)) {
-	f, err := rule.LoadData("old", []byte(tc.old))
+	f, err := rule.LoadData("old", "", []byte(tc.old))
 	if err != nil {
 		t.Fatalf("%s: parse error: %v", tc.desc, err)
 	}
diff --git a/internal/language/go/generate_test.go b/internal/language/go/generate_test.go
index 9685e17..5e57ddb 100644
--- a/internal/language/go/generate_test.go
+++ b/internal/language/go/generate_test.go
@@ -64,7 +64,7 @@
 				// there's no test.
 				return
 			}
-			f := rule.EmptyFile("test")
+			f := rule.EmptyFile("test", "")
 			for _, r := range gen {
 				r.Insert(f)
 			}
@@ -93,7 +93,7 @@
 	if len(gen) > 0 {
 		t.Errorf("got %d generated rules; want 0", len(gen))
 	}
-	f := rule.EmptyFile("test")
+	f := rule.EmptyFile("test", "")
 	for _, r := range empty {
 		r.Insert(f)
 	}
@@ -138,7 +138,7 @@
     srcs = ["dead.proto"],
 )
 `)
-	old, err := rule.LoadData("BUILD.bazel", oldContent)
+	old, err := rule.LoadData("BUILD.bazel", "", oldContent)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -147,7 +147,7 @@
 		es, _ := lang.GenerateRules(c, "./foo", "foo", old, nil, nil, nil, empty, nil)
 		empty = append(empty, es...)
 	}
-	f := rule.EmptyFile("test")
+	f := rule.EmptyFile("test", "")
 	for _, r := range empty {
 		r.Insert(f)
 	}
diff --git a/internal/language/go/resolve_test.go b/internal/language/go/resolve_test.go
index f31484f..e720905 100644
--- a/internal/language/go/resolve_test.go
+++ b/internal/language/go/resolve_test.go
@@ -739,7 +739,7 @@
 
 			for _, bf := range tc.index {
 				buildPath := filepath.Join(filepath.FromSlash(bf.rel), "BUILD.bazel")
-				f, err := rule.LoadData(buildPath, []byte(bf.content))
+				f, err := rule.LoadData(buildPath, bf.rel, []byte(bf.content))
 				if err != nil {
 					t.Fatal(err)
 				}
@@ -748,7 +748,7 @@
 				}
 			}
 			buildPath := filepath.Join(filepath.FromSlash(tc.old.rel), "BUILD.bazel")
-			f, err := rule.LoadData(buildPath, []byte(tc.old.content))
+			f, err := rule.LoadData(buildPath, tc.old.rel, []byte(tc.old.content))
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -814,7 +814,7 @@
     ],
 )
 `)
-	f, err := rule.LoadData("BUILD.bazel", oldContent)
+	f, err := rule.LoadData("BUILD.bazel", "", oldContent)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/language/proto/generate_test.go b/internal/language/proto/generate_test.go
index 7a7ceb4..183d732 100644
--- a/internal/language/proto/generate_test.go
+++ b/internal/language/proto/generate_test.go
@@ -50,7 +50,7 @@
 			if len(empty) > 0 {
 				t.Errorf("got %d empty rules; want 0", len(empty))
 			}
-			f := rule.EmptyFile("test")
+			f := rule.EmptyFile("test", "")
 			for _, r := range gen {
 				r.Insert(f)
 			}
@@ -94,7 +94,7 @@
     srcs = COMPLICATED_SRCS,
 )
 `)
-	old, err := rule.LoadData("BUILD.bazel", oldContent)
+	old, err := rule.LoadData("BUILD.bazel", "", oldContent)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -103,7 +103,7 @@
 	if len(gen) > 0 {
 		t.Errorf("got %d generated rules; want 0", len(gen))
 	}
-	f := rule.EmptyFile("test")
+	f := rule.EmptyFile("test", "")
 	for _, r := range empty {
 		r.Insert(f)
 	}
diff --git a/internal/language/proto/resolve.go b/internal/language/proto/resolve.go
index 0267277..0d25ad3 100644
--- a/internal/language/proto/resolve.go
+++ b/internal/language/proto/resolve.go
@@ -30,7 +30,7 @@
 )
 
 func (_ *protoLang) Imports(c *config.Config, r *rule.Rule, f *rule.File) []resolve.ImportSpec {
-	rel := f.Rel(c.RepoRoot)
+	rel := f.Pkg
 	srcs := r.AttrStrings("srcs")
 	imports := make([]resolve.ImportSpec, len(srcs))
 	for i, src := range srcs {
diff --git a/internal/language/proto/resolve_test.go b/internal/language/proto/resolve_test.go
index aee061f..490ca04 100644
--- a/internal/language/proto/resolve_test.go
+++ b/internal/language/proto/resolve_test.go
@@ -179,7 +179,7 @@
 			ix := resolve.NewRuleIndex(map[string]resolve.Resolver{"proto_library": lang})
 			rc := (*repos.RemoteCache)(nil)
 			for _, bf := range tc.index {
-				f, err := rule.LoadData(filepath.Join(bf.rel, "BUILD.bazel"), []byte(bf.content))
+				f, err := rule.LoadData(filepath.Join(bf.rel, "BUILD.bazel"), bf.rel, []byte(bf.content))
 				if err != nil {
 					t.Fatal(err)
 				}
@@ -187,7 +187,7 @@
 					ix.AddRule(c, r, f)
 				}
 			}
-			f, err := rule.LoadData("test/BUILD.bazel", []byte(tc.old))
+			f, err := rule.LoadData("test/BUILD.bazel", "test", []byte(tc.old))
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/merger/merger.go b/internal/merger/merger.go
index e695ea6..16cc227 100644
--- a/internal/merger/merger.go
+++ b/internal/merger/merger.go
@@ -56,6 +56,9 @@
 	// Merge empty rules into the file and delete any rules which become empty.
 	for _, emptyRule := range emptyRules {
 		if oldRule, _ := match(oldFile.Rules, emptyRule, kinds[emptyRule.Kind()]); oldRule != nil {
+			if oldRule.ShouldKeep() {
+				continue
+			}
 			rule.MergeRules(emptyRule, oldRule, getMergeAttrs(emptyRule), oldFile.Path)
 			if oldRule.IsEmpty(kinds[oldRule.Kind()]) {
 				oldRule.Delete()
diff --git a/internal/merger/merger_test.go b/internal/merger/merger_test.go
index 84de20a..20f8f74 100644
--- a/internal/merger/merger_test.go
+++ b/internal/merger/merger_test.go
@@ -870,15 +870,15 @@
 func TestMergeFile(t *testing.T) {
 	for _, tc := range testCases {
 		t.Run(tc.desc, func(t *testing.T) {
-			genFile, err := rule.LoadData("current", []byte(tc.current))
+			genFile, err := rule.LoadData("current", "", []byte(tc.current))
 			if err != nil {
 				t.Fatalf("%s: %v", tc.desc, err)
 			}
-			f, err := rule.LoadData("previous", []byte(tc.previous))
+			f, err := rule.LoadData("previous", "", []byte(tc.previous))
 			if err != nil {
 				t.Fatalf("%s: %v", tc.desc, err)
 			}
-			emptyFile, err := rule.LoadData("empty", []byte(tc.empty))
+			emptyFile, err := rule.LoadData("empty", "", []byte(tc.empty))
 			if err != nil {
 				t.Fatalf("%s: %v", tc.desc, err)
 			}
@@ -970,11 +970,11 @@
 		},
 	} {
 		t.Run(tc.desc, func(t *testing.T) {
-			genFile, err := rule.LoadData("gen", []byte(tc.gen))
+			genFile, err := rule.LoadData("gen", "", []byte(tc.gen))
 			if err != nil {
 				t.Fatal(err)
 			}
-			oldFile, err := rule.LoadData("old", []byte(tc.old))
+			oldFile, err := rule.LoadData("old", "", []byte(tc.old))
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -987,7 +987,7 @@
 			} else if tc.wantError {
 				t.Error("unexpected success")
 			} else if got == nil && tc.wantIndex >= 0 {
-				t.Error("got nil; want index %d", tc.wantIndex)
+				t.Errorf("got nil; want index %d", tc.wantIndex)
 			} else if got != nil && got.Index() != tc.wantIndex {
 				t.Fatalf("got index %d ; want %d", got.Index(), tc.wantIndex)
 			}
diff --git a/internal/repos/import_test.go b/internal/repos/import_test.go
index d16876f..21afa12 100644
--- a/internal/repos/import_test.go
+++ b/internal/repos/import_test.go
@@ -76,7 +76,7 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	f := rule.EmptyFile("test")
+	f := rule.EmptyFile("test", "")
 	for _, r := range rules {
 		r.Insert(f)
 	}
diff --git a/internal/repos/repo_test.go b/internal/repos/repo_test.go
index 7646f88..8474277 100644
--- a/internal/repos/repo_test.go
+++ b/internal/repos/repo_test.go
@@ -33,7 +33,7 @@
 		Commit:   "123456",
 	}
 	r := GenerateRule(repo)
-	f := rule.EmptyFile("test")
+	f := rule.EmptyFile("test", "")
 	r.Insert(f)
 	got := strings.TrimSpace(string(f.Format()))
 	want := `go_repository(
@@ -110,7 +110,7 @@
 		},
 	} {
 		t.Run(tc.desc, func(t *testing.T) {
-			workspace, err := rule.LoadData("WORKSPACE", []byte(tc.workspace))
+			workspace, err := rule.LoadData("WORKSPACE", "", []byte(tc.workspace))
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/resolve/index.go b/internal/resolve/index.go
index 5414766..211eba6 100644
--- a/internal/resolve/index.go
+++ b/internal/resolve/index.go
@@ -119,10 +119,9 @@
 		return
 	}
 
-	rel := f.Rel(c.RepoRoot)
 	record := &ruleRecord{
 		rule:       r,
-		label:      label.New(c.RepoName, rel, r.Name()),
+		label:      label.New(c.RepoName, f.Pkg, r.Name()),
 		importedAs: imps,
 	}
 	if _, ok := ix.labelMap[record.label]; ok {
diff --git a/internal/rule/merge.go b/internal/rule/merge.go
index 3a246f7..0bc30c7 100644
--- a/internal/rule/merge.go
+++ b/internal/rule/merge.go
@@ -42,7 +42,7 @@
 // a "# keep" comment will be dropped. If the attribute is empty afterward,
 // it will be deleted.
 func MergeRules(src, dst *Rule, mergeable map[string]bool, filename string) {
-	if ShouldKeep(dst.call) {
+	if dst.ShouldKeep() {
 		return
 	}
 
@@ -270,7 +270,7 @@
 // fails because the expression is not understood, an error is returned,
 // and neither rule is modified.
 func SquashRules(src, dst *Rule, filename string) error {
-	if ShouldKeep(dst.call) {
+	if dst.ShouldKeep() {
 		return nil
 	}
 
diff --git a/internal/rule/rule.go b/internal/rule/rule.go
index d5a6601..8424359 100644
--- a/internal/rule/rule.go
+++ b/internal/rule/rule.go
@@ -27,7 +27,7 @@
 
 import (
 	"io/ioutil"
-	"log"
+	"os"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -46,6 +46,9 @@
 	// may modify this, but editing is not complete until Sync() is called.
 	File *bzl.File
 
+	// Pkg is the Bazel package this build file defines.
+	Pkg string
+
 	// Path is the file system path to the build file (same as File.Path).
 	Path string
 
@@ -63,10 +66,11 @@
 }
 
 // EmptyFile creates a File wrapped around an empty syntax tree.
-func EmptyFile(path string) *File {
+func EmptyFile(path, pkg string) *File {
 	return &File{
 		File: &bzl.File{Path: path},
 		Path: path,
+		Pkg:  pkg,
 	}
 }
 
@@ -76,30 +80,31 @@
 //
 // This function returns I/O and parse errors without modification. It's safe
 // to use os.IsNotExist and similar predicates.
-func LoadFile(path string) (*File, error) {
+func LoadFile(path, pkg string) (*File, error) {
 	data, err := ioutil.ReadFile(path)
 	if err != nil {
 		return nil, err
 	}
-	return LoadData(path, data)
+	return LoadData(path, pkg, data)
 }
 
 // LoadData parses a build file from a byte slice and scans it for rules and
 // load statements. The syntax tree within the returned File will be modified
 // by editing methods.
-func LoadData(path string, data []byte) (*File, error) {
+func LoadData(path, pkg string, data []byte) (*File, error) {
 	ast, err := bzl.Parse(path, data)
 	if err != nil {
 		return nil, err
 	}
-	return ScanAST(ast), nil
+	return ScanAST(pkg, ast), nil
 }
 
 // ScanAST creates a File wrapped around the given syntax tree. This tree
 // will be modified by editing methods.
-func ScanAST(bzlFile *bzl.File) *File {
+func ScanAST(pkg string, bzlFile *bzl.File) *File {
 	f := &File{
 		File: bzlFile,
+		Pkg:  pkg,
 		Path: bzlFile.Path,
 	}
 	for i, stmt := range f.File.Stmt {
@@ -125,18 +130,19 @@
 	return f
 }
 
-// Rel returns the slash-separated relative path from the given absolute path to
-// the directory containing this file. If the file is in the root directory, Rel
-// returns "". This string may be used as a Bazel package name.
-func (f *File) Rel(root string) string {
-	rel, err := filepath.Rel(root, filepath.Dir(f.Path))
-	if err != nil {
-		log.Panicf("%s is not a parent of %s", root, f.Path)
+// MatchBuildFileName looks for a file in files that has a name from names.
+// If there is at least one matching file, a path will be returned by joining
+// dir and the first matching name. If there are no matching files, the
+// empty string is returned.
+func MatchBuildFileName(dir string, names []string, files []os.FileInfo) string {
+	for _, name := range names {
+		for _, fi := range files {
+			if fi.Name() == name && !fi.IsDir() {
+				return filepath.Join(dir, name)
+			}
+		}
 	}
-	if rel == "." {
-		rel = ""
-	}
-	return filepath.ToSlash(rel)
+	return ""
 }
 
 // Sync writes all changes back to the wrapped syntax tree. This should be
@@ -215,12 +221,11 @@
 	return bzl.Format(f.File)
 }
 
-// Save writes the build file to disk at the same path it was loaded from.
-// This method calls Sync internally.
-func (f *File) Save() error {
+// Save writes the build file to disk. This method calls Sync internally.
+func (f *File) Save(path string) error {
 	f.Sync()
 	data := bzl.Format(f.File)
-	return ioutil.WriteFile(f.Path, data, 0666)
+	return ioutil.WriteFile(path, data, 0666)
 }
 
 type stmt struct {
diff --git a/internal/rule/rule_test.go b/internal/rule/rule_test.go
index 78e259e..9f9d901 100644
--- a/internal/rule/rule_test.go
+++ b/internal/rule/rule_test.go
@@ -37,7 +37,7 @@
 
 y_library(name = "bar")
 `)
-	f, err := LoadData("old", old)
+	f, err := LoadData("old", "", old)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -84,7 +84,7 @@
 
 x_library(name = "bar")
 `)
-	f, err := LoadData("old", old)
+	f, err := LoadData("old", "", old)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -103,7 +103,7 @@
 }
 
 func TestSymbolsReturnsKeys(t *testing.T) {
-	f, err := LoadData("load", []byte(`load("a.bzl", "y", z = "a")`))
+	f, err := LoadData("load", "", []byte(`load("a.bzl", "y", z = "a")`))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -158,7 +158,7 @@
 		},
 	} {
 		t.Run(tc.desc, func(t *testing.T) {
-			f, err := LoadData(tc.desc, []byte(tc.src))
+			f, err := LoadData(tc.desc, "", []byte(tc.src))
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/internal/walk/walk.go b/internal/walk/walk.go
index 32688a0..896a793 100644
--- a/internal/walk/walk.go
+++ b/internal/walk/walk.go
@@ -16,7 +16,6 @@
 package walk
 
 import (
-	"fmt"
 	"io/ioutil"
 	"log"
 	"os"
@@ -90,7 +89,7 @@
 			return
 		}
 
-		f, err := loadBuildFile(dir, files, c.ValidBuildFileNames)
+		f, err := loadBuildFile(c, rel, dir, files)
 		if err != nil {
 			log.Print(err)
 			haveError = true
@@ -155,25 +154,22 @@
 	return false
 }
 
-func loadBuildFile(dir string, files []os.FileInfo, buildFileNames []string) (*rule.File, error) {
-	var f *rule.File
-	for _, base := range buildFileNames {
-		for _, fi := range files {
-			if fi.Name() != base || fi.IsDir() {
-				continue
-			}
-			if f != nil {
-				return f, fmt.Errorf("in directory %s, multiple Bazel files are present: %s, %s", dir, filepath.Base(f.Path), base)
-			}
-			var err error
-			f, err = rule.LoadFile(filepath.Join(dir, base))
-			if err != nil {
-				return nil, err
-			}
-			return f, nil
+func loadBuildFile(c *config.Config, pkg, dir string, files []os.FileInfo) (*rule.File, error) {
+	var err error
+	readDir := dir
+	readFiles := files
+	if c.ReadBuildFilesDir != "" {
+		readDir = filepath.Join(c.ReadBuildFilesDir, filepath.FromSlash(pkg))
+		readFiles, err = ioutil.ReadDir(readDir)
+		if err != nil {
+			return nil, err
 		}
 	}
-	return f, nil
+	path := rule.MatchBuildFileName(readDir, c.ValidBuildFileNames, readFiles)
+	if path == "" {
+		return nil, nil
+	}
+	return rule.LoadFile(path, pkg)
 }
 
 func configure(cexts []config.Configurer, knownDirectives map[string]bool, c *config.Config, rel string, f *rule.File) *config.Config {