Skip to content

Commit 85e6ad7

Browse files
committed
gopls/internal/lsp/safetoken: fix bug in Offset at EOF
During parser error recovery, it may synthesize tokens such as RBRACE at EOF, causing the End position of the incomplete syntax nodes to be computed as Rbrace+len("}"), which is out of bounds, and would cause token.File.Offset to panic, or safetoken.Offset to return an error. This change is a workaround in gopls so that such End positions are considered valid, and are mapped to the end of the file. Also - a regression test. - remove safetoken.InRange, to avoid ambiguity. It was used in only one place (and dubiously even there). Fixes golang/go#57484 Updates golang/go#57490 Change-Id: I75bbe4f3b3c54aedf47a36649e8ee56bca205c8d Reviewed-on: https://go-review.googlesource.com/c/tools/+/459735 Run-TryBot: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]>
1 parent ef1ec5d commit 85e6ad7

File tree

4 files changed

+65
-16
lines changed

4 files changed

+65
-16
lines changed

gopls/internal/lsp/cache/analysis.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) {
10611061

10621062
diagnostic, err := toGobDiagnostic(posToLocation, d)
10631063
if err != nil {
1064-
bug.Reportf("internal error converting diagnostic from analyzer %s: %v", analyzer.Name, err)
1064+
bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err)
10651065
return
10661066
}
10671067
diagnostics = append(diagnostics, diagnostic)

gopls/internal/lsp/safetoken/safetoken.go

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,58 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
// Package safetoken provides wrappers around methods in go/token, that return
6-
// errors rather than panicking.
5+
// Package safetoken provides wrappers around methods in go/token,
6+
// that return errors rather than panicking. It also provides a
7+
// central place for workarounds in the underlying packages.
78
package safetoken
89

910
import (
1011
"fmt"
1112
"go/token"
1213
)
1314

14-
// Offset returns f.Offset(pos), but first checks that the pos is in range
15-
// for the given file.
15+
// Offset returns f.Offset(pos), but first checks that the file
16+
// contains the pos.
17+
//
18+
// The definition of "contains" here differs from that of token.File
19+
// in order to work around a bug in the parser (issue #57490): during
20+
// error recovery, the parser may create syntax nodes whose computed
21+
// End position is 1 byte beyond EOF, which would cause
22+
// token.File.Offset to panic. The workaround is that this function
23+
// accepts a Pos that is exactly 1 byte beyond EOF and maps it to the
24+
// EOF offset.
25+
//
26+
// The use of this function instead of (*token.File).Offset is
27+
// mandatory in the gopls codebase; this is enforced by static check.
1628
func Offset(f *token.File, pos token.Pos) (int, error) {
17-
if !InRange(f, pos) {
29+
if !inRange(f, pos) {
30+
// Accept a Pos that is 1 byte beyond EOF,
31+
// and map it to the EOF offset.
32+
// (Workaround for #57490.)
33+
if int(pos) == f.Base()+f.Size()+1 {
34+
return f.Size(), nil
35+
}
36+
1837
return -1, fmt.Errorf("pos %d is not in range [%d:%d] of file %s",
1938
pos, f.Base(), f.Base()+f.Size(), f.Name())
2039
}
2140
return int(pos) - f.Base(), nil
2241
}
2342

24-
// Pos returns f.Pos(offset), but first checks that the offset is valid for
25-
// the given file.
43+
// Pos returns f.Pos(offset), but first checks that the offset is
44+
// non-negative and not larger than the size of the file.
2645
func Pos(f *token.File, offset int) (token.Pos, error) {
2746
if !(0 <= offset && offset <= f.Size()) {
2847
return token.NoPos, fmt.Errorf("offset %d is not in range for file %s of size %d", offset, f.Name(), f.Size())
2948
}
3049
return token.Pos(f.Base() + offset), nil
3150
}
3251

33-
// InRange reports whether file f contains position pos.
34-
func InRange(f *token.File, pos token.Pos) bool {
52+
// inRange reports whether file f contains position pos,
53+
// according to the invariants of token.File.
54+
//
55+
// This function is not public because of the ambiguity it would
56+
// create w.r.t. the definition of "contains". Use Offset instead.
57+
func inRange(f *token.File, pos token.Pos) bool {
3558
return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size())
3659
}

gopls/internal/lsp/safetoken/safetoken_test.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,54 @@
55
package safetoken_test
66

77
import (
8+
"go/parser"
89
"go/token"
910
"go/types"
1011
"testing"
1112

1213
"golang.org/x/tools/go/packages"
14+
"golang.org/x/tools/gopls/internal/lsp/safetoken"
1315
"golang.org/x/tools/internal/testenv"
1416
)
1517

18+
func TestWorkaroundIssue57490(t *testing.T) {
19+
// During error recovery the parser synthesizes various close
20+
// tokens at EOF, causing the End position of incomplete
21+
// syntax nodes, computed as Rbrace+len("}"), to be beyond EOF.
22+
src := `package p; func f() { var x struct`
23+
fset := token.NewFileSet()
24+
file, _ := parser.ParseFile(fset, "", src, 0)
25+
tf := fset.File(file.Pos())
26+
if false {
27+
tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35])
28+
}
29+
30+
// The offset of the EOF position is the file size.
31+
offset, err := safetoken.Offset(tf, file.End()-1)
32+
if err != nil || offset != tf.Size() {
33+
t.Errorf("Offset(EOF) = (%d, %v), want token.File.Size %d", offset, err, tf.Size())
34+
}
35+
36+
// The offset of the file.End() position, 1 byte beyond EOF,
37+
// is also the size of the file.
38+
offset, err = safetoken.Offset(tf, file.End())
39+
if err != nil || offset != tf.Size() {
40+
t.Errorf("Offset(ast.File.End()) = (%d, %v), want token.File.Size %d", offset, err, tf.Size())
41+
}
42+
}
43+
1644
// This test reports any unexpected uses of (*go/token.File).Offset within
1745
// the gopls codebase to ensure that we don't check in more code that is prone
1846
// to panicking. All calls to (*go/token.File).Offset should be replaced with
1947
// calls to safetoken.Offset.
20-
func TestTokenOffset(t *testing.T) {
48+
func TestGoplsSourceDoesNotCallTokenFileOffset(t *testing.T) {
2149
testenv.NeedsGoPackages(t)
2250

2351
fset := token.NewFileSet()
2452
pkgs, err := packages.Load(&packages.Config{
2553
Fset: fset,
2654
Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
27-
}, "go/token", "golang.org/x/tools/gopls/internal/lsp/...", "golang.org/x/tools/gopls/...")
55+
}, "go/token", "golang.org/x/tools/gopls/...")
2856
if err != nil {
2957
t.Fatal(err)
3058
}

gopls/internal/lsp/semantic.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,14 +244,12 @@ func (e *encoded) strStack() string {
244244
}
245245
if len(e.stack) > 0 {
246246
loc := e.stack[len(e.stack)-1].Pos()
247-
if !safetoken.InRange(e.pgf.Tok, loc) {
247+
if _, err := safetoken.Offset(e.pgf.Tok, loc); err != nil {
248248
msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, e.pgf.URI))
249-
} else if safetoken.InRange(e.pgf.Tok, loc) {
249+
} else {
250250
add := e.pgf.Tok.PositionFor(loc, false) // ignore line directives
251251
nm := filepath.Base(add.Filename)
252252
msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column))
253-
} else {
254-
msg = append(msg, fmt.Sprintf("(loc %d out of range)", loc))
255253
}
256254
}
257255
msg = append(msg, "]")

0 commit comments

Comments
 (0)