summaryrefslogtreecommitdiffstats
path: root/src/tools/rust-analyzer/crates/ide-diagnostics
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 12:02:58 +0000
commit698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch)
tree173a775858bd501c378080a10dca74132f05bc50 /src/tools/rust-analyzer/crates/ide-diagnostics
parentInitial commit. (diff)
downloadrustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz
rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/rust-analyzer/crates/ide-diagnostics')
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/Cargo.toml34
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/break_outside_of_loop.rs30
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/field_shorthand.rs203
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/inactive_code.rs144
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/incorrect_case.rs486
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_derive_target.rs38
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/macro_error.rs218
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/malformed_derive.rs37
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs334
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_fields.rs837
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_match_arms.rs1012
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs101
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/no_such_field.rs283
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs131
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/type_mismatch.rs573
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unimplemented_builtin_macro.rs16
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unlinked_file.rs336
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_extern_crate.rs49
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_import.rs90
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_macro_call.rs76
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_module.rs156
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_proc_macro.rs62
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/useless_braces.rs148
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/lib.rs260
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/tests.rs145
-rw-r--r--src/tools/rust-analyzer/crates/ide-diagnostics/src/tests/sourcegen.rs73
26 files changed, 5872 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/Cargo.toml b/src/tools/rust-analyzer/crates/ide-diagnostics/Cargo.toml
new file mode 100644
index 000000000..e221425ed
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "ide-diagnostics"
+version = "0.0.0"
+description = "TBD"
+license = "MIT OR Apache-2.0"
+edition = "2021"
+rust-version = "1.57"
+
+[lib]
+doctest = false
+
+[dependencies]
+cov-mark = "2.0.0-pre.1"
+itertools = "0.10.3"
+
+
+either = "1.7.0"
+
+profile = { path = "../profile", version = "0.0.0" }
+stdx = { path = "../stdx", version = "0.0.0" }
+syntax = { path = "../syntax", version = "0.0.0" }
+text-edit = { path = "../text-edit", version = "0.0.0" }
+cfg = { path = "../cfg", version = "0.0.0" }
+hir = { path = "../hir", version = "0.0.0" }
+ide-db = { path = "../ide-db", version = "0.0.0" }
+
+[dev-dependencies]
+expect-test = "1.4.0"
+
+test-utils = { path = "../test-utils" }
+sourcegen = { path = "../sourcegen" }
+
+[features]
+in-rust-tree = []
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/break_outside_of_loop.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/break_outside_of_loop.rs
new file mode 100644
index 000000000..d12594a4c
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/break_outside_of_loop.rs
@@ -0,0 +1,30 @@
+use crate::{Diagnostic, DiagnosticsContext};
+
+// Diagnostic: break-outside-of-loop
+//
+// This diagnostic is triggered if the `break` keyword is used outside of a loop.
+pub(crate) fn break_outside_of_loop(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::BreakOutsideOfLoop,
+) -> Diagnostic {
+ Diagnostic::new(
+ "break-outside-of-loop",
+ "break outside of loop",
+ ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn break_outside_of_loop() {
+ check_diagnostics(
+ r#"
+fn foo() { break; }
+ //^^^^^ error: break outside of loop
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/field_shorthand.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/field_shorthand.rs
new file mode 100644
index 000000000..2b7105362
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/field_shorthand.rs
@@ -0,0 +1,203 @@
+//! Suggests shortening `Foo { field: field }` to `Foo { field }` in both
+//! expressions and patterns.
+
+use ide_db::{base_db::FileId, source_change::SourceChange};
+use syntax::{ast, match_ast, AstNode, SyntaxNode};
+use text_edit::TextEdit;
+
+use crate::{fix, Diagnostic, Severity};
+
+pub(crate) fn field_shorthand(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) {
+ match_ast! {
+ match node {
+ ast::RecordExpr(it) => check_expr_field_shorthand(acc, file_id, it),
+ ast::RecordPat(it) => check_pat_field_shorthand(acc, file_id, it),
+ _ => ()
+ }
+ };
+}
+
+fn check_expr_field_shorthand(
+ acc: &mut Vec<Diagnostic>,
+ file_id: FileId,
+ record_expr: ast::RecordExpr,
+) {
+ let record_field_list = match record_expr.record_expr_field_list() {
+ Some(it) => it,
+ None => return,
+ };
+ for record_field in record_field_list.fields() {
+ let (name_ref, expr) = match record_field.name_ref().zip(record_field.expr()) {
+ Some(it) => it,
+ None => continue,
+ };
+
+ let field_name = name_ref.syntax().text().to_string();
+ let field_expr = expr.syntax().text().to_string();
+ let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
+ if field_name != field_expr || field_name_is_tup_index {
+ continue;
+ }
+
+ let mut edit_builder = TextEdit::builder();
+ edit_builder.delete(record_field.syntax().text_range());
+ edit_builder.insert(record_field.syntax().text_range().start(), field_name);
+ let edit = edit_builder.finish();
+
+ let field_range = record_field.syntax().text_range();
+ acc.push(
+ Diagnostic::new("use-field-shorthand", "Shorthand struct initialization", field_range)
+ .severity(Severity::WeakWarning)
+ .with_fixes(Some(vec![fix(
+ "use_expr_field_shorthand",
+ "Use struct shorthand initialization",
+ SourceChange::from_text_edit(file_id, edit),
+ field_range,
+ )])),
+ );
+ }
+}
+
+fn check_pat_field_shorthand(
+ acc: &mut Vec<Diagnostic>,
+ file_id: FileId,
+ record_pat: ast::RecordPat,
+) {
+ let record_pat_field_list = match record_pat.record_pat_field_list() {
+ Some(it) => it,
+ None => return,
+ };
+ for record_pat_field in record_pat_field_list.fields() {
+ let (name_ref, pat) = match record_pat_field.name_ref().zip(record_pat_field.pat()) {
+ Some(it) => it,
+ None => continue,
+ };
+
+ let field_name = name_ref.syntax().text().to_string();
+ let field_pat = pat.syntax().text().to_string();
+ let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
+ if field_name != field_pat || field_name_is_tup_index {
+ continue;
+ }
+
+ let mut edit_builder = TextEdit::builder();
+ edit_builder.delete(record_pat_field.syntax().text_range());
+ edit_builder.insert(record_pat_field.syntax().text_range().start(), field_name);
+ let edit = edit_builder.finish();
+
+ let field_range = record_pat_field.syntax().text_range();
+ acc.push(
+ Diagnostic::new("use-field-shorthand", "Shorthand struct pattern", field_range)
+ .severity(Severity::WeakWarning)
+ .with_fixes(Some(vec![fix(
+ "use_pat_field_shorthand",
+ "Use struct field shorthand",
+ SourceChange::from_text_edit(file_id, edit),
+ field_range,
+ )])),
+ );
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::{check_diagnostics, check_fix};
+
+ #[test]
+ fn test_check_expr_field_shorthand() {
+ check_diagnostics(
+ r#"
+struct A { a: &'static str }
+fn main() { A { a: "hello" }; }
+"#,
+ );
+ check_diagnostics(
+ r#"
+struct A(usize);
+fn main() { A { 0: 0 }; }
+"#,
+ );
+
+ check_fix(
+ r#"
+struct A { a: &'static str }
+fn main() {
+ let a = "haha";
+ A { a$0: a };
+}
+"#,
+ r#"
+struct A { a: &'static str }
+fn main() {
+ let a = "haha";
+ A { a };
+}
+"#,
+ );
+
+ check_fix(
+ r#"
+struct A { a: &'static str, b: &'static str }
+fn main() {
+ let a = "haha";
+ let b = "bb";
+ A { a$0: a, b };
+}
+"#,
+ r#"
+struct A { a: &'static str, b: &'static str }
+fn main() {
+ let a = "haha";
+ let b = "bb";
+ A { a, b };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_check_pat_field_shorthand() {
+ check_diagnostics(
+ r#"
+struct A { a: &'static str }
+fn f(a: A) { let A { a: hello } = a; }
+"#,
+ );
+ check_diagnostics(
+ r#"
+struct A(usize);
+fn f(a: A) { let A { 0: 0 } = a; }
+"#,
+ );
+
+ check_fix(
+ r#"
+struct A { a: &'static str }
+fn f(a: A) {
+ let A { a$0: a } = a;
+}
+"#,
+ r#"
+struct A { a: &'static str }
+fn f(a: A) {
+ let A { a } = a;
+}
+"#,
+ );
+
+ check_fix(
+ r#"
+struct A { a: &'static str, b: &'static str }
+fn f(a: A) {
+ let A { a$0: a, b } = a;
+}
+"#,
+ r#"
+struct A { a: &'static str, b: &'static str }
+fn f(a: A) {
+ let A { a, b } = a;
+}
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/inactive_code.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/inactive_code.rs
new file mode 100644
index 000000000..97ea5c456
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/inactive_code.rs
@@ -0,0 +1,144 @@
+use cfg::DnfExpr;
+use stdx::format_to;
+
+use crate::{Diagnostic, DiagnosticsContext, Severity};
+
+// Diagnostic: inactive-code
+//
+// This diagnostic is shown for code with inactive `#[cfg]` attributes.
+pub(crate) fn inactive_code(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::InactiveCode,
+) -> Option<Diagnostic> {
+ // If there's inactive code somewhere in a macro, don't propagate to the call-site.
+ if d.node.file_id.is_macro() {
+ return None;
+ }
+
+ let inactive = DnfExpr::new(d.cfg.clone()).why_inactive(&d.opts);
+ let mut message = "code is inactive due to #[cfg] directives".to_string();
+
+ if let Some(inactive) = inactive {
+ let inactive_reasons = inactive.to_string();
+
+ if inactive_reasons.is_empty() {
+ format_to!(message);
+ } else {
+ format_to!(message, ": {}", inactive);
+ }
+ }
+
+ let res = Diagnostic::new(
+ "inactive-code",
+ message,
+ ctx.sema.diagnostics_display_range(d.node.clone()).range,
+ )
+ .severity(Severity::WeakWarning)
+ .with_unused(true);
+ Some(res)
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{tests::check_diagnostics_with_config, DiagnosticsConfig};
+
+ pub(crate) fn check(ra_fixture: &str) {
+ let config = DiagnosticsConfig::default();
+ check_diagnostics_with_config(config, ra_fixture)
+ }
+
+ #[test]
+ fn cfg_diagnostics() {
+ check(
+ r#"
+fn f() {
+ // The three g̶e̶n̶d̶e̶r̶s̶ statements:
+
+ #[cfg(a)] fn f() {} // Item statement
+ //^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
+ #[cfg(a)] {} // Expression statement
+ //^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
+ #[cfg(a)] let x = 0; // let statement
+ //^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
+
+ abc(#[cfg(a)] 0);
+ //^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
+ let x = Struct {
+ #[cfg(a)] f: 0,
+ //^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
+ };
+ match () {
+ () => (),
+ #[cfg(a)] () => (),
+ //^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
+ }
+
+ #[cfg(a)] 0 // Trailing expression of block
+ //^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: a is disabled
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn inactive_item() {
+ // Additional tests in `cfg` crate. This only tests disabled cfgs.
+
+ check(
+ r#"
+ #[cfg(no)] pub fn f() {}
+ //^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled
+
+ #[cfg(no)] #[cfg(no2)] mod m;
+ //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no and no2 are disabled
+
+ #[cfg(all(not(a), b))] enum E {}
+ //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: b is disabled
+
+ #[cfg(feature = "std")] use std;
+ //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: feature = "std" is disabled
+
+ #[cfg(any())] pub fn f() {}
+ //^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives
+"#,
+ );
+ }
+
+ #[test]
+ fn inactive_assoc_item() {
+ // FIXME these currently don't work, hence the *
+ check(
+ r#"
+struct Foo;
+impl Foo {
+ #[cfg(any())] pub fn f() {}
+ //*************************** weak: code is inactive due to #[cfg] directives
+}
+
+trait Bar {
+ #[cfg(any())] pub fn f() {}
+ //*************************** weak: code is inactive due to #[cfg] directives
+}
+"#,
+ );
+ }
+
+ /// Tests that `cfg` attributes behind `cfg_attr` is handled properly.
+ #[test]
+ fn inactive_via_cfg_attr() {
+ cov_mark::check!(cfg_attr_active);
+ check(
+ r#"
+ #[cfg_attr(not(never), cfg(no))] fn f() {}
+ //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled
+
+ #[cfg_attr(not(never), cfg(not(no)))] fn f() {}
+
+ #[cfg_attr(never, cfg(no))] fn g() {}
+
+ #[cfg_attr(not(never), inline, cfg(no))] fn h() {}
+ //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: no is disabled
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/incorrect_case.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/incorrect_case.rs
new file mode 100644
index 000000000..6a78c08d4
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/incorrect_case.rs
@@ -0,0 +1,486 @@
+use hir::{db::AstDatabase, InFile};
+use ide_db::{assists::Assist, defs::NameClass};
+use syntax::AstNode;
+
+use crate::{
+ // references::rename::rename_with_semantics,
+ unresolved_fix,
+ Diagnostic,
+ DiagnosticsContext,
+ Severity,
+};
+
+// Diagnostic: incorrect-ident-case
+//
+// 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].
+pub(crate) fn incorrect_case(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Diagnostic {
+ Diagnostic::new(
+ "incorrect-ident-case",
+ format!(
+ "{} `{}` should have {} name, e.g. `{}`",
+ d.ident_type, d.ident_text, d.expected_case, d.suggested_text
+ ),
+ ctx.sema.diagnostics_display_range(InFile::new(d.file, d.ident.clone().into())).range,
+ )
+ .severity(Severity::WeakWarning)
+ .with_fixes(fixes(ctx, d))
+}
+
+fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option<Vec<Assist>> {
+ let root = ctx.sema.db.parse_or_expand(d.file)?;
+ let name_node = d.ident.to_node(&root);
+ let def = NameClass::classify(&ctx.sema, &name_node)?.defined()?;
+
+ let name_node = InFile::new(d.file, name_node.syntax());
+ let frange = name_node.original_file_range(ctx.sema.db);
+
+ let label = format!("Rename to {}", d.suggested_text);
+ let mut res = unresolved_fix("change_case", &label, frange.range);
+ if ctx.resolve.should_resolve(&res.id) {
+ let source_change = def.rename(&ctx.sema, &d.suggested_text);
+ res.source_change = Some(source_change.ok().unwrap_or_default());
+ }
+
+ Some(vec![res])
+}
+
+#[cfg(test)]
+mod change_case {
+ use crate::tests::{check_diagnostics, check_fix};
+
+ #[test]
+ fn test_rename_incorrect_case() {
+ check_fix(
+ r#"
+pub struct test_struct$0 { one: i32 }
+
+pub fn some_fn(val: test_struct) -> test_struct {
+ test_struct { one: val.one + 1 }
+}
+"#,
+ r#"
+pub struct TestStruct { one: i32 }
+
+pub fn some_fn(val: TestStruct) -> TestStruct {
+ TestStruct { one: val.one + 1 }
+}
+"#,
+ );
+
+ check_fix(
+ r#"
+pub fn some_fn(NonSnakeCase$0: u8) -> u8 {
+ NonSnakeCase
+}
+"#,
+ r#"
+pub fn some_fn(non_snake_case: u8) -> u8 {
+ non_snake_case
+}
+"#,
+ );
+
+ check_fix(
+ r#"
+pub fn SomeFn$0(val: u8) -> u8 {
+ if val != 0 { SomeFn(val - 1) } else { val }
+}
+"#,
+ r#"
+pub fn some_fn(val: u8) -> u8 {
+ if val != 0 { some_fn(val - 1) } else { val }
+}
+"#,
+ );
+
+ check_fix(
+ r#"
+fn some_fn() {
+ let whatAWeird_Formatting$0 = 10;
+ another_func(whatAWeird_Formatting);
+}
+"#,
+ r#"
+fn some_fn() {
+ let what_aweird_formatting = 10;
+ another_func(what_aweird_formatting);
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_uppercase_const_no_diagnostics() {
+ check_diagnostics(
+ r#"
+fn foo() {
+ const ANOTHER_ITEM: &str = "some_item";
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_rename_incorrect_case_struct_method() {
+ check_fix(
+ r#"
+pub struct TestStruct;
+
+impl TestStruct {
+ pub fn SomeFn$0() -> TestStruct {
+ TestStruct
+ }
+}
+"#,
+ r#"
+pub struct TestStruct;
+
+impl TestStruct {
+ pub fn some_fn() -> TestStruct {
+ TestStruct
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
+ check_diagnostics(
+ r#"
+fn FOO() {}
+// ^^^ 💡 weak: Function `FOO` should have snake_case name, e.g. `foo`
+"#,
+ );
+ check_fix(r#"fn FOO$0() {}"#, r#"fn foo() {}"#);
+ }
+
+ #[test]
+ fn incorrect_function_name() {
+ check_diagnostics(
+ r#"
+fn NonSnakeCaseName() {}
+// ^^^^^^^^^^^^^^^^ 💡 weak: Function `NonSnakeCaseName` should have snake_case name, e.g. `non_snake_case_name`
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_function_params() {
+ check_diagnostics(
+ r#"
+fn foo(SomeParam: u8) {}
+ // ^^^^^^^^^ 💡 weak: Parameter `SomeParam` should have snake_case name, e.g. `some_param`
+
+fn foo2(ok_param: &str, CAPS_PARAM: u8) {}
+ // ^^^^^^^^^^ 💡 weak: Parameter `CAPS_PARAM` should have snake_case name, e.g. `caps_param`
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_variable_names() {
+ check_diagnostics(
+ r#"
+fn foo() {
+ let SOME_VALUE = 10;
+ // ^^^^^^^^^^ 💡 weak: Variable `SOME_VALUE` should have snake_case name, e.g. `some_value`
+ let AnotherValue = 20;
+ // ^^^^^^^^^^^^ 💡 weak: Variable `AnotherValue` should have snake_case name, e.g. `another_value`
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_struct_names() {
+ check_diagnostics(
+ r#"
+struct non_camel_case_name {}
+ // ^^^^^^^^^^^^^^^^^^^ 💡 weak: Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName`
+
+struct SCREAMING_CASE {}
+ // ^^^^^^^^^^^^^^ 💡 weak: Structure `SCREAMING_CASE` should have CamelCase name, e.g. `ScreamingCase`
+"#,
+ );
+ }
+
+ #[test]
+ fn no_diagnostic_for_camel_cased_acronyms_in_struct_name() {
+ check_diagnostics(
+ r#"
+struct AABB {}
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_struct_field() {
+ check_diagnostics(
+ r#"
+struct SomeStruct { SomeField: u8 }
+ // ^^^^^^^^^ 💡 weak: Field `SomeField` should have snake_case name, e.g. `some_field`
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_enum_names() {
+ check_diagnostics(
+ r#"
+enum some_enum { Val(u8) }
+ // ^^^^^^^^^ 💡 weak: Enum `some_enum` should have CamelCase name, e.g. `SomeEnum`
+
+enum SOME_ENUM {}
+ // ^^^^^^^^^ 💡 weak: Enum `SOME_ENUM` should have CamelCase name, e.g. `SomeEnum`
+"#,
+ );
+ }
+
+ #[test]
+ fn no_diagnostic_for_camel_cased_acronyms_in_enum_name() {
+ check_diagnostics(
+ r#"
+enum AABB {}
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_enum_variant_name() {
+ check_diagnostics(
+ r#"
+enum SomeEnum { SOME_VARIANT(u8) }
+ // ^^^^^^^^^^^^ 💡 weak: Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant`
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_const_name() {
+ check_diagnostics(
+ r#"
+const some_weird_const: u8 = 10;
+ // ^^^^^^^^^^^^^^^^ 💡 weak: Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
+"#,
+ );
+ }
+
+ #[test]
+ fn incorrect_static_name() {
+ check_diagnostics(
+ r#"
+static some_weird_const: u8 = 10;
+ // ^^^^^^^^^^^^^^^^ 💡 weak: Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
+"#,
+ );
+ }
+
+ #[test]
+ fn fn_inside_impl_struct() {
+ check_diagnostics(
+ r#"
+struct someStruct;
+ // ^^^^^^^^^^ 💡 weak: Structure `someStruct` should have CamelCase name, e.g. `SomeStruct`
+
+impl someStruct {
+ fn SomeFunc(&self) {
+ // ^^^^^^^^ 💡 weak: Function `SomeFunc` should have snake_case name, e.g. `some_func`
+ let WHY_VAR_IS_CAPS = 10;
+ // ^^^^^^^^^^^^^^^ 💡 weak: Variable `WHY_VAR_IS_CAPS` should have snake_case name, e.g. `why_var_is_caps`
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn no_diagnostic_for_enum_varinats() {
+ check_diagnostics(
+ r#"
+enum Option { Some, None }
+
+fn main() {
+ match Option::None {
+ None => (),
+ Some => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn non_let_bind() {
+ check_diagnostics(
+ r#"
+enum Option { Some, None }
+
+fn main() {
+ match Option::None {
+ SOME_VAR @ None => (),
+ // ^^^^^^^^ 💡 weak: Variable `SOME_VAR` should have snake_case name, e.g. `some_var`
+ Some => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn allow_attributes_crate_attr() {
+ check_diagnostics(
+ r#"
+#![allow(non_snake_case)]
+#![allow(non_camel_case_types)]
+
+struct S {
+ fooBar: bool,
+}
+
+enum E {
+ fooBar,
+}
+
+mod F {
+ fn CheckItWorksWithCrateAttr(BAD_NAME_HI: u8) {}
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn complex_ignore() {
+ // FIXME: this should trigger errors for the second case.
+ check_diagnostics(
+ r#"
+trait T { fn a(); }
+struct U {}
+impl T for U {
+ fn a() {
+ #[allow(non_snake_case)]
+ trait __BitFlagsOk {
+ const HiImAlsoBad: u8 = 2;
+ fn Dirty(&self) -> bool { false }
+ }
+
+ trait __BitFlagsBad {
+ const HiImAlsoBad: u8 = 2;
+ fn Dirty(&self) -> bool { false }
+ }
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn infinite_loop_inner_items() {
+ check_diagnostics(
+ r#"
+fn qualify() {
+ mod foo {
+ use super::*;
+ }
+}
+ "#,
+ )
+ }
+
+ #[test] // Issue #8809.
+ fn parenthesized_parameter() {
+ check_diagnostics(r#"fn f((O): _) {}"#)
+ }
+
+ #[test]
+ fn ignores_extern_items() {
+ cov_mark::check!(extern_func_incorrect_case_ignored);
+ cov_mark::check!(extern_static_incorrect_case_ignored);
+ check_diagnostics(
+ r#"
+extern {
+ fn NonSnakeCaseName(SOME_VAR: u8) -> u8;
+ pub static SomeStatic: u8 = 10;
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn ignores_extern_items_from_macro() {
+ check_diagnostics(
+ r#"
+macro_rules! m {
+ () => {
+ fn NonSnakeCaseName(SOME_VAR: u8) -> u8;
+ pub static SomeStatic: u8 = 10;
+ }
+}
+
+extern {
+ m!();
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn bug_traits_arent_checked() {
+ // FIXME: Traits and functions in traits aren't currently checked by
+ // r-a, even though rustc will complain about them.
+ check_diagnostics(
+ r#"
+trait BAD_TRAIT {
+ fn BAD_FUNCTION();
+ fn BadFunction();
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn allow_attributes() {
+ check_diagnostics(
+ r#"
+#[allow(non_snake_case)]
+fn NonSnakeCaseName(SOME_VAR: u8) -> u8{
+ // cov_flags generated output from elsewhere in this file
+ extern "C" {
+ #[no_mangle]
+ static lower_case: u8;
+ }
+
+ let OtherVar = SOME_VAR + 1;
+ OtherVar
+}
+
+#[allow(nonstandard_style)]
+mod CheckNonstandardStyle {
+ fn HiImABadFnName() {}
+}
+
+#[allow(bad_style)]
+mod CheckBadStyle {
+ fn HiImABadFnName() {}
+}
+
+mod F {
+ #![allow(non_snake_case)]
+ fn CheckItWorksWithModAttr(BAD_NAME_HI: u8) {}
+}
+
+#[allow(non_snake_case, non_camel_case_types)]
+pub struct some_type {
+ SOME_FIELD: u8,
+ SomeField: u16,
+}
+
+#[allow(non_upper_case_globals)]
+pub const some_const: u8 = 10;
+
+#[allow(non_upper_case_globals)]
+pub static SomeStatic: u8 = 10;
+ "#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_derive_target.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_derive_target.rs
new file mode 100644
index 000000000..c779266bc
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/invalid_derive_target.rs
@@ -0,0 +1,38 @@
+use crate::{Diagnostic, DiagnosticsContext, Severity};
+
+// Diagnostic: invalid-derive-target
+//
+// This diagnostic is shown when the derive attribute is used on an item other than a `struct`,
+// `enum` or `union`.
+pub(crate) fn invalid_derive_target(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::InvalidDeriveTarget,
+) -> Diagnostic {
+ let display_range = ctx.sema.diagnostics_display_range(d.node.clone()).range;
+
+ Diagnostic::new(
+ "invalid-derive-target",
+ "`derive` may only be applied to `struct`s, `enum`s and `union`s",
+ display_range,
+ )
+ .severity(Severity::Error)
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn fails_on_function() {
+ check_diagnostics(
+ r#"
+//- minicore:derive
+mod __ {
+ #[derive()]
+ //^^^^^^^^^^^ error: `derive` may only be applied to `struct`s, `enum`s and `union`s
+ fn main() {}
+}
+ "#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/macro_error.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/macro_error.rs
new file mode 100644
index 000000000..d6a66dc15
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/macro_error.rs
@@ -0,0 +1,218 @@
+use crate::{Diagnostic, DiagnosticsContext};
+
+// Diagnostic: macro-error
+//
+// This diagnostic is shown for macro expansion errors.
+pub(crate) fn macro_error(ctx: &DiagnosticsContext<'_>, d: &hir::MacroError) -> Diagnostic {
+ // Use more accurate position if available.
+ let display_range = d
+ .precise_location
+ .unwrap_or_else(|| ctx.sema.diagnostics_display_range(d.node.clone()).range);
+
+ Diagnostic::new("macro-error", d.message.clone(), display_range).experimental()
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ tests::{check_diagnostics, check_diagnostics_with_config},
+ DiagnosticsConfig,
+ };
+
+ #[test]
+ fn builtin_macro_fails_expansion() {
+ check_diagnostics(
+ r#"
+#[rustc_builtin_macro]
+macro_rules! include { () => {} }
+
+#[rustc_builtin_macro]
+macro_rules! compile_error { () => {} }
+
+ include!("doesntexist");
+//^^^^^^^ error: failed to load file `doesntexist`
+
+ compile_error!("compile_error macro works");
+//^^^^^^^^^^^^^ error: compile_error macro works
+ "#,
+ );
+ }
+
+ #[test]
+ fn eager_macro_concat() {
+ // FIXME: this is incorrectly handling `$crate`, resulting in a wrong diagnostic.
+ // See: https://github.com/rust-lang/rust-analyzer/issues/10300
+
+ check_diagnostics(
+ r#"
+//- /lib.rs crate:lib deps:core
+use core::{panic, concat};
+
+mod private {
+ pub use core::concat;
+}
+
+macro_rules! m {
+ () => {
+ panic!(concat!($crate::private::concat!("")));
+ };
+}
+
+fn f() {
+ m!();
+ //^^^^ error: unresolved macro `$crate::private::concat!`
+}
+
+//- /core.rs crate:core
+#[macro_export]
+#[rustc_builtin_macro]
+macro_rules! concat { () => {} }
+
+pub macro panic {
+ ($msg:expr) => (
+ $crate::panicking::panic_str($msg)
+ ),
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn include_macro_should_allow_empty_content() {
+ let mut config = DiagnosticsConfig::default();
+
+ // FIXME: This is a false-positive, the file is actually linked in via
+ // `include!` macro
+ config.disabled.insert("unlinked-file".to_string());
+
+ check_diagnostics_with_config(
+ config,
+ r#"
+//- /lib.rs
+#[rustc_builtin_macro]
+macro_rules! include { () => {} }
+
+include!("foo/bar.rs");
+//- /foo/bar.rs
+// empty
+"#,
+ );
+ }
+
+ #[test]
+ fn good_out_dir_diagnostic() {
+ check_diagnostics(
+ r#"
+#[rustc_builtin_macro]
+macro_rules! include { () => {} }
+#[rustc_builtin_macro]
+macro_rules! env { () => {} }
+#[rustc_builtin_macro]
+macro_rules! concat { () => {} }
+
+ include!(concat!(env!("OUT_DIR"), "/out.rs"));
+//^^^^^^^ error: `OUT_DIR` not set, enable "build scripts" to fix
+"#,
+ );
+ }
+
+ #[test]
+ fn register_attr_and_tool() {
+ cov_mark::check!(register_attr);
+ cov_mark::check!(register_tool);
+ check_diagnostics(
+ r#"
+#![register_tool(tool)]
+#![register_attr(attr)]
+
+#[tool::path]
+#[attr]
+struct S;
+"#,
+ );
+ // NB: we don't currently emit diagnostics here
+ }
+
+ #[test]
+ fn macro_diag_builtin() {
+ check_diagnostics(
+ r#"
+#[rustc_builtin_macro]
+macro_rules! env {}
+
+#[rustc_builtin_macro]
+macro_rules! include {}
+
+#[rustc_builtin_macro]
+macro_rules! compile_error {}
+
+#[rustc_builtin_macro]
+macro_rules! format_args { () => {} }
+
+fn main() {
+ // Test a handful of built-in (eager) macros:
+
+ include!(invalid);
+ //^^^^^^^ error: could not convert tokens
+ include!("does not exist");
+ //^^^^^^^ error: failed to load file `does not exist`
+
+ env!(invalid);
+ //^^^ error: could not convert tokens
+
+ env!("OUT_DIR");
+ //^^^ error: `OUT_DIR` not set, enable "build scripts" to fix
+
+ compile_error!("compile_error works");
+ //^^^^^^^^^^^^^ error: compile_error works
+
+ // Lazy:
+
+ format_args!();
+ //^^^^^^^^^^^ error: no rule matches input tokens
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn macro_rules_diag() {
+ check_diagnostics(
+ r#"
+macro_rules! m {
+ () => {};
+}
+fn f() {
+ m!();
+
+ m!(hi);
+ //^ error: leftover tokens
+}
+ "#,
+ );
+ }
+ #[test]
+ fn dollar_crate_in_builtin_macro() {
+ check_diagnostics(
+ r#"
+#[macro_export]
+#[rustc_builtin_macro]
+macro_rules! format_args {}
+
+#[macro_export]
+macro_rules! arg { () => {} }
+
+#[macro_export]
+macro_rules! outer {
+ () => {
+ $crate::format_args!( "", $crate::arg!(1) )
+ };
+}
+
+fn f() {
+ outer!();
+} //^^^^^^^^ error: leftover tokens
+"#,
+ )
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/malformed_derive.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/malformed_derive.rs
new file mode 100644
index 000000000..cd48bdba0
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/malformed_derive.rs
@@ -0,0 +1,37 @@
+use crate::{Diagnostic, DiagnosticsContext, Severity};
+
+// Diagnostic: malformed-derive
+//
+// This diagnostic is shown when the derive attribute has invalid input.
+pub(crate) fn malformed_derive(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::MalformedDerive,
+) -> Diagnostic {
+ let display_range = ctx.sema.diagnostics_display_range(d.node.clone()).range;
+
+ Diagnostic::new(
+ "malformed-derive",
+ "malformed derive input, derive attributes are of the form `#[derive(Derive1, Derive2, ...)]`",
+ display_range,
+ )
+ .severity(Severity::Error)
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn invalid_input() {
+ check_diagnostics(
+ r#"
+//- minicore:derive
+mod __ {
+ #[derive = "aaaa"]
+ //^^^^^^^^^^^^^^^^^^ error: malformed derive input, derive attributes are of the form `#[derive(Derive1, Derive2, ...)]`
+ struct Foo;
+}
+ "#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs
new file mode 100644
index 000000000..5f8b3e543
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs
@@ -0,0 +1,334 @@
+use syntax::{
+ ast::{self, HasArgList},
+ AstNode, TextRange,
+};
+
+use crate::{adjusted_display_range, Diagnostic, DiagnosticsContext};
+
+// Diagnostic: mismatched-arg-count
+//
+// This diagnostic is triggered if a function is invoked with an incorrect amount of arguments.
+pub(crate) fn mismatched_arg_count(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::MismatchedArgCount,
+) -> Diagnostic {
+ let s = if d.expected == 1 { "" } else { "s" };
+ let message = format!("expected {} argument{}, found {}", d.expected, s, d.found);
+ Diagnostic::new("mismatched-arg-count", message, invalid_args_range(ctx, d))
+}
+
+fn invalid_args_range(ctx: &DiagnosticsContext<'_>, d: &hir::MismatchedArgCount) -> TextRange {
+ adjusted_display_range::<ast::Expr>(ctx, d.call_expr.clone().map(|it| it.into()), &|expr| {
+ let arg_list = match expr {
+ ast::Expr::CallExpr(call) => call.arg_list()?,
+ ast::Expr::MethodCallExpr(call) => call.arg_list()?,
+ _ => return None,
+ };
+ if d.found < d.expected {
+ if d.found == 0 {
+ return Some(arg_list.syntax().text_range());
+ }
+ if let Some(r_paren) = arg_list.r_paren_token() {
+ return Some(r_paren.text_range());
+ }
+ }
+ if d.expected < d.found {
+ if d.expected == 0 {
+ return Some(arg_list.syntax().text_range());
+ }
+ let zip = arg_list.args().nth(d.expected).zip(arg_list.r_paren_token());
+ if let Some((arg, r_paren)) = zip {
+ return Some(arg.syntax().text_range().cover(r_paren.text_range()));
+ }
+ }
+
+ None
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn simple_free_fn_zero() {
+ check_diagnostics(
+ r#"
+fn zero() {}
+fn f() { zero(1); }
+ //^^^ error: expected 0 arguments, found 1
+"#,
+ );
+
+ check_diagnostics(
+ r#"
+fn zero() {}
+fn f() { zero(); }
+"#,
+ );
+ }
+
+ #[test]
+ fn simple_free_fn_one() {
+ check_diagnostics(
+ r#"
+fn one(arg: u8) {}
+fn f() { one(); }
+ //^^ error: expected 1 argument, found 0
+"#,
+ );
+
+ check_diagnostics(
+ r#"
+fn one(arg: u8) {}
+fn f() { one(1); }
+"#,
+ );
+ }
+
+ #[test]
+ fn method_as_fn() {
+ check_diagnostics(
+ r#"
+struct S;
+impl S { fn method(&self) {} }
+
+fn f() {
+ S::method();
+} //^^ error: expected 1 argument, found 0
+"#,
+ );
+
+ check_diagnostics(
+ r#"
+struct S;
+impl S { fn method(&self) {} }
+
+fn f() {
+ S::method(&S);
+ S.method();
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn method_with_arg() {
+ check_diagnostics(
+ r#"
+struct S;
+impl S { fn method(&self, arg: u8) {} }
+
+ fn f() {
+ S.method();
+ } //^^ error: expected 1 argument, found 0
+ "#,
+ );
+
+ check_diagnostics(
+ r#"
+struct S;
+impl S { fn method(&self, arg: u8) {} }
+
+fn f() {
+ S::method(&S, 0);
+ S.method(1);
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn method_unknown_receiver() {
+ // note: this is incorrect code, so there might be errors on this in the
+ // future, but we shouldn't emit an argument count diagnostic here
+ check_diagnostics(
+ r#"
+trait Foo { fn method(&self, arg: usize) {} }
+
+fn f() {
+ let x;
+ x.method();
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn tuple_struct() {
+ check_diagnostics(
+ r#"
+struct Tup(u8, u16);
+fn f() {
+ Tup(0);
+} //^ error: expected 2 arguments, found 1
+"#,
+ )
+ }
+
+ #[test]
+ fn enum_variant() {
+ check_diagnostics(
+ r#"
+enum En { Variant(u8, u16), }
+fn f() {
+ En::Variant(0);
+} //^ error: expected 2 arguments, found 1
+"#,
+ )
+ }
+
+ #[test]
+ fn enum_variant_type_macro() {
+ check_diagnostics(
+ r#"
+macro_rules! Type {
+ () => { u32 };
+}
+enum Foo {
+ Bar(Type![])
+}
+impl Foo {
+ fn new() {
+ Foo::Bar(0);
+ Foo::Bar(0, 1);
+ //^^ error: expected 1 argument, found 2
+ Foo::Bar();
+ //^^ error: expected 1 argument, found 0
+ }
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn varargs() {
+ check_diagnostics(
+ r#"
+extern "C" {
+ fn fixed(fixed: u8);
+ fn varargs(fixed: u8, ...);
+ fn varargs2(...);
+}
+
+fn f() {
+ unsafe {
+ fixed(0);
+ fixed(0, 1);
+ //^^ error: expected 1 argument, found 2
+ varargs(0);
+ varargs(0, 1);
+ varargs2();
+ varargs2(0);
+ varargs2(0, 1);
+ }
+}
+ "#,
+ )
+ }
+
+ #[test]
+ fn arg_count_lambda() {
+ check_diagnostics(
+ r#"
+fn main() {
+ let f = |()| ();
+ f();
+ //^^ error: expected 1 argument, found 0
+ f(());
+ f((), ());
+ //^^^ error: expected 1 argument, found 2
+}
+"#,
+ )
+ }
+
+ #[test]
+ fn cfgd_out_call_arguments() {
+ check_diagnostics(
+ r#"
+struct C(#[cfg(FALSE)] ());
+impl C {
+ fn new() -> Self {
+ Self(
+ #[cfg(FALSE)]
+ (),
+ )
+ }
+
+ fn method(&self) {}
+}
+
+fn main() {
+ C::new().method(#[cfg(FALSE)] 0);
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn cfgd_out_fn_params() {
+ check_diagnostics(
+ r#"
+fn foo(#[cfg(NEVER)] x: ()) {}
+
+struct S;
+
+impl S {
+ fn method(#[cfg(NEVER)] self) {}
+ fn method2(#[cfg(NEVER)] self, arg: u8) {}
+ fn method3(self, #[cfg(NEVER)] arg: u8) {}
+}
+
+extern "C" {
+ fn fixed(fixed: u8, #[cfg(NEVER)] ...);
+ fn varargs(#[cfg(not(NEVER))] ...);
+}
+
+fn main() {
+ foo();
+ S::method();
+ S::method2(0);
+ S::method3(S);
+ S.method3();
+ unsafe {
+ fixed(0);
+ varargs(1, 2, 3);
+ }
+}
+ "#,
+ )
+ }
+
+ #[test]
+ fn legacy_const_generics() {
+ check_diagnostics(
+ r#"
+#[rustc_legacy_const_generics(1, 3)]
+fn mixed<const N1: &'static str, const N2: bool>(
+ a: u8,
+ b: i8,
+) {}
+
+fn f() {
+ mixed(0, "", -1, true);
+ mixed::<"", true>(0, -1);
+}
+
+#[rustc_legacy_const_generics(1, 3)]
+fn b<const N1: u8, const N2: u8>(
+ a: u8,
+ b: u8,
+) {}
+
+fn g() {
+ b(0, 1, 2, 3);
+ b::<1, 3>(0, 2);
+
+ b(0, 1, 2);
+ //^ error: expected 4 arguments, found 3
+}
+ "#,
+ )
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_fields.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_fields.rs
new file mode 100644
index 000000000..edb1fc091
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_fields.rs
@@ -0,0 +1,837 @@
+use either::Either;
+use hir::{
+ db::{AstDatabase, HirDatabase},
+ known, AssocItem, HirDisplay, InFile, Type,
+};
+use ide_db::{
+ assists::Assist, famous_defs::FamousDefs, imports::import_assets::item_for_path_search,
+ source_change::SourceChange, use_trivial_contructor::use_trivial_constructor, FxHashMap,
+};
+use stdx::format_to;
+use syntax::{
+ algo,
+ ast::{self, make},
+ AstNode, SyntaxNode, SyntaxNodePtr,
+};
+use text_edit::TextEdit;
+
+use crate::{fix, Diagnostic, DiagnosticsContext};
+
+// Diagnostic: missing-fields
+//
+// This diagnostic is triggered if record lacks some fields that exist in the corresponding structure.
+//
+// Example:
+//
+// ```rust
+// struct A { a: u8, b: u8 }
+//
+// let a = A { a: 10 };
+// ```
+pub(crate) fn missing_fields(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Diagnostic {
+ let mut message = String::from("missing structure fields:\n");
+ for field in &d.missed_fields {
+ format_to!(message, "- {}\n", field);
+ }
+
+ let ptr = InFile::new(
+ d.file,
+ d.field_list_parent_path
+ .clone()
+ .map(SyntaxNodePtr::from)
+ .unwrap_or_else(|| d.field_list_parent.clone().either(|it| it.into(), |it| it.into())),
+ );
+
+ Diagnostic::new("missing-fields", message, ctx.sema.diagnostics_display_range(ptr).range)
+ .with_fixes(fixes(ctx, d))
+}
+
+fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingFields) -> Option<Vec<Assist>> {
+ // Note that although we could add a diagnostics to
+ // fill the missing tuple field, e.g :
+ // `struct A(usize);`
+ // `let a = A { 0: () }`
+ // but it is uncommon usage and it should not be encouraged.
+ if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
+ return None;
+ }
+
+ let root = ctx.sema.db.parse_or_expand(d.file)?;
+
+ let current_module = match &d.field_list_parent {
+ Either::Left(ptr) => ctx.sema.scope(ptr.to_node(&root).syntax()).map(|it| it.module()),
+ Either::Right(ptr) => ctx.sema.scope(ptr.to_node(&root).syntax()).map(|it| it.module()),
+ };
+
+ let build_text_edit = |parent_syntax, new_syntax: &SyntaxNode, old_syntax| {
+ let edit = {
+ let mut builder = TextEdit::builder();
+ if d.file.is_macro() {
+ // we can't map the diff up into the macro input unfortunately, as the macro loses all
+ // whitespace information so the diff wouldn't be applicable no matter what
+ // This has the downside that the cursor will be moved in macros by doing it without a diff
+ // but that is a trade off we can make.
+ // FIXME: this also currently discards a lot of whitespace in the input... we really need a formatter here
+ let range = ctx.sema.original_range_opt(old_syntax)?;
+ builder.replace(range.range, new_syntax.to_string());
+ } else {
+ algo::diff(old_syntax, new_syntax).into_text_edit(&mut builder);
+ }
+ builder.finish()
+ };
+ Some(vec![fix(
+ "fill_missing_fields",
+ "Fill struct fields",
+ SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit),
+ ctx.sema.original_range(parent_syntax).range,
+ )])
+ };
+
+ match &d.field_list_parent {
+ Either::Left(record_expr) => {
+ let field_list_parent = record_expr.to_node(&root);
+ let missing_fields = ctx.sema.record_literal_missing_fields(&field_list_parent);
+
+ let mut locals = FxHashMap::default();
+ ctx.sema.scope(field_list_parent.syntax())?.process_all_names(&mut |name, def| {
+ if let hir::ScopeDef::Local(local) = def {
+ locals.insert(name, local);
+ }
+ });
+
+ let generate_fill_expr = |ty: &Type| match ctx.config.expr_fill_default {
+ crate::ExprFillDefaultMode::Todo => make::ext::expr_todo(),
+ crate::ExprFillDefaultMode::Default => {
+ get_default_constructor(ctx, d, ty).unwrap_or_else(|| make::ext::expr_todo())
+ }
+ };
+
+ let old_field_list = field_list_parent.record_expr_field_list()?;
+ let new_field_list = old_field_list.clone_for_update();
+ for (f, ty) in missing_fields.iter() {
+ let field_expr = if let Some(local_candidate) = locals.get(&f.name(ctx.sema.db)) {
+ cov_mark::hit!(field_shorthand);
+ let candidate_ty = local_candidate.ty(ctx.sema.db);
+ if ty.could_unify_with(ctx.sema.db, &candidate_ty) {
+ None
+ } else {
+ Some(generate_fill_expr(ty))
+ }
+ } else {
+ let expr = (|| -> Option<ast::Expr> {
+ let item_in_ns = hir::ItemInNs::from(hir::ModuleDef::from(ty.as_adt()?));
+
+ let type_path = current_module?.find_use_path(
+ ctx.sema.db,
+ item_for_path_search(ctx.sema.db, item_in_ns)?,
+ )?;
+
+ use_trivial_constructor(
+ &ctx.sema.db,
+ ide_db::helpers::mod_path_to_ast(&type_path),
+ &ty,
+ )
+ })();
+
+ if expr.is_some() {
+ expr
+ } else {
+ Some(generate_fill_expr(ty))
+ }
+ };
+ let field = make::record_expr_field(
+ make::name_ref(&f.name(ctx.sema.db).to_smol_str()),
+ field_expr,
+ );
+ new_field_list.add_field(field.clone_for_update());
+ }
+ build_text_edit(
+ field_list_parent.syntax(),
+ new_field_list.syntax(),
+ old_field_list.syntax(),
+ )
+ }
+ Either::Right(record_pat) => {
+ let field_list_parent = record_pat.to_node(&root);
+ let missing_fields = ctx.sema.record_pattern_missing_fields(&field_list_parent);
+
+ let old_field_list = field_list_parent.record_pat_field_list()?;
+ let new_field_list = old_field_list.clone_for_update();
+ for (f, _) in missing_fields.iter() {
+ let field = make::record_pat_field_shorthand(make::name_ref(
+ &f.name(ctx.sema.db).to_smol_str(),
+ ));
+ new_field_list.add_field(field.clone_for_update());
+ }
+ build_text_edit(
+ field_list_parent.syntax(),
+ new_field_list.syntax(),
+ old_field_list.syntax(),
+ )
+ }
+ }
+}
+
+fn make_ty(ty: &hir::Type, db: &dyn HirDatabase, module: hir::Module) -> ast::Type {
+ let ty_str = match ty.as_adt() {
+ Some(adt) => adt.name(db).to_string(),
+ None => ty.display_source_code(db, module.into()).ok().unwrap_or_else(|| "_".to_string()),
+ };
+
+ make::ty(&ty_str)
+}
+
+fn get_default_constructor(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::MissingFields,
+ ty: &Type,
+) -> Option<ast::Expr> {
+ if let Some(builtin_ty) = ty.as_builtin() {
+ if builtin_ty.is_int() || builtin_ty.is_uint() {
+ return Some(make::ext::zero_number());
+ }
+ if builtin_ty.is_float() {
+ return Some(make::ext::zero_float());
+ }
+ if builtin_ty.is_char() {
+ return Some(make::ext::empty_char());
+ }
+ if builtin_ty.is_str() {
+ return Some(make::ext::empty_str());
+ }
+ if builtin_ty.is_bool() {
+ return Some(make::ext::default_bool());
+ }
+ }
+
+ let krate = ctx.sema.to_module_def(d.file.original_file(ctx.sema.db))?.krate();
+ let module = krate.root_module(ctx.sema.db);
+
+ // Look for a ::new() associated function
+ let has_new_func = ty
+ .iterate_assoc_items(ctx.sema.db, krate, |assoc_item| {
+ if let AssocItem::Function(func) = assoc_item {
+ if func.name(ctx.sema.db) == known::new
+ && func.assoc_fn_params(ctx.sema.db).is_empty()
+ {
+ return Some(());
+ }
+ }
+
+ None
+ })
+ .is_some();
+
+ let famous_defs = FamousDefs(&ctx.sema, krate);
+ if has_new_func {
+ Some(make::ext::expr_ty_new(&make_ty(ty, ctx.sema.db, module)))
+ } else if ty.as_adt() == famous_defs.core_option_Option()?.ty(ctx.sema.db).as_adt() {
+ Some(make::ext::option_none())
+ } else if !ty.is_array()
+ && ty.impls_trait(ctx.sema.db, famous_defs.core_default_Default()?, &[])
+ {
+ Some(make::ext::expr_ty_default(&make_ty(ty, ctx.sema.db, module)))
+ } else {
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::{check_diagnostics, check_fix};
+
+ #[test]
+ fn missing_record_pat_field_diagnostic() {
+ check_diagnostics(
+ r#"
+struct S { foo: i32, bar: () }
+fn baz(s: S) {
+ let S { foo: _ } = s;
+ //^ 💡 error: missing structure fields:
+ //| - bar
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn missing_record_pat_field_no_diagnostic_if_not_exhaustive() {
+ check_diagnostics(
+ r"
+struct S { foo: i32, bar: () }
+fn baz(s: S) -> i32 {
+ match s {
+ S { foo, .. } => foo,
+ }
+}
+",
+ )
+ }
+
+ #[test]
+ fn missing_record_pat_field_box() {
+ check_diagnostics(
+ r"
+struct S { s: Box<u32> }
+fn x(a: S) {
+ let S { box s } = a;
+}
+",
+ )
+ }
+
+ #[test]
+ fn missing_record_pat_field_ref() {
+ check_diagnostics(
+ r"
+struct S { s: u32 }
+fn x(a: S) {
+ let S { ref s } = a;
+}
+",
+ )
+ }
+
+ #[test]
+ fn missing_record_expr_in_assignee_expr() {
+ check_diagnostics(
+ r"
+struct S { s: usize, t: usize }
+struct S2 { s: S, t: () }
+struct T(S);
+fn regular(a: S) {
+ let s;
+ S { s, .. } = a;
+}
+fn nested(a: S2) {
+ let s;
+ S2 { s: S { s, .. }, .. } = a;
+}
+fn in_tuple(a: (S,)) {
+ let s;
+ (S { s, .. },) = a;
+}
+fn in_array(a: [S;1]) {
+ let s;
+ [S { s, .. },] = a;
+}
+fn in_tuple_struct(a: T) {
+ let s;
+ T(S { s, .. }) = a;
+}
+ ",
+ );
+ }
+
+ #[test]
+ fn range_mapping_out_of_macros() {
+ check_fix(
+ r#"
+fn some() {}
+fn items() {}
+fn here() {}
+
+macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
+
+fn main() {
+ let _x = id![Foo { a: $042 }];
+}
+
+pub struct Foo { pub a: i32, pub b: i32 }
+"#,
+ r#"
+fn some() {}
+fn items() {}
+fn here() {}
+
+macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
+
+fn main() {
+ let _x = id![Foo {a:42, b: 0 }];
+}
+
+pub struct Foo { pub a: i32, pub b: i32 }
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_empty() {
+ check_fix(
+ r#"
+//- minicore: option
+struct TestStruct { one: i32, two: i64, three: Option<i32>, four: bool }
+
+fn test_fn() {
+ let s = TestStruct {$0};
+}
+"#,
+ r#"
+struct TestStruct { one: i32, two: i64, three: Option<i32>, four: bool }
+
+fn test_fn() {
+ let s = TestStruct { one: 0, two: 0, three: None, four: false };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_zst_fields() {
+ check_fix(
+ r#"
+struct Empty;
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct {$0};
+}
+"#,
+ r#"
+struct Empty;
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct { one: 0, two: Empty };
+}
+"#,
+ );
+ check_fix(
+ r#"
+enum Empty { Foo };
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct {$0};
+}
+"#,
+ r#"
+enum Empty { Foo };
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct { one: 0, two: Empty::Foo };
+}
+"#,
+ );
+
+ // make sure the assist doesn't fill non Unit variants
+ check_fix(
+ r#"
+struct Empty {};
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct {$0};
+}
+"#,
+ r#"
+struct Empty {};
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct { one: 0, two: todo!() };
+}
+"#,
+ );
+ check_fix(
+ r#"
+enum Empty { Foo {} };
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct {$0};
+}
+"#,
+ r#"
+enum Empty { Foo {} };
+
+struct TestStruct { one: i32, two: Empty }
+
+fn test_fn() {
+ let s = TestStruct { one: 0, two: todo!() };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_self() {
+ check_fix(
+ r#"
+struct TestStruct { one: i32 }
+
+impl TestStruct {
+ fn test_fn() { let s = Self {$0}; }
+}
+"#,
+ r#"
+struct TestStruct { one: i32 }
+
+impl TestStruct {
+ fn test_fn() { let s = Self { one: 0 }; }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_enum() {
+ check_fix(
+ r#"
+enum Expr {
+ Bin { lhs: Box<Expr>, rhs: Box<Expr> }
+}
+
+impl Expr {
+ fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
+ Expr::Bin {$0 }
+ }
+}
+"#,
+ r#"
+enum Expr {
+ Bin { lhs: Box<Expr>, rhs: Box<Expr> }
+}
+
+impl Expr {
+ fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
+ Expr::Bin { lhs, rhs }
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_partial() {
+ check_fix(
+ r#"
+struct TestStruct { one: i32, two: i64 }
+
+fn test_fn() {
+ let s = TestStruct{ two: 2$0 };
+}
+"#,
+ r"
+struct TestStruct { one: i32, two: i64 }
+
+fn test_fn() {
+ let s = TestStruct{ two: 2, one: 0 };
+}
+",
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_new() {
+ check_fix(
+ r#"
+struct TestWithNew(usize);
+impl TestWithNew {
+ pub fn new() -> Self {
+ Self(0)
+ }
+}
+struct TestStruct { one: i32, two: TestWithNew }
+
+fn test_fn() {
+ let s = TestStruct{ $0 };
+}
+"#,
+ r"
+struct TestWithNew(usize);
+impl TestWithNew {
+ pub fn new() -> Self {
+ Self(0)
+ }
+}
+struct TestStruct { one: i32, two: TestWithNew }
+
+fn test_fn() {
+ let s = TestStruct{ one: 0, two: TestWithNew::new() };
+}
+",
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_default() {
+ check_fix(
+ r#"
+//- minicore: default, option
+struct TestWithDefault(usize);
+impl Default for TestWithDefault {
+ pub fn default() -> Self {
+ Self(0)
+ }
+}
+struct TestStruct { one: i32, two: TestWithDefault }
+
+fn test_fn() {
+ let s = TestStruct{ $0 };
+}
+"#,
+ r"
+struct TestWithDefault(usize);
+impl Default for TestWithDefault {
+ pub fn default() -> Self {
+ Self(0)
+ }
+}
+struct TestStruct { one: i32, two: TestWithDefault }
+
+fn test_fn() {
+ let s = TestStruct{ one: 0, two: TestWithDefault::default() };
+}
+",
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_raw_ident() {
+ check_fix(
+ r#"
+struct TestStruct { r#type: u8 }
+
+fn test_fn() {
+ TestStruct { $0 };
+}
+"#,
+ r"
+struct TestStruct { r#type: u8 }
+
+fn test_fn() {
+ TestStruct { r#type: 0 };
+}
+",
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_no_diagnostic() {
+ check_diagnostics(
+ r#"
+struct TestStruct { one: i32, two: i64 }
+
+fn test_fn() {
+ let one = 1;
+ let s = TestStruct{ one, two: 2 };
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_no_diagnostic_on_spread() {
+ check_diagnostics(
+ r#"
+struct TestStruct { one: i32, two: i64 }
+
+fn test_fn() {
+ let one = 1;
+ let s = TestStruct{ ..a };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_blank_line() {
+ check_fix(
+ r#"
+struct S { a: (), b: () }
+
+fn f() {
+ S {
+ $0
+ };
+}
+"#,
+ r#"
+struct S { a: (), b: () }
+
+fn f() {
+ S {
+ a: todo!(),
+ b: todo!(),
+ };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_shorthand() {
+ cov_mark::check!(field_shorthand);
+ check_fix(
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let a = "hello";
+ let b = 1i32;
+ S {
+ $0
+ };
+}
+"#,
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let a = "hello";
+ let b = 1i32;
+ S {
+ a,
+ b,
+ };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_shorthand_ty_mismatch() {
+ check_fix(
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let a = "hello";
+ let b = 1usize;
+ S {
+ $0
+ };
+}
+"#,
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let a = "hello";
+ let b = 1usize;
+ S {
+ a,
+ b: 0,
+ };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_fields_shorthand_unifies() {
+ check_fix(
+ r#"
+struct S<T> { a: &'static str, b: T }
+
+fn f() {
+ let a = "hello";
+ let b = 1i32;
+ S {
+ $0
+ };
+}
+"#,
+ r#"
+struct S<T> { a: &'static str, b: T }
+
+fn f() {
+ let a = "hello";
+ let b = 1i32;
+ S {
+ a,
+ b,
+ };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_pat_fields() {
+ check_fix(
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let S {
+ $0
+ };
+}
+"#,
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let S {
+ a,
+ b,
+ };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_fill_struct_pat_fields_partial() {
+ check_fix(
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let S {
+ a,$0
+ };
+}
+"#,
+ r#"
+struct S { a: &'static str, b: i32 }
+
+fn f() {
+ let S {
+ a,
+ b,
+ };
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn import_extern_crate_clash_with_inner_item() {
+ // This is more of a resolver test, but doesn't really work with the hir_def testsuite.
+
+ check_diagnostics(
+ r#"
+//- /lib.rs crate:lib deps:jwt
+mod permissions;
+
+use permissions::jwt;
+
+fn f() {
+ fn inner() {}
+ jwt::Claims {}; // should resolve to the local one with 0 fields, and not get a diagnostic
+}
+
+//- /permissions.rs
+pub mod jwt {
+ pub struct Claims {}
+}
+
+//- /jwt/lib.rs crate:jwt
+pub struct Claims {
+ field: u8,
+}
+ "#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_match_arms.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_match_arms.rs
new file mode 100644
index 000000000..9e66fbfb7
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_match_arms.rs
@@ -0,0 +1,1012 @@
+use hir::InFile;
+
+use crate::{Diagnostic, DiagnosticsContext};
+
+// Diagnostic: missing-match-arm
+//
+// This diagnostic is triggered if `match` block is missing one or more match arms.
+pub(crate) fn missing_match_arms(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::MissingMatchArms,
+) -> Diagnostic {
+ Diagnostic::new(
+ "missing-match-arm",
+ format!("missing match arm: {}", d.uncovered_patterns),
+ ctx.sema.diagnostics_display_range(InFile::new(d.file, d.match_expr.clone().into())).range,
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ fn check_diagnostics_no_bails(ra_fixture: &str) {
+ cov_mark::check_count!(validate_match_bailed_out, 0);
+ crate::tests::check_diagnostics(ra_fixture)
+ }
+
+ #[test]
+ fn empty_tuple() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match () { }
+ //^^ error: missing match arm: type `()` is non-empty
+ match (()) { }
+ //^^^^ error: missing match arm: type `()` is non-empty
+
+ match () { _ => (), }
+ match () { () => (), }
+ match (()) { (()) => (), }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn tuple_of_two_empty_tuple() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match ((), ()) { }
+ //^^^^^^^^ error: missing match arm: type `((), ())` is non-empty
+
+ match ((), ()) { ((), ()) => (), }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn boolean() {
+ check_diagnostics_no_bails(
+ r#"
+fn test_main() {
+ match false { }
+ //^^^^^ error: missing match arm: type `bool` is non-empty
+ match false { true => (), }
+ //^^^^^ error: missing match arm: `false` not covered
+ match (false, true) {}
+ //^^^^^^^^^^^^^ error: missing match arm: type `(bool, bool)` is non-empty
+ match (false, true) { (true, true) => (), }
+ //^^^^^^^^^^^^^ error: missing match arm: `(false, _)` not covered
+ match (false, true) {
+ //^^^^^^^^^^^^^ error: missing match arm: `(true, true)` not covered
+ (false, true) => (),
+ (false, false) => (),
+ (true, false) => (),
+ }
+ match (false, true) { (true, _x) => (), }
+ //^^^^^^^^^^^^^ error: missing match arm: `(false, _)` not covered
+
+ match false { true => (), false => (), }
+ match (false, true) {
+ (false, _) => (),
+ (true, false) => (),
+ (_, true) => (),
+ }
+ match (false, true) {
+ (true, true) => (),
+ (true, false) => (),
+ (false, true) => (),
+ (false, false) => (),
+ }
+ match (false, true) {
+ (true, _x) => (),
+ (false, true) => (),
+ (false, false) => (),
+ }
+ match (false, true, false) {
+ (false, ..) => (),
+ (true, ..) => (),
+ }
+ match (false, true, false) {
+ (.., false) => (),
+ (.., true) => (),
+ }
+ match (false, true, false) { (..) => (), }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn tuple_of_tuple_and_bools() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match (false, ((), false)) {}
+ //^^^^^^^^^^^^^^^^^^^^ error: missing match arm: type `(bool, ((), bool))` is non-empty
+ match (false, ((), false)) { (true, ((), true)) => (), }
+ //^^^^^^^^^^^^^^^^^^^^ error: missing match arm: `(false, _)` not covered
+ match (false, ((), false)) { (true, _) => (), }
+ //^^^^^^^^^^^^^^^^^^^^ error: missing match arm: `(false, _)` not covered
+
+ match (false, ((), false)) {
+ (true, ((), true)) => (),
+ (true, ((), false)) => (),
+ (false, ((), true)) => (),
+ (false, ((), false)) => (),
+ }
+ match (false, ((), false)) {
+ (true, ((), true)) => (),
+ (true, ((), false)) => (),
+ (false, _) => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn enums() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either { A, B, }
+
+fn main() {
+ match Either::A { }
+ //^^^^^^^^^ error: missing match arm: `A` and `B` not covered
+ match Either::B { Either::A => (), }
+ //^^^^^^^^^ error: missing match arm: `B` not covered
+
+ match &Either::B {
+ //^^^^^^^^^^ error: missing match arm: `&B` not covered
+ Either::A => (),
+ }
+
+ match Either::B {
+ Either::A => (), Either::B => (),
+ }
+ match &Either::B {
+ Either::A => (), Either::B => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn enum_containing_bool() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either { A(bool), B }
+
+fn main() {
+ match Either::B { }
+ //^^^^^^^^^ error: missing match arm: `A(_)` and `B` not covered
+ match Either::B {
+ //^^^^^^^^^ error: missing match arm: `A(false)` not covered
+ Either::A(true) => (), Either::B => ()
+ }
+
+ match Either::B {
+ Either::A(true) => (),
+ Either::A(false) => (),
+ Either::B => (),
+ }
+ match Either::B {
+ Either::B => (),
+ _ => (),
+ }
+ match Either::B {
+ Either::A(_) => (),
+ Either::B => (),
+ }
+
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn enum_different_sizes() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either { A(bool), B(bool, bool) }
+
+fn main() {
+ match Either::A(false) {
+ //^^^^^^^^^^^^^^^^ error: missing match arm: `B(true, _)` not covered
+ Either::A(_) => (),
+ Either::B(false, _) => (),
+ }
+
+ match Either::A(false) {
+ Either::A(_) => (),
+ Either::B(true, _) => (),
+ Either::B(false, _) => (),
+ }
+ match Either::A(false) {
+ Either::A(true) | Either::A(false) => (),
+ Either::B(true, _) => (),
+ Either::B(false, _) => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn tuple_of_enum_no_diagnostic() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either { A(bool), B(bool, bool) }
+enum Either2 { C, D }
+
+fn main() {
+ match (Either::A(false), Either2::C) {
+ (Either::A(true), _) | (Either::A(false), _) => (),
+ (Either::B(true, _), Either2::C) => (),
+ (Either::B(false, _), Either2::C) => (),
+ (Either::B(_, _), Either2::D) => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn or_pattern_no_diagnostic() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either {A, B}
+
+fn main() {
+ match (Either::A, Either::B) {
+ (Either::A | Either::B, _) => (),
+ }
+}"#,
+ )
+ }
+
+ #[test]
+ fn mismatched_types() {
+ cov_mark::check_count!(validate_match_bailed_out, 4);
+ // Match statements with arms that don't match the
+ // expression pattern do not fire this diagnostic.
+ check_diagnostics(
+ r#"
+enum Either { A, B }
+enum Either2 { C, D }
+
+fn main() {
+ match Either::A {
+ Either2::C => (),
+ Either2::D => (),
+ }
+ match (true, false) {
+ (true, false, true) => (),
+ (true) => (),
+ // ^^^^ error: expected (bool, bool), found bool
+ }
+ match (true, false) { (true,) => {} }
+ match (0) { () => () }
+ match Unresolved::Bar { Unresolved::Baz => () }
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn mismatched_types_in_or_patterns() {
+ cov_mark::check_count!(validate_match_bailed_out, 2);
+ check_diagnostics(
+ r#"
+fn main() {
+ match false { true | () => {} }
+ match (false,) { (true | (),) => {} }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn malformed_match_arm_tuple_enum_missing_pattern() {
+ // We are testing to be sure we don't panic here when the match
+ // arm `Either::B` is missing its pattern.
+ check_diagnostics_no_bails(
+ r#"
+enum Either { A, B(u32) }
+
+fn main() {
+ match Either::A {
+ Either::A => (),
+ Either::B() => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn malformed_match_arm_extra_fields() {
+ cov_mark::check_count!(validate_match_bailed_out, 2);
+ check_diagnostics(
+ r#"
+enum A { B(isize, isize), C }
+fn main() {
+ match A::B(1, 2) {
+ A::B(_, _, _) => (),
+ }
+ match A::B(1, 2) {
+ A::C(_) => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn expr_diverges() {
+ cov_mark::check_count!(validate_match_bailed_out, 2);
+ check_diagnostics(
+ r#"
+enum Either { A, B }
+
+fn main() {
+ match loop {} {
+ Either::A => (),
+ Either::B => (),
+ }
+ match loop {} {
+ Either::A => (),
+ }
+ match loop { break Foo::A } {
+ //^^^^^^^^^^^^^^^^^^^^^ error: missing match arm: `B` not covered
+ Either::A => (),
+ }
+ match loop { break Foo::A } {
+ Either::A => (),
+ Either::B => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn expr_partially_diverges() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either<T> { A(T), B }
+
+fn foo() -> Either<!> { Either::B }
+fn main() -> u32 {
+ match foo() {
+ Either::A(val) => val,
+ Either::B => 0,
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn enum_record() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either { A { foo: bool }, B }
+
+fn main() {
+ let a = Either::A { foo: true };
+ match a { }
+ //^ error: missing match arm: `A { .. }` and `B` not covered
+ match a { Either::A { foo: true } => () }
+ //^ error: missing match arm: `B` not covered
+ match a {
+ Either::A { } => (),
+ //^^^^^^^^^ 💡 error: missing structure fields:
+ // | - foo
+ Either::B => (),
+ }
+ match a {
+ //^ error: missing match arm: `B` not covered
+ Either::A { } => (),
+ } //^^^^^^^^^ 💡 error: missing structure fields:
+ // | - foo
+
+ match a {
+ Either::A { foo: true } => (),
+ Either::A { foo: false } => (),
+ Either::B => (),
+ }
+ match a {
+ Either::A { foo: _ } => (),
+ Either::B => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn enum_record_fields_out_of_order() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either {
+ A { foo: bool, bar: () },
+ B,
+}
+
+fn main() {
+ let a = Either::A { foo: true, bar: () };
+ match a {
+ //^ error: missing match arm: `B` not covered
+ Either::A { bar: (), foo: false } => (),
+ Either::A { foo: true, bar: () } => (),
+ }
+
+ match a {
+ Either::A { bar: (), foo: false } => (),
+ Either::A { foo: true, bar: () } => (),
+ Either::B => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn enum_record_ellipsis() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either {
+ A { foo: bool, bar: bool },
+ B,
+}
+
+fn main() {
+ let a = Either::B;
+ match a {
+ //^ error: missing match arm: `A { foo: false, .. }` not covered
+ Either::A { foo: true, .. } => (),
+ Either::B => (),
+ }
+ match a {
+ //^ error: missing match arm: `B` not covered
+ Either::A { .. } => (),
+ }
+
+ match a {
+ Either::A { foo: true, .. } => (),
+ Either::A { foo: false, .. } => (),
+ Either::B => (),
+ }
+
+ match a {
+ Either::A { .. } => (),
+ Either::B => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn enum_tuple_partial_ellipsis() {
+ check_diagnostics_no_bails(
+ r#"
+enum Either {
+ A(bool, bool, bool, bool),
+ B,
+}
+
+fn main() {
+ match Either::B {
+ //^^^^^^^^^ error: missing match arm: `A(false, _, _, true)` not covered
+ Either::A(true, .., true) => (),
+ Either::A(true, .., false) => (),
+ Either::A(false, .., false) => (),
+ Either::B => (),
+ }
+ match Either::B {
+ //^^^^^^^^^ error: missing match arm: `A(false, _, _, false)` not covered
+ Either::A(true, .., true) => (),
+ Either::A(true, .., false) => (),
+ Either::A(.., true) => (),
+ Either::B => (),
+ }
+
+ match Either::B {
+ Either::A(true, .., true) => (),
+ Either::A(true, .., false) => (),
+ Either::A(false, .., true) => (),
+ Either::A(false, .., false) => (),
+ Either::B => (),
+ }
+ match Either::B {
+ Either::A(true, .., true) => (),
+ Either::A(true, .., false) => (),
+ Either::A(.., true) => (),
+ Either::A(.., false) => (),
+ Either::B => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn never() {
+ check_diagnostics_no_bails(
+ r#"
+enum Never {}
+
+fn enum_(never: Never) {
+ match never {}
+}
+fn enum_ref(never: &Never) {
+ match never {}
+ //^^^^^ error: missing match arm: type `&Never` is non-empty
+}
+fn bang(never: !) {
+ match never {}
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn unknown_type() {
+ cov_mark::check_count!(validate_match_bailed_out, 1);
+
+ check_diagnostics(
+ r#"
+enum Option<T> { Some(T), None }
+
+fn main() {
+ // `Never` is deliberately not defined so that it's an uninferred type.
+ match Option::<Never>::None {
+ None => (),
+ Some(never) => match never {},
+ }
+ match Option::<Never>::None {
+ //^^^^^^^^^^^^^^^^^^^^^ error: missing match arm: `None` not covered
+ Option::Some(_never) => {},
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn tuple_of_bools_with_ellipsis_at_end_missing_arm() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match (false, true, false) {
+ //^^^^^^^^^^^^^^^^^^^^ error: missing match arm: `(true, _, _)` not covered
+ (false, ..) => (),
+ }
+}"#,
+ );
+ }
+
+ #[test]
+ fn tuple_of_bools_with_ellipsis_at_beginning_missing_arm() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match (false, true, false) {
+ //^^^^^^^^^^^^^^^^^^^^ error: missing match arm: `(_, _, true)` not covered
+ (.., false) => (),
+ }
+}"#,
+ );
+ }
+
+ #[test]
+ fn tuple_of_bools_with_ellipsis_in_middle_missing_arm() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match (false, true, false) {
+ //^^^^^^^^^^^^^^^^^^^^ error: missing match arm: `(false, _, _)` not covered
+ (true, .., false) => (),
+ }
+}"#,
+ );
+ }
+
+ #[test]
+ fn record_struct() {
+ check_diagnostics_no_bails(
+ r#"struct Foo { a: bool }
+fn main(f: Foo) {
+ match f {}
+ //^ error: missing match arm: type `Foo` is non-empty
+ match f { Foo { a: true } => () }
+ //^ error: missing match arm: `Foo { a: false }` not covered
+ match &f { Foo { a: true } => () }
+ //^^ error: missing match arm: `&Foo { a: false }` not covered
+ match f { Foo { a: _ } => () }
+ match f {
+ Foo { a: true } => (),
+ Foo { a: false } => (),
+ }
+ match &f {
+ Foo { a: true } => (),
+ Foo { a: false } => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn tuple_struct() {
+ check_diagnostics_no_bails(
+ r#"struct Foo(bool);
+fn main(f: Foo) {
+ match f {}
+ //^ error: missing match arm: type `Foo` is non-empty
+ match f { Foo(true) => () }
+ //^ error: missing match arm: `Foo(false)` not covered
+ match f {
+ Foo(true) => (),
+ Foo(false) => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn unit_struct() {
+ check_diagnostics_no_bails(
+ r#"struct Foo;
+fn main(f: Foo) {
+ match f {}
+ //^ error: missing match arm: type `Foo` is non-empty
+ match f { Foo => () }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn record_struct_ellipsis() {
+ check_diagnostics_no_bails(
+ r#"struct Foo { foo: bool, bar: bool }
+fn main(f: Foo) {
+ match f { Foo { foo: true, .. } => () }
+ //^ error: missing match arm: `Foo { foo: false, .. }` not covered
+ match f {
+ //^ error: missing match arm: `Foo { foo: false, bar: true }` not covered
+ Foo { foo: true, .. } => (),
+ Foo { bar: false, .. } => ()
+ }
+ match f { Foo { .. } => () }
+ match f {
+ Foo { foo: true, .. } => (),
+ Foo { foo: false, .. } => ()
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn internal_or() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ enum Either { A(bool), B }
+ match Either::B {
+ //^^^^^^^^^ error: missing match arm: `B` not covered
+ Either::A(true | false) => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn no_panic_at_unimplemented_subpattern_type() {
+ cov_mark::check_count!(validate_match_bailed_out, 1);
+
+ check_diagnostics(
+ r#"
+struct S { a: char}
+fn main(v: S) {
+ match v { S{ a } => {} }
+ match v { S{ a: _x } => {} }
+ match v { S{ a: 'a' } => {} }
+ match v { S{..} => {} }
+ match v { _ => {} }
+ match v { }
+ //^ error: missing match arm: type `S` is non-empty
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn binding() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match true {
+ _x @ true => {}
+ false => {}
+ }
+ match true { _x @ true => {} }
+ //^^^^ error: missing match arm: `false` not covered
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn binding_ref_has_correct_type() {
+ cov_mark::check_count!(validate_match_bailed_out, 1);
+
+ // Asserts `PatKind::Binding(ref _x): bool`, not &bool.
+ // If that's not true match checking will panic with "incompatible constructors"
+ // FIXME: make facilities to test this directly like `tests::check_infer(..)`
+ check_diagnostics(
+ r#"
+enum Foo { A }
+fn main() {
+ // FIXME: this should not bail out but current behavior is such as the old algorithm.
+ // ExprValidator::validate_match(..) checks types of top level patterns incorrecly.
+ match Foo::A {
+ ref _x => {}
+ Foo::A => {}
+ }
+ match (true,) {
+ (ref _x,) => {}
+ (true,) => {}
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn enum_non_exhaustive() {
+ check_diagnostics_no_bails(
+ r#"
+//- /lib.rs crate:lib
+#[non_exhaustive]
+pub enum E { A, B }
+fn _local() {
+ match E::A { _ => {} }
+ match E::A {
+ E::A => {}
+ E::B => {}
+ }
+ match E::A {
+ E::A | E::B => {}
+ }
+}
+
+//- /main.rs crate:main deps:lib
+use lib::E;
+fn main() {
+ match E::A { _ => {} }
+ match E::A {
+ //^^^^ error: missing match arm: `_` not covered
+ E::A => {}
+ E::B => {}
+ }
+ match E::A {
+ //^^^^ error: missing match arm: `_` not covered
+ E::A | E::B => {}
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn match_guard() {
+ check_diagnostics_no_bails(
+ r#"
+fn main() {
+ match true {
+ true if false => {}
+ true => {}
+ false => {}
+ }
+ match true {
+ //^^^^ error: missing match arm: `true` not covered
+ true if false => {}
+ false => {}
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn pattern_type_is_of_substitution() {
+ check_diagnostics_no_bails(
+ r#"
+struct Foo<T>(T);
+struct Bar;
+fn main() {
+ match Foo(Bar) {
+ _ | Foo(Bar) => {}
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn record_struct_no_such_field() {
+ cov_mark::check_count!(validate_match_bailed_out, 1);
+
+ check_diagnostics(
+ r#"
+struct Foo { }
+fn main(f: Foo) {
+ match f { Foo { bar } => () }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn match_ergonomics_issue_9095() {
+ check_diagnostics_no_bails(
+ r#"
+enum Foo<T> { A(T) }
+fn main() {
+ match &Foo::A(true) {
+ _ => {}
+ Foo::A(_) => {}
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn normalize_field_ty() {
+ check_diagnostics_no_bails(
+ r"
+trait Trait { type Projection; }
+enum E {Foo, Bar}
+struct A;
+impl Trait for A { type Projection = E; }
+struct Next<T: Trait>(T::Projection);
+static __: () = {
+ let n: Next<A> = Next(E::Foo);
+ match n { Next(E::Foo) => {} }
+ // ^ error: missing match arm: `Next(Bar)` not covered
+ match n { Next(E::Foo | E::Bar) => {} }
+ match n { Next(E::Foo | _ ) => {} }
+ match n { Next(_ | E::Bar) => {} }
+ match n { _ | Next(E::Bar) => {} }
+ match &n { Next(E::Foo | E::Bar) => {} }
+ match &n { _ | Next(E::Bar) => {} }
+};",
+ );
+ }
+
+ #[test]
+ fn binding_mode_by_ref() {
+ check_diagnostics_no_bails(
+ r"
+enum E{ A, B }
+fn foo() {
+ match &E::A {
+ E::A => {}
+ x => {}
+ }
+}",
+ );
+ }
+
+ #[test]
+ fn macro_or_pat() {
+ check_diagnostics_no_bails(
+ r#"
+macro_rules! m {
+ () => {
+ Enum::Type1 | Enum::Type2
+ };
+}
+
+enum Enum {
+ Type1,
+ Type2,
+ Type3,
+}
+
+fn f(ty: Enum) {
+ match ty {
+ //^^ error: missing match arm: `Type3` not covered
+ m!() => (),
+ }
+
+ match ty {
+ m!() | Enum::Type3 => ()
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn unexpected_ty_fndef() {
+ cov_mark::check!(validate_match_bailed_out);
+ check_diagnostics(
+ r"
+enum Exp {
+ Tuple(()),
+}
+fn f() {
+ match __unknown {
+ Exp::Tuple => {}
+ }
+}",
+ );
+ }
+
+ mod false_negatives {
+ //! The implementation of match checking here is a work in progress. As we roll this out, we
+ //! prefer false negatives to false positives (ideally there would be no false positives). This
+ //! test module should document known false negatives. Eventually we will have a complete
+ //! implementation of match checking and this module will be empty.
+ //!
+ //! The reasons for documenting known false negatives:
+ //!
+ //! 1. It acts as a backlog of work that can be done to improve the behavior of the system.
+ //! 2. It ensures the code doesn't panic when handling these cases.
+ use super::*;
+
+ #[test]
+ fn integers() {
+ cov_mark::check_count!(validate_match_bailed_out, 1);
+
+ // We don't currently check integer exhaustiveness.
+ check_diagnostics(
+ r#"
+fn main() {
+ match 5 {
+ 10 => (),
+ 11..20 => (),
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn reference_patterns_at_top_level() {
+ cov_mark::check_count!(validate_match_bailed_out, 1);
+
+ check_diagnostics(
+ r#"
+fn main() {
+ match &false {
+ &true => {}
+ }
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn reference_patterns_in_fields() {
+ cov_mark::check_count!(validate_match_bailed_out, 2);
+
+ check_diagnostics(
+ r#"
+fn main() {
+ match (&false,) {
+ (true,) => {}
+ }
+ match (&false,) {
+ (&true,) => {}
+ }
+}
+ "#,
+ );
+ }
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs
new file mode 100644
index 000000000..7acd9228a
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/missing_unsafe.rs
@@ -0,0 +1,101 @@
+use crate::{Diagnostic, DiagnosticsContext};
+
+// Diagnostic: missing-unsafe
+//
+// This diagnostic is triggered if an operation marked as `unsafe` is used outside of an `unsafe` function or block.
+pub(crate) fn missing_unsafe(ctx: &DiagnosticsContext<'_>, d: &hir::MissingUnsafe) -> Diagnostic {
+ Diagnostic::new(
+ "missing-unsafe",
+ "this operation is unsafe and requires an unsafe function or block",
+ ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn missing_unsafe_diagnostic_with_raw_ptr() {
+ check_diagnostics(
+ r#"
+fn main() {
+ let x = &5 as *const usize;
+ unsafe { let y = *x; }
+ let z = *x;
+} //^^ error: this operation is unsafe and requires an unsafe function or block
+"#,
+ )
+ }
+
+ #[test]
+ fn missing_unsafe_diagnostic_with_unsafe_call() {
+ check_diagnostics(
+ r#"
+struct HasUnsafe;
+
+impl HasUnsafe {
+ unsafe fn unsafe_fn(&self) {
+ let x = &5 as *const usize;
+ let y = *x;
+ }
+}
+
+unsafe fn unsafe_fn() {
+ let x = &5 as *const usize;
+ let y = *x;
+}
+
+fn main() {
+ unsafe_fn();
+ //^^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
+ HasUnsafe.unsafe_fn();
+ //^^^^^^^^^^^^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
+ unsafe {
+ unsafe_fn();
+ HasUnsafe.unsafe_fn();
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn missing_unsafe_diagnostic_with_static_mut() {
+ check_diagnostics(
+ r#"
+struct Ty {
+ a: u8,
+}
+
+static mut STATIC_MUT: Ty = Ty { a: 0 };
+
+fn main() {
+ let x = STATIC_MUT.a;
+ //^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
+ unsafe {
+ let x = STATIC_MUT.a;
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn no_missing_unsafe_diagnostic_with_safe_intrinsic() {
+ check_diagnostics(
+ r#"
+extern "rust-intrinsic" {
+ pub fn bitreverse(x: u32) -> u32; // Safe intrinsic
+ pub fn floorf32(x: f32) -> f32; // Unsafe intrinsic
+}
+
+fn main() {
+ let _ = bitreverse(12);
+ let _ = floorf32(12.0);
+ //^^^^^^^^^^^^^^ error: this operation is unsafe and requires an unsafe function or block
+}
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/no_such_field.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/no_such_field.rs
new file mode 100644
index 000000000..e032c578f
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/no_such_field.rs
@@ -0,0 +1,283 @@
+use hir::{db::AstDatabase, HasSource, HirDisplay, Semantics};
+use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase};
+use syntax::{
+ ast::{self, edit::IndentLevel, make},
+ AstNode,
+};
+use text_edit::TextEdit;
+
+use crate::{fix, Assist, Diagnostic, DiagnosticsContext};
+
+// Diagnostic: no-such-field
+//
+// This diagnostic is triggered if created structure does not have field provided in record.
+pub(crate) fn no_such_field(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Diagnostic {
+ Diagnostic::new(
+ "no-such-field",
+ "no such field",
+ ctx.sema.diagnostics_display_range(d.field.clone().map(|it| it.into())).range,
+ )
+ .with_fixes(fixes(ctx, d))
+}
+
+fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Option<Vec<Assist>> {
+ let root = ctx.sema.db.parse_or_expand(d.field.file_id)?;
+ missing_record_expr_field_fixes(
+ &ctx.sema,
+ d.field.file_id.original_file(ctx.sema.db),
+ &d.field.value.to_node(&root),
+ )
+}
+
+fn missing_record_expr_field_fixes(
+ sema: &Semantics<'_, RootDatabase>,
+ usage_file_id: FileId,
+ record_expr_field: &ast::RecordExprField,
+) -> Option<Vec<Assist>> {
+ let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
+ let def_id = sema.resolve_variant(record_lit)?;
+ let module;
+ let def_file_id;
+ let record_fields = match def_id {
+ hir::VariantDef::Struct(s) => {
+ module = s.module(sema.db);
+ let source = s.source(sema.db)?;
+ def_file_id = source.file_id;
+ let fields = source.value.field_list()?;
+ record_field_list(fields)?
+ }
+ hir::VariantDef::Union(u) => {
+ module = u.module(sema.db);
+ let source = u.source(sema.db)?;
+ def_file_id = source.file_id;
+ source.value.record_field_list()?
+ }
+ hir::VariantDef::Variant(e) => {
+ module = e.module(sema.db);
+ let source = e.source(sema.db)?;
+ def_file_id = source.file_id;
+ let fields = source.value.field_list()?;
+ record_field_list(fields)?
+ }
+ };
+ let def_file_id = def_file_id.original_file(sema.db);
+
+ let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?.adjusted();
+ if new_field_type.is_unknown() {
+ return None;
+ }
+ let new_field = make::record_field(
+ None,
+ make::name(&record_expr_field.field_name()?.text()),
+ make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
+ );
+
+ let last_field = record_fields.fields().last()?;
+ let last_field_syntax = last_field.syntax();
+ let indent = IndentLevel::from_node(last_field_syntax);
+
+ let mut new_field = new_field.to_string();
+ if usage_file_id != def_file_id {
+ new_field = format!("pub(crate) {}", new_field);
+ }
+ new_field = format!("\n{}{}", indent, new_field);
+
+ let needs_comma = !last_field_syntax.to_string().ends_with(',');
+ if needs_comma {
+ new_field = format!(",{}", new_field);
+ }
+
+ let source_change = SourceChange::from_text_edit(
+ def_file_id,
+ TextEdit::insert(last_field_syntax.text_range().end(), new_field),
+ );
+
+ return Some(vec![fix(
+ "create_field",
+ "Create field",
+ source_change,
+ record_expr_field.syntax().text_range(),
+ )]);
+
+ fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
+ match field_def_list {
+ ast::FieldList::RecordFieldList(it) => Some(it),
+ ast::FieldList::TupleFieldList(_) => None,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::{check_diagnostics, check_fix};
+
+ #[test]
+ fn no_such_field_diagnostics() {
+ check_diagnostics(
+ r#"
+struct S { foo: i32, bar: () }
+impl S {
+ fn new() -> S {
+ S {
+ //^ 💡 error: missing structure fields:
+ //| - bar
+ foo: 92,
+ baz: 62,
+ //^^^^^^^ 💡 error: no such field
+ }
+ }
+}
+"#,
+ );
+ }
+ #[test]
+ fn no_such_field_with_feature_flag_diagnostics() {
+ check_diagnostics(
+ r#"
+//- /lib.rs crate:foo cfg:feature=foo
+struct MyStruct {
+ my_val: usize,
+ #[cfg(feature = "foo")]
+ bar: bool,
+}
+
+impl MyStruct {
+ #[cfg(feature = "foo")]
+ pub(crate) fn new(my_val: usize, bar: bool) -> Self {
+ Self { my_val, bar }
+ }
+ #[cfg(not(feature = "foo"))]
+ pub(crate) fn new(my_val: usize, _bar: bool) -> Self {
+ Self { my_val }
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn no_such_field_enum_with_feature_flag_diagnostics() {
+ check_diagnostics(
+ r#"
+//- /lib.rs crate:foo cfg:feature=foo
+enum Foo {
+ #[cfg(not(feature = "foo"))]
+ Buz,
+ #[cfg(feature = "foo")]
+ Bar,
+ Baz
+}
+
+fn test_fn(f: Foo) {
+ match f {
+ Foo::Bar => {},
+ Foo::Baz => {},
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn no_such_field_with_feature_flag_diagnostics_on_struct_lit() {
+ check_diagnostics(
+ r#"
+//- /lib.rs crate:foo cfg:feature=foo
+struct S {
+ #[cfg(feature = "foo")]
+ foo: u32,
+ #[cfg(not(feature = "foo"))]
+ bar: u32,
+}
+
+impl S {
+ #[cfg(feature = "foo")]
+ fn new(foo: u32) -> Self {
+ Self { foo }
+ }
+ #[cfg(not(feature = "foo"))]
+ fn new(bar: u32) -> Self {
+ Self { bar }
+ }
+ fn new2(bar: u32) -> Self {
+ #[cfg(feature = "foo")]
+ { Self { foo: bar } }
+ #[cfg(not(feature = "foo"))]
+ { Self { bar } }
+ }
+ fn new2(val: u32) -> Self {
+ Self {
+ #[cfg(feature = "foo")]
+ foo: val,
+ #[cfg(not(feature = "foo"))]
+ bar: val,
+ }
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn no_such_field_with_type_macro() {
+ check_diagnostics(
+ r#"
+macro_rules! Type { () => { u32 }; }
+struct Foo { bar: Type![] }
+
+impl Foo {
+ fn new() -> Self {
+ Foo { bar: 0 }
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_add_field_from_usage() {
+ check_fix(
+ r"
+fn main() {
+ Foo { bar: 3, baz$0: false};
+}
+struct Foo {
+ bar: i32
+}
+",
+ r"
+fn main() {
+ Foo { bar: 3, baz: false};
+}
+struct Foo {
+ bar: i32,
+ baz: bool
+}
+",
+ )
+ }
+
+ #[test]
+ fn test_add_field_in_other_file_from_usage() {
+ check_fix(
+ r#"
+//- /main.rs
+mod foo;
+
+fn main() {
+ foo::Foo { bar: 3, $0baz: false};
+}
+//- /foo.rs
+struct Foo {
+ bar: i32
+}
+"#,
+ r#"
+struct Foo {
+ bar: i32,
+ pub(crate) baz: bool
+}
+"#,
+ )
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs
new file mode 100644
index 000000000..9826e1c70
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs
@@ -0,0 +1,131 @@
+use hir::{db::AstDatabase, InFile};
+use ide_db::source_change::SourceChange;
+use syntax::{
+ ast::{self, HasArgList},
+ AstNode, TextRange,
+};
+use text_edit::TextEdit;
+
+use crate::{fix, Assist, Diagnostic, DiagnosticsContext, Severity};
+
+// Diagnostic: replace-filter-map-next-with-find-map
+//
+// This diagnostic is triggered when `.filter_map(..).next()` is used, rather than the more concise `.find_map(..)`.
+pub(crate) fn replace_filter_map_next_with_find_map(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::ReplaceFilterMapNextWithFindMap,
+) -> Diagnostic {
+ Diagnostic::new(
+ "replace-filter-map-next-with-find-map",
+ "replace filter_map(..).next() with find_map(..)",
+ ctx.sema.diagnostics_display_range(InFile::new(d.file, d.next_expr.clone().into())).range,
+ )
+ .severity(Severity::WeakWarning)
+ .with_fixes(fixes(ctx, d))
+}
+
+fn fixes(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::ReplaceFilterMapNextWithFindMap,
+) -> Option<Vec<Assist>> {
+ let root = ctx.sema.db.parse_or_expand(d.file)?;
+ let next_expr = d.next_expr.to_node(&root);
+ let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
+
+ let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
+ let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
+ let filter_map_args = filter_map_call.arg_list()?;
+
+ let range_to_replace =
+ TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
+ let replacement = format!("find_map{}", filter_map_args.syntax().text());
+ let trigger_range = next_expr.syntax().text_range();
+
+ let edit = TextEdit::replace(range_to_replace, replacement);
+
+ let source_change = SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit);
+
+ Some(vec![fix(
+ "replace_with_find_map",
+ "Replace filter_map(..).next() with find_map()",
+ source_change,
+ trigger_range,
+ )])
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::{check_diagnostics, check_fix};
+
+ #[test]
+ fn replace_filter_map_next_with_find_map2() {
+ check_diagnostics(
+ r#"
+//- minicore: iterators
+fn foo() {
+ let m = core::iter::repeat(()).filter_map(|()| Some(92)).next();
+} //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 💡 weak: replace filter_map(..).next() with find_map(..)
+"#,
+ );
+ }
+
+ #[test]
+ fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() {
+ check_diagnostics(
+ r#"
+//- minicore: iterators
+fn foo() {
+ let m = core::iter::repeat(())
+ .filter_map(|()| Some(92))
+ .count();
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() {
+ check_diagnostics(
+ r#"
+//- minicore: iterators
+fn foo() {
+ let m = core::iter::repeat(())
+ .filter_map(|()| Some(92))
+ .map(|x| x + 2)
+ .next();
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() {
+ check_diagnostics(
+ r#"
+//- minicore: iterators
+fn foo() {
+ let m = core::iter::repeat(())
+ .filter_map(|()| Some(92));
+ let n = m.next();
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn replace_with_find_map() {
+ check_fix(
+ r#"
+//- minicore: iterators
+fn foo() {
+ let m = core::iter::repeat(()).$0filter_map(|()| Some(92)).next();
+}
+"#,
+ r#"
+fn foo() {
+ let m = core::iter::repeat(()).find_map(|()| Some(92));
+}
+"#,
+ )
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/type_mismatch.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/type_mismatch.rs
new file mode 100644
index 000000000..6bf90e645
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/type_mismatch.rs
@@ -0,0 +1,573 @@
+use hir::{db::AstDatabase, HirDisplay, Type};
+use ide_db::{famous_defs::FamousDefs, source_change::SourceChange};
+use syntax::{
+ ast::{self, BlockExpr, ExprStmt},
+ AstNode,
+};
+use text_edit::TextEdit;
+
+use crate::{adjusted_display_range, fix, Assist, Diagnostic, DiagnosticsContext};
+
+// Diagnostic: type-mismatch
+//
+// This diagnostic is triggered when the type of an expression does not match
+// the expected type.
+pub(crate) fn type_mismatch(ctx: &DiagnosticsContext<'_>, d: &hir::TypeMismatch) -> Diagnostic {
+ let display_range = adjusted_display_range::<ast::BlockExpr>(
+ ctx,
+ d.expr.clone().map(|it| it.into()),
+ &|block| {
+ let r_curly_range = block.stmt_list()?.r_curly_token()?.text_range();
+ cov_mark::hit!(type_mismatch_on_block);
+ Some(r_curly_range)
+ },
+ );
+
+ let mut diag = Diagnostic::new(
+ "type-mismatch",
+ format!(
+ "expected {}, found {}",
+ d.expected.display(ctx.sema.db),
+ d.actual.display(ctx.sema.db)
+ ),
+ display_range,
+ )
+ .with_fixes(fixes(ctx, d));
+ if diag.fixes.is_none() {
+ diag.experimental = true;
+ }
+ diag
+}
+
+fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::TypeMismatch) -> Option<Vec<Assist>> {
+ let mut fixes = Vec::new();
+
+ add_reference(ctx, d, &mut fixes);
+ add_missing_ok_or_some(ctx, d, &mut fixes);
+ remove_semicolon(ctx, d, &mut fixes);
+ str_ref_to_owned(ctx, d, &mut fixes);
+
+ if fixes.is_empty() {
+ None
+ } else {
+ Some(fixes)
+ }
+}
+
+fn add_reference(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::TypeMismatch,
+ acc: &mut Vec<Assist>,
+) -> Option<()> {
+ let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
+ let expr_node = d.expr.value.to_node(&root);
+
+ let range = ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range;
+
+ let (_, mutability) = d.expected.as_reference()?;
+ let actual_with_ref = Type::reference(&d.actual, mutability);
+ if !actual_with_ref.could_coerce_to(ctx.sema.db, &d.expected) {
+ return None;
+ }
+
+ let ampersands = format!("&{}", mutability.as_keyword_for_ref());
+
+ let edit = TextEdit::insert(expr_node.syntax().text_range().start(), ampersands);
+ let source_change =
+ SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
+ acc.push(fix("add_reference_here", "Add reference here", source_change, range));
+ Some(())
+}
+
+fn add_missing_ok_or_some(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::TypeMismatch,
+ acc: &mut Vec<Assist>,
+) -> Option<()> {
+ let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
+ let expr = d.expr.value.to_node(&root);
+ let expr_range = expr.syntax().text_range();
+ let scope = ctx.sema.scope(expr.syntax())?;
+
+ let expected_adt = d.expected.as_adt()?;
+ let expected_enum = expected_adt.as_enum()?;
+
+ let famous_defs = FamousDefs(&ctx.sema, scope.krate());
+ let core_result = famous_defs.core_result_Result();
+ let core_option = famous_defs.core_option_Option();
+
+ if Some(expected_enum) != core_result && Some(expected_enum) != core_option {
+ return None;
+ }
+
+ let variant_name = if Some(expected_enum) == core_result { "Ok" } else { "Some" };
+
+ let wrapped_actual_ty = expected_adt.ty_with_args(ctx.sema.db, &[d.actual.clone()]);
+
+ if !d.expected.could_unify_with(ctx.sema.db, &wrapped_actual_ty) {
+ return None;
+ }
+
+ let mut builder = TextEdit::builder();
+ builder.insert(expr.syntax().text_range().start(), format!("{}(", variant_name));
+ builder.insert(expr.syntax().text_range().end(), ")".to_string());
+ let source_change =
+ SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), builder.finish());
+ let name = format!("Wrap in {}", variant_name);
+ acc.push(fix("wrap_in_constructor", &name, source_change, expr_range));
+ Some(())
+}
+
+fn remove_semicolon(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::TypeMismatch,
+ acc: &mut Vec<Assist>,
+) -> Option<()> {
+ let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
+ let expr = d.expr.value.to_node(&root);
+ if !d.actual.is_unit() {
+ return None;
+ }
+ let block = BlockExpr::cast(expr.syntax().clone())?;
+ let expr_before_semi =
+ block.statements().last().and_then(|s| ExprStmt::cast(s.syntax().clone()))?;
+ let type_before_semi = ctx.sema.type_of_expr(&expr_before_semi.expr()?)?.original();
+ if !type_before_semi.could_coerce_to(ctx.sema.db, &d.expected) {
+ return None;
+ }
+ let semicolon_range = expr_before_semi.semicolon_token()?.text_range();
+
+ let edit = TextEdit::delete(semicolon_range);
+ let source_change =
+ SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
+
+ acc.push(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon_range));
+ Some(())
+}
+
+fn str_ref_to_owned(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::TypeMismatch,
+ acc: &mut Vec<Assist>,
+) -> Option<()> {
+ let expected = d.expected.display(ctx.sema.db);
+ let actual = d.actual.display(ctx.sema.db);
+
+ if expected.to_string() != "String" || actual.to_string() != "&str" {
+ return None;
+ }
+
+ let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
+ let expr = d.expr.value.to_node(&root);
+ let expr_range = expr.syntax().text_range();
+
+ let to_owned = format!(".to_owned()");
+
+ let edit = TextEdit::insert(expr.syntax().text_range().end(), to_owned);
+ let source_change =
+ SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
+ acc.push(fix("str_ref_to_owned", "Add .to_owned() here", source_change, expr_range));
+
+ Some(())
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::{check_diagnostics, check_fix, check_no_fix};
+
+ #[test]
+ fn missing_reference() {
+ check_diagnostics(
+ r#"
+fn main() {
+ test(123);
+ //^^^ 💡 error: expected &i32, found i32
+}
+fn test(arg: &i32) {}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_add_reference_to_int() {
+ check_fix(
+ r#"
+fn main() {
+ test(123$0);
+}
+fn test(arg: &i32) {}
+ "#,
+ r#"
+fn main() {
+ test(&123);
+}
+fn test(arg: &i32) {}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_add_mutable_reference_to_int() {
+ check_fix(
+ r#"
+fn main() {
+ test($0123);
+}
+fn test(arg: &mut i32) {}
+ "#,
+ r#"
+fn main() {
+ test(&mut 123);
+}
+fn test(arg: &mut i32) {}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_add_reference_to_array() {
+ check_fix(
+ r#"
+//- minicore: coerce_unsized
+fn main() {
+ test($0[1, 2, 3]);
+}
+fn test(arg: &[i32]) {}
+ "#,
+ r#"
+fn main() {
+ test(&[1, 2, 3]);
+}
+fn test(arg: &[i32]) {}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_add_reference_with_autoderef() {
+ check_fix(
+ r#"
+//- minicore: coerce_unsized, deref
+struct Foo;
+struct Bar;
+impl core::ops::Deref for Foo {
+ type Target = Bar;
+}
+
+fn main() {
+ test($0Foo);
+}
+fn test(arg: &Bar) {}
+ "#,
+ r#"
+struct Foo;
+struct Bar;
+impl core::ops::Deref for Foo {
+ type Target = Bar;
+}
+
+fn main() {
+ test(&Foo);
+}
+fn test(arg: &Bar) {}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_add_reference_to_method_call() {
+ check_fix(
+ r#"
+fn main() {
+ Test.call_by_ref($0123);
+}
+struct Test;
+impl Test {
+ fn call_by_ref(&self, arg: &i32) {}
+}
+ "#,
+ r#"
+fn main() {
+ Test.call_by_ref(&123);
+}
+struct Test;
+impl Test {
+ fn call_by_ref(&self, arg: &i32) {}
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_add_reference_to_let_stmt() {
+ check_fix(
+ r#"
+fn main() {
+ let test: &i32 = $0123;
+}
+ "#,
+ r#"
+fn main() {
+ let test: &i32 = &123;
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_add_mutable_reference_to_let_stmt() {
+ check_fix(
+ r#"
+fn main() {
+ let test: &mut i32 = $0123;
+}
+ "#,
+ r#"
+fn main() {
+ let test: &mut i32 = &mut 123;
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_wrap_return_type_option() {
+ check_fix(
+ r#"
+//- minicore: option, result
+fn div(x: i32, y: i32) -> Option<i32> {
+ if y == 0 {
+ return None;
+ }
+ x / y$0
+}
+"#,
+ r#"
+fn div(x: i32, y: i32) -> Option<i32> {
+ if y == 0 {
+ return None;
+ }
+ Some(x / y)
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn const_generic_type_mismatch() {
+ check_diagnostics(
+ r#"
+ pub struct Rate<const N: u32>;
+ fn f<const N: u64>() -> Rate<N> { // FIXME: add some error
+ loop {}
+ }
+ fn run(t: Rate<5>) {
+ }
+ fn main() {
+ run(f()) // FIXME: remove this error
+ //^^^ error: expected Rate<5>, found Rate<_>
+ }
+"#,
+ );
+ }
+
+ #[test]
+ fn const_generic_unknown() {
+ check_diagnostics(
+ r#"
+ pub struct Rate<T, const NOM: u32, const DENOM: u32>(T);
+ fn run(t: Rate<u32, 1, 1>) {
+ }
+ fn main() {
+ run(Rate::<_, _, _>(5));
+ }
+"#,
+ );
+ }
+
+ #[test]
+ fn test_wrap_return_type_option_tails() {
+ check_fix(
+ r#"
+//- minicore: option, result
+fn div(x: i32, y: i32) -> Option<i32> {
+ if y == 0 {
+ Some(0)
+ } else if true {
+ 100$0
+ } else {
+ None
+ }
+}
+"#,
+ r#"
+fn div(x: i32, y: i32) -> Option<i32> {
+ if y == 0 {
+ Some(0)
+ } else if true {
+ Some(100)
+ } else {
+ None
+ }
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_wrap_return_type() {
+ check_fix(
+ r#"
+//- minicore: option, result
+fn div(x: i32, y: i32) -> Result<i32, ()> {
+ if y == 0 {
+ return Err(());
+ }
+ x / y$0
+}
+"#,
+ r#"
+fn div(x: i32, y: i32) -> Result<i32, ()> {
+ if y == 0 {
+ return Err(());
+ }
+ Ok(x / y)
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_wrap_return_type_handles_generic_functions() {
+ check_fix(
+ r#"
+//- minicore: option, result
+fn div<T>(x: T) -> Result<T, i32> {
+ if x == 0 {
+ return Err(7);
+ }
+ $0x
+}
+"#,
+ r#"
+fn div<T>(x: T) -> Result<T, i32> {
+ if x == 0 {
+ return Err(7);
+ }
+ Ok(x)
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_wrap_return_type_handles_type_aliases() {
+ check_fix(
+ r#"
+//- minicore: option, result
+type MyResult<T> = Result<T, ()>;
+
+fn div(x: i32, y: i32) -> MyResult<i32> {
+ if y == 0 {
+ return Err(());
+ }
+ x $0/ y
+}
+"#,
+ r#"
+type MyResult<T> = Result<T, ()>;
+
+fn div(x: i32, y: i32) -> MyResult<i32> {
+ if y == 0 {
+ return Err(());
+ }
+ Ok(x / y)
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_in_const_and_static() {
+ check_fix(
+ r#"
+//- minicore: option, result
+static A: Option<()> = {($0)};
+ "#,
+ r#"
+static A: Option<()> = {Some(())};
+ "#,
+ );
+ check_fix(
+ r#"
+//- minicore: option, result
+const _: Option<()> = {($0)};
+ "#,
+ r#"
+const _: Option<()> = {Some(())};
+ "#,
+ );
+ }
+
+ #[test]
+ fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
+ check_no_fix(
+ r#"
+//- minicore: option, result
+fn foo() -> Result<(), i32> { 0$0 }
+"#,
+ );
+ }
+
+ #[test]
+ fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() {
+ check_no_fix(
+ r#"
+//- minicore: option, result
+enum SomeOtherEnum { Ok(i32), Err(String) }
+
+fn foo() -> SomeOtherEnum { 0$0 }
+"#,
+ );
+ }
+
+ #[test]
+ fn remove_semicolon() {
+ check_fix(r#"fn f() -> i32 { 92$0; }"#, r#"fn f() -> i32 { 92 }"#);
+ }
+
+ #[test]
+ fn str_ref_to_owned() {
+ check_fix(
+ r#"
+struct String;
+
+fn test() -> String {
+ "a"$0
+}
+ "#,
+ r#"
+struct String;
+
+fn test() -> String {
+ "a".to_owned()
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn type_mismatch_on_block() {
+ cov_mark::check!(type_mismatch_on_block);
+ check_diagnostics(
+ r#"
+fn f() -> i32 {
+ let x = 1;
+ let y = 2;
+ let _ = x + y;
+ }
+//^ error: expected i32, found ()
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unimplemented_builtin_macro.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unimplemented_builtin_macro.rs
new file mode 100644
index 000000000..e879de75c
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unimplemented_builtin_macro.rs
@@ -0,0 +1,16 @@
+use crate::{Diagnostic, DiagnosticsContext, Severity};
+
+// Diagnostic: unimplemented-builtin-macro
+//
+// This diagnostic is shown for builtin macros which are not yet implemented by rust-analyzer
+pub(crate) fn unimplemented_builtin_macro(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::UnimplementedBuiltinMacro,
+) -> Diagnostic {
+ Diagnostic::new(
+ "unimplemented-builtin-macro",
+ "unimplemented built-in macro".to_string(),
+ ctx.sema.diagnostics_display_range(d.node.clone()).range,
+ )
+ .severity(Severity::WeakWarning)
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unlinked_file.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unlinked_file.rs
new file mode 100644
index 000000000..c626932f1
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unlinked_file.rs
@@ -0,0 +1,336 @@
+//! Diagnostic emitted for files that aren't part of any crate.
+
+use hir::db::DefDatabase;
+use ide_db::{
+ base_db::{FileId, FileLoader, SourceDatabase, SourceDatabaseExt},
+ source_change::SourceChange,
+ RootDatabase,
+};
+use syntax::{
+ ast::{self, HasModuleItem, HasName},
+ AstNode, TextRange, TextSize,
+};
+use text_edit::TextEdit;
+
+use crate::{fix, Assist, Diagnostic, DiagnosticsContext, Severity};
+
+// Diagnostic: unlinked-file
+//
+// This diagnostic is shown for files that are not included in any crate, or files that are part of
+// crates rust-analyzer failed to discover. The file will not have IDE features available.
+pub(crate) fn unlinked_file(
+ ctx: &DiagnosticsContext<'_>,
+ acc: &mut Vec<Diagnostic>,
+ file_id: FileId,
+) {
+ // Limit diagnostic to the first few characters in the file. This matches how VS Code
+ // renders it with the full span, but on other editors, and is less invasive.
+ let range = ctx.sema.db.parse(file_id).syntax_node().text_range();
+ // FIXME: This is wrong if one of the first three characters is not ascii: `//Ы`.
+ let range = range.intersect(TextRange::up_to(TextSize::of("..."))).unwrap_or(range);
+
+ acc.push(
+ Diagnostic::new("unlinked-file", "file not included in module tree", range)
+ .severity(Severity::WeakWarning)
+ .with_fixes(fixes(ctx, file_id)),
+ );
+}
+
+fn fixes(ctx: &DiagnosticsContext<'_>, file_id: FileId) -> Option<Vec<Assist>> {
+ // If there's an existing module that could add `mod` or `pub mod` items to include the unlinked file,
+ // suggest that as a fix.
+
+ let source_root = ctx.sema.db.source_root(ctx.sema.db.file_source_root(file_id));
+ let our_path = source_root.path_for_file(&file_id)?;
+ let (mut module_name, _) = our_path.name_and_extension()?;
+
+ // Candidates to look for:
+ // - `mod.rs`, `main.rs` and `lib.rs` in the same folder
+ // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id`
+ let parent = our_path.parent()?;
+ let paths = {
+ let parent = if module_name == "mod" {
+ // for mod.rs we need to actually look up one higher
+ // and take the parent as our to be module name
+ let (name, _) = parent.name_and_extension()?;
+ module_name = name;
+ parent.parent()?
+ } else {
+ parent
+ };
+ let mut paths =
+ vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?];
+
+ // `submod/bla.rs` -> `submod.rs`
+ let parent_mod = (|| {
+ let (name, _) = parent.name_and_extension()?;
+ parent.parent()?.join(&format!("{}.rs", name))
+ })();
+ paths.extend(parent_mod);
+ paths
+ };
+
+ for &parent_id in paths.iter().filter_map(|path| source_root.file_for_path(path)) {
+ for &krate in ctx.sema.db.relevant_crates(parent_id).iter() {
+ let crate_def_map = ctx.sema.db.crate_def_map(krate);
+ for (_, module) in crate_def_map.modules() {
+ if module.origin.is_inline() {
+ // We don't handle inline `mod parent {}`s, they use different paths.
+ continue;
+ }
+
+ if module.origin.file_id() == Some(parent_id) {
+ return make_fixes(ctx.sema.db, parent_id, module_name, file_id);
+ }
+ }
+ }
+ }
+
+ None
+}
+
+fn make_fixes(
+ db: &RootDatabase,
+ parent_file_id: FileId,
+ new_mod_name: &str,
+ added_file_id: FileId,
+) -> Option<Vec<Assist>> {
+ fn is_outline_mod(item: &ast::Item) -> bool {
+ matches!(item, ast::Item::Module(m) if m.item_list().is_none())
+ }
+
+ let mod_decl = format!("mod {};", new_mod_name);
+ let pub_mod_decl = format!("pub mod {};", new_mod_name);
+
+ let ast: ast::SourceFile = db.parse(parent_file_id).tree();
+
+ let mut mod_decl_builder = TextEdit::builder();
+ let mut pub_mod_decl_builder = TextEdit::builder();
+
+ // If there's an existing `mod m;` statement matching the new one, don't emit a fix (it's
+ // probably `#[cfg]`d out).
+ for item in ast.items() {
+ if let ast::Item::Module(m) = item {
+ if let Some(name) = m.name() {
+ if m.item_list().is_none() && name.to_string() == new_mod_name {
+ cov_mark::hit!(unlinked_file_skip_fix_when_mod_already_exists);
+ return None;
+ }
+ }
+ }
+ }
+
+ // If there are existing `mod m;` items, append after them (after the first group of them, rather).
+ match ast.items().skip_while(|item| !is_outline_mod(item)).take_while(is_outline_mod).last() {
+ Some(last) => {
+ cov_mark::hit!(unlinked_file_append_to_existing_mods);
+ let offset = last.syntax().text_range().end();
+ mod_decl_builder.insert(offset, format!("\n{}", mod_decl));
+ pub_mod_decl_builder.insert(offset, format!("\n{}", pub_mod_decl));
+ }
+ None => {
+ // Prepend before the first item in the file.
+ match ast.items().next() {
+ Some(item) => {
+ cov_mark::hit!(unlinked_file_prepend_before_first_item);
+ let offset = item.syntax().text_range().start();
+ mod_decl_builder.insert(offset, format!("{}\n\n", mod_decl));
+ pub_mod_decl_builder.insert(offset, format!("{}\n\n", pub_mod_decl));
+ }
+ None => {
+ // No items in the file, so just append at the end.
+ cov_mark::hit!(unlinked_file_empty_file);
+ let offset = ast.syntax().text_range().end();
+ mod_decl_builder.insert(offset, format!("{}\n", mod_decl));
+ pub_mod_decl_builder.insert(offset, format!("{}\n", pub_mod_decl));
+ }
+ }
+ }
+ }
+
+ let trigger_range = db.parse(added_file_id).tree().syntax().text_range();
+ Some(vec![
+ fix(
+ "add_mod_declaration",
+ &format!("Insert `{}`", mod_decl),
+ SourceChange::from_text_edit(parent_file_id, mod_decl_builder.finish()),
+ trigger_range,
+ ),
+ fix(
+ "add_pub_mod_declaration",
+ &format!("Insert `{}`", pub_mod_decl),
+ SourceChange::from_text_edit(parent_file_id, pub_mod_decl_builder.finish()),
+ trigger_range,
+ ),
+ ])
+}
+
+#[cfg(test)]
+mod tests {
+
+ use crate::tests::{check_diagnostics, check_fix, check_fixes, check_no_fix};
+
+ #[test]
+ fn unlinked_file_prepend_first_item() {
+ cov_mark::check!(unlinked_file_prepend_before_first_item);
+ // Only tests the first one for `pub mod` since the rest are the same
+ check_fixes(
+ r#"
+//- /main.rs
+fn f() {}
+//- /foo.rs
+$0
+"#,
+ vec![
+ r#"
+mod foo;
+
+fn f() {}
+"#,
+ r#"
+pub mod foo;
+
+fn f() {}
+"#,
+ ],
+ );
+ }
+
+ #[test]
+ fn unlinked_file_append_mod() {
+ cov_mark::check!(unlinked_file_append_to_existing_mods);
+ check_fix(
+ r#"
+//- /main.rs
+//! Comment on top
+
+mod preexisting;
+
+mod preexisting2;
+
+struct S;
+
+mod preexisting_bottom;)
+//- /foo.rs
+$0
+"#,
+ r#"
+//! Comment on top
+
+mod preexisting;
+
+mod preexisting2;
+mod foo;
+
+struct S;
+
+mod preexisting_bottom;)
+"#,
+ );
+ }
+
+ #[test]
+ fn unlinked_file_insert_in_empty_file() {
+ cov_mark::check!(unlinked_file_empty_file);
+ check_fix(
+ r#"
+//- /main.rs
+//- /foo.rs
+$0
+"#,
+ r#"
+mod foo;
+"#,
+ );
+ }
+
+ #[test]
+ fn unlinked_file_insert_in_empty_file_mod_file() {
+ check_fix(
+ r#"
+//- /main.rs
+//- /foo/mod.rs
+$0
+"#,
+ r#"
+mod foo;
+"#,
+ );
+ check_fix(
+ r#"
+//- /main.rs
+mod bar;
+//- /bar.rs
+// bar module
+//- /bar/foo/mod.rs
+$0
+"#,
+ r#"
+// bar module
+mod foo;
+"#,
+ );
+ }
+
+ #[test]
+ fn unlinked_file_old_style_modrs() {
+ check_fix(
+ r#"
+//- /main.rs
+mod submod;
+//- /submod/mod.rs
+// in mod.rs
+//- /submod/foo.rs
+$0
+"#,
+ r#"
+// in mod.rs
+mod foo;
+"#,
+ );
+ }
+
+ #[test]
+ fn unlinked_file_new_style_mod() {
+ check_fix(
+ r#"
+//- /main.rs
+mod submod;
+//- /submod.rs
+//- /submod/foo.rs
+$0
+"#,
+ r#"
+mod foo;
+"#,
+ );
+ }
+
+ #[test]
+ fn unlinked_file_with_cfg_off() {
+ cov_mark::check!(unlinked_file_skip_fix_when_mod_already_exists);
+ check_no_fix(
+ r#"
+//- /main.rs
+#[cfg(never)]
+mod foo;
+
+//- /foo.rs
+$0
+"#,
+ );
+ }
+
+ #[test]
+ fn unlinked_file_with_cfg_on() {
+ check_diagnostics(
+ r#"
+//- /main.rs
+#[cfg(not(never))]
+mod foo;
+
+//- /foo.rs
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_extern_crate.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_extern_crate.rs
new file mode 100644
index 000000000..74e4a69c6
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_extern_crate.rs
@@ -0,0 +1,49 @@
+use crate::{Diagnostic, DiagnosticsContext};
+
+// Diagnostic: unresolved-extern-crate
+//
+// This diagnostic is triggered if rust-analyzer is unable to discover referred extern crate.
+pub(crate) fn unresolved_extern_crate(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::UnresolvedExternCrate,
+) -> Diagnostic {
+ Diagnostic::new(
+ "unresolved-extern-crate",
+ "unresolved extern crate",
+ ctx.sema.diagnostics_display_range(d.decl.clone().map(|it| it.into())).range,
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn unresolved_extern_crate() {
+ check_diagnostics(
+ r#"
+//- /main.rs crate:main deps:core
+extern crate core;
+ extern crate doesnotexist;
+//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unresolved extern crate
+//- /lib.rs crate:core
+"#,
+ );
+ }
+
+ #[test]
+ fn extern_crate_self_as() {
+ cov_mark::check!(extern_crate_self_as);
+ check_diagnostics(
+ r#"
+//- /lib.rs
+ extern crate doesnotexist;
+//^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unresolved extern crate
+// Should not error.
+extern crate self as foo;
+struct Foo;
+use foo::Foo as Bar;
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_import.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_import.rs
new file mode 100644
index 000000000..e52a88459
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_import.rs
@@ -0,0 +1,90 @@
+use crate::{Diagnostic, DiagnosticsContext};
+
+// Diagnostic: unresolved-import
+//
+// This diagnostic is triggered if rust-analyzer is unable to resolve a path in
+// a `use` declaration.
+pub(crate) fn unresolved_import(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::UnresolvedImport,
+) -> Diagnostic {
+ Diagnostic::new(
+ "unresolved-import",
+ "unresolved import",
+ ctx.sema.diagnostics_display_range(d.decl.clone().map(|it| it.into())).range,
+ )
+ // This currently results in false positives in the following cases:
+ // - `cfg_if!`-generated code in libstd (we don't load the sysroot correctly)
+ // - `core::arch` (we don't handle `#[path = "../<path>"]` correctly)
+ // - proc macros and/or proc macro generated code
+ .experimental()
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn unresolved_import() {
+ check_diagnostics(
+ r#"
+use does_exist;
+use does_not_exist;
+ //^^^^^^^^^^^^^^ error: unresolved import
+
+mod does_exist {}
+"#,
+ );
+ }
+
+ #[test]
+ fn unresolved_import_in_use_tree() {
+ // Only the relevant part of a nested `use` item should be highlighted.
+ check_diagnostics(
+ r#"
+use does_exist::{Exists, DoesntExist};
+ //^^^^^^^^^^^ error: unresolved import
+
+use {does_not_exist::*, does_exist};
+ //^^^^^^^^^^^^^^^^^ error: unresolved import
+
+use does_not_exist::{
+ a,
+ //^ error: unresolved import
+ b,
+ //^ error: unresolved import
+ c,
+ //^ error: unresolved import
+};
+
+mod does_exist {
+ pub struct Exists;
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn dedup_unresolved_import_from_unresolved_crate() {
+ check_diagnostics(
+ r#"
+//- /main.rs crate:main
+mod a {
+ extern crate doesnotexist;
+ //^^^^^^^^^^^^^^^^^^^^^^^^^^ error: unresolved extern crate
+
+ // Should not error, since we already errored for the missing crate.
+ use doesnotexist::{self, bla, *};
+
+ use crate::doesnotexist;
+ //^^^^^^^^^^^^^^^^^^^ error: unresolved import
+}
+
+mod m {
+ use super::doesnotexist;
+ //^^^^^^^^^^^^^^^^^^^ error: unresolved import
+}
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_macro_call.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_macro_call.rs
new file mode 100644
index 000000000..4b4312475
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_macro_call.rs
@@ -0,0 +1,76 @@
+use crate::{Diagnostic, DiagnosticsContext};
+
+// Diagnostic: unresolved-macro-call
+//
+// This diagnostic is triggered if rust-analyzer is unable to resolve the path
+// to a macro in a macro invocation.
+pub(crate) fn unresolved_macro_call(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::UnresolvedMacroCall,
+) -> Diagnostic {
+ // Use more accurate position if available.
+ let display_range = d
+ .precise_location
+ .unwrap_or_else(|| ctx.sema.diagnostics_display_range(d.macro_call.clone()).range);
+
+ let bang = if d.is_bang { "!" } else { "" };
+ Diagnostic::new(
+ "unresolved-macro-call",
+ format!("unresolved macro `{}{}`", d.path, bang),
+ display_range,
+ )
+ .experimental()
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::check_diagnostics;
+
+ #[test]
+ fn unresolved_macro_diag() {
+ check_diagnostics(
+ r#"
+fn f() {
+ m!();
+} //^ error: unresolved macro `m!`
+
+"#,
+ );
+ }
+
+ #[test]
+ fn test_unresolved_macro_range() {
+ check_diagnostics(
+ r#"
+foo::bar!(92);
+ //^^^ error: unresolved macro `foo::bar!`
+"#,
+ );
+ }
+
+ #[test]
+ fn unresolved_legacy_scope_macro() {
+ check_diagnostics(
+ r#"
+macro_rules! m { () => {} }
+
+m!(); m2!();
+ //^^ error: unresolved macro `m2!`
+"#,
+ );
+ }
+
+ #[test]
+ fn unresolved_module_scope_macro() {
+ check_diagnostics(
+ r#"
+mod mac {
+#[macro_export]
+macro_rules! m { () => {} } }
+
+self::m!(); self::m2!();
+ //^^ error: unresolved macro `self::m2!`
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_module.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_module.rs
new file mode 100644
index 000000000..b8f2a9e94
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_module.rs
@@ -0,0 +1,156 @@
+use hir::db::AstDatabase;
+use ide_db::{assists::Assist, base_db::AnchoredPathBuf, source_change::FileSystemEdit};
+use itertools::Itertools;
+use syntax::AstNode;
+
+use crate::{fix, Diagnostic, DiagnosticsContext};
+
+// Diagnostic: unresolved-module
+//
+// This diagnostic is triggered if rust-analyzer is unable to discover referred module.
+pub(crate) fn unresolved_module(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::UnresolvedModule,
+) -> Diagnostic {
+ Diagnostic::new(
+ "unresolved-module",
+ match &*d.candidates {
+ [] => "unresolved module".to_string(),
+ [candidate] => format!("unresolved module, can't find module file: {}", candidate),
+ [candidates @ .., last] => {
+ format!(
+ "unresolved module, can't find module file: {}, or {}",
+ candidates.iter().format(", "),
+ last
+ )
+ }
+ },
+ ctx.sema.diagnostics_display_range(d.decl.clone().map(|it| it.into())).range,
+ )
+ .with_fixes(fixes(ctx, d))
+}
+
+fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedModule) -> Option<Vec<Assist>> {
+ let root = ctx.sema.db.parse_or_expand(d.decl.file_id)?;
+ let unresolved_module = d.decl.value.to_node(&root);
+ Some(
+ d.candidates
+ .iter()
+ .map(|candidate| {
+ fix(
+ "create_module",
+ &format!("Create module at `{candidate}`"),
+ FileSystemEdit::CreateFile {
+ dst: AnchoredPathBuf {
+ anchor: d.decl.file_id.original_file(ctx.sema.db),
+ path: candidate.clone(),
+ },
+ initial_contents: "".to_string(),
+ }
+ .into(),
+ unresolved_module.syntax().text_range(),
+ )
+ })
+ .collect(),
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use expect_test::expect;
+
+ use crate::tests::{check_diagnostics, check_expect};
+
+ #[test]
+ fn unresolved_module() {
+ check_diagnostics(
+ r#"
+//- /lib.rs
+mod foo;
+ mod bar;
+//^^^^^^^^ 💡 error: unresolved module, can't find module file: bar.rs, or bar/mod.rs
+mod baz {}
+//- /foo.rs
+"#,
+ );
+ }
+
+ #[test]
+ fn test_unresolved_module_diagnostic() {
+ check_expect(
+ r#"mod foo;"#,
+ expect![[r#"
+ [
+ Diagnostic {
+ code: DiagnosticCode(
+ "unresolved-module",
+ ),
+ message: "unresolved module, can't find module file: foo.rs, or foo/mod.rs",
+ range: 0..8,
+ severity: Error,
+ unused: false,
+ experimental: false,
+ fixes: Some(
+ [
+ Assist {
+ id: AssistId(
+ "create_module",
+ QuickFix,
+ ),
+ label: "Create module at `foo.rs`",
+ group: None,
+ target: 0..8,
+ source_change: Some(
+ SourceChange {
+ source_file_edits: {},
+ file_system_edits: [
+ CreateFile {
+ dst: AnchoredPathBuf {
+ anchor: FileId(
+ 0,
+ ),
+ path: "foo.rs",
+ },
+ initial_contents: "",
+ },
+ ],
+ is_snippet: false,
+ },
+ ),
+ trigger_signature_help: false,
+ },
+ Assist {
+ id: AssistId(
+ "create_module",
+ QuickFix,
+ ),
+ label: "Create module at `foo/mod.rs`",
+ group: None,
+ target: 0..8,
+ source_change: Some(
+ SourceChange {
+ source_file_edits: {},
+ file_system_edits: [
+ CreateFile {
+ dst: AnchoredPathBuf {
+ anchor: FileId(
+ 0,
+ ),
+ path: "foo/mod.rs",
+ },
+ initial_contents: "",
+ },
+ ],
+ is_snippet: false,
+ },
+ ),
+ trigger_signature_help: false,
+ },
+ ],
+ ),
+ },
+ ]
+ "#]],
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_proc_macro.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_proc_macro.rs
new file mode 100644
index 000000000..760f51f90
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/unresolved_proc_macro.rs
@@ -0,0 +1,62 @@
+use hir::db::DefDatabase;
+use syntax::NodeOrToken;
+
+use crate::{Diagnostic, DiagnosticsContext, Severity};
+
+// Diagnostic: unresolved-proc-macro
+//
+// This diagnostic is shown when a procedural macro can not be found. This usually means that
+// procedural macro support is simply disabled (and hence is only a weak hint instead of an error),
+// but can also indicate project setup problems.
+//
+// If you are seeing a lot of "proc macro not expanded" warnings, you can add this option to the
+// `rust-analyzer.diagnostics.disabled` list to prevent them from showing. Alternatively you can
+// enable support for procedural macros (see `rust-analyzer.procMacro.attributes.enable`).
+pub(crate) fn unresolved_proc_macro(
+ ctx: &DiagnosticsContext<'_>,
+ d: &hir::UnresolvedProcMacro,
+ proc_macros_enabled: bool,
+ proc_attr_macros_enabled: bool,
+) -> Diagnostic {
+ // Use more accurate position if available.
+ let display_range = (|| {
+ let precise_location = d.precise_location?;
+ let root = ctx.sema.parse_or_expand(d.node.file_id)?;
+ match root.covering_element(precise_location) {
+ NodeOrToken::Node(it) => Some(ctx.sema.original_range(&it)),
+ NodeOrToken::Token(it) => d.node.with_value(it).original_file_range_opt(ctx.sema.db),
+ }
+ })()
+ .unwrap_or_else(|| ctx.sema.diagnostics_display_range(d.node.clone()))
+ .range;
+
+ let config_enabled = match d.kind {
+ hir::MacroKind::Attr => proc_macros_enabled && proc_attr_macros_enabled,
+ _ => proc_macros_enabled,
+ };
+
+ let message = match &d.macro_name {
+ Some(name) => format!("proc macro `{}` not expanded", name),
+ None => "proc macro not expanded".to_string(),
+ };
+ let severity = if config_enabled { Severity::Error } else { Severity::WeakWarning };
+ let def_map = ctx.sema.db.crate_def_map(d.krate);
+ let message = format!(
+ "{message}: {}",
+ if config_enabled {
+ match def_map.proc_macro_loading_error() {
+ Some(e) => e,
+ None => "proc macro not found in the built dylib",
+ }
+ } else {
+ match d.kind {
+ hir::MacroKind::Attr if proc_macros_enabled => {
+ "attribute macro expansion is disabled"
+ }
+ _ => "proc-macro expansion is disabled",
+ }
+ },
+ );
+
+ Diagnostic::new("unresolved-proc-macro", message, display_range).severity(severity)
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/useless_braces.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/useless_braces.rs
new file mode 100644
index 000000000..8b9330e04
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/handlers/useless_braces.rs
@@ -0,0 +1,148 @@
+use ide_db::{base_db::FileId, source_change::SourceChange};
+use itertools::Itertools;
+use syntax::{ast, AstNode, SyntaxNode, TextRange};
+use text_edit::TextEdit;
+
+use crate::{fix, Diagnostic, Severity};
+
+// Diagnostic: unnecessary-braces
+//
+// Diagnostic for unnecessary braces in `use` items.
+pub(crate) fn useless_braces(
+ acc: &mut Vec<Diagnostic>,
+ file_id: FileId,
+ node: &SyntaxNode,
+) -> Option<()> {
+ let use_tree_list = ast::UseTreeList::cast(node.clone())?;
+ if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
+ // If there is a comment inside the bracketed `use`,
+ // assume it is a commented out module path and don't show diagnostic.
+ if use_tree_list.has_inner_comment() {
+ return Some(());
+ }
+
+ let use_range = use_tree_list.syntax().text_range();
+ let edit = remove_braces(&single_use_tree).unwrap_or_else(|| {
+ let to_replace = single_use_tree.syntax().text().to_string();
+ let mut edit_builder = TextEdit::builder();
+ edit_builder.delete(use_range);
+ edit_builder.insert(use_range.start(), to_replace);
+ edit_builder.finish()
+ });
+
+ acc.push(
+ Diagnostic::new(
+ "unnecessary-braces",
+ "Unnecessary braces in use statement".to_string(),
+ use_range,
+ )
+ .severity(Severity::WeakWarning)
+ .with_fixes(Some(vec![fix(
+ "remove_braces",
+ "Remove unnecessary braces",
+ SourceChange::from_text_edit(file_id, edit),
+ use_range,
+ )])),
+ );
+ }
+
+ Some(())
+}
+
+fn remove_braces(single_use_tree: &ast::UseTree) -> Option<TextEdit> {
+ let use_tree_list_node = single_use_tree.syntax().parent()?;
+ if single_use_tree.path()?.segment()?.self_token().is_some() {
+ let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
+ let end = use_tree_list_node.text_range().end();
+ return Some(TextEdit::delete(TextRange::new(start, end)));
+ }
+ None
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::tests::{check_diagnostics, check_fix};
+
+ #[test]
+ fn test_check_unnecessary_braces_in_use_statement() {
+ check_diagnostics(
+ r#"
+use a;
+use a::{c, d::e};
+
+mod a {
+ mod c {}
+ mod d {
+ mod e {}
+ }
+}
+"#,
+ );
+ check_diagnostics(
+ r#"
+use a;
+use a::{
+ c,
+ // d::e
+};
+
+mod a {
+ mod c {}
+ mod d {
+ mod e {}
+ }
+}
+"#,
+ );
+ check_fix(
+ r#"
+mod b {}
+use {$0b};
+"#,
+ r#"
+mod b {}
+use b;
+"#,
+ );
+ check_fix(
+ r#"
+mod b {}
+use {b$0};
+"#,
+ r#"
+mod b {}
+use b;
+"#,
+ );
+ check_fix(
+ r#"
+mod a { mod c {} }
+use a::{c$0};
+"#,
+ r#"
+mod a { mod c {} }
+use a::c;
+"#,
+ );
+ check_fix(
+ r#"
+mod a {}
+use a::{self$0};
+"#,
+ r#"
+mod a {}
+use a;
+"#,
+ );
+ check_fix(
+ r#"
+mod a { mod c {} mod d { mod e {} } }
+use a::{c, d::{e$0}};
+"#,
+ r#"
+mod a { mod c {} mod d { mod e {} } }
+use a::{c, d::e};
+"#,
+ );
+ }
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/lib.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/lib.rs
new file mode 100644
index 000000000..41abaa836
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/lib.rs
@@ -0,0 +1,260 @@
+//! Diagnostics rendering and fixits.
+//!
+//! Most of the diagnostics originate from the dark depth of the compiler, and
+//! are originally expressed in term of IR. When we emit the diagnostic, we are
+//! usually not in the position to decide how to best "render" it in terms of
+//! user-authored source code. We are especially not in the position to offer
+//! fixits, as the compiler completely lacks the infrastructure to edit the
+//! source code.
+//!
+//! Instead, we "bubble up" raw, structured diagnostics until the `hir` crate,
+//! where we "cook" them so that each diagnostic is formulated in terms of `hir`
+//! types. Well, at least that's the aspiration, the "cooking" is somewhat
+//! ad-hoc at the moment. Anyways, we get a bunch of ide-friendly diagnostic
+//! structs from hir, and we want to render them to unified serializable
+//! representation (span, level, message) here. If we can, we also provide
+//! fixits. By the way, that's why we want to keep diagnostics structured
+//! internally -- so that we have all the info to make fixes.
+//!
+//! We have one "handler" module per diagnostic code. Such a module contains
+//! rendering, optional fixes and tests. It's OK if some low-level compiler
+//! functionality ends up being tested via a diagnostic.
+//!
+//! There are also a couple of ad-hoc diagnostics implemented directly here, we
+//! don't yet have a great pattern for how to do them properly.
+
+#![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
+
+mod handlers {
+ pub(crate) mod break_outside_of_loop;
+ pub(crate) mod inactive_code;
+ pub(crate) mod incorrect_case;
+ pub(crate) mod invalid_derive_target;
+ pub(crate) mod macro_error;
+ pub(crate) mod malformed_derive;
+ pub(crate) mod mismatched_arg_count;
+ pub(crate) mod missing_fields;
+ pub(crate) mod missing_match_arms;
+ pub(crate) mod missing_unsafe;
+ pub(crate) mod no_such_field;
+ pub(crate) mod replace_filter_map_next_with_find_map;
+ pub(crate) mod type_mismatch;
+ pub(crate) mod unimplemented_builtin_macro;
+ pub(crate) mod unresolved_extern_crate;
+ pub(crate) mod unresolved_import;
+ pub(crate) mod unresolved_macro_call;
+ pub(crate) mod unresolved_module;
+ pub(crate) mod unresolved_proc_macro;
+
+ // The handlers below are unusual, the implement the diagnostics as well.
+ pub(crate) mod field_shorthand;
+ pub(crate) mod useless_braces;
+ pub(crate) mod unlinked_file;
+}
+
+#[cfg(test)]
+mod tests;
+
+use hir::{diagnostics::AnyDiagnostic, InFile, Semantics};
+use ide_db::{
+ assists::{Assist, AssistId, AssistKind, AssistResolveStrategy},
+ base_db::{FileId, FileRange, SourceDatabase},
+ label::Label,
+ source_change::SourceChange,
+ FxHashSet, RootDatabase,
+};
+use syntax::{algo::find_node_at_range, ast::AstNode, SyntaxNodePtr, TextRange};
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct DiagnosticCode(pub &'static str);
+
+impl DiagnosticCode {
+ pub fn as_str(&self) -> &str {
+ self.0
+ }
+}
+
+#[derive(Debug)]
+pub struct Diagnostic {
+ pub code: DiagnosticCode,
+ pub message: String,
+ pub range: TextRange,
+ pub severity: Severity,
+ pub unused: bool,
+ pub experimental: bool,
+ pub fixes: Option<Vec<Assist>>,
+}
+
+impl Diagnostic {
+ fn new(code: &'static str, message: impl Into<String>, range: TextRange) -> Diagnostic {
+ let message = message.into();
+ Diagnostic {
+ code: DiagnosticCode(code),
+ message,
+ range,
+ severity: Severity::Error,
+ unused: false,
+ experimental: false,
+ fixes: None,
+ }
+ }
+
+ fn experimental(mut self) -> Diagnostic {
+ self.experimental = true;
+ self
+ }
+
+ fn severity(mut self, severity: Severity) -> Diagnostic {
+ self.severity = severity;
+ self
+ }
+
+ fn with_fixes(mut self, fixes: Option<Vec<Assist>>) -> Diagnostic {
+ self.fixes = fixes;
+ self
+ }
+
+ fn with_unused(mut self, unused: bool) -> Diagnostic {
+ self.unused = unused;
+ self
+ }
+}
+
+#[derive(Debug, Copy, Clone)]
+pub enum Severity {
+ Error,
+ // We don't actually emit this one yet, but we should at some point.
+ // Warning,
+ WeakWarning,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum ExprFillDefaultMode {
+ Todo,
+ Default,
+}
+impl Default for ExprFillDefaultMode {
+ fn default() -> Self {
+ Self::Todo
+ }
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct DiagnosticsConfig {
+ pub proc_macros_enabled: bool,
+ pub proc_attr_macros_enabled: bool,
+ pub disable_experimental: bool,
+ pub disabled: FxHashSet<String>,
+ pub expr_fill_default: ExprFillDefaultMode,
+}
+
+struct DiagnosticsContext<'a> {
+ config: &'a DiagnosticsConfig,
+ sema: Semantics<'a, RootDatabase>,
+ resolve: &'a AssistResolveStrategy,
+}
+
+pub fn diagnostics(
+ db: &RootDatabase,
+ config: &DiagnosticsConfig,
+ resolve: &AssistResolveStrategy,
+ file_id: FileId,
+) -> Vec<Diagnostic> {
+ let _p = profile::span("diagnostics");
+ let sema = Semantics::new(db);
+ let parse = db.parse(file_id);
+ let mut res = Vec::new();
+
+ // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
+ res.extend(
+ parse.errors().iter().take(128).map(|err| {
+ Diagnostic::new("syntax-error", format!("Syntax Error: {}", err), err.range())
+ }),
+ );
+
+ for node in parse.tree().syntax().descendants() {
+ handlers::useless_braces::useless_braces(&mut res, file_id, &node);
+ handlers::field_shorthand::field_shorthand(&mut res, file_id, &node);
+ }
+
+ let module = sema.to_module_def(file_id);
+
+ let ctx = DiagnosticsContext { config, sema, resolve };
+ if module.is_none() {
+ handlers::unlinked_file::unlinked_file(&ctx, &mut res, file_id);
+ }
+
+ let mut diags = Vec::new();
+ if let Some(m) = module {
+ m.diagnostics(db, &mut diags)
+ }
+
+ for diag in diags {
+ #[rustfmt::skip]
+ let d = match diag {
+ AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d),
+ AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d),
+ AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d),
+ AnyDiagnostic::MalformedDerive(d) => handlers::malformed_derive::malformed_derive(&ctx, &d),
+ AnyDiagnostic::MismatchedArgCount(d) => handlers::mismatched_arg_count::mismatched_arg_count(&ctx, &d),
+ AnyDiagnostic::MissingFields(d) => handlers::missing_fields::missing_fields(&ctx, &d),
+ AnyDiagnostic::MissingMatchArms(d) => handlers::missing_match_arms::missing_match_arms(&ctx, &d),
+ AnyDiagnostic::MissingUnsafe(d) => handlers::missing_unsafe::missing_unsafe(&ctx, &d),
+ AnyDiagnostic::NoSuchField(d) => handlers::no_such_field::no_such_field(&ctx, &d),
+ AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
+ AnyDiagnostic::TypeMismatch(d) => handlers::type_mismatch::type_mismatch(&ctx, &d),
+ AnyDiagnostic::UnimplementedBuiltinMacro(d) => handlers::unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),
+ AnyDiagnostic::UnresolvedExternCrate(d) => handlers::unresolved_extern_crate::unresolved_extern_crate(&ctx, &d),
+ AnyDiagnostic::UnresolvedImport(d) => handlers::unresolved_import::unresolved_import(&ctx, &d),
+ AnyDiagnostic::UnresolvedMacroCall(d) => handlers::unresolved_macro_call::unresolved_macro_call(&ctx, &d),
+ AnyDiagnostic::UnresolvedModule(d) => handlers::unresolved_module::unresolved_module(&ctx, &d),
+ AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d, config.proc_macros_enabled, config.proc_attr_macros_enabled),
+ AnyDiagnostic::InvalidDeriveTarget(d) => handlers::invalid_derive_target::invalid_derive_target(&ctx, &d),
+
+ AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
+ Some(it) => it,
+ None => continue,
+ }
+ };
+ res.push(d)
+ }
+
+ res.retain(|d| {
+ !ctx.config.disabled.contains(d.code.as_str())
+ && !(ctx.config.disable_experimental && d.experimental)
+ });
+
+ res
+}
+
+fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
+ let mut res = unresolved_fix(id, label, target);
+ res.source_change = Some(source_change);
+ res
+}
+
+fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
+ assert!(!id.contains(' '));
+ Assist {
+ id: AssistId(id, AssistKind::QuickFix),
+ label: Label::new(label.to_string()),
+ group: None,
+ target,
+ source_change: None,
+ trigger_signature_help: false,
+ }
+}
+
+fn adjusted_display_range<N: AstNode>(
+ ctx: &DiagnosticsContext<'_>,
+ diag_ptr: InFile<SyntaxNodePtr>,
+ adj: &dyn Fn(N) -> Option<TextRange>,
+) -> TextRange {
+ let FileRange { file_id, range } = ctx.sema.diagnostics_display_range(diag_ptr);
+
+ let source_file = ctx.sema.db.parse(file_id);
+ find_node_at_range::<N>(&source_file.syntax_node(), range)
+ .filter(|it| it.syntax().text_range() == range)
+ .and_then(adj)
+ .unwrap_or(range)
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/tests.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/tests.rs
new file mode 100644
index 000000000..7312bca32
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/tests.rs
@@ -0,0 +1,145 @@
+#[cfg(not(feature = "in-rust-tree"))]
+mod sourcegen;
+
+use expect_test::Expect;
+use ide_db::{
+ assists::AssistResolveStrategy,
+ base_db::{fixture::WithFixture, SourceDatabaseExt},
+ RootDatabase,
+};
+use stdx::trim_indent;
+use test_utils::{assert_eq_text, extract_annotations};
+
+use crate::{DiagnosticsConfig, ExprFillDefaultMode, Severity};
+
+/// Takes a multi-file input fixture with annotated cursor positions,
+/// and checks that:
+/// * a diagnostic is produced
+/// * the first diagnostic fix trigger range touches the input cursor position
+/// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
+#[track_caller]
+pub(crate) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
+ check_nth_fix(0, ra_fixture_before, ra_fixture_after);
+}
+/// Takes a multi-file input fixture with annotated cursor positions,
+/// and checks that:
+/// * a diagnostic is produced
+/// * every diagnostic fixes trigger range touches the input cursor position
+/// * that the contents of the file containing the cursor match `after` after each diagnostic fix is applied
+pub(crate) fn check_fixes(ra_fixture_before: &str, ra_fixtures_after: Vec<&str>) {
+ for (i, ra_fixture_after) in ra_fixtures_after.iter().enumerate() {
+ check_nth_fix(i, ra_fixture_before, ra_fixture_after)
+ }
+}
+
+#[track_caller]
+fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
+ let after = trim_indent(ra_fixture_after);
+
+ let (db, file_position) = RootDatabase::with_position(ra_fixture_before);
+ let mut conf = DiagnosticsConfig::default();
+ conf.expr_fill_default = ExprFillDefaultMode::Default;
+ let diagnostic =
+ super::diagnostics(&db, &conf, &AssistResolveStrategy::All, file_position.file_id)
+ .pop()
+ .expect("no diagnostics");
+ let fix = &diagnostic.fixes.expect("diagnostic misses fixes")[nth];
+ let actual = {
+ let source_change = fix.source_change.as_ref().unwrap();
+ let file_id = *source_change.source_file_edits.keys().next().unwrap();
+ let mut actual = db.file_text(file_id).to_string();
+
+ for edit in source_change.source_file_edits.values() {
+ edit.apply(&mut actual);
+ }
+ actual
+ };
+
+ assert!(
+ fix.target.contains_inclusive(file_position.offset),
+ "diagnostic fix range {:?} does not touch cursor position {:?}",
+ fix.target,
+ file_position.offset
+ );
+ assert_eq_text!(&after, &actual);
+}
+
+/// Checks that there's a diagnostic *without* fix at `$0`.
+pub(crate) fn check_no_fix(ra_fixture: &str) {
+ let (db, file_position) = RootDatabase::with_position(ra_fixture);
+ let diagnostic = super::diagnostics(
+ &db,
+ &DiagnosticsConfig::default(),
+ &AssistResolveStrategy::All,
+ file_position.file_id,
+ )
+ .pop()
+ .unwrap();
+ assert!(diagnostic.fixes.is_none(), "got a fix when none was expected: {:?}", diagnostic);
+}
+
+pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
+ let (db, file_id) = RootDatabase::with_single_file(ra_fixture);
+ let diagnostics = super::diagnostics(
+ &db,
+ &DiagnosticsConfig::default(),
+ &AssistResolveStrategy::All,
+ file_id,
+ );
+ expect.assert_debug_eq(&diagnostics)
+}
+
+#[track_caller]
+pub(crate) fn check_diagnostics(ra_fixture: &str) {
+ let mut config = DiagnosticsConfig::default();
+ config.disabled.insert("inactive-code".to_string());
+ check_diagnostics_with_config(config, ra_fixture)
+}
+
+#[track_caller]
+pub(crate) fn check_diagnostics_with_config(config: DiagnosticsConfig, ra_fixture: &str) {
+ let (db, files) = RootDatabase::with_many_files(ra_fixture);
+ for file_id in files {
+ let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
+
+ let expected = extract_annotations(&*db.file_text(file_id));
+ let mut actual = diagnostics
+ .into_iter()
+ .map(|d| {
+ let mut annotation = String::new();
+ if let Some(fixes) = &d.fixes {
+ assert!(!fixes.is_empty());
+ annotation.push_str("💡 ")
+ }
+ annotation.push_str(match d.severity {
+ Severity::Error => "error",
+ Severity::WeakWarning => "weak",
+ });
+ annotation.push_str(": ");
+ annotation.push_str(&d.message);
+ (d.range, annotation)
+ })
+ .collect::<Vec<_>>();
+ actual.sort_by_key(|(range, _)| range.start());
+ assert_eq!(expected, actual);
+ }
+}
+
+#[test]
+fn test_disabled_diagnostics() {
+ let mut config = DiagnosticsConfig::default();
+ config.disabled.insert("unresolved-module".into());
+
+ let (db, file_id) = RootDatabase::with_single_file(r#"mod foo;"#);
+
+ let diagnostics = super::diagnostics(&db, &config, &AssistResolveStrategy::All, file_id);
+ assert!(diagnostics.is_empty());
+
+ let diagnostics = super::diagnostics(
+ &db,
+ &DiagnosticsConfig::default(),
+ &AssistResolveStrategy::All,
+ file_id,
+ );
+ assert!(!diagnostics.is_empty());
+}
diff --git a/src/tools/rust-analyzer/crates/ide-diagnostics/src/tests/sourcegen.rs b/src/tools/rust-analyzer/crates/ide-diagnostics/src/tests/sourcegen.rs
new file mode 100644
index 000000000..ec6558a46
--- /dev/null
+++ b/src/tools/rust-analyzer/crates/ide-diagnostics/src/tests/sourcegen.rs
@@ -0,0 +1,73 @@
+//! Generates `assists.md` documentation.
+
+use std::{fmt, fs, io, path::PathBuf};
+
+use sourcegen::project_root;
+
+#[test]
+fn sourcegen_diagnostic_docs() {
+ let diagnostics = Diagnostic::collect().unwrap();
+ let contents =
+ diagnostics.into_iter().map(|it| it.to_string()).collect::<Vec<_>>().join("\n\n");
+ let contents = sourcegen::add_preamble("sourcegen_diagnostic_docs", contents);
+ let dst = project_root().join("docs/user/generated_diagnostic.adoc");
+ fs::write(&dst, &contents).unwrap();
+}
+
+#[derive(Debug)]
+struct Diagnostic {
+ id: String,
+ location: sourcegen::Location,
+ doc: String,
+}
+
+impl Diagnostic {
+ fn collect() -> io::Result<Vec<Diagnostic>> {
+ let handlers_dir = project_root().join("crates/ide-diagnostics/src/handlers");
+
+ let mut res = Vec::new();
+ for path in sourcegen::list_rust_files(&handlers_dir) {
+ collect_file(&mut res, path)?;
+ }
+ res.sort_by(|lhs, rhs| lhs.id.cmp(&rhs.id));
+ return Ok(res);
+
+ fn collect_file(acc: &mut Vec<Diagnostic>, path: PathBuf) -> io::Result<()> {
+ let text = fs::read_to_string(&path)?;
+ let comment_blocks = sourcegen::CommentBlock::extract("Diagnostic", &text);
+
+ for block in comment_blocks {
+ let id = block.id;
+ if let Err(msg) = is_valid_diagnostic_name(&id) {
+ panic!("invalid diagnostic name: {:?}:\n {}", id, msg)
+ }
+ let doc = block.contents.join("\n");
+ let location = sourcegen::Location { file: path.clone(), line: block.line };
+ acc.push(Diagnostic { id, location, doc })
+ }
+
+ Ok(())
+ }
+ }
+}
+
+fn is_valid_diagnostic_name(diagnostic: &str) -> Result<(), String> {
+ let diagnostic = diagnostic.trim();
+ if diagnostic.find(char::is_whitespace).is_some() {
+ return Err("Diagnostic names can't contain whitespace symbols".into());
+ }
+ if diagnostic.chars().any(|c| c.is_ascii_uppercase()) {
+ return Err("Diagnostic names can't contain uppercase symbols".into());
+ }
+ if diagnostic.chars().any(|c| !c.is_ascii()) {
+ return Err("Diagnostic can't contain non-ASCII symbols".into());
+ }
+
+ Ok(())
+}
+
+impl fmt::Display for Diagnostic {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ writeln!(f, "=== {}\n**Source:** {}\n{}", self.id, self.location, self.doc)
+ }
+}