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