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  }