aboutsummaryrefslogtreecommitdiff
path: root/crates/ide
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide')
-rw-r--r--crates/ide/src/diagnostics.rs146
-rw-r--r--crates/ide/src/diagnostics/fixes.rs (renamed from crates/ide/src/diagnostics/diagnostics_with_fix.rs)10
-rw-r--r--crates/ide/src/lib.rs43
3 files changed, 86 insertions, 113 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}
diff --git a/crates/ide/src/diagnostics/diagnostics_with_fix.rs b/crates/ide/src/diagnostics/fixes.rs
index 85b46c995..68ae1c239 100644
--- a/crates/ide/src/diagnostics/diagnostics_with_fix.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -1,7 +1,5 @@
1//! Provides a way to attach fixes to the diagnostics. 1//! Provides a way to attach fixes to the diagnostics.
2//! The same module also has all curret custom fixes for the diagnostics implemented. 2//! The same module also has all curret custom fixes for the diagnostics implemented.
3use crate::Fix;
4use ast::{edit::IndentLevel, make};
5use base_db::FileId; 3use base_db::FileId;
6use hir::{ 4use hir::{
7 db::AstDatabase, 5 db::AstDatabase,
@@ -12,9 +10,15 @@ use ide_db::{
12 source_change::{FileSystemEdit, SourceFileEdit}, 10 source_change::{FileSystemEdit, SourceFileEdit},
13 RootDatabase, 11 RootDatabase,
14}; 12};
15use syntax::{algo, ast, AstNode}; 13use syntax::{
14 algo,
15 ast::{self, edit::IndentLevel, make},
16 AstNode,
17};
16use text_edit::TextEdit; 18use text_edit::TextEdit;
17 19
20use crate::diagnostics::Fix;
21
18/// A [Diagnostic] that potentially has a fix available. 22/// A [Diagnostic] that potentially has a fix available.
19/// 23///
20/// [Diagnostic]: hir::diagnostics::Diagnostic 24/// [Diagnostic]: hir::diagnostics::Diagnostic
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 4b797f374..e3af6d5bc 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -44,7 +44,7 @@ mod syntax_highlighting;
44mod syntax_tree; 44mod syntax_tree;
45mod typing; 45mod typing;
46 46
47use std::{collections::HashSet, sync::Arc}; 47use std::sync::Arc;
48 48
49use base_db::{ 49use base_db::{
50 salsa::{self, ParallelDatabase}, 50 salsa::{self, ParallelDatabase},
@@ -65,7 +65,7 @@ pub use crate::{
65 completion::{ 65 completion::{
66 CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat, 66 CompletionConfig, CompletionItem, CompletionItemKind, CompletionScore, InsertTextFormat,
67 }, 67 },
68 diagnostics::Severity, 68 diagnostics::{Diagnostic, DiagnosticsConfig, Fix, Severity},
69 display::NavigationTarget, 69 display::NavigationTarget,
70 expand_macro::ExpandedMacro, 70 expand_macro::ExpandedMacro,
71 file_structure::StructureNode, 71 file_structure::StructureNode,
@@ -88,6 +88,7 @@ pub use base_db::{
88pub use hir::{Documentation, Semantics}; 88pub use hir::{Documentation, Semantics};
89pub use ide_db::{ 89pub use ide_db::{
90 change::AnalysisChange, 90 change::AnalysisChange,
91 label::Label,
91 line_index::{LineCol, LineIndex}, 92 line_index::{LineCol, LineIndex},
92 search::SearchScope, 93 search::SearchScope,
93 source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, 94 source_change::{FileSystemEdit, SourceChange, SourceFileEdit},
@@ -99,35 +100,6 @@ pub use text_edit::{Indel, TextEdit};
99 100
100pub type Cancelable<T> = Result<T, Canceled>; 101pub type Cancelable<T> = Result<T, Canceled>;
101 102
102#[derive(Debug)]
103pub struct Diagnostic {
104 pub name: Option<String>,
105 pub message: String,
106 pub range: TextRange,
107 pub severity: Severity,
108 pub fix: Option<Fix>,
109}
110
111#[derive(Debug)]
112pub struct Fix {
113 pub label: String,
114 pub source_change: SourceChange,
115 /// Allows to trigger the fix only when the caret is in the range given
116 pub fix_trigger_range: TextRange,
117}
118
119impl Fix {
120 pub fn new(
121 label: impl Into<String>,
122 source_change: SourceChange,
123 fix_trigger_range: TextRange,
124 ) -> Self {
125 let label = label.into();
126 assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.'));
127 Self { label, source_change, fix_trigger_range }
128 }
129}
130
131/// Info associated with a text range. 103/// Info associated with a text range.
132#[derive(Debug)] 104#[derive(Debug)]
133pub struct RangeInfo<T> { 105pub struct RangeInfo<T> {
@@ -148,7 +120,7 @@ pub struct AnalysisHost {
148} 120}
149 121
150impl AnalysisHost { 122impl AnalysisHost {
151 pub fn new(lru_capacity: Option<usize>) -> Self { 123 pub fn new(lru_capacity: Option<usize>) -> AnalysisHost {
152 AnalysisHost { db: RootDatabase::new(lru_capacity) } 124 AnalysisHost { db: RootDatabase::new(lru_capacity) }
153 } 125 }
154 126
@@ -495,13 +467,10 @@ impl Analysis {
495 /// Computes the set of diagnostics for the given file. 467 /// Computes the set of diagnostics for the given file.
496 pub fn diagnostics( 468 pub fn diagnostics(
497 &self, 469 &self,
470 config: &DiagnosticsConfig,
498 file_id: FileId, 471 file_id: FileId,
499 enable_experimental: bool,
500 disabled_diagnostics: Option<HashSet<String>>,
501 ) -> Cancelable<Vec<Diagnostic>> { 472 ) -> Cancelable<Vec<Diagnostic>> {
502 self.with_db(|db| { 473 self.with_db(|db| diagnostics::diagnostics(db, config, file_id))
503 diagnostics::diagnostics(db, file_id, enable_experimental, disabled_diagnostics)
504 })
505 } 474 }
506 475
507 /// Returns the edit required to rename reference at the position to the new 476 /// Returns the edit required to rename reference at the position to the new