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.rs510
1 files changed, 510 insertions, 0 deletions
diff --git a/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs
index e69de29bb..a104a702d 100644
--- a/crates/ide_diagnostics/src/lib.rs
+++ b/crates/ide_diagnostics/src/lib.rs
@@ -0,0 +1,510 @@
1//! Collects diagnostics & fixits for a single file.
2//!
3//! The tricky bit here is that diagnostics are produced by hir in terms of
4//! macro-expanded files, but we need to present them to the users in terms of
5//! original files. So we need to map the ranges.
6
7mod break_outside_of_loop;
8mod inactive_code;
9mod incorrect_case;
10mod macro_error;
11mod mismatched_arg_count;
12mod missing_fields;
13mod missing_match_arms;
14mod missing_ok_or_some_in_tail_expr;
15mod missing_unsafe;
16mod no_such_field;
17mod remove_this_semicolon;
18mod replace_filter_map_next_with_find_map;
19mod unimplemented_builtin_macro;
20mod unlinked_file;
21mod unresolved_extern_crate;
22mod unresolved_import;
23mod unresolved_macro_call;
24mod unresolved_module;
25mod unresolved_proc_macro;
26
27mod field_shorthand;
28
29use hir::{diagnostics::AnyDiagnostic, Semantics};
30use ide_assists::AssistResolveStrategy;
31use ide_db::{
32 base_db::{FileId, SourceDatabase},
33 label::Label,
34 source_change::SourceChange,
35 RootDatabase,
36};
37use itertools::Itertools;
38use rustc_hash::FxHashSet;
39use syntax::{
40 ast::{self, AstNode},
41 SyntaxNode, TextRange,
42};
43use text_edit::TextEdit;
44use unlinked_file::UnlinkedFile;
45
46use ide_assists::{Assist, AssistId, AssistKind};
47
48#[derive(Copy, Clone, Debug, PartialEq)]
49pub struct DiagnosticCode(pub &'static str);
50
51impl DiagnosticCode {
52 pub fn as_str(&self) -> &str {
53 self.0
54 }
55}
56
57#[derive(Debug)]
58pub struct Diagnostic {
59 pub code: DiagnosticCode,
60 pub message: String,
61 pub range: TextRange,
62 pub severity: Severity,
63 pub unused: bool,
64 pub experimental: bool,
65 pub fixes: Option<Vec<Assist>>,
66}
67
68impl Diagnostic {
69 fn new(code: &'static str, message: impl Into<String>, range: TextRange) -> Diagnostic {
70 let message = message.into();
71 Diagnostic {
72 code: DiagnosticCode(code),
73 message,
74 range,
75 severity: Severity::Error,
76 unused: false,
77 experimental: false,
78 fixes: None,
79 }
80 }
81
82 fn experimental(mut self) -> Diagnostic {
83 self.experimental = true;
84 self
85 }
86
87 fn severity(mut self, severity: Severity) -> Diagnostic {
88 self.severity = severity;
89 self
90 }
91
92 fn with_fixes(mut self, fixes: Option<Vec<Assist>>) -> Diagnostic {
93 self.fixes = fixes;
94 self
95 }
96
97 fn with_unused(mut self, unused: bool) -> Diagnostic {
98 self.unused = unused;
99 self
100 }
101}
102
103#[derive(Debug, Copy, Clone)]
104pub enum Severity {
105 Error,
106 WeakWarning,
107}
108
109#[derive(Default, Debug, Clone)]
110pub struct DiagnosticsConfig {
111 pub disable_experimental: bool,
112 pub disabled: FxHashSet<String>,
113}
114
115struct DiagnosticsContext<'a> {
116 config: &'a DiagnosticsConfig,
117 sema: Semantics<'a, RootDatabase>,
118 resolve: &'a AssistResolveStrategy,
119}
120
121pub fn diagnostics(
122 db: &RootDatabase,
123 config: &DiagnosticsConfig,
124 resolve: &AssistResolveStrategy,
125 file_id: FileId,
126) -> Vec<Diagnostic> {
127 let _p = profile::span("diagnostics");
128 let sema = Semantics::new(db);
129 let parse = db.parse(file_id);
130 let mut res = Vec::new();
131
132 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
133 res.extend(
134 parse.errors().iter().take(128).map(|err| {
135 Diagnostic::new("syntax-error", format!("Syntax Error: {}", err), err.range())
136 }),
137 );
138
139 for node in parse.tree().syntax().descendants() {
140 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
141 field_shorthand::check(&mut res, file_id, &node);
142 }
143
144 let mut diags = Vec::new();
145 let module = sema.to_module_def(file_id);
146 if let Some(m) = module {
147 m.diagnostics(db, &mut diags)
148 }
149
150 let ctx = DiagnosticsContext { config, sema, resolve };
151 if module.is_none() {
152 let d = UnlinkedFile { file: file_id };
153 let d = unlinked_file::unlinked_file(&ctx, &d);
154 res.push(d)
155 }
156
157 for diag in diags {
158 #[rustfmt::skip]
159 let d = match diag {
160 AnyDiagnostic::BreakOutsideOfLoop(d) => break_outside_of_loop::break_outside_of_loop(&ctx, &d),
161 AnyDiagnostic::IncorrectCase(d) => incorrect_case::incorrect_case(&ctx, &d),
162 AnyDiagnostic::MacroError(d) => macro_error::macro_error(&ctx, &d),
163 AnyDiagnostic::MismatchedArgCount(d) => mismatched_arg_count::mismatched_arg_count(&ctx, &d),
164 AnyDiagnostic::MissingFields(d) => missing_fields::missing_fields(&ctx, &d),
165 AnyDiagnostic::MissingMatchArms(d) => missing_match_arms::missing_match_arms(&ctx, &d),
166 AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d),
167 AnyDiagnostic::MissingUnsafe(d) => missing_unsafe::missing_unsafe(&ctx, &d),
168 AnyDiagnostic::NoSuchField(d) => no_such_field::no_such_field(&ctx, &d),
169 AnyDiagnostic::RemoveThisSemicolon(d) => remove_this_semicolon::remove_this_semicolon(&ctx, &d),
170 AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
171 AnyDiagnostic::UnimplementedBuiltinMacro(d) => unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),
172 AnyDiagnostic::UnresolvedExternCrate(d) => unresolved_extern_crate::unresolved_extern_crate(&ctx, &d),
173 AnyDiagnostic::UnresolvedImport(d) => unresolved_import::unresolved_import(&ctx, &d),
174 AnyDiagnostic::UnresolvedMacroCall(d) => unresolved_macro_call::unresolved_macro_call(&ctx, &d),
175 AnyDiagnostic::UnresolvedModule(d) => unresolved_module::unresolved_module(&ctx, &d),
176 AnyDiagnostic::UnresolvedProcMacro(d) => unresolved_proc_macro::unresolved_proc_macro(&ctx, &d),
177
178 AnyDiagnostic::InactiveCode(d) => match inactive_code::inactive_code(&ctx, &d) {
179 Some(it) => it,
180 None => continue,
181 }
182 };
183 res.push(d)
184 }
185
186 res.retain(|d| {
187 !ctx.config.disabled.contains(d.code.as_str())
188 && !(ctx.config.disable_experimental && d.experimental)
189 });
190
191 res
192}
193
194fn check_unnecessary_braces_in_use_statement(
195 acc: &mut Vec<Diagnostic>,
196 file_id: FileId,
197 node: &SyntaxNode,
198) -> Option<()> {
199 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
200 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
201 // If there is a comment inside the bracketed `use`,
202 // assume it is a commented out module path and don't show diagnostic.
203 if use_tree_list.has_inner_comment() {
204 return Some(());
205 }
206
207 let use_range = use_tree_list.syntax().text_range();
208 let edit =
209 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
210 .unwrap_or_else(|| {
211 let to_replace = single_use_tree.syntax().text().to_string();
212 let mut edit_builder = TextEdit::builder();
213 edit_builder.delete(use_range);
214 edit_builder.insert(use_range.start(), to_replace);
215 edit_builder.finish()
216 });
217
218 acc.push(
219 Diagnostic::new(
220 "unnecessary-braces",
221 "Unnecessary braces in use statement".to_string(),
222 use_range,
223 )
224 .severity(Severity::WeakWarning)
225 .with_fixes(Some(vec![fix(
226 "remove_braces",
227 "Remove unnecessary braces",
228 SourceChange::from_text_edit(file_id, edit),
229 use_range,
230 )])),
231 );
232 }
233
234 Some(())
235}
236
237fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
238 single_use_tree: &ast::UseTree,
239) -> Option<TextEdit> {
240 let use_tree_list_node = single_use_tree.syntax().parent()?;
241 if single_use_tree.path()?.segment()?.self_token().is_some() {
242 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
243 let end = use_tree_list_node.text_range().end();
244 return Some(TextEdit::delete(TextRange::new(start, end)));
245 }
246 None
247}
248
249fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
250 let mut res = unresolved_fix(id, label, target);
251 res.source_change = Some(source_change);
252 res
253}
254
255fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
256 assert!(!id.contains(' '));
257 Assist {
258 id: AssistId(id, AssistKind::QuickFix),
259 label: Label::new(label),
260 group: None,
261 target,
262 source_change: None,
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use expect_test::Expect;
269 use ide_assists::AssistResolveStrategy;
270 use ide_db::{
271 base_db::{fixture::WithFixture, SourceDatabaseExt},
272 RootDatabase,
273 };
274 use stdx::trim_indent;
275 use test_utils::{assert_eq_text, extract_annotations};
276
277 use crate::DiagnosticsConfig;
278
279 /// Takes a multi-file input fixture with annotated cursor positions,
280 /// and checks that:
281 /// * a diagnostic is produced
282 /// * the first diagnostic fix trigger range touches the input cursor position
283 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
284 #[track_caller]
285 pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
286 check_nth_fix(0, ra_fixture_before, ra_fixture_after);
287 }
288 /// Takes a multi-file input fixture with annotated cursor positions,
289 /// and checks that:
290 /// * a diagnostic is produced
291 /// * every diagnostic fixes trigger range touches the input cursor position
292 /// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
293 pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
294 for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
295 check_nth_fix(i, ra_fixture_before, ra_fixture_after)
296 }
297 }
298
299 #[track_caller]
300 fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
301 let after = trim_indent(ra_fixture_after);
302
303 let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
304 let diagnostic = super::diagnostics(
305 &db,
306 &DiagnosticsConfig::default(),
307 &AssistResolveStrategy::All,
308 file_position.file_id,
309 )
310 .pop()
311 .expect("no diagnostics");
312 let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth];
313 let actual = {
314 let source_change = fix.source_change.as_ref().unwrap();
315 let file_id = *source_change.source_file_edits.keys().next().unwrap();
316 let mut actual = db.file_text(file_id).to_string();
317
318 for edit in source_change.source_file_edits.values() {
319 edit.apply(&mut actual);
320 }
321 actual
322 };
323
324 assert_eq_text!(&after, &actual);
325 assert!(
326 fix.target.contains_inclusive(file_position.offset),
327 "diagnostic fix range {:?} does not touch cursor position {:?}",
328 fix.target,
329 file_position.offset
330 );
331 }
332
333 /// Checks that there's a diagnostic *without* fix at `$0`.
334 pub(crate) fn check_no_fix(ra_fixture: &str) {
335 let (db, file_position) = RootDatabase::with_position(ra_fixture);
336 let diagnostic = super::diagnostics(
337 &db,
338 &DiagnosticsConfig::default(),
339 &AssistResolveStrategy::All,
340 file_position.file_id,
341 )
342 .pop()
343 .unwrap();
344 assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
345 }
346
347 pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
348 let (db, file_id) = RootDatabase::with_single_file(ra_fixture);
349 let diagnostics = super::diagnostics(
350 &db,
351 &DiagnosticsConfig::default(),
352 &AssistResolveStrategy::All,
353 file_id,
354 );
355 expect.assert_debug_eq(&diagnostics)
356 }
357
358 #[track_caller]
359 pub(crate) fn check_diagnostics(ra_fixture: &str) {
360 let mut config = DiagnosticsConfig::default();
361 config.disabled.insert("inactive-code".to_string());
362 check_diagnostics_with_config(config, ra_fixture)
363 }
364
365 #[track_caller]
366 pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
367 let (db, files) = RootDatabase::with_many_files(ra_fixture);
368 for file_id in files {
369 let diagnostics =
370 super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
371
372 let expected = extract_annotations(&*db.file_text(file_id));
373 let mut actual =
374 diagnostics.into_iter().map(|d| (d.range, d.message)).collect::<Vec<_>>();
375 actual.sort_by_key(|(range, _)| range.start());
376 assert_eq!(expected, actual);
377 }
378 }
379
380 #[test]
381 fn test_check_unnecessary_braces_in_use_statement() {
382 check_diagnostics(
383 r#"
384use a;
385use a::{c, d::e};
386
387mod a {
388 mod c {}
389 mod d {
390 mod e {}
391 }
392}
393"#,
394 );
395 check_diagnostics(
396 r#"
397use a;
398use a::{
399 c,
400 // d::e
401};
402
403mod a {
404 mod c {}
405 mod d {
406 mod e {}
407 }
408}
409"#,
410 );
411 check_fix(
412 r"
413 mod b {}
414 use {$0b};
415 ",
416 r"
417 mod b {}
418 use b;
419 ",
420 );
421 check_fix(
422 r"
423 mod b {}
424 use {b$0};
425 ",
426 r"
427 mod b {}
428 use b;
429 ",
430 );
431 check_fix(
432 r"
433 mod a { mod c {} }
434 use a::{c$0};
435 ",
436 r"
437 mod a { mod c {} }
438 use a::c;
439 ",
440 );
441 check_fix(
442 r"
443 mod a {}
444 use a::{self$0};
445 ",
446 r"
447 mod a {}
448 use a;
449 ",
450 );
451 check_fix(
452 r"
453 mod a { mod c {} mod d { mod e {} } }
454 use a::{c, d::{e$0}};
455 ",
456 r"
457 mod a { mod c {} mod d { mod e {} } }
458 use a::{c, d::e};
459 ",
460 );
461 }
462
463 #[test]
464 fn test_disabled_diagnostics() {
465 let mut config = DiagnosticsConfig::default();
466 config.disabled.insert("unresolved-module".into());
467
468 let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#);
469
470 let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
471 assert!(diagnostics.is_empty());
472
473 let diagnostics = super::diagnostics(
474 &db,
475 &DiagnosticsConfig::default(),
476 &AssistResolveStrategy::All,
477 file_id,
478 );
479 assert!(!diagnostics.is_empty());
480 }
481
482 #[test]
483 fn import_extern_crate_clash_with_inner_item() {
484 // This is more of a resolver test, but doesn't really work with the hir_def testsuite.
485
486 check_diagnostics(
487 r#"
488//- /lib.rs crate:lib deps:jwt
489mod permissions;
490
491use permissions::jwt;
492
493fn f() {
494 fn inner() {}
495 jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic
496}
497
498//- /permissions.rs
499pub mod jwt {
500 pub struct Claims {}
501}
502
503//- /jwt/lib.rs crate:jwt
504pub struct Claims {
505 field: u8,
506}
507 "#,
508 );
509 }
510}