mirror of
https://github.com/golang/go
synced 2024-11-05 17:06:13 -07:00
58eba7e750
Currently, diagnostics for modules that are missing from the go.mod appear in the Go files in which those modules are imported. As a result, the diagnostics were previously calculated as part of the Go file diagnostic calculations. This is convoluted and required passing around an extra map. This CL puts that logic in the ModTidyHandle where it belongs. The diagnostics for the Go files are combined from the multiple sources. Also, added a skipped test for golang/go#39784, since this CL was originally intended to be a fix for that issue... Change-Id: Ic0f9aa235dcd56ea131a2339de9801346f715415 Reviewed-on: https://go-review.googlesource.com/c/tools/+/242579 Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Heschi Kreinick <heschi@google.com>
536 lines
15 KiB
Go
536 lines
15 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
|
|
|
|
// diagnostics are any errors and associated suggested fixes for
|
|
// the go.mod file.
|
|
diagnostics []source.Error
|
|
|
|
err error
|
|
}
|
|
|
|
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 (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 (
|
|
folder = s.View().Folder()
|
|
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: folder.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(pmh.Mod().URI(), 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 files.
|
|
missingModuleErrs, err := missingModuleErrors(ctx, fset, m, workspacePkgs, missingDeps, options)
|
|
if err != nil {
|
|
return &modTidyData{err: err}
|
|
}
|
|
return &modTidyData{
|
|
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(uri span.URI, 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(uri, m, req, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
errors = append(errors, directErr)
|
|
}
|
|
// Handle unused dependencies.
|
|
if missingDeps[dep] == nil {
|
|
rng, err := rangeFromPositions(uri, m, req.Syntax.Start, req.Syntax.End)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
edits, err := dropDependencyEdits(uri, 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: uri,
|
|
SuggestedFixes: []source.SuggestedFix{{
|
|
Title: fmt.Sprintf("Remove dependency: %s", dep),
|
|
Edits: map[span.URI][]protocol.TextEdit{
|
|
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(uri span.URI, m *protocol.ColumnMapper, req *modfile.Require, options source.Options) (source.Error, error) {
|
|
rng, err := rangeFromPositions(uri, 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(uri, m, comments.Suffix[0].Start, end)
|
|
if err != nil {
|
|
return source.Error{}, err
|
|
}
|
|
}
|
|
edits, err := changeDirectnessEdits(uri, 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: uri,
|
|
SuggestedFixes: []source.SuggestedFix{{
|
|
Title: fmt.Sprintf("Make %s direct", req.Mod.Path),
|
|
Edits: map[span.URI][]protocol.TextEdit{
|
|
uri: edits,
|
|
},
|
|
}},
|
|
}, nil
|
|
}
|
|
// If the dependency should be indirect, add the // indirect.
|
|
edits, err := changeDirectnessEdits(uri, 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: uri,
|
|
SuggestedFixes: []source.SuggestedFix{{
|
|
Title: fmt.Sprintf("Make %s indirect", req.Mod.Path),
|
|
Edits: map[span.URI][]protocol.TextEdit{
|
|
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(uri span.URI, 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(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(uri span.URI, 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(uri, string(m.Content), string(newContent))
|
|
edits, err := source.ToProtocolEdits(m, diff)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return edits, nil
|
|
}
|
|
|
|
func rangeFromPositions(uri span.URI, 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(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, missingMods map[string]*modfile.Require, options source.Options) ([]source.Error, error) {
|
|
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
|
|
}
|
|
for dep, req := range missingMods {
|
|
// 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".
|
|
if req != nil && strings.HasPrefix(imp.PkgPath(), dep) {
|
|
missingPkgs[imp.PkgPath()] = req
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(missingPkgs) > 0 {
|
|
errors, err := missingModules(ctx, fset, modMapper, pkg, missingPkgs, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return errors, nil
|
|
}
|
|
}
|
|
return nil, 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
|
|
}
|