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 source 6 7import ( 8 "context" 9 "fmt" 10 11 "golang.org/x/tools/go/analysis" 12 "golang.org/x/tools/internal/lsp/protocol" 13 "golang.org/x/tools/internal/lsp/telemetry" 14 "golang.org/x/tools/internal/span" 15 "golang.org/x/tools/internal/telemetry/log" 16 "golang.org/x/tools/internal/telemetry/trace" 17 errors "golang.org/x/xerrors" 18) 19 20type Diagnostic struct { 21 Range protocol.Range 22 Message string 23 Source string 24 Severity protocol.DiagnosticSeverity 25 Tags []protocol.DiagnosticTag 26 27 SuggestedFixes []SuggestedFix 28 Related []RelatedInformation 29} 30 31type SuggestedFix struct { 32 Title string 33 Edits map[span.URI][]protocol.TextEdit 34} 35 36type RelatedInformation struct { 37 URI span.URI 38 Range protocol.Range 39 Message string 40} 41 42func Diagnostics(ctx context.Context, snapshot Snapshot, f File, withAnalysis bool, disabledAnalyses map[string]struct{}) (map[FileIdentity][]Diagnostic, string, error) { 43 ctx, done := trace.StartSpan(ctx, "source.Diagnostics", telemetry.File.Of(f.URI())) 44 defer done() 45 46 fh := snapshot.Handle(ctx, f) 47 phs, err := snapshot.PackageHandles(ctx, fh) 48 if err != nil { 49 return nil, "", err 50 } 51 ph, err := WidestCheckPackageHandle(phs) 52 if err != nil { 53 return nil, "", err 54 } 55 // If we are missing dependencies, it may because the user's workspace is 56 // not correctly configured. Report errors, if possible. 57 var warningMsg string 58 if len(ph.MissingDependencies()) > 0 { 59 if warningMsg, err = checkCommonErrors(ctx, snapshot.View(), f.URI()); err != nil { 60 log.Error(ctx, "error checking common errors", err, telemetry.File.Of(f.URI)) 61 } 62 } 63 pkg, err := ph.Check(ctx) 64 if err != nil { 65 return nil, "", err 66 } 67 // Prepare the reports we will send for the files in this package. 68 reports := make(map[FileIdentity][]Diagnostic) 69 for _, fh := range pkg.CompiledGoFiles() { 70 clearReports(snapshot, reports, fh.File().Identity()) 71 } 72 // Prepare any additional reports for the errors in this package. 73 for _, e := range pkg.GetErrors() { 74 // We only need to handle lower-level errors. 75 if !(e.Kind == UnknownError || e.Kind == ListError) { 76 continue 77 } 78 // If no file is associated with the error, default to the current file. 79 if e.File.URI.Filename() == "" { 80 e.File = fh.Identity() 81 } 82 clearReports(snapshot, reports, e.File) 83 } 84 // Run diagnostics for the package that this URI belongs to. 85 if !diagnostics(ctx, snapshot, pkg, reports) && withAnalysis { 86 // If we don't have any list, parse, or type errors, run analyses. 87 if err := analyses(ctx, snapshot, ph, disabledAnalyses, reports); err != nil { 88 // Exit early if the context has been canceled. 89 if err == context.Canceled { 90 return nil, "", err 91 } 92 log.Error(ctx, "failed to run analyses", err, telemetry.File.Of(f.URI())) 93 } 94 } 95 // Updates to the diagnostics for this package may need to be propagated. 96 for _, id := range snapshot.GetReverseDependencies(pkg.ID()) { 97 ph, err := snapshot.PackageHandle(ctx, id) 98 if err != nil { 99 return nil, warningMsg, err 100 } 101 pkg, err := ph.Check(ctx) 102 if err != nil { 103 return nil, warningMsg, err 104 } 105 for _, fh := range pkg.CompiledGoFiles() { 106 clearReports(snapshot, reports, fh.File().Identity()) 107 } 108 diagnostics(ctx, snapshot, pkg, reports) 109 } 110 return reports, warningMsg, nil 111} 112 113type diagnosticSet struct { 114 listErrors, parseErrors, typeErrors []*Diagnostic 115} 116 117func diagnostics(ctx context.Context, snapshot Snapshot, pkg Package, reports map[FileIdentity][]Diagnostic) bool { 118 ctx, done := trace.StartSpan(ctx, "source.diagnostics", telemetry.Package.Of(pkg.ID())) 119 _ = ctx // circumvent SA4006 120 defer done() 121 122 diagSets := make(map[FileIdentity]*diagnosticSet) 123 for _, e := range pkg.GetErrors() { 124 diag := &Diagnostic{ 125 Message: e.Message, 126 Range: e.Range, 127 Severity: protocol.SeverityError, 128 } 129 set, ok := diagSets[e.File] 130 if !ok { 131 set = &diagnosticSet{} 132 diagSets[e.File] = set 133 } 134 switch e.Kind { 135 case ParseError: 136 set.parseErrors = append(set.parseErrors, diag) 137 diag.Source = "syntax" 138 case TypeError: 139 set.typeErrors = append(set.typeErrors, diag) 140 diag.Source = "compiler" 141 case ListError, UnknownError: 142 set.listErrors = append(set.listErrors, diag) 143 diag.Source = "go list" 144 } 145 } 146 var nonEmptyDiagnostics bool // track if we actually send non-empty diagnostics 147 for fileID, set := range diagSets { 148 // Don't report type errors if there are parse errors or list errors. 149 diags := set.typeErrors 150 if len(set.parseErrors) > 0 { 151 diags = set.parseErrors 152 } else if len(set.listErrors) > 0 { 153 diags = set.listErrors 154 } 155 if len(diags) > 0 { 156 nonEmptyDiagnostics = true 157 } 158 addReports(ctx, reports, snapshot, fileID, diags...) 159 } 160 return nonEmptyDiagnostics 161} 162 163func analyses(ctx context.Context, snapshot Snapshot, ph PackageHandle, disabledAnalyses map[string]struct{}, reports map[FileIdentity][]Diagnostic) error { 164 var analyzers []*analysis.Analyzer 165 for _, a := range snapshot.View().Options().Analyzers { 166 if _, ok := disabledAnalyses[a.Name]; ok { 167 continue 168 } 169 analyzers = append(analyzers, a) 170 } 171 172 diagnostics, err := snapshot.Analyze(ctx, ph.ID(), analyzers) 173 if err != nil { 174 return err 175 } 176 177 // Report diagnostics and errors from root analyzers. 178 for _, e := range diagnostics { 179 // This is a bit of a hack, but clients > 3.15 will be able to grey out unnecessary code. 180 // If we are deleting code as part of all of our suggested fixes, assume that this is dead code. 181 // TODO(golang/go/#34508): Return these codes from the diagnostics themselves. 182 var tags []protocol.DiagnosticTag 183 if onlyDeletions(e.SuggestedFixes) { 184 tags = append(tags, protocol.Unnecessary) 185 } 186 addReports(ctx, reports, snapshot, e.File, &Diagnostic{ 187 Range: e.Range, 188 Message: e.Message, 189 Source: e.Category, 190 Severity: protocol.SeverityWarning, 191 Tags: tags, 192 SuggestedFixes: e.SuggestedFixes, 193 Related: e.Related, 194 }) 195 } 196 return nil 197} 198 199func clearReports(snapshot Snapshot, reports map[FileIdentity][]Diagnostic, fileID FileIdentity) { 200 if snapshot.View().Ignore(fileID.URI) { 201 return 202 } 203 reports[fileID] = []Diagnostic{} 204} 205 206func addReports(ctx context.Context, reports map[FileIdentity][]Diagnostic, snapshot Snapshot, fileID FileIdentity, diagnostics ...*Diagnostic) error { 207 if snapshot.View().Ignore(fileID.URI) { 208 return nil 209 } 210 if _, ok := reports[fileID]; !ok { 211 return errors.Errorf("diagnostics for unexpected file %s", fileID.URI) 212 } 213 for _, diag := range diagnostics { 214 if diag == nil { 215 continue 216 } 217 reports[fileID] = append(reports[fileID], *diag) 218 } 219 return nil 220} 221 222func singleDiagnostic(fileID FileIdentity, format string, a ...interface{}) map[FileIdentity][]Diagnostic { 223 return map[FileIdentity][]Diagnostic{ 224 fileID: []Diagnostic{ 225 { 226 Source: "gopls", 227 Range: protocol.Range{}, 228 Message: fmt.Sprintf(format, a...), 229 Severity: protocol.SeverityError, 230 }, 231 }, 232 } 233} 234 235// onlyDeletions returns true if all of the suggested fixes are deletions. 236func onlyDeletions(fixes []SuggestedFix) bool { 237 for _, fix := range fixes { 238 for _, edits := range fix.Edits { 239 for _, edit := range edits { 240 if edit.NewText != "" { 241 return false 242 } 243 if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 { 244 return false 245 } 246 } 247 } 248 } 249 return true 250} 251