| // 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/gopls/internal/cache" | |
| "golang.org/x/tools/gopls/internal/file" | |
| "golang.org/x/tools/gopls/internal/protocol" | |
| "golang.org/x/tools/gopls/internal/protocol/command" | |
| "golang.org/x/tools/gopls/internal/settings" | |
| ) | |
| // CodeLensSources returns the sources of code lenses for go.mod files. | |
| func CodeLensSources() map[settings.CodeLensSource]cache.CodeLensSourceFunc { | |
| return map[settings.CodeLensSource]cache.CodeLensSourceFunc{ | |
| settings.CodeLensUpgradeDependency: upgradeLenses, // commands: CheckUpgrades, UpgradeDependency | |
| settings.CodeLensTidy: tidyLens, // commands: Tidy | |
| settings.CodeLensVendor: vendorLens, // commands: Vendor | |
| settings.CodeLensVulncheck: vulncheckLenses, // commands: Vulncheck | |
| settings.CodeLensRunGovulncheck: runGovulncheckLenses, // commands: RunGovulncheck | |
| } | |
| } | |
| func upgradeLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { | |
| pm, err := snapshot.ParseMod(ctx, fh) | |
| if err != nil || pm.File == nil { | |
| return nil, err | |
| } | |
| uri := fh.URI() | |
| reset := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URI: uri}) | |
| // Put the `Reset go.mod diagnostics` codelens on the module statement. | |
| modrng, err := moduleStmtRange(fh, pm) | |
| if err != nil { | |
| return nil, err | |
| } | |
| lenses := []protocol.CodeLens{{Range: modrng, Command: reset}} | |
| if len(pm.File.Require) == 0 { | |
| // Nothing to upgrade. | |
| return lenses, nil | |
| } | |
| var requires []string | |
| for _, req := range pm.File.Require { | |
| requires = append(requires, req.Mod.Path) | |
| } | |
| checkUpgrade := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{ | |
| URI: uri, | |
| Modules: requires, | |
| }) | |
| upgradeTransitive := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{ | |
| URI: uri, | |
| AddRequire: false, | |
| GoCmdArgs: []string{"-d", "-u", "-t", "./..."}, | |
| }) | |
| upgradeDirect := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{ | |
| URI: uri, | |
| AddRequire: false, | |
| GoCmdArgs: append([]string{"-d"}, requires...), | |
| }) | |
| // Put the upgrade code lenses above the first require block or statement. | |
| rng, err := firstRequireRange(fh, pm) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return append(lenses, []protocol.CodeLens{ | |
| {Range: rng, Command: checkUpgrade}, | |
| {Range: rng, Command: upgradeTransitive}, | |
| {Range: rng, Command: upgradeDirect}, | |
| }...), nil | |
| } | |
| func tidyLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { | |
| pm, err := snapshot.ParseMod(ctx, fh) | |
| if err != nil || pm.File == nil { | |
| return nil, err | |
| } | |
| uri := fh.URI() | |
| cmd := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}}) | |
| 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 *cache.Snapshot, fh file.Handle) ([]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 := fh.URI() | |
| cmd := command.NewVendorCommand(title, command.URIArg{URI: uri}) | |
| // Change the message depending on whether or not the module already has a | |
| // vendor directory. | |
| vendorDir := filepath.Join(fh.URI().DirPath(), "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 file.Handle, pm *cache.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 pm.Mapper.OffsetRange(syntax.Start.Byte, syntax.End.Byte) | |
| } | |
| // 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 file.Handle, pm *cache.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 pm.Mapper.OffsetRange(start.Byte, end.Byte) | |
| } | |
| func vulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]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 := fh.URI() | |
| rng, err := moduleStmtRange(fh, pm) | |
| if err != nil { | |
| return nil, err | |
| } | |
| vulncheck := command.NewVulncheckCommand("Run govulncheck", command.VulncheckArgs{ | |
| URI: uri, | |
| Pattern: "./...", | |
| }) | |
| return []protocol.CodeLens{ | |
| {Range: rng, Command: vulncheck}, | |
| }, nil | |
| } | |
| func runGovulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { | |
| // If CodeLensVulncheck is enabled, do not use the legacy CodeLensRunGovulncheck. | |
| if snapshot.Options().UserOptions.UIOptions.Codelenses[settings.CodeLensVulncheck] { | |
| return nil, nil | |
| } | |
| 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 := fh.URI() | |
| rng, err := moduleStmtRange(fh, pm) | |
| if err != nil { | |
| return nil, err | |
| } | |
| vulncheck := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ | |
| URI: uri, | |
| Pattern: "./...", | |
| }) | |
| return []protocol.CodeLens{ | |
| {Range: rng, Command: vulncheck}, | |
| }, nil | |
| } |