1
0
mirror of https://github.com/golang/go synced 2024-11-05 20:26:13 -07:00
go/internal/lsp/cache/mod_tidy.go
Heschi Kreinick 72051f7961 internal/lsp: pass snapshot/view to memoize.Functions
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>
2020-07-28 17:34:46 +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"
"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
}