aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_diagnostics/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide_diagnostics/src/lib.rs')
-rw-r--r--crates/ide_diagnostics/src/lib.rs374
1 files changed, 374 insertions, 0 deletions
diff --git a/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs
new file mode 100644
index 000000000..6ad1b4373
--- /dev/null
+++ b/crates/ide_diagnostics/src/lib.rs
@@ -0,0 +1,374 @@
1//! Diagnostics rendering and fixits.
2//!
3//! Most of the diagnostics originate from the dark depth of the compiler, and
4//! are originally expressed in term of IR. When we emit the diagnostic, we are
5//! usually not in the position to decide how to best "render" it in terms of
6//! user-authored source code. We are especially not in the position to offer
7//! fixits, as the compiler completely lacks the infrastructure to edit the
8//! source code.
9//!
10//! Instead, we "bubble up" raw, structured diagnostics until the `hir` crate,
11//! where we "cook" them so that each diagnostic is formulated in terms of `hir`
12//! types. Well, at least that's the aspiration, the "cooking" is somewhat
13//! ad-hoc at the moment. Anyways, we get a bunch of ide-friendly diagnostic
14//! structs from hir, and we want to render them to unified serializable
15//! representation (span, level, message) here. If we can, we also provide
16//! fixits. By the way, that's why we want to keep diagnostics structured
17//! internally -- so that we have all the info to make fixes.
18//!
19//! We have one "handler" module per diagnostic code. Such a module contains
20//! rendering, optional fixes and tests. It's OK if some low-level compiler
21//! functionality ends up being tested via a diagnostic.
22//!
23//! There are also a couple of ad-hoc diagnostics implemented directly here, we
24//! don't yet have a great pattern for how to do them properly.
25
26mod handlers {
27 pub(crate) mod break_outside_of_loop;
28 pub(crate) mod inactive_code;
29 pub(crate) mod incorrect_case;
30 pub(crate) mod macro_error;
31 pub(crate) mod mismatched_arg_count;
32 pub(crate) mod missing_fields;
33 pub(crate) mod missing_match_arms;
34 pub(crate) mod missing_ok_or_some_in_tail_expr;
35 pub(crate) mod missing_unsafe;
36 pub(crate) mod no_such_field;
37 pub(crate) mod remove_this_semicolon;
38 pub(crate) mod replace_filter_map_next_with_find_map;
39 pub(crate) mod unimplemented_builtin_macro;
40 pub(crate) mod unresolved_extern_crate;
41 pub(crate) mod unresolved_import;
42 pub(crate) mod unresolved_macro_call;
43 pub(crate) mod unresolved_module;
44 pub(crate) mod unresolved_proc_macro;
45
46 // The handlers bellow are unusual, the implement the diagnostics as well.
47 pub(crate) mod field_shorthand;
48 pub(crate) mod useless_braces;
49 pub(crate) mod unlinked_file;
50}
51
52use hir::{diagnostics::AnyDiagnostic, Semantics};
53use ide_db::{
54 assists::{Assist, AssistId, AssistKind, AssistResolveStrategy},
55 base_db::{FileId, SourceDatabase},
56 label::Label,
57 source_change::SourceChange,
58 RootDatabase,
59};
60use rustc_hash::FxHashSet;
61use syntax::{ast::AstNode, TextRange};
62
63#[derive(Copy, Clone, Debug, PartialEq)]
64pub struct DiagnosticCode(pub &'static str);
65
66impl DiagnosticCode {
67 pub fn as_str(&self) -> &str {
68 self.0
69 }
70}
71
72#[derive(Debug)]
73pub struct Diagnostic {
74 pub code: DiagnosticCode,
75 pub message: String,
76 pub range: TextRange,
77 pub severity: Severity,
78 pub unused: bool,
79 pub experimental: bool,
80 pub fixes: Option<Vec<Assist>>,
81}
82
83impl Diagnostic {
84 fn new(code: &'static str, message: impl Into<String>, range: TextRange) -> Diagnostic {
85 let message = message.into();
86 Diagnostic {
87 code: DiagnosticCode(code),
88 message,
89 range,
90 severity: Severity::Error,
91 unused: false,
92 experimental: false,
93 fixes: None,
94 }
95 }
96
97 fn experimental(mut self) -> Diagnostic {
98 self.experimental = true;
99 self
100 }
101
102 fn severity(mut self, severity: Severity) -> Diagnostic {
103 self.severity = severity;
104 self
105 }
106
107 fn with_fixes(mut self, fixes: Option<Vec<Assist>>) -> Diagnostic {
108 self.fixes = fixes;
109 self
110 }
111
112 fn with_unused(mut self, unused: bool) -> Diagnostic {
113 self.unused = unused;
114 self
115 }
116}
117
118#[derive(Debug, Copy, Clone)]
119pub enum Severity {
120 Error,
121 // We don't actually emit this one yet, but we should at some point.
122 // Warning,
123 WeakWarning,
124}
125
126#[derive(Default, Debug, Clone)]
127pub struct DiagnosticsConfig {
128 pub disable_experimental: bool,
129 pub disabled: FxHashSet<String>,
130}
131
132struct DiagnosticsContext<'a> {
133 config: &'a DiagnosticsConfig,
134 sema: Semantics<'a, RootDatabase>,
135 resolve: &'a AssistResolveStrategy,
136}
137
138pub fn diagnostics(
139 db: &RootDatabase,
140 config: &DiagnosticsConfig,
141 resolve: &AssistResolveStrategy,
142 file_id: FileId,
143) -> Vec<Diagnostic> {
144 let _p = profile::span("diagnostics");
145 let sema = Semantics::new(db);
146 let parse = db.parse(file_id);
147 let mut res = Vec::new();
148
149 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
150 res.extend(
151 parse.errors().iter().take(128).map(|err| {
152 Diagnostic::new("syntax-error", format!("Syntax Error: {}", err), err.range())
153 }),
154 );
155
156 for node in parse.tree().syntax().descendants() {
157 handlers::useless_braces::useless_braces(&mut res, file_id, &node);
158 handlers::field_shorthand::field_shorthand(&mut res, file_id, &node);
159 }
160
161 let module = sema.to_module_def(file_id);
162
163 let ctx = DiagnosticsContext { config, sema, resolve };
164 if module.is_none() {
165 handlers::unlinked_file::unlinked_file(&ctx, &mut res, file_id);
166 }
167
168 let mut diags = Vec::new();
169 if let Some(m) = module {
170 m.diagnostics(db, &mut diags)
171 }
172
173 for diag in diags {
174 #[rustfmt::skip]
175 let d = match diag {
176 AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d),
177 AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d),
178 AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d),
179 AnyDiagnostic::MismatchedArgCount(d) => handlers::mismatched_arg_count::mismatched_arg_count(&ctx, &d),
180 AnyDiagnostic::MissingFields(d) => handlers::missing_fields::missing_fields(&ctx, &d),
181 AnyDiagnostic::MissingMatchArms(d) => handlers::missing_match_arms::missing_match_arms(&ctx, &d),
182 AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => handlers::missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d),
183 AnyDiagnostic::MissingUnsafe(d) => handlers::missing_unsafe::missing_unsafe(&ctx, &d),
184 AnyDiagnostic::NoSuchField(d) => handlers::no_such_field::no_such_field(&ctx, &d),
185 AnyDiagnostic::RemoveThisSemicolon(d) => handlers::remove_this_semicolon::remove_this_semicolon(&ctx, &d),
186 AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
187 AnyDiagnostic::UnimplementedBuiltinMacro(d) => handlers::unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),
188 AnyDiagnostic::UnresolvedExternCrate(d) => handlers::unresolved_extern_crate::unresolved_extern_crate(&ctx, &d),
189 AnyDiagnostic::UnresolvedImport(d) => handlers::unresolved_import::unresolved_import(&ctx, &d),
190 AnyDiagnostic::UnresolvedMacroCall(d) => handlers::unresolved_macro_call::unresolved_macro_call(&ctx, &d),
191 AnyDiagnostic::UnresolvedModule(d) => handlers::unresolved_module::unresolved_module(&ctx, &d),
192 AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d),
193
194 AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
195 Some(it) => it,
196 None => continue,
197 }
198 };
199 res.push(d)
200 }
201
202 res.retain(|d| {
203 !ctx.config.disabled.contains(d.code.as_str())
204 && !(ctx.config.disable_experimental && d.experimental)
205 });
206
207 res
208}
209
210fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
211 let mut res = unresolved_fix(id, label, target);
212 res.source_change = Some(source_change);
213 res
214}
215
216fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
217 assert!(!id.contains(' '));
218 Assist {
219 id: AssistId(id, AssistKind::QuickFix),
220 label: Label::new(label),
221 group: None,
222 target,
223 source_change: None,
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use expect_test::Expect;
230 use ide_db::{
231 assists::AssistResolveStrategy,
232 base_db::{fixture::WithFixture, SourceDatabaseExt},
233 RootDatabase,
234 };
235 use stdx::trim_indent;
236 use test_utils::{assert_eq_text, extract_annotations};
237
238 use crate::{DiagnosticsConfig, Severity};
239
240 /// Takes a multi-file input fixture with annotated cursor positions,
241 /// and checks that:
242 /// * a diagnostic is produced
243 /// * the first diagnostic fix trigger range touches the input cursor position
244 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
245 #[track_caller]
246 pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
247 check_nth_fix(0, ra_fixture_before, ra_fixture_after);
248 }
249 /// Takes a multi-file input fixture with annotated cursor positions,
250 /// and checks that:
251 /// * a diagnostic is produced
252 /// * every diagnostic fixes trigger range touches the input cursor position
253 /// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
254 pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
255 for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
256 check_nth_fix(i, ra_fixture_before, ra_fixture_after)
257 }
258 }
259
260 #[track_caller]
261 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
262 let after = trim_indent(ra_fixture_after);
263
264 let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
265 let diagnostic = super::diagnostics(
266 &db,
267 &DiagnosticsConfig::default(),
268 &AssistResolveStrategy::All,
269 file_position.file_id,
270 )
271 .pop()
272 .expect("no diagnostics");
273 let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth];
274 let actual = {
275 let source_change = fix.source_change.as_ref().unwrap();
276 let file_id = *source_change.source_file_edits.keys().next().unwrap();
277 let mut actual = db.file_text(file_id).to_string();
278
279 for edit in source_change.source_file_edits.values() {
280 edit.apply(&mut actual);
281 }
282 actual
283 };
284
285 assert_eq_text!(&after, &actual);
286 assert!(
287 fix.target.contains_inclusive(file_position.offset),
288 "diagnostic fix range {:?} does not touch cursor position {:?}",
289 fix.target,
290 file_position.offset
291 );
292 }
293
294 /// Checks that there's a diagnostic *without* fix at `$0`.
295 pub(crate) fn check_no_fix(ra_fixture: &str) {
296 let (db, file_position) = RootDatabase::with_position(ra_fixture);
297 let diagnostic = super::diagnostics(
298 &db,
299 &DiagnosticsConfig::default(),
300 &AssistResolveStrategy::All,
301 file_position.file_id,
302 )
303 .pop()
304 .unwrap();
305 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
306 }
307
308 pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
309 let (db, file_id) = RootDatabase::with_single_file(ra_fixture);
310 let diagnostics = super::diagnostics(
311 &db,
312 &DiagnosticsConfig::default(),
313 &AssistResolveStrategy::All,
314 file_id,
315 );
316 expect.assert_debug_eq(&diagnostics)
317 }
318
319 #[track_caller]
320 pub(crate) fn check_diagnostics(ra_fixture: &str) {
321 let mut config = DiagnosticsConfig::default();
322 config.disabled.insert("inactive-code".to_string());
323 check_diagnostics_with_config(config, ra_fixture)
324 }
325
326 #[track_caller]
327 pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
328 let (db, files) = RootDatabase::with_many_files(ra_fixture);
329 for file_id in files {
330 let diagnostics =
331 super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
332
333 let expected = extract_annotations(&*db.file_text(file_id));
334 let mut actual = diagnostics
335 .into_iter()
336 .map(|d| {
337 let mut annotation = String::new();
338 if let Some(fixes) = &d.fixes {
339 assert!(!fixes.is_empty());
340 annotation.push_str("💡 ")
341 }
342 annotation.push_str(match d.severity {
343 Severity::Error => "error",
344 Severity::WeakWarning => "weak",
345 });
346 annotation.push_str(": ");
347 annotation.push_str(&d.message);
348 (d.range, annotation)
349 })
350 .collect::<Vec<_>>();
351 actual.sort_by_key(|(range, _)| range.start());
352 assert_eq!(expected, actual);
353 }
354 }
355
356 #[test]
357 fn test_disabled_diagnostics() {
358 let mut config = DiagnosticsConfig::default();
359 config.disabled.insert("unresolved-module".into());
360
361 let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#);
362
363 let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
364 assert!(diagnostics.is_empty());
365
366 let diagnostics = super::diagnostics(
367 &db,
368 &DiagnosticsConfig::default(),
369 &AssistResolveStrategy::All,
370 file_id,
371 );
372 assert!(!diagnostics.is_empty());
373 }
374}