// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package span import ( "fmt" "go/token" "golang.org/x/tools/internal/lsp/bug" ) // Range represents a source code range in token.Pos form. // It also carries the token.File that produced the positions, so that it is // self contained. type Range struct { TokFile *token.File // non-nil Start, End token.Pos // both IsValid() } // NewRange creates a new Range from a token.File and two valid positions within it. // // (If you only have a token.FileSet, use file = fset.File(start). But // most callers know exactly which token.File they're dealing with and // should pass it explicitly. Not only does this save a lookup, but it // brings us a step closer to eliminating the global FileSet.) func NewRange(file *token.File, start, end token.Pos) Range { if file == nil { panic("nil *token.File") } if !start.IsValid() { panic("invalid start token.Pos") } if !end.IsValid() { panic("invalid end token.Pos") } // TODO(adonovan): ideally we would make this stronger assertion: // // // Assert that file is non-nil and contains start and end. // _ = file.Offset(start) // _ = file.Offset(end) // // but some callers (e.g. packageCompletionSurrounding, // posToMappedRange) don't ensure this precondition. return Range{ TokFile: file, Start: start, End: end, } } // NewTokenFile returns a token.File for the given file content. func NewTokenFile(filename string, content []byte) *token.File { fset := token.NewFileSet() f := fset.AddFile(filename, -1, len(content)) f.SetLinesForContent(content) return 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) { return FileSpan(r.TokFile, r.TokFile, r.Start, r.End) } // FileSpan returns a span within the file referenced by start and end, using a // token.File to translate between offsets and positions. // // The start and end position must be contained within posFile, though due to // line directives they may reference positions in another file. If srcFile is // provided, it is used to map the line:column positions referenced by start // and end to offsets in the corresponding file. // // TODO(adonovan): clarify whether it is valid to pass posFile==srcFile when // //line directives are in use. If so, fix this function; if not, fix Range.Span. func FileSpan(posFile, srcFile *token.File, start, end token.Pos) (Span, error) { if !start.IsValid() { return Span{}, fmt.Errorf("start pos is not valid") } if posFile == nil { return Span{}, bug.Errorf("missing file association") // should never get here with a nil file } var s Span var err error var startFilename string startFilename, s.v.Start.Line, s.v.Start.Column, err = position(posFile, start) if err != nil { return Span{}, err } s.v.URI = URIFromPath(startFilename) if end.IsValid() { var endFilename string endFilename, s.v.End.Line, s.v.End.Column, err = position(posFile, end) if err != nil { return Span{}, err } // In the presence of line directives, a single File can have sections from // multiple file names. if endFilename != startFilename { return Span{}, fmt.Errorf("span begins in file %q but ends in %q", startFilename, endFilename) } } s.v.Start.clean() s.v.End.clean() s.v.clean() tf := posFile if srcFile != nil { tf = srcFile } if startFilename != tf.Name() { // 'start' identifies a position specified by a //line directive // in a file other than the one containing the directive. // (Debugging support for https://github.com/golang/go/issues/54655.) // // This used to be a bug.Errorf, but that was unsound because // Range.Span passes this function the same TokFile argument twice, // which is never going to pass this test for a file containing // a //line directive. // TODO(adonovan): decide where the bug.Errorf really belongs. return Span{}, fmt.Errorf("must supply Converter for file %q (tf.Name() = %q)", startFilename, tf.Name()) } return s.WithOffset(tf) } func position(tf *token.File, pos token.Pos) (string, int, int, error) { off, err := offset(tf, pos) if err != nil { return "", 0, 0, err } return positionFromOffset(tf, off) } func positionFromOffset(tf *token.File, offset int) (string, int, int, error) { if offset > tf.Size() { return "", 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, tf.Size()) } pos := tf.Pos(offset) p := tf.Position(pos) // TODO(golang/go#41029): Consider returning line, column instead of line+1, 1 if // the file's last character is not a newline. if offset == tf.Size() { return p.Filename, p.Line + 1, 1, nil } return p.Filename, p.Line, p.Column, nil } // 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(tf *token.File, pos token.Pos) (int, error) { if int(pos) < tf.Base() || int(pos) > tf.Base()+tf.Size() { return 0, fmt.Errorf("invalid pos: %d not in [%d, %d]", pos, tf.Base(), tf.Base()+tf.Size()) } return int(pos) - tf.Base(), nil } // Range converts a Span to a Range that represents the Span for the supplied // File. func (s Span) Range(tf *token.File) (Range, error) { s, err := s.WithOffset(tf) 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() > tf.Size() { return Range{}, bug.Errorf("start offset %v is past the end of the file %v", s.Start(), tf.Size()) } if s.End().Offset() > tf.Size() { return Range{}, bug.Errorf("end offset %v is past the end of the file %v", s.End(), tf.Size()) } return Range{ Start: tf.Pos(s.Start().Offset()), End: tf.Pos(s.End().Offset()), TokFile: tf, }, nil } // ToPosition converts a byte offset in the file corresponding to tf into // 1-based line and utf-8 column indexes. func ToPosition(tf *token.File, offset int) (int, int, error) { _, line, col, err := positionFromOffset(tf, offset) return line, col, err } // ToOffset converts a 1-based line and utf-8 column index into a byte offset // in the file corresponding to tf. func ToOffset(tf *token.File, line, col int) (int, error) { if line < 1 { // token.File.LineStart panics if line < 1 return -1, fmt.Errorf("invalid line: %d", line) } lineMax := tf.LineCount() + 1 if line > lineMax { return -1, fmt.Errorf("line %d is beyond end of file %v", line, 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 tf.Size(), nil } pos := tf.LineStart(line) if !pos.IsValid() { // bug.Errorf here because LineStart panics on out-of-bound input, and so // should never return invalid positions. return -1, bug.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) // Debugging support for https://github.com/golang/go/issues/54655. if pos > token.Pos(tf.Base()+tf.Size()) { return 0, fmt.Errorf("ToOffset: column %d is beyond end of file", col) } return offset(tf, pos) }