internal/lsp: use source.Offset instead of tok.Offset

This isn't strictly necessary for some of the cases, but it's better to
use it in all cases. Also added a test to ensure that we avoid
(*token.File).Offset in all of gopls--test was probably overkill, but it
was quick to write.

Change-Id: I6dd0126e2211796d5de4e7a389386d7aa81014f0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/353890
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Trust: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index fc110c7..81bad5e 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -782,7 +782,10 @@
 	// If the "{" is already in the source code, there isn't anything to
 	// fix since we aren't missing curlies.
 	if b.Lbrace.IsValid() {
-		braceOffset := tok.Offset(b.Lbrace)
+		braceOffset, err := source.Offset(tok, b.Lbrace)
+		if err != nil {
+			return nil
+		}
 		if braceOffset < len(src) && src[braceOffset] == '{' {
 			return nil
 		}
@@ -834,7 +837,11 @@
 
 	var buf bytes.Buffer
 	buf.Grow(len(src) + 3)
-	buf.Write(src[:tok.Offset(insertPos)])
+	offset, err := source.Offset(tok, insertPos)
+	if err != nil {
+		return nil
+	}
+	buf.Write(src[:offset])
 
 	// Detect if we need to insert a semicolon to fix "for" loop situations like:
 	//
@@ -854,7 +861,7 @@
 	// Insert "{}" at insertPos.
 	buf.WriteByte('{')
 	buf.WriteByte('}')
-	buf.Write(src[tok.Offset(insertPos):])
+	buf.Write(src[offset:])
 	return buf.Bytes()
 }
 
@@ -888,7 +895,10 @@
 
 	// If the right brace is actually in the source code at the
 	// specified position, don't mess with it.
-	braceOffset := tok.Offset(body.Rbrace)
+	braceOffset, err := source.Offset(tok, body.Rbrace)
+	if err != nil {
+		return
+	}
 	if braceOffset < len(src) && src[braceOffset] == '}' {
 		return
 	}
@@ -923,8 +933,12 @@
 		return nil
 	}
 
+	insertOffset, err := source.Offset(tok, s.X.End())
+	if err != nil {
+		return nil
+	}
 	// Insert directly after the selector's ".".
-	insertOffset := tok.Offset(s.X.End()) + 1
+	insertOffset++
 	if src[insertOffset-1] != '.' {
 		return nil
 	}
@@ -980,7 +994,10 @@
 
 	// Phantom underscore means the underscore is not actually in the
 	// program text.
-	offset := tok.Offset(id.Pos())
+	offset, err := source.Offset(tok, id.Pos())
+	if err != nil {
+		return false
+	}
 	return len(src) <= offset || src[offset] != '_'
 }
 
@@ -995,11 +1012,15 @@
 	}
 
 	// Try to extract a statement from the BadExpr.
-	// Make sure that the positions are in range first.
-	if !source.InRange(tok, bad.Pos()) || !source.InRange(tok, bad.End()-1) {
+	start, err := source.Offset(tok, bad.Pos())
+	if err != nil {
 		return
 	}
-	stmtBytes := src[tok.Offset(bad.Pos()) : tok.Offset(bad.End()-1)+1]
+	end, err := source.Offset(tok, bad.End()-1)
+	if err != nil {
+		return
+	}
+	stmtBytes := src[start : end+1]
 	stmt, err := parseStmt(bad.Pos(), stmtBytes)
 	if err != nil {
 		return
@@ -1039,7 +1060,11 @@
 // readKeyword reads the keyword starting at pos, if any.
 func readKeyword(pos token.Pos, tok *token.File, src []byte) string {
 	var kwBytes []byte
-	for i := tok.Offset(pos); i < len(src); i++ {
+	offset, err := source.Offset(tok, pos)
+	if err != nil {
+		return ""
+	}
+	for i := offset; i < len(src); i++ {
 		// Use a simplified identifier check since keywords are always lowercase ASCII.
 		if src[i] < 'a' || src[i] > 'z' {
 			break
@@ -1076,15 +1101,15 @@
 	// Avoid doing tok.Offset(to) since that panics if badExpr ends at EOF.
 	// It also panics if the position is not in the range of the file, and
 	// badExprs may not necessarily have good positions, so check first.
-	if !source.InRange(tok, from) {
+	fromOffset, err := source.Offset(tok, from)
+	if err != nil {
 		return false
 	}
-	if !source.InRange(tok, to-1) {
+	toOffset, err := source.Offset(tok, to-1)
+	if err != nil {
 		return false
 	}
-	fromOffset := tok.Offset(from)
-	toOffset := tok.Offset(to-1) + 1
-	exprBytes = append(exprBytes, src[fromOffset:toOffset]...)
+	exprBytes = append(exprBytes, src[fromOffset:toOffset+1]...)
 	exprBytes = bytes.TrimSpace(exprBytes)
 
 	// If our expression ends in "]" (e.g. "[]"), add a phantom selector
@@ -1237,18 +1262,26 @@
 		}
 	}
 
-	if !from.IsValid() || tok.Offset(from) >= len(src) {
+	fromOffset, err := source.Offset(tok, from)
+	if err != nil {
+		return false
+	}
+	if !from.IsValid() || fromOffset >= len(src) {
 		return false
 	}
 
-	if !to.IsValid() || tok.Offset(to) >= len(src) {
+	toOffset, err := source.Offset(tok, to)
+	if err != nil {
+		return false
+	}
+	if !to.IsValid() || toOffset >= len(src) {
 		return false
 	}
 
 	// Insert any phantom selectors needed to prevent dangling "." from messing
 	// up the AST.
 	exprBytes := make([]byte, 0, int(to-from)+len(phantomSelectors))
-	for i, b := range src[tok.Offset(from):tok.Offset(to)] {
+	for i, b := range src[fromOffset:toOffset] {
 		if len(phantomSelectors) > 0 && from+token.Pos(i) == phantomSelectors[0] {
 			exprBytes = append(exprBytes, '_')
 			phantomSelectors = phantomSelectors[1:]
diff --git a/internal/lsp/semantic.go b/internal/lsp/semantic.go
index d59ecad..b1707ab 100644
--- a/internal/lsp/semantic.go
+++ b/internal/lsp/semantic.go
@@ -268,7 +268,10 @@
 func (e *encoded) srcLine(x ast.Node) string {
 	file := e.pgf.Tok
 	line := file.Line(x.Pos())
-	start := file.Offset(file.LineStart(line))
+	start, err := source.Offset(file, file.LineStart(line))
+	if err != nil {
+		return ""
+	}
 	end := start
 	for ; end < len(e.pgf.Src) && e.pgf.Src[end] != '\n'; end++ {
 
diff --git a/internal/lsp/source/completion/package.go b/internal/lsp/source/completion/package.go
index 0ed66e6..c7e52d7 100644
--- a/internal/lsp/source/completion/package.go
+++ b/internal/lsp/source/completion/package.go
@@ -80,12 +80,15 @@
 		return nil, fmt.Errorf("unparseable file (%s)", pgf.URI)
 	}
 	tok := fset.File(expr.Pos())
-	offset := pgf.Tok.Offset(pos)
+	offset, err := source.Offset(pgf.Tok, pos)
+	if err != nil {
+		return nil, err
+	}
 	if offset > tok.Size() {
 		debug.Bug(ctx, "out of bounds cursor", "cursor offset (%d) out of bounds for %s (size: %d)", offset, pgf.URI, tok.Size())
 		return nil, fmt.Errorf("cursor out of bounds")
 	}
-	cursor := tok.Pos(pgf.Tok.Offset(pos))
+	cursor := tok.Pos(offset)
 	m := &protocol.ColumnMapper{
 		URI:       pgf.URI,
 		Content:   pgf.Src,
diff --git a/internal/lsp/source/extract.go b/internal/lsp/source/extract.go
index 8f7010a..54170fd 100644
--- a/internal/lsp/source/extract.go
+++ b/internal/lsp/source/extract.go
@@ -63,7 +63,11 @@
 	if tok == nil {
 		return nil, fmt.Errorf("no file for pos %v", fset.Position(file.Pos()))
 	}
-	newLineIndent := "\n" + calculateIndentation(src, tok, insertBeforeStmt)
+	indent, err := calculateIndentation(src, tok, insertBeforeStmt)
+	if err != nil {
+		return nil, err
+	}
+	newLineIndent := "\n" + indent
 
 	lhs := strings.Join(lhsNames, ", ")
 	assignStmt := &ast.AssignStmt{
@@ -128,11 +132,17 @@
 // When inserting lines of code, we must ensure that the lines have consistent
 // formatting (i.e. the proper indentation). To do so, we observe the indentation on the
 // line of code on which the insertion occurs.
-func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.Node) string {
+func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.Node) (string, error) {
 	line := tok.Line(insertBeforeStmt.Pos())
-	lineOffset := tok.Offset(tok.LineStart(line))
-	stmtOffset := tok.Offset(insertBeforeStmt.Pos())
-	return string(content[lineOffset:stmtOffset])
+	lineOffset, err := Offset(tok, tok.LineStart(line))
+	if err != nil {
+		return "", err
+	}
+	stmtOffset, err := Offset(tok, insertBeforeStmt.Pos())
+	if err != nil {
+		return "", err
+	}
+	return string(content[lineOffset:stmtOffset]), nil
 }
 
 // generateAvailableIdentifier adjusts the new function name until there are no collisons in scope.
@@ -390,8 +400,14 @@
 
 	// We put the selection in a constructed file. We can then traverse and edit
 	// the extracted selection without modifying the original AST.
-	startOffset := tok.Offset(rng.Start)
-	endOffset := tok.Offset(rng.End)
+	startOffset, err := Offset(tok, rng.Start)
+	if err != nil {
+		return nil, err
+	}
+	endOffset, err := Offset(tok, rng.End)
+	if err != nil {
+		return nil, err
+	}
 	selection := src[startOffset:endOffset]
 	extractedBlock, err := parseBlockStmt(fset, selection)
 	if err != nil {
@@ -584,11 +600,21 @@
 
 	// We're going to replace the whole enclosing function,
 	// so preserve the text before and after the selected block.
-	outerStart := tok.Offset(outer.Pos())
-	outerEnd := tok.Offset(outer.End())
+	outerStart, err := Offset(tok, outer.Pos())
+	if err != nil {
+		return nil, err
+	}
+	outerEnd, err := Offset(tok, outer.End())
+	if err != nil {
+		return nil, err
+	}
 	before := src[outerStart:startOffset]
 	after := src[endOffset:outerEnd]
-	newLineIndent := "\n" + calculateIndentation(src, tok, start)
+	indent, err := calculateIndentation(src, tok, start)
+	if err != nil {
+		return nil, err
+	}
+	newLineIndent := "\n" + indent
 
 	var fullReplacement strings.Builder
 	fullReplacement.Write(before)
@@ -634,8 +660,11 @@
 // their cursors for whitespace. To support this use case, we must manually adjust the
 // ranges to match the correct AST node. In this particular example, we would adjust
 // rng.Start forward by one byte, and rng.End backwards by two bytes.
-func adjustRangeForWhitespace(rng span.Range, tok *token.File, content []byte) span.Range {
-	offset := tok.Offset(rng.Start)
+func adjustRangeForWhitespace(rng span.Range, tok *token.File, content []byte) (span.Range, error) {
+	offset, err := Offset(tok, rng.Start)
+	if err != nil {
+		return span.Range{}, err
+	}
 	for offset < len(content) {
 		if !unicode.IsSpace(rune(content[offset])) {
 			break
@@ -646,7 +675,10 @@
 	rng.Start = tok.Pos(offset)
 
 	// Move backwards to find a non-whitespace character.
-	offset = tok.Offset(rng.End)
+	offset, err = Offset(tok, rng.End)
+	if err != nil {
+		return span.Range{}, err
+	}
 	for o := offset - 1; 0 <= o && o < len(content); o-- {
 		if !unicode.IsSpace(rune(content[o])) {
 			break
@@ -654,7 +686,7 @@
 		offset = o
 	}
 	rng.End = tok.Pos(offset)
-	return rng
+	return rng, nil
 }
 
 // findParent finds the parent AST node of the given target node, if the target is a
@@ -916,7 +948,11 @@
 	if tok == nil {
 		return nil, false, false, fmt.Errorf("no file for pos %v", fset.Position(file.Pos()))
 	}
-	rng = adjustRangeForWhitespace(rng, tok, src)
+	var err error
+	rng, err = adjustRangeForWhitespace(rng, tok, src)
+	if err != nil {
+		return nil, false, false, err
+	}
 	path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End)
 	if len(path) == 0 {
 		return nil, false, false, fmt.Errorf("no path enclosing interval")
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 0d61172..16e72c2 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -153,7 +153,10 @@
 
 func computeFixEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) {
 	// trim the original data to match fixedData
-	left := importPrefix(pgf.Src)
+	left, err := importPrefix(pgf.Src)
+	if err != nil {
+		return nil, err
+	}
 	extra := !strings.Contains(left, "\n") // one line may have more than imports
 	if extra {
 		left = string(pgf.Src)
@@ -185,25 +188,30 @@
 // importPrefix returns the prefix of the given file content through the final
 // import statement. If there are no imports, the prefix is the package
 // statement and any comment groups below it.
-func importPrefix(src []byte) string {
+func importPrefix(src []byte) (string, error) {
 	fset := token.NewFileSet()
 	// do as little parsing as possible
 	f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly|parser.ParseComments)
 	if err != nil { // This can happen if 'package' is misspelled
-		return ""
+		return "", fmt.Errorf("importPrefix: failed to parse: %s", err)
 	}
 	tok := fset.File(f.Pos())
 	var importEnd int
 	for _, d := range f.Decls {
 		if x, ok := d.(*ast.GenDecl); ok && x.Tok == token.IMPORT {
-			if e := tok.Offset(d.End()); e > importEnd {
+			if e, err := Offset(tok, d.End()); err != nil {
+				return "", fmt.Errorf("importPrefix: %s", err)
+			} else if e > importEnd {
 				importEnd = e
 			}
 		}
 	}
 
 	maybeAdjustToLineEnd := func(pos token.Pos, isCommentNode bool) int {
-		offset := tok.Offset(pos)
+		offset, err := Offset(tok, pos)
+		if err != nil {
+			return -1
+		}
 
 		// Don't go past the end of the file.
 		if offset > len(src) {
@@ -215,7 +223,10 @@
 		// return a position on the next line whenever possible.
 		switch line := tok.Line(tok.Pos(offset)); {
 		case line < tok.LineCount():
-			nextLineOffset := tok.Offset(tok.LineStart(line + 1))
+			nextLineOffset, err := Offset(tok, tok.LineStart(line+1))
+			if err != nil {
+				return -1
+			}
 			// If we found a position that is at the end of a line, move the
 			// offset to the start of the next line.
 			if offset+1 == nextLineOffset {
@@ -234,14 +245,19 @@
 	}
 	for _, cgroup := range f.Comments {
 		for _, c := range cgroup.List {
-			if end := tok.Offset(c.End()); end > importEnd {
+			if end, err := Offset(tok, c.End()); err != nil {
+				return "", err
+			} else if end > importEnd {
 				startLine := tok.Position(c.Pos()).Line
 				endLine := tok.Position(c.End()).Line
 
 				// Work around golang/go#41197 by checking if the comment might
 				// contain "\r", and if so, find the actual end position of the
 				// comment by scanning the content of the file.
-				startOffset := tok.Offset(c.Pos())
+				startOffset, err := Offset(tok, c.Pos())
+				if err != nil {
+					return "", err
+				}
 				if startLine != endLine && bytes.Contains(src[startOffset:], []byte("\r")) {
 					if commentEnd := scanForCommentEnd(src[startOffset:]); commentEnd > 0 {
 						end = startOffset + commentEnd
@@ -254,7 +270,7 @@
 	if importEnd > len(src) {
 		importEnd = len(src)
 	}
-	return string(src[:importEnd])
+	return string(src[:importEnd]), nil
 }
 
 // scanForCommentEnd returns the offset of the end of the multi-line comment
diff --git a/internal/lsp/source/format_test.go b/internal/lsp/source/format_test.go
index 5d93a4e..eac78d9 100644
--- a/internal/lsp/source/format_test.go
+++ b/internal/lsp/source/format_test.go
@@ -35,7 +35,10 @@
 		{"package x; import \"os\"; func f() {}\n\n", "package x; import \"os\""},
 		{"package x; func f() {fmt.Println()}\n\n", "package x"},
 	} {
-		got := importPrefix([]byte(tt.input))
+		got, err := importPrefix([]byte(tt.input))
+		if err != nil {
+			t.Fatal(err)
+		}
 		if got != tt.want {
 			t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, tt.want, got))
 		}
@@ -62,7 +65,10 @@
 */`,
 		},
 	} {
-		got := importPrefix([]byte(strings.ReplaceAll(tt.input, "\n", "\r\n")))
+		got, err := importPrefix([]byte(strings.ReplaceAll(tt.input, "\n", "\r\n")))
+		if err != nil {
+			t.Fatal(err)
+		}
 		want := strings.ReplaceAll(tt.want, "\n", "\r\n")
 		if got != want {
 			t.Errorf("%d: failed for %q:\n%s", i, tt.input, diffStr(t, want, got))
diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go
index 2039dfe..e8db266 100644
--- a/internal/lsp/source/hover.go
+++ b/internal/lsp/source/hover.go
@@ -203,8 +203,14 @@
 		// It's a string, scan only if it contains a unicode escape sequence under or before the
 		// current cursor position.
 		var found bool
-		litOffset := pgf.Tok.Offset(lit.Pos())
-		offset := pgf.Tok.Offset(pos)
+		litOffset, err := Offset(pgf.Tok, lit.Pos())
+		if err != nil {
+			return 0, MappedRange{}, err
+		}
+		offset, err := Offset(pgf.Tok, pos)
+		if err != nil {
+			return 0, MappedRange{}, err
+		}
 		for i := offset - litOffset; i > 0; i-- {
 			// Start at the cursor position and search backward for the beginning of a rune escape sequence.
 			rr, _ := utf8.DecodeRuneInString(lit.Value[i:])
@@ -486,17 +492,27 @@
 			// obj may not have been produced by type checking the AST containing
 			// node, so we need to be careful about using token.Pos.
 			tok := s.FileSet().File(obj.Pos())
-			offset := tok.Offset(obj.Pos())
+			offset, err := Offset(tok, obj.Pos())
+			if err != nil {
+				return nil, err
+			}
 			tok2 := s.FileSet().File(node.Pos())
 			var spec ast.Spec
 			for _, s := range node.Specs {
 				// Avoid panics by guarding the calls to token.Offset (golang/go#48249).
-				if InRange(tok2, s.Pos()) && InRange(tok2, s.End()) && tok2.Offset(s.Pos()) <= offset && offset <= tok2.Offset(s.End()) {
+				start, err := Offset(tok2, s.Pos())
+				if err != nil {
+					return nil, err
+				}
+				end, err := Offset(tok2, s.End())
+				if err != nil {
+					return nil, err
+				}
+				if start <= offset && offset <= end {
 					spec = s
 					break
 				}
 			}
-			var err error
 			info, err = formatGenDecl(node, spec, obj, obj.Type())
 			if err != nil {
 				return nil, err
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index c709d8d..140a9d2 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -331,7 +331,10 @@
 		fset := snapshot.FileSet()
 		file2, _ := parser.ParseFile(fset, tok.Name(), pgf.Src, parser.AllErrors|parser.ParseComments)
 		if file2 != nil {
-			offset := tok.Offset(obj.Pos())
+			offset, err := Offset(tok, obj.Pos())
+			if err != nil {
+				return nil, err
+			}
 			file = file2
 			tok2 := fset.File(file2.Pos())
 			pos = tok2.Pos(offset)
diff --git a/internal/lsp/source/implementation.go b/internal/lsp/source/implementation.go
index 04aea37..b53d7c9 100644
--- a/internal/lsp/source/implementation.go
+++ b/internal/lsp/source/implementation.go
@@ -223,7 +223,6 @@
 		return nil, errNoObjectFound
 	}
 	pkg := pkgs[0]
-	var offset int
 	pgf, err := pkg.File(uri)
 	if err != nil {
 		return nil, err
@@ -236,7 +235,10 @@
 	if err != nil {
 		return nil, err
 	}
-	offset = pgf.Tok.Offset(rng.Start)
+	offset, err := Offset(pgf.Tok, rng.Start)
+	if err != nil {
+		return nil, err
+	}
 	return qualifiedObjsAtLocation(ctx, s, objSearchKey{uri, offset}, map[objSearchKey]bool{})
 }
 
@@ -350,7 +352,11 @@
 			offset := -1
 			for _, pgf := range pkg.CompiledGoFiles() {
 				if pgf.Tok.Base() <= int(pos) && int(pos) <= pgf.Tok.Base()+pgf.Tok.Size() {
-					offset = pgf.Tok.Offset(pos)
+					var err error
+					offset, err = Offset(pgf.Tok, pos)
+					if err != nil {
+						return nil, err
+					}
 					uri = pgf.URI
 				}
 			}
diff --git a/internal/lsp/source/offset_test.go b/internal/lsp/source/offset_test.go
new file mode 100644
index 0000000..1007677
--- /dev/null
+++ b/internal/lsp/source/offset_test.go
@@ -0,0 +1,71 @@
+// 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 source_test
+
+import (
+	"go/token"
+	"go/types"
+	"testing"
+
+	"golang.org/x/tools/go/packages"
+)
+
+// This test reports any unexpected uses of (*go/token.File).Offset within
+// the gopls codebase to ensure that we don't check in more code that is prone
+// to panicking. All calls to (*go/token.File).Offset should be replaced with
+// calls to source.Offset.
+func TestTokenOffset(t *testing.T) {
+	fset := token.NewFileSet()
+	pkgs, err := packages.Load(&packages.Config{
+		Fset: fset,
+		Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps,
+	}, "go/token", "golang.org/x/tools/internal/lsp/...", "golang.org/x/tools/gopls/...")
+	if err != nil {
+		t.Fatal(err)
+	}
+	var tokPkg *packages.Package
+	for _, pkg := range pkgs {
+		if pkg.PkgPath == "go/token" {
+			tokPkg = pkg
+			break
+		}
+	}
+	typname, ok := tokPkg.Types.Scope().Lookup("File").(*types.TypeName)
+	if !ok {
+		t.Fatal("expected go/token.File typename, got none")
+	}
+	named, ok := typname.Type().(*types.Named)
+	if !ok {
+		t.Fatalf("expected named type, got %T", typname.Type)
+	}
+	var offset *types.Func
+	for i := 0; i < named.NumMethods(); i++ {
+		meth := named.Method(i)
+		if meth.Name() == "Offset" {
+			offset = meth
+			break
+		}
+	}
+	for _, pkg := range pkgs {
+		for ident, obj := range pkg.TypesInfo.Uses {
+			if ident.Name != "Offset" {
+				continue
+			}
+			if pkg.PkgPath == "go/token" {
+				continue
+			}
+			if !types.Identical(offset.Type(), obj.Type()) {
+				continue
+			}
+			// The only permitted use is in golang.org/x/tools/internal/lsp/source.Offset,
+			// so check the enclosing function.
+			sourceOffset := pkg.Types.Scope().Lookup("Offset").(*types.Func)
+			if sourceOffset.Pos() <= ident.Pos() && ident.Pos() <= sourceOffset.Scope().End() {
+				continue // accepted usage
+			}
+			t.Errorf(`%s: Unexpected use of (*go/token.File).Offset. Please use golang.org/x/tools/internal/lsp/source.Offset instead.`, fset.Position(ident.Pos()))
+		}
+	}
+}
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 00ab860..9500eee 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"fmt"
 	"go/ast"
 	"go/printer"
 	"go/token"
@@ -543,6 +544,15 @@
 	return strings.Contains(s, "command-line-arguments")
 }
 
+// Offset returns tok.Offset(pos), but it also checks that the pos is in range
+// for the given file.
+func Offset(tok *token.File, pos token.Pos) (int, error) {
+	if !InRange(tok, pos) {
+		return -1, fmt.Errorf("pos %v is not in range for file [%v:%v)", pos, tok.Base(), tok.Base()+tok.Size())
+	}
+	return tok.Offset(pos), nil
+}
+
 // InRange reports whether the given position is in the given token.File.
 func InRange(tok *token.File, pos token.Pos) bool {
 	size := tok.Pos(tok.Size())