internal/span: eliminate Converter and FileConverter

The only real implementation of position conversion was via a
*token.File, so refactor the converter logic to eliminate the Converter
interface, and just use a single converter implementation that uses a
*token.File to convert between offsets and positions.

This change is meant to be a zero-impact refactoring for non-test code.
As such, I abstained from panicking in several places where it would
make sense. In later CLs, once the bug reporting API lands, we can
insert bug reports in these places.

Change-Id: Id2e503acd80d089bc5d73e983215784015471f04
Reviewed-on: https://go-review.googlesource.com/c/tools/+/405546
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/go/packages/packagestest/expect.go b/go/packages/packagestest/expect.go
index 3088d43..4302586 100644
--- a/go/packages/packagestest/expect.go
+++ b/go/packages/packagestest/expect.go
@@ -422,7 +422,7 @@
 			// end of file identifier, look up the current file
 			f := e.ExpectFileSet.File(n.Pos)
 			eof := f.Pos(f.Size())
-			return span.Range{FileSet: e.ExpectFileSet, Start: eof, End: token.NoPos}, args, nil
+			return span.NewRange(e.ExpectFileSet, eof, token.NoPos), args, nil
 		default:
 			// look up an marker by name
 			mark, ok := e.markers[string(arg)]
@@ -439,7 +439,7 @@
 		if start == token.NoPos {
 			return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg)
 		}
-		return span.Range{FileSet: e.ExpectFileSet, Start: start, End: end}, args, nil
+		return span.NewRange(e.ExpectFileSet, start, end), args, nil
 	case *regexp.Regexp:
 		start, end, err := expect.MatchBefore(e.ExpectFileSet, e.FileContents, n.Pos, arg)
 		if err != nil {
@@ -448,7 +448,7 @@
 		if start == token.NoPos {
 			return span.Range{}, nil, fmt.Errorf("%v: pattern %s did not match", e.ExpectFileSet.Position(n.Pos), arg)
 		}
-		return span.Range{FileSet: e.ExpectFileSet, Start: start, End: end}, args, nil
+		return span.NewRange(e.ExpectFileSet, start, end), args, nil
 	default:
 		return span.Range{}, nil, fmt.Errorf("cannot convert %v to pos", arg)
 	}
diff --git a/internal/lsp/cache/parse.go b/internal/lsp/cache/parse.go
index d6c4bc0..5d751eb 100644
--- a/internal/lsp/cache/parse.go
+++ b/internal/lsp/cache/parse.go
@@ -324,7 +324,7 @@
 			Tok:  tok,
 			Mapper: &protocol.ColumnMapper{
 				URI:       fh.URI(),
-				Converter: span.NewTokenConverter(fset, tok),
+				Converter: span.NewTokenConverter(tok),
 				Content:   src,
 			},
 			ParseErr: parseErr,
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 05fb005..d8e66ab 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -322,7 +322,7 @@
 
 // ProtocolEditsFromSource converts text edits to LSP edits using the original
 // source.
-func ProtocolEditsFromSource(src []byte, edits []diff.TextEdit, converter span.Converter) ([]protocol.TextEdit, error) {
+func ProtocolEditsFromSource(src []byte, edits []diff.TextEdit, converter *span.TokenConverter) ([]protocol.TextEdit, error) {
 	m := lsppos.NewMapper(src)
 	var result []protocol.TextEdit
 	for _, edit := range edits {
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 768ff8c..a76c916 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -36,13 +36,8 @@
 // NewMappedRange returns a MappedRange for the given start and end token.Pos.
 func NewMappedRange(fset *token.FileSet, m *protocol.ColumnMapper, start, end token.Pos) MappedRange {
 	return MappedRange{
-		spanRange: span.Range{
-			FileSet:   fset,
-			Start:     start,
-			End:       end,
-			Converter: m.Converter,
-		},
-		m: m,
+		spanRange: span.NewRange(fset, start, end),
+		m:         m,
 	}
 }
 
diff --git a/internal/span/span.go b/internal/span/span.go
index 4d2ad09..fdf0644 100644
--- a/internal/span/span.go
+++ b/internal/span/span.go
@@ -41,15 +41,6 @@
 
 var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}}
 
-// Converter is the interface to an object that can convert between line:column
-// and offset forms for a single file.
-type Converter interface {
-	//ToPosition converts from an offset to a line:column pair.
-	ToPosition(offset int) (int, int, error)
-	//ToOffset converts from a line:column pair to an offset.
-	ToOffset(line, col int) (int, error)
-}
-
 func New(uri URI, start Point, end Point) Span {
 	s := Span{v: span{URI: uri, Start: start.v, End: end.v}}
 	s.v.clean()
@@ -217,28 +208,28 @@
 	}
 }
 
-func (s Span) WithPosition(c Converter) (Span, error) {
+func (s Span) WithPosition(c *TokenConverter) (Span, error) {
 	if err := s.update(c, true, false); err != nil {
 		return Span{}, err
 	}
 	return s, nil
 }
 
-func (s Span) WithOffset(c Converter) (Span, error) {
+func (s Span) WithOffset(c *TokenConverter) (Span, error) {
 	if err := s.update(c, false, true); err != nil {
 		return Span{}, err
 	}
 	return s, nil
 }
 
-func (s Span) WithAll(c Converter) (Span, error) {
+func (s Span) WithAll(c *TokenConverter) (Span, error) {
 	if err := s.update(c, true, true); err != nil {
 		return Span{}, err
 	}
 	return s, nil
 }
 
-func (s *Span) update(c Converter, withPos, withOffset bool) error {
+func (s *Span) update(c *TokenConverter, withPos, withOffset bool) error {
 	if !s.IsValid() {
 		return fmt.Errorf("cannot add information to an invalid span")
 	}
@@ -265,7 +256,7 @@
 	return nil
 }
 
-func (p *point) updatePosition(c Converter) error {
+func (p *point) updatePosition(c *TokenConverter) error {
 	line, col, err := c.ToPosition(p.Offset)
 	if err != nil {
 		return err
@@ -275,7 +266,7 @@
 	return nil
 }
 
-func (p *point) updateOffset(c Converter) error {
+func (p *point) updateOffset(c *TokenConverter) error {
 	offset, err := c.ToOffset(p.Line, p.Column)
 	if err != nil {
 		return err
diff --git a/internal/span/span_test.go b/internal/span/span_test.go
index 150ea3f..3956565 100644
--- a/internal/span/span_test.go
+++ b/internal/span/span_test.go
@@ -6,6 +6,7 @@
 
 import (
 	"fmt"
+	"go/token"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -59,12 +60,15 @@
 	return filepath.FromSlash(value)
 }
 
-type lines int
-
-func (l lines) ToPosition(offset int) (int, int, error) {
-	return (offset / int(l)) + 1, (offset % int(l)) + 1, nil
-}
-
-func (l lines) ToOffset(line, col int) (int, error) {
-	return (int(l) * (line - 1)) + (col - 1), nil
+// lines creates a new tokenConverter for a file with 1000 lines, each width
+// bytes wide.
+func lines(width int) *span.TokenConverter {
+	fset := token.NewFileSet()
+	f := fset.AddFile("", -1, 1000*width)
+	var lines []int
+	for i := 0; i < 1000; i++ {
+		lines = append(lines, i*width)
+	}
+	f.SetLines(lines)
+	return span.NewTokenConverter(f)
 }
diff --git a/internal/span/token.go b/internal/span/token.go
index 3fd2219..c2b65e7 100644
--- a/internal/span/token.go
+++ b/internal/span/token.go
@@ -5,6 +5,7 @@
 package span
 
 import (
+	"errors"
 	"fmt"
 	"go/token"
 )
@@ -13,38 +14,37 @@
 // It also carries the FileSet that produced the positions, so that it is
 // self contained.
 type Range struct {
-	FileSet   *token.FileSet
-	Start     token.Pos
-	End       token.Pos
-	Converter Converter
+	Start token.Pos
+	End   token.Pos
+	file  *token.File // possibly nil, if Start or End is invalid
 }
 
-type FileConverter struct {
-	file *token.File
-}
-
-// TokenConverter is a Converter backed by a token file set and file.
-// It uses the file set methods to work out the conversions, which
-// makes it fast and does not require the file contents.
+// TokenConverter converts between offsets and (col, row) using a token.File.
 type TokenConverter struct {
-	FileConverter
-	fset *token.FileSet
+	file *token.File
 }
 
 // NewRange creates a new Range from a FileSet and two positions.
 // To represent a point pass a 0 as the end pos.
 func NewRange(fset *token.FileSet, start, end token.Pos) Range {
+	file := fset.File(start)
+	if file == nil {
+		// TODO: report a bug here via the bug reporting API, once it is available.
+	}
 	return Range{
-		FileSet: fset,
-		Start:   start,
-		End:     end,
+		Start: start,
+		End:   end,
+		file:  file,
 	}
 }
 
 // NewTokenConverter returns an implementation of Converter backed by a
 // token.File.
-func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter {
-	return &TokenConverter{fset: fset, FileConverter: FileConverter{file: f}}
+func NewTokenConverter(f *token.File) *TokenConverter {
+	if f == nil {
+		// TODO: report a bug here using the bug reporting API.
+	}
+	return &TokenConverter{file: f}
 }
 
 // NewContentConverter returns an implementation of Converter for the
@@ -53,7 +53,7 @@
 	fset := token.NewFileSet()
 	f := fset.AddFile(filename, -1, len(content))
 	f.SetLinesForContent(content)
-	return NewTokenConverter(fset, f)
+	return NewTokenConverter(f)
 }
 
 // IsPoint returns true if the range represents a single point.
@@ -68,16 +68,15 @@
 	if !r.Start.IsValid() {
 		return Span{}, fmt.Errorf("start pos is not valid")
 	}
-	f := r.FileSet.File(r.Start)
-	if f == nil {
-		return Span{}, fmt.Errorf("file not found in FileSet")
+	if r.file == nil {
+		return Span{}, errors.New("range missing file association")
 	}
-	return FileSpan(f, r.Converter, r.Start, r.End)
+	return FileSpan(r.file, NewTokenConverter(r.file), r.Start, r.End)
 }
 
 // FileSpan returns a span within tok, using converter to translate between
 // offsets and positions.
-func FileSpan(tok *token.File, converter Converter, start, end token.Pos) (Span, error) {
+func FileSpan(tok *token.File, converter *TokenConverter, start, end token.Pos) (Span, error) {
 	var s Span
 	var err error
 	var startFilename string
@@ -107,7 +106,7 @@
 	if startFilename != tok.Name() {
 		return Span{}, fmt.Errorf("must supply Converter for file %q containing lines from %q", tok.Name(), startFilename)
 	}
-	return s.WithOffset(&FileConverter{tok})
+	return s.WithOffset(&TokenConverter{tok})
 }
 
 func position(f *token.File, pos token.Pos) (string, int, int, error) {
@@ -136,7 +135,7 @@
 // that it does not panic on invalid positions.
 func offset(f *token.File, pos token.Pos) (int, error) {
 	if int(pos) < f.Base() || int(pos) > f.Base()+f.Size() {
-		return 0, fmt.Errorf("invalid pos")
+		return 0, fmt.Errorf("invalid pos: %d not in [%d, %d]", pos, f.Base(), f.Base()+f.Size())
 	}
 	return int(pos) - f.Base(), nil
 }
@@ -148,28 +147,30 @@
 	if err != nil {
 		return Range{}, err
 	}
+	file := converter.file
 	// go/token will panic if the offset is larger than the file's size,
 	// so check here to avoid panicking.
-	if s.Start().Offset() > converter.file.Size() {
-		return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), converter.file.Size())
+	if s.Start().Offset() > file.Size() {
+		// TODO: report a bug here via the future bug reporting API.
+		return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), file.Size())
 	}
-	if s.End().Offset() > converter.file.Size() {
-		return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), converter.file.Size())
+	if s.End().Offset() > file.Size() {
+		// TODO: report a bug here via the future bug reporting API.
+		return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), file.Size())
 	}
 	return Range{
-		FileSet:   converter.fset,
-		Start:     converter.file.Pos(s.Start().Offset()),
-		End:       converter.file.Pos(s.End().Offset()),
-		Converter: converter,
+		Start: file.Pos(s.Start().Offset()),
+		End:   file.Pos(s.End().Offset()),
+		file:  file,
 	}, nil
 }
 
-func (l *FileConverter) ToPosition(offset int) (int, int, error) {
+func (l *TokenConverter) ToPosition(offset int) (int, int, error) {
 	_, line, col, err := positionFromOffset(l.file, offset)
 	return line, col, err
 }
 
-func (l *FileConverter) ToOffset(line, col int) (int, error) {
+func (l *TokenConverter) ToOffset(line, col int) (int, error) {
 	if line < 0 {
 		return -1, fmt.Errorf("line is not valid")
 	}
diff --git a/internal/span/token_test.go b/internal/span/token_test.go
index 81b2631..5f48d68 100644
--- a/internal/span/token_test.go
+++ b/internal/span/token_test.go
@@ -48,7 +48,7 @@
 	}
 	for _, test := range tokenTests {
 		f := files[test.URI()]
-		c := span.NewTokenConverter(fset, f)
+		c := span.NewTokenConverter(f)
 		t.Run(path.Base(f.Name()), func(t *testing.T) {
 			checkToken(t, c, span.New(
 				test.URI(),