github.com/jd-ly/tools@v0.5.7/internal/lsp/source/diagnostics.go (about)

     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  
     5  package source
     6  
     7  import (
     8  	"context"
     9  
    10  	"github.com/jd-ly/tools/go/analysis"
    11  	"github.com/jd-ly/tools/internal/event"
    12  	"github.com/jd-ly/tools/internal/lsp/debug/tag"
    13  	"github.com/jd-ly/tools/internal/lsp/protocol"
    14  	"github.com/jd-ly/tools/internal/span"
    15  )
    16  
    17  type Diagnostic struct {
    18  	Range    protocol.Range
    19  	Message  string
    20  	Source   string
    21  	Severity protocol.DiagnosticSeverity
    22  	Tags     []protocol.DiagnosticTag
    23  
    24  	Related []RelatedInformation
    25  }
    26  
    27  type SuggestedFix struct {
    28  	Title   string
    29  	Edits   map[span.URI][]protocol.TextEdit
    30  	Command *protocol.Command
    31  }
    32  
    33  type RelatedInformation struct {
    34  	URI     span.URI
    35  	Range   protocol.Range
    36  	Message string
    37  }
    38  
    39  func GetTypeCheckDiagnostics(ctx context.Context, snapshot Snapshot, pkg Package) TypeCheckDiagnostics {
    40  	onlyIgnoredFiles := true
    41  	for _, pgf := range pkg.CompiledGoFiles() {
    42  		onlyIgnoredFiles = onlyIgnoredFiles && snapshot.IgnoredFile(pgf.URI)
    43  	}
    44  	if onlyIgnoredFiles {
    45  		return TypeCheckDiagnostics{}
    46  	}
    47  
    48  	// Prepare any additional reports for the errors in this package.
    49  	for _, e := range pkg.GetErrors() {
    50  		// We only need to handle lower-level errors.
    51  		if e.Kind != ListError {
    52  			continue
    53  		}
    54  		// If no file is associated with the error, pick an open file from the package.
    55  		if e.URI.Filename() == "" {
    56  			for _, pgf := range pkg.CompiledGoFiles() {
    57  				if snapshot.IsOpen(pgf.URI) {
    58  					e.URI = pgf.URI
    59  				}
    60  			}
    61  		}
    62  	}
    63  	return typeCheckDiagnostics(ctx, snapshot, pkg)
    64  }
    65  
    66  func Analyze(ctx context.Context, snapshot Snapshot, pkg Package, typeCheckResult TypeCheckDiagnostics) (map[span.URI][]*Diagnostic, error) {
    67  	// Exit early if the context has been canceled. This also protects us
    68  	// from a race on Options, see golang/go#36699.
    69  	if ctx.Err() != nil {
    70  		return nil, ctx.Err()
    71  	}
    72  	// If we don't have any list or parse errors, run analyses.
    73  	analyzers := pickAnalyzers(snapshot, typeCheckResult.HasTypeErrors)
    74  	analysisErrors, err := snapshot.Analyze(ctx, pkg.ID(), analyzers...)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	reports := emptyDiagnostics(pkg)
    80  	// Report diagnostics and errors from root analyzers.
    81  	for _, e := range analysisErrors {
    82  		// If the diagnostic comes from a "convenience" analyzer, it is not
    83  		// meant to provide diagnostics, but rather only suggested fixes.
    84  		// Skip these types of errors in diagnostics; we will use their
    85  		// suggested fixes when providing code actions.
    86  		if isConvenienceAnalyzer(e.Category) {
    87  			continue
    88  		}
    89  		// This is a bit of a hack, but clients > 3.15 will be able to grey out unnecessary code.
    90  		// If we are deleting code as part of all of our suggested fixes, assume that this is dead code.
    91  		// TODO(golang/go#34508): Return these codes from the diagnostics themselves.
    92  		var tags []protocol.DiagnosticTag
    93  		if onlyDeletions(e.SuggestedFixes) {
    94  			tags = append(tags, protocol.Unnecessary)
    95  		}
    96  		// Type error analyzers only alter the tags for existing type errors.
    97  		if _, ok := snapshot.View().Options().TypeErrorAnalyzers[e.Category]; ok {
    98  			existingDiagnostics := typeCheckResult.Diagnostics[e.URI]
    99  			for _, d := range existingDiagnostics {
   100  				if r := protocol.CompareRange(e.Range, d.Range); r != 0 {
   101  					continue
   102  				}
   103  				if e.Message != d.Message {
   104  					continue
   105  				}
   106  				d.Tags = append(d.Tags, tags...)
   107  			}
   108  		} else {
   109  			reports[e.URI] = append(reports[e.URI], &Diagnostic{
   110  				Range:    e.Range,
   111  				Message:  e.Message,
   112  				Source:   e.Category,
   113  				Severity: protocol.SeverityWarning,
   114  				Tags:     tags,
   115  				Related:  e.Related,
   116  			})
   117  		}
   118  	}
   119  	return reports, nil
   120  }
   121  
   122  func pickAnalyzers(snapshot Snapshot, hadTypeErrors bool) []*analysis.Analyzer {
   123  	// Always run convenience analyzers.
   124  	categories := []map[string]Analyzer{snapshot.View().Options().ConvenienceAnalyzers}
   125  	// If we had type errors, only run type error analyzers.
   126  	if hadTypeErrors {
   127  		categories = append(categories, snapshot.View().Options().TypeErrorAnalyzers)
   128  	} else {
   129  		categories = append(categories, snapshot.View().Options().DefaultAnalyzers, snapshot.View().Options().StaticcheckAnalyzers)
   130  	}
   131  	var analyzers []*analysis.Analyzer
   132  	for _, m := range categories {
   133  		for _, a := range m {
   134  			if a.IsEnabled(snapshot.View()) {
   135  				analyzers = append(analyzers, a.Analyzer)
   136  			}
   137  		}
   138  	}
   139  	return analyzers
   140  }
   141  
   142  func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (VersionedFileIdentity, []*Diagnostic, error) {
   143  	fh, err := snapshot.GetVersionedFile(ctx, uri)
   144  	if err != nil {
   145  		return VersionedFileIdentity{}, nil, err
   146  	}
   147  	pkg, _, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
   148  	if err != nil {
   149  		return VersionedFileIdentity{}, nil, err
   150  	}
   151  	typeCheckResults := GetTypeCheckDiagnostics(ctx, snapshot, pkg)
   152  	diagnostics := typeCheckResults.Diagnostics[fh.URI()]
   153  	if !typeCheckResults.HasParseOrListErrors {
   154  		reports, err := Analyze(ctx, snapshot, pkg, typeCheckResults)
   155  		if err != nil {
   156  			return VersionedFileIdentity{}, nil, err
   157  		}
   158  		diagnostics = append(diagnostics, reports[fh.URI()]...)
   159  	}
   160  	return fh.VersionedFileIdentity(), diagnostics, nil
   161  }
   162  
   163  type TypeCheckDiagnostics struct {
   164  	HasTypeErrors        bool
   165  	HasParseOrListErrors bool
   166  	Diagnostics          map[span.URI][]*Diagnostic
   167  }
   168  
   169  type diagnosticSet struct {
   170  	listErrors, parseErrors, typeErrors []*Diagnostic
   171  }
   172  
   173  func typeCheckDiagnostics(ctx context.Context, snapshot Snapshot, pkg Package) TypeCheckDiagnostics {
   174  	ctx, done := event.Start(ctx, "source.diagnostics", tag.Package.Of(pkg.ID()))
   175  	_ = ctx // circumvent SA4006
   176  	defer done()
   177  
   178  	diagSets := make(map[span.URI]*diagnosticSet)
   179  	for _, e := range pkg.GetErrors() {
   180  		diag := &Diagnostic{
   181  			Message:  e.Message,
   182  			Range:    e.Range,
   183  			Severity: protocol.SeverityError,
   184  			Related:  e.Related,
   185  		}
   186  		set, ok := diagSets[e.URI]
   187  		if !ok {
   188  			set = &diagnosticSet{}
   189  			diagSets[e.URI] = set
   190  		}
   191  		switch e.Kind {
   192  		case ParseError:
   193  			set.parseErrors = append(set.parseErrors, diag)
   194  			diag.Source = "syntax"
   195  		case TypeError:
   196  			set.typeErrors = append(set.typeErrors, diag)
   197  			diag.Source = "compiler"
   198  		case ListError:
   199  			set.listErrors = append(set.listErrors, diag)
   200  			diag.Source = "go list"
   201  		}
   202  	}
   203  	typecheck := TypeCheckDiagnostics{
   204  		Diagnostics: emptyDiagnostics(pkg),
   205  	}
   206  	for uri, set := range diagSets {
   207  		// Don't report type errors if there are parse errors or list errors.
   208  		diags := set.typeErrors
   209  		switch {
   210  		case len(set.parseErrors) > 0:
   211  			typecheck.HasParseOrListErrors = true
   212  			diags = set.parseErrors
   213  		case len(set.listErrors) > 0:
   214  			typecheck.HasParseOrListErrors = true
   215  			if len(pkg.MissingDependencies()) > 0 {
   216  				diags = set.listErrors
   217  			}
   218  		case len(set.typeErrors) > 0:
   219  			typecheck.HasTypeErrors = true
   220  		}
   221  		typecheck.Diagnostics[uri] = diags
   222  	}
   223  	return typecheck
   224  }
   225  
   226  func emptyDiagnostics(pkg Package) map[span.URI][]*Diagnostic {
   227  	diags := map[span.URI][]*Diagnostic{}
   228  	for _, pgf := range pkg.CompiledGoFiles() {
   229  		if _, ok := diags[pgf.URI]; !ok {
   230  			diags[pgf.URI] = nil
   231  		}
   232  	}
   233  	return diags
   234  }
   235  
   236  // onlyDeletions returns true if all of the suggested fixes are deletions.
   237  func onlyDeletions(fixes []SuggestedFix) bool {
   238  	for _, fix := range fixes {
   239  		for _, edits := range fix.Edits {
   240  			for _, edit := range edits {
   241  				if edit.NewText != "" {
   242  					return false
   243  				}
   244  				if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 {
   245  					return false
   246  				}
   247  			}
   248  		}
   249  	}
   250  	return len(fixes) > 0
   251  }
   252  
   253  func isConvenienceAnalyzer(category string) bool {
   254  	for _, a := range DefaultOptions().ConvenienceAnalyzers {
   255  		if category == a.Analyzer.Name {
   256  			return true
   257  		}
   258  	}
   259  	return false
   260  }