1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package span
6
7import (
8	"fmt"
9	"go/token"
10)
11
12// Range represents a source code range in token.Pos form.
13// It also carries the FileSet that produced the positions, so that it is
14// self contained.
15type Range struct {
16	FileSet   *token.FileSet
17	Start     token.Pos
18	End       token.Pos
19	Converter Converter
20}
21
22type FileConverter struct {
23	file *token.File
24}
25
26// TokenConverter is a Converter backed by a token file set and file.
27// It uses the file set methods to work out the conversions, which
28// makes it fast and does not require the file contents.
29type TokenConverter struct {
30	FileConverter
31	fset *token.FileSet
32}
33
34// NewRange creates a new Range from a FileSet and two positions.
35// To represent a point pass a 0 as the end pos.
36func NewRange(fset *token.FileSet, start, end token.Pos) Range {
37	return Range{
38		FileSet: fset,
39		Start:   start,
40		End:     end,
41	}
42}
43
44// NewTokenConverter returns an implementation of Converter backed by a
45// token.File.
46func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter {
47	return &TokenConverter{fset: fset, FileConverter: FileConverter{file: f}}
48}
49
50// NewContentConverter returns an implementation of Converter for the
51// given file content.
52func NewContentConverter(filename string, content []byte) *TokenConverter {
53	fset := token.NewFileSet()
54	f := fset.AddFile(filename, -1, len(content))
55	f.SetLinesForContent(content)
56	return NewTokenConverter(fset, f)
57}
58
59// IsPoint returns true if the range represents a single point.
60func (r Range) IsPoint() bool {
61	return r.Start == r.End
62}
63
64// Span converts a Range to a Span that represents the Range.
65// It will fill in all the members of the Span, calculating the line and column
66// information.
67func (r Range) Span() (Span, error) {
68	if !r.Start.IsValid() {
69		return Span{}, fmt.Errorf("start pos is not valid")
70	}
71	f := r.FileSet.File(r.Start)
72	if f == nil {
73		return Span{}, fmt.Errorf("file not found in FileSet")
74	}
75	return FileSpan(f, r.Converter, r.Start, r.End)
76}
77
78// FileSpan returns a span within tok, using converter to translate between
79// offsets and positions.
80func FileSpan(tok *token.File, converter Converter, start, end token.Pos) (Span, error) {
81	var s Span
82	var err error
83	var startFilename string
84	startFilename, s.v.Start.Line, s.v.Start.Column, err = position(tok, start)
85	if err != nil {
86		return Span{}, err
87	}
88	s.v.URI = URIFromPath(startFilename)
89	if end.IsValid() {
90		var endFilename string
91		endFilename, s.v.End.Line, s.v.End.Column, err = position(tok, end)
92		if err != nil {
93			return Span{}, err
94		}
95		// In the presence of line directives, a single File can have sections from
96		// multiple file names.
97		if endFilename != startFilename {
98			return Span{}, fmt.Errorf("span begins in file %q but ends in %q", startFilename, endFilename)
99		}
100	}
101	s.v.Start.clean()
102	s.v.End.clean()
103	s.v.clean()
104	if converter != nil {
105		return s.WithOffset(converter)
106	}
107	if startFilename != tok.Name() {
108		return Span{}, fmt.Errorf("must supply Converter for file %q containing lines from %q", tok.Name(), startFilename)
109	}
110	return s.WithOffset(&FileConverter{tok})
111}
112
113func position(f *token.File, pos token.Pos) (string, int, int, error) {
114	off, err := offset(f, pos)
115	if err != nil {
116		return "", 0, 0, err
117	}
118	return positionFromOffset(f, off)
119}
120
121func positionFromOffset(f *token.File, offset int) (string, int, int, error) {
122	if offset > f.Size() {
123		return "", 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, f.Size())
124	}
125	pos := f.Pos(offset)
126	p := f.Position(pos)
127	// TODO(golang/go#41029): Consider returning line, column instead of line+1, 1 if
128	// the file's last character is not a newline.
129	if offset == f.Size() {
130		return p.Filename, p.Line + 1, 1, nil
131	}
132	return p.Filename, p.Line, p.Column, nil
133}
134
135// offset is a copy of the Offset function in go/token, but with the adjustment
136// that it does not panic on invalid positions.
137func offset(f *token.File, pos token.Pos) (int, error) {
138	if int(pos) < f.Base() || int(pos) > f.Base()+f.Size() {
139		return 0, fmt.Errorf("invalid pos")
140	}
141	return int(pos) - f.Base(), nil
142}
143
144// Range converts a Span to a Range that represents the Span for the supplied
145// File.
146func (s Span) Range(converter *TokenConverter) (Range, error) {
147	s, err := s.WithOffset(converter)
148	if err != nil {
149		return Range{}, err
150	}
151	// go/token will panic if the offset is larger than the file's size,
152	// so check here to avoid panicking.
153	if s.Start().Offset() > converter.file.Size() {
154		return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), converter.file.Size())
155	}
156	if s.End().Offset() > converter.file.Size() {
157		return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), converter.file.Size())
158	}
159	return Range{
160		FileSet:   converter.fset,
161		Start:     converter.file.Pos(s.Start().Offset()),
162		End:       converter.file.Pos(s.End().Offset()),
163		Converter: converter,
164	}, nil
165}
166
167func (l *FileConverter) ToPosition(offset int) (int, int, error) {
168	_, line, col, err := positionFromOffset(l.file, offset)
169	return line, col, err
170}
171
172func (l *FileConverter) ToOffset(line, col int) (int, error) {
173	if line < 0 {
174		return -1, fmt.Errorf("line is not valid")
175	}
176	lineMax := l.file.LineCount() + 1
177	if line > lineMax {
178		return -1, fmt.Errorf("line is beyond end of file %v", lineMax)
179	} else if line == lineMax {
180		if col > 1 {
181			return -1, fmt.Errorf("column is beyond end of file")
182		}
183		// at the end of the file, allowing for a trailing eol
184		return l.file.Size(), nil
185	}
186	pos := lineStart(l.file, line)
187	if !pos.IsValid() {
188		return -1, fmt.Errorf("line is not in file")
189	}
190	// we assume that column is in bytes here, and that the first byte of a
191	// line is at column 1
192	pos += token.Pos(col - 1)
193	return offset(l.file, pos)
194}
195