Skip to content

Commit 81e741e

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/lsp/safetoken: funnel more calls through this package
This change expands the dominion of safetoken to include calls to token.File{,Set}.Position{,For}, since all need workarounds similar to that in Offset. As a side benefit, we now have a centralized place to implement the workaround for other bugs such as golang/go#41029, the newline at EOF problem). Unfortunately the former callers of FileSet.Position must stipulate whether the Pos is a start or an end, as the same value may denote the position 1 beyond the end of one file, or the start of the following file in the file set. Hence the two different functions, {Start,End}Position. The static check has been expanded, as have the tests. Of course, this approach is not foolproof: gopls has many dependencies that call methods of File and FileSet directly. Updates golang/go#41029 Updates golang/go#57484 Updates golang/go#57490 Change-Id: Ia727e3f55ca2703dd17ff2cac05e786793ca38eb Reviewed-on: https://go-review.googlesource.com/c/tools/+/459736 Run-TryBot: Alan Donovan <adonovan@google.com> Reviewed-by: Robert Findley <rfindley@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Auto-Submit: Alan Donovan <adonovan@google.com> gopls-CI: kokoro <noreply+kokoro@google.com>
1 parent 8367fb2 commit 81e741e

File tree

19 files changed

+152
-64
lines changed

19 files changed

+152
-64
lines changed

gopls/doc/generate.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"golang.org/x/tools/gopls/internal/lsp/command"
3737
"golang.org/x/tools/gopls/internal/lsp/command/commandmeta"
3838
"golang.org/x/tools/gopls/internal/lsp/mod"
39+
"golang.org/x/tools/gopls/internal/lsp/safetoken"
3940
"golang.org/x/tools/gopls/internal/lsp/source"
4041
)
4142

@@ -555,7 +556,7 @@ func upperFirst(x string) string {
555556
func fileForPos(pkg *packages.Package, pos token.Pos) (*ast.File, error) {
556557
fset := pkg.Fset
557558
for _, f := range pkg.Syntax {
558-
if fset.PositionFor(f.Pos(), false).Filename == fset.PositionFor(pos, false).Filename {
559+
if safetoken.StartPosition(fset, f.Pos()).Filename == safetoken.StartPosition(fset, pos).Filename {
559560
return f, nil
560561
}
561562
}

gopls/internal/lsp/analysis/fillstruct/fillstruct.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"golang.org/x/tools/go/analysis/passes/inspect"
2727
"golang.org/x/tools/go/ast/astutil"
2828
"golang.org/x/tools/go/ast/inspector"
29+
"golang.org/x/tools/gopls/internal/lsp/safetoken"
2930
"golang.org/x/tools/gopls/internal/span"
3031
"golang.org/x/tools/internal/analysisinternal"
3132
"golang.org/x/tools/internal/fuzzy"
@@ -271,8 +272,8 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast
271272

272273
// Find the line on which the composite literal is declared.
273274
split := bytes.Split(content, []byte("\n"))
274-
lineNumber := fset.PositionFor(expr.Lbrace, false).Line // ignore line directives
275-
firstLine := split[lineNumber-1] // lines are 1-indexed
275+
lineNumber := safetoken.StartPosition(fset, expr.Lbrace).Line
276+
firstLine := split[lineNumber-1] // lines are 1-indexed
276277

277278
// Trim the whitespace from the left of the line, and use the index
278279
// to get the amount of whitespace on the left.

gopls/internal/lsp/analysis/undeclaredname/undeclared.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818

1919
"golang.org/x/tools/go/analysis"
2020
"golang.org/x/tools/go/ast/astutil"
21+
"golang.org/x/tools/gopls/internal/lsp/safetoken"
2122
"golang.org/x/tools/gopls/internal/span"
2223
"golang.org/x/tools/internal/analysisinternal"
2324
)
@@ -112,8 +113,8 @@ func runForError(pass *analysis.Pass, err types.Error) {
112113
if tok == nil {
113114
return
114115
}
115-
offset := pass.Fset.PositionFor(err.Pos, false).Offset
116-
end := tok.Pos(offset + len(name))
116+
offset := safetoken.StartPosition(pass.Fset, err.Pos).Offset
117+
end := tok.Pos(offset + len(name)) // TODO(adonovan): dubious! err.Pos + len(name)??
117118
pass.Report(analysis.Diagnostic{
118119
Pos: err.Pos,
119120
End: end,
@@ -146,7 +147,7 @@ func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast
146147
return nil, fmt.Errorf("could not locate insertion point")
147148
}
148149

149-
insertBefore := fset.PositionFor(insertBeforeStmt.Pos(), false).Offset // ignore line directives
150+
insertBefore := safetoken.StartPosition(fset, insertBeforeStmt.Pos()).Offset
150151

151152
// Get the indent to add on the line after the new statement.
152153
// Since this will have a parse error, we can not use format.Source().

gopls/internal/lsp/cache/errors.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"golang.org/x/tools/go/packages"
2121
"golang.org/x/tools/gopls/internal/lsp/command"
2222
"golang.org/x/tools/gopls/internal/lsp/protocol"
23+
"golang.org/x/tools/gopls/internal/lsp/safetoken"
2324
"golang.org/x/tools/gopls/internal/lsp/source"
2425
"golang.org/x/tools/gopls/internal/span"
2526
"golang.org/x/tools/internal/analysisinternal"
@@ -315,7 +316,7 @@ func typeErrorData(pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Sp
315316
if fset != terr.Fset {
316317
return 0, span.Span{}, bug.Errorf("wrong FileSet for type error")
317318
}
318-
posn := fset.PositionFor(start, false) // ignore line directives
319+
posn := safetoken.StartPosition(fset, start)
319320
if !posn.IsValid() {
320321
return 0, span.Span{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr)
321322
}

gopls/internal/lsp/safetoken/safetoken.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
// license that can be found in the LICENSE file.
44

55
// 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.
6+
// that return errors rather than panicking.
7+
//
8+
// It also provides a central place for workarounds in the underlying
9+
// packages. The use of this package's functions instead of methods of
10+
// token.File (such as Offset, Position, and PositionFor) is mandatory
11+
// throughout the gopls codebase and enforced by a static check.
812
package safetoken
913

1014
import (
@@ -22,9 +26,6 @@ import (
2226
// token.File.Offset to panic. The workaround is that this function
2327
// accepts a Pos that is exactly 1 byte beyond EOF and maps it to the
2428
// 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.
2829
func Offset(f *token.File, pos token.Pos) (int, error) {
2930
if !inRange(f, pos) {
3031
// Accept a Pos that is 1 byte beyond EOF,
@@ -57,3 +58,52 @@ func Pos(f *token.File, offset int) (token.Pos, error) {
5758
func inRange(f *token.File, pos token.Pos) bool {
5859
return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size())
5960
}
61+
62+
// Position returns the Position for the pos value in the given file.
63+
//
64+
// p must be NoPos, a valid Pos in the range of f, or exactly 1 byte
65+
// beyond the end of f. (See [Offset] for explanation.)
66+
// Any other value causes a panic.
67+
//
68+
// Line directives (//line comments) are ignored.
69+
func Position(f *token.File, pos token.Pos) token.Position {
70+
// Work around issue #57490.
71+
if int(pos) == f.Base()+f.Size()+1 {
72+
pos--
73+
}
74+
75+
// TODO(adonovan): centralize the workaround for
76+
// golang/go#41029 (newline at EOF) here too.
77+
78+
return f.PositionFor(pos, false)
79+
}
80+
81+
// StartPosition converts a start Pos in the FileSet into a Position.
82+
//
83+
// Call this function only if start represents the start of a token or
84+
// parse tree, such as the result of Node.Pos(). If start is the end of
85+
// an interval, such as Node.End(), call EndPosition instead, as it
86+
// may need the correction described at [Position].
87+
func StartPosition(fset *token.FileSet, start token.Pos) (_ token.Position) {
88+
if f := fset.File(start); f != nil {
89+
return Position(f, start)
90+
}
91+
return
92+
}
93+
94+
// EndPosition converts an end Pos in the FileSet into a Position.
95+
//
96+
// Call this function only if pos represents the end of
97+
// a non-empty interval, such as the result of Node.End().
98+
func EndPosition(fset *token.FileSet, end token.Pos) (_ token.Position) {
99+
if f := fset.File(end); f != nil && int(end) > f.Base() {
100+
return Position(f, end)
101+
}
102+
103+
// Work around issue #57490.
104+
if f := fset.File(end - 1); f != nil {
105+
return Position(f, end)
106+
}
107+
108+
return
109+
}

gopls/internal/lsp/safetoken/safetoken_test.go

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
package safetoken_test
66

77
import (
8+
"fmt"
89
"go/parser"
910
"go/token"
1011
"go/types"
12+
"os"
1113
"testing"
1214

1315
"golang.org/x/tools/go/packages"
@@ -21,10 +23,20 @@ func TestWorkaroundIssue57490(t *testing.T) {
2123
// syntax nodes, computed as Rbrace+len("}"), to be beyond EOF.
2224
src := `package p; func f() { var x struct`
2325
fset := token.NewFileSet()
24-
file, _ := parser.ParseFile(fset, "", src, 0)
26+
file, _ := parser.ParseFile(fset, "a.go", src, 0)
2527
tf := fset.File(file.Pos())
28+
29+
// Add another file to the FileSet.
30+
file2, _ := parser.ParseFile(fset, "b.go", "package q", 0)
31+
32+
// This is the ambiguity of #57490...
33+
if file.End() != file2.Pos() {
34+
t.Errorf("file.End() %d != %d file2.Pos()", file.End(), file2.Pos())
35+
}
36+
// ...which causes these statements to panic.
2637
if false {
27-
tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35])
38+
tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35])
39+
tf.Position(file.End()) // panic: invalid Pos value 36 (should be in [1, 35])
2840
}
2941

3042
// The offset of the EOF position is the file size.
@@ -39,57 +51,71 @@ func TestWorkaroundIssue57490(t *testing.T) {
3951
if err != nil || offset != tf.Size() {
4052
t.Errorf("Offset(ast.File.End()) = (%d, %v), want token.File.Size %d", offset, err, tf.Size())
4153
}
54+
55+
if got, want := safetoken.Position(tf, file.End()).String(), "a.go:1:35"; got != want {
56+
t.Errorf("Position(ast.File.End()) = %s, want %s", got, want)
57+
}
58+
59+
if got, want := safetoken.EndPosition(fset, file.End()).String(), "a.go:1:35"; got != want {
60+
t.Errorf("EndPosition(ast.File.End()) = %s, want %s", got, want)
61+
}
62+
63+
// Note that calling StartPosition on an end may yield the wrong file:
64+
if got, want := safetoken.StartPosition(fset, file.End()).String(), "b.go:1:1"; got != want {
65+
t.Errorf("StartPosition(ast.File.End()) = %s, want %s", got, want)
66+
}
4267
}
4368

44-
// This test reports any unexpected uses of (*go/token.File).Offset within
45-
// the gopls codebase to ensure that we don't check in more code that is prone
46-
// to panicking. All calls to (*go/token.File).Offset should be replaced with
47-
// calls to safetoken.Offset.
48-
func TestGoplsSourceDoesNotCallTokenFileOffset(t *testing.T) {
69+
// To reduce the risk of panic, or bugs for which this package
70+
// provides a workaround, this test statically reports references to
71+
// forbidden methods of token.File or FileSet throughout gopls and
72+
// suggests alternatives.
73+
func TestGoplsSourceDoesNotCallTokenFileMethods(t *testing.T) {
4974
testenv.NeedsGoPackages(t)
5075

51-
fset := token.NewFileSet()
5276
pkgs, err := packages.Load(&packages.Config{
53-
Fset: fset,
5477
Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
5578
}, "go/token", "golang.org/x/tools/gopls/...")
5679
if err != nil {
5780
t.Fatal(err)
5881
}
59-
var tokenPkg, safePkg *packages.Package
82+
var tokenPkg *packages.Package
6083
for _, pkg := range pkgs {
61-
switch pkg.PkgPath {
62-
case "go/token":
84+
if pkg.PkgPath == "go/token" {
6385
tokenPkg = pkg
64-
case "golang.org/x/tools/gopls/internal/lsp/safetoken":
65-
safePkg = pkg
86+
break
6687
}
6788
}
68-
6989
if tokenPkg == nil {
7090
t.Fatal("missing package go/token")
7191
}
72-
if safePkg == nil {
73-
t.Fatal("missing package golang.org/x/tools/gopls/internal/lsp/safetoken")
74-
}
7592

76-
fileObj := tokenPkg.Types.Scope().Lookup("File")
77-
tokenOffset, _, _ := types.LookupFieldOrMethod(fileObj.Type(), true, fileObj.Pkg(), "Offset")
93+
File := tokenPkg.Types.Scope().Lookup("File")
94+
FileSet := tokenPkg.Types.Scope().Lookup("FileSet")
7895

79-
safeOffset := safePkg.Types.Scope().Lookup("Offset").(*types.Func)
96+
alternative := make(map[types.Object]string)
97+
setAlternative := func(recv types.Object, old, new string) {
98+
oldMethod, _, _ := types.LookupFieldOrMethod(recv.Type(), true, recv.Pkg(), old)
99+
alternative[oldMethod] = new
100+
}
101+
setAlternative(File, "Offset", "safetoken.Offset")
102+
setAlternative(File, "Position", "safetoken.Position")
103+
setAlternative(File, "PositionFor", "safetoken.Position")
104+
setAlternative(FileSet, "Position", "safetoken.StartPosition or EndPosition")
105+
setAlternative(FileSet, "PositionFor", "safetoken.StartPosition or EndPosition")
80106

81107
for _, pkg := range pkgs {
82-
if pkg.PkgPath == "go/token" { // Allow usage from within go/token itself.
83-
continue
108+
switch pkg.PkgPath {
109+
case "go/token", "golang.org/x/tools/gopls/internal/lsp/safetoken":
110+
continue // allow calls within these packages
84111
}
112+
85113
for ident, obj := range pkg.TypesInfo.Uses {
86-
if obj != tokenOffset {
87-
continue
88-
}
89-
if safeOffset.Pos() <= ident.Pos() && ident.Pos() <= safeOffset.Scope().End() {
90-
continue // accepted usage
114+
if alt, ok := alternative[obj]; ok {
115+
posn := safetoken.StartPosition(pkg.Fset, ident.Pos())
116+
fmt.Fprintf(os.Stderr, "%s: forbidden use of %v; use %s instead.\n", posn, obj, alt)
117+
t.Fail()
91118
}
92-
t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/gopls/internal/lsp/safetoken.Offset instead.`, fset.PositionFor(ident.Pos(), false))
93119
}
94120
}
95121
}

gopls/internal/lsp/semantic.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ func (e *encoded) strStack() string {
243243
if _, err := safetoken.Offset(e.pgf.Tok, loc); err != nil {
244244
msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, e.pgf.URI))
245245
} else {
246-
add := e.pgf.Tok.PositionFor(loc, false) // ignore line directives
246+
add := safetoken.Position(e.pgf.Tok, loc)
247247
nm := filepath.Base(add.Filename)
248248
msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column))
249249
}
@@ -413,7 +413,7 @@ func (e *encoded) inspector(n ast.Node) bool {
413413
return true
414414
// not going to see these
415415
case *ast.File, *ast.Package:
416-
e.unexpected(fmt.Sprintf("implement %T %s", x, e.pgf.Tok.PositionFor(x.Pos(), false)))
416+
e.unexpected(fmt.Sprintf("implement %T %s", x, safetoken.Position(e.pgf.Tok, x.Pos())))
417417
// other things we knowingly ignore
418418
case *ast.Comment, *ast.CommentGroup:
419419
pop()
@@ -773,7 +773,7 @@ func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []st
773773
}
774774
}
775775
// can't happen
776-
msg := fmt.Sprintf("failed to find the decl for %s", e.pgf.Tok.PositionFor(x.Pos(), false))
776+
msg := fmt.Sprintf("failed to find the decl for %s", safetoken.Position(e.pgf.Tok, x.Pos()))
777777
e.unexpected(msg)
778778
return "", []string{""}
779779
}
@@ -803,8 +803,8 @@ func (e *encoded) multiline(start, end token.Pos, val string, tok tokenType) {
803803
}
804804
return int(f.LineStart(line+1) - n)
805805
}
806-
spos := e.fset.PositionFor(start, false)
807-
epos := e.fset.PositionFor(end, false)
806+
spos := safetoken.StartPosition(e.fset, start)
807+
epos := safetoken.EndPosition(e.fset, end)
808808
sline := spos.Line
809809
eline := epos.Line
810810
// first line is from spos.Column to end
@@ -827,7 +827,7 @@ func (e *encoded) findKeyword(keyword string, start, end token.Pos) token.Pos {
827827
return start + token.Pos(idx)
828828
}
829829
//(in unparsable programs: type _ <-<-chan int)
830-
e.unexpected(fmt.Sprintf("not found:%s %v", keyword, e.fset.PositionFor(start, false)))
830+
e.unexpected(fmt.Sprintf("not found:%s %v", keyword, safetoken.StartPosition(e.fset, start)))
831831
return token.NoPos
832832
}
833833

gopls/internal/lsp/source/completion/format.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strings"
1515

1616
"golang.org/x/tools/gopls/internal/lsp/protocol"
17+
"golang.org/x/tools/gopls/internal/lsp/safetoken"
1718
"golang.org/x/tools/gopls/internal/lsp/snippet"
1819
"golang.org/x/tools/gopls/internal/lsp/source"
1920
"golang.org/x/tools/gopls/internal/span"
@@ -227,7 +228,7 @@ Suffixes:
227228
if !c.opts.documentation {
228229
return item, nil
229230
}
230-
pos := c.pkg.FileSet().PositionFor(obj.Pos(), false)
231+
pos := safetoken.StartPosition(c.pkg.FileSet(), obj.Pos())
231232

232233
// We ignore errors here, because some types, like "unsafe" or "error",
233234
// may not have valid positions that we can use to get documentation.

gopls/internal/lsp/source/completion/package.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func packageCompletionSurrounding(fset *token.FileSet, pgf *source.ParsedGoFile,
124124
if cursorLine <= 0 || cursorLine > len(lines) {
125125
return nil, fmt.Errorf("invalid line number")
126126
}
127-
if fset.PositionFor(expr.Pos(), false).Line == cursorLine { // ignore line directives
127+
if safetoken.StartPosition(fset, expr.Pos()).Line == cursorLine {
128128
words := strings.Fields(lines[cursorLine-1])
129129
if len(words) > 0 && words[0] == PACKAGE {
130130
content := PACKAGE

gopls/internal/lsp/source/completion/snippet.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package completion
77
import (
88
"go/ast"
99

10+
"golang.org/x/tools/gopls/internal/lsp/safetoken"
1011
"golang.org/x/tools/gopls/internal/lsp/snippet"
1112
)
1213

@@ -43,7 +44,7 @@ func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snip
4344

4445
// If the cursor position is on a different line from the literal's opening brace,
4546
// we are in a multiline literal. Ignore line directives.
46-
if fset.PositionFor(c.pos, false).Line != fset.PositionFor(clInfo.cl.Lbrace, false).Line {
47+
if safetoken.StartPosition(fset, c.pos).Line != safetoken.StartPosition(fset, clInfo.cl.Lbrace).Line {
4748
snip.WriteText(",")
4849
}
4950
}

0 commit comments

Comments
 (0)