From 579903b3add4b11d5d9efc63dbdafb4e26e193c0 Mon Sep 17 00:00:00 2001 From: Rohan Challa Date: Tue, 14 Jan 2020 11:10:54 -0500 Subject: [PATCH] internal/lsp: add mapper for go.mod files This change adds a protocol.ColumnMapper when parsing go.mod files. This will prevent us from having to worry about line and column offsets, specifically when converting from the x/mod/modfile position to a span.Span. Updates golang/go#31999 Change-Id: Iacdfb42d61dfea9b5f70325cf5a87c9575f8f345 Reviewed-on: https://go-review.googlesource.com/c/tools/+/214699 Run-TryBot: Rohan Challa TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/cache/parse_mod.go | 29 ++++++++++++++++++----------- internal/lsp/mod/diagnostics.go | 32 ++++++++++++++++++++++++-------- internal/lsp/source/view.go | 2 +- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/internal/lsp/cache/parse_mod.go b/internal/lsp/cache/parse_mod.go index 07b219f75b..cddf09bee8 100644 --- a/internal/lsp/cache/parse_mod.go +++ b/internal/lsp/cache/parse_mod.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/telemetry" "golang.org/x/tools/internal/memoize" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/log" "golang.org/x/tools/internal/telemetry/trace" errors "golang.org/x/xerrors" @@ -29,13 +30,14 @@ type parseModData struct { memoize.NoCopy modfile *modfile.File + mapper *protocol.ColumnMapper err error } func (c *cache) ParseModHandle(fh source.FileHandle) source.ParseModHandle { h := c.store.Bind(fh.Identity(), func(ctx context.Context) interface{} { data := &parseModData{} - data.modfile, data.err = parseMod(ctx, fh) + data.modfile, data.mapper, data.err = parseMod(ctx, fh) return data }) return &parseModHandle{ @@ -44,29 +46,29 @@ func (c *cache) ParseModHandle(fh source.FileHandle) source.ParseModHandle { } } -func parseMod(ctx context.Context, fh source.FileHandle) (*modfile.File, error) { +func parseMod(ctx context.Context, fh source.FileHandle) (*modfile.File, *protocol.ColumnMapper, error) { ctx, done := trace.StartSpan(ctx, "cache.parseMod", telemetry.File.Of(fh.Identity().URI.Filename())) defer done() buf, _, err := fh.Read(ctx) if err != nil { - return nil, err + return nil, nil, err } - f, err := modfile.Parse(fh.Identity().URI.Filename(), buf, nil) + parsed, err := modfile.Parse(fh.Identity().URI.Filename(), buf, nil) if err != nil { // TODO(golang/go#36486): This can be removed when modfile.Parse returns structured errors. re := regexp.MustCompile(`.*:([\d]+): (.+)`) matches := re.FindStringSubmatch(strings.TrimSpace(err.Error())) if len(matches) < 3 { log.Error(ctx, "could not parse golang/x/mod error message", err) - return nil, err + return nil, nil, err } line, e := strconv.Atoi(matches[1]) if e != nil { - return nil, err + return nil, nil, err } contents := strings.Split(string(buf), "\n")[line-1] - return nil, &source.Error{ + return nil, nil, &source.Error{ Message: matches[2], Range: protocol.Range{ Start: protocol.Position{Line: float64(line - 1), Character: float64(0)}, @@ -74,7 +76,12 @@ func parseMod(ctx context.Context, fh source.FileHandle) (*modfile.File, error) }, } } - return f, nil + m := &protocol.ColumnMapper{ + URI: fh.Identity().URI, + Converter: span.NewContentConverter(fh.Identity().URI.Filename(), buf), + Content: buf, + } + return parsed, m, nil } func (pgh *parseModHandle) String() string { @@ -85,11 +92,11 @@ func (pgh *parseModHandle) File() source.FileHandle { return pgh.file } -func (pgh *parseModHandle) Parse(ctx context.Context) (*modfile.File, error) { +func (pgh *parseModHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) { v := pgh.handle.Get(ctx) if v == nil { - return nil, errors.Errorf("no parsed file for %s", pgh.File().Identity().URI) + return nil, nil, errors.Errorf("no parsed file for %s", pgh.File().Identity().URI) } data := v.(*parseModData) - return data.modfile, data.err + return data.modfile, data.mapper, data.err } diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go index 850411f94f..2ef87f9b08 100644 --- a/internal/lsp/mod/diagnostics.go +++ b/internal/lsp/mod/diagnostics.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/telemetry" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/trace" ) @@ -43,7 +44,7 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.File } } - realMod, err := snapshot.View().Session().Cache().ParseModHandle(realfh).Parse(ctx) + realMod, m, err := snapshot.View().Session().Cache().ParseModHandle(realfh).Parse(ctx) // If the go.mod file fails to parse, return errors right away. if err, ok := err.(*source.Error); ok { return map[source.FileIdentity][]source.Diagnostic{ @@ -58,7 +59,7 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.File if err != nil { return nil, err } - tempMod, err := snapshot.View().Session().Cache().ParseModHandle(tempfh).Parse(ctx) + tempMod, _, err := snapshot.View().Session().Cache().ParseModHandle(tempfh).Parse(ctx) if err != nil { return nil, err } @@ -84,10 +85,25 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.File continue } dep := req.Mod.Path + + start, err := positionToPoint(m, req.Syntax.Start) + if err != nil { + return nil, err + } + end, err := positionToPoint(m, req.Syntax.End) + if err != nil { + return nil, err + } + spn := span.New(realfh.Identity().URI, start, end) + rng, err := m.Range(spn) + if err != nil { + return nil, err + } + diag := &source.Diagnostic{ Message: fmt.Sprintf("%s is not used in this module.", dep), Source: "go mod tidy", - Range: protocol.Range{Start: getPos(req.Syntax.Start), End: getPos(req.Syntax.End)}, + Range: rng, Severity: protocol.SeverityWarning, } if tempReqs[dep] != nil && req.Indirect != tempReqs[dep].Indirect { @@ -101,10 +117,10 @@ func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.File return reports, nil } -// TODO: Check to see if we need to go through internal/span (for multiple byte characters). -func getPos(pos modfile.Position) protocol.Position { - return protocol.Position{ - Line: float64(pos.Line - 1), - Character: float64(pos.LineRune - 1), +func positionToPoint(m *protocol.ColumnMapper, pos modfile.Position) (span.Point, error) { + line, col, err := m.Converter.ToPosition(pos.Byte) + if err != nil { + return span.Point{}, err } + return span.NewPoint(line, col, pos.Byte), nil } diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index 83e659ffef..cb6995b46c 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -247,7 +247,7 @@ type ParseModHandle interface { // Parse returns the parsed modifle for the go.mod file. // If the file is not available, returns nil and an error. - Parse(ctx context.Context) (*modfile.File, error) + Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) } // ParseMode controls the content of the AST produced when parsing a source file.