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