Skip to content

Commit 17213ba

Browse files
committed
gopls/internal/cache/parsego: support lazy object resolution for Files
For the purposes of go/analysis, gopls could not skip syntactic object resolution, as it is part of the go/analysis contract. This prevents analysis from reusing existing type-checked packages, leading to increased CPU and memory during diagnostics. Fix this by making object resolution lazy, and ensuring that all analysed files are resolved prior to analysis. This could introduce a race if gopls were to read the fields set by object resolution, for example if it was printing the tree using ast.Fprint, so we include a test that these fields are only accessed from packages or declarations that are verified to be safe. Since the resolver is not separate from the parser, we fork the code and use go generate to keep it in sync. For golang/go#53275 Change-Id: I24ce94b5d8532c5e679789d2ec1f75376e9e9208 Reviewed-on: https://go-review.googlesource.com/c/tools/+/619516 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 7d1d070 commit 17213ba

File tree

9 files changed

+892
-11
lines changed

9 files changed

+892
-11
lines changed

go/ast/astutil/imports.go

+5
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,12 @@ func RewriteImport(fset *token.FileSet, f *ast.File, oldPath, newPath string) (r
344344
}
345345

346346
// UsesImport reports whether a given import is used.
347+
// The provided File must have been parsed with syntactic object resolution
348+
// (not using go/parser.SkipObjectResolution).
347349
func UsesImport(f *ast.File, path string) (used bool) {
350+
if f.Scope == nil {
351+
panic("file f was not parsed with syntactic object resolution")
352+
}
348353
spec := importSpec(f, path)
349354
if spec == nil {
350355
return

gopls/internal/cache/analysis.go

+13-8
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"errors"
1616
"fmt"
1717
"go/ast"
18-
"go/parser"
1918
"go/token"
2019
"go/types"
2120
"log"
@@ -234,7 +233,10 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
234233
facty = requiredAnalyzers(facty)
235234

236235
// File set for this batch (entire graph) of analysis.
237-
fset := token.NewFileSet()
236+
//
237+
// Start at reservedForParsing so that cached parsed files can be inserted
238+
// into the fileset retroactively.
239+
fset := fileSetWithBase(reservedForParsing)
238240

239241
// Get the metadata graph once for lock-free access during analysis.
240242
meta := s.MetadataGraph()
@@ -261,6 +263,7 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
261263

262264
an = &analysisNode{
263265
allNodes: nodes,
266+
parseCache: s.view.parseCache,
264267
fset: fset,
265268
fsource: s, // expose only ReadFile
266269
viewType: s.View().Type(),
@@ -548,6 +551,7 @@ func (an *analysisNode) decrefPreds() {
548551
// type-checking and analyzing syntax (miss).
549552
type analysisNode struct {
550553
allNodes map[PackageID]*analysisNode // all nodes, for lazy lookup of transitive dependencies
554+
parseCache *parseCache // shared parse cache
551555
fset *token.FileSet // file set shared by entire batch (DAG)
552556
fsource file.Source // Snapshot.ReadFile, for use by Pass.ReadFile
553557
viewType ViewType // type of view
@@ -868,12 +872,13 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) {
868872
for i, fh := range an.files {
869873
i, fh := i, fh
870874
group.Go(func() error {
871-
// Call parseGoImpl directly, not the caching wrapper,
872-
// as cached ASTs require the global FileSet.
873-
// ast.Object resolution is unfortunately an implied part of the
874-
// go/analysis contract.
875-
pgf, err := parseGoImpl(ctx, an.fset, fh, parsego.Full&^parser.SkipObjectResolution, false)
876-
parsed[i] = pgf
875+
// Files fetched from the cache must also have their ast.Ident.Objects
876+
// resolved, as it is part of the analysis contract.
877+
pgfs, err := an.parseCache.parseFiles(ctx, an.fset, parsego.Full, false, fh)
878+
if err == nil {
879+
pgfs[0].Resolve()
880+
}
881+
parsed[i] = pgfs[0]
877882
return err
878883
})
879884
}

gopls/internal/cache/parsego/file.go

+35-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"go/parser"
1010
"go/scanner"
1111
"go/token"
12+
"sync"
1213

1314
"golang.org/x/tools/gopls/internal/protocol"
1415
"golang.org/x/tools/gopls/internal/util/safetoken"
@@ -18,6 +19,10 @@ import (
1819
type File struct {
1920
URI protocol.DocumentURI
2021
Mode parser.Mode
22+
23+
// File is the file resulting from parsing. Clients must not access the AST's
24+
// legacy ast.Object-related fields without first ensuring that
25+
// [File.Resolve] was already called.
2126
File *ast.File
2227
Tok *token.File
2328
// Source code used to build the AST. It may be different from the
@@ -39,13 +44,16 @@ type File struct {
3944
fixedAST bool
4045
Mapper *protocol.Mapper // may map fixed Src, not file content
4146
ParseErr scanner.ErrorList
47+
48+
// resolveOnce guards the lazy ast.Object resolution. See [File.Resolve].
49+
resolveOnce sync.Once
4250
}
4351

44-
func (pgf File) String() string { return string(pgf.URI) }
52+
func (pgf *File) String() string { return string(pgf.URI) }
4553

4654
// Fixed reports whether p was "Fixed", meaning that its source or positions
4755
// may not correlate with the original file.
48-
func (pgf File) Fixed() bool {
56+
func (pgf *File) Fixed() bool {
4957
return pgf.fixedSrc || pgf.fixedAST
5058
}
5159

@@ -100,3 +108,28 @@ func (pgf *File) RangePos(r protocol.Range) (token.Pos, token.Pos, error) {
100108
}
101109
return pgf.Tok.Pos(start), pgf.Tok.Pos(end), nil
102110
}
111+
112+
// Resolve lazily resolves ast.Ident.Objects in the enclosed syntax tree.
113+
//
114+
// Resolve must be called before accessing any of:
115+
// - pgf.File.Scope
116+
// - pgf.File.Unresolved
117+
// - Ident.Obj, for any Ident in pgf.File
118+
func (pgf *File) Resolve() {
119+
pgf.resolveOnce.Do(func() {
120+
if pgf.File.Scope != nil {
121+
return // already resolved by parsing without SkipObjectResolution.
122+
}
123+
defer func() {
124+
// (panic handler duplicated from go/parser)
125+
if e := recover(); e != nil {
126+
// A bailout indicates the resolution stack has exceeded max depth.
127+
if _, ok := e.(bailout); !ok {
128+
panic(e)
129+
}
130+
}
131+
}()
132+
declErr := func(token.Pos, string) {}
133+
resolveFile(pgf.File, pgf.Tok, declErr)
134+
})
135+
}

gopls/internal/cache/parsego/parse.go

+8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:generate go run resolver_gen.go
6+
7+
// The parsego package defines the [File] type, a wrapper around a go/ast.File
8+
// that is useful for answering LSP queries. Notably, it bundles the
9+
// *token.File and *protocol.Mapper necessary for token.Pos locations to and
10+
// from UTF-16 LSP positions.
11+
//
12+
// Run `go generate` to update resolver.go from GOROOT.
513
package parsego
614

715
import (

0 commit comments

Comments
 (0)