aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_diagnostics
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide_diagnostics')
-rw-r--r--crates/ide_diagnostics/Cargo.toml29
-rw-r--r--crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs30
-rw-r--r--crates/ide_diagnostics/src/handlers/field_shorthand.rs203
-rw-r--r--crates/ide_diagnostics/src/handlers/inactive_code.rs116
-rw-r--r--crates/ide_diagnostics/src/handlers/incorrect_case.rs459
-rw-r--r--crates/ide_diagnostics/src/handlers/macro_error.rs173
-rw-r--r--crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs272
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_fields.rs355
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_match_arms.rs929
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs229
-rw-r--r--crates/ide_diagnostics/src/handlers/missing_unsafe.rs101
-rw-r--r--crates/ide_diagnostics/src/handlers/no_such_field.rs283
-rw-r--r--crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs61
-rw-r--r--crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs179
-rw-r--r--crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs16
-rw-r--r--crates/ide_diagnostics/src/handlers/unlinked_file.rs298
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs49
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_import.rs90
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs84
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_module.rs110
-rw-r--r--crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs27
-rw-r--r--crates/ide_diagnostics/src/handlers/useless_braces.rs148
-rw-r--r--crates/ide_diagnostics/src/lib.rs374
23 files changed, 4615 insertions, 0 deletions
diff --git a/crates/ide_diagnostics/Cargo.toml b/crates/ide_diagnostics/Cargo.toml
new file mode 100644
index 000000000..fa2adf212
--- /dev/null
+++ b/crates/ide_diagnostics/Cargo.toml
@@ -0,0 +1,29 @@
1[package]
2name = "ide_diagnostics"
3version = "0.0.0"
4description = "TBD"
5license = "MIT OR Apache-2.0"
6authors = ["rust-analyzer developers"]
7edition = "2018"
8
9[lib]
10doctest = false
11
12[dependencies]
13cov-mark = "2.0.0-pre.1"
14itertools = "0.10.0"
15rustc-hash = "1.1.0"
16either = "1.5.3"
17
18profile = { path = "../profile", version = "0.0.0" }
19stdx = { path = "../stdx", version = "0.0.0" }
20syntax = { path = "../syntax", version = "0.0.0" }
21text_edit = { path = "../text_edit", version = "0.0.0" }
22cfg = { path = "../cfg", version = "0.0.0" }
23hir = { path = "../hir", version = "0.0.0" }
24ide_db = { path = "../ide_db", version = "0.0.0" }
25
26[dev-dependencies]
27expect-test = "1.1"
28
29test_utils = { path = "../test_utils" }
diff --git a/crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs b/crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs
new file mode 100644
index 000000000..d12594a4c
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs
@@ -0,0 +1,30 @@
1use crate::{Diagnostic, DiagnosticsContext};
2
3// Diagnostic: break-outside-of-loop
4//
5// This diagnostic is triggered if the `break` keyword is used outside of a loop.
6pub(crate) fn break_outside_of_loop(
7 ctx: &DiagnosticsContext<'_>,
8 d: &hir::BreakOutsideOfLoop,
9) -> Diagnostic {
10 Diagnostic::new(
11 "break-outside-of-loop",
12 "break outside of loop",
13 ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
14 )
15}
16
17#[cfg(test)]
18mod tests {
19 use crate::tests::check_diagnostics;
20
21 #[test]
22 fn break_outside_of_loop() {
23 check_diagnostics(
24 r#"
25fn foo() { break; }
26 //^^^^^ error: break outside of loop
27"#,
28 );
29 }
30}
diff --git a/crates/ide_diagnostics/src/handlers/field_shorthand.rs b/crates/ide_diagnostics/src/handlers/field_shorthand.rs
new file mode 100644
index 000000000..33152e284
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/field_shorthand.rs
@@ -0,0 +1,203 @@
1//! Suggests shortening `Foo { field: field }` to `Foo { field }` in both
2//! expressions and patterns.
3
4use ide_db::{base_db::FileId, source_change::SourceChange};
5use syntax::{ast, match_ast, AstNode, SyntaxNode};
6use text_edit::TextEdit;
7
8use crate::{fix, Diagnostic, Severity};
9
10pub(crate) fn field_shorthand(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) {
11 match_ast! {
12 match node {
13 ast::RecordExpr(it) => check_expr_field_shorthand(acc, file_id, it),
14 ast::RecordPat(it) => check_pat_field_shorthand(acc, file_id, it),
15 _ => ()
16 }
17 };
18}
19
20fn check_expr_field_shorthand(
21 acc: &mut Vec<Diagnostic>,
22 file_id: FileId,
23 record_expr: ast::RecordExpr,
24) {
25 let record_field_list = match record_expr.record_expr_field_list() {
26 Some(it) => it,
27 None => return,
28 };
29 for record_field in record_field_list.fields() {
30 let (name_ref, expr) = match record_field.name_ref().zip(record_field.expr()) {
31 Some(it) => it,
32 None => continue,
33 };
34
35 let field_name = name_ref.syntax().text().to_string();
36 let field_expr = expr.syntax().text().to_string();
37 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
38 if field_name != field_expr || field_name_is_tup_index {
39 continue;
40 }
41
42 let mut edit_builder = TextEdit::builder();
43 edit_builder.delete(record_field.syntax().text_range());
44 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
45 let edit = edit_builder.finish();
46
47 let field_range = record_field.syntax().text_range();
48 acc.push(
49 Diagnostic::new("use-field-shorthand", "Shorthand struct initialization", field_range)
50 .severity(Severity::WeakWarning)
51 .with_fixes(Some(vec![fix(
52 "use_expr_field_shorthand",
53 "Use struct shorthand initialization",
54 SourceChange::from_text_edit(file_id, edit),
55 field_range,
56 )])),
57 );
58 }
59}
60
61fn check_pat_field_shorthand(
62 acc: &mut Vec<Diagnostic>,
63 file_id: FileId,
64 record_pat: ast::RecordPat,
65) {
66 let record_pat_field_list = match record_pat.record_pat_field_list() {
67 Some(it) => it,
68 None => return,
69 };
70 for record_pat_field in record_pat_field_list.fields() {
71 let (name_ref, pat) = match record_pat_field.name_ref().zip(record_pat_field.pat()) {
72 Some(it) => it,
73 None => continue,
74 };
75
76 let field_name = name_ref.syntax().text().to_string();
77 let field_pat = pat.syntax().text().to_string();
78 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
79 if field_name != field_pat || field_name_is_tup_index {
80 continue;
81 }
82
83 let mut edit_builder = TextEdit::builder();
84 edit_builder.delete(record_pat_field.syntax().text_range());
85 edit_builder.insert(record_pat_field.syntax().text_range().start(), field_name);
86 let edit = edit_builder.finish();
87
88 let field_range = record_pat_field.syntax().text_range();
89 acc.push(
90 Diagnostic::new("use-field-shorthand", "Shorthand struct pattern", field_range)
91 .severity(Severity::WeakWarning)
92 .with_fixes(Some(vec![fix(
93 "use_pat_field_shorthand",
94 "Use struct field shorthand",
95 SourceChange::from_text_edit(file_id, edit),
96 field_range,
97 )])),
98 );
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use crate::tests::{check_diagnostics, check_fix};
105
106 #[test]
107 fn test_check_expr_field_shorthand() {
108 check_diagnostics(
109 r#"
110struct A { a: &'static str }
111fn main() { A { a: "hello" } }
112"#,
113 );
114 check_diagnostics(
115 r#"
116struct A(usize);
117fn main() { A { 0: 0 } }
118"#,
119 );
120
121 check_fix(
122 r#"
123struct A { a: &'static str }
124fn main() {
125 let a = "haha";
126 A { a$0: a }
127}
128"#,
129 r#"
130struct A { a: &'static str }
131fn main() {
132 let a = "haha";
133 A { a }
134}
135"#,
136 );
137
138 check_fix(
139 r#"
140struct A { a: &'static str, b: &'static str }
141fn main() {
142 let a = "haha";
143 let b = "bb";
144 A { a$0: a, b }
145}
146"#,
147 r#"
148struct A { a: &'static str, b: &'static str }
149fn main() {
150 let a = "haha";
151 let b = "bb";
152 A { a, b }
153}
154"#,
155 );
156 }
157
158 #[test]
159 fn test_check_pat_field_shorthand() {
160 check_diagnostics(
161 r#"
162struct A { a: &'static str }
163fn f(a: A) { let A { a: hello } = a; }
164"#,
165 );
166 check_diagnostics(
167 r#"
168struct A(usize);
169fn f(a: A) { let A { 0: 0 } = a; }
170"#,
171 );
172
173 check_fix(
174 r#"
175struct A { a: &'static str }
176fn f(a: A) {
177 let A { a$0: a } = a;
178}
179"#,
180 r#"
181struct A { a: &'static str }
182fn f(a: A) {
183 let A { a } = a;
184}
185"#,
186 );
187
188 check_fix(
189 r#"
190struct A { a: &'static str, b: &'static str }
191fn f(a: A) {
192 let A { a$0: a, b } = a;
193}
194"#,
195 r#"
196struct A { a: &'static str, b: &'static str }
197fn f(a: A) {
198 let A { a, b } = a;
199}
200"#,
201 );
202 }
203}
diff --git a/crates/ide_diagnostics/src/handlers/inactive_code.rs b/crates/ide_diagnostics/src/handlers/inactive_code.rs
new file mode 100644
index 000000000..dfd0e3351
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/inactive_code.rs
@@ -0,0 +1,116 @@
1use cfg::DnfExpr;
2use stdx::format_to;
3
4use crate::{Diagnostic, DiagnosticsContext, Severity};
5
6// Diagnostic: inactive-code
7//
8// This diagnostic is shown for code with inactive `#[cfg]` attributes.
9pub(crate) fn inactive_code(
10 ctx: &DiagnosticsContext<'_>,
11 d: &hir::InactiveCode,
12) -> Option<Diagnostic> {
13 // If there's inactive code somewhere in a macro, don't propagate to the call-site.
14 if d.node.file_id.expansion_info(ctx.sema.db).is_some() {
15 return None;
16 }
17
18 let inactive = DnfExpr::new(d.cfg.clone()).why_inactive(&d.opts);
19 let mut message = "code is inactive due to #[cfg] directives".to_string();
20
21 if let Some(inactive) = inactive {
22 format_to!(message, ": {}", inactive);
23 }
24
25 let res = Diagnostic::new(
26 "inactive-code",
27 message,
28 ctx.sema.diagnostics_display_range(d.node.clone()).range,
29 )
30 .severity(Severity::WeakWarning)
31 .with_unused(true);
32 Some(res)
33}
34
35#[cfg(test)]
36mod tests {
37 use crate::{tests::check_diagnostics_with_config, DiagnosticsConfig};
38
39 pub(crate) fn check(ra_fixture: &str) {
40 let config = DiagnosticsConfig::default();
41 check_diagnostics_with_config(config, ra_fixture)
42 }
43
44 #[test]
45 fn cfg_diagnostics() {
46 check(
47 r#"
48fn f() {
49 // The three g̶e̶n̶d̶e̶r̶s̶ statements:
50
51 #[cfg(a)] fn f() {} // Item statement
52 //^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
53 #[cfg(a)] {} // Expression statement
54 //^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
55 #[cfg(a)] let x = 0; // let statement
56 //^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
57
58 abc(#[cfg(a)] 0);
59 //^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
60 let x = Struct {
61 #[cfg(a)] f: 0,
62 //^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
63 };
64 match () {
65 () => (),
66 #[cfg(a)] () => (),
67 //^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
68 }
69
70 #[cfg(a)] 0 // Trailing expression of block
71 //^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
72}
73 "#,
74 );
75 }
76
77 #[test]
78 fn inactive_item() {
79 // Additional tests in `cfg` crate. This only tests disabled cfgs.
80
81 check(
82 r#"
83 #[cfg(no)] pub fn f() {}
84 //^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled
85
86 #[cfg(no)] #[cfg(no2)] mod m;
87 //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no and no2 are disabled
88
89 #[cfg(all(not(a), b))] enum E {}
90 //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: b is disabled
91
92 #[cfg(feature = "std")] use std;
93 //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: feature = "std" is disabled
94"#,
95 );
96 }
97
98 /// Tests that `cfg` attributes behind `cfg_attr` is handled properly.
99 #[test]
100 fn inactive_via_cfg_attr() {
101 cov_mark::check!(cfg_attr_active);
102 check(
103 r#"
104 #[cfg_attr(not(never), cfg(no))] fn f() {}
105 //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled
106
107 #[cfg_attr(not(never), cfg(not(no)))] fn f() {}
108
109 #[cfg_attr(never, cfg(no))] fn g() {}
110
111 #[cfg_attr(not(never), inline, cfg(no))] fn h() {}
112 //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled
113"#,
114 );
115 }
116}
diff --git a/crates/ide_diagnostics/src/handlers/incorrect_case.rs b/crates/ide_diagnostics/src/handlers/incorrect_case.rs
new file mode 100644
index 000000000..68f25f284
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/incorrect_case.rs
@@ -0,0 +1,459 @@
1use hir::{db::AstDatabase, InFile};
2use ide_db::{assists::Assist, defs::NameClass};
3use syntax::AstNode;
4
5use crate::{
6 // references::rename::rename_with_semantics,
7 unresolved_fix,
8 Diagnostic,
9 DiagnosticsContext,
10 Severity,
11};
12
13// Diagnostic: incorrect-ident-case
14//
15// This diagnostic is triggered if an item name doesn't follow https://doc.rust-lang.org/1.0.0/style/style/naming/README.html[Rust naming convention].
16pub(crate) fn incorrect_case(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Diagnostic {
17 Diagnostic::new(
18 "incorrect-ident-case",
19 format!(
20 "{} `{}` should have {} name, e.g. `{}`",
21 d.ident_type, d.ident_text, d.expected_case, d.suggested_text
22 ),
23 ctx.sema.diagnostics_display_range(InFile::new(d.file, d.ident.clone().into())).range,
24 )
25 .severity(Severity::WeakWarning)
26 .with_fixes(fixes(ctx, d))
27}
28
29fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option<Vec<Assist>> {
30 let root = ctx.sema.db.parse_or_expand(d.file)?;
31 let name_node = d.ident.to_node(&root);
32 let def = NameClass::classify(&ctx.sema, &name_node)?.defined(ctx.sema.db)?;
33
34 let name_node = InFile::new(d.file, name_node.syntax());
35 let frange = name_node.original_file_range(ctx.sema.db);
36
37 let label = format!("Rename to {}", d.suggested_text);
38 let mut res = unresolved_fix("change_case", &label, frange.range);
39 if ctx.resolve.should_resolve(&res.id) {
40 let source_change = def.rename(&ctx.sema, &d.suggested_text);
41 res.source_change = Some(source_change.ok().unwrap_or_default());
42 }
43
44 Some(vec![res])
45}
46
47#[cfg(test)]
48mod change_case {
49 use crate::tests::{check_diagnostics, check_fix};
50
51 #[test]
52 fn test_rename_incorrect_case() {
53 check_fix(
54 r#"
55pub struct test_struct$0 { one: i32 }
56
57pub fn some_fn(val: test_struct) -> test_struct {
58 test_struct { one: val.one + 1 }
59}
60"#,
61 r#"
62pub struct TestStruct { one: i32 }
63
64pub fn some_fn(val: TestStruct) -> TestStruct {
65 TestStruct { one: val.one + 1 }
66}
67"#,
68 );
69
70 check_fix(
71 r#"
72pub fn some_fn(NonSnakeCase$0: u8) -> u8 {
73 NonSnakeCase
74}
75"#,
76 r#"
77pub fn some_fn(non_snake_case: u8) -> u8 {
78 non_snake_case
79}
80"#,
81 );
82
83 check_fix(
84 r#"
85pub fn SomeFn$0(val: u8) -> u8 {
86 if val != 0 { SomeFn(val - 1) } else { val }
87}
88"#,
89 r#"
90pub fn some_fn(val: u8) -> u8 {
91 if val != 0 { some_fn(val - 1) } else { val }
92}
93"#,
94 );
95
96 check_fix(
97 r#"
98fn some_fn() {
99 let whatAWeird_Formatting$0 = 10;
100 another_func(whatAWeird_Formatting);
101}
102"#,
103 r#"
104fn some_fn() {
105 let what_a_weird_formatting = 10;
106 another_func(what_a_weird_formatting);
107}
108"#,
109 );
110 }
111
112 #[test]
113 fn test_uppercase_const_no_diagnostics() {
114 check_diagnostics(
115 r#"
116fn foo() {
117 const ANOTHER_ITEM: &str = "some_item";
118}
119"#,
120 );
121 }
122
123 #[test]
124 fn test_rename_incorrect_case_struct_method() {
125 check_fix(
126 r#"
127pub struct TestStruct;
128
129impl TestStruct {
130 pub fn SomeFn$0() -> TestStruct {
131 TestStruct
132 }
133}
134"#,
135 r#"
136pub struct TestStruct;
137
138impl TestStruct {
139 pub fn some_fn() -> TestStruct {
140 TestStruct
141 }
142}
143"#,
144 );
145 }
146
147 #[test]
148 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
149 check_diagnostics(
150 r#"
151fn FOO() {}
152// ^^^ 💡 weak: Function `FOO` should have snake_case name, e.g. `foo`
153"#,
154 );
155 check_fix(r#"fn FOO$0() {}"#, r#"fn foo() {}"#);
156 }
157
158 #[test]
159 fn incorrect_function_name() {
160 check_diagnostics(
161 r#"
162fn NonSnakeCaseName() {}
163// ^^^^^^^^^^^^^^^^ 💡 weak: Function `NonSnakeCaseName` should have snake_case name, e.g. `non_snake_case_name`
164"#,
165 );
166 }
167
168 #[test]
169 fn incorrect_function_params() {
170 check_diagnostics(
171 r#"
172fn foo(SomeParam: u8) {}
173 // ^^^^^^^^^ 💡 weak: Parameter `SomeParam` should have snake_case name, e.g. `some_param`
174
175fn foo2(ok_param: &str, CAPS_PARAM: u8) {}
176 // ^^^^^^^^^^ 💡 weak: Parameter `CAPS_PARAM` should have snake_case name, e.g. `caps_param`
177"#,
178 );
179 }
180
181 #[test]
182 fn incorrect_variable_names() {
183 check_diagnostics(
184 r#"
185fn foo() {
186 let SOME_VALUE = 10;
187 // ^^^^^^^^^^ 💡 weak: Variable `SOME_VALUE` should have snake_case name, e.g. `some_value`
188 let AnotherValue = 20;
189 // ^^^^^^^^^^^^ 💡 weak: Variable `AnotherValue` should have snake_case name, e.g. `another_value`
190}
191"#,
192 );
193 }
194
195 #[test]
196 fn incorrect_struct_names() {
197 check_diagnostics(
198 r#"
199struct non_camel_case_name {}
200 // ^^^^^^^^^^^^^^^^^^^ 💡 weak: Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName`
201
202struct SCREAMING_CASE {}
203 // ^^^^^^^^^^^^^^ 💡 weak: Structure `SCREAMING_CASE` should have CamelCase name, e.g. `ScreamingCase`
204"#,
205 );
206 }
207
208 #[test]
209 fn no_diagnostic_for_camel_cased_acronyms_in_struct_name() {
210 check_diagnostics(
211 r#"
212struct AABB {}
213"#,
214 );
215 }
216
217 #[test]
218 fn incorrect_struct_field() {
219 check_diagnostics(
220 r#"
221struct SomeStruct { SomeField: u8 }
222 // ^^^^^^^^^ 💡 weak: Field `SomeField` should have snake_case name, e.g. `some_field`
223"#,
224 );
225 }
226
227 #[test]
228 fn incorrect_enum_names() {
229 check_diagnostics(
230 r#"
231enum some_enum { Val(u8) }
232 // ^^^^^^^^^ 💡 weak: Enum `some_enum` should have CamelCase name, e.g. `SomeEnum`
233
234enum SOME_ENUM {}
235 // ^^^^^^^^^ 💡 weak: Enum `SOME_ENUM` should have CamelCase name, e.g. `SomeEnum`
236"#,
237 );
238 }
239
240 #[test]
241 fn no_diagnostic_for_camel_cased_acronyms_in_enum_name() {
242 check_diagnostics(
243 r#"
244enum AABB {}
245"#,
246 );
247 }
248
249 #[test]
250 fn incorrect_enum_variant_name() {
251 check_diagnostics(
252 r#"
253enum SomeEnum { SOME_VARIANT(u8) }
254 // ^^^^^^^^^^^^ 💡 weak: Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant`
255"#,
256 );
257 }
258
259 #[test]
260 fn incorrect_const_name() {
261 check_diagnostics(
262 r#"
263const some_weird_const: u8 = 10;
264 // ^^^^^^^^^^^^^^^^ 💡 weak: Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
265"#,
266 );
267 }
268
269 #[test]
270 fn incorrect_static_name() {
271 check_diagnostics(
272 r#"
273static some_weird_const: u8 = 10;
274 // ^^^^^^^^^^^^^^^^ 💡 weak: Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
275"#,
276 );
277 }
278
279 #[test]
280 fn fn_inside_impl_struct() {
281 check_diagnostics(
282 r#"
283struct someStruct;
284 // ^^^^^^^^^^ 💡 weak: Structure `someStruct` should have CamelCase name, e.g. `SomeStruct`
285
286impl someStruct {
287 fn SomeFunc(&self) {
288 // ^^^^^^^^ 💡 weak: Function `SomeFunc` should have snake_case name, e.g. `some_func`
289 let WHY_VAR_IS_CAPS = 10;
290 // ^^^^^^^^^^^^^^^ 💡 weak: Variable `WHY_VAR_IS_CAPS` should have snake_case name, e.g. `why_var_is_caps`
291 }
292}
293"#,
294 );
295 }
296
297 #[test]
298 fn no_diagnostic_for_enum_varinats() {
299 check_diagnostics(
300 r#"
301enum Option { Some, None }
302
303fn main() {
304 match Option::None {
305 None => (),
306 Some => (),
307 }
308}
309"#,
310 );
311 }
312
313 #[test]
314 fn non_let_bind() {
315 check_diagnostics(
316 r#"
317enum Option { Some, None }
318
319fn main() {
320 match Option::None {
321 SOME_VAR @ None => (),
322 // ^^^^^^^^ 💡 weak: Variable `SOME_VAR` should have snake_case name, e.g. `some_var`
323 Some => (),
324 }
325}
326"#,
327 );
328 }
329
330 #[test]
331 fn allow_attributes_crate_attr() {
332 check_diagnostics(
333 r#"
334#![allow(non_snake_case)]
335
336mod F {
337 fn CheckItWorksWithCrateAttr(BAD_NAME_HI: u8) {}
338}
339 "#,
340 );
341 }
342
343 #[test]
344 fn complex_ignore() {
345 // FIXME: this should trigger errors for the second case.
346 check_diagnostics(
347 r#"
348trait T { fn a(); }
349struct U {}
350impl T for U {
351 fn a() {
352 #[allow(non_snake_case)]
353 trait __BitFlagsOk {
354 const HiImAlsoBad: u8 = 2;
355 fn Dirty(&self) -> bool { false }
356 }
357
358 trait __BitFlagsBad {
359 const HiImAlsoBad: u8 = 2;
360 fn Dirty(&self) -> bool { false }
361 }
362 }
363}
364"#,
365 );
366 }
367
368 #[test]
369 fn infinite_loop_inner_items() {
370 check_diagnostics(
371 r#"
372fn qualify() {
373 mod foo {
374 use super::*;
375 }
376}
377 "#,
378 )
379 }
380
381 #[test] // Issue #8809.
382 fn parenthesized_parameter() {
383 check_diagnostics(r#"fn f((O): _) {}"#)
384 }
385
386 #[test]
387 fn ignores_extern_items() {
388 cov_mark::check!(extern_func_incorrect_case_ignored);
389 cov_mark::check!(extern_static_incorrect_case_ignored);
390 check_diagnostics(
391 r#"
392extern {
393 fn NonSnakeCaseName(SOME_VAR: u8) -> u8;
394 pub static SomeStatic: u8 = 10;
395}
396 "#,
397 );
398 }
399
400 #[test]
401 fn bug_traits_arent_checked() {
402 // FIXME: Traits and functions in traits aren't currently checked by
403 // r-a, even though rustc will complain about them.
404 check_diagnostics(
405 r#"
406trait BAD_TRAIT {
407 fn BAD_FUNCTION();
408 fn BadFunction();
409}
410 "#,
411 );
412 }
413
414 #[test]
415 fn allow_attributes() {
416 check_diagnostics(
417 r#"
418#[allow(non_snake_case)]
419fn NonSnakeCaseName(SOME_VAR: u8) -> u8{
420 // cov_flags generated output from elsewhere in this file
421 extern "C" {
422 #[no_mangle]
423 static lower_case: u8;
424 }
425
426 let OtherVar = SOME_VAR + 1;
427 OtherVar
428}
429
430#[allow(nonstandard_style)]
431mod CheckNonstandardStyle {
432 fn HiImABadFnName() {}
433}
434
435#[allow(bad_style)]
436mod CheckBadStyle {
437 fn HiImABadFnName() {}
438}
439
440mod F {
441 #![allow(non_snake_case)]
442 fn CheckItWorksWithModAttr(BAD_NAME_HI: u8) {}
443}
444
445#[allow(non_snake_case, non_camel_case_types)]
446pub struct some_type {
447 SOME_FIELD: u8,
448 SomeField: u16,
449}
450
451#[allow(non_upper_case_globals)]
452pub const some_const: u8 = 10;
453
454#[allow(non_upper_case_globals)]
455pub static SomeStatic: u8 = 10;
456 "#,
457 );
458 }
459}
diff --git a/crates/ide_diagnostics/src/handlers/macro_error.rs b/crates/ide_diagnostics/src/handlers/macro_error.rs
new file mode 100644
index 000000000..356f089b2
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/macro_error.rs
@@ -0,0 +1,173 @@
1use crate::{Diagnostic, DiagnosticsContext};
2
3// Diagnostic: macro-error
4//
5// This diagnostic is shown for macro expansion errors.
6pub(crate) fn macro_error(ctx: &DiagnosticsContext<'_>, d: &hir::MacroError) -> Diagnostic {
7 Diagnostic::new(
8 "macro-error",
9 d.message.clone(),
10 ctx.sema.diagnostics_display_range(d.node.clone()).range,
11 )
12 .experimental()
13}
14
15#[cfg(test)]
16mod tests {
17 use crate::{
18 tests::{check_diagnostics, check_diagnostics_with_config},
19 DiagnosticsConfig,
20 };
21
22 #[test]
23 fn builtin_macro_fails_expansion() {
24 check_diagnostics(
25 r#"
26#[rustc_builtin_macro]
27macro_rules! include { () => {} }
28
29 include!("doesntexist");
30//^^^^^^^^^^^^^^^^^^^^^^^^ error: failed to load file `doesntexist`
31 "#,
32 );
33 }
34
35 #[test]
36 fn include_macro_should_allow_empty_content() {
37 let mut config = DiagnosticsConfig::default();
38
39 // FIXME: This is a false-positive, the file is actually linked in via
40 // `include!` macro
41 config.disabled.insert("unlinked-file".to_string());
42
43 check_diagnostics_with_config(
44 config,
45 r#"
46//- /lib.rs
47#[rustc_builtin_macro]
48macro_rules! include { () => {} }
49
50include!("foo/bar.rs");
51//- /foo/bar.rs
52// empty
53"#,
54 );
55 }
56
57 #[test]
58 fn good_out_dir_diagnostic() {
59 check_diagnostics(
60 r#"
61#[rustc_builtin_macro]
62macro_rules! include { () => {} }
63#[rustc_builtin_macro]
64macro_rules! env { () => {} }
65#[rustc_builtin_macro]
66macro_rules! concat { () => {} }
67
68 include!(concat!(env!("OUT_DIR"), "/out.rs"));
69//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: `OUT_DIR` not set, enable "run build scripts" to fix
70"#,
71 );
72 }
73
74 #[test]
75 fn register_attr_and_tool() {
76 cov_mark::check!(register_attr);
77 cov_mark::check!(register_tool);
78 check_diagnostics(
79 r#"
80#![register_tool(tool)]
81#![register_attr(attr)]
82
83#[tool::path]
84#[attr]
85struct S;
86"#,
87 );
88 // NB: we don't currently emit diagnostics here
89 }
90
91 #[test]
92 fn macro_diag_builtin() {
93 check_diagnostics(
94 r#"
95#[rustc_builtin_macro]
96macro_rules! env {}
97
98#[rustc_builtin_macro]
99macro_rules! include {}
100
101#[rustc_builtin_macro]
102macro_rules! compile_error {}
103
104#[rustc_builtin_macro]
105macro_rules! format_args { () => {} }
106
107fn main() {
108 // Test a handful of built-in (eager) macros:
109
110 include!(invalid);
111 //^^^^^^^^^^^^^^^^^ error: could not convert tokens
112 include!("does not exist");
113 //^^^^^^^^^^^^^^^^^^^^^^^^^^ error: failed to load file `does not exist`
114
115 env!(invalid);
116 //^^^^^^^^^^^^^ error: could not convert tokens
117
118 env!("OUT_DIR");
119 //^^^^^^^^^^^^^^^ error: `OUT_DIR` not set, enable "run build scripts" to fix
120
121 compile_error!("compile_error works");
122 //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: compile_error works
123
124 // Lazy:
125
126 format_args!();
127 //^^^^^^^^^^^^^^ error: no rule matches input tokens
128}
129"#,
130 );
131 }
132
133 #[test]
134 fn macro_rules_diag() {
135 check_diagnostics(
136 r#"
137macro_rules! m {
138 () => {};
139}
140fn f() {
141 m!();
142
143 m!(hi);
144 //^^^^^^ error: leftover tokens
145}
146 "#,
147 );
148 }
149 #[test]
150 fn dollar_crate_in_builtin_macro() {
151 check_diagnostics(
152 r#"
153#[macro_export]
154#[rustc_builtin_macro]
155macro_rules! format_args {}
156
157#[macro_export]
158macro_rules! arg { () => {} }
159
160#[macro_export]
161macro_rules! outer {
162 () => {
163 $crate::format_args!( "", $crate::arg!(1) )
164 };
165}
166
167fn f() {
168 outer!();
169} //^^^^^^^^ error: leftover tokens
170"#,
171 )
172 }
173}
diff --git a/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs b/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs
new file mode 100644
index 000000000..a9b6d3870
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs
@@ -0,0 +1,272 @@
1use crate::{Diagnostic, DiagnosticsContext};
2
3// Diagnostic: mismatched-arg-count
4//
5// This diagnostic is triggered if a function is invoked with an incorrect amount of arguments.
6pub(crate) fn mismatched_arg_count(
7 ctx: &DiagnosticsContext<'_>,
8 d: &hir::MismatchedArgCount,
9) -> Diagnostic {
10 let s = if d.expected == 1 { "" } else { "s" };
11 let message = format!("expected {} argument{}, found {}", d.expected, s, d.found);
12 Diagnostic::new(
13 "mismatched-arg-count",
14 message,
15 ctx.sema.diagnostics_display_range(d.call_expr.clone().map(|it| it.into())).range,
16 )
17}
18
19#[cfg(test)]
20mod tests {
21 use crate::tests::check_diagnostics;
22
23 #[test]
24 fn simple_free_fn_zero() {
25 check_diagnostics(
26 r#"
27fn zero() {}
28fn f() { zero(1); }
29 //^^^^^^^ error: expected 0 arguments, found 1
30"#,
31 );
32
33 check_diagnostics(
34 r#"
35fn zero() {}
36fn f() { zero(); }
37"#,
38 );
39 }
40
41 #[test]
42 fn simple_free_fn_one() {
43 check_diagnostics(
44 r#"
45fn one(arg: u8) {}
46fn f() { one(); }
47 //^^^^^ error: expected 1 argument, found 0
48"#,
49 );
50
51 check_diagnostics(
52 r#"
53fn one(arg: u8) {}
54fn f() { one(1); }
55"#,
56 );
57 }
58
59 #[test]
60 fn method_as_fn() {
61 check_diagnostics(
62 r#"
63struct S;
64impl S { fn method(&self) {} }
65
66fn f() {
67 S::method();
68} //^^^^^^^^^^^ error: expected 1 argument, found 0
69"#,
70 );
71
72 check_diagnostics(
73 r#"
74struct S;
75impl S { fn method(&self) {} }
76
77fn f() {
78 S::method(&S);
79 S.method();
80}
81"#,
82 );
83 }
84
85 #[test]
86 fn method_with_arg() {
87 check_diagnostics(
88 r#"
89struct S;
90impl S { fn method(&self, arg: u8) {} }
91
92 fn f() {
93 S.method();
94 } //^^^^^^^^^^ error: expected 1 argument, found 0
95 "#,
96 );
97
98 check_diagnostics(
99 r#"
100struct S;
101impl S { fn method(&self, arg: u8) {} }
102
103fn f() {
104 S::method(&S, 0);
105 S.method(1);
106}
107"#,
108 );
109 }
110
111 #[test]
112 fn method_unknown_receiver() {
113 // note: this is incorrect code, so there might be errors on this in the
114 // future, but we shouldn't emit an argument count diagnostic here
115 check_diagnostics(
116 r#"
117trait Foo { fn method(&self, arg: usize) {} }
118
119fn f() {
120 let x;
121 x.method();
122}
123"#,
124 );
125 }
126
127 #[test]
128 fn tuple_struct() {
129 check_diagnostics(
130 r#"
131struct Tup(u8, u16);
132fn f() {
133 Tup(0);
134} //^^^^^^ error: expected 2 arguments, found 1
135"#,
136 )
137 }
138
139 #[test]
140 fn enum_variant() {
141 check_diagnostics(
142 r#"
143enum En { Variant(u8, u16), }
144fn f() {
145 En::Variant(0);
146} //^^^^^^^^^^^^^^ error: expected 2 arguments, found 1
147"#,
148 )
149 }
150
151 #[test]
152 fn enum_variant_type_macro() {
153 check_diagnostics(
154 r#"
155macro_rules! Type {
156 () => { u32 };
157}
158enum Foo {
159 Bar(Type![])
160}
161impl Foo {
162 fn new() {
163 Foo::Bar(0);
164 Foo::Bar(0, 1);
165 //^^^^^^^^^^^^^^ error: expected 1 argument, found 2
166 Foo::Bar();
167 //^^^^^^^^^^ error: expected 1 argument, found 0
168 }
169}
170 "#,
171 );
172 }
173
174 #[test]
175 fn varargs() {
176 check_diagnostics(
177 r#"
178extern "C" {
179 fn fixed(fixed: u8);
180 fn varargs(fixed: u8, ...);
181 fn varargs2(...);
182}
183
184fn f() {
185 unsafe {
186 fixed(0);
187 fixed(0, 1);
188 //^^^^^^^^^^^ error: expected 1 argument, found 2
189 varargs(0);
190 varargs(0, 1);
191 varargs2();
192 varargs2(0);
193 varargs2(0, 1);
194 }
195}
196 "#,
197 )
198 }
199
200 #[test]
201 fn arg_count_lambda() {
202 check_diagnostics(
203 r#"
204fn main() {
205 let f = |()| ();
206 f();
207 //^^^ error: expected 1 argument, found 0
208 f(());
209 f((), ());
210 //^^^^^^^^^ error: expected 1 argument, found 2
211}
212"#,
213 )
214 }
215
216 #[test]
217 fn cfgd_out_call_arguments() {
218 check_diagnostics(
219 r#"
220struct C(#[cfg(FALSE)] ());
221impl C {
222 fn new() -> Self {
223 Self(
224 #[cfg(FALSE)]
225 (),
226 )
227 }
228
229 fn method(&self) {}
230}
231
232fn main() {
233 C::new().method(#[cfg(FALSE)] 0);
234}
235 "#,
236 );
237 }
238
239 #[test]
240 fn cfgd_out_fn_params() {
241 check_diagnostics(
242 r#"
243fn foo(#[cfg(NEVER)] x: ()) {}
244
245struct S;
246
247impl S {
248 fn method(#[cfg(NEVER)] self) {}
249 fn method2(#[cfg(NEVER)] self, arg: u8) {}
250 fn method3(self, #[cfg(NEVER)] arg: u8) {}
251}
252
253extern "C" {
254 fn fixed(fixed: u8, #[cfg(NEVER)] ...);
255 fn varargs(#[cfg(not(NEVER))] ...);
256}
257
258fn main() {
259 foo();
260 S::method();
261 S::method2(0);
262 S::method3(S);
263 S.method3();
264 unsafe {
265 fixed(0);
266 varargs(1, 2, 3);
267 }
268}
269 "#,
270 )
271 }
272}
diff --git a/crates/ide_diagnostics/src/handlers/missing_fields.rs b/crates/ide_diagnostics/src/handlers/missing_fields.rs
new file mode 100644
index 000000000..bc56e0342
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/missing_fields.rs
@@ -0,0 +1,355 @@
1use either::Either;
2use hir::{db::AstDatabase, InFile};
3use ide_db::{assists::Assist, source_change::SourceChange};
4use stdx::format_to;
5use syntax::{algo, ast::make, AstNode, SyntaxNodePtr};
6use text_edit::TextEdit;
7
8use crate::{fix, Diagnostic, DiagnosticsContext};
9
10// Diagnostic: missing-fields
11//
12// This diagnostic is triggered if record lacks some fields that exist in the corresponding structure.
13//
14// Example:
15//
16// ```rust
17// struct A { a: u8, b: u8 }
18//
19// let a = A { a: 10 };
20// ```
21pub(crate) fn missing_fields(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Diagnostic {
22 let mut message = String::from("missing structure fields:\n");
23 for field in &d.missed_fields {
24 format_to!(message, "- {}\n", field);
25 }
26
27 let ptr = InFile::new(
28 d.file,
29 d.field_list_parent_path
30 .clone()
31 .map(SyntaxNodePtr::from)
32 .unwrap_or_else(|| d.field_list_parent.clone().either(|it| it.into(), |it| it.into())),
33 );
34
35 Diagnostic::new("missing-fields", message, ctx.sema.diagnostics_display_range(ptr).range)
36 .with_fixes(fixes(ctx, d))
37}
38
39fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Assist>> {
40 // Note that although we could add a diagnostics to
41 // fill the missing tuple field, e.g :
42 // `struct A(usize);`
43 // `let a = A { 0: () }`
44 // but it is uncommon usage and it should not be encouraged.
45 if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
46 return None;
47 }
48
49 let root = ctx.sema.db.parse_or_expand(d.file)?;
50 let field_list_parent = match &d.field_list_parent {
51 Either::Left(record_expr) => record_expr.to_node(&root),
52 // FIXE: patterns should be fixable as well.
53 Either::Right(_) => return None,
54 };
55 let old_field_list = field_list_parent.record_expr_field_list()?;
56 let new_field_list = old_field_list.clone_for_update();
57 for f in d.missed_fields.iter() {
58 let field =
59 make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit()))
60 .clone_for_update();
61 new_field_list.add_field(field);
62 }
63
64 let edit = {
65 let mut builder = TextEdit::builder();
66 algo::diff(old_field_list.syntax(), new_field_list.syntax()).into_text_edit(&mut builder);
67 builder.finish()
68 };
69 Some(vec![fix(
70 "fill_missing_fields",
71 "Fill struct fields",
72 SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit),
73 ctx.sema.original_range(field_list_parent.syntax()).range,
74 )])
75}
76
77#[cfg(test)]
78mod tests {
79 use crate::tests::{check_diagnostics, check_fix};
80
81 #[test]
82 fn missing_record_pat_field_diagnostic() {
83 check_diagnostics(
84 r#"
85struct S { foo: i32, bar: () }
86fn baz(s: S) {
87 let S { foo: _ } = s;
88 //^ error: missing structure fields:
89 //| - bar
90}
91"#,
92 );
93 }
94
95 #[test]
96 fn missing_record_pat_field_no_diagnostic_if_not_exhaustive() {
97 check_diagnostics(
98 r"
99struct S { foo: i32, bar: () }
100fn baz(s: S) -> i32 {
101 match s {
102 S { foo, .. } => foo,
103 }
104}
105",
106 )
107 }
108
109 #[test]
110 fn missing_record_pat_field_box() {
111 check_diagnostics(
112 r"
113struct S { s: Box<u32> }
114fn x(a: S) {
115 let S { box s } = a;
116}
117",
118 )
119 }
120
121 #[test]
122 fn missing_record_pat_field_ref() {
123 check_diagnostics(
124 r"
125struct S { s: u32 }
126fn x(a: S) {
127 let S { ref s } = a;
128}
129",
130 )
131 }
132
133 #[test]
134 fn range_mapping_out_of_macros() {
135 // FIXME: this is very wrong, but somewhat tricky to fix.
136 check_fix(
137 r#"
138fn some() {}
139fn items() {}
140fn here() {}
141
142macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
143
144fn main() {
145 let _x = id![Foo { a: $042 }];
146}
147
148pub struct Foo { pub a: i32, pub b: i32 }
149"#,
150 r#"
151fn some(, b: () ) {}
152fn items() {}
153fn here() {}
154
155macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
156
157fn main() {
158 let _x = id![Foo { a: 42 }];
159}
160
161pub struct Foo { pub a: i32, pub b: i32 }
162"#,
163 );
164 }
165
166 #[test]
167 fn test_fill_struct_fields_empty() {
168 check_fix(
169 r#"
170struct TestStruct { one: i32, two: i64 }
171
172fn test_fn() {
173 let s = TestStruct {$0};
174}
175"#,
176 r#"
177struct TestStruct { one: i32, two: i64 }
178
179fn test_fn() {
180 let s = TestStruct { one: (), two: () };
181}
182"#,
183 );
184 }
185
186 #[test]
187 fn test_fill_struct_fields_self() {
188 check_fix(
189 r#"
190struct TestStruct { one: i32 }
191
192impl TestStruct {
193 fn test_fn() { let s = Self {$0}; }
194}
195"#,
196 r#"
197struct TestStruct { one: i32 }
198
199impl TestStruct {
200 fn test_fn() { let s = Self { one: () }; }
201}
202"#,
203 );
204 }
205
206 #[test]
207 fn test_fill_struct_fields_enum() {
208 check_fix(
209 r#"
210enum Expr {
211 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
212}
213
214impl Expr {
215 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
216 Expr::Bin {$0 }
217 }
218}
219"#,
220 r#"
221enum Expr {
222 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
223}
224
225impl Expr {
226 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
227 Expr::Bin { lhs: (), rhs: () }
228 }
229}
230"#,
231 );
232 }
233
234 #[test]
235 fn test_fill_struct_fields_partial() {
236 check_fix(
237 r#"
238struct TestStruct { one: i32, two: i64 }
239
240fn test_fn() {
241 let s = TestStruct{ two: 2$0 };
242}
243"#,
244 r"
245struct TestStruct { one: i32, two: i64 }
246
247fn test_fn() {
248 let s = TestStruct{ two: 2, one: () };
249}
250",
251 );
252 }
253
254 #[test]
255 fn test_fill_struct_fields_raw_ident() {
256 check_fix(
257 r#"
258struct TestStruct { r#type: u8 }
259
260fn test_fn() {
261 TestStruct { $0 };
262}
263"#,
264 r"
265struct TestStruct { r#type: u8 }
266
267fn test_fn() {
268 TestStruct { r#type: () };
269}
270",
271 );
272 }
273
274 #[test]
275 fn test_fill_struct_fields_no_diagnostic() {
276 check_diagnostics(
277 r#"
278struct TestStruct { one: i32, two: i64 }
279
280fn test_fn() {
281 let one = 1;
282 let s = TestStruct{ one, two: 2 };
283}
284 "#,
285 );
286 }
287
288 #[test]
289 fn test_fill_struct_fields_no_diagnostic_on_spread() {
290 check_diagnostics(
291 r#"
292struct TestStruct { one: i32, two: i64 }
293
294fn test_fn() {
295 let one = 1;
296 let s = TestStruct{ ..a };
297}
298"#,
299 );
300 }
301
302 #[test]
303 fn test_fill_struct_fields_blank_line() {
304 check_fix(
305 r#"
306struct S { a: (), b: () }
307
308fn f() {
309 S {
310 $0
311 };
312}
313"#,
314 r#"
315struct S { a: (), b: () }
316
317fn f() {
318 S {
319 a: (),
320 b: (),
321 };
322}
323"#,
324 );
325 }
326
327 #[test]
328 fn import_extern_crate_clash_with_inner_item() {
329 // This is more of a resolver test, but doesn't really work with the hir_def testsuite.
330
331 check_diagnostics(
332 r#"
333//- /lib.rs crate:lib deps:jwt
334mod permissions;
335
336use permissions::jwt;
337
338fn f() {
339 fn inner() {}
340 jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic
341}
342
343//- /permissions.rs
344pub mod jwt {
345 pub struct Claims {}
346}
347
348//- /jwt/lib.rs crate:jwt
349pub struct Claims {
350 field: u8,
351}
352 "#,
353 );
354 }
355}
diff --git a/crates/ide_diagnostics/src/handlers/missing_match_arms.rs b/crates/ide_diagnostics/src/handlers/missing_match_arms.rs
new file mode 100644
index 000000000..947b0f2e2
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/missing_match_arms.rs
@@ -0,0 +1,929 @@
1use hir::InFile;
2
3use crate::{Diagnostic, DiagnosticsContext};
4
5// Diagnostic: missing-match-arm
6//
7// This diagnostic is triggered if `match` block is missing one or more match arms.
8pub(crate) fn missing_match_arms(
9 ctx: &DiagnosticsContext<'_>,
10 d: &hir::MissingMatchArms,
11) -> Diagnostic {
12 Diagnostic::new(
13 "missing-match-arm",
14 "missing match arm",
15 ctx.sema.diagnostics_display_range(InFile::new(d.file, d.match_expr.clone().into())).range,
16 )
17}
18
19#[cfg(test)]
20mod tests {
21 use crate::tests::check_diagnostics;
22
23 fn check_diagnostics_no_bails(ra_fixture: &str) {
24 cov_mark::check_count!(validate_match_bailed_out, 0);
25 crate::tests::check_diagnostics(ra_fixture)
26 }
27
28 #[test]
29 fn empty_tuple() {
30 check_diagnostics_no_bails(
31 r#"
32fn main() {
33 match () { }
34 //^^ error: missing match arm
35 match (()) { }
36 //^^^^ error: missing match arm
37
38 match () { _ => (), }
39 match () { () => (), }
40 match (()) { (()) => (), }
41}
42"#,
43 );
44 }
45
46 #[test]
47 fn tuple_of_two_empty_tuple() {
48 check_diagnostics_no_bails(
49 r#"
50fn main() {
51 match ((), ()) { }
52 //^^^^^^^^ error: missing match arm
53
54 match ((), ()) { ((), ()) => (), }
55}
56"#,
57 );
58 }
59
60 #[test]
61 fn boolean() {
62 check_diagnostics_no_bails(
63 r#"
64fn test_main() {
65 match false { }
66 //^^^^^ error: missing match arm
67 match false { true => (), }
68 //^^^^^ error: missing match arm
69 match (false, true) {}
70 //^^^^^^^^^^^^^ error: missing match arm
71 match (false, true) { (true, true) => (), }
72 //^^^^^^^^^^^^^ error: missing match arm
73 match (false, true) {
74 //^^^^^^^^^^^^^ error: missing match arm
75 (false, true) => (),
76 (false, false) => (),
77 (true, false) => (),
78 }
79 match (false, true) { (true, _x) => (), }
80 //^^^^^^^^^^^^^ error: missing match arm
81
82 match false { true => (), false => (), }
83 match (false, true) {
84 (false, _) => (),
85 (true, false) => (),
86 (_, true) => (),
87 }
88 match (false, true) {
89 (true, true) => (),
90 (true, false) => (),
91 (false, true) => (),
92 (false, false) => (),
93 }
94 match (false, true) {
95 (true, _x) => (),
96 (false, true) => (),
97 (false, false) => (),
98 }
99 match (false, true, false) {
100 (false, ..) => (),
101 (true, ..) => (),
102 }
103 match (false, true, false) {
104 (.., false) => (),
105 (.., true) => (),
106 }
107 match (false, true, false) { (..) => (), }
108}
109"#,
110 );
111 }
112
113 #[test]
114 fn tuple_of_tuple_and_bools() {
115 check_diagnostics_no_bails(
116 r#"
117fn main() {
118 match (false, ((), false)) {}
119 //^^^^^^^^^^^^^^^^^^^^ error: missing match arm
120 match (false, ((), false)) { (true, ((), true)) => (), }
121 //^^^^^^^^^^^^^^^^^^^^ error: missing match arm
122 match (false, ((), false)) { (true, _) => (), }
123 //^^^^^^^^^^^^^^^^^^^^ error: missing match arm
124
125 match (false, ((), false)) {
126 (true, ((), true)) => (),
127 (true, ((), false)) => (),
128 (false, ((), true)) => (),
129 (false, ((), false)) => (),
130 }
131 match (false, ((), false)) {
132 (true, ((), true)) => (),
133 (true, ((), false)) => (),
134 (false, _) => (),
135 }
136}
137"#,
138 );
139 }
140
141 #[test]
142 fn enums() {
143 check_diagnostics_no_bails(
144 r#"
145enum Either { A, B, }
146
147fn main() {
148 match Either::A { }
149 //^^^^^^^^^ error: missing match arm
150 match Either::B { Either::A => (), }
151 //^^^^^^^^^ error: missing match arm
152
153 match &Either::B {
154 //^^^^^^^^^^ error: missing match arm
155 Either::A => (),
156 }
157
158 match Either::B {
159 Either::A => (), Either::B => (),
160 }
161 match &Either::B {
162 Either::A => (), Either::B => (),
163 }
164}
165"#,
166 );
167 }
168
169 #[test]
170 fn enum_containing_bool() {
171 check_diagnostics_no_bails(
172 r#"
173enum Either { A(bool), B }
174
175fn main() {
176 match Either::B { }
177 //^^^^^^^^^ error: missing match arm
178 match Either::B {
179 //^^^^^^^^^ error: missing match arm
180 Either::A(true) => (), Either::B => ()
181 }
182
183 match Either::B {
184 Either::A(true) => (),
185 Either::A(false) => (),
186 Either::B => (),
187 }
188 match Either::B {
189 Either::B => (),
190 _ => (),
191 }
192 match Either::B {
193 Either::A(_) => (),
194 Either::B => (),
195 }
196
197}
198 "#,
199 );
200 }
201
202 #[test]
203 fn enum_different_sizes() {
204 check_diagnostics_no_bails(
205 r#"
206enum Either { A(bool), B(bool, bool) }
207
208fn main() {
209 match Either::A(false) {
210 //^^^^^^^^^^^^^^^^ error: missing match arm
211 Either::A(_) => (),
212 Either::B(false, _) => (),
213 }
214
215 match Either::A(false) {
216 Either::A(_) => (),
217 Either::B(true, _) => (),
218 Either::B(false, _) => (),
219 }
220 match Either::A(false) {
221 Either::A(true) | Either::A(false) => (),
222 Either::B(true, _) => (),
223 Either::B(false, _) => (),
224 }
225}
226"#,
227 );
228 }
229
230 #[test]
231 fn tuple_of_enum_no_diagnostic() {
232 check_diagnostics_no_bails(
233 r#"
234enum Either { A(bool), B(bool, bool) }
235enum Either2 { C, D }
236
237fn main() {
238 match (Either::A(false), Either2::C) {
239 (Either::A(true), _) | (Either::A(false), _) => (),
240 (Either::B(true, _), Either2::C) => (),
241 (Either::B(false, _), Either2::C) => (),
242 (Either::B(_, _), Either2::D) => (),
243 }
244}
245"#,
246 );
247 }
248
249 #[test]
250 fn or_pattern_no_diagnostic() {
251 check_diagnostics_no_bails(
252 r#"
253enum Either {A, B}
254
255fn main() {
256 match (Either::A, Either::B) {
257 (Either::A | Either::B, _) => (),
258 }
259}"#,
260 )
261 }
262
263 #[test]
264 fn mismatched_types() {
265 cov_mark::check_count!(validate_match_bailed_out, 4);
266 // Match statements with arms that don't match the
267 // expression pattern do not fire this diagnostic.
268 check_diagnostics(
269 r#"
270enum Either { A, B }
271enum Either2 { C, D }
272
273fn main() {
274 match Either::A {
275 Either2::C => (),
276 Either2::D => (),
277 }
278 match (true, false) {
279 (true, false, true) => (),
280 (true) => (),
281 }
282 match (true, false) { (true,) => {} }
283 match (0) { () => () }
284 match Unresolved::Bar { Unresolved::Baz => () }
285}
286 "#,
287 );
288 }
289
290 #[test]
291 fn mismatched_types_in_or_patterns() {
292 cov_mark::check_count!(validate_match_bailed_out, 2);
293 check_diagnostics(
294 r#"
295fn main() {
296 match false { true | () => {} }
297 match (false,) { (true | (),) => {} }
298}
299"#,
300 );
301 }
302
303 #[test]
304 fn malformed_match_arm_tuple_enum_missing_pattern() {
305 // We are testing to be sure we don't panic here when the match
306 // arm `Either::B` is missing its pattern.
307 check_diagnostics_no_bails(
308 r#"
309enum Either { A, B(u32) }
310
311fn main() {
312 match Either::A {
313 Either::A => (),
314 Either::B() => (),
315 }
316}
317"#,
318 );
319 }
320
321 #[test]
322 fn malformed_match_arm_extra_fields() {
323 cov_mark::check_count!(validate_match_bailed_out, 2);
324 check_diagnostics(
325 r#"
326enum A { B(isize, isize), C }
327fn main() {
328 match A::B(1, 2) {
329 A::B(_, _, _) => (),
330 }
331 match A::B(1, 2) {
332 A::C(_) => (),
333 }
334}
335"#,
336 );
337 }
338
339 #[test]
340 fn expr_diverges() {
341 cov_mark::check_count!(validate_match_bailed_out, 2);
342 check_diagnostics(
343 r#"
344enum Either { A, B }
345
346fn main() {
347 match loop {} {
348 Either::A => (),
349 Either::B => (),
350 }
351 match loop {} {
352 Either::A => (),
353 }
354 match loop { break Foo::A } {
355 //^^^^^^^^^^^^^^^^^^^^^ error: missing match arm
356 Either::A => (),
357 }
358 match loop { break Foo::A } {
359 Either::A => (),
360 Either::B => (),
361 }
362}
363"#,
364 );
365 }
366
367 #[test]
368 fn expr_partially_diverges() {
369 check_diagnostics_no_bails(
370 r#"
371enum Either<T> { A(T), B }
372
373fn foo() -> Either<!> { Either::B }
374fn main() -> u32 {
375 match foo() {
376 Either::A(val) => val,
377 Either::B => 0,
378 }
379}
380"#,
381 );
382 }
383
384 #[test]
385 fn enum_record() {
386 check_diagnostics_no_bails(
387 r#"
388enum Either { A { foo: bool }, B }
389
390fn main() {
391 let a = Either::A { foo: true };
392 match a { }
393 //^ error: missing match arm
394 match a { Either::A { foo: true } => () }
395 //^ error: missing match arm
396 match a {
397 Either::A { } => (),
398 //^^^^^^^^^ error: missing structure fields:
399 // | - foo
400 Either::B => (),
401 }
402 match a {
403 //^ error: missing match arm
404 Either::A { } => (),
405 } //^^^^^^^^^ error: missing structure fields:
406 // | - foo
407
408 match a {
409 Either::A { foo: true } => (),
410 Either::A { foo: false } => (),
411 Either::B => (),
412 }
413 match a {
414 Either::A { foo: _ } => (),
415 Either::B => (),
416 }
417}
418"#,
419 );
420 }
421
422 #[test]
423 fn enum_record_fields_out_of_order() {
424 check_diagnostics_no_bails(
425 r#"
426enum Either {
427 A { foo: bool, bar: () },
428 B,
429}
430
431fn main() {
432 let a = Either::A { foo: true, bar: () };
433 match a {
434 //^ error: missing match arm
435 Either::A { bar: (), foo: false } => (),
436 Either::A { foo: true, bar: () } => (),
437 }
438
439 match a {
440 Either::A { bar: (), foo: false } => (),
441 Either::A { foo: true, bar: () } => (),
442 Either::B => (),
443 }
444}
445"#,
446 );
447 }
448
449 #[test]
450 fn enum_record_ellipsis() {
451 check_diagnostics_no_bails(
452 r#"
453enum Either {
454 A { foo: bool, bar: bool },
455 B,
456}
457
458fn main() {
459 let a = Either::B;
460 match a {
461 //^ error: missing match arm
462 Either::A { foo: true, .. } => (),
463 Either::B => (),
464 }
465 match a {
466 //^ error: missing match arm
467 Either::A { .. } => (),
468 }
469
470 match a {
471 Either::A { foo: true, .. } => (),
472 Either::A { foo: false, .. } => (),
473 Either::B => (),
474 }
475
476 match a {
477 Either::A { .. } => (),
478 Either::B => (),
479 }
480}
481"#,
482 );
483 }
484
485 #[test]
486 fn enum_tuple_partial_ellipsis() {
487 check_diagnostics_no_bails(
488 r#"
489enum Either {
490 A(bool, bool, bool, bool),
491 B,
492}
493
494fn main() {
495 match Either::B {
496 //^^^^^^^^^ error: missing match arm
497 Either::A(true, .., true) => (),
498 Either::A(true, .., false) => (),
499 Either::A(false, .., false) => (),
500 Either::B => (),
501 }
502 match Either::B {
503 //^^^^^^^^^ error: missing match arm
504 Either::A(true, .., true) => (),
505 Either::A(true, .., false) => (),
506 Either::A(.., true) => (),
507 Either::B => (),
508 }
509
510 match Either::B {
511 Either::A(true, .., true) => (),
512 Either::A(true, .., false) => (),
513 Either::A(false, .., true) => (),
514 Either::A(false, .., false) => (),
515 Either::B => (),
516 }
517 match Either::B {
518 Either::A(true, .., true) => (),
519 Either::A(true, .., false) => (),
520 Either::A(.., true) => (),
521 Either::A(.., false) => (),
522 Either::B => (),
523 }
524}
525"#,
526 );
527 }
528
529 #[test]
530 fn never() {
531 check_diagnostics_no_bails(
532 r#"
533enum Never {}
534
535fn enum_(never: Never) {
536 match never {}
537}
538fn enum_ref(never: &Never) {
539 match never {}
540 //^^^^^ error: missing match arm
541}
542fn bang(never: !) {
543 match never {}
544}
545"#,
546 );
547 }
548
549 #[test]
550 fn unknown_type() {
551 cov_mark::check_count!(validate_match_bailed_out, 1);
552
553 check_diagnostics(
554 r#"
555enum Option<T> { Some(T), None }
556
557fn main() {
558 // `Never` is deliberately not defined so that it's an uninferred type.
559 match Option::<Never>::None {
560 None => (),
561 Some(never) => match never {},
562 }
563 match Option::<Never>::None {
564 //^^^^^^^^^^^^^^^^^^^^^ error: missing match arm
565 Option::Some(_never) => {},
566 }
567}
568"#,
569 );
570 }
571
572 #[test]
573 fn tuple_of_bools_with_ellipsis_at_end_missing_arm() {
574 check_diagnostics_no_bails(
575 r#"
576fn main() {
577 match (false, true, false) {
578 //^^^^^^^^^^^^^^^^^^^^ error: missing match arm
579 (false, ..) => (),
580 }
581}"#,
582 );
583 }
584
585 #[test]
586 fn tuple_of_bools_with_ellipsis_at_beginning_missing_arm() {
587 check_diagnostics_no_bails(
588 r#"
589fn main() {
590 match (false, true, false) {
591 //^^^^^^^^^^^^^^^^^^^^ error: missing match arm
592 (.., false) => (),
593 }
594}"#,
595 );
596 }
597
598 #[test]
599 fn tuple_of_bools_with_ellipsis_in_middle_missing_arm() {
600 check_diagnostics_no_bails(
601 r#"
602fn main() {
603 match (false, true, false) {
604 //^^^^^^^^^^^^^^^^^^^^ error: missing match arm
605 (true, .., false) => (),
606 }
607}"#,
608 );
609 }
610
611 #[test]
612 fn record_struct() {
613 check_diagnostics_no_bails(
614 r#"struct Foo { a: bool }
615fn main(f: Foo) {
616 match f {}
617 //^ error: missing match arm
618 match f { Foo { a: true } => () }
619 //^ error: missing match arm
620 match &f { Foo { a: true } => () }
621 //^^ error: missing match arm
622 match f { Foo { a: _ } => () }
623 match f {
624 Foo { a: true } => (),
625 Foo { a: false } => (),
626 }
627 match &f {
628 Foo { a: true } => (),
629 Foo { a: false } => (),
630 }
631}
632"#,
633 );
634 }
635
636 #[test]
637 fn tuple_struct() {
638 check_diagnostics_no_bails(
639 r#"struct Foo(bool);
640fn main(f: Foo) {
641 match f {}
642 //^ error: missing match arm
643 match f { Foo(true) => () }
644 //^ error: missing match arm
645 match f {
646 Foo(true) => (),
647 Foo(false) => (),
648 }
649}
650"#,
651 );
652 }
653
654 #[test]
655 fn unit_struct() {
656 check_diagnostics_no_bails(
657 r#"struct Foo;
658fn main(f: Foo) {
659 match f {}
660 //^ error: missing match arm
661 match f { Foo => () }
662}
663"#,
664 );
665 }
666
667 #[test]
668 fn record_struct_ellipsis() {
669 check_diagnostics_no_bails(
670 r#"struct Foo { foo: bool, bar: bool }
671fn main(f: Foo) {
672 match f { Foo { foo: true, .. } => () }
673 //^ error: missing match arm
674 match f {
675 //^ error: missing match arm
676 Foo { foo: true, .. } => (),
677 Foo { bar: false, .. } => ()
678 }
679 match f { Foo { .. } => () }
680 match f {
681 Foo { foo: true, .. } => (),
682 Foo { foo: false, .. } => ()
683 }
684}
685"#,
686 );
687 }
688
689 #[test]
690 fn internal_or() {
691 check_diagnostics_no_bails(
692 r#"
693fn main() {
694 enum Either { A(bool), B }
695 match Either::B {
696 //^^^^^^^^^ error: missing match arm
697 Either::A(true | false) => (),
698 }
699}
700"#,
701 );
702 }
703
704 #[test]
705 fn no_panic_at_unimplemented_subpattern_type() {
706 cov_mark::check_count!(validate_match_bailed_out, 1);
707
708 check_diagnostics(
709 r#"
710struct S { a: char}
711fn main(v: S) {
712 match v { S{ a } => {} }
713 match v { S{ a: _x } => {} }
714 match v { S{ a: 'a' } => {} }
715 match v { S{..} => {} }
716 match v { _ => {} }
717 match v { }
718 //^ error: missing match arm
719}
720"#,
721 );
722 }
723
724 #[test]
725 fn binding() {
726 check_diagnostics_no_bails(
727 r#"
728fn main() {
729 match true {
730 _x @ true => {}
731 false => {}
732 }
733 match true { _x @ true => {} }
734 //^^^^ error: missing match arm
735}
736"#,
737 );
738 }
739
740 #[test]
741 fn binding_ref_has_correct_type() {
742 cov_mark::check_count!(validate_match_bailed_out, 1);
743
744 // Asserts `PatKind::Binding(ref _x): bool`, not &bool.
745 // If that's not true match checking will panic with "incompatible constructors"
746 // FIXME: make facilities to test this directly like `tests::check_infer(..)`
747 check_diagnostics(
748 r#"
749enum Foo { A }
750fn main() {
751 // FIXME: this should not bail out but current behavior is such as the old algorithm.
752 // ExprValidator::validate_match(..) checks types of top level patterns incorrecly.
753 match Foo::A {
754 ref _x => {}
755 Foo::A => {}
756 }
757 match (true,) {
758 (ref _x,) => {}
759 (true,) => {}
760 }
761}
762"#,
763 );
764 }
765
766 #[test]
767 fn enum_non_exhaustive() {
768 check_diagnostics_no_bails(
769 r#"
770//- /lib.rs crate:lib
771#[non_exhaustive]
772pub enum E { A, B }
773fn _local() {
774 match E::A { _ => {} }
775 match E::A {
776 E::A => {}
777 E::B => {}
778 }
779 match E::A {
780 E::A | E::B => {}
781 }
782}
783
784//- /main.rs crate:main deps:lib
785use lib::E;
786fn main() {
787 match E::A { _ => {} }
788 match E::A {
789 //^^^^ error: missing match arm
790 E::A => {}
791 E::B => {}
792 }
793 match E::A {
794 //^^^^ error: missing match arm
795 E::A | E::B => {}
796 }
797}
798"#,
799 );
800 }
801
802 #[test]
803 fn match_guard() {
804 check_diagnostics_no_bails(
805 r#"
806fn main() {
807 match true {
808 true if false => {}
809 true => {}
810 false => {}
811 }
812 match true {
813 //^^^^ error: missing match arm
814 true if false => {}
815 false => {}
816 }
817}
818"#,
819 );
820 }
821
822 #[test]
823 fn pattern_type_is_of_substitution() {
824 cov_mark::check!(match_check_wildcard_expanded_to_substitutions);
825 check_diagnostics_no_bails(
826 r#"
827struct Foo<T>(T);
828struct Bar;
829fn main() {
830 match Foo(Bar) {
831 _ | Foo(Bar) => {}
832 }
833}
834"#,
835 );
836 }
837
838 #[test]
839 fn record_struct_no_such_field() {
840 cov_mark::check_count!(validate_match_bailed_out, 1);
841
842 check_diagnostics(
843 r#"
844struct Foo { }
845fn main(f: Foo) {
846 match f { Foo { bar } => () }
847}
848"#,
849 );
850 }
851
852 #[test]
853 fn match_ergonomics_issue_9095() {
854 check_diagnostics_no_bails(
855 r#"
856enum Foo<T> { A(T) }
857fn main() {
858 match &Foo::A(true) {
859 _ => {}
860 Foo::A(_) => {}
861 }
862}
863"#,
864 );
865 }
866
867 mod false_negatives {
868 //! The implementation of match checking here is a work in progress. As we roll this out, we
869 //! prefer false negatives to false positives (ideally there would be no false positives). This
870 //! test module should document known false negatives. Eventually we will have a complete
871 //! implementation of match checking and this module will be empty.
872 //!
873 //! The reasons for documenting known false negatives:
874 //!
875 //! 1. It acts as a backlog of work that can be done to improve the behavior of the system.
876 //! 2. It ensures the code doesn't panic when handling these cases.
877 use super::*;
878
879 #[test]
880 fn integers() {
881 cov_mark::check_count!(validate_match_bailed_out, 1);
882
883 // We don't currently check integer exhaustiveness.
884 check_diagnostics(
885 r#"
886fn main() {
887 match 5 {
888 10 => (),
889 11..20 => (),
890 }
891}
892"#,
893 );
894 }
895
896 #[test]
897 fn reference_patterns_at_top_level() {
898 cov_mark::check_count!(validate_match_bailed_out, 1);
899
900 check_diagnostics(
901 r#"
902fn main() {
903 match &false {
904 &true => {}
905 }
906}
907 "#,
908 );
909 }
910
911 #[test]
912 fn reference_patterns_in_fields() {
913 cov_mark::check_count!(validate_match_bailed_out, 2);
914
915 check_diagnostics(
916 r#"
917fn main() {
918 match (&false,) {
919 (true,) => {}
920 }
921 match (&false,) {
922 (&true,) => {}
923 }
924}
925 "#,
926 );
927 }
928 }
929}
diff --git a/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs b/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs
new file mode 100644
index 000000000..63de54570
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs
@@ -0,0 +1,229 @@
1use hir::db::AstDatabase;
2use ide_db::{assists::Assist, source_change::SourceChange};
3use syntax::AstNode;
4use text_edit::TextEdit;
5
6use crate::{fix, Diagnostic, DiagnosticsContext};
7
8// Diagnostic: missing-ok-or-some-in-tail-expr
9//
10// This diagnostic is triggered if a block that should return `Result` returns a value not wrapped in `Ok`,
11// or if a block that should return `Option` returns a value not wrapped in `Some`.
12//
13// Example:
14//
15// ```rust
16// fn foo() -> Result<u8, ()> {
17// 10
18// }
19// ```
20pub(crate) fn missing_ok_or_some_in_tail_expr(
21 ctx: &DiagnosticsContext<'_>,
22 d: &hir::MissingOkOrSomeInTailExpr,
23) -> Diagnostic {
24 Diagnostic::new(
25 "missing-ok-or-some-in-tail-expr",
26 format!("wrap return expression in {}", d.required),
27 ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
28 )
29 .with_fixes(fixes(ctx, d))
30}
31
32fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingOkOrSomeInTailExpr) -> Option<Vec<Assist>> {
33 let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
34 let tail_expr = d.expr.value.to_node(&root);
35 let tail_expr_range = tail_expr.syntax().text_range();
36 let replacement = format!("{}({})", d.required, tail_expr.syntax());
37 let edit = TextEdit::replace(tail_expr_range, replacement);
38 let source_change =
39 SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
40 let name = if d.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
41 Some(vec![fix("wrap_tail_expr", name, source_change, tail_expr_range)])
42}
43
44#[cfg(test)]
45mod tests {
46 use crate::tests::{check_diagnostics, check_fix};
47
48 #[test]
49 fn test_wrap_return_type_option() {
50 check_fix(
51 r#"
52//- /main.rs crate:main deps:core
53use core::option::Option::{self, Some, None};
54
55fn div(x: i32, y: i32) -> Option<i32> {
56 if y == 0 {
57 return None;
58 }
59 x / y$0
60}
61//- /core/lib.rs crate:core
62pub mod result {
63 pub enum Result<T, E> { Ok(T), Err(E) }
64}
65pub mod option {
66 pub enum Option<T> { Some(T), None }
67}
68"#,
69 r#"
70use core::option::Option::{self, Some, None};
71
72fn div(x: i32, y: i32) -> Option<i32> {
73 if y == 0 {
74 return None;
75 }
76 Some(x / y)
77}
78"#,
79 );
80 }
81
82 #[test]
83 fn test_wrap_return_type() {
84 check_fix(
85 r#"
86//- /main.rs crate:main deps:core
87use core::result::Result::{self, Ok, Err};
88
89fn div(x: i32, y: i32) -> Result<i32, ()> {
90 if y == 0 {
91 return Err(());
92 }
93 x / y$0
94}
95//- /core/lib.rs crate:core
96pub mod result {
97 pub enum Result<T, E> { Ok(T), Err(E) }
98}
99pub mod option {
100 pub enum Option<T> { Some(T), None }
101}
102"#,
103 r#"
104use core::result::Result::{self, Ok, Err};
105
106fn div(x: i32, y: i32) -> Result<i32, ()> {
107 if y == 0 {
108 return Err(());
109 }
110 Ok(x / y)
111}
112"#,
113 );
114 }
115
116 #[test]
117 fn test_wrap_return_type_handles_generic_functions() {
118 check_fix(
119 r#"
120//- /main.rs crate:main deps:core
121use core::result::Result::{self, Ok, Err};
122
123fn div<T>(x: T) -> Result<T, i32> {
124 if x == 0 {
125 return Err(7);
126 }
127 $0x
128}
129//- /core/lib.rs crate:core
130pub mod result {
131 pub enum Result<T, E> { Ok(T), Err(E) }
132}
133pub mod option {
134 pub enum Option<T> { Some(T), None }
135}
136"#,
137 r#"
138use core::result::Result::{self, Ok, Err};
139
140fn div<T>(x: T) -> Result<T, i32> {
141 if x == 0 {
142 return Err(7);
143 }
144 Ok(x)
145}
146"#,
147 );
148 }
149
150 #[test]
151 fn test_wrap_return_type_handles_type_aliases() {
152 check_fix(
153 r#"
154//- /main.rs crate:main deps:core
155use core::result::Result::{self, Ok, Err};
156
157type MyResult<T> = Result<T, ()>;
158
159fn div(x: i32, y: i32) -> MyResult<i32> {
160 if y == 0 {
161 return Err(());
162 }
163 x $0/ y
164}
165//- /core/lib.rs crate:core
166pub mod result {
167 pub enum Result<T, E> { Ok(T), Err(E) }
168}
169pub mod option {
170 pub enum Option<T> { Some(T), None }
171}
172"#,
173 r#"
174use core::result::Result::{self, Ok, Err};
175
176type MyResult<T> = Result<T, ()>;
177
178fn div(x: i32, y: i32) -> MyResult<i32> {
179 if y == 0 {
180 return Err(());
181 }
182 Ok(x / y)
183}
184"#,
185 );
186 }
187
188 #[test]
189 fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
190 check_diagnostics(
191 r#"
192//- /main.rs crate:main deps:core
193use core::result::Result::{self, Ok, Err};
194
195fn foo() -> Result<(), i32> { 0 }
196
197//- /core/lib.rs crate:core
198pub mod result {
199 pub enum Result<T, E> { Ok(T), Err(E) }
200}
201pub mod option {
202 pub enum Option<T> { Some(T), None }
203}
204"#,
205 );
206 }
207
208 #[test]
209 fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() {
210 check_diagnostics(
211 r#"
212//- /main.rs crate:main deps:core
213use core::result::Result::{self, Ok, Err};
214
215enum SomeOtherEnum { Ok(i32), Err(String) }
216
217fn foo() -> SomeOtherEnum { 0 }
218
219//- /core/lib.rs crate:core
220pub mod result {
221 pub enum Result<T, E> { Ok(T), Err(E) }
222}
223pub mod option {
224 pub enum Option<T> { Some(T), None }
225}
226"#,
227 );
228 }
229}
diff --git a/crates/ide_diagnostics/src/handlers/missing_unsafe.rs b/crates/ide_diagnostics/src/handlers/missing_unsafe.rs
new file mode 100644
index 000000000..7acd9228a
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/missing_unsafe.rs
@@ -0,0 +1,101 @@
1use crate::{Diagnostic, DiagnosticsContext};
2
3// Diagnostic: missing-unsafe
4//
5// This diagnostic is triggered if an operation marked as `unsafe` is used outside of an `unsafe` function or block.
6pub(crate) fn missing_unsafe(ctx: &DiagnosticsContext<'_>, d: &hir::MissingUnsafe) -> Diagnostic {
7 Diagnostic::new(
8 "missing-unsafe",
9 "this operation is unsafe and requires an unsafe function or block",
10 ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
11 )
12}
13
14#[cfg(test)]
15mod tests {
16 use crate::tests::check_diagnostics;
17
18 #[test]
19 fn missing_unsafe_diagnostic_with_raw_ptr() {
20 check_diagnostics(
21 r#"
22fn main() {
23 let x = &5 as *const usize;
24 unsafe { let y = *x; }
25 let z = *x;
26} //^^ error: this operation is unsafe and requires an unsafe function or block
27"#,
28 )
29 }
30
31 #[test]
32 fn missing_unsafe_diagnostic_with_unsafe_call() {
33 check_diagnostics(
34 r#"
35struct HasUnsafe;
36
37impl HasUnsafe {
38 unsafe fn unsafe_fn(&self) {
39 let x = &5 as *const usize;
40 let y = *x;
41 }
42}
43
44unsafe fn unsafe_fn() {
45 let x = &5 as *const usize;
46 let y = *x;
47}
48
49fn main() {
50 unsafe_fn();
51 //^^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
52 HasUnsafe.unsafe_fn();
53 //^^^^^^^^^^^^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
54 unsafe {
55 unsafe_fn();
56 HasUnsafe.unsafe_fn();
57 }
58}
59"#,
60 );
61 }
62
63 #[test]
64 fn missing_unsafe_diagnostic_with_static_mut() {
65 check_diagnostics(
66 r#"
67struct Ty {
68 a: u8,
69}
70
71static mut STATIC_MUT: Ty = Ty { a: 0 };
72
73fn main() {
74 let x = STATIC_MUT.a;
75 //^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
76 unsafe {
77 let x = STATIC_MUT.a;
78 }
79}
80"#,
81 );
82 }
83
84 #[test]
85 fn no_missing_unsafe_diagnostic_with_safe_intrinsic() {
86 check_diagnostics(
87 r#"
88extern "rust-intrinsic" {
89 pub fn bitreverse(x: u32) -> u32; // Safe intrinsic
90 pub fn floorf32(x: f32) -> f32; // Unsafe intrinsic
91}
92
93fn main() {
94 let _ = bitreverse(12);
95 let _ = floorf32(12.0);
96 //^^^^^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
97}
98"#,
99 );
100 }
101}
diff --git a/crates/ide_diagnostics/src/handlers/no_such_field.rs b/crates/ide_diagnostics/src/handlers/no_such_field.rs
new file mode 100644
index 000000000..92e8867f4
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/no_such_field.rs
@@ -0,0 +1,283 @@
1use hir::{db::AstDatabase, HasSource, HirDisplay, Semantics};
2use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase};
3use syntax::{
4 ast::{self, edit::IndentLevel, make},
5 AstNode,
6};
7use text_edit::TextEdit;
8
9use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
10
11// Diagnostic: no-such-field
12//
13// This diagnostic is triggered if created structure does not have field provided in record.
14pub(crate) fn no_such_field(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Diagnostic {
15 Diagnostic::new(
16 "no-such-field",
17 "no such field",
18 ctx.sema.diagnostics_display_range(d.field.clone().map(|it| it.into())).range,
19 )
20 .with_fixes(fixes(ctx, d))
21}
22
23fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Option<Vec<Assist>> {
24 let root = ctx.sema.db.parse_or_expand(d.field.file_id)?;
25 missing_record_expr_field_fixes(
26 &ctx.sema,
27 d.field.file_id.original_file(ctx.sema.db),
28 &d.field.value.to_node(&root),
29 )
30}
31
32fn missing_record_expr_field_fixes(
33 sema: &Semantics<RootDatabase>,
34 usage_file_id: FileId,
35 record_expr_field: &ast::RecordExprField,
36) -> Option<Vec<Assist>> {
37 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
38 let def_id = sema.resolve_variant(record_lit)?;
39 let module;
40 let def_file_id;
41 let record_fields = match def_id {
42 hir::VariantDef::Struct(s) => {
43 module = s.module(sema.db);
44 let source = s.source(sema.db)?;
45 def_file_id = source.file_id;
46 let fields = source.value.field_list()?;
47 record_field_list(fields)?
48 }
49 hir::VariantDef::Union(u) => {
50 module = u.module(sema.db);
51 let source = u.source(sema.db)?;
52 def_file_id = source.file_id;
53 source.value.record_field_list()?
54 }
55 hir::VariantDef::Variant(e) => {
56 module = e.module(sema.db);
57 let source = e.source(sema.db)?;
58 def_file_id = source.file_id;
59 let fields = source.value.field_list()?;
60 record_field_list(fields)?
61 }
62 };
63 let def_file_id = def_file_id.original_file(sema.db);
64
65 let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
66 if new_field_type.is_unknown() {
67 return None;
68 }
69 let new_field = make::record_field(
70 None,
71 make::name(&record_expr_field.field_name()?.text()),
72 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
73 );
74
75 let last_field = record_fields.fields().last()?;
76 let last_field_syntax = last_field.syntax();
77 let indent = IndentLevel::from_node(last_field_syntax);
78
79 let mut new_field = new_field.to_string();
80 if usage_file_id != def_file_id {
81 new_field = format!("pub(crate) {}", new_field);
82 }
83 new_field = format!("\n{}{}", indent, new_field);
84
85 let needs_comma = !last_field_syntax.to_string().ends_with(',');
86 if needs_comma {
87 new_field = format!(",{}", new_field);
88 }
89
90 let source_change = SourceChange::from_text_edit(
91 def_file_id,
92 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
93 );
94
95 return Some(vec![fix(
96 "create_field",
97 "Create field",
98 source_change,
99 record_expr_field.syntax().text_range(),
100 )]);
101
102 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
103 match field_def_list {
104 ast::FieldList::RecordFieldList(it) => Some(it),
105 ast::FieldList::TupleFieldList(_) => None,
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use crate::tests::{check_diagnostics, check_fix};
113
114 #[test]
115 fn no_such_field_diagnostics() {
116 check_diagnostics(
117 r#"
118struct S { foo: i32, bar: () }
119impl S {
120 fn new() -> S {
121 S {
122 //^ 💡 error: missing structure fields:
123 //| - bar
124 foo: 92,
125 baz: 62,
126 //^^^^^^^ 💡 error: no such field
127 }
128 }
129}
130"#,
131 );
132 }
133 #[test]
134 fn no_such_field_with_feature_flag_diagnostics() {
135 check_diagnostics(
136 r#"
137//- /lib.rs crate:foo cfg:feature=foo
138struct MyStruct {
139 my_val: usize,
140 #[cfg(feature = "foo")]
141 bar: bool,
142}
143
144impl MyStruct {
145 #[cfg(feature = "foo")]
146 pub(crate) fn new(my_val: usize, bar: bool) -> Self {
147 Self { my_val, bar }
148 }
149 #[cfg(not(feature = "foo"))]
150 pub(crate) fn new(my_val: usize, _bar: bool) -> Self {
151 Self { my_val }
152 }
153}
154"#,
155 );
156 }
157
158 #[test]
159 fn no_such_field_enum_with_feature_flag_diagnostics() {
160 check_diagnostics(
161 r#"
162//- /lib.rs crate:foo cfg:feature=foo
163enum Foo {
164 #[cfg(not(feature = "foo"))]
165 Buz,
166 #[cfg(feature = "foo")]
167 Bar,
168 Baz
169}
170
171fn test_fn(f: Foo) {
172 match f {
173 Foo::Bar => {},
174 Foo::Baz => {},
175 }
176}
177"#,
178 );
179 }
180
181 #[test]
182 fn no_such_field_with_feature_flag_diagnostics_on_struct_lit() {
183 check_diagnostics(
184 r#"
185//- /lib.rs crate:foo cfg:feature=foo
186struct S {
187 #[cfg(feature = "foo")]
188 foo: u32,
189 #[cfg(not(feature = "foo"))]
190 bar: u32,
191}
192
193impl S {
194 #[cfg(feature = "foo")]
195 fn new(foo: u32) -> Self {
196 Self { foo }
197 }
198 #[cfg(not(feature = "foo"))]
199 fn new(bar: u32) -> Self {
200 Self { bar }
201 }
202 fn new2(bar: u32) -> Self {
203 #[cfg(feature = "foo")]
204 { Self { foo: bar } }
205 #[cfg(not(feature = "foo"))]
206 { Self { bar } }
207 }
208 fn new2(val: u32) -> Self {
209 Self {
210 #[cfg(feature = "foo")]
211 foo: val,
212 #[cfg(not(feature = "foo"))]
213 bar: val,
214 }
215 }
216}
217"#,
218 );
219 }
220
221 #[test]
222 fn no_such_field_with_type_macro() {
223 check_diagnostics(
224 r#"
225macro_rules! Type { () => { u32 }; }
226struct Foo { bar: Type![] }
227
228impl Foo {
229 fn new() -> Self {
230 Foo { bar: 0 }
231 }
232}
233"#,
234 );
235 }
236
237 #[test]
238 fn test_add_field_from_usage() {
239 check_fix(
240 r"
241fn main() {
242 Foo { bar: 3, baz$0: false};
243}
244struct Foo {
245 bar: i32
246}
247",
248 r"
249fn main() {
250 Foo { bar: 3, baz: false};
251}
252struct Foo {
253 bar: i32,
254 baz: bool
255}
256",
257 )
258 }
259
260 #[test]
261 fn test_add_field_in_other_file_from_usage() {
262 check_fix(
263 r#"
264//- /main.rs
265mod foo;
266
267fn main() {
268 foo::Foo { bar: 3, $0baz: false};
269}
270//- /foo.rs
271struct Foo {
272 bar: i32
273}
274"#,
275 r#"
276struct Foo {
277 bar: i32,
278 pub(crate) baz: bool
279}
280"#,
281 )
282 }
283}
diff --git a/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs b/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs
new file mode 100644
index 000000000..4e639f214
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs
@@ -0,0 +1,61 @@
1use hir::db::AstDatabase;
2use ide_db::source_change::SourceChange;
3use syntax::{ast, AstNode};
4use text_edit::TextEdit;
5
6use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
7
8// Diagnostic: remove-this-semicolon
9//
10// This diagnostic is triggered when there's an erroneous `;` at the end of the block.
11pub(crate) fn remove_this_semicolon(
12 ctx: &DiagnosticsContext<'_>,
13 d: &hir::RemoveThisSemicolon,
14) -> Diagnostic {
15 Diagnostic::new(
16 "remove-this-semicolon",
17 "remove this semicolon",
18 ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
19 )
20 .with_fixes(fixes(ctx, d))
21}
22
23fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::RemoveThisSemicolon) -> Option<Vec<Assist>> {
24 let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
25
26 let semicolon = d
27 .expr
28 .value
29 .to_node(&root)
30 .syntax()
31 .parent()
32 .and_then(ast::ExprStmt::cast)
33 .and_then(|expr| expr.semicolon_token())?
34 .text_range();
35
36 let edit = TextEdit::delete(semicolon);
37 let source_change =
38 SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
39
40 Some(vec![fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)])
41}
42
43#[cfg(test)]
44mod tests {
45 use crate::tests::{check_diagnostics, check_fix};
46
47 #[test]
48 fn missing_semicolon() {
49 check_diagnostics(
50 r#"
51fn test() -> i32 { 123; }
52 //^^^ 💡 error: remove this semicolon
53"#,
54 );
55 }
56
57 #[test]
58 fn remove_semicolon() {
59 check_fix(r#"fn f() -> i32 { 92$0; }"#, r#"fn f() -> i32 { 92 }"#);
60 }
61}
diff --git a/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs b/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs
new file mode 100644
index 000000000..cd87a10bb
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs
@@ -0,0 +1,179 @@
1use hir::{db::AstDatabase, InFile};
2use ide_db::source_change::SourceChange;
3use syntax::{
4 ast::{self, ArgListOwner},
5 AstNode, TextRange,
6};
7use text_edit::TextEdit;
8
9use crate::{fix, Assist, Diagnostic, DiagnosticsContext, Severity};
10
11// Diagnostic: replace-filter-map-next-with-find-map
12//
13// This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`.
14pub(crate) fn replace_filter_map_next_with_find_map(
15 ctx: &DiagnosticsContext<'_>,
16 d: &hir::ReplaceFilterMapNextWithFindMap,
17) -> Diagnostic {
18 Diagnostic::new(
19 "replace-filter-map-next-with-find-map",
20 "replace filter_map(..).next() with find_map(..)",
21 ctx.sema.diagnostics_display_range(InFile::new(d.file, d.next_expr.clone().into())).range,
22 )
23 .severity(Severity::WeakWarning)
24 .with_fixes(fixes(ctx, d))
25}
26
27fn fixes(
28 ctx: &DiagnosticsContext<'_>,
29 d: &hir::ReplaceFilterMapNextWithFindMap,
30) -> Option<Vec<Assist>> {
31 let root = ctx.sema.db.parse_or_expand(d.file)?;
32 let next_expr = d.next_expr.to_node(&root);
33 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
34
35 let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
36 let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
37 let filter_map_args = filter_map_call.arg_list()?;
38
39 let range_to_replace =
40 TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
41 let replacement = format!("find_map{}", filter_map_args.syntax().text());
42 let trigger_range = next_expr.syntax().text_range();
43
44 let edit = TextEdit::replace(range_to_replace, replacement);
45
46 let source_change = SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit);
47
48 Some(vec![fix(
49 "replace_with_find_map",
50 "Replace filter_map(..).next() with find_map()",
51 source_change,
52 trigger_range,
53 )])
54}
55
56#[cfg(test)]
57mod tests {
58 use crate::tests::check_fix;
59
60 // Register the required standard library types to make the tests work
61 #[track_caller]
62 fn check_diagnostics(ra_fixture: &str) {
63 let prefix = r#"
64//- /main.rs crate:main deps:core
65use core::iter::Iterator;
66use core::option::Option::{self, Some, None};
67"#;
68 let suffix = r#"
69//- /core/lib.rs crate:core
70pub mod option {
71 pub enum Option<T> { Some(T), None }
72}
73pub mod iter {
74 pub trait Iterator {
75 type Item;
76 fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap }
77 fn next(&mut self) -> Option<Self::Item>;
78 }
79 pub struct FilterMap {}
80 impl Iterator for FilterMap {
81 type Item = i32;
82 fn next(&mut self) -> i32 { 7 }
83 }
84}
85"#;
86 crate::tests::check_diagnostics(&format!("{}{}{}", prefix, ra_fixture, suffix))
87 }
88
89 #[test]
90 fn replace_filter_map_next_with_find_map2() {
91 check_diagnostics(
92 r#"
93 fn foo() {
94 let m = [1, 2, 3].iter().filter_map(|x| Some(92)).next();
95 } //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 weak: replace filter_map(..).next() with find_map(..)
96"#,
97 );
98 }
99
100 #[test]
101 fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() {
102 check_diagnostics(
103 r#"
104fn foo() {
105 let m = [1, 2, 3]
106 .iter()
107 .filter_map(|x| Some(92))
108 .len();
109}
110"#,
111 );
112 }
113
114 #[test]
115 fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() {
116 check_diagnostics(
117 r#"
118fn foo() {
119 let m = [1, 2, 3]
120 .iter()
121 .filter_map(|x| Some(92))
122 .map(|x| x + 2)
123 .len();
124}
125"#,
126 );
127 }
128
129 #[test]
130 fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() {
131 check_diagnostics(
132 r#"
133fn foo() {
134 let m = [1, 2, 3]
135 .iter()
136 .filter_map(|x| Some(92));
137 let n = m.next();
138}
139"#,
140 );
141 }
142
143 #[test]
144 fn replace_with_wind_map() {
145 check_fix(
146 r#"
147//- /main.rs crate:main deps:core
148use core::iter::Iterator;
149use core::option::Option::{self, Some, None};
150fn foo() {
151 let m = [1, 2, 3].iter().$0filter_map(|x| Some(92)).next();
152}
153//- /core/lib.rs crate:core
154pub mod option {
155 pub enum Option<T> { Some(T), None }
156}
157pub mod iter {
158 pub trait Iterator {
159 type Item;
160 fn filter_map<B, F>(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option<B> { FilterMap }
161 fn next(&mut self) -> Option<Self::Item>;
162 }
163 pub struct FilterMap {}
164 impl Iterator for FilterMap {
165 type Item = i32;
166 fn next(&mut self) -> i32 { 7 }
167 }
168}
169"#,
170 r#"
171use core::iter::Iterator;
172use core::option::Option::{self, Some, None};
173fn foo() {
174 let m = [1, 2, 3].iter().find_map(|x| Some(92));
175}
176"#,
177 )
178 }
179}
diff --git a/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs b/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs
new file mode 100644
index 000000000..e879de75c
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs
@@ -0,0 +1,16 @@
1use crate::{Diagnostic, DiagnosticsContext, Severity};
2
3// Diagnostic: unimplemented-builtin-macro
4//
5// This diagnostic is shown for builtin macros which are not yet implemented by rust-analyzer
6pub(crate) fn unimplemented_builtin_macro(
7 ctx: &DiagnosticsContext<'_>,
8 d: &hir::UnimplementedBuiltinMacro,
9) -> Diagnostic {
10 Diagnostic::new(
11 "unimplemented-builtin-macro",
12 "unimplemented built-in macro".to_string(),
13 ctx.sema.diagnostics_display_range(d.node.clone()).range,
14 )
15 .severity(Severity::WeakWarning)
16}
diff --git a/crates/ide_diagnostics/src/handlers/unlinked_file.rs b/crates/ide_diagnostics/src/handlers/unlinked_file.rs
new file mode 100644
index 000000000..8e601fa48
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unlinked_file.rs
@@ -0,0 +1,298 @@
1//! Diagnostic emitted for files that aren't part of any crate.
2
3use hir::db::DefDatabase;
4use ide_db::{
5 base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
6 source_change::SourceChange,
7 RootDatabase,
8};
9use syntax::{
10 ast::{self, ModuleItemOwner, NameOwner},
11 AstNode, TextRange, TextSize,
12};
13use text_edit::TextEdit;
14
15use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
16
17// Diagnostic: unlinked-file
18//
19// This diagnostic is shown for files that are not included in any crate, or files that are part of
20// crates rust-analyzer failed to discover. The file will not have IDE features available.
21pub(crate) fn unlinked_file(ctx: &DiagnosticsContext, acc: &mut Vec<Diagnostic>, file_id: FileId) {
22 // Limit diagnostic to the first few characters in the file. This matches how VS Code
23 // renders it with the full span, but on other editors, and is less invasive.
24 let range = ctx.sema.db.parse(file_id).syntax_node().text_range();
25 // FIXME: This is wrong if one of the first three characters is not ascii: `//Ы`.
26 let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
27
28 acc.push(
29 Diagnostic::new("unlinked-file", "file not included in module tree", range)
30 .with_fixes(fixes(ctx, file_id)),
31 );
32}
33
34fn fixes(ctx: &DiagnosticsContext, file_id: FileId) -> Option<Vec<Assist>> {
35 // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file,
36 // suggest that as a fix.
37
38 let source_root = ctx.sema.db.source_root(ctx.sema.db.file_source_root(file_id));
39 let our_path = source_root.path_for_file(&file_id)?;
40 let module_name = our_path.name_and_extension()?.0;
41
42 // Candidates to look for:
43 // - `mod.rs` in the same folder
44 // - we also check `main.rs` and `lib.rs`
45 // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
46 let parent = our_path.parent()?;
47 let mut paths = vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
48
49 // `submod/bla.rs` -> `submod.rs`
50 if let Some(newmod) = (|| {
51 let name = parent.name_and_extension()?.0;
52 parent.parent()?.join(&format!("{}.rs", name))
53 })() {
54 paths.push(newmod);
55 }
56
57 for path in &paths {
58 if let Some(parent_id) = source_root.file_for_path(path) {
59 for krate in ctx.sema.db.relevant_crates(*parent_id).iter() {
60 let crate_def_map = ctx.sema.db.crate_def_map(*krate);
61 for (_, module) in crate_def_map.modules() {
62 if module.origin.is_inline() {
63 // We don't handle inline `mod parent {}`s, they use different paths.
64 continue;
65 }
66
67 if module.origin.file_id() == Some(*parent_id) {
68 return make_fixes(ctx.sema.db, *parent_id, module_name, file_id);
69 }
70 }
71 }
72 }
73 }
74
75 None
76}
77
78fn make_fixes(
79 db: &RootDatabase,
80 parent_file_id: FileId,
81 new_mod_name: &str,
82 added_file_id: FileId,
83) -> Option<Vec<Assist>> {
84 fn is_outline_mod(item: &ast::Item) -> bool {
85 matches!(item, ast::Item::Module(m) if m.item_list().is_none())
86 }
87
88 let mod_decl = format!("mod {};", new_mod_name);
89 let pub_mod_decl = format!("pub mod {};", new_mod_name);
90
91 let ast: ast::SourceFile = db.parse(parent_file_id).tree();
92
93 let mut mod_decl_builder = TextEdit::builder();
94 let mut pub_mod_decl_builder = TextEdit::builder();
95
96 // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
97 // probably `#[cfg]`d out).
98 for item in ast.items() {
99 if let ast::Item::Module(m) = item {
100 if let Some(name) = m.name() {
101 if m.item_list().is_none() && name.to_string() == new_mod_name {
102 cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
103 return None;
104 }
105 }
106 }
107 }
108
109 // If there are existing `mod m;` items, append after them (after the first group of them, rather).
110 match ast
111 .items()
112 .skip_while(|item| !is_outline_mod(item))
113 .take_while(|item| is_outline_mod(item))
114 .last()
115 {
116 Some(last) => {
117 cov_mark::hit!(unlinked_file_append_to_existing_mods);
118 let offset = last.syntax().text_range().end();
119 mod_decl_builder.insert(offset, format!("\n{}", mod_decl));
120 pub_mod_decl_builder.insert(offset, format!("\n{}", pub_mod_decl));
121 }
122 None => {
123 // Prepend before the first item in the file.
124 match ast.items().next() {
125 Some(item) => {
126 cov_mark::hit!(unlinked_file_prepend_before_first_item);
127 let offset = item.syntax().text_range().start();
128 mod_decl_builder.insert(offset, format!("{}\n\n", mod_decl));
129 pub_mod_decl_builder.insert(offset, format!("{}\n\n", pub_mod_decl));
130 }
131 None => {
132 // No items in the file, so just append at the end.
133 cov_mark::hit!(unlinked_file_empty_file);
134 let offset = ast.syntax().text_range().end();
135 mod_decl_builder.insert(offset, format!("{}\n", mod_decl));
136 pub_mod_decl_builder.insert(offset, format!("{}\n", pub_mod_decl));
137 }
138 }
139 }
140 }
141
142 let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
143 Some(vec![
144 fix(
145 "add_mod_declaration",
146 &format!("Insert `{}`", mod_decl),
147 SourceChange::from_text_edit(parent_file_id, mod_decl_builder.finish()),
148 trigger_range,
149 ),
150 fix(
151 "add_pub_mod_declaration",
152 &format!("Insert `{}`", pub_mod_decl),
153 SourceChange::from_text_edit(parent_file_id, pub_mod_decl_builder.finish()),
154 trigger_range,
155 ),
156 ])
157}
158
159#[cfg(test)]
160mod tests {
161 use crate::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
162
163 #[test]
164 fn unlinked_file_prepend_first_item() {
165 cov_mark::check!(unlinked_file_prepend_before_first_item);
166 // Only tests the first one for `pub mod` since the rest are the same
167 check_fixes(
168 r#"
169//- /main.rs
170fn f() {}
171//- /foo.rs
172$0
173"#,
174 vec![
175 r#"
176mod foo;
177
178fn f() {}
179"#,
180 r#"
181pub mod foo;
182
183fn f() {}
184"#,
185 ],
186 );
187 }
188
189 #[test]
190 fn unlinked_file_append_mod() {
191 cov_mark::check!(unlinked_file_append_to_existing_mods);
192 check_fix(
193 r#"
194//- /main.rs
195//! Comment on top
196
197mod preexisting;
198
199mod preexisting2;
200
201struct S;
202
203mod preexisting_bottom;)
204//- /foo.rs
205$0
206"#,
207 r#"
208//! Comment on top
209
210mod preexisting;
211
212mod preexisting2;
213mod foo;
214
215struct S;
216
217mod preexisting_bottom;)
218"#,
219 );
220 }
221
222 #[test]
223 fn unlinked_file_insert_in_empty_file() {
224 cov_mark::check!(unlinked_file_empty_file);
225 check_fix(
226 r#"
227//- /main.rs
228//- /foo.rs
229$0
230"#,
231 r#"
232mod foo;
233"#,
234 );
235 }
236
237 #[test]
238 fn unlinked_file_old_style_modrs() {
239 check_fix(
240 r#"
241//- /main.rs
242mod submod;
243//- /submod/mod.rs
244// in mod.rs
245//- /submod/foo.rs
246$0
247"#,
248 r#"
249// in mod.rs
250mod foo;
251"#,
252 );
253 }
254
255 #[test]
256 fn unlinked_file_new_style_mod() {
257 check_fix(
258 r#"
259//- /main.rs
260mod submod;
261//- /submod.rs
262//- /submod/foo.rs
263$0
264"#,
265 r#"
266mod foo;
267"#,
268 );
269 }
270
271 #[test]
272 fn unlinked_file_with_cfg_off() {
273 cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
274 check_no_fix(
275 r#"
276//- /main.rs
277#[cfg(never)]
278mod foo;
279
280//- /foo.rs
281$0
282"#,
283 );
284 }
285
286 #[test]
287 fn unlinked_file_with_cfg_on() {
288 check_diagnostics(
289 r#"
290//- /main.rs
291#[cfg(not(never))]
292mod foo;
293
294//- /foo.rs
295"#,
296 );
297 }
298}
diff --git a/crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs b/crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs
new file mode 100644
index 000000000..74e4a69c6
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs
@@ -0,0 +1,49 @@
1use crate::{Diagnostic, DiagnosticsContext};
2
3// Diagnostic: unresolved-extern-crate
4//
5// This diagnostic is triggered if rust-analyzer is unable to discover referred extern crate.
6pub(crate) fn unresolved_extern_crate(
7 ctx: &DiagnosticsContext<'_>,
8 d: &hir::UnresolvedExternCrate,
9) -> Diagnostic {
10 Diagnostic::new(
11 "unresolved-extern-crate",
12 "unresolved extern crate",
13 ctx.sema.diagnostics_display_range(d.decl.clone().map(|it| it.into())).range,
14 )
15}
16
17#[cfg(test)]
18mod tests {
19 use crate::tests::check_diagnostics;
20
21 #[test]
22 fn unresolved_extern_crate() {
23 check_diagnostics(
24 r#"
25//- /main.rs crate:main deps:core
26extern crate core;
27 extern crate doesnotexist;
28//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unresolved extern crate
29//- /lib.rs crate:core
30"#,
31 );
32 }
33
34 #[test]
35 fn extern_crate_self_as() {
36 cov_mark::check!(extern_crate_self_as);
37 check_diagnostics(
38 r#"
39//- /lib.rs
40 extern crate doesnotexist;
41//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unresolved extern crate
42// Should not error.
43extern crate self as foo;
44struct Foo;
45use foo::Foo as Bar;
46"#,
47 );
48 }
49}
diff --git a/crates/ide_diagnostics/src/handlers/unresolved_import.rs b/crates/ide_diagnostics/src/handlers/unresolved_import.rs
new file mode 100644
index 000000000..e52a88459
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unresolved_import.rs
@@ -0,0 +1,90 @@
1use crate::{Diagnostic, DiagnosticsContext};
2
3// Diagnostic: unresolved-import
4//
5// This diagnostic is triggered if rust-analyzer is unable to resolve a path in
6// a `use` declaration.
7pub(crate) fn unresolved_import(
8 ctx: &DiagnosticsContext<'_>,
9 d: &hir::UnresolvedImport,
10) -> Diagnostic {
11 Diagnostic::new(
12 "unresolved-import",
13 "unresolved import",
14 ctx.sema.diagnostics_display_range(d.decl.clone().map(|it| it.into())).range,
15 )
16 // This currently results in false positives in the following cases:
17 // - `cfg_if!`-generated code in libstd (we don't load the sysroot correctly)
18 // - `core::arch` (we don't handle `#[path = "../<path>"]` correctly)
19 // - proc macros and/or proc macro generated code
20 .experimental()
21}
22
23#[cfg(test)]
24mod tests {
25 use crate::tests::check_diagnostics;
26
27 #[test]
28 fn unresolved_import() {
29 check_diagnostics(
30 r#"
31use does_exist;
32use does_not_exist;
33 //^^^^^^^^^^^^^^ error: unresolved import
34
35mod does_exist {}
36"#,
37 );
38 }
39
40 #[test]
41 fn unresolved_import_in_use_tree() {
42 // Only the relevant part of a nested `use` item should be highlighted.
43 check_diagnostics(
44 r#"
45use does_exist::{Exists, DoesntExist};
46 //^^^^^^^^^^^ error: unresolved import
47
48use {does_not_exist::*, does_exist};
49 //^^^^^^^^^^^^^^^^^ error: unresolved import
50
51use does_not_exist::{
52 a,
53 //^ error: unresolved import
54 b,
55 //^ error: unresolved import
56 c,
57 //^ error: unresolved import
58};
59
60mod does_exist {
61 pub struct Exists;
62}
63"#,
64 );
65 }
66
67 #[test]
68 fn dedup_unresolved_import_from_unresolved_crate() {
69 check_diagnostics(
70 r#"
71//- /main.rs crate:main
72mod a {
73 extern crate doesnotexist;
74 //^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unresolved extern crate
75
76 // Should not error, since we already errored for the missing crate.
77 use doesnotexist::{self, bla, *};
78
79 use crate::doesnotexist;
80 //^^^^^^^^^^^^^^^^^^^ error: unresolved import
81}
82
83mod m {
84 use super::doesnotexist;
85 //^^^^^^^^^^^^^^^^^^^ error: unresolved import
86}
87"#,
88 );
89 }
90}
diff --git a/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs b/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs
new file mode 100644
index 000000000..f0f7725db
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs
@@ -0,0 +1,84 @@
1use hir::{db::AstDatabase, InFile};
2use syntax::{AstNode, SyntaxNodePtr};
3
4use crate::{Diagnostic, DiagnosticsContext};
5
6// Diagnostic: unresolved-macro-call
7//
8// This diagnostic is triggered if rust-analyzer is unable to resolve the path
9// to a macro in a macro invocation.
10pub(crate) fn unresolved_macro_call(
11 ctx: &DiagnosticsContext<'_>,
12 d: &hir::UnresolvedMacroCall,
13) -> Diagnostic {
14 let last_path_segment = ctx.sema.db.parse_or_expand(d.macro_call.file_id).and_then(|root| {
15 d.macro_call
16 .value
17 .to_node(&root)
18 .path()
19 .and_then(|it| it.segment())
20 .and_then(|it| it.name_ref())
21 .map(|it| InFile::new(d.macro_call.file_id, SyntaxNodePtr::new(it.syntax())))
22 });
23 let diagnostics = last_path_segment.unwrap_or_else(|| d.macro_call.clone().map(|it| it.into()));
24
25 Diagnostic::new(
26 "unresolved-macro-call",
27 format!("unresolved macro `{}!`", d.path),
28 ctx.sema.diagnostics_display_range(diagnostics).range,
29 )
30 .experimental()
31}
32
33#[cfg(test)]
34mod tests {
35 use crate::tests::check_diagnostics;
36
37 #[test]
38 fn unresolved_macro_diag() {
39 check_diagnostics(
40 r#"
41fn f() {
42 m!();
43} //^ error: unresolved macro `m!`
44
45"#,
46 );
47 }
48
49 #[test]
50 fn test_unresolved_macro_range() {
51 check_diagnostics(
52 r#"
53foo::bar!(92);
54 //^^^ error: unresolved macro `foo::bar!`
55"#,
56 );
57 }
58
59 #[test]
60 fn unresolved_legacy_scope_macro() {
61 check_diagnostics(
62 r#"
63macro_rules! m { () => {} }
64
65m!(); m2!();
66 //^^ error: unresolved macro `self::m2!`
67"#,
68 );
69 }
70
71 #[test]
72 fn unresolved_module_scope_macro() {
73 check_diagnostics(
74 r#"
75mod mac {
76#[macro_export]
77macro_rules! m { () => {} } }
78
79self::m!(); self::m2!();
80 //^^ error: unresolved macro `self::m2!`
81"#,
82 );
83 }
84}
diff --git a/crates/ide_diagnostics/src/handlers/unresolved_module.rs b/crates/ide_diagnostics/src/handlers/unresolved_module.rs
new file mode 100644
index 000000000..61fc43604
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unresolved_module.rs
@@ -0,0 +1,110 @@
1use hir::db::AstDatabase;
2use ide_db::{assists::Assist, base_db::AnchoredPathBuf, source_change::FileSystemEdit};
3use syntax::AstNode;
4
5use crate::{fix, Diagnostic, DiagnosticsContext};
6
7// Diagnostic: unresolved-module
8//
9// This diagnostic is triggered if rust-analyzer is unable to discover referred module.
10pub(crate) fn unresolved_module(
11 ctx: &DiagnosticsContext<'_>,
12 d: &hir::UnresolvedModule,
13) -> Diagnostic {
14 Diagnostic::new(
15 "unresolved-module",
16 "unresolved module",
17 ctx.sema.diagnostics_display_range(d.decl.clone().map(|it| it.into())).range,
18 )
19 .with_fixes(fixes(ctx, d))
20}
21
22fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedModule) -> Option<Vec<Assist>> {
23 let root = ctx.sema.db.parse_or_expand(d.decl.file_id)?;
24 let unresolved_module = d.decl.value.to_node(&root);
25 Some(vec![fix(
26 "create_module",
27 "Create module",
28 FileSystemEdit::CreateFile {
29 dst: AnchoredPathBuf {
30 anchor: d.decl.file_id.original_file(ctx.sema.db),
31 path: d.candidate.clone(),
32 },
33 initial_contents: "".to_string(),
34 }
35 .into(),
36 unresolved_module.syntax().text_range(),
37 )])
38}
39
40#[cfg(test)]
41mod tests {
42 use expect_test::expect;
43
44 use crate::tests::{check_diagnostics, check_expect};
45
46 #[test]
47 fn unresolved_module() {
48 check_diagnostics(
49 r#"
50//- /lib.rs
51mod foo;
52 mod bar;
53//^^^^^^^^ 💡 error: unresolved module
54mod baz {}
55//- /foo.rs
56"#,
57 );
58 }
59
60 #[test]
61 fn test_unresolved_module_diagnostic() {
62 check_expect(
63 r#"mod foo;"#,
64 expect![[r#"
65 [
66 Diagnostic {
67 code: DiagnosticCode(
68 "unresolved-module",
69 ),
70 message: "unresolved module",
71 range: 0..8,
72 severity: Error,
73 unused: false,
74 experimental: false,
75 fixes: Some(
76 [
77 Assist {
78 id: AssistId(
79 "create_module",
80 QuickFix,
81 ),
82 label: "Create module",
83 group: None,
84 target: 0..8,
85 source_change: Some(
86 SourceChange {
87 source_file_edits: {},
88 file_system_edits: [
89 CreateFile {
90 dst: AnchoredPathBuf {
91 anchor: FileId(
92 0,
93 ),
94 path: "foo.rs",
95 },
96 initial_contents: "",
97 },
98 ],
99 is_snippet: false,
100 },
101 ),
102 },
103 ],
104 ),
105 },
106 ]
107 "#]],
108 );
109 }
110}
diff --git a/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs b/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs
new file mode 100644
index 000000000..fde1d1323
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs
@@ -0,0 +1,27 @@
1use crate::{Diagnostic, DiagnosticsContext, Severity};
2
3// Diagnostic: unresolved-proc-macro
4//
5// This diagnostic is shown when a procedural macro can not be found. This usually means that
6// procedural macro support is simply disabled (and hence is only a weak hint instead of an error),
7// but can also indicate project setup problems.
8//
9// If you are seeing a lot of "proc macro not expanded" warnings, you can add this option to the
10// `rust-analyzer.diagnostics.disabled` list to prevent them from showing. Alternatively you can
11// enable support for procedural macros (see `rust-analyzer.procMacro.enable`).
12pub(crate) fn unresolved_proc_macro(
13 ctx: &DiagnosticsContext<'_>,
14 d: &hir::UnresolvedProcMacro,
15) -> Diagnostic {
16 // Use more accurate position if available.
17 let display_range = d
18 .precise_location
19 .unwrap_or_else(|| ctx.sema.diagnostics_display_range(d.node.clone()).range);
20 // FIXME: it would be nice to tell the user whether proc macros are currently disabled
21 let message = match &d.macro_name {
22 Some(name) => format!("proc macro `{}` not expanded", name),
23 None => "proc macro not expanded".to_string(),
24 };
25
26 Diagnostic::new("unresolved-proc-macro", message, display_range).severity(Severity::WeakWarning)
27}
diff --git a/crates/ide_diagnostics/src/handlers/useless_braces.rs b/crates/ide_diagnostics/src/handlers/useless_braces.rs
new file mode 100644
index 000000000..8b9330e04
--- /dev/null
+++ b/crates/ide_diagnostics/src/handlers/useless_braces.rs
@@ -0,0 +1,148 @@
1use ide_db::{base_db::FileId, source_change::SourceChange};
2use itertools::Itertools;
3use syntax::{ast, AstNode, SyntaxNode, TextRange};
4use text_edit::TextEdit;
5
6use crate::{fix, Diagnostic, Severity};
7
8// Diagnostic: unnecessary-braces
9//
10// Diagnostic for unnecessary braces in `use` items.
11pub(crate) fn useless_braces(
12 acc: &mut Vec<Diagnostic>,
13 file_id: FileId,
14 node: &SyntaxNode,
15) -> Option<()> {
16 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
17 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
18 // If there is a comment inside the bracketed `use`,
19 // assume it is a commented out module path and don't show diagnostic.
20 if use_tree_list.has_inner_comment() {
21 return Some(());
22 }
23
24 let use_range = use_tree_list.syntax().text_range();
25 let edit = remove_braces(&single_use_tree).unwrap_or_else(|| {
26 let to_replace = single_use_tree.syntax().text().to_string();
27 let mut edit_builder = TextEdit::builder();
28 edit_builder.delete(use_range);
29 edit_builder.insert(use_range.start(), to_replace);
30 edit_builder.finish()
31 });
32
33 acc.push(
34 Diagnostic::new(
35 "unnecessary-braces",
36 "Unnecessary braces in use statement".to_string(),
37 use_range,
38 )
39 .severity(Severity::WeakWarning)
40 .with_fixes(Some(vec![fix(
41 "remove_braces",
42 "Remove unnecessary braces",
43 SourceChange::from_text_edit(file_id, edit),
44 use_range,
45 )])),
46 );
47 }
48
49 Some(())
50}
51
52fn remove_braces(single_use_tree: &ast::UseTree) -> Option<TextEdit> {
53 let use_tree_list_node = single_use_tree.syntax().parent()?;
54 if single_use_tree.path()?.segment()?.self_token().is_some() {
55 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
56 let end = use_tree_list_node.text_range().end();
57 return Some(TextEdit::delete(TextRange::new(start, end)));
58 }
59 None
60}
61
62#[cfg(test)]
63mod tests {
64 use crate::tests::{check_diagnostics, check_fix};
65
66 #[test]
67 fn test_check_unnecessary_braces_in_use_statement() {
68 check_diagnostics(
69 r#"
70use a;
71use a::{c, d::e};
72
73mod a {
74 mod c {}
75 mod d {
76 mod e {}
77 }
78}
79"#,
80 );
81 check_diagnostics(
82 r#"
83use a;
84use a::{
85 c,
86 // d::e
87};
88
89mod a {
90 mod c {}
91 mod d {
92 mod e {}
93 }
94}
95"#,
96 );
97 check_fix(
98 r#"
99mod b {}
100use {$0b};
101"#,
102 r#"
103mod b {}
104use b;
105"#,
106 );
107 check_fix(
108 r#"
109mod b {}
110use {b$0};
111"#,
112 r#"
113mod b {}
114use b;
115"#,
116 );
117 check_fix(
118 r#"
119mod a { mod c {} }
120use a::{c$0};
121"#,
122 r#"
123mod a { mod c {} }
124use a::c;
125"#,
126 );
127 check_fix(
128 r#"
129mod a {}
130use a::{self$0};
131"#,
132 r#"
133mod a {}
134use a;
135"#,
136 );
137 check_fix(
138 r#"
139mod a { mod c {} mod d { mod e {} } }
140use a::{c, d::{e$0}};
141"#,
142 r#"
143mod a { mod c {} mod d { mod e {} } }
144use a::{c, d::e};
145"#,
146 );
147 }
148}
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}