Skip to main content

rust_analyzer/
diagnostics.rs

1//! Book keeping for keeping diagnostics easily in sync with the client.
2pub(crate) mod flycheck_to_proto;
3
4use std::mem;
5
6use ide::FileId;
7use ide_db::{FxHashMap, base_db::DbPanicContext};
8use itertools::Itertools;
9use rustc_hash::FxHashSet;
10use smallvec::SmallVec;
11use stdx::iter_eq_by;
12use triomphe::Arc;
13
14use crate::{
15    flycheck::PackageSpecifier, global_state::GlobalStateSnapshot, lsp, lsp_ext,
16    main_loop::DiagnosticsTaskKind,
17};
18
19pub(crate) type CheckFixes =
20    Arc<Vec<FxHashMap<Option<PackageSpecifier>, FxHashMap<FileId, Vec<Fix>>>>>;
21
22#[derive(Debug, Default, Clone)]
23pub struct DiagnosticsMapConfig {
24    pub remap_prefix: FxHashMap<String, String>,
25    pub warnings_as_info: Vec<String>,
26    pub warnings_as_hint: Vec<String>,
27    pub check_ignore: FxHashSet<String>,
28}
29
30pub(crate) type DiagnosticsGeneration = usize;
31
32#[derive(Debug, Clone, Default)]
33pub(crate) struct WorkspaceFlycheckDiagnostic {
34    pub(crate) per_package: FxHashMap<Option<PackageSpecifier>, PackageFlycheckDiagnostic>,
35}
36
37#[derive(Debug, Clone)]
38pub(crate) struct PackageFlycheckDiagnostic {
39    generation: DiagnosticsGeneration,
40    per_file: FxHashMap<FileId, Vec<lsp_types::Diagnostic>>,
41}
42
43#[derive(Debug, Default, Clone)]
44pub(crate) struct DiagnosticCollection {
45    // FIXME: should be FxHashMap<FileId, Vec<ra_id::Diagnostic>>
46    pub(crate) native_syntax:
47        FxHashMap<FileId, (DiagnosticsGeneration, Vec<lsp_types::Diagnostic>)>,
48    pub(crate) native_semantic:
49        FxHashMap<FileId, (DiagnosticsGeneration, Vec<lsp_types::Diagnostic>)>,
50    pub(crate) check: Vec<WorkspaceFlycheckDiagnostic>,
51    pub(crate) check_fixes: CheckFixes,
52    changes: FxHashSet<FileId>,
53    /// Counter for supplying a new generation number for diagnostics.
54    /// This is used to keep track of when to clear the diagnostics for a given file as we compute
55    /// diagnostics on multiple worker threads simultaneously which may result in multiple diagnostics
56    /// updates for the same file in a single generation update (due to macros affecting multiple files).
57    generation: DiagnosticsGeneration,
58}
59
60#[derive(Debug, Clone)]
61pub(crate) struct Fix {
62    // Fixes may be triggerable from multiple ranges.
63    pub(crate) ranges: SmallVec<[lsp_types::Range; 1]>,
64    pub(crate) action: lsp_ext::CodeAction,
65}
66
67impl DiagnosticCollection {
68    pub(crate) fn clear_check(&mut self, flycheck_id: usize) {
69        let Some(check) = self.check.get_mut(flycheck_id) else {
70            return;
71        };
72        self.changes.extend(check.per_package.drain().flat_map(|(_, v)| v.per_file.into_keys()));
73        if let Some(fixes) = Arc::make_mut(&mut self.check_fixes).get_mut(flycheck_id) {
74            fixes.clear();
75        }
76    }
77
78    pub(crate) fn clear_check_all(&mut self) {
79        Arc::make_mut(&mut self.check_fixes).clear();
80        self.changes.extend(
81            self.check
82                .iter_mut()
83                .flat_map(|it| it.per_package.drain().flat_map(|(_, v)| v.per_file.into_keys())),
84        )
85    }
86
87    pub(crate) fn clear_check_for_package(
88        &mut self,
89        flycheck_id: usize,
90        package_id: PackageSpecifier,
91    ) {
92        let Some(check) = self.check.get_mut(flycheck_id) else {
93            return;
94        };
95        let package_id = Some(package_id);
96        if let Some(checks) = check.per_package.remove(&package_id) {
97            self.changes.extend(checks.per_file.into_keys());
98        }
99        if let Some(fixes) = Arc::make_mut(&mut self.check_fixes).get_mut(flycheck_id) {
100            fixes.remove(&package_id);
101        }
102    }
103
104    pub(crate) fn clear_check_older_than(
105        &mut self,
106        flycheck_id: usize,
107        generation: DiagnosticsGeneration,
108    ) {
109        if let Some(flycheck) = self.check.get_mut(flycheck_id) {
110            let mut packages = vec![];
111            self.changes.extend(
112                flycheck
113                    .per_package
114                    .extract_if(|_, v| v.generation < generation)
115                    .inspect(|(package_id, _)| packages.push(package_id.clone()))
116                    .flat_map(|(_, v)| v.per_file.into_keys()),
117            );
118            if let Some(fixes) = Arc::make_mut(&mut self.check_fixes).get_mut(flycheck_id) {
119                for package in packages {
120                    fixes.remove(&package);
121                }
122            }
123        }
124    }
125
126    pub(crate) fn clear_check_older_than_for_package(
127        &mut self,
128        flycheck_id: usize,
129        package_id: PackageSpecifier,
130        generation: DiagnosticsGeneration,
131    ) {
132        let Some(check) = self.check.get_mut(flycheck_id) else {
133            return;
134        };
135        let package_id = Some(package_id);
136        let Some((_, checks)) = check
137            .per_package
138            .extract_if(|k, v| *k == package_id && v.generation < generation)
139            .next()
140        else {
141            return;
142        };
143        self.changes.extend(checks.per_file.into_keys());
144        if let Some(fixes) = Arc::make_mut(&mut self.check_fixes).get_mut(flycheck_id) {
145            fixes.remove(&package_id);
146        }
147    }
148
149    pub(crate) fn clear_native_for(&mut self, file_id: FileId) {
150        self.native_syntax.remove(&file_id);
151        self.native_semantic.remove(&file_id);
152        self.changes.insert(file_id);
153    }
154
155    pub(crate) fn add_check_diagnostic(
156        &mut self,
157        flycheck_id: usize,
158        generation: DiagnosticsGeneration,
159        package_id: &Option<PackageSpecifier>,
160        file_id: FileId,
161        diagnostic: lsp_types::Diagnostic,
162        fix: Option<Box<Fix>>,
163    ) {
164        if self.check.len() <= flycheck_id {
165            self.check.resize_with(flycheck_id + 1, WorkspaceFlycheckDiagnostic::default);
166        }
167
168        let check = &mut self.check[flycheck_id];
169        let package = check.per_package.entry(package_id.clone()).or_insert_with(|| {
170            PackageFlycheckDiagnostic { generation, per_file: FxHashMap::default() }
171        });
172        // Getting message from old generation. Might happen in restarting checks.
173        if package.generation > generation {
174            return;
175        }
176        package.generation = generation;
177        let diagnostics = package.per_file.entry(file_id).or_default();
178        for existing_diagnostic in diagnostics.iter() {
179            if are_diagnostics_equal(existing_diagnostic, &diagnostic) {
180                return;
181            }
182        }
183
184        if let Some(fix) = fix {
185            let check_fixes = Arc::make_mut(&mut self.check_fixes);
186            if check_fixes.len() <= flycheck_id {
187                check_fixes.resize_with(flycheck_id + 1, Default::default);
188            }
189            check_fixes[flycheck_id]
190                .entry(package_id.clone())
191                .or_default()
192                .entry(file_id)
193                .or_default()
194                .push(*fix);
195        }
196        diagnostics.push(diagnostic);
197        self.changes.insert(file_id);
198    }
199
200    pub(crate) fn set_native_diagnostics(&mut self, kind: DiagnosticsTaskKind) {
201        let (generation, diagnostics, target) = match kind {
202            DiagnosticsTaskKind::Syntax(generation, diagnostics) => {
203                (generation, diagnostics, &mut self.native_syntax)
204            }
205            DiagnosticsTaskKind::Semantic(generation, diagnostics) => {
206                (generation, diagnostics, &mut self.native_semantic)
207            }
208        };
209
210        for (file_id, mut diagnostics) in diagnostics {
211            diagnostics.sort_by_key(|it| (it.range.start, it.range.end));
212
213            if let Some((old_gen, existing_diagnostics)) = target.get_mut(&file_id) {
214                if existing_diagnostics.len() == diagnostics.len()
215                    && iter_eq_by(&diagnostics, &*existing_diagnostics, |new, existing| {
216                        are_diagnostics_equal(new, existing)
217                    })
218                {
219                    // don't signal an update if the diagnostics are the same
220                    continue;
221                }
222                if *old_gen < generation || generation == 0 {
223                    target.insert(file_id, (generation, diagnostics));
224                } else {
225                    existing_diagnostics.extend(diagnostics);
226                    // FIXME: Doing the merge step of a merge sort here would be a bit more performant
227                    // but eh
228                    existing_diagnostics.sort_by_key(|it| (it.range.start, it.range.end))
229                }
230            } else {
231                target.insert(file_id, (generation, diagnostics));
232            }
233            self.changes.insert(file_id);
234        }
235    }
236
237    pub(crate) fn diagnostics_for(
238        &self,
239        file_id: FileId,
240    ) -> impl Iterator<Item = &lsp_types::Diagnostic> {
241        let native_syntax = self.native_syntax.get(&file_id).into_iter().flat_map(|(_, d)| d);
242        let native_semantic = self.native_semantic.get(&file_id).into_iter().flat_map(|(_, d)| d);
243        let check = self
244            .check
245            .iter()
246            .flat_map(|it| it.per_package.values())
247            .filter_map(move |it| it.per_file.get(&file_id))
248            .flatten();
249        native_syntax.chain(native_semantic).chain(check)
250    }
251
252    pub(crate) fn take_changes(&mut self) -> Option<FxHashSet<FileId>> {
253        if self.changes.is_empty() {
254            return None;
255        }
256        Some(mem::take(&mut self.changes))
257    }
258
259    pub(crate) fn next_generation(&mut self) -> usize {
260        self.generation += 1;
261        self.generation
262    }
263}
264
265fn are_diagnostics_equal(left: &lsp_types::Diagnostic, right: &lsp_types::Diagnostic) -> bool {
266    left.source == right.source
267        && left.severity == right.severity
268        && left.range == right.range
269        && left.message == right.message
270}
271
272pub(crate) enum NativeDiagnosticsFetchKind {
273    Syntax,
274    Semantic,
275}
276
277pub(crate) fn fetch_native_diagnostics(
278    snapshot: &GlobalStateSnapshot,
279    subscriptions: std::sync::Arc<[FileId]>,
280    slice: std::ops::Range<usize>,
281    kind: NativeDiagnosticsFetchKind,
282) -> Vec<(FileId, Vec<lsp_types::Diagnostic>)> {
283    let _p = tracing::info_span!("fetch_native_diagnostics").entered();
284    let _ctx = DbPanicContext::enter("fetch_native_diagnostics".to_owned());
285
286    // the diagnostics produced may point to different files not requested by the concrete request,
287    // put those into here and filter later
288    let mut odd_ones = Vec::new();
289    let mut diagnostics = subscriptions[slice]
290        .iter()
291        .copied()
292        .map(|file_id| {
293            let diagnostics = (|| {
294                let line_index = snapshot.file_line_index(file_id).ok()?;
295                let source_root = snapshot.analysis.source_root_id(file_id).ok()?;
296
297                let config = &snapshot.config.diagnostics(Some(source_root));
298                let diagnostics = match kind {
299                    NativeDiagnosticsFetchKind::Syntax => {
300                        snapshot.analysis.syntax_diagnostics(config, file_id).ok()?
301                    }
302
303                    NativeDiagnosticsFetchKind::Semantic if config.enabled => snapshot
304                        .analysis
305                        .semantic_diagnostics(config, ide::AssistResolveStrategy::None, file_id)
306                        .ok()?,
307                    NativeDiagnosticsFetchKind::Semantic => return None,
308                };
309                Some(
310                    diagnostics
311                        .into_iter()
312                        .filter_map(|d| {
313                            if d.range.file_id == file_id {
314                                Some(convert_diagnostic(&line_index, d))
315                            } else {
316                                odd_ones.push(d);
317                                None
318                            }
319                        })
320                        .collect::<Vec<_>>(),
321                )
322            })()
323            .unwrap_or_default();
324
325            (file_id, diagnostics)
326        })
327        .collect::<Vec<_>>();
328
329    // Add back any diagnostics that point to files we are subscribed to
330    for (file_id, group) in odd_ones
331        .into_iter()
332        .sorted_by_key(|it| it.range.file_id)
333        .chunk_by(|it| it.range.file_id)
334        .into_iter()
335    {
336        if !subscriptions.contains(&file_id) {
337            continue;
338        }
339        let Some((_, diagnostics)) = diagnostics.iter_mut().find(|&&mut (id, _)| id == file_id)
340        else {
341            continue;
342        };
343        let Some(line_index) = snapshot.file_line_index(file_id).ok() else {
344            break;
345        };
346        for diagnostic in group {
347            diagnostics.push(convert_diagnostic(&line_index, diagnostic));
348        }
349    }
350    diagnostics
351}
352
353pub(crate) fn convert_diagnostic(
354    line_index: &crate::line_index::LineIndex,
355    d: ide::Diagnostic,
356) -> lsp_types::Diagnostic {
357    lsp_types::Diagnostic {
358        range: lsp::to_proto::range(line_index, d.range.range),
359        severity: Some(lsp::to_proto::diagnostic_severity(d.severity)),
360        code: Some(lsp_types::NumberOrString::String(d.code.as_str().to_owned())),
361        code_description: Some(lsp_types::CodeDescription {
362            href: lsp_types::Url::parse(&d.code.url()).unwrap(),
363        }),
364        source: Some("rust-analyzer".to_owned()),
365        message: d.message,
366        related_information: None,
367        tags: d.unused.then(|| vec![lsp_types::DiagnosticTag::UNNECESSARY]),
368        data: None,
369    }
370}