github.com/jd-ly/tools@v0.5.7/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/jd-ly/tools/internal/event"
    20  	"github.com/jd-ly/tools/internal/gocommand"
    21  	"github.com/jd-ly/tools/internal/lsp/debug/tag"
    22  	"github.com/jd-ly/tools/internal/lsp/diff"
    23  	"github.com/jd-ly/tools/internal/lsp/protocol"
    24  	"github.com/jd-ly/tools/internal/lsp/source"
    25  	"github.com/jd-ly/tools/internal/memoize"
    26  	"github.com/jd-ly/tools/internal/span"
    27  )
    28  
    29  type modTidyKey struct {
    30  	sessionID       string
    31  	env             string
    32  	gomod           source.FileIdentity
    33  	imports         string
    34  	unsavedOverlays string
    35  	view            string
    36  }
    37  
    38  type modTidyHandle struct {
    39  	handle *memoize.Handle
    40  }
    41  
    42  type modTidyData struct {
    43  	tidied *source.TidiedModule
    44  	err    error
    45  }
    46  
    47  func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) {
    48  	v, err := mth.handle.Get(ctx, snapshot.generation, snapshot)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	data := v.(*modTidyData)
    53  	return data.tidied, data.err
    54  }
    55  
    56  func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
    57  	if pm.File == nil {
    58  		return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI)
    59  	}
    60  	if handle := s.getModTidyHandle(pm.URI); handle != nil {
    61  		return handle.tidy(ctx, s)
    62  	}
    63  	fh, err := s.GetFile(ctx, pm.URI)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	// If the file handle is an overlay, it may not be written to disk.
    68  	// The go.mod file has to be on disk for `go mod tidy` to work.
    69  	if _, ok := fh.(*overlay); ok {
    70  		if info, _ := os.Stat(fh.URI().Filename()); info == nil {
    71  			return nil, source.ErrNoModOnDisk
    72  		}
    73  	}
    74  	workspacePkgs, err := s.WorkspacePackages(ctx)
    75  	if err != nil {
    76  		if tm, ok := s.parseModErrors(ctx, fh, err); ok {
    77  			return tm, nil
    78  		}
    79  		return nil, err
    80  	}
    81  	importHash, err := hashImports(ctx, workspacePkgs)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	s.mu.Lock()
    87  	overlayHash := hashUnsavedOverlays(s.files)
    88  	s.mu.Unlock()
    89  
    90  	key := modTidyKey{
    91  		sessionID:       s.view.session.id,
    92  		view:            s.view.folder.Filename(),
    93  		imports:         importHash,
    94  		unsavedOverlays: overlayHash,
    95  		gomod:           fh.FileIdentity(),
    96  		env:             hashEnv(s),
    97  	}
    98  	h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
    99  		ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
   100  		defer done()
   101  
   102  		snapshot := arg.(*snapshot)
   103  		inv := &gocommand.Invocation{
   104  			Verb:       "mod",
   105  			Args:       []string{"tidy"},
   106  			WorkingDir: filepath.Dir(fh.URI().Filename()),
   107  		}
   108  		tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile|source.AllowNetwork, inv)
   109  		if err != nil {
   110  			return &modTidyData{err: err}
   111  		}
   112  		// Keep the temporary go.mod file around long enough to parse it.
   113  		defer cleanup()
   114  
   115  		if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
   116  			return &modTidyData{err: err}
   117  		}
   118  		// Go directly to disk to get the temporary mod file, since it is
   119  		// always on disk.
   120  		tempContents, err := ioutil.ReadFile(tmpURI.Filename())
   121  		if err != nil {
   122  			return &modTidyData{err: err}
   123  		}
   124  		ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
   125  		if err != nil {
   126  			// We do not need to worry about the temporary file's parse errors
   127  			// since it has been "tidied".
   128  			return &modTidyData{err: err}
   129  		}
   130  		// Compare the original and tidied go.mod files to compute errors and
   131  		// suggested fixes.
   132  		errors, err := modTidyErrors(ctx, snapshot, pm, ideal, workspacePkgs)
   133  		if err != nil {
   134  			return &modTidyData{err: err}
   135  		}
   136  		return &modTidyData{
   137  			tidied: &source.TidiedModule{
   138  				Errors:        errors,
   139  				TidiedContent: tempContents,
   140  			},
   141  		}
   142  	}, nil)
   143  
   144  	mth := &modTidyHandle{handle: h}
   145  	s.mu.Lock()
   146  	s.modTidyHandles[fh.URI()] = mth
   147  	s.mu.Unlock()
   148  
   149  	return mth.tidy(ctx, s)
   150  }
   151  
   152  func (s *snapshot) parseModErrors(ctx context.Context, fh source.FileHandle, goCommandErr error) (*source.TidiedModule, bool) {
   153  	if goCommandErr == nil {
   154  		return nil, false
   155  	}
   156  
   157  	// Match on common error messages. This is really hacky, but I'm not sure
   158  	// of any better way. This can be removed when golang/go#39164 is resolved.
   159  	errText := goCommandErr.Error()
   160  	isInconsistentVendor := strings.Contains(errText, "inconsistent vendoring")
   161  	isGoSumUpdates := strings.Contains(errText, "updates to go.sum needed") || strings.Contains(errText, "missing go.sum entry")
   162  
   163  	if !isInconsistentVendor && !isGoSumUpdates {
   164  		return nil, false
   165  	}
   166  
   167  	pmf, err := s.ParseMod(ctx, fh)
   168  	if err != nil {
   169  		return nil, false
   170  	}
   171  	if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
   172  		return nil, false
   173  	}
   174  	rng, err := rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
   175  	if err != nil {
   176  		return nil, false
   177  	}
   178  	args, err := source.MarshalArgs(protocol.URIFromSpanURI(fh.URI()))
   179  	if err != nil {
   180  		return nil, false
   181  	}
   182  
   183  	switch {
   184  	case isInconsistentVendor:
   185  		return &source.TidiedModule{
   186  			Errors: []*source.Error{{
   187  				URI:   fh.URI(),
   188  				Range: rng,
   189  				Kind:  source.ListError,
   190  				Message: `Inconsistent vendoring detected. Please re-run "go mod vendor".
   191  See https://github.com/golang/go/issues/39164 for more detail on this issue.`,
   192  				SuggestedFixes: []source.SuggestedFix{{
   193  					Title: source.CommandVendor.Title,
   194  					Command: &protocol.Command{
   195  						Command:   source.CommandVendor.ID(),
   196  						Title:     source.CommandVendor.Title,
   197  						Arguments: args,
   198  					},
   199  				}},
   200  			}},
   201  		}, true
   202  
   203  	case isGoSumUpdates:
   204  		return &source.TidiedModule{
   205  			Errors: []*source.Error{{
   206  				URI:     fh.URI(),
   207  				Range:   rng,
   208  				Kind:    source.ListError,
   209  				Message: `go.sum is out of sync with go.mod. Please update it or run "go mod tidy".`,
   210  				SuggestedFixes: []source.SuggestedFix{
   211  					{
   212  						Command: &protocol.Command{
   213  							Command:   source.CommandTidy.ID(),
   214  							Title:     source.CommandTidy.Title,
   215  							Arguments: args,
   216  						},
   217  					},
   218  					{
   219  						Command: &protocol.Command{
   220  							Command:   source.CommandUpdateGoSum.ID(),
   221  							Title:     source.CommandUpdateGoSum.Title,
   222  							Arguments: args,
   223  						},
   224  					},
   225  				},
   226  			}},
   227  		}, true
   228  	}
   229  
   230  	return nil, false
   231  }
   232  
   233  func hashImports(ctx context.Context, wsPackages []source.Package) (string, error) {
   234  	results := make(map[string]bool)
   235  	var imports []string
   236  	for _, pkg := range wsPackages {
   237  		for _, path := range pkg.Imports() {
   238  			imp := path.PkgPath()
   239  			if _, ok := results[imp]; !ok {
   240  				results[imp] = true
   241  				imports = append(imports, imp)
   242  			}
   243  		}
   244  		imports = append(imports, pkg.MissingDependencies()...)
   245  	}
   246  	sort.Strings(imports)
   247  	hashed := strings.Join(imports, ",")
   248  	return hashContents([]byte(hashed)), nil
   249  }
   250  
   251  // modTidyErrors computes the differences between the original and tidied
   252  // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
   253  // may appear on the Go files that import packages from missing modules.
   254  func modTidyErrors(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []source.Package) (errors []*source.Error, err error) {
   255  	// First, determine which modules are unused and which are missing from the
   256  	// original go.mod file.
   257  	var (
   258  		unused          = make(map[string]*modfile.Require, len(pm.File.Require))
   259  		missing         = make(map[string]*modfile.Require, len(ideal.Require))
   260  		wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
   261  	)
   262  	for _, req := range pm.File.Require {
   263  		unused[req.Mod.Path] = req
   264  	}
   265  	for _, req := range ideal.Require {
   266  		origReq := unused[req.Mod.Path]
   267  		if origReq == nil {
   268  			missing[req.Mod.Path] = req
   269  			continue
   270  		} else if origReq.Indirect != req.Indirect {
   271  			wrongDirectness[req.Mod.Path] = origReq
   272  		}
   273  		delete(unused, req.Mod.Path)
   274  	}
   275  	for _, req := range unused {
   276  		srcErr, err := unusedError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  		errors = append(errors, srcErr)
   281  	}
   282  	for _, req := range wrongDirectness {
   283  		// Handle dependencies that are incorrectly labeled indirect and
   284  		// vice versa.
   285  		srcErr, err := directnessError(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
   286  		if err != nil {
   287  			return nil, err
   288  		}
   289  		errors = append(errors, srcErr)
   290  	}
   291  	// Next, compute any diagnostics for modules that are missing from the
   292  	// go.mod file. The fixes will be for the go.mod file, but the
   293  	// diagnostics should also appear in both the go.mod file and the import
   294  	// statements in the Go files in which the dependencies are used.
   295  	missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
   296  	for _, req := range missing {
   297  		srcErr, err := missingModuleError(snapshot, pm, req)
   298  		if err != nil {
   299  			return nil, err
   300  		}
   301  		missingModuleFixes[req] = srcErr.SuggestedFixes
   302  		errors = append(errors, srcErr)
   303  	}
   304  	// Add diagnostics for missing modules anywhere they are imported in the
   305  	// workspace.
   306  	for _, pkg := range workspacePkgs {
   307  		missingImports := map[string]*modfile.Require{}
   308  		var importedPkgs []string
   309  
   310  		// If -mod=readonly is not set we may have successfully imported
   311  		// packages from missing modules. Otherwise they'll be in
   312  		// MissingDependencies. Combine both.
   313  		for _, imp := range pkg.Imports() {
   314  			importedPkgs = append(importedPkgs, imp.PkgPath())
   315  		}
   316  		importedPkgs = append(importedPkgs, pkg.MissingDependencies()...)
   317  
   318  		for _, imp := range importedPkgs {
   319  			if req, ok := missing[imp]; ok {
   320  				missingImports[imp] = req
   321  				break
   322  			}
   323  			// If the import is a package of the dependency, then add the
   324  			// package to the map, this will eliminate the need to do this
   325  			// prefix package search on each import for each file.
   326  			// Example:
   327  			//
   328  			// import (
   329  			//   "github.com/jd-ly/tools/go/expect"
   330  			//   "github.com/jd-ly/tools/go/packages"
   331  			// )
   332  			// They both are related to the same module: "github.com/jd-ly/tools".
   333  			var match string
   334  			for _, req := range ideal.Require {
   335  				if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
   336  					match = req.Mod.Path
   337  				}
   338  			}
   339  			if req, ok := missing[match]; ok {
   340  				missingImports[imp] = req
   341  			}
   342  		}
   343  		// None of this package's imports are from missing modules.
   344  		if len(missingImports) == 0 {
   345  			continue
   346  		}
   347  		for _, pgf := range pkg.CompiledGoFiles() {
   348  			file, m := pgf.File, pgf.Mapper
   349  			if file == nil || m == nil {
   350  				continue
   351  			}
   352  			imports := make(map[string]*ast.ImportSpec)
   353  			for _, imp := range file.Imports {
   354  				if imp.Path == nil {
   355  					continue
   356  				}
   357  				if target, err := strconv.Unquote(imp.Path.Value); err == nil {
   358  					imports[target] = imp
   359  				}
   360  			}
   361  			if len(imports) == 0 {
   362  				continue
   363  			}
   364  			for importPath, req := range missingImports {
   365  				imp, ok := imports[importPath]
   366  				if !ok {
   367  					continue
   368  				}
   369  				fixes, ok := missingModuleFixes[req]
   370  				if !ok {
   371  					return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
   372  				}
   373  				srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes)
   374  				if err != nil {
   375  					return nil, err
   376  				}
   377  				errors = append(errors, srcErr)
   378  			}
   379  		}
   380  	}
   381  	return errors, nil
   382  }
   383  
   384  // unusedError returns a source.Error for an unused require.
   385  func unusedError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Error, error) {
   386  	rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  	args, err := source.MarshalArgs(m.URI, false, []string{req.Mod.Path + "@none"})
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  	return &source.Error{
   395  		Category: source.GoModTidy,
   396  		Message:  fmt.Sprintf("%s is not used in this module", req.Mod.Path),
   397  		Range:    rng,
   398  		URI:      m.URI,
   399  		SuggestedFixes: []source.SuggestedFix{{
   400  			Title: fmt.Sprintf("Remove dependency: %s", req.Mod.Path),
   401  			Command: &protocol.Command{
   402  				Title:     source.CommandRemoveDependency.Title,
   403  				Command:   source.CommandRemoveDependency.ID(),
   404  				Arguments: args,
   405  			},
   406  		}},
   407  	}, nil
   408  }
   409  
   410  // directnessError extracts errors when a dependency is labeled indirect when
   411  // it should be direct and vice versa.
   412  func directnessError(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Error, error) {
   413  	rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
   414  	if err != nil {
   415  		return nil, err
   416  	}
   417  	direction := "indirect"
   418  	if req.Indirect {
   419  		direction = "direct"
   420  
   421  		// If the dependency should be direct, just highlight the // indirect.
   422  		if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
   423  			end := comments.Suffix[0].Start
   424  			end.LineRune += len(comments.Suffix[0].Token)
   425  			end.Byte += len([]byte(comments.Suffix[0].Token))
   426  			rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
   427  			if err != nil {
   428  				return nil, err
   429  			}
   430  		}
   431  	}
   432  	// If the dependency should be indirect, add the // indirect.
   433  	edits, err := switchDirectness(req, m, computeEdits)
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  	return &source.Error{
   438  		Message:  fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
   439  		Range:    rng,
   440  		URI:      m.URI,
   441  		Category: source.GoModTidy,
   442  		SuggestedFixes: []source.SuggestedFix{{
   443  			Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
   444  			Edits: map[span.URI][]protocol.TextEdit{
   445  				m.URI: edits,
   446  			},
   447  		}},
   448  	}, nil
   449  }
   450  
   451  func missingModuleError(snapshot source.Snapshot, pm *source.ParsedModule, req *modfile.Require) (*source.Error, error) {
   452  	var rng protocol.Range
   453  	// Default to the start of the file if there is no module declaration.
   454  	if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
   455  		start, end := pm.File.Module.Syntax.Span()
   456  		var err error
   457  		rng, err = rangeFromPositions(pm.Mapper, start, end)
   458  		if err != nil {
   459  			return nil, err
   460  		}
   461  	}
   462  	args, err := source.MarshalArgs(pm.Mapper.URI, !req.Indirect, []string{req.Mod.Path + "@" + req.Mod.Version})
   463  	if err != nil {
   464  		return nil, err
   465  	}
   466  	return &source.Error{
   467  		URI:      pm.Mapper.URI,
   468  		Range:    rng,
   469  		Message:  fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   470  		Category: source.GoModTidy,
   471  		Kind:     source.ModTidyError,
   472  		SuggestedFixes: []source.SuggestedFix{{
   473  			Title: fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path),
   474  			Command: &protocol.Command{
   475  				Title:     source.CommandAddDependency.Title,
   476  				Command:   source.CommandAddDependency.ID(),
   477  				Arguments: args,
   478  			},
   479  		}},
   480  	}, nil
   481  }
   482  
   483  // switchDirectness gets the edits needed to change an indirect dependency to
   484  // direct and vice versa.
   485  func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
   486  	// We need a private copy of the parsed go.mod file, since we're going to
   487  	// modify it.
   488  	copied, err := modfile.Parse("", m.Content, nil)
   489  	if err != nil {
   490  		return nil, err
   491  	}
   492  	// Change the directness in the matching require statement. To avoid
   493  	// reordering the require statements, rewrite all of them.
   494  	var requires []*modfile.Require
   495  	for _, r := range copied.Require {
   496  		if r.Mod.Path == req.Mod.Path {
   497  			requires = append(requires, &modfile.Require{
   498  				Mod:      r.Mod,
   499  				Syntax:   r.Syntax,
   500  				Indirect: !r.Indirect,
   501  			})
   502  			continue
   503  		}
   504  		requires = append(requires, r)
   505  	}
   506  	copied.SetRequire(requires)
   507  	newContent, err := copied.Format()
   508  	if err != nil {
   509  		return nil, err
   510  	}
   511  	// Calculate the edits to be made due to the change.
   512  	diff := computeEdits(m.URI, string(m.Content), string(newContent))
   513  	return source.ToProtocolEdits(m, diff)
   514  }
   515  
   516  // missingModuleForImport creates an error for a given import path that comes
   517  // from a missing module.
   518  func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Error, error) {
   519  	if req.Syntax == nil {
   520  		return nil, fmt.Errorf("no syntax for %v", req)
   521  	}
   522  	spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
   523  	if err != nil {
   524  		return nil, err
   525  	}
   526  	rng, err := m.Range(spn)
   527  	if err != nil {
   528  		return nil, err
   529  	}
   530  	return &source.Error{
   531  		Category:       source.GoModTidy,
   532  		URI:            m.URI,
   533  		Range:          rng,
   534  		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   535  		Kind:           source.ModTidyError,
   536  		SuggestedFixes: fixes,
   537  	}, nil
   538  }
   539  
   540  func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
   541  	toPoint := func(offset int) (span.Point, error) {
   542  		l, c, err := m.Converter.ToPosition(offset)
   543  		if err != nil {
   544  			return span.Point{}, err
   545  		}
   546  		return span.NewPoint(l, c, offset), nil
   547  	}
   548  	start, err := toPoint(s.Byte)
   549  	if err != nil {
   550  		return protocol.Range{}, err
   551  	}
   552  	end, err := toPoint(e.Byte)
   553  	if err != nil {
   554  		return protocol.Range{}, err
   555  	}
   556  	return m.Range(span.New(m.URI, start, end))
   557  }