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 5package lsp 6 7import ( 8 "context" 9 "strings" 10 11 "golang.org/x/tools/internal/lsp/protocol" 12 "golang.org/x/tools/internal/lsp/source" 13 "golang.org/x/tools/internal/lsp/telemetry" 14 "golang.org/x/tools/internal/telemetry/log" 15 "golang.org/x/tools/internal/telemetry/trace" 16) 17 18func (s *Server) diagnose(snapshot source.Snapshot, f source.File) error { 19 switch f.Kind() { 20 case source.Go: 21 go s.diagnoseFile(snapshot, f) 22 case source.Mod: 23 go s.diagnoseSnapshot(snapshot) 24 } 25 return nil 26} 27 28func (s *Server) diagnoseSnapshot(snapshot source.Snapshot) { 29 ctx := snapshot.View().BackgroundContext() 30 ctx, done := trace.StartSpan(ctx, "lsp:background-worker") 31 defer done() 32 33 for _, id := range snapshot.WorkspacePackageIDs(ctx) { 34 ph, err := snapshot.PackageHandle(ctx, id) 35 if err != nil { 36 log.Error(ctx, "diagnoseSnapshot: no PackageHandle for workspace package", err, telemetry.Package.Of(id)) 37 continue 38 } 39 if len(ph.CompiledGoFiles()) == 0 { 40 continue 41 } 42 // Find a file on which to call diagnostics. 43 uri := ph.CompiledGoFiles()[0].File().Identity().URI 44 f, err := snapshot.View().GetFile(ctx, uri) 45 if err != nil { 46 log.Error(ctx, "no file", err, telemetry.URI.Of(uri)) 47 continue 48 } 49 // Run diagnostics on the workspace package. 50 go func(snapshot source.Snapshot, f source.File) { 51 reports, _, err := source.Diagnostics(ctx, snapshot, f, false, snapshot.View().Options().DisabledAnalyses) 52 if err != nil { 53 log.Error(ctx, "no diagnostics", err, telemetry.URI.Of(f.URI())) 54 return 55 } 56 // Don't publish empty diagnostics. 57 s.publishReports(ctx, reports, false) 58 }(snapshot, f) 59 } 60} 61 62func (s *Server) diagnoseFile(snapshot source.Snapshot, f source.File) { 63 ctx := snapshot.View().BackgroundContext() 64 ctx, done := trace.StartSpan(ctx, "lsp:background-worker") 65 defer done() 66 67 ctx = telemetry.File.With(ctx, f.URI()) 68 69 reports, warningMsg, err := source.Diagnostics(ctx, snapshot, f, true, snapshot.View().Options().DisabledAnalyses) 70 // Check the warning message first. 71 if warningMsg != "" { 72 s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 73 Type: protocol.Info, 74 Message: warningMsg, 75 }) 76 } 77 if err != nil { 78 if err != context.Canceled { 79 log.Error(ctx, "diagnoseFile: could not generate diagnostics", err) 80 } 81 return 82 } 83 // Publish empty diagnostics for files. 84 s.publishReports(ctx, reports, true) 85} 86 87func (s *Server) publishReports(ctx context.Context, reports map[source.FileIdentity][]source.Diagnostic, publishEmpty bool) { 88 // Check for context cancellation before publishing diagnostics. 89 if ctx.Err() != nil { 90 return 91 } 92 93 s.deliveredMu.Lock() 94 defer s.deliveredMu.Unlock() 95 96 for fileID, diagnostics := range reports { 97 // Don't deliver diagnostics if the context has already been canceled. 98 if ctx.Err() != nil { 99 break 100 } 101 // Don't publish empty diagnostics unless specified. 102 if len(diagnostics) == 0 && !publishEmpty { 103 continue 104 } 105 // Pre-sort diagnostics to avoid extra work when we compare them. 106 source.SortDiagnostics(diagnostics) 107 toSend := sentDiagnostics{ 108 version: fileID.Version, 109 identifier: fileID.Identifier, 110 sorted: diagnostics, 111 } 112 113 if delivered, ok := s.delivered[fileID.URI]; ok { 114 // We only reuse cached diagnostics in two cases: 115 // 1. This file is at a greater version than that of the previously sent diagnostics. 116 // 2. There are no known versions for the file. 117 greaterVersion := fileID.Version > delivered.version && delivered.version > 0 118 noVersions := (fileID.Version == 0 && delivered.version == 0) && delivered.identifier == fileID.Identifier 119 if (greaterVersion || noVersions) && equalDiagnostics(delivered.sorted, diagnostics) { 120 // Update the delivered map even if we reuse cached diagnostics. 121 s.delivered[fileID.URI] = toSend 122 continue 123 } 124 } 125 if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ 126 Diagnostics: toProtocolDiagnostics(ctx, diagnostics), 127 URI: protocol.NewURI(fileID.URI), 128 Version: fileID.Version, 129 }); err != nil { 130 log.Error(ctx, "failed to deliver diagnostic", err, telemetry.File) 131 continue 132 } 133 // Update the delivered map. 134 s.delivered[fileID.URI] = toSend 135 } 136} 137 138// equalDiagnostics returns true if the 2 lists of diagnostics are equal. 139// It assumes that both a and b are already sorted. 140func equalDiagnostics(a, b []source.Diagnostic) bool { 141 if len(a) != len(b) { 142 return false 143 } 144 for i := 0; i < len(a); i++ { 145 if source.CompareDiagnostic(a[i], b[i]) != 0 { 146 return false 147 } 148 } 149 return true 150} 151 152func toProtocolDiagnostics(ctx context.Context, diagnostics []source.Diagnostic) []protocol.Diagnostic { 153 reports := []protocol.Diagnostic{} 154 for _, diag := range diagnostics { 155 related := make([]protocol.DiagnosticRelatedInformation, 0, len(diag.Related)) 156 for _, rel := range diag.Related { 157 related = append(related, protocol.DiagnosticRelatedInformation{ 158 Location: protocol.Location{ 159 URI: protocol.NewURI(rel.URI), 160 Range: rel.Range, 161 }, 162 Message: rel.Message, 163 }) 164 } 165 reports = append(reports, protocol.Diagnostic{ 166 Message: strings.TrimSpace(diag.Message), // go list returns errors prefixed by newline 167 Range: diag.Range, 168 Severity: diag.Severity, 169 Source: diag.Source, 170 Tags: diag.Tags, 171 RelatedInformation: related, 172 }) 173 } 174 return reports 175} 176