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