github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/internal/lsp/cache/mod_tidy.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cache
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"go/ast"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"golang.org/x/mod/modfile"
    19  	"github.com/powerman/golang-tools/internal/event"
    20  	"github.com/powerman/golang-tools/internal/gocommand"
    21  	"github.com/powerman/golang-tools/internal/lsp/command"
    22  	"github.com/powerman/golang-tools/internal/lsp/debug/tag"
    23  	"github.com/powerman/golang-tools/internal/lsp/diff"
    24  	"github.com/powerman/golang-tools/internal/lsp/protocol"
    25  	"github.com/powerman/golang-tools/internal/lsp/source"
    26  	"github.com/powerman/golang-tools/internal/memoize"
    27  	"github.com/powerman/golang-tools/internal/span"
    28  )
    29  
    30  type modTidyKey struct {
    31  	sessionID       string
    32  	env             string
    33  	gomod           source.FileIdentity
    34  	imports         string
    35  	unsavedOverlays string
    36  	view            string
    37  }
    38  
    39  type modTidyHandle struct {
    40  	handle *memoize.Handle
    41  }
    42  
    43  type modTidyData struct {
    44  	tidied *source.TidiedModule
    45  	err    error
    46  }
    47  
    48  func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) {
    49  	v, err := mth.handle.Get(ctx, snapshot.generation, snapshot)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	data := v.(*modTidyData)
    54  	return data.tidied, data.err
    55  }
    56  
    57  func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
    58  	if pm.File == nil {
    59  		return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI)
    60  	}
    61  	if handle := s.getModTidyHandle(pm.URI); handle != nil {
    62  		return handle.tidy(ctx, s)
    63  	}
    64  	fh, err := s.GetFile(ctx, pm.URI)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	// If the file handle is an overlay, it may not be written to disk.
    69  	// The go.mod file has to be on disk for `go mod tidy` to work.
    70  	if _, ok := fh.(*overlay); ok {
    71  		if info, _ := os.Stat(fh.URI().Filename()); info == nil {
    72  			return nil, source.ErrNoModOnDisk
    73  		}
    74  	}
    75  	if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
    76  		return &source.TidiedModule{
    77  			Diagnostics: criticalErr.DiagList,
    78  		}, nil
    79  	}
    80  	workspacePkgs, err := s.workspacePackageHandles(ctx)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  	importHash, err := s.hashImports(ctx, workspacePkgs)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	s.mu.Lock()
    90  	overlayHash := hashUnsavedOverlays(s.files)
    91  	s.mu.Unlock()
    92  
    93  	key := modTidyKey{
    94  		sessionID:       s.view.session.id,
    95  		view:            s.view.folder.Filename(),
    96  		imports:         importHash,
    97  		unsavedOverlays: overlayHash,
    98  		gomod:           fh.FileIdentity(),
    99  		env:             hashEnv(s),
   100  	}
   101  	h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
   102  		ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
   103  		defer done()
   104  
   105  		snapshot := arg.(*snapshot)
   106  		inv := &gocommand.Invocation{
   107  			Verb:       "mod",
   108  			Args:       []string{"tidy"},
   109  			WorkingDir: filepath.Dir(fh.URI().Filename()),
   110  		}
   111  		tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
   112  		if err != nil {
   113  			return &modTidyData{err: err}
   114  		}
   115  		// Keep the temporary go.mod file around long enough to parse it.
   116  		defer cleanup()
   117  
   118  		if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
   119  			return &modTidyData{err: err}
   120  		}
   121  		// Go directly to disk to get the temporary mod file, since it is
   122  		// always on disk.
   123  		tempContents, err := ioutil.ReadFile(tmpURI.Filename())
   124  		if err != nil {
   125  			return &modTidyData{err: err}
   126  		}
   127  		ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
   128  		if err != nil {
   129  			// We do not need to worry about the temporary file's parse errors
   130  			// since it has been "tidied".
   131  			return &modTidyData{err: err}
   132  		}
   133  		// Compare the original and tidied go.mod files to compute errors and
   134  		// suggested fixes.
   135  		diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs)
   136  		if err != nil {
   137  			return &modTidyData{err: err}
   138  		}
   139  		return &modTidyData{
   140  			tidied: &source.TidiedModule{
   141  				Diagnostics:   diagnostics,
   142  				TidiedContent: tempContents,
   143  			},
   144  		}
   145  	}, nil)
   146  
   147  	mth := &modTidyHandle{handle: h}
   148  	s.mu.Lock()
   149  	s.modTidyHandles[fh.URI()] = mth
   150  	s.mu.Unlock()
   151  
   152  	return mth.tidy(ctx, s)
   153  }
   154  
   155  func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) (string, error) {
   156  	seen := map[string]struct{}{}
   157  	var imports []string
   158  	for _, ph := range wsPackages {
   159  		for _, imp := range ph.imports(ctx, s) {
   160  			if _, ok := seen[imp]; !ok {
   161  				imports = append(imports, imp)
   162  				seen[imp] = struct{}{}
   163  			}
   164  		}
   165  	}
   166  	sort.Strings(imports)
   167  	hashed := strings.Join(imports, ",")
   168  	return hashContents([]byte(hashed)), nil
   169  }
   170  
   171  // modTidyDiagnostics computes the differences between the original and tidied
   172  // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
   173  // may appear on the Go files that import packages from missing modules.
   174  func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []*packageHandle) (diagnostics []*source.Diagnostic, err error) {
   175  	// First, determine which modules are unused and which are missing from the
   176  	// original go.mod file.
   177  	var (
   178  		unused          = make(map[string]*modfile.Require, len(pm.File.Require))
   179  		missing         = make(map[string]*modfile.Require, len(ideal.Require))
   180  		wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
   181  	)
   182  	for _, req := range pm.File.Require {
   183  		unused[req.Mod.Path] = req
   184  	}
   185  	for _, req := range ideal.Require {
   186  		origReq := unused[req.Mod.Path]
   187  		if origReq == nil {
   188  			missing[req.Mod.Path] = req
   189  			continue
   190  		} else if origReq.Indirect != req.Indirect {
   191  			wrongDirectness[req.Mod.Path] = origReq
   192  		}
   193  		delete(unused, req.Mod.Path)
   194  	}
   195  	for _, req := range wrongDirectness {
   196  		// Handle dependencies that are incorrectly labeled indirect and
   197  		// vice versa.
   198  		srcDiag, err := directnessDiagnostic(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
   199  		if err != nil {
   200  			// We're probably in a bad state if we can't compute a
   201  			// directnessDiagnostic, but try to keep going so as to not suppress
   202  			// other, valid diagnostics.
   203  			event.Error(ctx, "computing directness diagnostic", err)
   204  			continue
   205  		}
   206  		diagnostics = append(diagnostics, srcDiag)
   207  	}
   208  	// Next, compute any diagnostics for modules that are missing from the
   209  	// go.mod file. The fixes will be for the go.mod file, but the
   210  	// diagnostics should also appear in both the go.mod file and the import
   211  	// statements in the Go files in which the dependencies are used.
   212  	missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
   213  	for _, req := range missing {
   214  		srcDiag, err := missingModuleDiagnostic(pm, req)
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  		missingModuleFixes[req] = srcDiag.SuggestedFixes
   219  		diagnostics = append(diagnostics, srcDiag)
   220  	}
   221  	// Add diagnostics for missing modules anywhere they are imported in the
   222  	// workspace.
   223  	for _, ph := range workspacePkgs {
   224  		missingImports := map[string]*modfile.Require{}
   225  
   226  		// If -mod=readonly is not set we may have successfully imported
   227  		// packages from missing modules. Otherwise they'll be in
   228  		// MissingDependencies. Combine both.
   229  		importedPkgs := ph.imports(ctx, snapshot)
   230  
   231  		for _, imp := range importedPkgs {
   232  			if req, ok := missing[imp]; ok {
   233  				missingImports[imp] = req
   234  				break
   235  			}
   236  			// If the import is a package of the dependency, then add the
   237  			// package to the map, this will eliminate the need to do this
   238  			// prefix package search on each import for each file.
   239  			// Example:
   240  			//
   241  			// import (
   242  			//   "github.com/powerman/golang-tools/go/expect"
   243  			//   "github.com/powerman/golang-tools/go/packages"
   244  			// )
   245  			// They both are related to the same module: "github.com/powerman/golang-tools".
   246  			var match string
   247  			for _, req := range ideal.Require {
   248  				if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
   249  					match = req.Mod.Path
   250  				}
   251  			}
   252  			if req, ok := missing[match]; ok {
   253  				missingImports[imp] = req
   254  			}
   255  		}
   256  		// None of this package's imports are from missing modules.
   257  		if len(missingImports) == 0 {
   258  			continue
   259  		}
   260  		for _, pgh := range ph.compiledGoFiles {
   261  			pgf, err := snapshot.ParseGo(ctx, pgh.file, source.ParseHeader)
   262  			if err != nil {
   263  				continue
   264  			}
   265  			file, m := pgf.File, pgf.Mapper
   266  			if file == nil || m == nil {
   267  				continue
   268  			}
   269  			imports := make(map[string]*ast.ImportSpec)
   270  			for _, imp := range file.Imports {
   271  				if imp.Path == nil {
   272  					continue
   273  				}
   274  				if target, err := strconv.Unquote(imp.Path.Value); err == nil {
   275  					imports[target] = imp
   276  				}
   277  			}
   278  			if len(imports) == 0 {
   279  				continue
   280  			}
   281  			for importPath, req := range missingImports {
   282  				imp, ok := imports[importPath]
   283  				if !ok {
   284  					continue
   285  				}
   286  				fixes, ok := missingModuleFixes[req]
   287  				if !ok {
   288  					return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
   289  				}
   290  				srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes)
   291  				if err != nil {
   292  					return nil, err
   293  				}
   294  				diagnostics = append(diagnostics, srcErr)
   295  			}
   296  		}
   297  	}
   298  	// Finally, add errors for any unused dependencies.
   299  	onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
   300  	for _, req := range unused {
   301  		srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  		diagnostics = append(diagnostics, srcErr)
   306  	}
   307  	return diagnostics, nil
   308  }
   309  
   310  // unusedDiagnostic returns a source.Diagnostic for an unused require.
   311  func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) {
   312  	rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path)
   317  	cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{
   318  		URI:            protocol.URIFromSpanURI(m.URI),
   319  		OnlyDiagnostic: onlyDiagnostic,
   320  		ModulePath:     req.Mod.Path,
   321  	})
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  	return &source.Diagnostic{
   326  		URI:            m.URI,
   327  		Range:          rng,
   328  		Severity:       protocol.SeverityWarning,
   329  		Source:         source.ModTidyError,
   330  		Message:        fmt.Sprintf("%s is not used in this module", req.Mod.Path),
   331  		SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   332  	}, nil
   333  }
   334  
   335  // directnessDiagnostic extracts errors when a dependency is labeled indirect when
   336  // it should be direct and vice versa.
   337  func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Diagnostic, error) {
   338  	rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  	direction := "indirect"
   343  	if req.Indirect {
   344  		direction = "direct"
   345  
   346  		// If the dependency should be direct, just highlight the // indirect.
   347  		if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
   348  			end := comments.Suffix[0].Start
   349  			end.LineRune += len(comments.Suffix[0].Token)
   350  			end.Byte += len([]byte(comments.Suffix[0].Token))
   351  			rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
   352  			if err != nil {
   353  				return nil, err
   354  			}
   355  		}
   356  	}
   357  	// If the dependency should be indirect, add the // indirect.
   358  	edits, err := switchDirectness(req, m, computeEdits)
   359  	if err != nil {
   360  		return nil, err
   361  	}
   362  	return &source.Diagnostic{
   363  		URI:      m.URI,
   364  		Range:    rng,
   365  		Severity: protocol.SeverityWarning,
   366  		Source:   source.ModTidyError,
   367  		Message:  fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
   368  		SuggestedFixes: []source.SuggestedFix{{
   369  			Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
   370  			Edits: map[span.URI][]protocol.TextEdit{
   371  				m.URI: edits,
   372  			},
   373  			ActionKind: protocol.QuickFix,
   374  		}},
   375  	}, nil
   376  }
   377  
   378  func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*source.Diagnostic, error) {
   379  	var rng protocol.Range
   380  	// Default to the start of the file if there is no module declaration.
   381  	if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
   382  		start, end := pm.File.Module.Syntax.Span()
   383  		var err error
   384  		rng, err = rangeFromPositions(pm.Mapper, start, end)
   385  		if err != nil {
   386  			return nil, err
   387  		}
   388  	}
   389  	title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path)
   390  	cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
   391  		URI:        protocol.URIFromSpanURI(pm.Mapper.URI),
   392  		AddRequire: !req.Indirect,
   393  		GoCmdArgs:  []string{req.Mod.Path + "@" + req.Mod.Version},
   394  	})
   395  	if err != nil {
   396  		return nil, err
   397  	}
   398  	return &source.Diagnostic{
   399  		URI:            pm.Mapper.URI,
   400  		Range:          rng,
   401  		Severity:       protocol.SeverityError,
   402  		Source:         source.ModTidyError,
   403  		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   404  		SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   405  	}, nil
   406  }
   407  
   408  // switchDirectness gets the edits needed to change an indirect dependency to
   409  // direct and vice versa.
   410  func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
   411  	// We need a private copy of the parsed go.mod file, since we're going to
   412  	// modify it.
   413  	copied, err := modfile.Parse("", m.Content, nil)
   414  	if err != nil {
   415  		return nil, err
   416  	}
   417  	// Change the directness in the matching require statement. To avoid
   418  	// reordering the require statements, rewrite all of them.
   419  	var requires []*modfile.Require
   420  	seenVersions := make(map[string]string)
   421  	for _, r := range copied.Require {
   422  		if seen := seenVersions[r.Mod.Path]; seen != "" && seen != r.Mod.Version {
   423  			// Avoid a panic in SetRequire below, which panics on conflicting
   424  			// versions.
   425  			return nil, fmt.Errorf("%q has conflicting versions: %q and %q", r.Mod.Path, seen, r.Mod.Version)
   426  		}
   427  		seenVersions[r.Mod.Path] = r.Mod.Version
   428  		if r.Mod.Path == req.Mod.Path {
   429  			requires = append(requires, &modfile.Require{
   430  				Mod:      r.Mod,
   431  				Syntax:   r.Syntax,
   432  				Indirect: !r.Indirect,
   433  			})
   434  			continue
   435  		}
   436  		requires = append(requires, r)
   437  	}
   438  	copied.SetRequire(requires)
   439  	newContent, err := copied.Format()
   440  	if err != nil {
   441  		return nil, err
   442  	}
   443  	// Calculate the edits to be made due to the change.
   444  	diff, err := computeEdits(m.URI, string(m.Content), string(newContent))
   445  	if err != nil {
   446  		return nil, err
   447  	}
   448  	return source.ToProtocolEdits(m, diff)
   449  }
   450  
   451  // missingModuleForImport creates an error for a given import path that comes
   452  // from a missing module.
   453  func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) {
   454  	if req.Syntax == nil {
   455  		return nil, fmt.Errorf("no syntax for %v", req)
   456  	}
   457  	spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
   458  	if err != nil {
   459  		return nil, err
   460  	}
   461  	rng, err := m.Range(spn)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  	return &source.Diagnostic{
   466  		URI:            m.URI,
   467  		Range:          rng,
   468  		Severity:       protocol.SeverityError,
   469  		Source:         source.ModTidyError,
   470  		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   471  		SuggestedFixes: fixes,
   472  	}, nil
   473  }
   474  
   475  func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
   476  	spn, err := spanFromPositions(m, s, e)
   477  	if err != nil {
   478  		return protocol.Range{}, err
   479  	}
   480  	return m.Range(spn)
   481  }
   482  
   483  func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) {
   484  	toPoint := func(offset int) (span.Point, error) {
   485  		l, c, err := m.Converter.ToPosition(offset)
   486  		if err != nil {
   487  			return span.Point{}, err
   488  		}
   489  		return span.NewPoint(l, c, offset), nil
   490  	}
   491  	start, err := toPoint(s.Byte)
   492  	if err != nil {
   493  		return span.Span{}, err
   494  	}
   495  	end, err := toPoint(e.Byte)
   496  	if err != nil {
   497  		return span.Span{}, err
   498  	}
   499  	return span.New(m.URI, start, end), nil
   500  }