mirror of
https://github.com/golang/go
synced 2024-11-18 10:14:45 -07:00
internal/span: adding a source location package to unify the pos conversions
This implements a standard way of describing source locations along with printing and parsing of those locations and conversion to and from the token.Pos forms. Change-Id: Ibb3df0a282bc2effb0fa8bd3a51bb0d281b2ffb1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/163778 Run-TryBot: Ian Cottrell <iancottrell@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
parent
6a08e3108d
commit
b40df0fb21
121
internal/span/parse.go
Normal file
121
internal/span/parse.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
// 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 (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse returns the location represented by the input.
|
||||||
|
// All inputs are valid locations, as they can always be a pure filename.
|
||||||
|
func Parse(input string) Span {
|
||||||
|
// :0:0#0-0:0#0
|
||||||
|
valid := input
|
||||||
|
var hold, offset int
|
||||||
|
hadCol := false
|
||||||
|
suf := rstripSuffix(input)
|
||||||
|
if suf.sep == "#" {
|
||||||
|
offset = suf.num
|
||||||
|
suf = rstripSuffix(suf.remains)
|
||||||
|
}
|
||||||
|
if suf.sep == ":" {
|
||||||
|
valid = suf.remains
|
||||||
|
hold = suf.num
|
||||||
|
hadCol = true
|
||||||
|
suf = rstripSuffix(suf.remains)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case suf.sep == ":":
|
||||||
|
p := Point{Line: clamp(suf.num), Column: clamp(hold), Offset: clamp(offset)}
|
||||||
|
return Span{
|
||||||
|
URI: NewURI(suf.remains),
|
||||||
|
Start: p,
|
||||||
|
End: p,
|
||||||
|
}
|
||||||
|
case suf.sep == "-":
|
||||||
|
// we have a span, fall out of the case to continue
|
||||||
|
default:
|
||||||
|
// separator not valid, rewind to either the : or the start
|
||||||
|
p := Point{Line: clamp(hold), Column: 0, Offset: clamp(offset)}
|
||||||
|
return Span{
|
||||||
|
URI: NewURI(valid),
|
||||||
|
Start: p,
|
||||||
|
End: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// only the span form can get here
|
||||||
|
// at this point we still don't know what the numbers we have mean
|
||||||
|
// if have not yet seen a : then we might have either a line or a column depending
|
||||||
|
// on whether start has a column or not
|
||||||
|
// we build an end point and will fix it later if needed
|
||||||
|
end := Point{Line: clamp(suf.num), Column: clamp(hold), Offset: clamp(offset)}
|
||||||
|
hold, offset = 0, 0
|
||||||
|
suf = rstripSuffix(suf.remains)
|
||||||
|
if suf.sep == "#" {
|
||||||
|
offset = suf.num
|
||||||
|
suf = rstripSuffix(suf.remains)
|
||||||
|
}
|
||||||
|
if suf.sep != ":" {
|
||||||
|
// turns out we don't have a span after all, rewind
|
||||||
|
return Span{
|
||||||
|
URI: NewURI(valid),
|
||||||
|
Start: end,
|
||||||
|
End: end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid = suf.remains
|
||||||
|
hold = suf.num
|
||||||
|
suf = rstripSuffix(suf.remains)
|
||||||
|
if suf.sep != ":" {
|
||||||
|
// line#offset only
|
||||||
|
return Span{
|
||||||
|
URI: NewURI(valid),
|
||||||
|
Start: Point{Line: clamp(hold), Column: 0, Offset: clamp(offset)},
|
||||||
|
End: end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we have a column, so if end only had one number, it is also the column
|
||||||
|
if !hadCol {
|
||||||
|
end.Column = end.Line
|
||||||
|
end.Line = clamp(suf.num)
|
||||||
|
}
|
||||||
|
return Span{
|
||||||
|
URI: NewURI(suf.remains),
|
||||||
|
Start: Point{Line: clamp(suf.num), Column: clamp(hold), Offset: clamp(offset)},
|
||||||
|
End: end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type suffix struct {
|
||||||
|
remains string
|
||||||
|
sep string
|
||||||
|
num int
|
||||||
|
}
|
||||||
|
|
||||||
|
func rstripSuffix(input string) suffix {
|
||||||
|
if len(input) == 0 {
|
||||||
|
return suffix{"", "", -1}
|
||||||
|
}
|
||||||
|
remains := input
|
||||||
|
num := -1
|
||||||
|
// first see if we have a number at the end
|
||||||
|
last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' })
|
||||||
|
if last >= 0 && last < len(remains)-1 {
|
||||||
|
number, err := strconv.ParseInt(remains[last+1:], 10, 64)
|
||||||
|
if err == nil {
|
||||||
|
num = int(number)
|
||||||
|
remains = remains[:last+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now see if we have a trailing separator
|
||||||
|
r, w := utf8.DecodeLastRuneInString(remains)
|
||||||
|
if r != ':' && r != '#' && r == '#' {
|
||||||
|
return suffix{input, "", -1}
|
||||||
|
}
|
||||||
|
remains = remains[:len(remains)-w]
|
||||||
|
return suffix{remains, string(r), num}
|
||||||
|
}
|
202
internal/span/span.go
Normal file
202
internal/span/span.go
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Span represents a source code range in standardized form.
|
||||||
|
type Span struct {
|
||||||
|
URI URI `json:"uri"`
|
||||||
|
Start Point `json:"start"`
|
||||||
|
End Point `json:"end"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point represents a single point within a file.
|
||||||
|
// In general this should only be used as part of a Span, as on its own it
|
||||||
|
// does not carry enough information.
|
||||||
|
type Point struct {
|
||||||
|
Line int `json:"line"`
|
||||||
|
Column int `json:"column"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offsets is the interface to an object that can convert to offset
|
||||||
|
// from line:column forms for a single file.
|
||||||
|
type Offsets interface {
|
||||||
|
ToOffset(line, col int) int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coords is the interface to an object that can convert to line:column
|
||||||
|
// from offset forms for a single file.
|
||||||
|
type Coords interface {
|
||||||
|
ToCoord(offset int) (int, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converter is the interface to an object that can convert between line:column
|
||||||
|
// and offset forms for a single file.
|
||||||
|
type Converter interface {
|
||||||
|
Offsets
|
||||||
|
Coords
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format implements fmt.Formatter to print the Location in a standard form.
|
||||||
|
// The format produced is one that can be read back in using Parse.
|
||||||
|
func (s Span) Format(f fmt.State, c rune) {
|
||||||
|
fullForm := f.Flag('+')
|
||||||
|
preferOffset := f.Flag('#')
|
||||||
|
// we should always have a uri, simplify if it is file format
|
||||||
|
//TODO: make sure the end of the uri is unambiguous
|
||||||
|
uri := string(s.URI)
|
||||||
|
if !fullForm {
|
||||||
|
if filename, err := s.URI.Filename(); err == nil {
|
||||||
|
uri = filename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprint(f, uri)
|
||||||
|
// see which bits of start to write
|
||||||
|
printOffset := fullForm || (s.Start.Offset > 0 && (preferOffset || s.Start.Line <= 0))
|
||||||
|
printLine := fullForm || (s.Start.Line > 0 && !printOffset)
|
||||||
|
printColumn := fullForm || (printLine && (s.Start.Column > 1 || s.End.Column > 1))
|
||||||
|
if !printLine && !printColumn && !printOffset {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprint(f, ":")
|
||||||
|
if printLine {
|
||||||
|
fmt.Fprintf(f, "%d", clamp(s.Start.Line))
|
||||||
|
}
|
||||||
|
if printColumn {
|
||||||
|
fmt.Fprintf(f, ":%d", clamp(s.Start.Column))
|
||||||
|
}
|
||||||
|
if printOffset {
|
||||||
|
fmt.Fprintf(f, "#%d", clamp(s.Start.Offset))
|
||||||
|
}
|
||||||
|
// start is written, do we need end?
|
||||||
|
printLine = fullForm || (printLine && s.End.Line > s.Start.Line)
|
||||||
|
isPoint := s.End.Line == s.Start.Line && s.End.Column == s.Start.Column
|
||||||
|
printColumn = fullForm || (printColumn && s.End.Column > 0 && !isPoint)
|
||||||
|
printOffset = fullForm || (printOffset && s.End.Offset > s.Start.Offset)
|
||||||
|
if !printLine && !printColumn && !printOffset {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprint(f, "-")
|
||||||
|
if printLine {
|
||||||
|
fmt.Fprintf(f, "%d", clamp(s.End.Line))
|
||||||
|
}
|
||||||
|
if printColumn {
|
||||||
|
if printLine {
|
||||||
|
fmt.Fprint(f, ":")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(f, "%d", clamp(s.End.Column))
|
||||||
|
}
|
||||||
|
if printOffset {
|
||||||
|
fmt.Fprintf(f, "#%d", clamp(s.End.Offset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanOffset returns a copy of the Span with the Offset field updated.
|
||||||
|
// If the field is missing and Offsets is supplied it will be used to
|
||||||
|
// calculate it from the line and column.
|
||||||
|
// The value will then be adjusted to the canonical form.
|
||||||
|
func (s Span) CleanOffset(offsets Offsets) Span {
|
||||||
|
if offsets != nil {
|
||||||
|
if (s.Start.Line > 1 || s.Start.Column > 1) && s.Start.Offset == 0 {
|
||||||
|
s.Start.updateOffset(offsets)
|
||||||
|
}
|
||||||
|
if (s.End.Line > 1 || s.End.Column > 1) && s.End.Offset == 0 {
|
||||||
|
s.End.updateOffset(offsets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Start.Offset < 0 {
|
||||||
|
s.Start.Offset = 0
|
||||||
|
}
|
||||||
|
if s.End.Offset <= s.Start.Offset {
|
||||||
|
s.End.Offset = s.Start.Offset
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanCoords returns a copy of the Span with the Line and Column fields
|
||||||
|
// cleaned.
|
||||||
|
// If the fields are missing and Coords is supplied it will be used to
|
||||||
|
// calculate them from the offset.
|
||||||
|
// The values will then be adjusted to the canonical form.
|
||||||
|
func (s Span) CleanCoords(coords Coords) Span {
|
||||||
|
if coords != nil {
|
||||||
|
if s.Start.Line == 0 && s.Start.Offset > 0 {
|
||||||
|
s.Start.Line, s.Start.Column = coords.ToCoord(s.Start.Offset)
|
||||||
|
}
|
||||||
|
if s.End.Line == 0 && s.End.Offset > 0 {
|
||||||
|
s.End.Line, s.End.Column = coords.ToCoord(s.End.Offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Start.Line <= 0 {
|
||||||
|
s.Start.Line = 0
|
||||||
|
}
|
||||||
|
if s.Start.Line == 0 {
|
||||||
|
s.Start.Column = 0
|
||||||
|
} else if s.Start.Column <= 0 {
|
||||||
|
s.Start.Column = 0
|
||||||
|
}
|
||||||
|
if s.End.Line < s.Start.Line {
|
||||||
|
s.End.Line = s.Start.Line
|
||||||
|
}
|
||||||
|
if s.End.Column < s.Start.Column {
|
||||||
|
s.End.Column = s.Start.Column
|
||||||
|
}
|
||||||
|
if s.Start.Column <= 1 && s.End.Column <= 1 {
|
||||||
|
s.Start.Column = 0
|
||||||
|
s.End.Column = 0
|
||||||
|
}
|
||||||
|
if s.Start.Line <= 1 && s.End.Line <= 1 && s.Start.Column <= 1 && s.End.Column <= 1 {
|
||||||
|
s.Start.Line = 0
|
||||||
|
s.End.Line = 0
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean returns a copy of the Span fully normalized.
|
||||||
|
// If passed a converter, it will use it to fill in any missing fields by
|
||||||
|
// converting between offset and line column fields.
|
||||||
|
// It does not attempt to validate that already filled fields have consistent
|
||||||
|
// values.
|
||||||
|
func (s Span) Clean(converter Converter) Span {
|
||||||
|
s = s.CleanOffset(converter)
|
||||||
|
s = s.CleanCoords(converter)
|
||||||
|
if s.End.Offset == 0 {
|
||||||
|
// in case CleanCoords adjusted the end position
|
||||||
|
s.End.Offset = s.Start.Offset
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPoint returns true if the span represents a single point.
|
||||||
|
// It is only valid on spans that are "clean".
|
||||||
|
func (s Span) IsPoint() bool {
|
||||||
|
return s.Start == s.End
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Point) updateOffset(offsets Offsets) {
|
||||||
|
p.Offset = 0
|
||||||
|
if p.Line <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c := p.Column
|
||||||
|
if c < 1 {
|
||||||
|
c = 1
|
||||||
|
}
|
||||||
|
if p.Line == 1 && c == 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.Offset = offsets.ToOffset(p.Line, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clamp(v int) int {
|
||||||
|
if v < 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
68
internal/span/span_test.go
Normal file
68
internal/span/span_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
// 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/tools/internal/span"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
formats = []string{"%v", "%#v", "%+v"}
|
||||||
|
tests = [][]string{
|
||||||
|
{"C:/file_a", "C:/file_a", "file:///C:/file_a:0:0#0-0:0#0"},
|
||||||
|
{"C:/file_b:1:2", "C:/file_b:#1", "file:///C:/file_b:1:2#1-1:2#1"},
|
||||||
|
{"C:/file_c:1000", "C:/file_c:#9990", "file:///C:/file_c:1000:0#9990-1000:0#9990"},
|
||||||
|
{"C:/file_d:14:9", "C:/file_d:#138", "file:///C:/file_d:14:9#138-14:9#138"},
|
||||||
|
{"C:/file_e:1:2-7", "C:/file_e:#1-#6", "file:///C:/file_e:1:2#1-1:7#6"},
|
||||||
|
{"C:/file_f:500-502", "C:/file_f:#4990-#5010", "file:///C:/file_f:500:0#4990-502:0#5010"},
|
||||||
|
{"C:/file_g:3:7-8", "C:/file_g:#26-#27", "file:///C:/file_g:3:7#26-3:8#27"},
|
||||||
|
{"C:/file_h:3:7-4:8", "C:/file_h:#26-#37", "file:///C:/file_h:3:7#26-4:8#37"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormat(t *testing.T) {
|
||||||
|
converter := lines(10)
|
||||||
|
for _, test := range tests {
|
||||||
|
for ti, text := range test {
|
||||||
|
spn := span.Parse(text)
|
||||||
|
if ti <= 1 {
|
||||||
|
// we can check %v produces the same as the input
|
||||||
|
expect := toPath(test[ti])
|
||||||
|
if got := fmt.Sprintf("%v", spn); got != expect {
|
||||||
|
t.Errorf("printing %q got %q expected %q", text, got, expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
complete := spn.Clean(converter)
|
||||||
|
for fi, format := range []string{"%v", "%#v", "%+v"} {
|
||||||
|
expect := toPath(test[fi])
|
||||||
|
if got := fmt.Sprintf(format, complete); got != expect {
|
||||||
|
t.Errorf("printing completeted %q as %q got %q expected %q [%+v]", text, format, got, expect, spn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPath(value string) string {
|
||||||
|
if strings.HasPrefix(value, "file://") {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return filepath.FromSlash(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type lines int
|
||||||
|
|
||||||
|
func (l lines) ToCoord(offset int) (int, int) {
|
||||||
|
return (offset / int(l)) + 1, (offset % int(l)) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l lines) ToOffset(line, col int) int {
|
||||||
|
return (int(l) * (line - 1)) + (col - 1)
|
||||||
|
}
|
114
internal/span/token.go
Normal file
114
internal/span/token.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
// 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 (
|
||||||
|
"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 determine the conversions which
|
||||||
|
// make if fast and do 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 Coords and Offsets 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 Coords and Offsets for the
|
||||||
|
// given file content.
|
||||||
|
func NewContentConverter(filename string, content []byte) Converter {
|
||||||
|
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 {
|
||||||
|
f := r.FileSet.File(r.Start)
|
||||||
|
s := Span{URI: FileURI(f.Name())}
|
||||||
|
s.Start.Offset = f.Offset(r.Start)
|
||||||
|
if r.End.IsValid() {
|
||||||
|
s.End.Offset = f.Offset(r.End)
|
||||||
|
}
|
||||||
|
converter := NewTokenConverter(r.FileSet, f)
|
||||||
|
return s.CleanCoords(converter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range converts a Span to a Range that represents the Span for the supplied
|
||||||
|
// File.
|
||||||
|
func (s Span) Range(converter *TokenConverter) Range {
|
||||||
|
s = s.CleanOffset(converter)
|
||||||
|
return Range{
|
||||||
|
FileSet: converter.fset,
|
||||||
|
Start: converter.file.Pos(s.Start.Offset),
|
||||||
|
End: converter.file.Pos(s.End.Offset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *TokenConverter) ToCoord(offset int) (int, int) {
|
||||||
|
pos := l.file.Pos(offset)
|
||||||
|
p := l.fset.Position(pos)
|
||||||
|
return p.Line, p.Column
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *TokenConverter) ToOffset(line, col int) int {
|
||||||
|
if line < 0 {
|
||||||
|
// before the start of the file
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
lineMax := l.file.LineCount() + 1
|
||||||
|
if line > lineMax {
|
||||||
|
// after the end of the file
|
||||||
|
return -1
|
||||||
|
} else if line == lineMax {
|
||||||
|
if col > 1 {
|
||||||
|
// after the end of the file
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
// at the end of the file, allowing for a trailing eol
|
||||||
|
return l.file.Size()
|
||||||
|
}
|
||||||
|
pos := lineStart(l.file, line)
|
||||||
|
if !pos.IsValid() {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
// 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 l.file.Offset(pos)
|
||||||
|
}
|
39
internal/span/token111.go
Normal file
39
internal/span/token111.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// +build !go1.12
|
||||||
|
|
||||||
|
package span
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// lineStart is the pre-Go 1.12 version of (*token.File).LineStart. For Go
|
||||||
|
// versions <= 1.11, we borrow logic from the analysisutil package.
|
||||||
|
// TODO(rstambler): Delete this file when we no longer support Go 1.11.
|
||||||
|
func lineStart(f *token.File, line int) token.Pos {
|
||||||
|
// Use binary search to find the start offset of this line.
|
||||||
|
|
||||||
|
min := 0 // inclusive
|
||||||
|
max := f.Size() // exclusive
|
||||||
|
for {
|
||||||
|
offset := (min + max) / 2
|
||||||
|
pos := f.Pos(offset)
|
||||||
|
posn := f.Position(pos)
|
||||||
|
if posn.Line == line {
|
||||||
|
return pos - (token.Pos(posn.Column) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if min+1 >= max {
|
||||||
|
return token.NoPos
|
||||||
|
}
|
||||||
|
|
||||||
|
if posn.Line < line {
|
||||||
|
min = offset
|
||||||
|
} else {
|
||||||
|
max = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
internal/span/token112.go
Normal file
16
internal/span/token112.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// +build go1.12
|
||||||
|
|
||||||
|
package span
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(rstambler): Delete this file when we no longer support Go 1.11.
|
||||||
|
func lineStart(f *token.File, line int) token.Pos {
|
||||||
|
return f.LineStart(line)
|
||||||
|
}
|
69
internal/span/token_test.go
Normal file
69
internal/span/token_test.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// Copyright 2018 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/token"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/tools/internal/span"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testdata = []struct {
|
||||||
|
uri string
|
||||||
|
content []byte
|
||||||
|
}{
|
||||||
|
{"/a.go", []byte(`
|
||||||
|
// file a.go
|
||||||
|
package test
|
||||||
|
`)},
|
||||||
|
{"/b.go", []byte(`
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// file b.go
|
||||||
|
package test
|
||||||
|
`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenTests = []span.Span{
|
||||||
|
{span.FileURI("/a.go"), span.Point{}, span.Point{}},
|
||||||
|
{span.FileURI("/a.go"), span.Point{3, 7, 20}, span.Point{3, 7, 20}},
|
||||||
|
{span.FileURI("/b.go"), span.Point{4, 9, 15}, span.Point{4, 13, 19}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToken(t *testing.T) {
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
files := map[span.URI]*token.File{}
|
||||||
|
for _, f := range testdata {
|
||||||
|
file := fset.AddFile(f.uri, -1, len(f.content))
|
||||||
|
file.SetLinesForContent(f.content)
|
||||||
|
files[span.FileURI(f.uri)] = file
|
||||||
|
}
|
||||||
|
for _, test := range tokenTests {
|
||||||
|
f := files[test.URI]
|
||||||
|
c := span.NewTokenConverter(fset, f)
|
||||||
|
checkToken(t, c, span.Span{
|
||||||
|
URI: test.URI,
|
||||||
|
Start: span.Point{Line: test.Start.Line, Column: test.Start.Column},
|
||||||
|
End: span.Point{Line: test.End.Line, Column: test.End.Column},
|
||||||
|
}, test)
|
||||||
|
checkToken(t, c, span.Span{
|
||||||
|
URI: test.URI,
|
||||||
|
Start: span.Point{Offset: test.Start.Offset},
|
||||||
|
End: span.Point{Offset: test.End.Offset},
|
||||||
|
}, test)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkToken(t *testing.T, c *span.TokenConverter, in, expect span.Span) {
|
||||||
|
rng := in.Range(c)
|
||||||
|
gotLoc := rng.Span()
|
||||||
|
expected := fmt.Sprintf("%+v", expect)
|
||||||
|
got := fmt.Sprintf("%+v", gotLoc)
|
||||||
|
if expected != got {
|
||||||
|
t.Errorf("Expected %q got %q", expected, got)
|
||||||
|
}
|
||||||
|
}
|
99
internal/span/uri.go
Normal file
99
internal/span/uri.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
// 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"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fileScheme = "file"
|
||||||
|
|
||||||
|
// URI represents the full URI for a file.
|
||||||
|
type URI string
|
||||||
|
|
||||||
|
// Filename returns the file path for the given URI. It will return an error if
|
||||||
|
// the URI is invalid, or if the URI does not have the file scheme.
|
||||||
|
func (uri URI) Filename() (string, error) {
|
||||||
|
filename, err := filename(uri)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.FromSlash(filename), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filename(uri URI) (string, error) {
|
||||||
|
u, err := url.ParseRequestURI(string(uri))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if u.Scheme != fileScheme {
|
||||||
|
return "", fmt.Errorf("only file URIs are supported, got %v", u.Scheme)
|
||||||
|
}
|
||||||
|
if isWindowsDriveURI(u.Path) {
|
||||||
|
u.Path = u.Path[1:]
|
||||||
|
}
|
||||||
|
return u.Path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewURI returns a span URI for the string.
|
||||||
|
// It will attempt to detect if the string is a file path or uri.
|
||||||
|
func NewURI(s string) URI {
|
||||||
|
if strings.HasPrefix(s, fileScheme+"://") {
|
||||||
|
return URI(s)
|
||||||
|
}
|
||||||
|
return FileURI(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileURI returns a span URI for the supplied file path.
|
||||||
|
// It will always have the file scheme.
|
||||||
|
func FileURI(path string) URI {
|
||||||
|
// Handle standard library paths that contain the literal "$GOROOT".
|
||||||
|
// TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT.
|
||||||
|
const prefix = "$GOROOT"
|
||||||
|
if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) {
|
||||||
|
suffix := path[len(prefix):]
|
||||||
|
path = runtime.GOROOT() + suffix
|
||||||
|
}
|
||||||
|
if !isWindowsDrivePath(path) {
|
||||||
|
if abs, err := filepath.Abs(path); err == nil {
|
||||||
|
path = abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check the file path again, in case it became absolute.
|
||||||
|
if isWindowsDrivePath(path) {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
path = filepath.ToSlash(path)
|
||||||
|
u := url.URL{
|
||||||
|
Scheme: fileScheme,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
return URI(u.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWindowsDrivePath returns true if the file path is of the form used by
|
||||||
|
// Windows. We check if the path begins with a drive letter, followed by a ":".
|
||||||
|
func isWindowsDrivePath(path string) bool {
|
||||||
|
if len(path) < 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return unicode.IsLetter(rune(path[0])) && path[1] == ':'
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWindowsDriveURI returns true if the file URI is of the format used by
|
||||||
|
// Windows URIs. The url.Parse package does not specially handle Windows paths
|
||||||
|
// (see https://golang.org/issue/6027). We check if the URI path has
|
||||||
|
// a drive prefix (e.g. "/C:"). If so, we trim the leading "/".
|
||||||
|
func isWindowsDriveURI(uri string) bool {
|
||||||
|
if len(uri) < 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
|
||||||
|
}
|
50
internal/span/uri_test.go
Normal file
50
internal/span/uri_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/tools/internal/span"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestURI tests the conversion between URIs and filenames. The test cases
|
||||||
|
// include Windows-style URIs and filepaths, but we avoid having OS-specific
|
||||||
|
// tests by using only forward slashes, assuming that the standard library
|
||||||
|
// functions filepath.ToSlash and filepath.FromSlash do not need testing.
|
||||||
|
func TestURI(t *testing.T) {
|
||||||
|
for _, test := range []string{
|
||||||
|
`C:/Windows/System32`,
|
||||||
|
`C:/Go/src/bob.go`,
|
||||||
|
`c:/Go/src/bob.go`,
|
||||||
|
`/path/to/dir`,
|
||||||
|
`/a/b/c/src/bob.go`,
|
||||||
|
} {
|
||||||
|
testPath := filepath.FromSlash(test)
|
||||||
|
expectPath := testPath
|
||||||
|
if test[0] == '/' {
|
||||||
|
if abs, err := filepath.Abs(expectPath); err == nil {
|
||||||
|
expectPath = abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expectURI := filepath.ToSlash(expectPath)
|
||||||
|
if expectURI[0] != '/' {
|
||||||
|
expectURI = "/" + expectURI
|
||||||
|
}
|
||||||
|
expectURI = "file://" + expectURI
|
||||||
|
uri := span.FileURI(testPath)
|
||||||
|
if expectURI != string(uri) {
|
||||||
|
t.Errorf("ToURI: expected %s, got %s", expectURI, uri)
|
||||||
|
}
|
||||||
|
filename, err := uri.Filename()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if expectPath != filename {
|
||||||
|
t.Errorf("Filename: expected %s, got %s", expectPath, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user