github.com/v2fly/tools@v0.100.0/internal/lsp/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 lsp 6 7 import ( 8 "context" 9 "crypto/sha256" 10 "fmt" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 16 "github.com/v2fly/tools/internal/event" 17 "github.com/v2fly/tools/internal/lsp/debug/log" 18 "github.com/v2fly/tools/internal/lsp/debug/tag" 19 "github.com/v2fly/tools/internal/lsp/mod" 20 "github.com/v2fly/tools/internal/lsp/protocol" 21 "github.com/v2fly/tools/internal/lsp/source" 22 "github.com/v2fly/tools/internal/span" 23 "github.com/v2fly/tools/internal/xcontext" 24 errors "golang.org/x/xerrors" 25 ) 26 27 // diagnosticSource differentiates different sources of diagnostics. 28 type diagnosticSource int 29 30 const ( 31 modSource diagnosticSource = iota 32 gcDetailsSource 33 analysisSource 34 typeCheckSource 35 orphanedSource 36 ) 37 38 // A diagnosticReport holds results for a single diagnostic source. 39 type diagnosticReport struct { 40 snapshotID uint64 41 publishedHash string 42 diags map[string]*source.Diagnostic 43 } 44 45 // fileReports holds a collection of diagnostic reports for a single file, as 46 // well as the hash of the last published set of diagnostics. 47 type fileReports struct { 48 snapshotID uint64 49 publishedHash string 50 reports map[diagnosticSource]diagnosticReport 51 } 52 53 func (d diagnosticSource) String() string { 54 switch d { 55 case modSource: 56 return "FromSource" 57 case gcDetailsSource: 58 return "FromGCDetails" 59 case analysisSource: 60 return "FromAnalysis" 61 case typeCheckSource: 62 return "FromTypeChecking" 63 case orphanedSource: 64 return "FromOrphans" 65 default: 66 return fmt.Sprintf("From?%d?", d) 67 } 68 } 69 70 // hashDiagnostics computes a hash to identify diags. 71 func hashDiagnostics(diags ...*source.Diagnostic) string { 72 source.SortDiagnostics(diags) 73 h := sha256.New() 74 for _, d := range diags { 75 for _, t := range d.Tags { 76 fmt.Fprintf(h, "%s", t) 77 } 78 for _, r := range d.Related { 79 fmt.Fprintf(h, "%s%s%s", r.URI, r.Message, r.Range) 80 } 81 fmt.Fprintf(h, "%s%s%s%s", d.Message, d.Range, d.Severity, d.Source) 82 } 83 return fmt.Sprintf("%x", h.Sum(nil)) 84 } 85 86 func (s *Server) diagnoseDetached(snapshot source.Snapshot) { 87 ctx := snapshot.BackgroundContext() 88 ctx = xcontext.Detach(ctx) 89 s.diagnose(ctx, snapshot, false) 90 s.publishDiagnostics(ctx, true, snapshot) 91 } 92 93 func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.URI, onDisk bool) { 94 ctx := snapshot.BackgroundContext() 95 ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", tag.Snapshot.Of(snapshot.ID())) 96 defer done() 97 98 delay := snapshot.View().Options().ExperimentalDiagnosticsDelay 99 if delay > 0 { 100 // Experimental 2-phase diagnostics. 101 // 102 // The first phase just parses and checks packages that have been 103 // affected by file modifications (no analysis). 104 // 105 // The second phase does everything, and is debounced by the configured 106 // delay. 107 s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk) 108 s.publishDiagnostics(ctx, false, snapshot) 109 s.debouncer.debounce(snapshot.View().Name(), snapshot.ID(), delay, func() { 110 s.diagnose(ctx, snapshot, false) 111 s.publishDiagnostics(ctx, true, snapshot) 112 }) 113 return 114 } 115 116 // Ignore possible workspace configuration warnings in the normal flow. 117 s.diagnose(ctx, snapshot, false) 118 s.publishDiagnostics(ctx, true, snapshot) 119 } 120 121 func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snapshot, uris []span.URI, onDisk bool) { 122 ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", tag.Snapshot.Of(snapshot.ID())) 123 defer done() 124 125 packages := make(map[source.Package]struct{}) 126 for _, uri := range uris { 127 // If the change is only on-disk and the file is not open, don't 128 // directly request its package. It may not be a workspace package. 129 if onDisk && !snapshot.IsOpen(uri) { 130 continue 131 } 132 // If the file is not known to the snapshot (e.g., if it was deleted), 133 // don't diagnose it. 134 if snapshot.FindFile(uri) == nil { 135 continue 136 } 137 // Don't call PackagesForFile for builtin.go, as it results in a 138 // command-line-arguments load. 139 if snapshot.IsBuiltin(ctx, uri) { 140 continue 141 } 142 pkgs, err := snapshot.PackagesForFile(ctx, uri, source.TypecheckFull) 143 if err != nil { 144 // TODO (findleyr): we should probably do something with the error here, 145 // but as of now this can fail repeatedly if load fails, so can be too 146 // noisy to log (and we'll handle things later in the slow pass). 147 continue 148 } 149 for _, pkg := range pkgs { 150 packages[pkg] = struct{}{} 151 } 152 } 153 var wg sync.WaitGroup 154 for pkg := range packages { 155 wg.Add(1) 156 157 go func(pkg source.Package) { 158 defer wg.Done() 159 160 s.diagnosePkg(ctx, snapshot, pkg, false) 161 }(pkg) 162 } 163 wg.Wait() 164 } 165 166 // diagnose is a helper function for running diagnostics with a given context. 167 // Do not call it directly. forceAnalysis is only true for testing purposes. 168 func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAnalysis bool) { 169 ctx, done := event.Start(ctx, "Server.diagnose", tag.Snapshot.Of(snapshot.ID())) 170 defer done() 171 172 // Wait for a free diagnostics slot. 173 select { 174 case <-ctx.Done(): 175 return 176 case s.diagnosticsSema <- struct{}{}: 177 } 178 defer func() { 179 <-s.diagnosticsSema 180 }() 181 182 // First, diagnose the go.mod file. 183 modReports, modErr := mod.Diagnostics(ctx, snapshot) 184 if ctx.Err() != nil { 185 log.Trace.Log(ctx, "diagnose cancelled") 186 return 187 } 188 if modErr != nil { 189 event.Error(ctx, "warning: diagnose go.mod", modErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID())) 190 } 191 for id, diags := range modReports { 192 if id.URI == "" { 193 event.Error(ctx, "missing URI for module diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) 194 continue 195 } 196 s.storeDiagnostics(snapshot, id.URI, modSource, diags) 197 } 198 199 // Diagnose all of the packages in the workspace. 200 wsPkgs, err := snapshot.WorkspacePackages(ctx) 201 if s.shouldIgnoreError(ctx, snapshot, err) { 202 return 203 } 204 criticalErr := snapshot.GetCriticalError(ctx) 205 206 // Show the error as a progress error report so that it appears in the 207 // status bar. If a client doesn't support progress reports, the error 208 // will still be shown as a ShowMessage. If there is no error, any running 209 // error progress reports will be closed. 210 s.showCriticalErrorStatus(ctx, snapshot, criticalErr) 211 212 // If there are no workspace packages, there is nothing to diagnose and 213 // there are no orphaned files. 214 if len(wsPkgs) == 0 { 215 return 216 } 217 218 var ( 219 wg sync.WaitGroup 220 seen = map[span.URI]struct{}{} 221 ) 222 for _, pkg := range wsPkgs { 223 wg.Add(1) 224 225 for _, pgf := range pkg.CompiledGoFiles() { 226 seen[pgf.URI] = struct{}{} 227 } 228 229 go func(pkg source.Package) { 230 defer wg.Done() 231 232 s.diagnosePkg(ctx, snapshot, pkg, forceAnalysis) 233 }(pkg) 234 } 235 wg.Wait() 236 237 // Confirm that every opened file belongs to a package (if any exist in 238 // the workspace). Otherwise, add a diagnostic to the file. 239 for _, o := range s.session.Overlays() { 240 if _, ok := seen[o.URI()]; ok { 241 continue 242 } 243 diagnostic := s.checkForOrphanedFile(ctx, snapshot, o) 244 if diagnostic == nil { 245 continue 246 } 247 s.storeDiagnostics(snapshot, o.URI(), orphanedSource, []*source.Diagnostic{diagnostic}) 248 } 249 } 250 251 func (s *Server) diagnosePkg(ctx context.Context, snapshot source.Snapshot, pkg source.Package, alwaysAnalyze bool) { 252 ctx, done := event.Start(ctx, "Server.diagnosePkg", tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) 253 defer done() 254 enableDiagnostics := false 255 includeAnalysis := alwaysAnalyze // only run analyses for packages with open files 256 for _, pgf := range pkg.CompiledGoFiles() { 257 enableDiagnostics = enableDiagnostics || !snapshot.IgnoredFile(pgf.URI) 258 includeAnalysis = includeAnalysis || snapshot.IsOpen(pgf.URI) 259 } 260 // Don't show any diagnostics on ignored files. 261 if !enableDiagnostics { 262 return 263 } 264 265 pkgDiagnostics, err := snapshot.DiagnosePackage(ctx, pkg) 266 if err != nil { 267 event.Error(ctx, "warning: diagnosing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) 268 return 269 } 270 for _, cgf := range pkg.CompiledGoFiles() { 271 s.storeDiagnostics(snapshot, cgf.URI, typeCheckSource, pkgDiagnostics[cgf.URI]) 272 } 273 if includeAnalysis && !pkg.HasListOrParseErrors() { 274 reports, err := source.Analyze(ctx, snapshot, pkg, false) 275 if err != nil { 276 event.Error(ctx, "warning: analyzing package", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) 277 return 278 } 279 for _, cgf := range pkg.CompiledGoFiles() { 280 s.storeDiagnostics(snapshot, cgf.URI, analysisSource, reports[cgf.URI]) 281 } 282 } 283 284 // If gc optimization details are requested, add them to the 285 // diagnostic reports. 286 s.gcOptimizationDetailsMu.Lock() 287 _, enableGCDetails := s.gcOptimizationDetails[pkg.ID()] 288 s.gcOptimizationDetailsMu.Unlock() 289 if enableGCDetails { 290 gcReports, err := source.GCOptimizationDetails(ctx, snapshot, pkg) 291 if err != nil { 292 event.Error(ctx, "warning: gc details", err, tag.Snapshot.Of(snapshot.ID()), tag.Package.Of(pkg.ID())) 293 } 294 s.gcOptimizationDetailsMu.Lock() 295 _, enableGCDetails := s.gcOptimizationDetails[pkg.ID()] 296 297 // NOTE(golang/go#44826): hold the gcOptimizationDetails lock, and re-check 298 // whether gc optimization details are enabled, while storing gc_details 299 // results. This ensures that the toggling of GC details and clearing of 300 // diagnostics does not race with storing the results here. 301 if enableGCDetails { 302 for id, diags := range gcReports { 303 fh := snapshot.FindFile(id.URI) 304 // Don't publish gc details for unsaved buffers, since the underlying 305 // logic operates on the file on disk. 306 if fh == nil || !fh.Saved() { 307 continue 308 } 309 s.storeDiagnostics(snapshot, id.URI, gcDetailsSource, diags) 310 } 311 } 312 s.gcOptimizationDetailsMu.Unlock() 313 } 314 } 315 316 // storeDiagnostics stores results from a single diagnostic source. If merge is 317 // true, it merges results into any existing results for this snapshot. 318 func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsource diagnosticSource, diags []*source.Diagnostic) { 319 // Safeguard: ensure that the file actually exists in the snapshot 320 // (see golang.org/issues/38602). 321 fh := snapshot.FindFile(uri) 322 if fh == nil { 323 return 324 } 325 s.diagnosticsMu.Lock() 326 defer s.diagnosticsMu.Unlock() 327 if s.diagnostics[uri] == nil { 328 s.diagnostics[uri] = &fileReports{ 329 publishedHash: hashDiagnostics(), // Hash for 0 diagnostics. 330 reports: map[diagnosticSource]diagnosticReport{}, 331 } 332 } 333 report := s.diagnostics[uri].reports[dsource] 334 // Don't set obsolete diagnostics. 335 if report.snapshotID > snapshot.ID() { 336 return 337 } 338 if report.diags == nil || report.snapshotID != snapshot.ID() { 339 report.diags = map[string]*source.Diagnostic{} 340 } 341 report.snapshotID = snapshot.ID() 342 for _, d := range diags { 343 report.diags[hashDiagnostics(d)] = d 344 } 345 s.diagnostics[uri].reports[dsource] = report 346 } 347 348 // clearDiagnosticSource clears all diagnostics for a given source type. It is 349 // necessary for cases where diagnostics have been invalidated by something 350 // other than a snapshot change, for example when gc_details is toggled. 351 func (s *Server) clearDiagnosticSource(dsource diagnosticSource) { 352 s.diagnosticsMu.Lock() 353 defer s.diagnosticsMu.Unlock() 354 for _, reports := range s.diagnostics { 355 delete(reports.reports, dsource) 356 } 357 } 358 359 const WorkspaceLoadFailure = "Error loading workspace" 360 361 // showCriticalErrorStatus shows the error as a progress report. 362 // If the error is nil, it clears any existing error progress report. 363 func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Snapshot, err *source.CriticalError) { 364 s.criticalErrorStatusMu.Lock() 365 defer s.criticalErrorStatusMu.Unlock() 366 367 // Remove all newlines so that the error message can be formatted in a 368 // status bar. 369 var errMsg string 370 if err != nil { 371 event.Error(ctx, "errors loading workspace", err.MainError, tag.Snapshot.Of(snapshot.ID()), tag.Directory.Of(snapshot.View().Folder())) 372 for _, d := range err.DiagList { 373 s.storeDiagnostics(snapshot, d.URI, modSource, []*source.Diagnostic{d}) 374 } 375 errMsg = strings.Replace(err.MainError.Error(), "\n", " ", -1) 376 } 377 378 if s.criticalErrorStatus == nil { 379 if errMsg != "" { 380 s.criticalErrorStatus = s.progress.start(ctx, WorkspaceLoadFailure, errMsg, nil, nil) 381 } 382 return 383 } 384 385 // If an error is already shown to the user, update it or mark it as 386 // resolved. 387 if errMsg == "" { 388 s.criticalErrorStatus.end("Done.") 389 s.criticalErrorStatus = nil 390 } else { 391 s.criticalErrorStatus.report(errMsg, 0) 392 } 393 } 394 395 // checkForOrphanedFile checks that the given URIs can be mapped to packages. 396 // If they cannot and the workspace is not otherwise unloaded, it also surfaces 397 // a warning, suggesting that the user check the file for build tags. 398 func (s *Server) checkForOrphanedFile(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle) *source.Diagnostic { 399 if fh.Kind() != source.Go { 400 return nil 401 } 402 // builtin files won't have a package, but they are never orphaned. 403 if snapshot.IsBuiltin(ctx, fh.URI()) { 404 return nil 405 } 406 pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace) 407 if len(pkgs) > 0 || err == nil { 408 return nil 409 } 410 pgf, err := snapshot.ParseGo(ctx, fh, source.ParseHeader) 411 if err != nil { 412 return nil 413 } 414 spn, err := span.NewRange(snapshot.FileSet(), pgf.File.Name.Pos(), pgf.File.Name.End()).Span() 415 if err != nil { 416 return nil 417 } 418 rng, err := pgf.Mapper.Range(spn) 419 if err != nil { 420 return nil 421 } 422 // TODO(rstambler): We should be able to parse the build tags in the 423 // file and show a more specific error message. For now, put the diagnostic 424 // on the package declaration. 425 return &source.Diagnostic{ 426 URI: fh.URI(), 427 Range: rng, 428 Severity: protocol.SeverityWarning, 429 Source: source.ListError, 430 Message: fmt.Sprintf(`No packages found for open file %s: %v. 431 If this file contains build tags, try adding "-tags=<build tag>" to your gopls "buildFlag" configuration (see (https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string). 432 Otherwise, see the troubleshooting guidelines for help investigating (https://github.com/golang/tools/blob/master/gopls/doc/troubleshooting.md). 433 `, fh.URI().Filename(), err), 434 } 435 } 436 437 // publishDiagnostics collects and publishes any unpublished diagnostic reports. 438 func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot source.Snapshot) { 439 ctx, done := event.Start(ctx, "Server.publishDiagnostics", tag.Snapshot.Of(snapshot.ID())) 440 defer done() 441 s.diagnosticsMu.Lock() 442 defer s.diagnosticsMu.Unlock() 443 444 published := 0 445 defer func() { 446 log.Trace.Logf(ctx, "published %d diagnostics", published) 447 }() 448 449 for uri, r := range s.diagnostics { 450 // Snapshot IDs are always increasing, so we use them instead of file 451 // versions to create the correct order for diagnostics. 452 453 // If we've already delivered diagnostics for a future snapshot for this 454 // file, do not deliver them. 455 if r.snapshotID > snapshot.ID() { 456 continue 457 } 458 anyReportsChanged := false 459 reportHashes := map[diagnosticSource]string{} 460 var diags []*source.Diagnostic 461 for dsource, report := range r.reports { 462 if report.snapshotID != snapshot.ID() { 463 continue 464 } 465 var reportDiags []*source.Diagnostic 466 for _, d := range report.diags { 467 diags = append(diags, d) 468 reportDiags = append(reportDiags, d) 469 } 470 hash := hashDiagnostics(reportDiags...) 471 if hash != report.publishedHash { 472 anyReportsChanged = true 473 } 474 reportHashes[dsource] = hash 475 } 476 477 if !final && !anyReportsChanged { 478 // Don't invalidate existing reports on the client if we haven't got any 479 // new information. 480 continue 481 } 482 source.SortDiagnostics(diags) 483 hash := hashDiagnostics(diags...) 484 if hash == r.publishedHash { 485 // Update snapshotID to be the latest snapshot for which this diagnostic 486 // hash is valid. 487 r.snapshotID = snapshot.ID() 488 continue 489 } 490 var version int32 491 if fh := snapshot.FindFile(uri); fh != nil { // file may have been deleted 492 version = fh.Version() 493 } 494 if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ 495 Diagnostics: toProtocolDiagnostics(diags), 496 URI: protocol.URIFromSpanURI(uri), 497 Version: version, 498 }); err == nil { 499 published++ 500 r.publishedHash = hash 501 r.snapshotID = snapshot.ID() 502 for dsource, hash := range reportHashes { 503 report := r.reports[dsource] 504 report.publishedHash = hash 505 r.reports[dsource] = report 506 } 507 } else { 508 if ctx.Err() != nil { 509 // Publish may have failed due to a cancelled context. 510 log.Trace.Log(ctx, "publish cancelled") 511 return 512 } 513 event.Error(ctx, "publishReports: failed to deliver diagnostic", err, tag.URI.Of(uri)) 514 } 515 } 516 } 517 518 func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnostic { 519 reports := []protocol.Diagnostic{} 520 for _, diag := range diagnostics { 521 related := make([]protocol.DiagnosticRelatedInformation, 0, len(diag.Related)) 522 for _, rel := range diag.Related { 523 related = append(related, protocol.DiagnosticRelatedInformation{ 524 Location: protocol.Location{ 525 URI: protocol.URIFromSpanURI(rel.URI), 526 Range: rel.Range, 527 }, 528 Message: rel.Message, 529 }) 530 } 531 pdiag := protocol.Diagnostic{ 532 // diag.Message might start with \n or \t 533 Message: strings.TrimSpace(diag.Message), 534 Range: diag.Range, 535 Severity: diag.Severity, 536 Source: string(diag.Source), 537 Tags: diag.Tags, 538 RelatedInformation: related, 539 } 540 if diag.Code != "" { 541 pdiag.Code = diag.Code 542 } 543 if diag.CodeHref != "" { 544 pdiag.CodeDescription = &protocol.CodeDescription{Href: diag.CodeHref} 545 } 546 reports = append(reports, pdiag) 547 } 548 return reports 549 } 550 551 func (s *Server) shouldIgnoreError(ctx context.Context, snapshot source.Snapshot, err error) bool { 552 if err == nil { // if there is no error at all 553 return false 554 } 555 if errors.Is(err, context.Canceled) { 556 return true 557 } 558 // If the folder has no Go code in it, we shouldn't spam the user with a warning. 559 var hasGo bool 560 _ = filepath.Walk(snapshot.View().Folder().Filename(), func(path string, info os.FileInfo, err error) error { 561 if err != nil { 562 return err 563 } 564 if !strings.HasSuffix(info.Name(), ".go") { 565 return nil 566 } 567 hasGo = true 568 return errors.New("done") 569 }) 570 return !hasGo 571 } 572 573 // Diagnostics formattedfor the debug server 574 // (all the relevant fields of Server are private) 575 // (The alternative is to export them) 576 func (s *Server) Diagnostics() map[string][]string { 577 ans := make(map[string][]string) 578 s.diagnosticsMu.Lock() 579 defer s.diagnosticsMu.Unlock() 580 for k, v := range s.diagnostics { 581 fn := k.Filename() 582 for typ, d := range v.reports { 583 if len(d.diags) == 0 { 584 continue 585 } 586 for _, dx := range d.diags { 587 ans[fn] = append(ans[fn], auxStr(dx, d, typ)) 588 } 589 } 590 } 591 return ans 592 } 593 594 func auxStr(v *source.Diagnostic, d diagnosticReport, typ diagnosticSource) string { 595 // Tags? RelatedInformation? 596 msg := fmt.Sprintf("(%s)%q(source:%q,code:%q,severity:%s,snapshot:%d,type:%s)", 597 v.Range, v.Message, v.Source, v.Code, v.Severity, d.snapshotID, typ) 598 for _, r := range v.Related { 599 msg += fmt.Sprintf(" [%s:%s,%q]", r.URI.Filename(), r.Range, r.Message) 600 } 601 return msg 602 }