mirror of
https://github.com/golang/go
synced 2024-11-19 00:44:40 -07:00
de0b176007
This change adds quickfixes for unused dependencies and dependencies that should not be marked as indirect. It also updates the positions for the diagnostics to make more sense relative to the warning message. There is a testing harness now for suggested fixes. Updates golang/go#31999 Change-Id: I0db3382bf892fcc2d9ab3b633020d9167a0ad09b Reviewed-on: https://go-review.googlesource.com/c/tools/+/213917 Run-TryBot: Rohan Challa <rohan@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
241 lines
7.2 KiB
Go
241 lines
7.2 KiB
Go
// 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 mod provides core features related to go.mod file
|
|
// handling for use by Go editors and tools.
|
|
package mod
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/modfile"
|
|
"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"
|
|
)
|
|
|
|
func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.FileIdentity][]source.Diagnostic, error) {
|
|
// TODO: We will want to support diagnostics for go.mod files even when the -modfile flag is turned off.
|
|
realfh, tempfh, err := snapshot.ModFiles(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Check the case when the tempModfile flag is turned off.
|
|
if realfh == nil || tempfh == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
ctx, done := trace.StartSpan(ctx, "modfiles.Diagnostics", telemetry.File.Of(realfh.Identity().URI))
|
|
defer done()
|
|
|
|
// If the view has a temporary go.mod file, we want to run "go mod tidy" to be able to
|
|
// diff between the real and the temp files.
|
|
cfg := snapshot.View().Config(ctx)
|
|
args := append([]string{"mod", "tidy"}, cfg.BuildFlags...)
|
|
if _, err := source.InvokeGo(ctx, snapshot.View().Folder().Filename(), cfg.Env, args...); err != nil {
|
|
// Ignore parse errors here. They'll be handled below.
|
|
if !strings.Contains(err.Error(), "errors parsing go.mod") {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
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{
|
|
realfh.Identity(): []source.Diagnostic{{
|
|
Message: err.Message,
|
|
Source: "syntax",
|
|
Range: err.Range,
|
|
Severity: protocol.SeverityError,
|
|
}},
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tempMod, _, err := snapshot.View().Session().Cache().ParseModHandle(tempfh).Parse(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check indirect vs direct, and removal of dependencies.
|
|
reports := map[source.FileIdentity][]source.Diagnostic{
|
|
realfh.Identity(): []source.Diagnostic{},
|
|
}
|
|
realReqs := make(map[string]*modfile.Require, len(realMod.Require))
|
|
tempReqs := make(map[string]*modfile.Require, len(tempMod.Require))
|
|
for _, req := range realMod.Require {
|
|
realReqs[req.Mod.Path] = req
|
|
}
|
|
for _, req := range tempMod.Require {
|
|
realReq := realReqs[req.Mod.Path]
|
|
if realReq != nil && realReq.Indirect == req.Indirect {
|
|
delete(realReqs, req.Mod.Path)
|
|
}
|
|
tempReqs[req.Mod.Path] = req
|
|
}
|
|
for _, req := range realReqs {
|
|
if req.Syntax == nil {
|
|
continue
|
|
}
|
|
dep := req.Mod.Path
|
|
|
|
rng, err := getRangeFromPositions(m, realfh, req.Syntax.Start, req.Syntax.End)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var diag *source.Diagnostic
|
|
|
|
if tempReqs[dep] != nil && req.Indirect != tempReqs[dep].Indirect {
|
|
// Show diagnostics for dependencies that are incorrectly labeled indirect.
|
|
if req.Indirect {
|
|
var fix []source.SuggestedFix
|
|
// If the dependency should not be indirect, just highlight the // indirect.
|
|
if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
|
|
end := comments.Suffix[0].Start
|
|
end.LineRune += len(comments.Suffix[0].Token)
|
|
end.Byte += len([]byte(comments.Suffix[0].Token))
|
|
|
|
rng, err = getRangeFromPositions(m, realfh, comments.Suffix[0].Start, end)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fix = []source.SuggestedFix{
|
|
{
|
|
Title: "Remove indirect",
|
|
Edits: map[span.URI][]protocol.TextEdit{realfh.Identity().URI: []protocol.TextEdit{
|
|
{
|
|
Range: rng,
|
|
NewText: "",
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
}
|
|
diag = &source.Diagnostic{
|
|
Message: fmt.Sprintf("%s should be a direct dependency.", dep),
|
|
Range: rng,
|
|
SuggestedFixes: fix,
|
|
Source: "go mod tidy",
|
|
Severity: protocol.SeverityWarning,
|
|
}
|
|
} else {
|
|
diag = &source.Diagnostic{
|
|
Message: fmt.Sprintf("%s should be an indirect dependency.", dep),
|
|
Range: rng,
|
|
Source: "go mod tidy",
|
|
Severity: protocol.SeverityWarning,
|
|
}
|
|
}
|
|
}
|
|
// Handle unused dependencies.
|
|
if tempReqs[dep] == nil {
|
|
diag = &source.Diagnostic{
|
|
Message: fmt.Sprintf("%s is not used in this module.", dep),
|
|
Range: rng,
|
|
SuggestedFixes: []source.SuggestedFix{
|
|
{
|
|
Title: fmt.Sprintf("Remove %s.", dep),
|
|
Edits: map[span.URI][]protocol.TextEdit{realfh.Identity().URI: []protocol.TextEdit{
|
|
{
|
|
Range: rng,
|
|
NewText: "",
|
|
},
|
|
}},
|
|
},
|
|
},
|
|
Source: "go mod tidy",
|
|
Severity: protocol.SeverityWarning,
|
|
}
|
|
}
|
|
reports[realfh.Identity()] = append(reports[realfh.Identity()], *diag)
|
|
}
|
|
return reports, nil
|
|
}
|
|
|
|
// TODO: Add caching for go.mod diagnostics to be able to map them back to source.Diagnostics
|
|
// and reuse the cached suggested fixes.
|
|
func SuggestedFixes(fh source.FileHandle, diags []protocol.Diagnostic) []protocol.CodeAction {
|
|
var actions []protocol.CodeAction
|
|
for _, diag := range diags {
|
|
var title string
|
|
if strings.Contains(diag.Message, "is not used in this module") {
|
|
split := strings.Split(diag.Message, " ")
|
|
if len(split) < 1 {
|
|
continue
|
|
}
|
|
title = fmt.Sprintf("Remove dependency: %s", split[0])
|
|
}
|
|
if strings.Contains(diag.Message, "should be a direct dependency.") {
|
|
title = "Remove indirect"
|
|
}
|
|
if title == "" {
|
|
continue
|
|
}
|
|
actions = append(actions, protocol.CodeAction{
|
|
Title: title,
|
|
Kind: protocol.QuickFix,
|
|
Edit: protocol.WorkspaceEdit{
|
|
DocumentChanges: []protocol.TextDocumentEdit{
|
|
{
|
|
TextDocument: protocol.VersionedTextDocumentIdentifier{
|
|
Version: fh.Identity().Version,
|
|
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
|
|
URI: protocol.NewURI(fh.Identity().URI),
|
|
},
|
|
},
|
|
Edits: []protocol.TextEdit{protocol.TextEdit{Range: diag.Range, NewText: ""}},
|
|
},
|
|
},
|
|
},
|
|
Diagnostics: diags,
|
|
})
|
|
}
|
|
return actions
|
|
}
|
|
|
|
func getEndOfLine(req *modfile.Require, m *protocol.ColumnMapper) (span.Point, error) {
|
|
comments := req.Syntax.Comment()
|
|
if comments == nil {
|
|
return positionToPoint(m, req.Syntax.End)
|
|
}
|
|
suffix := comments.Suffix
|
|
if len(suffix) == 0 {
|
|
return positionToPoint(m, req.Syntax.End)
|
|
}
|
|
end := suffix[0].Start
|
|
end.LineRune += len(suffix[0].Token)
|
|
return positionToPoint(m, end)
|
|
}
|
|
|
|
func getRangeFromPositions(m *protocol.ColumnMapper, fh source.FileHandle, s, e modfile.Position) (protocol.Range, error) {
|
|
start, err := positionToPoint(m, s)
|
|
if err != nil {
|
|
return protocol.Range{}, err
|
|
}
|
|
end, err := positionToPoint(m, e)
|
|
if err != nil {
|
|
return protocol.Range{}, err
|
|
}
|
|
spn := span.New(fh.Identity().URI, start, end)
|
|
rng, err := m.Range(spn)
|
|
if err != nil {
|
|
return protocol.Range{}, err
|
|
}
|
|
return rng, nil
|
|
}
|
|
|
|
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
|
|
}
|