Skip to content

Commit ad4fc28

Browse files
committed
gopls/internal/lsp/cache: pre-compute load diagnostics
Process go/packages errors immediately after loading, to eliminate a dependency on syntax packages. This simplifies the cache.Package type, which is now just a simple join of metadata and type-checked syntax. For golang/go#57987 Change-Id: I3d120fb4cb5687df2a376f8d8617e23e16ddaa92 Reviewed-on: https://go-review.googlesource.com/c/tools/+/468776 gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent e5c9e63 commit ad4fc28

File tree

7 files changed

+184
-116
lines changed

7 files changed

+184
-116
lines changed

gopls/internal/lsp/cache/check.go

+23-20
Original file line numberDiff line numberDiff line change
@@ -636,12 +636,12 @@ func parseCompiledGoFiles(ctx context.Context, compiledGoFiles []source.FileHand
636636
// These may be attached to import declarations in the transitive source files
637637
// of pkg, or to 'requires' declarations in the package's go.mod file.
638638
//
639-
// TODO(rfindley): move this to errors.go
640-
func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsErrors []*packagesinternal.PackageError) ([]*source.Diagnostic, error) {
639+
// TODO(rfindley): move this to load.go
640+
func depsErrors(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages map[PackageID]PackagePath) ([]*source.Diagnostic, error) {
641641
// Select packages that can't be found, and were imported in non-workspace packages.
642642
// Workspace packages already show their own errors.
643643
var relevantErrors []*packagesinternal.PackageError
644-
for _, depsError := range depsErrors {
644+
for _, depsError := range m.DepsErrors {
645645
// Up to Go 1.15, the missing package was included in the stack, which
646646
// was presumably a bug. We want the next one up.
647647
directImporterIdx := len(depsError.ImportStack) - 1
@@ -650,7 +650,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsError
650650
}
651651

652652
directImporter := depsError.ImportStack[directImporterIdx]
653-
if s.isWorkspacePackage(PackageID(directImporter)) {
653+
if _, ok := workspacePackages[PackageID(directImporter)]; ok {
654654
continue
655655
}
656656
relevantErrors = append(relevantErrors, depsError)
@@ -661,21 +661,31 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsError
661661
return nil, nil
662662
}
663663

664+
// Subsequent checks require Go files.
665+
if len(m.CompiledGoFiles) == 0 {
666+
return nil, nil
667+
}
668+
664669
// Build an index of all imports in the package.
665670
type fileImport struct {
666671
cgf *source.ParsedGoFile
667672
imp *ast.ImportSpec
668673
}
669674
allImports := map[string][]fileImport{}
670-
for _, cgf := range pkg.compiledGoFiles {
675+
for _, uri := range m.CompiledGoFiles {
676+
pgf, err := parseGoURI(ctx, fs, uri, source.ParseHeader)
677+
if err != nil {
678+
return nil, err
679+
}
680+
fset := source.SingletonFileSet(pgf.Tok)
671681
// TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok).
672-
for _, group := range astutil.Imports(pkg.fset, cgf.File) {
682+
for _, group := range astutil.Imports(fset, pgf.File) {
673683
for _, imp := range group {
674684
if imp.Path == nil {
675685
continue
676686
}
677687
path := strings.Trim(imp.Path.Value, `"`)
678-
allImports[path] = append(allImports[path], fileImport{cgf, imp})
688+
allImports[path] = append(allImports[path], fileImport{pgf, imp})
679689
}
680690
}
681691
}
@@ -686,7 +696,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsError
686696
for _, depErr := range relevantErrors {
687697
for i := len(depErr.ImportStack) - 1; i >= 0; i-- {
688698
item := depErr.ImportStack[i]
689-
if s.isWorkspacePackage(PackageID(item)) {
699+
if _, ok := workspacePackages[PackageID(item)]; ok {
690700
break
691701
}
692702

@@ -695,7 +705,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsError
695705
if err != nil {
696706
return nil, err
697707
}
698-
fixes, err := goGetQuickFixes(s, imp.cgf.URI, item)
708+
fixes, err := goGetQuickFixes(m.Module != nil, imp.cgf.URI, item)
699709
if err != nil {
700710
return nil, err
701711
}
@@ -711,18 +721,11 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsError
711721
}
712722
}
713723

714-
if len(pkg.compiledGoFiles) == 0 {
715-
return errors, nil
716-
}
717-
mod := s.GoModForFile(pkg.compiledGoFiles[0].URI)
718-
if mod == "" {
719-
return errors, nil
720-
}
721-
fh, err := s.GetFile(ctx, mod)
724+
modFile, err := nearestModFile(ctx, m.CompiledGoFiles[0], fs)
722725
if err != nil {
723726
return nil, err
724727
}
725-
pm, err := s.ParseMod(ctx, fh)
728+
pm, err := parseModURI(ctx, fs, modFile)
726729
if err != nil {
727730
return nil, err
728731
}
@@ -732,7 +735,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsError
732735
for _, depErr := range relevantErrors {
733736
for i := len(depErr.ImportStack) - 1; i >= 0; i-- {
734737
item := depErr.ImportStack[i]
735-
m := s.Metadata(PackageID(item))
738+
m := meta.metadata[PackageID(item)]
736739
if m == nil || m.Module == nil {
737740
continue
738741
}
@@ -745,7 +748,7 @@ func (s *snapshot) depsErrors(ctx context.Context, pkg *syntaxPackage, depsError
745748
if err != nil {
746749
return nil, err
747750
}
748-
fixes, err := goGetQuickFixes(s, pm.URI, item)
751+
fixes, err := goGetQuickFixes(true, pm.URI, item)
749752
if err != nil {
750753
return nil, err
751754
}

gopls/internal/lsp/cache/debug.go

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ func debugf(format string, args ...interface{}) {
3030

3131
// If debugEnabled is true, dumpWorkspace prints a summary of workspace
3232
// packages to stderr. If debugEnabled is false, it is a no-op.
33+
//
34+
// TODO(rfindley): this has served its purpose. Delete.
3335
func (s *snapshot) dumpWorkspace(context string) {
3436
if !debugEnabled {
3537
return

gopls/internal/lsp/cache/errors.go

+71-24
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ package cache
99
// source.Diagnostic form, and suggesting quick fixes.
1010

1111
import (
12+
"context"
1213
"fmt"
1314
"go/scanner"
15+
"go/token"
1416
"go/types"
1517
"log"
1618
"regexp"
@@ -28,20 +30,26 @@ import (
2830
"golang.org/x/tools/internal/typesinternal"
2931
)
3032

31-
func goPackagesErrorDiagnostics(e packages.Error, pkg *syntaxPackage, fromDir string) (diags []*source.Diagnostic, rerr error) {
32-
if diag, ok := parseGoListImportCycleError(e, pkg); ok {
33+
// goPackagesErrorDiagnostics translates the given go/packages Error into a
34+
// diagnostic, using the provided metadata and filesource.
35+
//
36+
// The slice of diagnostics may be empty.
37+
func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) ([]*source.Diagnostic, error) {
38+
if diag, err := parseGoListImportCycleError(ctx, e, m, fs); err != nil {
39+
return nil, err
40+
} else if diag != nil {
3341
return []*source.Diagnostic{diag}, nil
3442
}
3543

3644
var spn span.Span
3745
if e.Pos == "" {
38-
spn = parseGoListError(e.Msg, fromDir)
46+
spn = parseGoListError(e.Msg, m.LoadDir)
3947
// We may not have been able to parse a valid span. Apply the errors to all files.
40-
if _, err := spanToRange(pkg, spn); err != nil {
48+
if _, err := spanToRange(ctx, fs, spn); err != nil {
4149
var diags []*source.Diagnostic
42-
for _, pgf := range pkg.compiledGoFiles {
50+
for _, uri := range m.CompiledGoFiles {
4351
diags = append(diags, &source.Diagnostic{
44-
URI: pgf.URI,
52+
URI: uri,
4553
Severity: protocol.SeverityError,
4654
Source: source.ListError,
4755
Message: e.Msg,
@@ -50,7 +58,7 @@ func goPackagesErrorDiagnostics(e packages.Error, pkg *syntaxPackage, fromDir st
5058
return diags, nil
5159
}
5260
} else {
53-
spn = span.ParseInDir(e.Pos, fromDir)
61+
spn = span.ParseInDir(e.Pos, m.LoadDir)
5462
}
5563

5664
// TODO(rfindley): in some cases the go command outputs invalid spans, for
@@ -64,7 +72,7 @@ func goPackagesErrorDiagnostics(e packages.Error, pkg *syntaxPackage, fromDir st
6472
// likely because *token.File lacks information about newline termination.
6573
//
6674
// We could do better here by handling that case.
67-
rng, err := spanToRange(pkg, spn)
75+
rng, err := spanToRange(ctx, fs, spn)
6876
if err != nil {
6977
return nil, err
7078
}
@@ -136,7 +144,7 @@ func typeErrorDiagnostics(snapshot *snapshot, pkg *syntaxPackage, e extendedErro
136144
}
137145

138146
if match := importErrorRe.FindStringSubmatch(e.primary.Msg); match != nil {
139-
diag.SuggestedFixes, err = goGetQuickFixes(snapshot, loc.URI.SpanURI(), match[1])
147+
diag.SuggestedFixes, err = goGetQuickFixes(snapshot.moduleMode(), loc.URI.SpanURI(), match[1])
140148
if err != nil {
141149
return nil, err
142150
}
@@ -150,9 +158,8 @@ func typeErrorDiagnostics(snapshot *snapshot, pkg *syntaxPackage, e extendedErro
150158
return []*source.Diagnostic{diag}, nil
151159
}
152160

153-
func goGetQuickFixes(snapshot *snapshot, uri span.URI, pkg string) ([]source.SuggestedFix, error) {
154-
// Go get only supports module mode for now.
155-
if snapshot.workspaceMode()&moduleMode == 0 {
161+
func goGetQuickFixes(moduleMode bool, uri span.URI, pkg string) ([]source.SuggestedFix, error) {
162+
if !moduleMode {
156163
return nil, nil
157164
}
158165
title := fmt.Sprintf("go get package %v", pkg)
@@ -312,14 +319,20 @@ func typeErrorData(pkg *syntaxPackage, terr types.Error) (typesinternal.ErrorCod
312319
return ecode, loc, err
313320
}
314321

315-
// spanToRange converts a span.Span to a protocol.Range,
316-
// assuming that the span belongs to the package whose diagnostics are being computed.
317-
func spanToRange(pkg *syntaxPackage, spn span.Span) (protocol.Range, error) {
318-
pgf, err := pkg.File(spn.URI())
322+
// spanToRange converts a span.Span to a protocol.Range, by mapping content
323+
// contained in the provided FileSource.
324+
func spanToRange(ctx context.Context, fs source.FileSource, spn span.Span) (protocol.Range, error) {
325+
uri := spn.URI()
326+
fh, err := fs.GetFile(ctx, uri)
327+
if err != nil {
328+
return protocol.Range{}, err
329+
}
330+
content, err := fh.Read()
319331
if err != nil {
320332
return protocol.Range{}, err
321333
}
322-
return pgf.Mapper.SpanRange(spn)
334+
mapper := protocol.NewMapper(uri, content)
335+
return mapper.SpanRange(spn)
323336
}
324337

325338
// parseGoListError attempts to parse a standard `go list` error message
@@ -338,28 +351,36 @@ func parseGoListError(input, wd string) span.Span {
338351
return span.ParseInDir(input[:msgIndex], wd)
339352
}
340353

341-
func parseGoListImportCycleError(e packages.Error, pkg *syntaxPackage) (*source.Diagnostic, bool) {
354+
// parseGoListImportCycleError attempts to parse the given go/packages error as
355+
// an import cycle, returning a diagnostic if successful.
356+
//
357+
// If the error is not detected as an import cycle error, it returns nil, nil.
358+
func parseGoListImportCycleError(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) (*source.Diagnostic, error) {
342359
re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`)
343360
matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg))
344361
if len(matches) < 3 {
345-
return nil, false
362+
return nil, nil
346363
}
347364
msg := matches[1]
348365
importList := strings.Split(matches[2], " ")
349366
// Since the error is relative to the current package. The import that is causing
350367
// the import cycle error is the second one in the list.
351368
if len(importList) < 2 {
352-
return nil, false
369+
return nil, nil
353370
}
354371
// Imports have quotation marks around them.
355372
circImp := strconv.Quote(importList[1])
356-
for _, pgf := range pkg.compiledGoFiles {
373+
for _, uri := range m.CompiledGoFiles {
374+
pgf, err := parseGoURI(ctx, fs, uri, source.ParseHeader)
375+
if err != nil {
376+
return nil, err
377+
}
357378
// Search file imports for the import that is causing the import cycle.
358379
for _, imp := range pgf.File.Imports {
359380
if imp.Path.Value == circImp {
360381
rng, err := pgf.NodeMappedRange(imp)
361382
if err != nil {
362-
return nil, false
383+
return nil, nil
363384
}
364385

365386
return &source.Diagnostic{
@@ -368,9 +389,35 @@ func parseGoListImportCycleError(e packages.Error, pkg *syntaxPackage) (*source.
368389
Severity: protocol.SeverityError,
369390
Source: source.ListError,
370391
Message: msg,
371-
}, true
392+
}, nil
372393
}
373394
}
374395
}
375-
return nil, false
396+
return nil, nil
397+
}
398+
399+
// parseGoURI is a helper to parse the Go file at the given URI from the file
400+
// source fs. The resulting syntax and token.File belong to an ephemeral,
401+
// encapsulated FileSet, so this file stands only on its own: it's not suitable
402+
// to use in a list of file of a package, for example.
403+
//
404+
// It returns an error if the file could not be read.
405+
func parseGoURI(ctx context.Context, fs source.FileSource, uri span.URI, mode source.ParseMode) (*source.ParsedGoFile, error) {
406+
fh, err := fs.GetFile(ctx, uri)
407+
if err != nil {
408+
return nil, err
409+
}
410+
return parseGoImpl(ctx, token.NewFileSet(), fh, source.ParseHeader)
411+
}
412+
413+
// parseModURI is a helper to parse the Mod file at the given URI from the file
414+
// source fs.
415+
//
416+
// It returns an error if the file could not be read.
417+
func parseModURI(ctx context.Context, fs source.FileSource, uri span.URI) (*source.ParsedModule, error) {
418+
fh, err := fs.GetFile(ctx, uri)
419+
if err != nil {
420+
return nil, err
421+
}
422+
return parseModImpl(ctx, fh)
376423
}

gopls/internal/lsp/cache/load.go

+53-3
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,20 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
229229

230230
event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates)))
231231

232-
s.meta = s.meta.Clone(updates)
233-
s.resetIsActivePackageLocked()
232+
// Before mutating the snapshot, ensure that we compute load diagnostics
233+
// successfully. This could fail if the context is cancelled, and we don't
234+
// want to leave the snapshot metadata in a partial state.
235+
meta := s.meta.Clone(updates)
236+
workspacePackages := computeWorkspacePackagesLocked(s, meta)
237+
for _, update := range updates {
238+
if err := computeLoadDiagnostics(ctx, update, meta, lockedSnapshot{s}, workspacePackages); err != nil {
239+
return err
240+
}
241+
}
242+
s.meta = meta
243+
s.workspacePackages = workspacePackages
234244

235-
s.workspacePackages = computeWorkspacePackagesLocked(s, s.meta)
245+
s.resetIsActivePackageLocked()
236246
s.dumpWorkspace("load")
237247
s.mu.Unlock()
238248

@@ -549,6 +559,46 @@ func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Con
549559
m.DepsByImpPath = depsByImpPath
550560
m.DepsByPkgPath = depsByPkgPath
551561

562+
// m.Diagnostics is set later in the loading pass, using
563+
// computeLoadDiagnostics.
564+
565+
return nil
566+
}
567+
568+
// computeLoadDiagnostics computes and sets m.Diagnostics for the given metadata m.
569+
//
570+
// It should only be called during metadata construction in snapshot.load.
571+
func computeLoadDiagnostics(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages map[PackageID]PackagePath) error {
572+
for _, packagesErr := range m.Errors {
573+
// Filter out parse errors from go list. We'll get them when we
574+
// actually parse, and buggy overlay support may generate spurious
575+
// errors. (See TestNewModule_Issue38207.)
576+
if strings.Contains(packagesErr.Msg, "expected '") {
577+
continue
578+
}
579+
pkgDiags, err := goPackagesErrorDiagnostics(ctx, packagesErr, m, fs)
580+
if err != nil {
581+
// There are certain cases where the go command returns invalid
582+
// positions, so we cannot panic or even bug.Reportf here.
583+
event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(m.ID)))
584+
continue
585+
}
586+
m.Diagnostics = append(m.Diagnostics, pkgDiags...)
587+
}
588+
589+
// TODO(rfindley): this is buggy: an insignificant change to a modfile
590+
// (or an unsaved modfile) could affect the position of deps errors,
591+
// without invalidating the package.
592+
depsDiags, err := depsErrors(ctx, m, meta, fs, workspacePackages)
593+
if err != nil {
594+
if ctx.Err() == nil {
595+
// TODO(rfindley): consider making this a bug.Reportf. depsErrors should
596+
// not normally fail.
597+
event.Error(ctx, "unable to compute deps errors", err, tag.Package.Of(string(m.ID)))
598+
}
599+
return nil
600+
}
601+
m.Diagnostics = append(m.Diagnostics, depsDiags...)
552602
return nil
553603
}
554604

0 commit comments

Comments
 (0)