aboutsummaryrefslogtreecommitdiff
path: root/gopls/internal/lsp/safetoken
diff options
context:
space:
mode:
Diffstat (limited to 'gopls/internal/lsp/safetoken')
-rw-r--r--gopls/internal/lsp/safetoken/safetoken.go122
-rw-r--r--gopls/internal/lsp/safetoken/safetoken_test.go121
2 files changed, 243 insertions, 0 deletions
diff --git a/gopls/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go
new file mode 100644
index 000000000..29cc1b1c6
--- /dev/null
+++ b/gopls/internal/lsp/safetoken/safetoken.go
@@ -0,0 +1,122 @@
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package safetoken provides wrappers around methods in go/token,
+// that return errors rather than panicking.
+//
+// It also provides a central place for workarounds in the underlying
+// packages. The use of this package's functions instead of methods of
+// token.File (such as Offset, Position, and PositionFor) is mandatory
+// throughout the gopls codebase and enforced by a static check.
+package safetoken
+
+import (
+ "fmt"
+ "go/token"
+)
+
+// Offset returns f.Offset(pos), but first checks that the file
+// contains the pos.
+//
+// The definition of "contains" here differs from that of token.File
+// in order to work around a bug in the parser (issue #57490): during
+// error recovery, the parser may create syntax nodes whose computed
+// End position is 1 byte beyond EOF, which would cause
+// token.File.Offset to panic. The workaround is that this function
+// accepts a Pos that is exactly 1 byte beyond EOF and maps it to the
+// EOF offset.
+func Offset(f *token.File, pos token.Pos) (int, error) {
+ if !inRange(f, pos) {
+ // Accept a Pos that is 1 byte beyond EOF,
+ // and map it to the EOF offset.
+ // (Workaround for #57490.)
+ if int(pos) == f.Base()+f.Size()+1 {
+ return f.Size(), nil
+ }
+
+ return -1, fmt.Errorf("pos %d is not in range [%d:%d] of file %s",
+ pos, f.Base(), f.Base()+f.Size(), f.Name())
+ }
+ return int(pos) - f.Base(), nil
+}
+
+// Offsets returns Offset(start) and Offset(end).
+func Offsets(f *token.File, start, end token.Pos) (int, int, error) {
+ startOffset, err := Offset(f, start)
+ if err != nil {
+ return 0, 0, fmt.Errorf("start: %v", err)
+ }
+ endOffset, err := Offset(f, end)
+ if err != nil {
+ return 0, 0, fmt.Errorf("end: %v", err)
+ }
+ return startOffset, endOffset, nil
+}
+
+// Pos returns f.Pos(offset), but first checks that the offset is
+// non-negative and not larger than the size of the file.
+func Pos(f *token.File, offset int) (token.Pos, error) {
+ if !(0 <= offset && offset <= f.Size()) {
+ return token.NoPos, fmt.Errorf("offset %d is not in range for file %s of size %d", offset, f.Name(), f.Size())
+ }
+ return token.Pos(f.Base() + offset), nil
+}
+
+// inRange reports whether file f contains position pos,
+// according to the invariants of token.File.
+//
+// This function is not public because of the ambiguity it would
+// create w.r.t. the definition of "contains". Use Offset instead.
+func inRange(f *token.File, pos token.Pos) bool {
+ return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size())
+}
+
+// Position returns the Position for the pos value in the given file.
+//
+// p must be NoPos, a valid Pos in the range of f, or exactly 1 byte
+// beyond the end of f. (See [Offset] for explanation.)
+// Any other value causes a panic.
+//
+// Line directives (//line comments) are ignored.
+func Position(f *token.File, pos token.Pos) token.Position {
+ // Work around issue #57490.
+ if int(pos) == f.Base()+f.Size()+1 {
+ pos--
+ }
+
+ // TODO(adonovan): centralize the workaround for
+ // golang/go#41029 (newline at EOF) here too.
+
+ return f.PositionFor(pos, false)
+}
+
+// StartPosition converts a start Pos in the FileSet into a Position.
+//
+// Call this function only if start represents the start of a token or
+// parse tree, such as the result of Node.Pos(). If start is the end of
+// an interval, such as Node.End(), call EndPosition instead, as it
+// may need the correction described at [Position].
+func StartPosition(fset *token.FileSet, start token.Pos) (_ token.Position) {
+ if f := fset.File(start); f != nil {
+ return Position(f, start)
+ }
+ return
+}
+
+// EndPosition converts an end Pos in the FileSet into a Position.
+//
+// Call this function only if pos represents the end of
+// a non-empty interval, such as the result of Node.End().
+func EndPosition(fset *token.FileSet, end token.Pos) (_ token.Position) {
+ if f := fset.File(end); f != nil && int(end) > f.Base() {
+ return Position(f, end)
+ }
+
+ // Work around issue #57490.
+ if f := fset.File(end - 1); f != nil {
+ return Position(f, end)
+ }
+
+ return
+}
diff --git a/gopls/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go
new file mode 100644
index 000000000..afd569472
--- /dev/null
+++ b/gopls/internal/lsp/safetoken/safetoken_test.go
@@ -0,0 +1,121 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package safetoken_test
+
+import (
+ "fmt"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "os"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/gopls/internal/lsp/safetoken"
+ "golang.org/x/tools/internal/testenv"
+)
+
+func TestWorkaroundIssue57490(t *testing.T) {
+ // During error recovery the parser synthesizes various close
+ // tokens at EOF, causing the End position of incomplete
+ // syntax nodes, computed as Rbrace+len("}"), to be beyond EOF.
+ src := `package p; func f() { var x struct`
+ fset := token.NewFileSet()
+ file, _ := parser.ParseFile(fset, "a.go", src, 0)
+ tf := fset.File(file.Pos())
+
+ // Add another file to the FileSet.
+ file2, _ := parser.ParseFile(fset, "b.go", "package q", 0)
+
+ // This is the ambiguity of #57490...
+ if file.End() != file2.Pos() {
+ t.Errorf("file.End() %d != %d file2.Pos()", file.End(), file2.Pos())
+ }
+ // ...which causes these statements to panic.
+ if false {
+ tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35])
+ tf.Position(file.End()) // panic: invalid Pos value 36 (should be in [1, 35])
+ }
+
+ // The offset of the EOF position is the file size.
+ offset, err := safetoken.Offset(tf, file.End()-1)
+ if err != nil || offset != tf.Size() {
+ t.Errorf("Offset(EOF) = (%d, %v), want token.File.Size %d", offset, err, tf.Size())
+ }
+
+ // The offset of the file.End() position, 1 byte beyond EOF,
+ // is also the size of the file.
+ offset, err = safetoken.Offset(tf, file.End())
+ if err != nil || offset != tf.Size() {
+ t.Errorf("Offset(ast.File.End()) = (%d, %v), want token.File.Size %d", offset, err, tf.Size())
+ }
+
+ if got, want := safetoken.Position(tf, file.End()).String(), "a.go:1:35"; got != want {
+ t.Errorf("Position(ast.File.End()) = %s, want %s", got, want)
+ }
+
+ if got, want := safetoken.EndPosition(fset, file.End()).String(), "a.go:1:35"; got != want {
+ t.Errorf("EndPosition(ast.File.End()) = %s, want %s", got, want)
+ }
+
+ // Note that calling StartPosition on an end may yield the wrong file:
+ if got, want := safetoken.StartPosition(fset, file.End()).String(), "b.go:1:1"; got != want {
+ t.Errorf("StartPosition(ast.File.End()) = %s, want %s", got, want)
+ }
+}
+
+// To reduce the risk of panic, or bugs for which this package
+// provides a workaround, this test statically reports references to
+// forbidden methods of token.File or FileSet throughout gopls and
+// suggests alternatives.
+func TestGoplsSourceDoesNotCallTokenFileMethods(t *testing.T) {
+ testenv.NeedsGoPackages(t)
+
+ pkgs, err := packages.Load(&packages.Config{
+ Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
+ }, "go/token", "golang.org/x/tools/gopls/...")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var tokenPkg *packages.Package
+ for _, pkg := range pkgs {
+ if pkg.PkgPath == "go/token" {
+ tokenPkg = pkg
+ break
+ }
+ }
+ if tokenPkg == nil {
+ t.Fatal("missing package go/token")
+ }
+
+ File := tokenPkg.Types.Scope().Lookup("File")
+ FileSet := tokenPkg.Types.Scope().Lookup("FileSet")
+
+ alternative := make(map[types.Object]string)
+ setAlternative := func(recv types.Object, old, new string) {
+ oldMethod, _, _ := types.LookupFieldOrMethod(recv.Type(), true, recv.Pkg(), old)
+ alternative[oldMethod] = new
+ }
+ setAlternative(File, "Offset", "safetoken.Offset")
+ setAlternative(File, "Position", "safetoken.Position")
+ setAlternative(File, "PositionFor", "safetoken.Position")
+ setAlternative(FileSet, "Position", "safetoken.StartPosition or EndPosition")
+ setAlternative(FileSet, "PositionFor", "safetoken.StartPosition or EndPosition")
+
+ for _, pkg := range pkgs {
+ switch pkg.PkgPath {
+ case "go/token", "golang.org/x/tools/gopls/internal/lsp/safetoken":
+ continue // allow calls within these packages
+ }
+
+ for ident, obj := range pkg.TypesInfo.Uses {
+ if alt, ok := alternative[obj]; ok {
+ posn := safetoken.StartPosition(pkg.Fset, ident.Pos())
+ fmt.Fprintf(os.Stderr, "%s: forbidden use of %v; use %s instead.\n", posn, obj, alt)
+ t.Fail()
+ }
+ }
+ }
+}