github.com/v2fly/tools@v0.100.0/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  	"github.com/v2fly/tools/internal/event"
    19  	"github.com/v2fly/tools/internal/gocommand"
    20  	"github.com/v2fly/tools/internal/lsp/command"
    21  	"github.com/v2fly/tools/internal/lsp/debug/tag"
    22  	"github.com/v2fly/tools/internal/lsp/diff"
    23  	"github.com/v2fly/tools/internal/lsp/protocol"
    24  	"github.com/v2fly/tools/internal/lsp/source"
    25  	"github.com/v2fly/tools/internal/memoize"
    26  	"github.com/v2fly/tools/internal/span"
    27  	"golang.org/x/mod/modfile"
    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) uriToModDecl(ctx context.Context, uri span.URI) (protocol.Range, error) {
   156  	fh, err := s.GetFile(ctx, uri)
   157  	if err != nil {
   158  		return protocol.Range{}, nil
   159  	}
   160  	pmf, err := s.ParseMod(ctx, fh)
   161  	if err != nil {
   162  		return protocol.Range{}, nil
   163  	}
   164  	if pmf.File.Module == nil || pmf.File.Module.Syntax == nil {
   165  		return protocol.Range{}, nil
   166  	}
   167  	return rangeFromPositions(pmf.Mapper, pmf.File.Module.Syntax.Start, pmf.File.Module.Syntax.End)
   168  }
   169  
   170  func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) (string, error) {
   171  	seen := map[string]struct{}{}
   172  	var imports []string
   173  	for _, ph := range wsPackages {
   174  		for _, imp := range ph.imports(ctx, s) {
   175  			if _, ok := seen[imp]; !ok {
   176  				imports = append(imports, imp)
   177  				seen[imp] = struct{}{}
   178  			}
   179  		}
   180  	}
   181  	sort.Strings(imports)
   182  	hashed := strings.Join(imports, ",")
   183  	return hashContents([]byte(hashed)), nil
   184  }
   185  
   186  // modTidyDiagnostics computes the differences between the original and tidied
   187  // go.mod files to produce diagnostic and suggested fixes. Some diagnostics
   188  // may appear on the Go files that import packages from missing modules.
   189  func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []*packageHandle) (diagnostics []*source.Diagnostic, err error) {
   190  	// First, determine which modules are unused and which are missing from the
   191  	// original go.mod file.
   192  	var (
   193  		unused          = make(map[string]*modfile.Require, len(pm.File.Require))
   194  		missing         = make(map[string]*modfile.Require, len(ideal.Require))
   195  		wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require))
   196  	)
   197  	for _, req := range pm.File.Require {
   198  		unused[req.Mod.Path] = req
   199  	}
   200  	for _, req := range ideal.Require {
   201  		origReq := unused[req.Mod.Path]
   202  		if origReq == nil {
   203  			missing[req.Mod.Path] = req
   204  			continue
   205  		} else if origReq.Indirect != req.Indirect {
   206  			wrongDirectness[req.Mod.Path] = origReq
   207  		}
   208  		delete(unused, req.Mod.Path)
   209  	}
   210  	for _, req := range wrongDirectness {
   211  		// Handle dependencies that are incorrectly labeled indirect and
   212  		// vice versa.
   213  		srcDiag, err := directnessDiagnostic(pm.Mapper, req, snapshot.View().Options().ComputeEdits)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		diagnostics = append(diagnostics, srcDiag)
   218  	}
   219  	// Next, compute any diagnostics for modules that are missing from the
   220  	// go.mod file. The fixes will be for the go.mod file, but the
   221  	// diagnostics should also appear in both the go.mod file and the import
   222  	// statements in the Go files in which the dependencies are used.
   223  	missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{}
   224  	for _, req := range missing {
   225  		srcDiag, err := missingModuleDiagnostic(pm, req)
   226  		if err != nil {
   227  			return nil, err
   228  		}
   229  		missingModuleFixes[req] = srcDiag.SuggestedFixes
   230  		diagnostics = append(diagnostics, srcDiag)
   231  	}
   232  	// Add diagnostics for missing modules anywhere they are imported in the
   233  	// workspace.
   234  	for _, ph := range workspacePkgs {
   235  		missingImports := map[string]*modfile.Require{}
   236  
   237  		// If -mod=readonly is not set we may have successfully imported
   238  		// packages from missing modules. Otherwise they'll be in
   239  		// MissingDependencies. Combine both.
   240  		importedPkgs := ph.imports(ctx, snapshot)
   241  
   242  		for _, imp := range importedPkgs {
   243  			if req, ok := missing[imp]; ok {
   244  				missingImports[imp] = req
   245  				break
   246  			}
   247  			// If the import is a package of the dependency, then add the
   248  			// package to the map, this will eliminate the need to do this
   249  			// prefix package search on each import for each file.
   250  			// Example:
   251  			//
   252  			// import (
   253  			//   "github.com/v2fly/tools/go/expect"
   254  			//   "github.com/v2fly/tools/go/packages"
   255  			// )
   256  			// They both are related to the same module: "github.com/v2fly/tools".
   257  			var match string
   258  			for _, req := range ideal.Require {
   259  				if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) {
   260  					match = req.Mod.Path
   261  				}
   262  			}
   263  			if req, ok := missing[match]; ok {
   264  				missingImports[imp] = req
   265  			}
   266  		}
   267  		// None of this package's imports are from missing modules.
   268  		if len(missingImports) == 0 {
   269  			continue
   270  		}
   271  		for _, pgh := range ph.compiledGoFiles {
   272  			pgf, err := snapshot.ParseGo(ctx, pgh.file, source.ParseHeader)
   273  			if err != nil {
   274  				continue
   275  			}
   276  			file, m := pgf.File, pgf.Mapper
   277  			if file == nil || m == nil {
   278  				continue
   279  			}
   280  			imports := make(map[string]*ast.ImportSpec)
   281  			for _, imp := range file.Imports {
   282  				if imp.Path == nil {
   283  					continue
   284  				}
   285  				if target, err := strconv.Unquote(imp.Path.Value); err == nil {
   286  					imports[target] = imp
   287  				}
   288  			}
   289  			if len(imports) == 0 {
   290  				continue
   291  			}
   292  			for importPath, req := range missingImports {
   293  				imp, ok := imports[importPath]
   294  				if !ok {
   295  					continue
   296  				}
   297  				fixes, ok := missingModuleFixes[req]
   298  				if !ok {
   299  					return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path)
   300  				}
   301  				srcErr, err := missingModuleForImport(snapshot, m, imp, req, fixes)
   302  				if err != nil {
   303  					return nil, err
   304  				}
   305  				diagnostics = append(diagnostics, srcErr)
   306  			}
   307  		}
   308  	}
   309  	// Finally, add errors for any unused dependencies.
   310  	onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1
   311  	for _, req := range unused {
   312  		srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic)
   313  		if err != nil {
   314  			return nil, err
   315  		}
   316  		diagnostics = append(diagnostics, srcErr)
   317  	}
   318  	return diagnostics, nil
   319  }
   320  
   321  // unusedDiagnostic returns a source.Diagnostic for an unused require.
   322  func unusedDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) {
   323  	rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path)
   328  	cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{
   329  		URI:            protocol.URIFromSpanURI(m.URI),
   330  		OnlyDiagnostic: onlyDiagnostic,
   331  		ModulePath:     req.Mod.Path,
   332  	})
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  	return &source.Diagnostic{
   337  		URI:            m.URI,
   338  		Range:          rng,
   339  		Severity:       protocol.SeverityWarning,
   340  		Source:         source.ModTidyError,
   341  		Message:        fmt.Sprintf("%s is not used in this module", req.Mod.Path),
   342  		SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   343  	}, nil
   344  }
   345  
   346  // directnessDiagnostic extracts errors when a dependency is labeled indirect when
   347  // it should be direct and vice versa.
   348  func directnessDiagnostic(m *protocol.ColumnMapper, req *modfile.Require, computeEdits diff.ComputeEdits) (*source.Diagnostic, error) {
   349  	rng, err := rangeFromPositions(m, req.Syntax.Start, req.Syntax.End)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	direction := "indirect"
   354  	if req.Indirect {
   355  		direction = "direct"
   356  
   357  		// If the dependency should be direct, just highlight the // indirect.
   358  		if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 {
   359  			end := comments.Suffix[0].Start
   360  			end.LineRune += len(comments.Suffix[0].Token)
   361  			end.Byte += len([]byte(comments.Suffix[0].Token))
   362  			rng, err = rangeFromPositions(m, comments.Suffix[0].Start, end)
   363  			if err != nil {
   364  				return nil, err
   365  			}
   366  		}
   367  	}
   368  	// If the dependency should be indirect, add the // indirect.
   369  	edits, err := switchDirectness(req, m, computeEdits)
   370  	if err != nil {
   371  		return nil, err
   372  	}
   373  	return &source.Diagnostic{
   374  		URI:      m.URI,
   375  		Range:    rng,
   376  		Severity: protocol.SeverityWarning,
   377  		Source:   source.ModTidyError,
   378  		Message:  fmt.Sprintf("%s should be %s", req.Mod.Path, direction),
   379  		SuggestedFixes: []source.SuggestedFix{{
   380  			Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction),
   381  			Edits: map[span.URI][]protocol.TextEdit{
   382  				m.URI: edits,
   383  			},
   384  			ActionKind: protocol.QuickFix,
   385  		}},
   386  	}, nil
   387  }
   388  
   389  func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*source.Diagnostic, error) {
   390  	var rng protocol.Range
   391  	// Default to the start of the file if there is no module declaration.
   392  	if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil {
   393  		start, end := pm.File.Module.Syntax.Span()
   394  		var err error
   395  		rng, err = rangeFromPositions(pm.Mapper, start, end)
   396  		if err != nil {
   397  			return nil, err
   398  		}
   399  	}
   400  	title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path)
   401  	cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{
   402  		URI:        protocol.URIFromSpanURI(pm.Mapper.URI),
   403  		AddRequire: !req.Indirect,
   404  		GoCmdArgs:  []string{req.Mod.Path + "@" + req.Mod.Version},
   405  	})
   406  	if err != nil {
   407  		return nil, err
   408  	}
   409  	return &source.Diagnostic{
   410  		URI:            pm.Mapper.URI,
   411  		Range:          rng,
   412  		Severity:       protocol.SeverityError,
   413  		Source:         source.ModTidyError,
   414  		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   415  		SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)},
   416  	}, nil
   417  }
   418  
   419  // switchDirectness gets the edits needed to change an indirect dependency to
   420  // direct and vice versa.
   421  func switchDirectness(req *modfile.Require, m *protocol.ColumnMapper, computeEdits diff.ComputeEdits) ([]protocol.TextEdit, error) {
   422  	// We need a private copy of the parsed go.mod file, since we're going to
   423  	// modify it.
   424  	copied, err := modfile.Parse("", m.Content, nil)
   425  	if err != nil {
   426  		return nil, err
   427  	}
   428  	// Change the directness in the matching require statement. To avoid
   429  	// reordering the require statements, rewrite all of them.
   430  	var requires []*modfile.Require
   431  	for _, r := range copied.Require {
   432  		if r.Mod.Path == req.Mod.Path {
   433  			requires = append(requires, &modfile.Require{
   434  				Mod:      r.Mod,
   435  				Syntax:   r.Syntax,
   436  				Indirect: !r.Indirect,
   437  			})
   438  			continue
   439  		}
   440  		requires = append(requires, r)
   441  	}
   442  	copied.SetRequire(requires)
   443  	newContent, err := copied.Format()
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  	// Calculate the edits to be made due to the change.
   448  	diff, err := computeEdits(m.URI, string(m.Content), string(newContent))
   449  	if err != nil {
   450  		return nil, err
   451  	}
   452  	return source.ToProtocolEdits(m, diff)
   453  }
   454  
   455  // missingModuleForImport creates an error for a given import path that comes
   456  // from a missing module.
   457  func missingModuleForImport(snapshot source.Snapshot, m *protocol.ColumnMapper, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) {
   458  	if req.Syntax == nil {
   459  		return nil, fmt.Errorf("no syntax for %v", req)
   460  	}
   461  	spn, err := span.NewRange(snapshot.FileSet(), imp.Path.Pos(), imp.Path.End()).Span()
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  	rng, err := m.Range(spn)
   466  	if err != nil {
   467  		return nil, err
   468  	}
   469  	return &source.Diagnostic{
   470  		URI:            m.URI,
   471  		Range:          rng,
   472  		Severity:       protocol.SeverityError,
   473  		Source:         source.ModTidyError,
   474  		Message:        fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path),
   475  		SuggestedFixes: fixes,
   476  	}, nil
   477  }
   478  
   479  func rangeFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (protocol.Range, error) {
   480  	spn, err := spanFromPositions(m, s, e)
   481  	if err != nil {
   482  		return protocol.Range{}, err
   483  	}
   484  	return m.Range(spn)
   485  }
   486  
   487  func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Span, error) {
   488  	toPoint := func(offset int) (span.Point, error) {
   489  		l, c, err := m.Converter.ToPosition(offset)
   490  		if err != nil {
   491  			return span.Point{}, err
   492  		}
   493  		return span.NewPoint(l, c, offset), nil
   494  	}
   495  	start, err := toPoint(s.Byte)
   496  	if err != nil {
   497  		return span.Span{}, err
   498  	}
   499  	end, err := toPoint(e.Byte)
   500  	if err != nil {
   501  		return span.Span{}, err
   502  	}
   503  	return span.New(m.URI, start, end), nil
   504  }