Skip to content

Commit 6f5e273

Browse files
committed
go/packages: handle invalid files in overlays
This change handles a specific case where `go list` returns an error on a file that lacks a package declaration. If only one package is returned as a response, we should add that file to that package, since it may have valid content in an overlay. This change uses the internal/span package to correctly parse the error message from `go list`. Change-Id: I5909c4fd765746df1003685aa915b7c5f9cdcee5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/201220 Run-TryBot: Rebecca Stambler <[email protected]> Reviewed-by: Michael Matloob <[email protected]>
1 parent f0068bd commit 6f5e273

File tree

2 files changed

+140
-22
lines changed

2 files changed

+140
-22
lines changed

go/packages/golist.go

Lines changed: 91 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"golang.org/x/tools/go/internal/packagesdriver"
2727
"golang.org/x/tools/internal/gopathwalk"
2828
"golang.org/x/tools/internal/semver"
29+
"golang.org/x/tools/internal/span"
2930
)
3031

3132
// debug controls verbose logging.
@@ -281,31 +282,42 @@ func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, q
281282
return fmt.Errorf("could not determine absolute path of file= query path %q: %v", query, err)
282283
}
283284
dirResponse, err := driver(cfg, pattern)
284-
if err != nil || (len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].Errors) == 1) {
285-
// There was an error loading the package. Try to load the file as an ad-hoc package.
286-
// Usually the error will appear in a returned package, but may not if we're in modules mode
287-
// and the ad-hoc is located outside a module.
285+
if err != nil {
288286
var queryErr error
289-
dirResponse, queryErr = driver(cfg, query)
290-
if queryErr != nil {
291-
// Return the original error if the attempt to fall back failed.
292-
return err
287+
if dirResponse, queryErr = adHocPackage(cfg, driver, pattern, query); queryErr != nil {
288+
return err // return the original error
293289
}
294-
// Special case to handle issue #33482:
295-
// If this is a file= query for ad-hoc packages where the file only exists on an overlay,
296-
// and exists outside of a module, add the file in for the package.
297-
if len(dirResponse.Packages) == 1 && (dirResponse.Packages[0].ID == "command-line-arguments" || dirResponse.Packages[0].PkgPath == filepath.ToSlash(query)) {
298-
if len(dirResponse.Packages[0].GoFiles) == 0 {
299-
filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath
300-
// TODO(matloob): check if the file is outside of a root dir?
301-
for path := range cfg.Overlay {
302-
if path == filename {
303-
dirResponse.Packages[0].Errors = nil
304-
dirResponse.Packages[0].GoFiles = []string{path}
305-
dirResponse.Packages[0].CompiledGoFiles = []string{path}
306-
}
307-
}
290+
}
291+
// `go list` can report errors for files that are not listed as part of a package's GoFiles.
292+
// In the case of an invalid Go file, we should assume that it is part of package if only
293+
// one package is in the response. The file may have valid contents in an overlay.
294+
if len(dirResponse.Packages) == 1 {
295+
pkg := dirResponse.Packages[0]
296+
for i, err := range pkg.Errors {
297+
s := errorSpan(err)
298+
if !s.IsValid() {
299+
break
300+
}
301+
if len(pkg.CompiledGoFiles) == 0 {
302+
break
303+
}
304+
dir := filepath.Dir(pkg.CompiledGoFiles[0])
305+
filename := filepath.Join(dir, filepath.Base(s.URI().Filename()))
306+
if info, err := os.Stat(filename); err != nil || info.IsDir() {
307+
break
308308
}
309+
if !contains(pkg.CompiledGoFiles, filename) {
310+
pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, filename)
311+
pkg.GoFiles = append(pkg.GoFiles, filename)
312+
pkg.Errors = append(pkg.Errors[:i], pkg.Errors[i+1:]...)
313+
}
314+
}
315+
}
316+
// A final attempt to construct an ad-hoc package.
317+
if len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].Errors) == 1 {
318+
var queryErr error
319+
if dirResponse, queryErr = adHocPackage(cfg, driver, pattern, query); queryErr != nil {
320+
return err // return the original error
309321
}
310322
}
311323
isRoot := make(map[string]bool, len(dirResponse.Roots))
@@ -333,6 +345,63 @@ func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, q
333345
return nil
334346
}
335347

348+
// adHocPackage attempts to construct an ad-hoc package given a query that failed.
349+
func adHocPackage(cfg *Config, driver driver, pattern, query string) (*driverResponse, error) {
350+
// There was an error loading the package. Try to load the file as an ad-hoc package.
351+
// Usually the error will appear in a returned package, but may not if we're in modules mode
352+
// and the ad-hoc is located outside a module.
353+
dirResponse, err := driver(cfg, query)
354+
if err != nil {
355+
return nil, err
356+
}
357+
// Special case to handle issue #33482:
358+
// If this is a file= query for ad-hoc packages where the file only exists on an overlay,
359+
// and exists outside of a module, add the file in for the package.
360+
if len(dirResponse.Packages) == 1 && (dirResponse.Packages[0].ID == "command-line-arguments" || dirResponse.Packages[0].PkgPath == filepath.ToSlash(query)) {
361+
if len(dirResponse.Packages[0].GoFiles) == 0 {
362+
filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath
363+
// TODO(matloob): check if the file is outside of a root dir?
364+
for path := range cfg.Overlay {
365+
if path == filename {
366+
dirResponse.Packages[0].Errors = nil
367+
dirResponse.Packages[0].GoFiles = []string{path}
368+
dirResponse.Packages[0].CompiledGoFiles = []string{path}
369+
}
370+
}
371+
}
372+
}
373+
return dirResponse, nil
374+
}
375+
376+
func contains(files []string, filename string) bool {
377+
for _, f := range files {
378+
if f == filename {
379+
return true
380+
}
381+
}
382+
return false
383+
}
384+
385+
// errorSpan attempts to parse a standard `go list` error message
386+
// by stripping off the trailing error message.
387+
//
388+
// It works only on errors whose message is prefixed by colon,
389+
// followed by a space (": "). For example:
390+
//
391+
// attributes.go:13:1: expected 'package', found 'type'
392+
//
393+
func errorSpan(err Error) span.Span {
394+
if err.Pos == "" {
395+
input := strings.TrimSpace(err.Msg)
396+
msgIndex := strings.Index(input, ": ")
397+
if msgIndex < 0 {
398+
return span.Parse(input)
399+
}
400+
return span.Parse(input[:msgIndex])
401+
}
402+
return span.Parse(err.Pos)
403+
}
404+
336405
// modCacheRegexp splits a path in a module cache into module, module version, and package.
337406
var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`)
338407

go/packages/packages_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,51 @@ func testNewPackagesInOverlay(t *testing.T, exporter packagestest.Exporter) {
10791079
}
10801080
}
10811081

1082+
func TestContainsInOverlays(t *testing.T) { packagestest.TestAll(t, testcontainsInOverlays) }
1083+
func testcontainsInOverlays(t *testing.T, exporter packagestest.Exporter) {
1084+
// This test checks a specific case where a file is empty on disk.
1085+
// In this case, `go list` will return the package golang.org/fake/c
1086+
// with only c.go as a GoFile, with an error message for c2.go.
1087+
// Since there is only one possible package for c2.go to be a part of,
1088+
// go/packages will parse the filename out of error and add it to GoFiles and CompiledGoFiles.
1089+
exported := packagestest.Export(t, exporter, []packagestest.Module{{
1090+
Name: "golang.org/fake",
1091+
Files: map[string]interface{}{
1092+
"c/c.go": `package c; const C = "c"`,
1093+
"c/c2.go": ``,
1094+
}}})
1095+
defer exported.Cleanup()
1096+
1097+
dir := filepath.Dir(exported.File("golang.org/fake", "c/c.go"))
1098+
1099+
for _, test := range []struct {
1100+
overlay map[string][]byte
1101+
want string
1102+
}{
1103+
{
1104+
overlay: map[string][]byte{filepath.Join(dir, "c2.go"): []byte(`nonsense`)},
1105+
want: "golang.org/fake/c",
1106+
},
1107+
} {
1108+
exported.Config.Overlay = test.overlay
1109+
exported.Config.Mode = packages.LoadImports
1110+
exported.Config.Logf = t.Logf
1111+
for filename := range exported.Config.Overlay {
1112+
initial, err := packages.Load(exported.Config, "file="+filename)
1113+
if err != nil {
1114+
t.Fatal(err)
1115+
}
1116+
if len(initial) == 0 {
1117+
t.Fatalf("no packages for %s", filename)
1118+
}
1119+
pkg := initial[0]
1120+
if pkg.PkgPath != test.want {
1121+
t.Errorf("got %s, want %s", pkg.PkgPath, test.want)
1122+
}
1123+
}
1124+
}
1125+
}
1126+
10821127
func TestAdHocPackagesBadImport(t *testing.T) {
10831128
// TODO: Enable this test when github.com/golang/go/issues/33374 is resolved.
10841129
t.Skip()
@@ -2201,10 +2246,14 @@ func testAdHocContains(t *testing.T, exporter packagestest.Exporter) {
22012246
}()
22022247

22032248
exported.Config.Mode = packages.NeedImports | packages.NeedFiles
2249+
exported.Config.Logf = t.Logf
22042250
pkgs, err := packages.Load(exported.Config, "file="+filename)
22052251
if err != nil {
22062252
t.Fatal(err)
22072253
}
2254+
if len(pkgs) == 0 {
2255+
t.Fatalf("no packages for %s", filename)
2256+
}
22082257
if len(pkgs) != 1 && pkgs[0].PkgPath != "command-line-arguments" {
22092258
t.Fatalf("packages.Load: want [command-line-arguments], got %v", pkgs)
22102259
}

0 commit comments

Comments
 (0)