github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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  type 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.
    29  type 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.
    36  func 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.
    46  func 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.
    52  func 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.
    60  func (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.
    67  func (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.
    80  func 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  
   113  func 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  
   121  func 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.
   137  func 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.
   146  func (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  
   167  func (l *FileConverter) ToPosition(offset int) (int, int, error) {
   168  	_, line, col, err := positionFromOffset(l.file, offset)
   169  	return line, col, err
   170  }
   171  
   172  func (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  }