golang.org/x/tools/gopls@v0.15.3/internal/mod/code_lens.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 mod
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"golang.org/x/mod/modfile"
    14  	"golang.org/x/tools/gopls/internal/cache"
    15  	"golang.org/x/tools/gopls/internal/file"
    16  	"golang.org/x/tools/gopls/internal/golang"
    17  	"golang.org/x/tools/gopls/internal/protocol"
    18  	"golang.org/x/tools/gopls/internal/protocol/command"
    19  )
    20  
    21  // LensFuncs returns the supported lensFuncs for go.mod files.
    22  func LensFuncs() map[command.Command]golang.LensFunc {
    23  	return map[command.Command]golang.LensFunc{
    24  		command.UpgradeDependency: upgradeLenses,
    25  		command.Tidy:              tidyLens,
    26  		command.Vendor:            vendorLens,
    27  		command.RunGovulncheck:    vulncheckLenses,
    28  	}
    29  }
    30  
    31  func upgradeLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
    32  	pm, err := snapshot.ParseMod(ctx, fh)
    33  	if err != nil || pm.File == nil {
    34  		return nil, err
    35  	}
    36  	uri := fh.URI()
    37  	reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URIArg: command.URIArg{URI: uri}})
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	// Put the `Reset go.mod diagnostics` codelens on the module statement.
    42  	modrng, err := moduleStmtRange(fh, pm)
    43  	if err != nil {
    44  		return nil, err
    45  	}
    46  	lenses := []protocol.CodeLens{{Range: modrng, Command: &reset}}
    47  	if len(pm.File.Require) == 0 {
    48  		// Nothing to upgrade.
    49  		return lenses, nil
    50  	}
    51  	var requires []string
    52  	for _, req := range pm.File.Require {
    53  		requires = append(requires, req.Mod.Path)
    54  	}
    55  	checkUpgrade, err := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{
    56  		URI:     uri,
    57  		Modules: requires,
    58  	})
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	upgradeTransitive, err := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{
    63  		URI:        uri,
    64  		AddRequire: false,
    65  		GoCmdArgs:  []string{"-d", "-u", "-t", "./..."},
    66  	})
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	upgradeDirect, err := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{
    71  		URI:        uri,
    72  		AddRequire: false,
    73  		GoCmdArgs:  append([]string{"-d"}, requires...),
    74  	})
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	// Put the upgrade code lenses above the first require block or statement.
    80  	rng, err := firstRequireRange(fh, pm)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	return append(lenses, []protocol.CodeLens{
    86  		{Range: rng, Command: &checkUpgrade},
    87  		{Range: rng, Command: &upgradeTransitive},
    88  		{Range: rng, Command: &upgradeDirect},
    89  	}...), nil
    90  }
    91  
    92  func tidyLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
    93  	pm, err := snapshot.ParseMod(ctx, fh)
    94  	if err != nil || pm.File == nil {
    95  		return nil, err
    96  	}
    97  	uri := fh.URI()
    98  	cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}})
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	rng, err := moduleStmtRange(fh, pm)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	return []protocol.CodeLens{{
   107  		Range:   rng,
   108  		Command: &cmd,
   109  	}}, nil
   110  }
   111  
   112  func vendorLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
   113  	pm, err := snapshot.ParseMod(ctx, fh)
   114  	if err != nil || pm.File == nil {
   115  		return nil, err
   116  	}
   117  	if len(pm.File.Require) == 0 {
   118  		// Nothing to vendor.
   119  		return nil, nil
   120  	}
   121  	rng, err := moduleStmtRange(fh, pm)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	title := "Create vendor directory"
   126  	uri := fh.URI()
   127  	cmd, err := command.NewVendorCommand(title, command.URIArg{URI: uri})
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	// Change the message depending on whether or not the module already has a
   132  	// vendor directory.
   133  	vendorDir := filepath.Join(filepath.Dir(fh.URI().Path()), "vendor")
   134  	if info, _ := os.Stat(vendorDir); info != nil && info.IsDir() {
   135  		title = "Sync vendor directory"
   136  	}
   137  	return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil
   138  }
   139  
   140  func moduleStmtRange(fh file.Handle, pm *cache.ParsedModule) (protocol.Range, error) {
   141  	if pm.File == nil || pm.File.Module == nil || pm.File.Module.Syntax == nil {
   142  		return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI())
   143  	}
   144  	syntax := pm.File.Module.Syntax
   145  	return pm.Mapper.OffsetRange(syntax.Start.Byte, syntax.End.Byte)
   146  }
   147  
   148  // firstRequireRange returns the range for the first "require" in the given
   149  // go.mod file. This is either a require block or an individual require line.
   150  func firstRequireRange(fh file.Handle, pm *cache.ParsedModule) (protocol.Range, error) {
   151  	if len(pm.File.Require) == 0 {
   152  		return protocol.Range{}, fmt.Errorf("no requires in the file %s", fh.URI())
   153  	}
   154  	var start, end modfile.Position
   155  	for _, stmt := range pm.File.Syntax.Stmt {
   156  		if b, ok := stmt.(*modfile.LineBlock); ok && len(b.Token) == 1 && b.Token[0] == "require" {
   157  			start, end = b.Span()
   158  			break
   159  		}
   160  	}
   161  
   162  	firstRequire := pm.File.Require[0].Syntax
   163  	if start.Byte == 0 || firstRequire.Start.Byte < start.Byte {
   164  		start, end = firstRequire.Start, firstRequire.End
   165  	}
   166  	return pm.Mapper.OffsetRange(start.Byte, end.Byte)
   167  }
   168  
   169  func vulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) {
   170  	pm, err := snapshot.ParseMod(ctx, fh)
   171  	if err != nil || pm.File == nil {
   172  		return nil, err
   173  	}
   174  	// Place the codelenses near the module statement.
   175  	// A module may not have the require block,
   176  	// but vulnerabilities can exist in standard libraries.
   177  	uri := fh.URI()
   178  	rng, err := moduleStmtRange(fh, pm)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{
   184  		URI:     uri,
   185  		Pattern: "./...",
   186  	})
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	return []protocol.CodeLens{
   191  		{Range: rng, Command: &vulncheck},
   192  	}, nil
   193  }