mirror of
https://github.com/golang/go
synced 2024-11-05 17:06:13 -07:00
c9619e8fac
FileHandle currently includes LSP-level information about Version and Session. That's dangerous, because the cache operates in terms of URIs and content only -- we explicitly want to share results across sessions and versions if they happen to be the same. Split the LSP information into separate types, VersionedFileHandle and VersionedFileIdentity. Change-Id: I158646b783375b58245468599301e2a29c657e71 Reviewed-on: https://go-review.googlesource.com/c/tools/+/245058 Run-TryBot: Heschi Kreinick <heschi@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org> Reviewed-by: Robert Findley <rfindley@google.com>
555 lines
16 KiB
Go
555 lines
16 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"
|
|
"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/span"
|
|
)
|
|
|
|
type modTidyKey struct {
|
|
sessionID string
|
|
cfg string
|
|
gomod source.FileIdentity
|
|
imports string
|
|
unsavedOverlays string
|
|
view string
|
|
}
|
|
|
|
type modTidyHandle struct {
|
|
handle *memoize.Handle
|
|
}
|
|
|
|
type modTidyData struct {
|
|
memoize.NoCopy
|
|
|
|
tidied *source.TidiedModule
|
|
err error
|
|
}
|
|
|
|
func (mth *modTidyHandle) tidy(ctx context.Context, s source.Snapshot) (*source.TidiedModule, error) {
|
|
v, err := mth.handle.Get(ctx, s.(*snapshot))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data := v.(*modTidyData)
|
|
return data.tidied, data.err
|
|
}
|
|
|
|
func (s *snapshot) ModTidy(ctx context.Context) (*source.TidiedModule, error) {
|
|
if !s.view.tmpMod {
|
|
return nil, source.ErrTmpModfileUnsupported
|
|
}
|
|
if handle := s.getModTidyHandle(); handle != nil {
|
|
return handle.tidy(ctx, s)
|
|
}
|
|
modFH, err := s.GetFile(ctx, s.view.modURI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
workspacePkgs, err := s.WorkspacePackages(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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()
|
|
)
|
|
key := modTidyKey{
|
|
sessionID: s.view.session.id,
|
|
view: s.view.root.Filename(),
|
|
imports: importHash,
|
|
unsavedOverlays: overlayHash,
|
|
gomod: modFH.FileIdentity(),
|
|
cfg: hashConfig(cfg),
|
|
}
|
|
h := s.view.session.cache.store.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
|
|
ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(modURI))
|
|
defer done()
|
|
|
|
snapshot := arg.(*snapshot)
|
|
pm, err := snapshot.ParseMod(ctx, modFH)
|
|
if err != nil {
|
|
return &modTidyData{err: err}
|
|
}
|
|
if len(pm.ParseErrors) > 0 {
|
|
return &modTidyData{
|
|
tidied: &source.TidiedModule{
|
|
Parsed: pm,
|
|
},
|
|
err: fmt.Errorf("could not parse module to tidy: %v", pm.ParseErrors),
|
|
}
|
|
}
|
|
tmpURI, runner, inv, cleanup, err := snapshot.goCommandInvocation(ctx, true, "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 := runner.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(pm.File.Require))
|
|
missingDeps := make(map[string]*modfile.Require, len(ideal.Require))
|
|
for _, req := range pm.File.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(pm.Mapper, 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, snapshot, pm.Mapper, workspacePkgs, ideal.Require, missingDeps, pm.File, options)
|
|
if err != nil {
|
|
return &modTidyData{err: err}
|
|
}
|
|
return &modTidyData{
|
|
tidied: &source.TidiedModule{
|
|
Parsed: pm,
|
|
TidiedContent: tempContents,
|
|
Errors: append(modRequireErrs, missingModuleErrs...),
|
|
},
|
|
}
|
|
})
|
|
|
|
mth := &modTidyHandle{handle: h}
|
|
s.mu.Lock()
|
|
s.modTidyHandle = mth
|
|
s.mu.Unlock()
|
|
|
|
return mth.tidy(ctx, s)
|
|
}
|
|
|
|
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, snapshot *snapshot, 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() {
|
|
found := func(req *modfile.Require) {
|
|
missingPkgs[imp.PkgPath()] = req
|
|
matchedMissingMods[req] = struct{}{}
|
|
}
|
|
if req, ok := missingMods[imp.PkgPath()]; ok {
|
|
found(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 {
|
|
found(req)
|
|
}
|
|
}
|
|
if len(missingPkgs) > 0 {
|
|
errs, err := missingModules(ctx, snapshot, 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, snapshot *snapshot, modMapper *protocol.ColumnMapper, pkg source.Package, missing map[string]*modfile.Require, options source.Options) ([]source.Error, error) {
|
|
var errors []source.Error
|
|
for _, pgf := range pkg.CompiledGoFiles() {
|
|
imports := make(map[string]*ast.ImportSpec)
|
|
for _, imp := range pgf.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(snapshot.view.session.cache.fset, imp.Path.Pos(), imp.Path.End()).Span()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rng, err := pgf.Mapper.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: pgf.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
|
|
}
|