diff options
Diffstat (limited to 'gopls/internal/span')
-rw-r--r-- | gopls/internal/span/parse.go | 114 | ||||
-rw-r--r-- | gopls/internal/span/span.go | 253 | ||||
-rw-r--r-- | gopls/internal/span/span_test.go | 57 | ||||
-rw-r--r-- | gopls/internal/span/uri.go | 185 | ||||
-rw-r--r-- | gopls/internal/span/uri_test.go | 117 | ||||
-rw-r--r-- | gopls/internal/span/uri_windows_test.go | 112 |
6 files changed, 838 insertions, 0 deletions
diff --git a/gopls/internal/span/parse.go b/gopls/internal/span/parse.go new file mode 100644 index 000000000..715d5fe44 --- /dev/null +++ b/gopls/internal/span/parse.go @@ -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 ( + "path/filepath" + "strconv" + "strings" + "unicode/utf8" +) + +// Parse returns the location represented by the input. +// Only file paths are accepted, not URIs. +// The returned span will be normalized, and thus if printed may produce a +// different string. +func Parse(input string) Span { + return ParseInDir(input, ".") +} + +// ParseInDir is like Parse, but interprets paths relative to wd. +func ParseInDir(input, wd string) Span { + uri := func(path string) URI { + if !filepath.IsAbs(path) { + path = filepath.Join(wd, path) + } + return URIFromPath(path) + } + // :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 == ":": + return New(uri(suf.remains), NewPoint(suf.num, hold, offset), Point{}) + 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 + return New(uri(valid), NewPoint(hold, 0, offset), Point{}) + } + // 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 := NewPoint(suf.num, hold, 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 New(uri(valid), end, Point{}) + } + valid = suf.remains + hold = suf.num + suf = rstripSuffix(suf.remains) + if suf.sep != ":" { + // line#offset only + return New(uri(valid), NewPoint(hold, 0, offset), end) + } + // we have a column, so if end only had one number, it is also the column + if !hadCol { + end = NewPoint(suf.num, end.v.Line, end.v.Offset) + } + return New(uri(suf.remains), NewPoint(suf.num, hold, offset), end) +} + +type suffix struct { + remains string + sep string + num int +} + +func rstripSuffix(input string) suffix { + if len(input) == 0 { + return suffix{"", "", -1} + } + remains := input + + // Remove optional trailing decimal number. + num := -1 + 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) + // TODO(adonovan): this condition is clearly wrong. Should the third byte be '-'? + if r != ':' && r != '#' && r == '#' { + return suffix{input, "", -1} + } + remains = remains[:len(remains)-w] + return suffix{remains, string(r), num} +} diff --git a/gopls/internal/span/span.go b/gopls/internal/span/span.go new file mode 100644 index 000000000..07345c8ef --- /dev/null +++ b/gopls/internal/span/span.go @@ -0,0 +1,253 @@ +// 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 contains support for representing with positions and ranges in +// text files. +package span + +import ( + "encoding/json" + "fmt" + "go/token" + "path" + "sort" + "strings" + + "golang.org/x/tools/gopls/internal/lsp/safetoken" +) + +// A Span represents a range of text within a source file. The start +// and end points of a valid span may be hold either its byte offset, +// or its (line, column) pair, or both. Columns are measured in bytes. +// +// Spans are appropriate in user interfaces (e.g. command-line tools) +// and tests where a position is notated without access to the content +// of the file. +// +// Use protocol.Mapper to convert between Span and other +// representations, such as go/token (also UTF-8) or the LSP protocol +// (UTF-16). The latter requires access to file contents. +// +// See overview comments at ../lsp/protocol/mapper.go. +type Span struct { + v span +} + +// 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 { + v point +} + +// The private span/point types have public fields to support JSON +// encoding, but the public Span/Point types hide these fields by +// defining methods that shadow them. (This is used by a few of the +// command-line tool subcommands, which emit spans and have a -json +// flag.) + +type span struct { + URI URI `json:"uri"` + Start point `json:"start"` + End point `json:"end"` +} + +type point struct { + Line int `json:"line"` // 1-based line number + Column int `json:"column"` // 1-based, UTF-8 codes (bytes) + Offset int `json:"offset"` // 0-based byte offset +} + +// Invalid is a span that reports false from IsValid +var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}} + +var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}} + +func New(uri URI, start, end Point) Span { + s := Span{v: span{URI: uri, Start: start.v, End: end.v}} + s.v.clean() + return s +} + +func NewPoint(line, col, offset int) Point { + p := Point{v: point{Line: line, Column: col, Offset: offset}} + p.v.clean() + return p +} + +// SortSpans sorts spans into a stable but unspecified order. +func SortSpans(spans []Span) { + sort.SliceStable(spans, func(i, j int) bool { + return compare(spans[i], spans[j]) < 0 + }) +} + +// compare implements a three-valued ordered comparison of Spans. +func compare(a, b Span) int { + // This is a textual comparison. It does not perform path + // cleaning, case folding, resolution of symbolic links, + // testing for existence, or any I/O. + if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 { + return cmp + } + if cmp := comparePoint(a.v.Start, b.v.Start); cmp != 0 { + return cmp + } + return comparePoint(a.v.End, b.v.End) +} + +func ComparePoint(a, b Point) int { + return comparePoint(a.v, b.v) +} + +func comparePoint(a, b point) int { + if !a.hasPosition() { + if a.Offset < b.Offset { + return -1 + } + if a.Offset > b.Offset { + return 1 + } + return 0 + } + if a.Line < b.Line { + return -1 + } + if a.Line > b.Line { + return 1 + } + if a.Column < b.Column { + return -1 + } + if a.Column > b.Column { + return 1 + } + return 0 +} + +func (s Span) HasPosition() bool { return s.v.Start.hasPosition() } +func (s Span) HasOffset() bool { return s.v.Start.hasOffset() } +func (s Span) IsValid() bool { return s.v.Start.isValid() } +func (s Span) IsPoint() bool { return s.v.Start == s.v.End } +func (s Span) URI() URI { return s.v.URI } +func (s Span) Start() Point { return Point{s.v.Start} } +func (s Span) End() Point { return Point{s.v.End} } +func (s *Span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) } +func (s *Span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) } + +func (p Point) HasPosition() bool { return p.v.hasPosition() } +func (p Point) HasOffset() bool { return p.v.hasOffset() } +func (p Point) IsValid() bool { return p.v.isValid() } +func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) } +func (p *Point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) } +func (p Point) Line() int { + if !p.v.hasPosition() { + panic(fmt.Errorf("position not set in %v", p.v)) + } + return p.v.Line +} +func (p Point) Column() int { + if !p.v.hasPosition() { + panic(fmt.Errorf("position not set in %v", p.v)) + } + return p.v.Column +} +func (p Point) Offset() int { + if !p.v.hasOffset() { + panic(fmt.Errorf("offset not set in %v", p.v)) + } + return p.v.Offset +} + +func (p point) hasPosition() bool { return p.Line > 0 } +func (p point) hasOffset() bool { return p.Offset >= 0 } +func (p point) isValid() bool { return p.hasPosition() || p.hasOffset() } +func (p point) isZero() bool { + return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0) +} + +func (s *span) clean() { + //this presumes the points are already clean + if !s.End.isValid() || (s.End == point{}) { + s.End = s.Start + } +} + +func (p *point) clean() { + if p.Line < 0 { + p.Line = 0 + } + if p.Column <= 0 { + if p.Line > 0 { + p.Column = 1 + } else { + p.Column = 0 + } + } + if p.Offset == 0 && (p.Line > 1 || p.Column > 1) { + p.Offset = -1 + } +} + +// 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.v.URI) + if c == 'f' { + uri = path.Base(uri) + } else if !fullForm { + uri = s.v.URI.Filename() + } + fmt.Fprint(f, uri) + if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) { + return + } + // see which bits of start to write + printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition()) + printLine := s.HasPosition() && (fullForm || !printOffset) + printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1)) + fmt.Fprint(f, ":") + if printLine { + fmt.Fprintf(f, "%d", s.v.Start.Line) + } + if printColumn { + fmt.Fprintf(f, ":%d", s.v.Start.Column) + } + if printOffset { + fmt.Fprintf(f, "#%d", s.v.Start.Offset) + } + // start is written, do we need end? + if s.IsPoint() { + return + } + // we don't print the line if it did not change + printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line) + fmt.Fprint(f, "-") + if printLine { + fmt.Fprintf(f, "%d", s.v.End.Line) + } + if printColumn { + if printLine { + fmt.Fprint(f, ":") + } + fmt.Fprintf(f, "%d", s.v.End.Column) + } + if printOffset { + fmt.Fprintf(f, "#%d", s.v.End.Offset) + } +} + +// SetRange implements packagestest.rangeSetter, allowing +// gopls' test suites to use Spans instead of Range in parameters. +func (span *Span) SetRange(file *token.File, start, end token.Pos) { + point := func(pos token.Pos) Point { + posn := safetoken.Position(file, pos) + return NewPoint(posn.Line, posn.Column, posn.Offset) + } + *span = New(URIFromPath(file.Name()), point(start), point(end)) +} diff --git a/gopls/internal/span/span_test.go b/gopls/internal/span/span_test.go new file mode 100644 index 000000000..d2aaff12c --- /dev/null +++ b/gopls/internal/span/span_test.go @@ -0,0 +1,57 @@ +// 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/gopls/internal/span" +) + +func TestFormat(t *testing.T) { + formats := []string{"%v", "%#v", "%+v"} + + // Element 0 is the input, and the elements 0-2 are the expected + // output in [%v %#v %+v] formats. Thus the first must be in + // canonical form (invariant under span.Parse + fmt.Sprint). + // The '#' form displays offsets; the '+' form outputs a URI. + // If len=4, element 0 is a noncanonical input and 1-3 are expected outputs. + for _, test := range [][]string{ + {"C:/file_a", "C:/file_a", "file:///C:/file_a:#0"}, + {"C:/file_b:1:2", "C:/file_b:1:2", "file:///C:/file_b:1:2"}, + {"C:/file_c:1000", "C:/file_c:1000", "file:///C:/file_c:1000:1"}, + {"C:/file_d:14:9", "C:/file_d:14:9", "file:///C:/file_d:14:9"}, + {"C:/file_e:1:2-7", "C:/file_e:1:2-7", "file:///C:/file_e:1:2-1:7"}, + {"C:/file_f:500-502", "C:/file_f:500-502", "file:///C:/file_f:500:1-502:1"}, + {"C:/file_g:3:7-8", "C:/file_g:3:7-8", "file:///C:/file_g:3:7-3:8"}, + {"C:/file_h:3:7-4:8", "C:/file_h:3:7-4:8", "file:///C:/file_h:3:7-4:8"}, + {"C:/file_i:#100", "C:/file_i:#100", "file:///C:/file_i:#100"}, + {"C:/file_j:#26-#28", "C:/file_j:#26-#28", "file:///C:/file_j:#26-0#28"}, // 0#28? + {"C:/file_h:3:7#26-4:8#37", // not canonical + "C:/file_h:3:7-4:8", "C:/file_h:#26-#37", "file:///C:/file_h:3:7#26-4:8#37"}} { + input := test[0] + spn := span.Parse(input) + wants := test[0:3] + if len(test) == 4 { + wants = test[1:4] + } + for i, format := range formats { + want := toPath(wants[i]) + if got := fmt.Sprintf(format, spn); got != want { + t.Errorf("Sprintf(%q, %q) = %q, want %q", format, input, got, want) + } + } + } +} + +func toPath(value string) string { + if strings.HasPrefix(value, "file://") { + return value + } + return filepath.FromSlash(value) +} diff --git a/gopls/internal/span/uri.go b/gopls/internal/span/uri.go new file mode 100644 index 000000000..e6191f7ab --- /dev/null +++ b/gopls/internal/span/uri.go @@ -0,0 +1,185 @@ +// 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" + "os" + "path/filepath" + "runtime" + "strings" + "unicode" +) + +const fileScheme = "file" + +// URI represents the full URI for a file. +type URI string + +func (uri URI) IsFile() bool { + return strings.HasPrefix(string(uri), "file://") +} + +// Filename returns the file path for the given URI. +// It is an error to call this on a URI that is not a valid filename. +func (uri URI) Filename() string { + filename, err := filename(uri) + if err != nil { + panic(err) + } + return filepath.FromSlash(filename) +} + +func filename(uri URI) (string, error) { + if uri == "" { + return "", nil + } + + // This conservative check for the common case + // of a simple non-empty absolute POSIX filename + // avoids the allocation of a net.URL. + if strings.HasPrefix(string(uri), "file:///") { + rest := string(uri)[len("file://"):] // leave one slash + for i := 0; i < len(rest); i++ { + b := rest[i] + // Reject these cases: + if b < ' ' || b == 0x7f || // control character + b == '%' || b == '+' || // URI escape + b == ':' || // Windows drive letter + b == '@' || b == '&' || b == '?' { // authority or query + goto slow + } + } + return rest, nil + } +slow: + + u, err := url.ParseRequestURI(string(uri)) + if err != nil { + return "", err + } + if u.Scheme != fileScheme { + return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri) + } + // If the URI is a Windows URI, we trim the leading "/" and uppercase + // the drive letter, which will never be case sensitive. + if isWindowsDriveURIPath(u.Path) { + u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] + } + + return u.Path, nil +} + +// TODO(adonovan): document this function, and any invariants of +// span.URI that it is supposed to establish. +func URIFromURI(s string) URI { + if !strings.HasPrefix(s, "file://") { + return URI(s) + } + + if !strings.HasPrefix(s, "file:///") { + // VS Code sends URLs with only two slashes, which are invalid. golang/go#39789. + s = "file:///" + s[len("file://"):] + } + // Even though the input is a URI, it may not be in canonical form. VS Code + // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize. + path, err := url.PathUnescape(s[len("file://"):]) + if err != nil { + panic(err) + } + + // File URIs from Windows may have lowercase drive letters. + // Since drive letters are guaranteed to be case insensitive, + // we change them to uppercase to remain consistent. + // For example, file:///c:/x/y/z becomes file:///C:/x/y/z. + if isWindowsDriveURIPath(path) { + path = path[:1] + strings.ToUpper(string(path[1])) + path[2:] + } + u := url.URL{Scheme: fileScheme, Path: path} + return URI(u.String()) +} + +// SameExistingFile reports whether two spans denote the +// same existing file by querying the file system. +func SameExistingFile(a, b URI) bool { + fa, err := filename(a) + if err != nil { + return false + } + fb, err := filename(b) + if err != nil { + return false + } + infoa, err := os.Stat(filepath.FromSlash(fa)) + if err != nil { + return false + } + infob, err := os.Stat(filepath.FromSlash(fb)) + if err != nil { + return false + } + return os.SameFile(infoa, infob) +} + +// URIFromPath returns a span URI for the supplied file path. +// +// For empty paths, URIFromPath returns the empty URI "". +// For non-empty paths, URIFromPath returns a uri with the file:// scheme. +func URIFromPath(path string) URI { + if path == "" { + return "" + } + // 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 = "/" + strings.ToUpper(string(path[0])) + path[1:] + } + 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 ":". +// For example: C:/x/y/z. +func isWindowsDrivePath(path string) bool { + if len(path) < 3 { + return false + } + return unicode.IsLetter(rune(path[0])) && path[1] == ':' +} + +// isWindowsDriveURIPath 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 golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). +func isWindowsDriveURIPath(uri string) bool { + if len(uri) < 4 { + return false + } + return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' +} + +// Dir returns the URI for the directory containing uri. Dir panics if uri is +// not a file uri. +// +// TODO(rfindley): add a unit test for various edge cases. +func Dir(uri URI) URI { + return URIFromPath(filepath.Dir(uri.Filename())) +} diff --git a/gopls/internal/span/uri_test.go b/gopls/internal/span/uri_test.go new file mode 100644 index 000000000..e99043785 --- /dev/null +++ b/gopls/internal/span/uri_test.go @@ -0,0 +1,117 @@ +// 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. + +//go:build !windows +// +build !windows + +package span_test + +import ( + "testing" + + "golang.org/x/tools/gopls/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 TestURIFromPath(t *testing.T) { + for _, test := range []struct { + path, wantFile string + wantURI span.URI + }{ + { + path: ``, + wantFile: ``, + wantURI: span.URI(""), + }, + { + path: `C:/Windows/System32`, + wantFile: `C:/Windows/System32`, + wantURI: span.URI("file:///C:/Windows/System32"), + }, + { + path: `C:/Go/src/bob.go`, + wantFile: `C:/Go/src/bob.go`, + wantURI: span.URI("file:///C:/Go/src/bob.go"), + }, + { + path: `c:/Go/src/bob.go`, + wantFile: `C:/Go/src/bob.go`, + wantURI: span.URI("file:///C:/Go/src/bob.go"), + }, + { + path: `/path/to/dir`, + wantFile: `/path/to/dir`, + wantURI: span.URI("file:///path/to/dir"), + }, + { + path: `/a/b/c/src/bob.go`, + wantFile: `/a/b/c/src/bob.go`, + wantURI: span.URI("file:///a/b/c/src/bob.go"), + }, + { + path: `c:/Go/src/bob george/george/george.go`, + wantFile: `C:/Go/src/bob george/george/george.go`, + wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), + }, + } { + got := span.URIFromPath(test.path) + if got != test.wantURI { + t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI) + } + gotFilename := got.Filename() + if gotFilename != test.wantFile { + t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) + } + } +} + +func TestURIFromURI(t *testing.T) { + for _, test := range []struct { + inputURI, wantFile string + wantURI span.URI + }{ + { + inputURI: `file:///c:/Go/src/bob%20george/george/george.go`, + wantFile: `C:/Go/src/bob george/george/george.go`, + wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), + }, + { + inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`, + wantFile: `C:/Go/src/bob george/george/george.go`, + wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), + }, + { + inputURI: `file:///path/to/%25p%25ercent%25/per%25cent.go`, + wantFile: `/path/to/%p%ercent%/per%cent.go`, + wantURI: span.URI(`file:///path/to/%25p%25ercent%25/per%25cent.go`), + }, + { + inputURI: `file:///C%3A/`, + wantFile: `C:/`, + wantURI: span.URI(`file:///C:/`), + }, + { + inputURI: `file:///`, + wantFile: `/`, + wantURI: span.URI(`file:///`), + }, + { + inputURI: `file://wsl%24/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`, + wantFile: `/wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`, + wantURI: span.URI(`file:///wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`), + }, + } { + got := span.URIFromURI(test.inputURI) + if got != test.wantURI { + t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI) + } + gotFilename := got.Filename() + if gotFilename != test.wantFile { + t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) + } + } +} diff --git a/gopls/internal/span/uri_windows_test.go b/gopls/internal/span/uri_windows_test.go new file mode 100644 index 000000000..3891e0d3e --- /dev/null +++ b/gopls/internal/span/uri_windows_test.go @@ -0,0 +1,112 @@ +// Copyright 2020 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. + +//go:build windows +// +build windows + +package span_test + +import ( + "testing" + + "golang.org/x/tools/gopls/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 TestURIFromPath(t *testing.T) { + for _, test := range []struct { + path, wantFile string + wantURI span.URI + }{ + { + path: ``, + wantFile: ``, + wantURI: span.URI(""), + }, + { + path: `C:\Windows\System32`, + wantFile: `C:\Windows\System32`, + wantURI: span.URI("file:///C:/Windows/System32"), + }, + { + path: `C:\Go\src\bob.go`, + wantFile: `C:\Go\src\bob.go`, + wantURI: span.URI("file:///C:/Go/src/bob.go"), + }, + { + path: `c:\Go\src\bob.go`, + wantFile: `C:\Go\src\bob.go`, + wantURI: span.URI("file:///C:/Go/src/bob.go"), + }, + { + path: `\path\to\dir`, + wantFile: `C:\path\to\dir`, + wantURI: span.URI("file:///C:/path/to/dir"), + }, + { + path: `\a\b\c\src\bob.go`, + wantFile: `C:\a\b\c\src\bob.go`, + wantURI: span.URI("file:///C:/a/b/c/src/bob.go"), + }, + { + path: `c:\Go\src\bob george\george\george.go`, + wantFile: `C:\Go\src\bob george\george\george.go`, + wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), + }, + } { + got := span.URIFromPath(test.path) + if got != test.wantURI { + t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI) + } + gotFilename := got.Filename() + if gotFilename != test.wantFile { + t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) + } + } +} + +func TestURIFromURI(t *testing.T) { + for _, test := range []struct { + inputURI, wantFile string + wantURI span.URI + }{ + { + inputURI: `file:///c:/Go/src/bob%20george/george/george.go`, + wantFile: `C:\Go\src\bob george\george\george.go`, + wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), + }, + { + inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`, + wantFile: `C:\Go\src\bob george\george\george.go`, + wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), + }, + { + inputURI: `file:///c:/path/to/%25p%25ercent%25/per%25cent.go`, + wantFile: `C:\path\to\%p%ercent%\per%cent.go`, + wantURI: span.URI(`file:///C:/path/to/%25p%25ercent%25/per%25cent.go`), + }, + { + inputURI: `file:///C%3A/`, + wantFile: `C:\`, + wantURI: span.URI(`file:///C:/`), + }, + { + inputURI: `file:///`, + wantFile: `\`, + wantURI: span.URI(`file:///`), + }, + } { + got := span.URIFromURI(test.inputURI) + if got != test.wantURI { + t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI) + } + gotFilename := got.Filename() + if gotFilename != test.wantFile { + t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) + } + } +} |