Skip to content

Commit cd9151d

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/cache: fix bug in toGobDiagnostic(Related)
Analyzers report Diagnostics, SuggestedFixes, and RelatedInformation containing token.Pos positions that need to be mapped to protocol.Location. Unlike the first two kinds, RelatedInformation positions needn't be in the current package, so the existing assertion was overstrict. This CL relaxes the assertion and attempts to map positions in dependencies to protocol.Location form, though it can do so at best heuristically since Mappers are not generally available. Also, extract posToLocation to a top-level function. + test Fixes golang/go#70791 Change-Id: Ibec2804b1b4b0574edd53b8ae2e0484630f01d6e Reviewed-on: https://go-review.googlesource.com/c/tools/+/667517 Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]>
1 parent d3a3775 commit cd9151d

File tree

2 files changed

+158
-92
lines changed

2 files changed

+158
-92
lines changed

gopls/internal/cache/analysis.go

+129-92
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"golang.org/x/tools/gopls/internal/util/frob"
4545
"golang.org/x/tools/gopls/internal/util/moremaps"
4646
"golang.org/x/tools/gopls/internal/util/persistent"
47+
"golang.org/x/tools/gopls/internal/util/safetoken"
4748
"golang.org/x/tools/internal/analysisinternal"
4849
"golang.org/x/tools/internal/event"
4950
"golang.org/x/tools/internal/facts"
@@ -1019,93 +1020,6 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
10191020
factFilter[reflect.TypeOf(f)] = true
10201021
}
10211022

1022-
// posToLocation converts from token.Pos to protocol form.
1023-
posToLocation := func(start, end token.Pos) (protocol.Location, error) {
1024-
tokFile := apkg.pkg.FileSet().File(start)
1025-
1026-
// Find existing mapper by file name.
1027-
// (Don't require an exact token.File match
1028-
// as the analyzer may have re-parsed the file.)
1029-
var (
1030-
mapper *protocol.Mapper
1031-
fixed bool
1032-
)
1033-
for _, p := range apkg.pkg.CompiledGoFiles() {
1034-
if p.Tok.Name() == tokFile.Name() {
1035-
mapper = p.Mapper
1036-
fixed = p.Fixed() // suppress some assertions after parser recovery
1037-
break
1038-
}
1039-
}
1040-
if mapper == nil {
1041-
// The start position was not among the package's parsed
1042-
// Go files, indicating that the analyzer added new files
1043-
// to the FileSet.
1044-
//
1045-
// For example, the cgocall analyzer re-parses and
1046-
// type-checks some of the files in a special environment;
1047-
// and asmdecl and other low-level runtime analyzers call
1048-
// ReadFile to parse non-Go files.
1049-
// (This is a supported feature, documented at go/analysis.)
1050-
//
1051-
// In principle these files could be:
1052-
//
1053-
// - OtherFiles (non-Go files such as asm).
1054-
// However, we set Pass.OtherFiles=[] because
1055-
// gopls won't service "diagnose" requests
1056-
// for non-Go files, so there's no point
1057-
// reporting diagnostics in them.
1058-
//
1059-
// - IgnoredFiles (files tagged for other configs).
1060-
// However, we set Pass.IgnoredFiles=[] because,
1061-
// in most cases, zero-config gopls should create
1062-
// another view that covers these files.
1063-
//
1064-
// - Referents of //line directives, as in cgo packages.
1065-
// The file names in this case are not known a priori.
1066-
// gopls generally tries to avoid honoring line directives,
1067-
// but analyzers such as cgocall may honor them.
1068-
//
1069-
// In short, it's unclear how this can be reached
1070-
// other than due to an analyzer bug.
1071-
return protocol.Location{}, bug.Errorf("diagnostic location is not among files of package: %s", tokFile.Name())
1072-
}
1073-
// Inv: mapper != nil
1074-
1075-
if end == token.NoPos {
1076-
end = start
1077-
}
1078-
1079-
// debugging #64547
1080-
fileStart := token.Pos(tokFile.Base())
1081-
fileEnd := fileStart + token.Pos(tokFile.Size())
1082-
if start < fileStart {
1083-
if !fixed {
1084-
bug.Reportf("start < start of file")
1085-
}
1086-
start = fileStart
1087-
}
1088-
if end < start {
1089-
// This can happen if End is zero (#66683)
1090-
// or a small positive displacement from zero
1091-
// due to recursive Node.End() computation.
1092-
// This usually arises from poor parser recovery
1093-
// of an incomplete term at EOF.
1094-
if !fixed {
1095-
bug.Reportf("end < start of file")
1096-
}
1097-
end = fileEnd
1098-
}
1099-
if end > fileEnd+1 {
1100-
if !fixed {
1101-
bug.Reportf("end > end of file + 1")
1102-
}
1103-
end = fileEnd
1104-
}
1105-
1106-
return mapper.PosLocation(tokFile, start, end)
1107-
}
1108-
11091023
// Now run the (pkg, analyzer) action.
11101024
var diagnostics []gobDiagnostic
11111025

@@ -1130,7 +1044,7 @@ func (act *action) exec(ctx context.Context) (any, *actionSummary, error) {
11301044
bug.Reportf("invalid SuggestedFixes: %v", err)
11311045
d.SuggestedFixes = nil
11321046
}
1133-
diagnostic, err := toGobDiagnostic(posToLocation, analyzer, d)
1047+
diagnostic, err := toGobDiagnostic(apkg.pkg, analyzer, d)
11341048
if err != nil {
11351049
// Don't bug.Report here: these errors all originate in
11361050
// posToLocation, and we can more accurately discriminate
@@ -1322,12 +1236,12 @@ type gobTextEdit struct {
13221236

13231237
// toGobDiagnostic converts an analysis.Diagnosic to a serializable gobDiagnostic,
13241238
// which requires expanding token.Pos positions into protocol.Location form.
1325-
func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location, error), a *analysis.Analyzer, diag analysis.Diagnostic) (gobDiagnostic, error) {
1239+
func toGobDiagnostic(pkg *Package, a *analysis.Analyzer, diag analysis.Diagnostic) (gobDiagnostic, error) {
13261240
var fixes []gobSuggestedFix
13271241
for _, fix := range diag.SuggestedFixes {
13281242
var gobEdits []gobTextEdit
13291243
for _, textEdit := range fix.TextEdits {
1330-
loc, err := posToLocation(textEdit.Pos, textEdit.End)
1244+
loc, err := diagnosticPosToLocation(pkg, false, textEdit.Pos, textEdit.End)
13311245
if err != nil {
13321246
return gobDiagnostic{}, fmt.Errorf("in SuggestedFixes: %w", err)
13331247
}
@@ -1344,7 +1258,10 @@ func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location
13441258

13451259
var related []gobRelatedInformation
13461260
for _, r := range diag.Related {
1347-
loc, err := posToLocation(r.Pos, r.End)
1261+
// The position of RelatedInformation may be
1262+
// within another (dependency) package.
1263+
const allowDeps = true
1264+
loc, err := diagnosticPosToLocation(pkg, allowDeps, r.Pos, r.End)
13481265
if err != nil {
13491266
return gobDiagnostic{}, fmt.Errorf("in Related: %w", err)
13501267
}
@@ -1354,7 +1271,7 @@ func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location
13541271
})
13551272
}
13561273

1357-
loc, err := posToLocation(diag.Pos, diag.End)
1274+
loc, err := diagnosticPosToLocation(pkg, false, diag.Pos, diag.End)
13581275
if err != nil {
13591276
return gobDiagnostic{}, err
13601277
}
@@ -1382,6 +1299,126 @@ func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location
13821299
}, nil
13831300
}
13841301

1302+
// diagnosticPosToLocation converts from token.Pos to protocol form, in the
1303+
// context of the specified package and, optionally, its dependencies.
1304+
func diagnosticPosToLocation(pkg *Package, allowDeps bool, start, end token.Pos) (protocol.Location, error) {
1305+
if end == token.NoPos {
1306+
end = start
1307+
}
1308+
1309+
fset := pkg.FileSet()
1310+
tokFile := fset.File(start)
1311+
1312+
// Find existing mapper by file name.
1313+
// (Don't require an exact token.File match
1314+
// as the analyzer may have re-parsed the file.)
1315+
var (
1316+
mapper *protocol.Mapper
1317+
fixed bool
1318+
)
1319+
for _, p := range pkg.CompiledGoFiles() {
1320+
if p.Tok.Name() == tokFile.Name() {
1321+
mapper = p.Mapper
1322+
fixed = p.Fixed() // suppress some assertions after parser recovery
1323+
break
1324+
}
1325+
}
1326+
// TODO(adonovan): search pkg.AsmFiles too; see #71754.
1327+
if mapper != nil {
1328+
// debugging #64547
1329+
fileStart := token.Pos(tokFile.Base())
1330+
fileEnd := fileStart + token.Pos(tokFile.Size())
1331+
if start < fileStart {
1332+
if !fixed {
1333+
bug.Reportf("start < start of file")
1334+
}
1335+
start = fileStart
1336+
}
1337+
if end < start {
1338+
// This can happen if End is zero (#66683)
1339+
// or a small positive displacement from zero
1340+
// due to recursive Node.End() computation.
1341+
// This usually arises from poor parser recovery
1342+
// of an incomplete term at EOF.
1343+
if !fixed {
1344+
bug.Reportf("end < start of file")
1345+
}
1346+
end = fileEnd
1347+
}
1348+
if end > fileEnd+1 {
1349+
if !fixed {
1350+
bug.Reportf("end > end of file + 1")
1351+
}
1352+
end = fileEnd
1353+
}
1354+
1355+
return mapper.PosLocation(tokFile, start, end)
1356+
}
1357+
1358+
// Inv: the positions are not within this package.
1359+
1360+
if allowDeps {
1361+
// Positions in Diagnostic.RelatedInformation may belong to a
1362+
// dependency package. We cannot accurately map them to
1363+
// protocol.Location coordinates without a Mapper for the
1364+
// relevant file, but none exists if the file was loaded from
1365+
// export data, and we have no means (Snapshot) of loading it.
1366+
//
1367+
// So, fall back to approximate conversion to UTF-16:
1368+
// for non-ASCII text, the column numbers may be wrong.
1369+
var (
1370+
startPosn = safetoken.StartPosition(fset, start)
1371+
endPosn = safetoken.EndPosition(fset, end)
1372+
)
1373+
return protocol.Location{
1374+
URI: protocol.URIFromPath(startPosn.Filename),
1375+
Range: protocol.Range{
1376+
Start: protocol.Position{
1377+
Line: uint32(startPosn.Line - 1),
1378+
Character: uint32(startPosn.Column - 1),
1379+
},
1380+
End: protocol.Position{
1381+
Line: uint32(endPosn.Line - 1),
1382+
Character: uint32(endPosn.Column - 1),
1383+
},
1384+
},
1385+
}, nil
1386+
}
1387+
1388+
// The start position was not among the package's parsed
1389+
// Go files, indicating that the analyzer added new files
1390+
// to the FileSet.
1391+
//
1392+
// For example, the cgocall analyzer re-parses and
1393+
// type-checks some of the files in a special environment;
1394+
// and asmdecl and other low-level runtime analyzers call
1395+
// ReadFile to parse non-Go files.
1396+
// (This is a supported feature, documented at go/analysis.)
1397+
//
1398+
// In principle these files could be:
1399+
//
1400+
// - OtherFiles (non-Go files such as asm).
1401+
// However, we set Pass.OtherFiles=[] because
1402+
// gopls won't service "diagnose" requests
1403+
// for non-Go files, so there's no point
1404+
// reporting diagnostics in them.
1405+
//
1406+
// - IgnoredFiles (files tagged for other configs).
1407+
// However, we set Pass.IgnoredFiles=[] because,
1408+
// in most cases, zero-config gopls should create
1409+
// another view that covers these files.
1410+
//
1411+
// - Referents of //line directives, as in cgo packages.
1412+
// The file names in this case are not known a priori.
1413+
// gopls generally tries to avoid honoring line directives,
1414+
// but analyzers such as cgocall may honor them.
1415+
//
1416+
// In short, it's unclear how this can be reached
1417+
// other than due to an analyzer bug.
1418+
1419+
return protocol.Location{}, bug.Errorf("diagnostic location is not among files of package: %s", tokFile.Name())
1420+
}
1421+
13851422
// effectiveURL computes the effective URL of diag,
13861423
// using the algorithm specified at Diagnostic.URL.
13871424
func effectiveURL(a *analysis.Analyzer, diag analysis.Diagnostic) string {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
In addition to the Diagnostic, the SA4023 analyzer reports a
2+
RelatedInformation at the position of b.B, in an another package.
3+
Since this is in a dependency package, we cannot resolve to
4+
protocol.Location coordinates. This used to trigger an assertion, but
5+
now we resolve the location approximately.
6+
7+
This is a regression test for #70791.
8+
9+
-- settings.json --
10+
{"analyses": {"SA4023": true}}
11+
12+
-- go.mod --
13+
module example.com
14+
go 1.18
15+
16+
-- a/a.go --
17+
package a
18+
19+
import "example.com/b"
20+
21+
var _ = b.B() == nil //@ diag("b.B", re"comparison is never true")
22+
23+
-- b/b.go --
24+
package b
25+
26+
func B() any { return (*int)(nil) }
27+
28+
29+

0 commit comments

Comments
 (0)