Skip to content

Commit 9a89d3a

Browse files
findleyrgopherbot
authored andcommitted
internal/analysisinternal: avoid sub-token spans in TypeErrorEndPos
Avoid assigning end positions to type errors that are within the current token (such as could happen in an import path). To test this, introduce our first named argument in the marker tests: 'exact' for the @diag marker. Also, attempt to document the heuristic of TypeErrorEndPos. Fixes golang/go#69505 Change-Id: If3cf82f241dd354d834a7dcbf24b7b3c59246911 Reviewed-on: https://go-review.googlesource.com/c/tools/+/625916 Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 1115af6 commit 9a89d3a

File tree

4 files changed

+114
-24
lines changed

4 files changed

+114
-24
lines changed

gopls/internal/test/marker/doc.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ The following markers are supported within marker tests:
127127
- complete(location, ...items): specifies expected completion results at
128128
the given location. Must be used in conjunction with @item.
129129
130-
- diag(location, regexp): specifies an expected diagnostic matching the
131-
given regexp at the given location. The test runner requires
132-
a 1:1 correspondence between observed diagnostics and diag annotations.
133-
The diagnostics source and kind fields are ignored, to reduce fuss.
130+
- diag(location, regexp, exact=bool): specifies an expected diagnostic
131+
matching the given regexp at the given location. The test runner requires a
132+
1:1 correspondence between observed diagnostics and diag annotations. The
133+
diagnostics source and kind fields are ignored, to reduce fuss.
134134
135135
The specified location must match the start position of the diagnostic,
136-
but end positions are ignored.
136+
but end positions are ignored unless exact=true.
137137
138138
TODO(adonovan): in the older marker framework, the annotation asserted
139139
two additional fields (source="compiler", kind="error"). Restore them?

gopls/internal/test/marker/marker_test.go

+47-14
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,6 @@ func Test(t *testing.T) {
205205
}
206206
}
207207

208-
// Wait for the didOpen notifications to be processed, then collect
209-
// diagnostics.
210208
for path, diags := range allDiags {
211209
uri := run.env.Sandbox.Workdir.URI(path)
212210
for _, diag := range diags {
@@ -246,7 +244,13 @@ func Test(t *testing.T) {
246244
if !test.ignoreExtraDiags {
247245
for loc, diags := range run.diags {
248246
for _, diag := range diags {
249-
t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(loc), diag.Message)
247+
// Note that loc is collapsed (start==end).
248+
// For formatting, show the exact span.
249+
exactLoc := protocol.Location{
250+
URI: loc.URI,
251+
Range: diag.Range,
252+
}
253+
t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(exactLoc), diag.Message)
250254
}
251255
}
252256
}
@@ -404,10 +408,11 @@ func valueMarkerFunc(fn any) func(marker) {
404408
// called during the processing of action markers (e.g. @action("abc", 123))
405409
// with marker arguments converted to function parameters. The provided
406410
// function's first parameter must be of type 'marker', and it must not return
407-
// any values.
411+
// any values. Any named arguments that may be used by the marker func must be
412+
// listed in allowedNames.
408413
//
409414
// The provided fn should not mutate the test environment.
410-
func actionMarkerFunc(fn any) func(marker) {
415+
func actionMarkerFunc(fn any, allowedNames ...string) func(marker) {
411416
ftype := reflect.TypeOf(fn)
412417
if ftype.NumIn() == 0 || ftype.In(0) != markerType {
413418
panic(fmt.Sprintf("action marker function %#v must accept marker as its first argument", ftype))
@@ -416,7 +421,21 @@ func actionMarkerFunc(fn any) func(marker) {
416421
panic(fmt.Sprintf("action marker function %#v cannot have results", ftype))
417422
}
418423

424+
var allowed map[string]bool
425+
if len(allowedNames) > 0 {
426+
allowed = make(map[string]bool)
427+
for _, name := range allowedNames {
428+
allowed[name] = true
429+
}
430+
}
431+
419432
return func(mark marker) {
433+
for name := range mark.note.NamedArgs {
434+
if !allowed[name] {
435+
mark.errorf("unexpected named argument %q", name)
436+
}
437+
}
438+
420439
args := append([]any{mark}, mark.note.Args...)
421440
argValues, err := convertArgs(mark, ftype, args)
422441
if err != nil {
@@ -470,6 +489,18 @@ func convertArgs(mark marker, ftype reflect.Type, args []any) ([]reflect.Value,
470489
return argValues, nil
471490
}
472491

492+
// namedArg returns the named argument for name, or the default value.
493+
func namedArg[T any](mark marker, name string, dflt T) T {
494+
if v, ok := mark.note.NamedArgs[name]; ok {
495+
if e, ok := v.(T); ok {
496+
return e
497+
} else {
498+
mark.errorf("invalid value for %q: %v", name, v)
499+
}
500+
}
501+
return dflt
502+
}
503+
473504
// is reports whether arg is a T.
474505
func is[T any](arg any) bool {
475506
_, ok := arg.(T)
@@ -492,7 +523,7 @@ var actionMarkerFuncs = map[string]func(marker){
492523
"codelenses": actionMarkerFunc(codeLensesMarker),
493524
"complete": actionMarkerFunc(completeMarker),
494525
"def": actionMarkerFunc(defMarker),
495-
"diag": actionMarkerFunc(diagMarker),
526+
"diag": actionMarkerFunc(diagMarker, "exact"),
496527
"documentlink": actionMarkerFunc(documentLinkMarker),
497528
"foldingrange": actionMarkerFunc(foldingRangeMarker),
498529
"format": actionMarkerFunc(formatMarker),
@@ -1659,7 +1690,8 @@ func locMarker(mark marker, loc protocol.Location) protocol.Location { return lo
16591690
// diagMarker implements the @diag marker. It eliminates diagnostics from
16601691
// the observed set in mark.test.
16611692
func diagMarker(mark marker, loc protocol.Location, re *regexp.Regexp) {
1662-
if _, ok := removeDiagnostic(mark, loc, re); !ok {
1693+
exact := namedArg(mark, "exact", false)
1694+
if _, ok := removeDiagnostic(mark, loc, exact, re); !ok {
16631695
mark.errorf("no diagnostic at %v matches %q", loc, re)
16641696
}
16651697
}
@@ -1670,12 +1702,13 @@ func diagMarker(mark marker, loc protocol.Location, re *regexp.Regexp) {
16701702
// from the unmatched set.
16711703
//
16721704
// If not found, it returns (protocol.Diagnostic{}, false).
1673-
func removeDiagnostic(mark marker, loc protocol.Location, re *regexp.Regexp) (protocol.Diagnostic, bool) {
1674-
loc.Range.End = loc.Range.Start // diagnostics ignore end position.
1675-
diags := mark.run.diags[loc]
1705+
func removeDiagnostic(mark marker, loc protocol.Location, matchEnd bool, re *regexp.Regexp) (protocol.Diagnostic, bool) {
1706+
key := loc
1707+
key.Range.End = key.Range.Start // diagnostics ignore end position.
1708+
diags := mark.run.diags[key]
16761709
for i, diag := range diags {
1677-
if re.MatchString(diag.Message) {
1678-
mark.run.diags[loc] = append(diags[:i], diags[i+1:]...)
1710+
if re.MatchString(diag.Message) && (!matchEnd || diag.Range.End == loc.Range.End) {
1711+
mark.run.diags[key] = append(diags[:i], diags[i+1:]...)
16791712
return diag, true
16801713
}
16811714
}
@@ -2007,7 +2040,7 @@ func (mark marker) consumeExtraNotes(name string, f func(marker)) {
20072040
func quickfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) {
20082041
loc.Range.End = loc.Range.Start // diagnostics ignore end position.
20092042
// Find and remove the matching diagnostic.
2010-
diag, ok := removeDiagnostic(mark, loc, re)
2043+
diag, ok := removeDiagnostic(mark, loc, false, re)
20112044
if !ok {
20122045
mark.errorf("no diagnostic at %v matches %q", loc, re)
20132046
return
@@ -2027,7 +2060,7 @@ func quickfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golde
20272060
func quickfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp, wantErr stringMatcher) {
20282061
loc.Range.End = loc.Range.Start // diagnostics ignore end position.
20292062
// Find and remove the matching diagnostic.
2030-
diag, ok := removeDiagnostic(mark, loc, re)
2063+
diag, ok := removeDiagnostic(mark, loc, false, re)
20312064
if !ok {
20322065
mark.errorf("no diagnostic at %v matches %q", loc, re)
20332066
return
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
This test checks that diagnostics ranges computed with the TypeErrorEndPos
2+
heuristic span at least a full token.
3+
4+
-- go.mod --
5+
module example.com
6+
7+
go 1.21
8+
9+
-- main.go --
10+
package main
11+
12+
import "example.com/foo-bar" //@ diag(re`"[^"]*"`, re`not used`, exact=true)
13+
14+
func f(int) {}
15+
16+
func main() {
17+
var x int
18+
_ = x + 1.e+0i //@ diag("1.e+0i", re`truncated`, exact=true)
19+
}
20+
21+
-- foo-bar/baz.go --
22+
package foo

internal/analysisinternal/analysis.go

+40-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"bytes"
1111
"fmt"
1212
"go/ast"
13+
"go/scanner"
1314
"go/token"
1415
"go/types"
1516
"os"
@@ -21,12 +22,46 @@ import (
2122

2223
func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos {
2324
// Get the end position for the type error.
24-
offset, end := fset.PositionFor(start, false).Offset, start
25-
if offset >= len(src) {
26-
return end
25+
file := fset.File(start)
26+
if file == nil {
27+
return start
2728
}
28-
if width := bytes.IndexAny(src[offset:], " \n,():;[]+-*"); width > 0 {
29-
end = start + token.Pos(width)
29+
if offset := file.PositionFor(start, false).Offset; offset > len(src) {
30+
return start
31+
} else {
32+
src = src[offset:]
33+
}
34+
35+
// Attempt to find a reasonable end position for the type error.
36+
//
37+
// TODO(rfindley): the heuristic implemented here is unclear. It looks like
38+
// it seeks the end of the primary operand starting at start, but that is not
39+
// quite implemented (for example, given a func literal this heuristic will
40+
// return the range of the func keyword).
41+
//
42+
// We should formalize this heuristic, or deprecate it by finally proposing
43+
// to add end position to all type checker errors.
44+
//
45+
// Nevertheless, ensure that the end position at least spans the current
46+
// token at the cursor (this was golang/go#69505).
47+
end := start
48+
{
49+
var s scanner.Scanner
50+
fset := token.NewFileSet()
51+
f := fset.AddFile("", fset.Base(), len(src))
52+
s.Init(f, src, nil /* no error handler */, scanner.ScanComments)
53+
pos, tok, lit := s.Scan()
54+
if tok != token.SEMICOLON && token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) {
55+
off := file.Offset(pos) + len(lit)
56+
src = src[off:]
57+
end += token.Pos(off)
58+
}
59+
}
60+
61+
// Look for bytes that might terminate the current operand. See note above:
62+
// this is imprecise.
63+
if width := bytes.IndexAny(src, " \n,():;[]+-*/"); width > 0 {
64+
end += token.Pos(width)
3065
}
3166
return end
3267
}

0 commit comments

Comments
 (0)