Skip to content

Commit 3b13d03

Browse files
committed
gopls/internal/cache: fix bug.Report converting Diagnostic positions
Analyzers are free to add new files to Pass.FileSet. (This is documented, but perhaps not clearly enough, so this CL clarifies the commentary.) Previously, gopls' analysis driver assumed that the token.File for a given Diagnostic must be one of the set of parsed Go files fed to the Pass; the previous point refutes this assumption. This change causes the driver to search for files by name, not *token.File pointer, to allow for re-parsing. The driver still calls bug.Report if the file is not found; this still indicates a likely analyzer bug. This should stem the prolific flow of telemetry field reports from this cause. Also, add a regression test that ensures that the cgocall analyzer, which not only parses but type-checks files anew, actually reports useful diagnostics when run from within gopls. (Previously, it would trigger the bug.Report.) Also, plumb the Snapshot's ReadFile and context down to the Pass so that Pass.ReadFile can make consistent reads from the snapshot instead of using os.ReadFile directly. We also reject attempts by the analyzer to read files other than the permitted ones. Fixes golang/go#66911 Fixes golang/go#64547 Change-Id: I5c35ad6cc5c15c99e9af48aaffb3df2aff621968 Reviewed-on: https://go-review.googlesource.com/c/tools/+/580836 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 7f3a258 commit 3b13d03

File tree

5 files changed

+144
-66
lines changed

5 files changed

+144
-66
lines changed

go/analysis/analysis.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type Pass struct {
9191
Analyzer *Analyzer // the identity of the current analyzer
9292

9393
// syntax and type information
94-
Fset *token.FileSet // file position information
94+
Fset *token.FileSet // file position information; Run may add new files
9595
Files []*ast.File // the abstract syntax tree of each file
9696
OtherFiles []string // names of non-Go files of this package
9797
IgnoredFiles []string // names of ignored source files in this package

go/analysis/diagnostic.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import "go/token"
1212
// which should be a constant, may be used to classify them.
1313
// It is primarily intended to make it easy to look up documentation.
1414
//
15-
// If End is provided, the diagnostic is specified to apply to the range between
15+
// All Pos values are interpreted relative to Pass.Fset. If End is
16+
// provided, the diagnostic is specified to apply to the range between
1617
// Pos and End.
1718
type Diagnostic struct {
1819
Pos token.Pos

gopls/internal/cache/analysis.go

+124-60
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"golang.org/x/tools/gopls/internal/util/bug"
4545
"golang.org/x/tools/gopls/internal/util/frob"
4646
"golang.org/x/tools/gopls/internal/util/maps"
47+
"golang.org/x/tools/gopls/internal/util/slices"
4748
"golang.org/x/tools/internal/analysisinternal"
4849
"golang.org/x/tools/internal/event"
4950
"golang.org/x/tools/internal/facts"
@@ -255,6 +256,7 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
255256

256257
an = &analysisNode{
257258
fset: fset,
259+
fsource: struct{ file.Source }{s}, // expose only ReadFile
258260
mp: mp,
259261
analyzers: facty, // all nodes run at least the facty analyzers
260262
allDeps: make(map[PackagePath]*analysisNode),
@@ -519,6 +521,7 @@ func (an *analysisNode) decrefPreds() {
519521
// type-checking and analyzing syntax (miss).
520522
type analysisNode struct {
521523
fset *token.FileSet // file set shared by entire batch (DAG)
524+
fsource file.Source // Snapshot.ReadFile, for use by Pass.ReadFile
522525
mp *metadata.Package // metadata for this package
523526
files []file.Handle // contents of CompiledGoFiles
524527
analyzers []*analysis.Analyzer // set of analyzers to run
@@ -885,6 +888,7 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) {
885888
}
886889
act = &action{
887890
a: a,
891+
fsource: an.fsource,
888892
stableName: an.stableNames[a],
889893
pkg: pkg,
890894
vdeps: an.succs,
@@ -902,7 +906,7 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) {
902906
}
903907

904908
// Execute the graph in parallel.
905-
execActions(roots)
909+
execActions(ctx, roots)
906910
// Inv: each root's summary is set (whether success or error).
907911

908912
// Don't return (or cache) the result in case of cancellation.
@@ -1135,7 +1139,8 @@ type analysisPackage struct {
11351139
type action struct {
11361140
once sync.Once
11371141
a *analysis.Analyzer
1138-
stableName string // cross-process stable name of analyzer
1142+
fsource file.Source // Snapshot.ReadFile, for Pass.ReadFile
1143+
stableName string // cross-process stable name of analyzer
11391144
pkg *analysisPackage
11401145
hdeps []*action // horizontal dependencies
11411146
vdeps map[PackageID]*analysisNode // vertical dependencies
@@ -1152,16 +1157,16 @@ func (act *action) String() string {
11521157

11531158
// execActions executes a set of action graph nodes in parallel.
11541159
// Postcondition: each action.summary is set, even in case of error.
1155-
func execActions(actions []*action) {
1160+
func execActions(ctx context.Context, actions []*action) {
11561161
var wg sync.WaitGroup
11571162
for _, act := range actions {
11581163
act := act
11591164
wg.Add(1)
11601165
go func() {
11611166
defer wg.Done()
11621167
act.once.Do(func() {
1163-
execActions(act.hdeps) // analyze "horizontal" dependencies
1164-
act.result, act.summary, act.err = act.exec()
1168+
execActions(ctx, act.hdeps) // analyze "horizontal" dependencies
1169+
act.result, act.summary, act.err = act.exec(ctx)
11651170
if act.err != nil {
11661171
act.summary = &actionSummary{Err: act.err.Error()}
11671172
// TODO(adonovan): suppress logging. But
@@ -1185,7 +1190,7 @@ func execActions(actions []*action) {
11851190
// along with its (serializable) facts and diagnostics.
11861191
// Or it returns an error if the analyzer did not run to
11871192
// completion and deliver a valid result.
1188-
func (act *action) exec() (interface{}, *actionSummary, error) {
1193+
func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
11891194
analyzer := act.a
11901195
pkg := act.pkg
11911196

@@ -1284,75 +1289,114 @@ func (act *action) exec() (interface{}, *actionSummary, error) {
12841289
factFilter[reflect.TypeOf(f)] = true
12851290
}
12861291

1287-
// If the package contains "fixed" files, it's not necessarily an error if we
1288-
// can't convert positions.
1289-
hasFixedFiles := false
1290-
for _, p := range pkg.parsed {
1291-
if p.Fixed() {
1292-
hasFixedFiles = true
1293-
break
1294-
}
1295-
}
1296-
12971292
// posToLocation converts from token.Pos to protocol form.
1298-
// TODO(adonovan): improve error messages.
12991293
posToLocation := func(start, end token.Pos) (protocol.Location, error) {
13001294
tokFile := pkg.fset.File(start)
13011295

1296+
// Find existing mapper by file name.
1297+
// (Don't require an exact token.File match
1298+
// as the analyzer may have re-parsed the file.)
1299+
var (
1300+
mapper *protocol.Mapper
1301+
fixed bool
1302+
)
13021303
for _, p := range pkg.parsed {
1303-
if p.Tok == tokFile {
1304-
if end == token.NoPos {
1305-
end = start
1306-
}
1304+
if p.Tok.Name() == tokFile.Name() {
1305+
mapper = p.Mapper
1306+
fixed = p.Fixed() // suppress some assertions after parser recovery
1307+
break
1308+
}
1309+
}
1310+
if mapper == nil {
1311+
// The start position was not among the package's parsed
1312+
// Go files, indicating that the analyzer added new files
1313+
// to the FileSet.
1314+
//
1315+
// For example, the cgocall analyzer re-parses and
1316+
// type-checks some of the files in a special environment;
1317+
// and asmdecl and other low-level runtime analyzers call
1318+
// ReadFile to parse non-Go files.
1319+
// (This is a supported feature, documented at go/analysis.)
1320+
//
1321+
// In principle these files could be:
1322+
//
1323+
// - OtherFiles (non-Go files such as asm).
1324+
// However, we set Pass.OtherFiles=[] because
1325+
// gopls won't service "diagnose" requests
1326+
// for non-Go files, so there's no point
1327+
// reporting diagnostics in them.
1328+
//
1329+
// - IgnoredFiles (files tagged for other configs).
1330+
// However, we set Pass.IgnoredFiles=[] because,
1331+
// in most cases, zero-config gopls should create
1332+
// another view that covers these files.
1333+
//
1334+
// - Referents of //line directives, as in cgo packages.
1335+
// The file names in this case are not known a priori.
1336+
// gopls generally tries to avoid honoring line directives,
1337+
// but analyzers such as cgocall may honor them.
1338+
//
1339+
// In short, it's unclear how this can be reached
1340+
// other than due to an analyzer bug.
1341+
return protocol.Location{}, bug.Errorf("diagnostic location is not among files of package: %s", tokFile.Name())
1342+
}
1343+
// Inv: mapper != nil
13071344

1308-
// debugging #64547
1309-
fileStart := token.Pos(tokFile.Base())
1310-
fileEnd := fileStart + token.Pos(tokFile.Size())
1311-
if start < fileStart {
1312-
bug.Reportf("start < start of file")
1313-
start = fileStart
1314-
}
1315-
if end < start {
1316-
// This can happen if End is zero (#66683)
1317-
// or a small positive displacement from zero
1318-
// due to recursively Node.End() computation.
1319-
// This usually arises from poor parser recovery
1320-
// of an incomplete term at EOF.
1321-
bug.Reportf("end < start of file")
1322-
end = fileEnd
1323-
}
1324-
if end > fileEnd+1 {
1325-
bug.Reportf("end > end of file + 1")
1326-
end = fileEnd
1327-
}
1345+
if end == token.NoPos {
1346+
end = start
1347+
}
13281348

1329-
return p.PosLocation(start, end)
1349+
// debugging #64547
1350+
fileStart := token.Pos(tokFile.Base())
1351+
fileEnd := fileStart + token.Pos(tokFile.Size())
1352+
if start < fileStart {
1353+
if !fixed {
1354+
bug.Reportf("start < start of file")
13301355
}
1356+
start = fileStart
13311357
}
1332-
errorf := bug.Errorf
1333-
if hasFixedFiles {
1334-
errorf = fmt.Errorf
1358+
if end < start {
1359+
// This can happen if End is zero (#66683)
1360+
// or a small positive displacement from zero
1361+
// due to recursive Node.End() computation.
1362+
// This usually arises from poor parser recovery
1363+
// of an incomplete term at EOF.
1364+
if !fixed {
1365+
bug.Reportf("end < start of file")
1366+
}
1367+
end = fileEnd
1368+
}
1369+
if end > fileEnd+1 {
1370+
if !fixed {
1371+
bug.Reportf("end > end of file + 1")
1372+
}
1373+
end = fileEnd
13351374
}
1336-
return protocol.Location{}, errorf("token.Pos not within package")
1375+
1376+
return mapper.PosLocation(tokFile, start, end)
13371377
}
13381378

13391379
// Now run the (pkg, analyzer) action.
13401380
var diagnostics []gobDiagnostic
1381+
13411382
pass := &analysis.Pass{
1342-
Analyzer: analyzer,
1343-
Fset: pkg.fset,
1344-
Files: pkg.files,
1345-
Pkg: pkg.types,
1346-
TypesInfo: pkg.typesInfo,
1347-
TypesSizes: pkg.typesSizes,
1348-
TypeErrors: pkg.typeErrors,
1349-
ResultOf: inputs,
1383+
Analyzer: analyzer,
1384+
Fset: pkg.fset,
1385+
Files: pkg.files,
1386+
OtherFiles: nil, // since gopls doesn't handle non-Go (e.g. asm) files
1387+
IgnoredFiles: nil, // zero-config gopls should analyze these files in another view
1388+
Pkg: pkg.types,
1389+
TypesInfo: pkg.typesInfo,
1390+
TypesSizes: pkg.typesSizes,
1391+
TypeErrors: pkg.typeErrors,
1392+
ResultOf: inputs,
13501393
Report: func(d analysis.Diagnostic) {
13511394
diagnostic, err := toGobDiagnostic(posToLocation, analyzer, d)
13521395
if err != nil {
1353-
if !hasFixedFiles {
1354-
bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err)
1355-
}
1396+
// Don't bug.Report here: these errors all originate in
1397+
// posToLocation, and we can more accurately discriminate
1398+
// severe errors from benign ones in that function.
1399+
event.Error(ctx, fmt.Sprintf("internal error converting diagnostic from analyzer %q", analyzer.Name), err)
13561400
return
13571401
}
13581402
diagnostics = append(diagnostics, diagnostic)
@@ -1364,9 +1408,29 @@ func (act *action) exec() (interface{}, *actionSummary, error) {
13641408
AllObjectFacts: func() []analysis.ObjectFact { return factset.AllObjectFacts(factFilter) },
13651409
AllPackageFacts: func() []analysis.PackageFact { return factset.AllPackageFacts(factFilter) },
13661410
}
1367-
// TODO(adonovan): integrate this into the snapshot's file
1368-
// cache and its dependency analysis.
1369-
pass.ReadFile = analysisinternal.MakeReadFile(pass)
1411+
1412+
pass.ReadFile = func(filename string) ([]byte, error) {
1413+
// Read file from snapshot, to ensure reads are consistent.
1414+
//
1415+
// TODO(adonovan): make the dependency analysis sound by
1416+
// incorporating these additional files into the the analysis
1417+
// hash. This requires either (a) preemptively reading and
1418+
// hashing a potentially large number of mostly irrelevant
1419+
// files; or (b) some kind of dynamic dependency discovery
1420+
// system like used in Bazel for C++ headers. Neither entices.
1421+
if err := analysisinternal.CheckReadable(pass, filename); err != nil {
1422+
return nil, err
1423+
}
1424+
h, err := act.fsource.ReadFile(ctx, protocol.URIFromPath(filename))
1425+
if err != nil {
1426+
return nil, err
1427+
}
1428+
content, err := h.Content()
1429+
if err != nil {
1430+
return nil, err // file doesn't exist
1431+
}
1432+
return slices.Clone(content), nil // follow ownership of os.ReadFile
1433+
}
13701434

13711435
// Recover from panics (only) within the analyzer logic.
13721436
// (Use an anonymous function to limit the recover scope.)

gopls/internal/test/marker/testdata/diagnostics/analyzers.txt

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Test of warning diagnostics from various analyzers:
2-
copylocks, printf, slog, tests, timeformat, and nilness.
2+
copylocks, printf, slog, tests, timeformat, nilness, and cgocall.
33

44
-- go.mod --
55
module example.com
@@ -55,6 +55,19 @@ func _(s struct{x int}) {
5555
s.x = 1 //@diag("x", re"unused write to field x")
5656
}
5757

58+
-- cgocall.go --
59+
package analyzer
60+
61+
import "unsafe"
62+
63+
// void f(void *ptr) {}
64+
import "C"
65+
66+
// cgocall
67+
func _(c chan bool) {
68+
C.f(unsafe.Pointer(&c)) //@ diag("unsafe", re"passing Go type with embedded pointer to C")
69+
}
70+
5871
-- bad_test_go121.go --
5972
//go:build go1.21
6073

internal/analysisinternal/analysis.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -399,15 +399,15 @@ func equivalentTypes(want, got types.Type) bool {
399399
// MakeReadFile returns a simple implementation of the Pass.ReadFile function.
400400
func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) {
401401
return func(filename string) ([]byte, error) {
402-
if err := checkReadable(pass, filename); err != nil {
402+
if err := CheckReadable(pass, filename); err != nil {
403403
return nil, err
404404
}
405405
return os.ReadFile(filename)
406406
}
407407
}
408408

409-
// checkReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
410-
func checkReadable(pass *analysis.Pass, filename string) error {
409+
// CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
410+
func CheckReadable(pass *analysis.Pass, filename string) error {
411411
if slicesContains(pass.OtherFiles, filename) ||
412412
slicesContains(pass.IgnoredFiles, filename) {
413413
return nil

0 commit comments

Comments
 (0)