1// Copyright 2019 The Go Authors. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package span 16 17import ( 18 "fmt" 19 "go/token" 20) 21 22// Range represents a source code range in token.Pos form. 23// It also carries the FileSet that produced the positions, so that it is 24// self contained. 25type Range struct { 26 FileSet *token.FileSet 27 Start token.Pos 28 End token.Pos 29} 30 31// TokenConverter is a Converter backed by a token file set and file. 32// It uses the file set methods to work out the conversions, which 33// makes it fast and does not require the file contents. 34type TokenConverter struct { 35 fset *token.FileSet 36 file *token.File 37} 38 39// NewRange creates a new Range from a FileSet and two positions. 40// To represent a point pass a 0 as the end pos. 41func NewRange(fset *token.FileSet, start, end token.Pos) Range { 42 return Range{ 43 FileSet: fset, 44 Start: start, 45 End: end, 46 } 47} 48 49// NewTokenConverter returns an implementation of Converter backed by a 50// token.File. 51func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter { 52 return &TokenConverter{fset: fset, file: f} 53} 54 55// NewContentConverter returns an implementation of Converter for the 56// given file content. 57func NewContentConverter(filename string, content []byte) *TokenConverter { 58 fset := token.NewFileSet() 59 f := fset.AddFile(filename, -1, len(content)) 60 f.SetLinesForContent(content) 61 return &TokenConverter{fset: fset, file: f} 62} 63 64// IsPoint returns true if the range represents a single point. 65func (r Range) IsPoint() bool { 66 return r.Start == r.End 67} 68 69// Span converts a Range to a Span that represents the Range. 70// It will fill in all the members of the Span, calculating the line and column 71// information. 72func (r Range) Span() (Span, error) { 73 f := r.FileSet.File(r.Start) 74 if f == nil { 75 return Span{}, fmt.Errorf("file not found in FileSet") 76 } 77 s := Span{v: span{URI: FileURI(f.Name())}} 78 var err error 79 s.v.Start.Offset, err = offset(f, r.Start) 80 if err != nil { 81 return Span{}, err 82 } 83 if r.End.IsValid() { 84 s.v.End.Offset, err = offset(f, r.End) 85 if err != nil { 86 return Span{}, err 87 } 88 } 89 s.v.Start.clean() 90 s.v.End.clean() 91 s.v.clean() 92 converter := NewTokenConverter(r.FileSet, f) 93 return s.WithPosition(converter) 94} 95 96// offset is a copy of the Offset function in go/token, but with the adjustment 97// that it does not panic on invalid positions. 98func offset(f *token.File, pos token.Pos) (int, error) { 99 if int(pos) < f.Base() || int(pos) > f.Base()+f.Size() { 100 return 0, fmt.Errorf("invalid pos") 101 } 102 return int(pos) - f.Base(), nil 103} 104 105// Range converts a Span to a Range that represents the Span for the supplied 106// File. 107func (s Span) Range(converter *TokenConverter) (Range, error) { 108 s, err := s.WithOffset(converter) 109 if err != nil { 110 return Range{}, err 111 } 112 // go/token will panic if the offset is larger than the file's size, 113 // so check here to avoid panicking. 114 if s.Start().Offset() > converter.file.Size() { 115 return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), converter.file.Size()) 116 } 117 if s.End().Offset() > converter.file.Size() { 118 return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), converter.file.Size()) 119 } 120 return Range{ 121 FileSet: converter.fset, 122 Start: converter.file.Pos(s.Start().Offset()), 123 End: converter.file.Pos(s.End().Offset()), 124 }, nil 125} 126 127func (l *TokenConverter) ToPosition(offset int) (int, int, error) { 128 if offset > l.file.Size() { 129 return 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, l.file.Size()) 130 } 131 pos := l.file.Pos(offset) 132 p := l.fset.Position(pos) 133 if offset == l.file.Size() { 134 return p.Line + 1, 1, nil 135 } 136 return p.Line, p.Column, nil 137} 138 139func (l *TokenConverter) ToOffset(line, col int) (int, error) { 140 if line < 0 { 141 return -1, fmt.Errorf("line is not valid") 142 } 143 lineMax := l.file.LineCount() + 1 144 if line > lineMax { 145 return -1, fmt.Errorf("line is beyond end of file %v", lineMax) 146 } else if line == lineMax { 147 if col > 1 { 148 return -1, fmt.Errorf("column is beyond end of file") 149 } 150 // at the end of the file, allowing for a trailing eol 151 return l.file.Size(), nil 152 } 153 pos := lineStart(l.file, line) 154 if !pos.IsValid() { 155 return -1, fmt.Errorf("line is not in file") 156 } 157 // we assume that column is in bytes here, and that the first byte of a 158 // line is at column 1 159 pos += token.Pos(col - 1) 160 return offset(l.file, pos) 161} 162