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