blob: 1dd937a543805e76dec03da682acedb2596f7a65 [file] [log] [blame]
// Copyright 2018 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 source
import (
"context"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
)
type Diagnostic struct {
Range protocol.Range
Message string
Source string
Code string
CodeHref string
Severity protocol.DiagnosticSeverity
Tags []protocol.DiagnosticTag
Related []RelatedInformation
}
type SuggestedFix struct {
Title string
Edits map[span.URI][]protocol.TextEdit
Command *protocol.Command
}
type RelatedInformation struct {
URI span.URI
Range protocol.Range
Message string
}
func GetTypeCheckDiagnostics(ctx context.Context, snapshot Snapshot, pkg Package) TypeCheckDiagnostics {
onlyIgnoredFiles := true
for _, pgf := range pkg.CompiledGoFiles() {
onlyIgnoredFiles = onlyIgnoredFiles && snapshot.IgnoredFile(pgf.URI)
}
if onlyIgnoredFiles {
return TypeCheckDiagnostics{}
}
// Prepare any additional reports for the errors in this package.
for _, e := range pkg.GetErrors() {
// We only need to handle lower-level errors.
if e.Kind != ListError {
continue
}
// If no file is associated with the error, pick an open file from the package.
if e.URI.Filename() == "" {
for _, pgf := range pkg.CompiledGoFiles() {
if snapshot.IsOpen(pgf.URI) {
e.URI = pgf.URI
}
}
}
}
return typeCheckDiagnostics(ctx, snapshot, pkg)
}
func Analyze(ctx context.Context, snapshot Snapshot, pkg Package, typeCheckResult TypeCheckDiagnostics) (map[span.URI][]*Diagnostic, error) {
// Exit early if the context has been canceled. This also protects us
// from a race on Options, see golang/go#36699.
if ctx.Err() != nil {
return nil, ctx.Err()
}
// If we don't have any list or parse errors, run analyses.
analyzers := pickAnalyzers(snapshot, typeCheckResult.HasTypeErrors)
analysisErrors, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...)
if err != nil {
return nil, err
}
reports := emptyDiagnostics(pkg)
// Report diagnostics and errors from root analyzers.
for _, e := range analysisErrors {
// If the diagnostic comes from a "convenience" analyzer, it is not
// meant to provide diagnostics, but rather only suggested fixes.
// Skip these types of errors in diagnostics; we will use their
// suggested fixes when providing code actions.
if isConvenienceAnalyzer(e.Category) {
continue
}
// This is a bit of a hack, but clients > 3.15 will be able to grey out unnecessary code.
// If we are deleting code as part of all of our suggested fixes, assume that this is dead code.
// TODO(golang/go#34508): Return these codes from the diagnostics themselves.
var tags []protocol.DiagnosticTag
if onlyDeletions(e.SuggestedFixes) {
tags = append(tags, protocol.Unnecessary)
}
// Type error analyzers only alter the tags for existing type errors.
if _, ok := snapshot.View().Options().TypeErrorAnalyzers[e.Category]; ok {
existingDiagnostics := typeCheckResult.Diagnostics[e.URI]
for _, d := range existingDiagnostics {
if r := protocol.CompareRange(e.Range, d.Range); r != 0 {
continue
}
if e.Message != d.Message {
continue
}
d.Tags = append(d.Tags, tags...)
}
} else {
reports[e.URI] = append(reports[e.URI], &Diagnostic{
Range: e.Range,
Message: e.Message,
Source: e.Category,
Severity: protocol.SeverityWarning,
Tags: tags,
Related: e.Related,
})
}
}
return reports, nil
}
func pickAnalyzers(snapshot Snapshot, hadTypeErrors bool) []*analysis.Analyzer {
// Always run convenience analyzers.
categories := []map[string]Analyzer{snapshot.View().Options().ConvenienceAnalyzers}
// If we had type errors, only run type error analyzers.
if hadTypeErrors {
categories = append(categories, snapshot.View().Options().TypeErrorAnalyzers)
} else {
categories = append(categories, snapshot.View().Options().DefaultAnalyzers, snapshot.View().Options().StaticcheckAnalyzers)
}
var analyzers []*analysis.Analyzer
for _, m := range categories {
for _, a := range m {
if a.IsEnabled(snapshot.View()) {
analyzers = append(analyzers, a.Analyzer)
}
}
}
return analyzers
}
func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (VersionedFileIdentity, []*Diagnostic, error) {
fh, err := snapshot.GetVersionedFile(ctx, uri)
if err != nil {
return VersionedFileIdentity{}, nil, err
}
pkg, _, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
if err != nil {
return VersionedFileIdentity{}, nil, err
}
typeCheckResults := GetTypeCheckDiagnostics(ctx, snapshot, pkg)
diagnostics := typeCheckResults.Diagnostics[fh.URI()]
if !typeCheckResults.HasParseOrListErrors {
reports, err := Analyze(ctx, snapshot, pkg, typeCheckResults)
if err != nil {
return VersionedFileIdentity{}, nil, err
}
diagnostics = append(diagnostics, reports[fh.URI()]...)
}
return fh.VersionedFileIdentity(), diagnostics, nil
}
type TypeCheckDiagnostics struct {
HasTypeErrors bool
HasParseOrListErrors bool
Diagnostics map[span.URI][]*Diagnostic
}
type diagnosticSet struct {
listErrors, parseErrors, typeErrors []*Diagnostic
}
func typeCheckDiagnostics(ctx context.Context, snapshot Snapshot, pkg Package) TypeCheckDiagnostics {
ctx, done := event.Start(ctx, "source.diagnostics", tag.Package.Of(pkg.ID()))
_ = ctx // circumvent SA4006
defer done()
diagSets := make(map[span.URI]*diagnosticSet)
for _, e := range pkg.GetErrors() {
diag := &Diagnostic{
Message: e.Message,
Range: e.Range,
Severity: protocol.SeverityError,
Related: e.Related,
}
set, ok := diagSets[e.URI]
if !ok {
set = &diagnosticSet{}
diagSets[e.URI] = set
}
switch e.Kind {
case ParseError:
set.parseErrors = append(set.parseErrors, diag)
diag.Source = "syntax"
case TypeError:
set.typeErrors = append(set.typeErrors, diag)
diag.Source = "compiler"
case ListError:
set.listErrors = append(set.listErrors, diag)
diag.Source = "go list"
}
}
typecheck := TypeCheckDiagnostics{
Diagnostics: emptyDiagnostics(pkg),
}
for uri, set := range diagSets {
// Don't report type errors if there are parse errors or list errors.
diags := set.typeErrors
switch {
case len(set.parseErrors) > 0:
typecheck.HasParseOrListErrors = true
diags = set.parseErrors
case len(set.listErrors) > 0:
typecheck.HasParseOrListErrors = true
if len(pkg.MissingDependencies()) > 0 {
diags = set.listErrors
}
case len(set.typeErrors) > 0:
typecheck.HasTypeErrors = true
}
typecheck.Diagnostics[uri] = diags
}
return typecheck
}
func emptyDiagnostics(pkg Package) map[span.URI][]*Diagnostic {
diags := map[span.URI][]*Diagnostic{}
for _, pgf := range pkg.CompiledGoFiles() {
if _, ok := diags[pgf.URI]; !ok {
diags[pgf.URI] = nil
}
}
return diags
}
// onlyDeletions returns true if all of the suggested fixes are deletions.
func onlyDeletions(fixes []SuggestedFix) bool {
for _, fix := range fixes {
for _, edits := range fix.Edits {
for _, edit := range edits {
if edit.NewText != "" {
return false
}
if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
return false
}
}
}
}
return len(fixes) > 0
}
func isConvenienceAnalyzer(category string) bool {
for _, a := range DefaultOptions().ConvenienceAnalyzers {
if category == a.Analyzer.Name {
return true
}
}
return false
}