1
0
mirror of https://github.com/golang/go synced 2024-10-01 01:08:33 -06:00
go/internal/lsp/cache/mod_tidy.go
Rebecca Stambler cbb3c69a37 internal/lsp: add errors for missing modules that don't map to an import
Some modules may need to be added to the go.mod without being associated
with an import statement. In such cases, we show the missing module
diagnostic on the whole go.mod file. This isn't ideal, but mapping to
the full require statement isn't that simple, and this is an easy enough
starting point. The code in mod_tidy.go is becoming more unwieldy - I
think I will clean it up in a follow-up.

Fixes golang/go#39784

Change-Id: Ib32ec1fd74c455ce42ba778ea6cba0a475cf245a
Reviewed-on: https://go-review.googlesource.com/c/tools/+/243218
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
2020-07-24 00:18:24 +00:00

586 lines
17 KiB
Go

// 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.
package cache
import (
"context"
"fmt"
"go/ast"
"go/token"
"io/ioutil"
"sort"
"strconv"
"strings"
"golang.org/x/mod/modfile"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
)
type modTidyKey struct {
sessionID string
cfg string
gomod string
imports string
unsavedOverlays string
view string
}
type modTidyHandle struct {
handle *memoize.Handle
pmh source.ParseModHandle
}
type modTidyData struct {
memoize.NoCopy
// tidiedContent is the content of the tidied file.
tidiedContent []byte
// diagnostics are any errors and associated suggested fixes for
// the go.mod file.
diagnostics []source.Error
err error
}
func (mth *modTidyHandle) ParseModHandle() source.ParseModHandle {
return mth.pmh
}
func (mth *modTidyHandle) Tidy(ctx context.Context) ([]source.Error, error) {
v, err := mth.handle.Get(ctx)
if err != nil {
return nil, err
}
data := v.(*modTidyData)
return data.diagnostics, data.err
}
func (mth *modTidyHandle) TidiedContent(ctx context.Context) ([]byte, error) {
v, err := mth.handle.Get(ctx)
if err != nil {
return nil, err
}
data := v.(*modTidyData)
return data.tidiedContent, data.err
}
func (s *snapshot) ModTidyHandle(ctx context.Context) (source.ModTidyHandle, error) {
if !s.view.tmpMod {
return nil, source.ErrTmpModfileUnsupported
}
if handle := s.getModTidyHandle(); handle != nil {
return handle, nil
}
fh, err := s.GetFile(ctx, s.view.modURI)
if err != nil {
return nil, err
}
pmh, err := s.ParseModHandle(ctx, fh)
if err != nil {
return nil, err
}
wsPhs, err := s.WorkspacePackages(ctx)
if ctx.Err() != nil {
return nil, ctx.Err()
}
if err != nil {
return nil, err
}
var workspacePkgs []source.Package
for _, ph := range wsPhs {
pkg, err := ph.Check(ctx)
if err != nil {
return nil, err
}
workspacePkgs = append(workspacePkgs, pkg)
}
importHash, err := hashImports(ctx, workspacePkgs)
if err != nil {
return nil, err
}
s.mu.Lock()
overlayHash := hashUnsavedOverlays(s.files)
s.mu.Unlock()
var (
modURI = s.view.modURI
cfg = s.config(ctx)
options = s.view.Options()
fset = s.view.session.cache.fset
)
key := modTidyKey{
sessionID: s.view.session.id,
view: s.view.root.Filename(),
imports: importHash,
unsavedOverlays: overlayHash,
gomod: pmh.Mod().Identity().String(),
cfg: hashConfig(cfg),
}
h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(modURI))
defer done()
original, m, parseErrors, err := pmh.Parse(ctx)
if err != nil || len(parseErrors) > 0 {
return &modTidyData{
diagnostics: parseErrors,
err: err,
}
}
tmpURI, inv, cleanup, err := goCommandInvocation(ctx, cfg, pmh, "mod", []string{"tidy"})
if err != nil {
return &modTidyData{err: err}
}
// Keep the temporary go.mod file around long enough to parse it.
defer cleanup()
if _, err := packagesinternal.GetGoCmdRunner(cfg).Run(ctx, *inv); err != nil {
return &modTidyData{err: err}
}
// Go directly to disk to get the temporary mod file, since it is
// always on disk.
tempContents, err := ioutil.ReadFile(tmpURI.Filename())
if err != nil {
return &modTidyData{err: err}
}
ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
if err != nil {
// We do not need to worry about the temporary file's parse errors
// since it has been "tidied".
return &modTidyData{err: err}
}
// Get the dependencies that are different between the original and
// ideal go.mod files.
unusedDeps := make(map[string]*modfile.Require, len(original.Require))
missingDeps := make(map[string]*modfile.Require, len(ideal.Require))
for _, req := range original.Require {
unusedDeps[req.Mod.Path] = req
}
for _, req := range ideal.Require {
origDep := unusedDeps[req.Mod.Path]
if origDep != nil && origDep.Indirect == req.Indirect {
delete(unusedDeps, req.Mod.Path)
} else {
missingDeps[req.Mod.Path] = req
}
}
// First, compute any errors specific to the go.mod file. These include
// unused dependencies and modules with incorrect // indirect comments.
/// Both the diagnostic and the fix will appear on the go.mod file.
modRequireErrs, err := modRequireErrors(m, missingDeps, unusedDeps, options)
if err != nil {
return &modTidyData{err: err}
}
for _, req := range missingDeps {
if unusedDeps[req.Mod.Path] != nil {
delete(missingDeps, req.Mod.Path)
}
}
// Next, compute any diagnostics for modules that are missing from the
// go.mod file. The fixes will be for the go.mod file, but the
// diagnostics should appear on the import statements in the Go or
// go.mod files.
missingModuleErrs, err := missingModuleErrors(ctx, fset, m, workspacePkgs, ideal.Require, missingDeps, original, options)
if err != nil {
return &modTidyData{err: err}
}
return &modTidyData{
tidiedContent: tempContents,
diagnostics: append(modRequireErrs, missingModuleErrs...),
}
})
s.mu.Lock()
defer s.mu.Unlock()
s.modTidyHandle = &modTidyHandle{
handle: h,
pmh: pmh,
}
return s.modTidyHandle, nil
}
func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
results := make(map[string]bool)
var imports []string
for _, pkg := range wsPackages {
for _, path := range pkg.Imports() {
imp := path.PkgPath()
if _, ok := results[imp]; !ok {
results[imp] = true
imports = append(imports, imp)
}
}
}
sort.Strings(imports)
hashed := strings.Join(imports, ",")
return hashContents([]byte(hashed)), nil
}
// modRequireErrors extracts the errors that occur on the require directives.
// It checks for directness issues and unused dependencies.
func modRequireErrors(m *protocol.ColumnMapper, missingDeps, unusedDeps map[string]*modfile.Require, options source.Options) ([]source.Error, error) {
var errors []source.Error
for dep, req := range unusedDeps {
if req.Syntax == nil {
continue
}
// Handle dependencies that are incorrectly labeled indirect and vice versa.
if missingDeps[dep] != nil && req.Indirect != missingDeps[dep].Indirect {
directErr, err := modDirectnessErrors(m, req, options)
if err != nil {
return nil, err
}
errors = append(errors, directErr)
}
// Handle unused dependencies.
if missingDeps[dep] == nil {
rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
if err != nil {
return nil, err
}
edits, err := dropDependencyEdits(m, req, options)
if err != nil {
return nil, err
}
errors = append(errors, source.Error{
Category: ModTidyError,
Message: fmt.Sprintf("%s is not used in this module.", dep),
Range: rng,
URI: m.URI,
SuggestedFixes: []source.SuggestedFix{{
Title: fmt.Sprintf("Remove dependency: %s", dep),
Edits: map[span.URI][]protocol.TextEdit{
m.URI: edits,
},
}},
})
}
}
return errors, nil
}
const ModTidyError = "go mod tidy"
// modDirectnessErrors extracts errors when a dependency is labeled indirect when it should be direct and vice versa.
func modDirectnessErrors(m *protocol.ColumnMapper, req *modfile.Require, options source.Options) (source.Error, error) {
rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
if err != nil {
return source.Error{}, err
}
if req.Indirect {
// If the dependency should be direct, 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 = rangeFromPositions(m, comments.Suffix[0].Start, end)
if err != nil {
return source.Error{}, err
}
}
edits, err := changeDirectnessEdits(m, req, false, options)
if err != nil {
return source.Error{}, err
}
return source.Error{
Category: ModTidyError,
Message: fmt.Sprintf("%s should be a direct dependency", req.Mod.Path),
Range: rng,
URI: m.URI,
SuggestedFixes: []source.SuggestedFix{{
Title: fmt.Sprintf("Make %s direct", req.Mod.Path),
Edits: map[span.URI][]protocol.TextEdit{
m.URI: edits,
},
}},
}, nil
}
// If the dependency should be indirect, add the // indirect.
edits, err := changeDirectnessEdits(m, req, true, options)
if err != nil {
return source.Error{}, err
}
return source.Error{
Category: ModTidyError,
Message: fmt.Sprintf("%s should be an indirect dependency", req.Mod.Path),
Range: rng,
URI: m.URI,
SuggestedFixes: []source.SuggestedFix{{
Title: fmt.Sprintf("Make %s indirect", req.Mod.Path),
Edits: map[span.URI][]protocol.TextEdit{
m.URI: edits,
},
}},
}, nil
}
// dropDependencyEdits gets the edits needed to remove the dependency from the go.mod file.
// As an example, this function will codify the edits needed to convert the before go.mod file to the after.
// Before:
// module t
//
// go 1.11
//
// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee
// After:
// module t
//
// go 1.11
func dropDependencyEdits(m *protocol.ColumnMapper, req *modfile.Require, options source.Options) ([]protocol.TextEdit, error) {
// We need a private copy of the parsed go.mod file, since we're going to
// modify it.
copied, err := modfile.Parse("", m.Content, nil)
if err != nil {
return nil, err
}
if err := copied.DropRequire(req.Mod.Path); err != nil {
return nil, err
}
copied.Cleanup()
newContent, err := copied.Format()
if err != nil {
return nil, err
}
// Calculate the edits to be made due to the change.
diff := options.ComputeEdits(m.URI, string(m.Content), string(newContent))
edits, err := source.ToProtocolEdits(m, diff)
if err != nil {
return nil, err
}
return edits, nil
}
// changeDirectnessEdits gets the edits needed to change an indirect dependency to direct and vice versa.
// As an example, this function will codify the edits needed to convert the before go.mod file to the after.
// Before:
// module t
//
// go 1.11
//
// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee
// After:
// module t
//
// go 1.11
//
// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee // indirect
func changeDirectnessEdits(m *protocol.ColumnMapper, req *modfile.Require, indirect bool, options source.Options) ([]protocol.TextEdit, error) {
// We need a private copy of the parsed go.mod file, since we're going to
// modify it.
copied, err := modfile.Parse("", m.Content, nil)
if err != nil {
return nil, err
}
// Change the directness in the matching require statement. To avoid
// reordering the require statements, rewrite all of them.
var requires []*modfile.Require
for _, r := range copied.Require {
if r.Mod.Path == req.Mod.Path {
requires = append(requires, &modfile.Require{
Mod: r.Mod,
Syntax: r.Syntax,
Indirect: indirect,
})
continue
}
requires = append(requires, r)
}
copied.SetRequire(requires)
newContent, err := copied.Format()
if err != nil {
return nil, err
}
// Calculate the edits to be made due to the change.
diff := options.ComputeEdits(m.URI, string(m.Content), string(newContent))
edits, err := source.ToProtocolEdits(m, diff)
if err != nil {
return nil, err
}
return edits, nil
}
func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
toPoint := func(offset int) (span.Point, error) {
l, c, err := m.Converter.ToPosition(offset)
if err != nil {
return span.Point{}, err
}
return span.NewPoint(l, c, offset), nil
}
start, err := toPoint(s.Byte)
if err != nil {
return protocol.Range{}, err
}
end, err := toPoint(e.Byte)
if err != nil {
return protocol.Range{}, err
}
return m.Range(span.New(m.URI, start, end))
}
// missingModuleErrors returns diagnostics for each file in each workspace
// package that has dependencies that are not reflected in the go.mod file.
func missingModuleErrors(ctx context.Context, fset *token.FileSet, modMapper *protocol.ColumnMapper, pkgs []source.Package, modules []*modfile.Require, missingMods map[string]*modfile.Require, original *modfile.File, options source.Options) ([]source.Error, error) {
var moduleErrs []source.Error
matchedMissingMods := make(map[*modfile.Require]struct{})
for _, pkg := range pkgs {
missingPkgs := map[string]*modfile.Require{}
for _, imp := range pkg.Imports() {
if req, ok := missingMods[imp.PkgPath()]; ok {
missingPkgs[imp.PkgPath()] = req
break
}
// If the import is a package of the dependency, then add the
// package to the map, this will eliminate the need to do this
// prefix package search on each import for each file.
// Example:
//
// import (
// "golang.org/x/tools/go/expect"
// "golang.org/x/tools/go/packages"
// )
// They both are related to the same module: "golang.org/x/tools".
var match string
for _, mod := range modules {
if strings.HasPrefix(imp.PkgPath(), mod.Mod.Path) && len(mod.Mod.Path) > len(match) {
match = mod.Mod.Path
}
}
if req, ok := missingMods[match]; ok {
missingPkgs[imp.PkgPath()] = req
matchedMissingMods[req] = struct{}{}
}
}
if len(missingPkgs) > 0 {
errs, err := missingModules(ctx, fset, modMapper, pkg, missingPkgs, options)
if err != nil {
return nil, err
}
moduleErrs = append(moduleErrs, errs...)
}
}
for _, req := range missingMods {
if _, ok := matchedMissingMods[req]; ok {
continue
}
s, e := original.Module.Syntax.Span()
rng, err := rangeFromPositions(modMapper, s, e)
if err != nil {
return nil, err
}
edits, err := addRequireFix(modMapper, req, options)
if err != nil {
return nil, err
}
moduleErrs = append(moduleErrs, source.Error{
URI: modMapper.URI,
Range: rng,
Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
Category: "go mod tidy",
Kind: source.ModTidyError,
SuggestedFixes: []source.SuggestedFix{
{
Title: "Add %s to your go.mod file",
Edits: edits,
},
},
})
}
return moduleErrs, nil
}
func missingModules(ctx context.Context, fset *token.FileSet, modMapper *protocol.ColumnMapper, pkg source.Package, missing map[string]*modfile.Require, options source.Options) ([]source.Error, error) {
var errors []source.Error
for _, pgh := range pkg.CompiledGoFiles() {
file, _, m, _, err := pgh.Parse(ctx)
if err != nil {
return nil, err
}
imports := make(map[string]*ast.ImportSpec)
for _, imp := range file.Imports {
if imp.Path == nil {
continue
}
if target, err := strconv.Unquote(imp.Path.Value); err == nil {
imports[target] = imp
}
}
if len(imports) == 0 {
continue
}
for mod, req := range missing {
if req.Syntax == nil {
continue
}
imp, ok := imports[mod]
if !ok {
continue
}
spn, err := span.NewRange(fset, imp.Path.Pos(), imp.Path.End()).Span()
if err != nil {
return nil, err
}
rng, err := m.Range(spn)
if err != nil {
return nil, err
}
edits, err := addRequireFix(modMapper, req, options)
if err != nil {
return nil, err
}
errors = append(errors, source.Error{
URI: pgh.File().URI(),
Range: rng,
Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
Category: "go mod tidy",
Kind: source.ModTidyError,
SuggestedFixes: []source.SuggestedFix{
{
Title: "Add %s to your go.mod file",
Edits: edits,
},
},
})
}
}
return errors, nil
}
// addRequireFix creates edits for adding a given require to a go.mod file.
func addRequireFix(m *protocol.ColumnMapper, req *modfile.Require, options source.Options) (map[span.URI][]protocol.TextEdit, error) {
// We need a private copy of the parsed go.mod file, since we're going to
// modify it.
copied, err := modfile.Parse("", m.Content, nil)
if err != nil {
return nil, err
}
// Calculate the quick fix edits that need to be made to the go.mod file.
if err := copied.AddRequire(req.Mod.Path, req.Mod.Version); err != nil {
return nil, err
}
copied.SortBlocks()
newContents, err := copied.Format()
if err != nil {
return nil, err
}
// Calculate the edits to be made due to the change.
diff := options.ComputeEdits(m.URI, string(m.Content), string(newContents))
edits, err := source.ToProtocolEdits(m, diff)
if err != nil {
return nil, err
}
return map[span.URI][]protocol.TextEdit{
m.URI: edits,
}, nil
}