1// Copyright 2018 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
5package source
6
7import (
8	"context"
9	"fmt"
10
11	"golang.org/x/tools/go/analysis"
12	"golang.org/x/tools/internal/lsp/protocol"
13	"golang.org/x/tools/internal/lsp/telemetry"
14	"golang.org/x/tools/internal/span"
15	"golang.org/x/tools/internal/telemetry/log"
16	"golang.org/x/tools/internal/telemetry/trace"
17	errors "golang.org/x/xerrors"
18)
19
20type Diagnostic struct {
21	Range    protocol.Range
22	Message  string
23	Source   string
24	Severity protocol.DiagnosticSeverity
25	Tags     []protocol.DiagnosticTag
26
27	SuggestedFixes []SuggestedFix
28	Related        []RelatedInformation
29}
30
31type SuggestedFix struct {
32	Title string
33	Edits map[span.URI][]protocol.TextEdit
34}
35
36type RelatedInformation struct {
37	URI     span.URI
38	Range   protocol.Range
39	Message string
40}
41
42func Diagnostics(ctx context.Context, snapshot Snapshot, f File, withAnalysis bool, disabledAnalyses map[string]struct{}) (map[FileIdentity][]Diagnostic, string, error) {
43	ctx, done := trace.StartSpan(ctx, "source.Diagnostics", telemetry.File.Of(f.URI()))
44	defer done()
45
46	fh := snapshot.Handle(ctx, f)
47	phs, err := snapshot.PackageHandles(ctx, fh)
48	if err != nil {
49		return nil, "", err
50	}
51	ph, err := WidestCheckPackageHandle(phs)
52	if err != nil {
53		return nil, "", err
54	}
55	// If we are missing dependencies, it may because the user's workspace is
56	// not correctly configured. Report errors, if possible.
57	var warningMsg string
58	if len(ph.MissingDependencies()) > 0 {
59		if warningMsg, err = checkCommonErrors(ctx, snapshot.View(), f.URI()); err != nil {
60			log.Error(ctx, "error checking common errors", err, telemetry.File.Of(f.URI))
61		}
62	}
63	pkg, err := ph.Check(ctx)
64	if err != nil {
65		return nil, "", err
66	}
67	// Prepare the reports we will send for the files in this package.
68	reports := make(map[FileIdentity][]Diagnostic)
69	for _, fh := range pkg.CompiledGoFiles() {
70		clearReports(snapshot, reports, fh.File().Identity())
71	}
72	// Prepare any additional reports for the errors in this package.
73	for _, e := range pkg.GetErrors() {
74		// We only need to handle lower-level errors.
75		if !(e.Kind == UnknownError || e.Kind == ListError) {
76			continue
77		}
78		// If no file is associated with the error, default to the current file.
79		if e.File.URI.Filename() == "" {
80			e.File = fh.Identity()
81		}
82		clearReports(snapshot, reports, e.File)
83	}
84	// Run diagnostics for the package that this URI belongs to.
85	if !diagnostics(ctx, snapshot, pkg, reports) && withAnalysis {
86		// If we don't have any list, parse, or type errors, run analyses.
87		if err := analyses(ctx, snapshot, ph, disabledAnalyses, reports); err != nil {
88			// Exit early if the context has been canceled.
89			if err == context.Canceled {
90				return nil, "", err
91			}
92			log.Error(ctx, "failed to run analyses", err, telemetry.File.Of(f.URI()))
93		}
94	}
95	// Updates to the diagnostics for this package may need to be propagated.
96	for _, id := range snapshot.GetReverseDependencies(pkg.ID()) {
97		ph, err := snapshot.PackageHandle(ctx, id)
98		if err != nil {
99			return nil, warningMsg, err
100		}
101		pkg, err := ph.Check(ctx)
102		if err != nil {
103			return nil, warningMsg, err
104		}
105		for _, fh := range pkg.CompiledGoFiles() {
106			clearReports(snapshot, reports, fh.File().Identity())
107		}
108		diagnostics(ctx, snapshot, pkg, reports)
109	}
110	return reports, warningMsg, nil
111}
112
113type diagnosticSet struct {
114	listErrors, parseErrors, typeErrors []*Diagnostic
115}
116
117func diagnostics(ctx context.Context, snapshot Snapshot, pkg Package, reports map[FileIdentity][]Diagnostic) bool {
118	ctx, done := trace.StartSpan(ctx, "source.diagnostics", telemetry.Package.Of(pkg.ID()))
119	_ = ctx // circumvent SA4006
120	defer done()
121
122	diagSets := make(map[FileIdentity]*diagnosticSet)
123	for _, e := range pkg.GetErrors() {
124		diag := &Diagnostic{
125			Message:  e.Message,
126			Range:    e.Range,
127			Severity: protocol.SeverityError,
128		}
129		set, ok := diagSets[e.File]
130		if !ok {
131			set = &diagnosticSet{}
132			diagSets[e.File] = set
133		}
134		switch e.Kind {
135		case ParseError:
136			set.parseErrors = append(set.parseErrors, diag)
137			diag.Source = "syntax"
138		case TypeError:
139			set.typeErrors = append(set.typeErrors, diag)
140			diag.Source = "compiler"
141		case ListError, UnknownError:
142			set.listErrors = append(set.listErrors, diag)
143			diag.Source = "go list"
144		}
145	}
146	var nonEmptyDiagnostics bool // track if we actually send non-empty diagnostics
147	for fileID, set := range diagSets {
148		// Don't report type errors if there are parse errors or list errors.
149		diags := set.typeErrors
150		if len(set.parseErrors) > 0 {
151			diags = set.parseErrors
152		} else if len(set.listErrors) > 0 {
153			diags = set.listErrors
154		}
155		if len(diags) > 0 {
156			nonEmptyDiagnostics = true
157		}
158		addReports(ctx, reports, snapshot, fileID, diags...)
159	}
160	return nonEmptyDiagnostics
161}
162
163func analyses(ctx context.Context, snapshot Snapshot, ph PackageHandle, disabledAnalyses map[string]struct{}, reports map[FileIdentity][]Diagnostic) error {
164	var analyzers []*analysis.Analyzer
165	for _, a := range snapshot.View().Options().Analyzers {
166		if _, ok := disabledAnalyses[a.Name]; ok {
167			continue
168		}
169		analyzers = append(analyzers, a)
170	}
171
172	diagnostics, err := snapshot.Analyze(ctx, ph.ID(), analyzers)
173	if err != nil {
174		return err
175	}
176
177	// Report diagnostics and errors from root analyzers.
178	for _, e := range diagnostics {
179		// This is a bit of a hack, but clients > 3.15 will be able to grey out unnecessary code.
180		// If we are deleting code as part of all of our suggested fixes, assume that this is dead code.
181		// TODO(golang/go/#34508): Return these codes from the diagnostics themselves.
182		var tags []protocol.DiagnosticTag
183		if onlyDeletions(e.SuggestedFixes) {
184			tags = append(tags, protocol.Unnecessary)
185		}
186		addReports(ctx, reports, snapshot, e.File, &Diagnostic{
187			Range:          e.Range,
188			Message:        e.Message,
189			Source:         e.Category,
190			Severity:       protocol.SeverityWarning,
191			Tags:           tags,
192			SuggestedFixes: e.SuggestedFixes,
193			Related:        e.Related,
194		})
195	}
196	return nil
197}
198
199func clearReports(snapshot Snapshot, reports map[FileIdentity][]Diagnostic, fileID FileIdentity) {
200	if snapshot.View().Ignore(fileID.URI) {
201		return
202	}
203	reports[fileID] = []Diagnostic{}
204}
205
206func addReports(ctx context.Context, reports map[FileIdentity][]Diagnostic, snapshot Snapshot, fileID FileIdentity, diagnostics ...*Diagnostic) error {
207	if snapshot.View().Ignore(fileID.URI) {
208		return nil
209	}
210	if _, ok := reports[fileID]; !ok {
211		return errors.Errorf("diagnostics for unexpected file %s", fileID.URI)
212	}
213	for _, diag := range diagnostics {
214		if diag == nil {
215			continue
216		}
217		reports[fileID] = append(reports[fileID], *diag)
218	}
219	return nil
220}
221
222func singleDiagnostic(fileID FileIdentity, format string, a ...interface{}) map[FileIdentity][]Diagnostic {
223	return map[FileIdentity][]Diagnostic{
224		fileID: []Diagnostic{
225			{
226				Source:   "gopls",
227				Range:    protocol.Range{},
228				Message:  fmt.Sprintf(format, a...),
229				Severity: protocol.SeverityError,
230			},
231		},
232	}
233}
234
235// onlyDeletions returns true if all of the suggested fixes are deletions.
236func onlyDeletions(fixes []SuggestedFix) bool {
237	for _, fix := range fixes {
238		for _, edits := range fix.Edits {
239			for _, edit := range edits {
240				if edit.NewText != "" {
241					return false
242				}
243				if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
244					return false
245				}
246			}
247		}
248	}
249	return true
250}
251