mirror of
https://github.com/golang/go
synced 2024-11-05 20:26:13 -07:00
72051f7961
Due to the runtime's inability to collect cycles involving finalizers, we can't close over handles in memoize.Functions without causing memory leaks. Up until now we've dealt with that by closing over all the bits of the snapshot that we want, but it distorts the design of all the code used in the Functions. We can solve the problem another way: instead of closing over the snapshot/view, we can force the caller to pass it in. This is somewhat scary: there is no requirement that the argument matches the data that we're working with. But the reality is that this is not a new problem: the Function used to calculate a cache value is not necessarily the one that the caller expects. As long as the cache key fully identifies all the inputs to the Function, the output should be correct. And since the caller used the snapshot/view to calculate that cache key, it should always be safe to pass in that snapshot/view. If it's not, then we already had a bug. The Arg type in memoize is clumsy, but I thought it would be nice to have at least a little bit of type safety. I'm open to suggestions. Change-Id: I23f546638b0c66a4698620a986949087211f4762 Reviewed-on: https://go-review.googlesource.com/c/tools/+/244019 Reviewed-by: Robert Findley <rfindley@google.com> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
586 lines
17 KiB
Go
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"
|
|
"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, s source.Snapshot) ([]source.Error, error) {
|
|
v, err := mth.handle.Get(ctx, s.(*snapshot))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data := v.(*modTidyData)
|
|
return data.diagnostics, data.err
|
|
}
|
|
|
|
func (mth *modTidyHandle) TidiedContent(ctx context.Context, s source.Snapshot) ([]byte, error) {
|
|
v, err := mth.handle.Get(ctx, s.(*snapshot))
|
|
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, s)
|
|
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()
|
|
)
|
|
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, arg memoize.Arg) interface{} {
|
|
ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(modURI))
|
|
defer done()
|
|
|
|
snapshot := arg.(*snapshot)
|
|
|
|
original, m, parseErrors, err := pmh.Parse(ctx, snapshot)
|
|
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, snapshot, 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, 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() {
|
|
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, 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 _, pgh := range pkg.CompiledGoFiles() {
|
|
file, _, m, _, err := pgh.Parse(ctx, snapshot.view)
|
|
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(snapshot.view.session.cache.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
|
|
}
|