// Copyright 2019 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 provides core features related to go.mod file // handling for use by Go editors and tools. package mod import ( "context" "fmt" "strings" "golang.org/x/mod/modfile" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/telemetry" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/trace" ) func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.FileIdentity][]source.Diagnostic, error) { // TODO: We will want to support diagnostics for go.mod files even when the -modfile flag is turned off. realfh, tempfh, err := snapshot.ModFiles(ctx) if err != nil { return nil, err } // Check the case when the tempModfile flag is turned off. if realfh == nil || tempfh == nil { return nil, nil } ctx, done := trace.StartSpan(ctx, "modfiles.Diagnostics", telemetry.File.Of(realfh.Identity().URI)) defer done() // If the view has a temporary go.mod file, we want to run "go mod tidy" to be able to // diff between the real and the temp files. cfg := snapshot.View().Config(ctx) args := append([]string{"mod", "tidy"}, cfg.BuildFlags...) if _, err := source.InvokeGo(ctx, snapshot.View().Folder().Filename(), cfg.Env, args...); err != nil { // Ignore parse errors here. They'll be handled below. if !strings.Contains(err.Error(), "errors parsing go.mod") { return nil, err } } realMod, m, err := snapshot.View().Session().Cache().ParseModHandle(realfh).Parse(ctx) // If the go.mod file fails to parse, return errors right away. if err, ok := err.(*source.Error); ok { return map[source.FileIdentity][]source.Diagnostic{ realfh.Identity(): []source.Diagnostic{{ Message: err.Message, Source: "syntax", Range: err.Range, Severity: protocol.SeverityError, }}, }, nil } if err != nil { return nil, err } tempMod, _, err := snapshot.View().Session().Cache().ParseModHandle(tempfh).Parse(ctx) if err != nil { return nil, err } // Check indirect vs direct, and removal of dependencies. reports := map[source.FileIdentity][]source.Diagnostic{ realfh.Identity(): []source.Diagnostic{}, } realReqs := make(map[string]*modfile.Require, len(realMod.Require)) tempReqs := make(map[string]*modfile.Require, len(tempMod.Require)) for _, req := range realMod.Require { realReqs[req.Mod.Path] = req } for _, req := range tempMod.Require { realReq := realReqs[req.Mod.Path] if realReq != nil && realReq.Indirect == req.Indirect { delete(realReqs, req.Mod.Path) } tempReqs[req.Mod.Path] = req } for _, req := range realReqs { if req.Syntax == nil { continue } dep := req.Mod.Path rng, err := getRangeFromPositions(m, realfh, req.Syntax.Start, req.Syntax.End) if err != nil { return nil, err } var diag *source.Diagnostic if tempReqs[dep] != nil && req.Indirect != tempReqs[dep].Indirect { // Show diagnostics for dependencies that are incorrectly labeled indirect. if req.Indirect { var fix []source.SuggestedFix // If the dependency should not be indirect, just highlight the // indirect. if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 { end := comments.Suffix[0].Start end.LineRune += len(comments.Suffix[0].Token) end.Byte += len([]byte(comments.Suffix[0].Token)) rng, err = getRangeFromPositions(m, realfh, comments.Suffix[0].Start, end) if err != nil { return nil, err } fix = []source.SuggestedFix{ { Title: "Remove indirect", Edits: map[span.URI][]protocol.TextEdit{realfh.Identity().URI: []protocol.TextEdit{ { Range: rng, NewText: "", }, }}, }, } } diag = &source.Diagnostic{ Message: fmt.Sprintf("%s should be a direct dependency.", dep), Range: rng, SuggestedFixes: fix, Source: "go mod tidy", Severity: protocol.SeverityWarning, } } else { diag = &source.Diagnostic{ Message: fmt.Sprintf("%s should be an indirect dependency.", dep), Range: rng, Source: "go mod tidy", Severity: protocol.SeverityWarning, } } } // Handle unused dependencies. if tempReqs[dep] == nil { diag = &source.Diagnostic{ Message: fmt.Sprintf("%s is not used in this module.", dep), Range: rng, SuggestedFixes: []source.SuggestedFix{ { Title: fmt.Sprintf("Remove %s.", dep), Edits: map[span.URI][]protocol.TextEdit{realfh.Identity().URI: []protocol.TextEdit{ { Range: rng, NewText: "", }, }}, }, }, Source: "go mod tidy", Severity: protocol.SeverityWarning, } } reports[realfh.Identity()] = append(reports[realfh.Identity()], *diag) } return reports, nil } // TODO: Add caching for go.mod diagnostics to be able to map them back to source.Diagnostics // and reuse the cached suggested fixes. func SuggestedFixes(fh source.FileHandle, diags []protocol.Diagnostic) []protocol.CodeAction { var actions []protocol.CodeAction for _, diag := range diags { var title string if strings.Contains(diag.Message, "is not used in this module") { split := strings.Split(diag.Message, " ") if len(split) < 1 { continue } title = fmt.Sprintf("Remove dependency: %s", split[0]) } if strings.Contains(diag.Message, "should be a direct dependency.") { title = "Remove indirect" } if title == "" { continue } actions = append(actions, protocol.CodeAction{ Title: title, Kind: protocol.QuickFix, Edit: protocol.WorkspaceEdit{ DocumentChanges: []protocol.TextDocumentEdit{ { TextDocument: protocol.VersionedTextDocumentIdentifier{ Version: fh.Identity().Version, TextDocumentIdentifier: protocol.TextDocumentIdentifier{ URI: protocol.NewURI(fh.Identity().URI), }, }, Edits: []protocol.TextEdit{protocol.TextEdit{Range: diag.Range, NewText: ""}}, }, }, }, Diagnostics: diags, }) } return actions } func getEndOfLine(req *modfile.Require, m *protocol.ColumnMapper) (span.Point, error) { comments := req.Syntax.Comment() if comments == nil { return positionToPoint(m, req.Syntax.End) } suffix := comments.Suffix if len(suffix) == 0 { return positionToPoint(m, req.Syntax.End) } end := suffix[0].Start end.LineRune += len(suffix[0].Token) return positionToPoint(m, end) } func getRangeFromPositions(m *protocol.ColumnMapper, fh source.FileHandle, s, e modfile.Position) (protocol.Range, error) { start, err := positionToPoint(m, s) if err != nil { return protocol.Range{}, err } end, err := positionToPoint(m, e) if err != nil { return protocol.Range{}, err } spn := span.New(fh.Identity().URI, start, end) rng, err := m.Range(spn) if err != nil { return protocol.Range{}, err } return rng, nil } func positionToPoint(m *protocol.ColumnMapper, pos modfile.Position) (span.Point, error) { line, col, err := m.Converter.ToPosition(pos.Byte) if err != nil { return span.Point{}, err } return span.NewPoint(line, col, pos.Byte), nil }