1
0
mirror of https://github.com/golang/go synced 2024-09-30 14:18:32 -06: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:
Ian Cottrell 2019-02-12 13:13:49 -05:00
parent 6a08e3108d
commit b40df0fb21
9 changed files with 778 additions and 0 deletions

121
internal/span/parse.go Normal file
View 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
View 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
}

View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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)
}
}
}