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 }