Skip to content

Commit 4acbe9a

Browse files
authored
Ignore extra packages in Gazelle (#386)
If Gazelle finds .go files in a directory with multiple distinct package declarations, it will now only generate rules to build .go files where the package declaration matches the directory name. If there are multiple packages, and none match the directory name, Gazelle will report a build.MultiplePackageError, as bove. If is a single package that doesn't match the directory name, Gazelle will still generate rules for it. This change helps address #339. When we are no longer applying build constraints by default, multiple packages are much more common; many files are hidden with "// +build ignore".
1 parent 84382d5 commit 4acbe9a

File tree

2 files changed

+250
-52
lines changed

2 files changed

+250
-52
lines changed

go/tools/gazelle/packages/walk.go

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,33 @@ package packages
1717

1818
import (
1919
"go/build"
20+
"go/parser"
21+
"go/token"
22+
"io/ioutil"
23+
"log"
2024
"os"
25+
"path"
2126
"path/filepath"
27+
"sort"
28+
"strings"
2229
)
2330

2431
// A WalkFunc is a callback called by Walk for each package.
2532
type WalkFunc func(pkg *build.Package) error
2633

27-
// Walk walks through Go packages under the given dir.
34+
// Walk walks through directories under "root".
2835
// It calls back "f" for each package.
2936
//
3037
// It is similar to "golang.org/x/tools/go/buildutil".ForEachPackage, but
3138
// it does not assume the standard Go tree because Bazel rules_go uses
3239
// go_prefix instead of the standard tree.
40+
//
41+
// If a directory contains no buildable Go code, "f" is not called. If a
42+
// directory contains one package with any name, "f" will be called with that
43+
// package. If a directory contains multiple packages and one of the package
44+
// names matches the directory name, "f" will be called on that package and the
45+
// other packages will be silently ignored. If none of the package names match
46+
// the directory name, a *build.MultiplePackageError error is returned.
3347
func Walk(bctx build.Context, root string, f WalkFunc) error {
3448
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
3549
if err != nil {
@@ -42,7 +56,7 @@ func Walk(bctx build.Context, root string, f WalkFunc) error {
4256
return filepath.SkipDir
4357
}
4458

45-
pkg, err := bctx.ImportDir(path, build.ImportComment)
59+
pkg, err := findPackage(bctx, path)
4660
if err != nil {
4761
if _, ok := err.(*build.NoGoError); ok {
4862
return nil
@@ -52,3 +66,106 @@ func Walk(bctx build.Context, root string, f WalkFunc) error {
5266
return f(pkg)
5367
})
5468
}
69+
70+
func findPackage(bctx build.Context, dir string) (*build.Package, error) {
71+
packageGoFiles, otherFiles, err := findPackageFiles(dir)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
packageName, err := selectPackageName(packageGoFiles, dir)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
var files []os.FileInfo
82+
files = append(files, packageGoFiles[packageName]...)
83+
files = append(files, otherFiles...)
84+
sort.Slice(files, func(i, j int) bool {
85+
return files[i].Name() < files[j].Name()
86+
})
87+
bctx.ReadDir = func(dir string) ([]os.FileInfo, error) {
88+
return files, nil
89+
}
90+
return bctx.ImportDir(dir, build.ImportComment)
91+
}
92+
93+
func findPackageFiles(dir string) (packageGoFiles map[string][]os.FileInfo, otherFiles []os.FileInfo, err error) {
94+
files, err := ioutil.ReadDir(dir)
95+
if err != nil {
96+
return nil, nil, err
97+
}
98+
99+
packageGoFiles = make(map[string][]os.FileInfo)
100+
for _, file := range files {
101+
if file.IsDir() {
102+
continue
103+
}
104+
105+
name := file.Name()
106+
filename := filepath.Join(dir, name)
107+
ext := path.Ext(name)
108+
isGo := ext == ".go"
109+
110+
if !isGo {
111+
otherFiles = append(otherFiles, file)
112+
continue
113+
}
114+
fset := token.NewFileSet()
115+
ast, err := parser.ParseFile(fset, filename, nil, parser.PackageClauseOnly)
116+
if err != nil {
117+
log.Printf("%s: error parsing package clause: %v", filename, err)
118+
continue
119+
}
120+
121+
packageName := ast.Name.Name
122+
if packageName == "documentation" {
123+
// go/build ignores this package.
124+
continue
125+
}
126+
if strings.HasSuffix(packageName, "_test") {
127+
packageName = packageName[:len(packageName)-len("_test")]
128+
}
129+
packageGoFiles[packageName] = append(packageGoFiles[packageName], file)
130+
}
131+
return packageGoFiles, otherFiles, nil
132+
}
133+
134+
func defaultPackageName(dir string) string {
135+
pname := filepath.Base(dir)
136+
if pname == "." || pname == "/" {
137+
// We'll only use this name at the root of the filesystem.
138+
return "unnamed"
139+
}
140+
return pname
141+
}
142+
143+
func selectPackageName(packageGoFiles map[string][]os.FileInfo, dir string) (string, error) {
144+
if len(packageGoFiles) == 0 {
145+
return "", &build.NoGoError{Dir: dir}
146+
}
147+
148+
if len(packageGoFiles) == 1 {
149+
var packageName string
150+
for name, _ := range packageGoFiles {
151+
packageName = name
152+
}
153+
return packageName, nil
154+
}
155+
156+
defaultName := defaultPackageName(dir)
157+
if _, ok := packageGoFiles[defaultName]; ok {
158+
return defaultName, nil
159+
}
160+
161+
err := &build.MultiplePackageError{Dir: dir}
162+
for name, files := range packageGoFiles {
163+
// Add the first file for each package for the error message.
164+
// Error() method expects these lists to be the same length. File
165+
// lists must be non-empty. These lists are only created by
166+
// findPackageFiles for packages with .go files present.
167+
err.Packages = append(err.Packages, name)
168+
err.Files = append(err.Files, files[0].Name())
169+
}
170+
return "", err
171+
}

go/tools/gazelle/packages/walk_test.go

Lines changed: 131 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import (
2121
"os"
2222
"path/filepath"
2323
"reflect"
24-
"sort"
24+
"strings"
2525
"testing"
2626

2727
"github.com/bazelbuild/rules_go/go/tools/gazelle/packages"
@@ -31,78 +31,159 @@ func tempDir() (string, error) {
3131
return ioutil.TempDir(os.Getenv("TEST_TMPDIR"), "walk_test")
3232
}
3333

34-
func TestWalkSimple(t *testing.T) {
35-
dir, err := tempDir()
34+
type fileSpec struct {
35+
path, content string
36+
}
37+
38+
func checkFiles(t *testing.T, files []fileSpec, want []*build.Package) {
39+
dir, err := createFiles(files)
3640
if err != nil {
37-
t.Fatalf("tempDir() failed with %v; want success", err)
41+
t.Fatalf("createFiles() failed with %v; want success", err)
3842
}
3943
defer os.RemoveAll(dir)
4044

41-
fname := filepath.Join(dir, "lib.go")
42-
if err := ioutil.WriteFile(fname, []byte("package lib"), 0600); err != nil {
43-
t.Fatalf(`ioutil.WriteFile(%q, "package lib", 0600) failed with %v; want success`, fname, err)
45+
got, err := walkPackages(dir)
46+
if err != nil {
47+
t.Errorf("walkPackages(%q) failed with %v; want success", dir, err)
4448
}
49+
checkPackages(t, got, want)
50+
}
4551

46-
var n int
47-
err = packages.Walk(build.Default, dir, func(pkg *build.Package) error {
48-
if got, want := pkg.Name, "lib"; got != want {
49-
t.Errorf("pkg.Name = %q; want %q", got, want)
52+
func createFiles(files []fileSpec) (string, error) {
53+
dir, err := tempDir()
54+
if err != nil {
55+
return "", err
56+
}
57+
for _, f := range files {
58+
path := filepath.Join(dir, f.path)
59+
if strings.HasSuffix(f.path, "/") {
60+
if err := os.MkdirAll(path, 0700); err != nil {
61+
return dir, err
62+
}
63+
continue
64+
}
65+
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
66+
return "", err
67+
}
68+
if err := ioutil.WriteFile(path, []byte(f.content), 0600); err != nil {
69+
return "", err
5070
}
51-
n++
71+
}
72+
return dir, nil
73+
}
74+
75+
func walkPackages(root string) ([]*build.Package, error) {
76+
var pkgs []*build.Package
77+
err := packages.Walk(build.Default, root, func(pkg *build.Package) error {
78+
pkgs = append(pkgs, pkg)
5279
return nil
5380
})
5481
if err != nil {
55-
t.Errorf("packages.Walk(build.Default, %q, func) failed with %v; want success", dir, err)
82+
return nil, err
5683
}
57-
if got, want := n, 1; got != want {
58-
t.Errorf("n = %d; want %d", got, want)
84+
return pkgs, nil
85+
}
86+
87+
func checkPackages(t *testing.T, got []*build.Package, want []*build.Package) {
88+
if len(got) != len(want) {
89+
t.Fatalf("got %d packages; want %d", len(got), len(want))
90+
}
91+
for i := 0; i < len(got); i++ {
92+
checkPackage(t, got[i], want[i])
5993
}
6094
}
6195

62-
func TestWalkNested(t *testing.T) {
63-
dir, err := tempDir()
64-
if err != nil {
65-
t.Fatalf("tempDir() failed with %v; want success", err)
96+
func checkPackage(t *testing.T, got, want *build.Package) {
97+
if !reflect.DeepEqual(got.Name, want.Name) {
98+
t.Errorf("got package name %q; want %q", got.Name, want.Name)
6699
}
67-
defer os.RemoveAll(dir)
100+
if !reflect.DeepEqual(got.GoFiles, want.GoFiles) {
101+
t.Errorf("in package %q, got GoFiles %v; want %v", got.Name, got.GoFiles, want.GoFiles)
102+
}
103+
if !reflect.DeepEqual(got.CgoFiles, want.CgoFiles) {
104+
t.Errorf("in package %q, got CgoFiles %v; want %v", got.Name, got.CgoFiles, want.CgoFiles)
105+
}
106+
if !reflect.DeepEqual(got.CFiles, want.CFiles) {
107+
t.Errorf("in package %q, got CFiles %v; want %v", got.Name, got.CFiles, want.CFiles)
108+
}
109+
if !reflect.DeepEqual(got.TestGoFiles, want.TestGoFiles) {
110+
t.Errorf("in package %q, got TestGoFiles %v; want %v", got.Name, got.TestGoFiles, want.TestGoFiles)
111+
}
112+
if !reflect.DeepEqual(got.XTestGoFiles, want.XTestGoFiles) {
113+
t.Errorf("in package %q, got XTestGoFiles %v; want %v", got.Name, got.XTestGoFiles, want.XTestGoFiles)
114+
}
115+
}
68116

69-
for _, p := range []struct {
70-
path, content string
71-
}{
117+
func TestWalkEmpty(t *testing.T) {
118+
files := []fileSpec{
119+
{path: "a/foo.c"},
120+
{path: "b/"},
121+
}
122+
want := []*build.Package{}
123+
checkFiles(t, files, want)
124+
}
125+
126+
func TestWalkSimple(t *testing.T) {
127+
files := []fileSpec{{path: "lib.go", content: "package lib"}}
128+
want := []*build.Package{
129+
{
130+
Name: "lib",
131+
GoFiles: []string{"lib.go"},
132+
},
133+
}
134+
checkFiles(t, files, want)
135+
}
136+
137+
func TestWalkNested(t *testing.T) {
138+
files := []fileSpec{
72139
{path: "a/foo.go", content: "package a"},
73140
{path: "b/c/bar.go", content: "package c"},
74141
{path: "b/d/baz.go", content: "package main"},
75-
} {
76-
path := filepath.Join(dir, p.path)
77-
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
78-
t.Fatalf("os.MkdirAll(%q, 0700) failed with %v; want success", filepath.Dir(path), err)
79-
}
80-
if err := ioutil.WriteFile(path, []byte(p.content), 0600); err != nil {
81-
t.Fatalf("ioutil.WriteFile(%q, %q, 0600) failed with %v; want success", path, p.content, err)
82-
}
83142
}
143+
want := []*build.Package{
144+
{
145+
Name: "a",
146+
GoFiles: []string{"foo.go"},
147+
},
148+
{
149+
Name: "c",
150+
GoFiles: []string{"bar.go"},
151+
},
152+
{
153+
Name: "main",
154+
GoFiles: []string{"baz.go"},
155+
},
156+
}
157+
checkFiles(t, files, want)
158+
}
84159

85-
var dirs, pkgs []string
86-
err = packages.Walk(build.Default, dir, func(pkg *build.Package) error {
87-
rel, err := filepath.Rel(dir, pkg.Dir)
88-
if err != nil {
89-
t.Errorf("filepath.Rel(%q, %q) failed with %v; want success", dir, pkg.Dir, err)
90-
return err
91-
}
92-
dirs = append(dirs, filepath.ToSlash(rel))
93-
pkgs = append(pkgs, pkg.Name)
94-
return nil
95-
})
96-
if err != nil {
97-
t.Errorf("packages.Walk(build.Default, %q, func) failed with %v; want success", dir, err)
160+
func TestMultiplePackagesWithDefault(t *testing.T) {
161+
files := []fileSpec{
162+
{path: "a/a.go", content: "package a"},
163+
{path: "a/b.go", content: "package b"},
98164
}
165+
want := []*build.Package{
166+
{
167+
Name: "a",
168+
GoFiles: []string{"a.go"},
169+
},
170+
}
171+
checkFiles(t, files, want)
172+
}
99173

100-
sort.Strings(dirs)
101-
if got, want := dirs, []string{"a", "b/c", "b/d"}; !reflect.DeepEqual(got, want) {
102-
t.Errorf("pkgs = %q; want %q", got, want)
174+
func TestMultiplePackagesWithoutDefault(t *testing.T) {
175+
files := []fileSpec{
176+
{path: "a/b.go", content: "package b"},
177+
{path: "a/c.go", content: "package c"},
178+
}
179+
dir, err := createFiles(files)
180+
if err != nil {
181+
t.Fatalf("createFiles() failed with %v; want success", err)
103182
}
104-
sort.Strings(pkgs)
105-
if got, want := pkgs, []string{"a", "c", "main"}; !reflect.DeepEqual(got, want) {
106-
t.Errorf("pkgs = %q; want %q", got, want)
183+
defer os.RemoveAll(dir)
184+
185+
_, err = walkPackages(dir)
186+
if _, ok := err.(*build.MultiplePackageError); !ok {
187+
t.Errorf("got %v; want MultiplePackageError", err)
107188
}
108189
}

0 commit comments

Comments
 (0)