| // 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 mod |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "path/filepath" |
| |
| "golang.org/x/mod/modfile" |
| "golang.org/x/tools/internal/lsp/command" |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/lsp/source" |
| ) |
| |
| // LensFuncs returns the supported lensFuncs for go.mod files. |
| func LensFuncs() map[command.Command]source.LensFunc { |
| return map[command.Command]source.LensFunc{ |
| command.UpgradeDependency: upgradeLenses, |
| command.Tidy: tidyLens, |
| command.Vendor: vendorLens, |
| command.RunVulncheckExp: vulncheckLenses, |
| } |
| } |
| |
| func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { |
| pm, err := snapshot.ParseMod(ctx, fh) |
| if err != nil || pm.File == nil { |
| return nil, err |
| } |
| if len(pm.File.Require) == 0 { |
| // Nothing to upgrade. |
| return nil, nil |
| } |
| var requires []string |
| for _, req := range pm.File.Require { |
| requires = append(requires, req.Mod.Path) |
| } |
| uri := protocol.URIFromSpanURI(fh.URI()) |
| checkUpgrade, err := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{ |
| URI: uri, |
| Modules: requires, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| upgradeTransitive, err := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{ |
| URI: uri, |
| AddRequire: false, |
| GoCmdArgs: []string{"-d", "-u", "-t", "./..."}, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| upgradeDirect, err := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{ |
| URI: uri, |
| AddRequire: false, |
| GoCmdArgs: append([]string{"-d"}, requires...), |
| }) |
| if err != nil { |
| return nil, err |
| } |
| // Put the upgrade code lenses above the first require block or statement. |
| rng, err := firstRequireRange(fh, pm) |
| if err != nil { |
| return nil, err |
| } |
| |
| return []protocol.CodeLens{ |
| {Range: rng, Command: checkUpgrade}, |
| {Range: rng, Command: upgradeTransitive}, |
| {Range: rng, Command: upgradeDirect}, |
| }, nil |
| } |
| |
| func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { |
| pm, err := snapshot.ParseMod(ctx, fh) |
| if err != nil || pm.File == nil { |
| return nil, err |
| } |
| uri := protocol.URIFromSpanURI(fh.URI()) |
| cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}}) |
| if err != nil { |
| return nil, err |
| } |
| rng, err := moduleStmtRange(fh, pm) |
| if err != nil { |
| return nil, err |
| } |
| return []protocol.CodeLens{{ |
| Range: rng, |
| Command: cmd, |
| }}, nil |
| } |
| |
| func vendorLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { |
| pm, err := snapshot.ParseMod(ctx, fh) |
| if err != nil || pm.File == nil { |
| return nil, err |
| } |
| if len(pm.File.Require) == 0 { |
| // Nothing to vendor. |
| return nil, nil |
| } |
| rng, err := moduleStmtRange(fh, pm) |
| if err != nil { |
| return nil, err |
| } |
| title := "Create vendor directory" |
| uri := protocol.URIFromSpanURI(fh.URI()) |
| cmd, err := command.NewVendorCommand(title, command.URIArg{URI: uri}) |
| if err != nil { |
| return nil, err |
| } |
| // Change the message depending on whether or not the module already has a |
| // vendor directory. |
| vendorDir := filepath.Join(filepath.Dir(fh.URI().Filename()), "vendor") |
| if info, _ := os.Stat(vendorDir); info != nil && info.IsDir() { |
| title = "Sync vendor directory" |
| } |
| return []protocol.CodeLens{{Range: rng, Command: cmd}}, nil |
| } |
| |
| func moduleStmtRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) { |
| if pm.File == nil || pm.File.Module == nil || pm.File.Module.Syntax == nil { |
| return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI()) |
| } |
| syntax := pm.File.Module.Syntax |
| return source.LineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End) |
| } |
| |
| // firstRequireRange returns the range for the first "require" in the given |
| // go.mod file. This is either a require block or an individual require line. |
| func firstRequireRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) { |
| if len(pm.File.Require) == 0 { |
| return protocol.Range{}, fmt.Errorf("no requires in the file %s", fh.URI()) |
| } |
| var start, end modfile.Position |
| for _, stmt := range pm.File.Syntax.Stmt { |
| if b, ok := stmt.(*modfile.LineBlock); ok && len(b.Token) == 1 && b.Token[0] == "require" { |
| start, end = b.Span() |
| break |
| } |
| } |
| |
| firstRequire := pm.File.Require[0].Syntax |
| if start.Byte == 0 || firstRequire.Start.Byte < start.Byte { |
| start, end = firstRequire.Start, firstRequire.End |
| } |
| return source.LineToRange(pm.Mapper, fh.URI(), start, end) |
| } |
| |
| func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { |
| pm, err := snapshot.ParseMod(ctx, fh) |
| if err != nil || pm.File == nil { |
| return nil, err |
| } |
| // Place the codelenses near the module statement. |
| // A module may not have the require block, |
| // but vulnerabilities can exist in standard libraries. |
| uri := protocol.URIFromSpanURI(fh.URI()) |
| rng, err := moduleStmtRange(fh, pm) |
| if err != nil { |
| return nil, err |
| } |
| |
| vulncheck, err := command.NewRunVulncheckExpCommand("Run govulncheck", command.VulncheckArgs{ |
| URI: uri, |
| Pattern: "./...", |
| }) |
| if err != nil { |
| return nil, err |
| } |
| return []protocol.CodeLens{ |
| {Range: rng, Command: vulncheck}, |
| }, nil |
| } |