aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/diagnostics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src/diagnostics.rs')
-rw-r--r--crates/ide/src/diagnostics.rs146
1 files changed, 73 insertions, 73 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 606a6064b..92b5adaa2 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -4,22 +4,48 @@
4//! macro-expanded files, but we need to present them to the users 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. 5//! original files. So we need to map the ranges.
6 6
7use std::{cell::RefCell, collections::HashSet}; 7mod fixes;
8
9use std::cell::RefCell;
8 10
9use base_db::SourceDatabase; 11use base_db::SourceDatabase;
10use hir::{diagnostics::DiagnosticSinkBuilder, Semantics}; 12use hir::{diagnostics::DiagnosticSinkBuilder, Semantics};
11use ide_db::RootDatabase; 13use ide_db::RootDatabase;
12use itertools::Itertools; 14use itertools::Itertools;
15use rustc_hash::FxHashSet;
13use syntax::{ 16use syntax::{
14 ast::{self, AstNode}, 17 ast::{self, AstNode},
15 SyntaxNode, TextRange, T, 18 SyntaxNode, TextRange, T,
16}; 19};
17use text_edit::TextEdit; 20use text_edit::TextEdit;
18 21
19use crate::{Diagnostic, FileId, Fix, SourceFileEdit}; 22use crate::{FileId, Label, SourceChange, SourceFileEdit};
23
24use self::fixes::DiagnosticWithFix;
25
26#[derive(Debug)]
27pub struct Diagnostic {
28 // pub name: Option<String>,
29 pub message: String,
30 pub range: TextRange,
31 pub severity: Severity,
32 pub fix: Option<Fix>,
33}
20 34
21mod diagnostics_with_fix; 35#[derive(Debug)]
22use diagnostics_with_fix::DiagnosticWithFix; 36pub struct Fix {
37 pub label: Label,
38 pub source_change: SourceChange,
39 /// Allows to trigger the fix only when the caret is in the range given
40 pub fix_trigger_range: TextRange,
41}
42
43impl Fix {
44 fn new(label: &str, source_change: SourceChange, fix_trigger_range: TextRange) -> Self {
45 let label = Label::new(label);
46 Self { label, source_change, fix_trigger_range }
47 }
48}
23 49
24#[derive(Debug, Copy, Clone)] 50#[derive(Debug, Copy, Clone)]
25pub enum Severity { 51pub enum Severity {
@@ -27,11 +53,16 @@ pub enum Severity {
27 WeakWarning, 53 WeakWarning,
28} 54}
29 55
56#[derive(Default, Debug, Clone)]
57pub struct DiagnosticsConfig {
58 pub disable_experimental: bool,
59 pub disabled: FxHashSet<String>,
60}
61
30pub(crate) fn diagnostics( 62pub(crate) fn diagnostics(
31 db: &RootDatabase, 63 db: &RootDatabase,
64 config: &DiagnosticsConfig,
32 file_id: FileId, 65 file_id: FileId,
33 enable_experimental: bool,
34 disabled_diagnostics: Option<HashSet<String>>,
35) -> Vec<Diagnostic> { 66) -> Vec<Diagnostic> {
36 let _p = profile::span("diagnostics"); 67 let _p = profile::span("diagnostics");
37 let sema = Semantics::new(db); 68 let sema = Semantics::new(db);
@@ -40,7 +71,7 @@ pub(crate) fn diagnostics(
40 71
41 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily. 72 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
42 res.extend(parse.errors().iter().take(128).map(|err| Diagnostic { 73 res.extend(parse.errors().iter().take(128).map(|err| Diagnostic {
43 name: None, 74 // name: None,
44 range: err.range(), 75 range: err.range(),
45 message: format!("Syntax Error: {}", err), 76 message: format!("Syntax Error: {}", err),
46 severity: Severity::Error, 77 severity: Severity::Error,
@@ -52,7 +83,7 @@ pub(crate) fn diagnostics(
52 check_struct_shorthand_initialization(&mut res, file_id, &node); 83 check_struct_shorthand_initialization(&mut res, file_id, &node);
53 } 84 }
54 let res = RefCell::new(res); 85 let res = RefCell::new(res);
55 let mut sink_builder = DiagnosticSinkBuilder::new() 86 let sink_builder = DiagnosticSinkBuilder::new()
56 .on::<hir::diagnostics::UnresolvedModule, _>(|d| { 87 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
57 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 88 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
58 }) 89 })
@@ -66,19 +97,15 @@ pub(crate) fn diagnostics(
66 res.borrow_mut().push(diagnostic_with_fix(d, &sema)); 97 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
67 }) 98 })
68 // Only collect experimental diagnostics when they're enabled. 99 // Only collect experimental diagnostics when they're enabled.
69 .filter(|diag| !diag.is_experimental() || enable_experimental); 100 .filter(|diag| !(diag.is_experimental() && config.disable_experimental))
70 101 .filter(|diag| !config.disabled.contains(diag.code().as_str()));
71 if let Some(disabled_diagnostics) = disabled_diagnostics {
72 // Do not collect disabled diagnostics.
73 sink_builder = sink_builder.filter(move |diag| !disabled_diagnostics.contains(diag.name()));
74 }
75 102
76 // Finalize the `DiagnosticSink` building process. 103 // Finalize the `DiagnosticSink` building process.
77 let mut sink = sink_builder 104 let mut sink = sink_builder
78 // Diagnostics not handled above get no fix and default treatment. 105 // Diagnostics not handled above get no fix and default treatment.
79 .build(|d| { 106 .build(|d| {
80 res.borrow_mut().push(Diagnostic { 107 res.borrow_mut().push(Diagnostic {
81 name: Some(d.name().into()), 108 // name: Some(d.name().into()),
82 message: d.message(), 109 message: d.message(),
83 range: sema.diagnostics_display_range(d).range, 110 range: sema.diagnostics_display_range(d).range,
84 severity: Severity::Error, 111 severity: Severity::Error,
@@ -95,7 +122,7 @@ pub(crate) fn diagnostics(
95 122
96fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { 123fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
97 Diagnostic { 124 Diagnostic {
98 name: Some(d.name().into()), 125 // name: Some(d.name().into()),
99 range: sema.diagnostics_display_range(d).range, 126 range: sema.diagnostics_display_range(d).range,
100 message: d.message(), 127 message: d.message(),
101 severity: Severity::Error, 128 severity: Severity::Error,
@@ -122,7 +149,7 @@ fn check_unnecessary_braces_in_use_statement(
122 }); 149 });
123 150
124 acc.push(Diagnostic { 151 acc.push(Diagnostic {
125 name: None, 152 // name: None,
126 range: use_range, 153 range: use_range,
127 message: "Unnecessary braces in use statement".to_string(), 154 message: "Unnecessary braces in use statement".to_string(),
128 severity: Severity::WeakWarning, 155 severity: Severity::WeakWarning,
@@ -169,7 +196,7 @@ fn check_struct_shorthand_initialization(
169 196
170 let field_range = record_field.syntax().text_range(); 197 let field_range = record_field.syntax().text_range();
171 acc.push(Diagnostic { 198 acc.push(Diagnostic {
172 name: None, 199 // name: None,
173 range: field_range, 200 range: field_range,
174 message: "Shorthand struct initialization".to_string(), 201 message: "Shorthand struct initialization".to_string(),
175 severity: Severity::WeakWarning, 202 severity: Severity::WeakWarning,
@@ -187,12 +214,14 @@ fn check_struct_shorthand_initialization(
187 214
188#[cfg(test)] 215#[cfg(test)]
189mod tests { 216mod tests {
190 use std::collections::HashSet; 217 use expect::{expect, Expect};
191 use stdx::trim_indent; 218 use stdx::trim_indent;
192 use test_utils::assert_eq_text; 219 use test_utils::assert_eq_text;
193 220
194 use crate::mock_analysis::{analysis_and_position, single_file, MockAnalysis}; 221 use crate::{
195 use expect::{expect, Expect}; 222 mock_analysis::{analysis_and_position, single_file, MockAnalysis},
223 DiagnosticsConfig,
224 };
196 225
197 /// Takes a multi-file input fixture with annotated cursor positions, 226 /// Takes a multi-file input fixture with annotated cursor positions,
198 /// and checks that: 227 /// and checks that:
@@ -203,8 +232,11 @@ mod tests {
203 let after = trim_indent(ra_fixture_after); 232 let after = trim_indent(ra_fixture_after);
204 233
205 let (analysis, file_position) = analysis_and_position(ra_fixture_before); 234 let (analysis, file_position) = analysis_and_position(ra_fixture_before);
206 let diagnostic = 235 let diagnostic = analysis
207 analysis.diagnostics(file_position.file_id, true, None).unwrap().pop().unwrap(); 236 .diagnostics(&DiagnosticsConfig::default(), file_position.file_id)
237 .unwrap()
238 .pop()
239 .unwrap();
208 let mut fix = diagnostic.fix.unwrap(); 240 let mut fix = diagnostic.fix.unwrap();
209 let edit = fix.source_change.source_file_edits.pop().unwrap().edit; 241 let edit = fix.source_change.source_file_edits.pop().unwrap().edit;
210 let target_file_contents = analysis.file_text(file_position.file_id).unwrap(); 242 let target_file_contents = analysis.file_text(file_position.file_id).unwrap();
@@ -230,7 +262,11 @@ mod tests {
230 let ra_fixture_after = &trim_indent(ra_fixture_after); 262 let ra_fixture_after = &trim_indent(ra_fixture_after);
231 let (analysis, file_pos) = analysis_and_position(ra_fixture_before); 263 let (analysis, file_pos) = analysis_and_position(ra_fixture_before);
232 let current_file_id = file_pos.file_id; 264 let current_file_id = file_pos.file_id;
233 let diagnostic = analysis.diagnostics(current_file_id, true, None).unwrap().pop().unwrap(); 265 let diagnostic = analysis
266 .diagnostics(&DiagnosticsConfig::default(), current_file_id)
267 .unwrap()
268 .pop()
269 .unwrap();
234 let mut fix = diagnostic.fix.unwrap(); 270 let mut fix = diagnostic.fix.unwrap();
235 let edit = fix.source_change.source_file_edits.pop().unwrap(); 271 let edit = fix.source_change.source_file_edits.pop().unwrap();
236 let changed_file_id = edit.file_id; 272 let changed_file_id = edit.file_id;
@@ -251,58 +287,16 @@ mod tests {
251 let analysis = mock.analysis(); 287 let analysis = mock.analysis();
252 let diagnostics = files 288 let diagnostics = files
253 .into_iter() 289 .into_iter()
254 .flat_map(|file_id| analysis.diagnostics(file_id, true, None).unwrap())
255 .collect::<Vec<_>>();
256 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
257 }
258
259 /// Takes a multi-file input fixture with annotated cursor position and the list of disabled diagnostics,
260 /// and checks that provided diagnostics aren't spawned during analysis.
261 fn check_disabled_diagnostics(ra_fixture: &str, disabled_diagnostics: &[&'static str]) {
262 let disabled_diagnostics: HashSet<_> =
263 disabled_diagnostics.into_iter().map(|diag| diag.to_string()).collect();
264
265 let mock = MockAnalysis::with_files(ra_fixture);
266 let files = mock.files().map(|(it, _)| it).collect::<Vec<_>>();
267 let analysis = mock.analysis();
268
269 let diagnostics = files
270 .clone()
271 .into_iter()
272 .flat_map(|file_id| { 290 .flat_map(|file_id| {
273 analysis.diagnostics(file_id, true, Some(disabled_diagnostics.clone())).unwrap() 291 analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap()
274 }) 292 })
275 .collect::<Vec<_>>(); 293 .collect::<Vec<_>>();
276 294 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
277 // First, we have to check that diagnostic is not emitted when it's added to the disabled diagnostics list.
278 for diagnostic in diagnostics {
279 if let Some(name) = diagnostic.name {
280 assert!(!disabled_diagnostics.contains(&name), "Diagnostic {} is disabled", name);
281 }
282 }
283
284 // Then, we must reset the config and repeat the check, so that we'll be sure that without
285 // config these diagnostics are emitted.
286 // This is required for tests to not become outdated if e.g. diagnostics name changes:
287 // without this additional run the test will pass simply because a diagnostic with an old name
288 // will no longer exist.
289 let diagnostics = files
290 .into_iter()
291 .flat_map(|file_id| analysis.diagnostics(file_id, true, None).unwrap())
292 .collect::<Vec<_>>();
293
294 assert!(
295 diagnostics
296 .into_iter()
297 .filter_map(|diag| diag.name)
298 .any(|name| disabled_diagnostics.contains(&name)),
299 "At least one of the diagnostics was not emitted even without config; are the diagnostics names correct?"
300 );
301 } 295 }
302 296
303 fn check_expect(ra_fixture: &str, expect: Expect) { 297 fn check_expect(ra_fixture: &str, expect: Expect) {
304 let (analysis, file_id) = single_file(ra_fixture); 298 let (analysis, file_id) = single_file(ra_fixture);
305 let diagnostics = analysis.diagnostics(file_id, true, None).unwrap(); 299 let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap();
306 expect.assert_debug_eq(&diagnostics) 300 expect.assert_debug_eq(&diagnostics)
307 } 301 }
308 302
@@ -562,9 +556,6 @@ fn test_fn() {
562 expect![[r#" 556 expect![[r#"
563 [ 557 [
564 Diagnostic { 558 Diagnostic {
565 name: Some(
566 "unresolved-module",
567 ),
568 message: "unresolved module", 559 message: "unresolved module",
569 range: 0..8, 560 range: 0..8,
570 severity: Error, 561 severity: Error,
@@ -741,6 +732,15 @@ struct Foo {
741 732
742 #[test] 733 #[test]
743 fn test_disabled_diagnostics() { 734 fn test_disabled_diagnostics() {
744 check_disabled_diagnostics(r#"mod foo;"#, &["unresolved-module"]); 735 let mut config = DiagnosticsConfig::default();
736 config.disabled.insert("unresolved-module".into());
737
738 let (analysis, file_id) = single_file(r#"mod foo;"#);
739
740 let diagnostics = analysis.diagnostics(&config, file_id).unwrap();
741 assert!(diagnostics.is_empty());
742
743 let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap();
744 assert!(!diagnostics.is_empty());
745 } 745 }
746} 746}