// 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/lsp/command" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/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.RunGovulncheck: 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 } uri := protocol.URIFromSpanURI(fh.URI()) reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URIArg: command.URIArg{URI: uri}}) if err != nil { return nil, err } // 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, 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 append(lenses, []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 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 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 pm.Mapper.OffsetRange(start.Byte, end.Byte) } 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.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ URI: uri, Pattern: "./...", }) if err != nil { return nil, err } return []protocol.CodeLens{ {Range: rng, Command: &vulncheck}, }, nil }