1// Copyright 2019 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 cache
6
7import (
8	"fmt"
9	"go/scanner"
10	"go/token"
11	"go/types"
12	"regexp"
13	"strconv"
14	"strings"
15
16	"golang.org/x/tools/go/analysis"
17	"golang.org/x/tools/go/packages"
18	"golang.org/x/tools/internal/analysisinternal"
19	"golang.org/x/tools/internal/lsp/command"
20	"golang.org/x/tools/internal/lsp/protocol"
21	"golang.org/x/tools/internal/lsp/source"
22	"golang.org/x/tools/internal/span"
23	"golang.org/x/tools/internal/typesinternal"
24	errors "golang.org/x/xerrors"
25)
26
27func goPackagesErrorDiagnostics(snapshot *snapshot, pkg *pkg, e packages.Error) ([]*source.Diagnostic, error) {
28	if msg, spn, ok := parseGoListImportCycleError(snapshot, e, pkg); ok {
29		rng, err := spanToRange(pkg, spn)
30		if err != nil {
31			return nil, err
32		}
33		return []*source.Diagnostic{{
34			URI:      spn.URI(),
35			Range:    rng,
36			Severity: protocol.SeverityError,
37			Source:   source.ListError,
38			Message:  msg,
39		}}, nil
40	}
41
42	var spn span.Span
43	if e.Pos == "" {
44		spn = parseGoListError(e.Msg, pkg.m.config.Dir)
45		// We may not have been able to parse a valid span. Apply the errors to all files.
46		if _, err := spanToRange(pkg, spn); err != nil {
47			var diags []*source.Diagnostic
48			for _, cgf := range pkg.compiledGoFiles {
49				diags = append(diags, &source.Diagnostic{
50					URI:      cgf.URI,
51					Severity: protocol.SeverityError,
52					Source:   source.ListError,
53					Message:  e.Msg,
54				})
55			}
56			return diags, nil
57		}
58	} else {
59		spn = span.ParseInDir(e.Pos, pkg.m.config.Dir)
60	}
61
62	rng, err := spanToRange(pkg, spn)
63	if err != nil {
64		return nil, err
65	}
66	return []*source.Diagnostic{{
67		URI:      spn.URI(),
68		Range:    rng,
69		Severity: protocol.SeverityError,
70		Source:   source.ListError,
71		Message:  e.Msg,
72	}}, nil
73}
74
75func parseErrorDiagnostics(snapshot *snapshot, pkg *pkg, errList scanner.ErrorList) ([]*source.Diagnostic, error) {
76	// The first parser error is likely the root cause of the problem.
77	if errList.Len() <= 0 {
78		return nil, errors.Errorf("no errors in %v", errList)
79	}
80	e := errList[0]
81	pgf, err := pkg.File(span.URIFromPath(e.Pos.Filename))
82	if err != nil {
83		return nil, err
84	}
85	pos := pgf.Tok.Pos(e.Pos.Offset)
86	spn, err := span.NewRange(snapshot.FileSet(), pos, pos).Span()
87	if err != nil {
88		return nil, err
89	}
90	rng, err := spanToRange(pkg, spn)
91	if err != nil {
92		return nil, err
93	}
94	return []*source.Diagnostic{{
95		URI:      spn.URI(),
96		Range:    rng,
97		Severity: protocol.SeverityError,
98		Source:   source.ParseError,
99		Message:  e.Msg,
100	}}, nil
101}
102
103var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`)
104
105func typeErrorDiagnostics(snapshot *snapshot, pkg *pkg, e extendedError) ([]*source.Diagnostic, error) {
106	code, spn, err := typeErrorData(snapshot.FileSet(), pkg, e.primary)
107	if err != nil {
108		return nil, err
109	}
110	rng, err := spanToRange(pkg, spn)
111	if err != nil {
112		return nil, err
113	}
114	diag := &source.Diagnostic{
115		URI:      spn.URI(),
116		Range:    rng,
117		Severity: protocol.SeverityError,
118		Source:   source.TypeError,
119		Message:  e.primary.Msg,
120	}
121	if code != 0 {
122		diag.Code = code.String()
123		diag.CodeHref = typesCodeHref(snapshot, code)
124	}
125
126	for _, secondary := range e.secondaries {
127		_, secondarySpan, err := typeErrorData(snapshot.FileSet(), pkg, secondary)
128		if err != nil {
129			return nil, err
130		}
131		rng, err := spanToRange(pkg, secondarySpan)
132		if err != nil {
133			return nil, err
134		}
135		diag.Related = append(diag.Related, source.RelatedInformation{
136			URI:     secondarySpan.URI(),
137			Range:   rng,
138			Message: secondary.Msg,
139		})
140	}
141
142	if match := importErrorRe.FindStringSubmatch(e.primary.Msg); match != nil {
143		diag.SuggestedFixes, err = goGetQuickFixes(snapshot, spn.URI(), match[1])
144		if err != nil {
145			return nil, err
146		}
147	}
148	return []*source.Diagnostic{diag}, nil
149}
150
151func goGetQuickFixes(snapshot *snapshot, uri span.URI, pkg string) ([]source.SuggestedFix, error) {
152	// Go get only supports module mode for now.
153	if snapshot.workspaceMode()&moduleMode == 0 {
154		return nil, nil
155	}
156	title := fmt.Sprintf("go get package %v", pkg)
157	cmd, err := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{
158		URI:        protocol.URIFromSpanURI(uri),
159		AddRequire: true,
160		Pkg:        pkg,
161	})
162	if err != nil {
163		return nil, err
164	}
165	return []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, nil
166}
167
168func analysisDiagnosticDiagnostics(snapshot *snapshot, pkg *pkg, a *analysis.Analyzer, e *analysis.Diagnostic) ([]*source.Diagnostic, error) {
169	var srcAnalyzer *source.Analyzer
170	// Find the analyzer that generated this diagnostic.
171	for _, sa := range source.EnabledAnalyzers(snapshot) {
172		if a == sa.Analyzer {
173			srcAnalyzer = sa
174			break
175		}
176	}
177
178	spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span()
179	if err != nil {
180		return nil, err
181	}
182	rng, err := spanToRange(pkg, spn)
183	if err != nil {
184		return nil, err
185	}
186	kinds := srcAnalyzer.ActionKind
187	if len(srcAnalyzer.ActionKind) == 0 {
188		kinds = append(kinds, protocol.QuickFix)
189	}
190	fixes, err := suggestedAnalysisFixes(snapshot, pkg, e, kinds)
191	if err != nil {
192		return nil, err
193	}
194	if srcAnalyzer.Fix != "" {
195		cmd, err := command.NewApplyFixCommand(e.Message, command.ApplyFixArgs{
196			URI:   protocol.URIFromSpanURI(spn.URI()),
197			Range: rng,
198			Fix:   srcAnalyzer.Fix,
199		})
200		if err != nil {
201			return nil, err
202		}
203		for _, kind := range kinds {
204			fixes = append(fixes, source.SuggestedFixFromCommand(cmd, kind))
205		}
206	}
207	related, err := relatedInformation(pkg, snapshot.FileSet(), e)
208	if err != nil {
209		return nil, err
210	}
211
212	severity := srcAnalyzer.Severity
213	if severity == 0 {
214		severity = protocol.SeverityWarning
215	}
216	diag := &source.Diagnostic{
217		URI:            spn.URI(),
218		Range:          rng,
219		Severity:       severity,
220		Source:         source.AnalyzerErrorKind(e.Category),
221		Message:        e.Message,
222		Related:        related,
223		SuggestedFixes: fixes,
224		Analyzer:       srcAnalyzer,
225	}
226	// If the fixes only delete code, assume that the diagnostic is reporting dead code.
227	if onlyDeletions(fixes) {
228		diag.Tags = []protocol.DiagnosticTag{protocol.Unnecessary}
229	}
230	return []*source.Diagnostic{diag}, nil
231}
232
233// onlyDeletions returns true if all of the suggested fixes are deletions.
234func onlyDeletions(fixes []source.SuggestedFix) bool {
235	for _, fix := range fixes {
236		for _, edits := range fix.Edits {
237			for _, edit := range edits {
238				if edit.NewText != "" {
239					return false
240				}
241				if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
242					return false
243				}
244			}
245		}
246	}
247	return len(fixes) > 0
248}
249
250func typesCodeHref(snapshot *snapshot, code typesinternal.ErrorCode) string {
251	target := snapshot.View().Options().LinkTarget
252	return source.BuildLink(target, "golang.org/x/tools/internal/typesinternal", code.String())
253}
254
255func suggestedAnalysisFixes(snapshot *snapshot, pkg *pkg, diag *analysis.Diagnostic, kinds []protocol.CodeActionKind) ([]source.SuggestedFix, error) {
256	var fixes []source.SuggestedFix
257	for _, fix := range diag.SuggestedFixes {
258		edits := make(map[span.URI][]protocol.TextEdit)
259		for _, e := range fix.TextEdits {
260			spn, err := span.NewRange(snapshot.FileSet(), e.Pos, e.End).Span()
261			if err != nil {
262				return nil, err
263			}
264			rng, err := spanToRange(pkg, spn)
265			if err != nil {
266				return nil, err
267			}
268			edits[spn.URI()] = append(edits[spn.URI()], protocol.TextEdit{
269				Range:   rng,
270				NewText: string(e.NewText),
271			})
272		}
273		for _, kind := range kinds {
274			fixes = append(fixes, source.SuggestedFix{
275				Title:      fix.Message,
276				Edits:      edits,
277				ActionKind: kind,
278			})
279		}
280
281	}
282	return fixes, nil
283}
284
285func relatedInformation(pkg *pkg, fset *token.FileSet, diag *analysis.Diagnostic) ([]source.RelatedInformation, error) {
286	var out []source.RelatedInformation
287	for _, related := range diag.Related {
288		spn, err := span.NewRange(fset, related.Pos, related.End).Span()
289		if err != nil {
290			return nil, err
291		}
292		rng, err := spanToRange(pkg, spn)
293		if err != nil {
294			return nil, err
295		}
296		out = append(out, source.RelatedInformation{
297			URI:     spn.URI(),
298			Range:   rng,
299			Message: related.Message,
300		})
301	}
302	return out, nil
303}
304
305func typeErrorData(fset *token.FileSet, pkg *pkg, terr types.Error) (typesinternal.ErrorCode, span.Span, error) {
306	ecode, start, end, ok := typesinternal.ReadGo116ErrorData(terr)
307	if !ok {
308		start, end = terr.Pos, terr.Pos
309		ecode = 0
310	}
311	posn := fset.Position(start)
312	pgf, err := pkg.File(span.URIFromPath(posn.Filename))
313	if err != nil {
314		return 0, span.Span{}, err
315	}
316	if !end.IsValid() || end == start {
317		end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start)
318	}
319	spn, err := parsedGoSpan(pgf, start, end)
320	if err != nil {
321		return 0, span.Span{}, err
322	}
323	return ecode, spn, nil
324}
325
326func parsedGoSpan(pgf *source.ParsedGoFile, start, end token.Pos) (span.Span, error) {
327	return span.FileSpan(pgf.Tok, pgf.Mapper.Converter, start, end)
328}
329
330// spanToRange converts a span.Span to a protocol.Range,
331// assuming that the span belongs to the package whose diagnostics are being computed.
332func spanToRange(pkg *pkg, spn span.Span) (protocol.Range, error) {
333	pgf, err := pkg.File(spn.URI())
334	if err != nil {
335		return protocol.Range{}, err
336	}
337	return pgf.Mapper.Range(spn)
338}
339
340// parseGoListError attempts to parse a standard `go list` error message
341// by stripping off the trailing error message.
342//
343// It works only on errors whose message is prefixed by colon,
344// followed by a space (": "). For example:
345//
346//   attributes.go:13:1: expected 'package', found 'type'
347//
348func parseGoListError(input, wd string) span.Span {
349	input = strings.TrimSpace(input)
350	msgIndex := strings.Index(input, ": ")
351	if msgIndex < 0 {
352		return span.Parse(input)
353	}
354	return span.ParseInDir(input[:msgIndex], wd)
355}
356
357func parseGoListImportCycleError(snapshot *snapshot, e packages.Error, pkg *pkg) (string, span.Span, bool) {
358	re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`)
359	matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg))
360	if len(matches) < 3 {
361		return e.Msg, span.Span{}, false
362	}
363	msg := matches[1]
364	importList := strings.Split(matches[2], " ")
365	// Since the error is relative to the current package. The import that is causing
366	// the import cycle error is the second one in the list.
367	if len(importList) < 2 {
368		return msg, span.Span{}, false
369	}
370	// Imports have quotation marks around them.
371	circImp := strconv.Quote(importList[1])
372	for _, cgf := range pkg.compiledGoFiles {
373		// Search file imports for the import that is causing the import cycle.
374		for _, imp := range cgf.File.Imports {
375			if imp.Path.Value == circImp {
376				spn, err := span.NewRange(snapshot.FileSet(), imp.Pos(), imp.End()).Span()
377				if err != nil {
378					return msg, span.Span{}, false
379				}
380				return msg, spn, true
381			}
382		}
383	}
384	return msg, span.Span{}, false
385}
386