// Copyright 2019 The Go Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package span import ( "fmt" "go/token" ) // Range represents a source code range in token.Pos form. // 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 } // 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. type TokenConverter struct { 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 { return Range{ FileSet: fset, Start: start, End: end, } } // NewTokenConverter returns an implementation of Converter backed by a // token.File. func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter { return &TokenConverter{fset: fset, file: f} } // NewContentConverter returns an implementation of Converter for the // given file content. func NewContentConverter(filename string, content []byte) *TokenConverter { fset := token.NewFileSet() f := fset.AddFile(filename, -1, len(content)) f.SetLinesForContent(content) return &TokenConverter{fset: fset, file: f} } // IsPoint returns true if the range represents a single point. func (r Range) IsPoint() bool { return r.Start == r.End } // Span converts a Range to a Span that represents the Range. // It will fill in all the members of the Span, calculating the line and column // information. func (r Range) Span() (Span, error) { f := r.FileSet.File(r.Start) if f == nil { return Span{}, fmt.Errorf("file not found in FileSet") } s := Span{v: span{URI: FileURI(f.Name())}} var err error s.v.Start.Offset, err = offset(f, r.Start) if err != nil { return Span{}, err } if r.End.IsValid() { s.v.End.Offset, err = offset(f, r.End) if err != nil { return Span{}, err } } s.v.Start.clean() s.v.End.clean() s.v.clean() converter := NewTokenConverter(r.FileSet, f) return s.WithPosition(converter) } // offset is a copy of the Offset function in go/token, but with the adjustment // 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 int(pos) - f.Base(), nil } // Range converts a Span to a Range that represents the Span for the supplied // File. func (s Span) Range(converter *TokenConverter) (Range, error) { s, err := s.WithOffset(converter) if err != nil { return Range{}, err } // 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.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()) } return Range{ FileSet: converter.fset, Start: converter.file.Pos(s.Start().Offset()), End: converter.file.Pos(s.End().Offset()), }, nil } func (l *TokenConverter) ToPosition(offset int) (int, int, error) { if offset > l.file.Size() { return 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, l.file.Size()) } pos := l.file.Pos(offset) p := l.fset.Position(pos) if offset == l.file.Size() { return p.Line + 1, 1, nil } return p.Line, p.Column, nil } func (l *TokenConverter) ToOffset(line, col int) (int, error) { if line < 0 { return -1, fmt.Errorf("line is not valid") } lineMax := l.file.LineCount() + 1 if line > lineMax { return -1, fmt.Errorf("line is beyond end of file %v", lineMax) } else if line == lineMax { if col > 1 { return -1, fmt.Errorf("column is beyond end of file") } // at the end of the file, allowing for a trailing eol return l.file.Size(), nil } pos := lineStart(l.file, line) if !pos.IsValid() { return -1, fmt.Errorf("line is not in file") } // we assume that column is in bytes here, and that the first byte of a // line is at column 1 pos += token.Pos(col - 1) return offset(l.file, pos) }