From 3d2f0400a26ef6b07d61a06e1b543072b627570e Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 14 Jun 2021 12:24:58 +0300 Subject: internal: start ide diagnostics crate --- crates/ide_diagnostics/Cargo.toml | 12 ++++++++++++ crates/ide_diagnostics/src/lib.rs | 0 2 files changed, 12 insertions(+) create mode 100644 crates/ide_diagnostics/Cargo.toml create mode 100644 crates/ide_diagnostics/src/lib.rs (limited to 'crates/ide_diagnostics') diff --git a/crates/ide_diagnostics/Cargo.toml b/crates/ide_diagnostics/Cargo.toml new file mode 100644 index 000000000..11cd8d570 --- /dev/null +++ b/crates/ide_diagnostics/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ide_diagnostics" +version = "0.0.0" +description = "TBD" +license = "MIT OR Apache-2.0" +authors = ["rust-analyzer developers"] +edition = "2018" + +[lib] +doctest = false + +[dependencies] diff --git a/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs new file mode 100644 index 000000000..e69de29bb -- cgit v1.2.3 From 1d2772c2c7dc0a42d8a9429d24ea41412add61b3 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 14 Jun 2021 13:15:05 +0300 Subject: internal: move diagnostics to a new crate --- crates/ide_diagnostics/Cargo.toml | 18 + .../ide_diagnostics/src/break_outside_of_loop.rs | 30 + crates/ide_diagnostics/src/field_shorthand.rs | 203 +++++ crates/ide_diagnostics/src/inactive_code.rs | 116 +++ crates/ide_diagnostics/src/incorrect_case.rs | 496 +++++++++++ crates/ide_diagnostics/src/lib.rs | 510 +++++++++++ crates/ide_diagnostics/src/macro_error.rs | 173 ++++ crates/ide_diagnostics/src/mismatched_arg_count.rs | 272 ++++++ crates/ide_diagnostics/src/missing_fields.rs | 327 ++++++++ crates/ide_diagnostics/src/missing_match_arms.rs | 929 +++++++++++++++++++++ .../src/missing_ok_or_some_in_tail_expr.rs | 230 +++++ crates/ide_diagnostics/src/missing_unsafe.rs | 101 +++ crates/ide_diagnostics/src/no_such_field.rs | 283 +++++++ .../ide_diagnostics/src/remove_this_semicolon.rs | 61 ++ .../src/replace_filter_map_next_with_find_map.rs | 179 ++++ .../src/unimplemented_builtin_macro.rs | 16 + crates/ide_diagnostics/src/unlinked_file.rs | 301 +++++++ .../ide_diagnostics/src/unresolved_extern_crate.rs | 49 ++ crates/ide_diagnostics/src/unresolved_import.rs | 90 ++ .../ide_diagnostics/src/unresolved_macro_call.rs | 84 ++ crates/ide_diagnostics/src/unresolved_module.rs | 111 +++ .../ide_diagnostics/src/unresolved_proc_macro.rs | 27 + 22 files changed, 4606 insertions(+) create mode 100644 crates/ide_diagnostics/src/break_outside_of_loop.rs create mode 100644 crates/ide_diagnostics/src/field_shorthand.rs create mode 100644 crates/ide_diagnostics/src/inactive_code.rs create mode 100644 crates/ide_diagnostics/src/incorrect_case.rs create mode 100644 crates/ide_diagnostics/src/macro_error.rs create mode 100644 crates/ide_diagnostics/src/mismatched_arg_count.rs create mode 100644 crates/ide_diagnostics/src/missing_fields.rs create mode 100644 crates/ide_diagnostics/src/missing_match_arms.rs create mode 100644 crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs create mode 100644 crates/ide_diagnostics/src/missing_unsafe.rs create mode 100644 crates/ide_diagnostics/src/no_such_field.rs create mode 100644 crates/ide_diagnostics/src/remove_this_semicolon.rs create mode 100644 crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs create mode 100644 crates/ide_diagnostics/src/unimplemented_builtin_macro.rs create mode 100644 crates/ide_diagnostics/src/unlinked_file.rs create mode 100644 crates/ide_diagnostics/src/unresolved_extern_crate.rs create mode 100644 crates/ide_diagnostics/src/unresolved_import.rs create mode 100644 crates/ide_diagnostics/src/unresolved_macro_call.rs create mode 100644 crates/ide_diagnostics/src/unresolved_module.rs create mode 100644 crates/ide_diagnostics/src/unresolved_proc_macro.rs (limited to 'crates/ide_diagnostics') diff --git a/crates/ide_diagnostics/Cargo.toml b/crates/ide_diagnostics/Cargo.toml index 11cd8d570..738fca14e 100644 --- a/crates/ide_diagnostics/Cargo.toml +++ b/crates/ide_diagnostics/Cargo.toml @@ -10,3 +10,21 @@ edition = "2018" doctest = false [dependencies] +cov-mark = "2.0.0-pre.1" +itertools = "0.10.0" +rustc-hash = "1.1.0" +either = "1.5.3" + +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" } +ide_assists = { path = "../ide_assists", version = "0.0.0" } + +[dev-dependencies] +expect-test = "1.1" + +test_utils = { path = "../test_utils" } diff --git a/crates/ide_diagnostics/src/break_outside_of_loop.rs b/crates/ide_diagnostics/src/break_outside_of_loop.rs new file mode 100644 index 000000000..79e8cea37 --- /dev/null +++ b/crates/ide_diagnostics/src/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(super) 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; } + //^^^^^ break outside of loop +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/field_shorthand.rs b/crates/ide_diagnostics/src/field_shorthand.rs new file mode 100644 index 000000000..0b6af9965 --- /dev/null +++ b/crates/ide_diagnostics/src/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(super) fn check(acc: &mut Vec, 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, + 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, + 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/crates/ide_diagnostics/src/inactive_code.rs b/crates/ide_diagnostics/src/inactive_code.rs new file mode 100644 index 000000000..34837cc0d --- /dev/null +++ b/crates/ide_diagnostics/src/inactive_code.rs @@ -0,0 +1,116 @@ +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(super) fn inactive_code( + ctx: &DiagnosticsContext<'_>, + d: &hir::InactiveCode, +) -> Option { + // If there's inactive code somewhere in a macro, don't propagate to the call-site. + if d.node.file_id.expansion_info(ctx.sema.db).is_some() { + 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 { + 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 + //^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + #[cfg(a)] {} // Expression statement + //^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + #[cfg(a)] let x = 0; // let statement + //^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + + abc(#[cfg(a)] 0); + //^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + let x = Struct { + #[cfg(a)] f: 0, + //^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + }; + match () { + () => (), + #[cfg(a)] () => (), + //^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + } + + #[cfg(a)] 0 // Trailing expression of block + //^^^^^^^^^^^ 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() {} + //^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no is disabled + + #[cfg(no)] #[cfg(no2)] mod m; + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no and no2 are disabled + + #[cfg(all(not(a), b))] enum E {} + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: b is disabled + + #[cfg(feature = "std")] use std; + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: feature = "std" is disabled +"#, + ); + } + + /// 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() {} + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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() {} + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no is disabled +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/incorrect_case.rs b/crates/ide_diagnostics/src/incorrect_case.rs new file mode 100644 index 000000000..04fc779ce --- /dev/null +++ b/crates/ide_diagnostics/src/incorrect_case.rs @@ -0,0 +1,496 @@ +use hir::{db::AstDatabase, InFile}; +use ide_assists::Assist; +use ide_db::base_db::FilePosition; +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(super) 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> { + if true { + return None; + } + + let root = ctx.sema.db.parse_or_expand(d.file)?; + let name_node = d.ident.to_node(&root); + + let name_node = InFile::new(d.file, name_node.syntax()); + let frange = name_node.original_file_range(ctx.sema.db); + let _file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; + + let label = format!("Rename to {}", d.suggested_text); + let res = unresolved_fix("change_case", &label, frange.range); + if ctx.resolve.should_resolve(&res.id) { + //let source_change = rename_with_semantics(&ctx.sema, file_position, &d.suggested_text); + //res.source_change = Some(source_change.ok().unwrap_or_default()); + todo!() + } + + Some(vec![res]) +} + +#[cfg(TODO)] +mod change_case { + use crate::{ + fixture, + tests::{check_diagnostics, check_fix}, + AssistResolveStrategy, DiagnosticsConfig, + }; + + #[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_a_weird_formatting = 10; + another_func(what_a_weird_formatting); +} +"#, + ); + } + + #[test] + fn test_uppercase_const_no_diagnostics() { + check_diagnostics( + r#" +fn foo() { + const ANOTHER_ITEM$0: &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() { + let input = r#"fn FOO$0() {}"#; + let expected = r#"fn foo() {}"#; + + let (analysis, file_position) = fixture::position(input); + let diagnostics = analysis + .diagnostics( + &DiagnosticsConfig::default(), + AssistResolveStrategy::All, + file_position.file_id, + ) + .unwrap(); + assert_eq!(diagnostics.len(), 1); + + check_fix(input, expected); + } + + #[test] + fn incorrect_function_name() { + check_diagnostics( + r#" +fn NonSnakeCaseName() {} +// ^^^^^^^^^^^^^^^^ 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) {} + // ^^^^^^^^^ Parameter `SomeParam` should have snake_case name, e.g. `some_param` + +fn foo2(ok_param: &str, CAPS_PARAM: u8) {} + // ^^^^^^^^^^ 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; + // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value` + let AnotherValue = 20; + // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value` +} +"#, + ); + } + + #[test] + fn incorrect_struct_names() { + check_diagnostics( + r#" +struct non_camel_case_name {} + // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName` + +struct SCREAMING_CASE {} + // ^^^^^^^^^^^^^^ 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 } + // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field` +"#, + ); + } + + #[test] + fn incorrect_enum_names() { + check_diagnostics( + r#" +enum some_enum { Val(u8) } + // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum` + +enum SOME_ENUM {} + // ^^^^^^^^^ 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) } + // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant` +"#, + ); + } + + #[test] + fn incorrect_const_name() { + check_diagnostics( + r#" +const some_weird_const: u8 = 10; + // ^^^^^^^^^^^^^^^^ 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; + // ^^^^^^^^^^^^^^^^ 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; + // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct` + +impl someStruct { + fn SomeFunc(&self) { + // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func` + let WHY_VAR_IS_CAPS = 10; + // ^^^^^^^^^^^^^^^ 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 => (), + // ^^^^^^^^ 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)] + +mod F { + fn CheckItWorksWithCrateAttr(BAD_NAME_HI: u8) {} +} + "#, + ); + } + + #[test] + #[ignore] + fn bug_trait_inside_fn() { + // FIXME: + // This is broken, and in fact, should not even be looked at by this + // lint in the first place. There's weird stuff going on in the + // collection phase. + // It's currently being brought in by: + // * validate_func on `a` recursing into modules + // * then it finds the trait and then the function while iterating + // through modules + // * then validate_func is called on Dirty + // * ... which then proceeds to look at some unknown module taking no + // attrs from either the impl or the fn a, and then finally to the root + // module + // + // It should find the attribute on the trait, but it *doesn't even see + // the trait* as far as I can tell. + + check_diagnostics( + r#" +trait T { fn a(); } +struct U {} +impl T for U { + fn a() { + // this comes out of bitflags, mostly + #[allow(non_snake_case)] + trait __BitFlags { + const HiImAlsoBad: u8 = 2; + #[inline] + 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] + #[ignore] + 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 { + // ^^^^^^^^^ Trait `BAD_TRAIT` should have CamelCase name, e.g. `BadTrait` + fn BAD_FUNCTION(); + // ^^^^^^^^^^^^ Function `BAD_FUNCTION` should have snake_case name, e.g. `bad_function` + fn BadFunction(); + // ^^^^^^^^^^^^ Function `BadFunction` should have snake_case name, e.g. `bad_function` +} + "#, + ); + } + + #[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/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs index e69de29bb..a104a702d 100644 --- a/crates/ide_diagnostics/src/lib.rs +++ b/crates/ide_diagnostics/src/lib.rs @@ -0,0 +1,510 @@ +//! Collects diagnostics & fixits for a single file. +//! +//! The tricky bit here is that diagnostics are produced by hir in terms of +//! macro-expanded files, but we need to present them to the users in terms of +//! original files. So we need to map the ranges. + +mod break_outside_of_loop; +mod inactive_code; +mod incorrect_case; +mod macro_error; +mod mismatched_arg_count; +mod missing_fields; +mod missing_match_arms; +mod missing_ok_or_some_in_tail_expr; +mod missing_unsafe; +mod no_such_field; +mod remove_this_semicolon; +mod replace_filter_map_next_with_find_map; +mod unimplemented_builtin_macro; +mod unlinked_file; +mod unresolved_extern_crate; +mod unresolved_import; +mod unresolved_macro_call; +mod unresolved_module; +mod unresolved_proc_macro; + +mod field_shorthand; + +use hir::{diagnostics::AnyDiagnostic, Semantics}; +use ide_assists::AssistResolveStrategy; +use ide_db::{ + base_db::{FileId, SourceDatabase}, + label::Label, + source_change::SourceChange, + RootDatabase, +}; +use itertools::Itertools; +use rustc_hash::FxHashSet; +use syntax::{ + ast::{self, AstNode}, + SyntaxNode, TextRange, +}; +use text_edit::TextEdit; +use unlinked_file::UnlinkedFile; + +use ide_assists::{Assist, AssistId, AssistKind}; + +#[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>, +} + +impl Diagnostic { + fn new(code: &'static str, message: impl Into, 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>) -> 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, + WeakWarning, +} + +#[derive(Default, Debug, Clone)] +pub struct DiagnosticsConfig { + pub disable_experimental: bool, + pub disabled: FxHashSet, +} + +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 { + 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() { + check_unnecessary_braces_in_use_statement(&mut res, file_id, &node); + field_shorthand::check(&mut res, file_id, &node); + } + + let mut diags = Vec::new(); + let module = sema.to_module_def(file_id); + if let Some(m) = module { + m.diagnostics(db, &mut diags) + } + + let ctx = DiagnosticsContext { config, sema, resolve }; + if module.is_none() { + let d = UnlinkedFile { file: file_id }; + let d = unlinked_file::unlinked_file(&ctx, &d); + res.push(d) + } + + for diag in diags { + #[rustfmt::skip] + let d = match diag { + AnyDiagnostic::BreakOutsideOfLoop(d) => break_outside_of_loop::break_outside_of_loop(&ctx, &d), + AnyDiagnostic::IncorrectCase(d) => incorrect_case::incorrect_case(&ctx, &d), + AnyDiagnostic::MacroError(d) => macro_error::macro_error(&ctx, &d), + AnyDiagnostic::MismatchedArgCount(d) => mismatched_arg_count::mismatched_arg_count(&ctx, &d), + AnyDiagnostic::MissingFields(d) => missing_fields::missing_fields(&ctx, &d), + AnyDiagnostic::MissingMatchArms(d) => missing_match_arms::missing_match_arms(&ctx, &d), + AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d), + AnyDiagnostic::MissingUnsafe(d) => missing_unsafe::missing_unsafe(&ctx, &d), + AnyDiagnostic::NoSuchField(d) => no_such_field::no_such_field(&ctx, &d), + AnyDiagnostic::RemoveThisSemicolon(d) => remove_this_semicolon::remove_this_semicolon(&ctx, &d), + AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d), + AnyDiagnostic::UnimplementedBuiltinMacro(d) => unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d), + AnyDiagnostic::UnresolvedExternCrate(d) => unresolved_extern_crate::unresolved_extern_crate(&ctx, &d), + AnyDiagnostic::UnresolvedImport(d) => unresolved_import::unresolved_import(&ctx, &d), + AnyDiagnostic::UnresolvedMacroCall(d) => unresolved_macro_call::unresolved_macro_call(&ctx, &d), + AnyDiagnostic::UnresolvedModule(d) => unresolved_module::unresolved_module(&ctx, &d), + AnyDiagnostic::UnresolvedProcMacro(d) => unresolved_proc_macro::unresolved_proc_macro(&ctx, &d), + + AnyDiagnostic::InactiveCode(d) => match 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 check_unnecessary_braces_in_use_statement( + acc: &mut Vec, + 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 = + text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&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 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement( + single_use_tree: &ast::UseTree, +) -> Option { + 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 +} + +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), + group: None, + target, + source_change: None, + } +} + +#[cfg(test)] +mod tests { + use expect_test::Expect; + use ide_assists::AssistResolveStrategy; + use ide_db::{ + base_db::{fixture::WithFixture, SourceDatabaseExt}, + RootDatabase, + }; + use stdx::trim_indent; + use test_utils::{assert_eq_text, extract_annotations}; + + use crate::DiagnosticsConfig; + + /// 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 diagnostic = super::diagnostics( + &db, + &DiagnosticsConfig::default(), + &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_eq_text!(&after, &actual); + assert!( + fix.target.contains_inclusive(file_position.offset), + "diagnostic fix range {:?} does not touch cursor position {:?}", + fix.target, + file_position.offset + ); + } + + /// 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| (d.range, d.message)).collect::>(); + actual.sort_by_key(|(range, _)| range.start()); + assert_eq!(expected, actual); + } + } + + #[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}; + ", + ); + } + + #[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()); + } + + #[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/crates/ide_diagnostics/src/macro_error.rs b/crates/ide_diagnostics/src/macro_error.rs new file mode 100644 index 000000000..180f297eb --- /dev/null +++ b/crates/ide_diagnostics/src/macro_error.rs @@ -0,0 +1,173 @@ +use crate::{Diagnostic, DiagnosticsContext}; + +// Diagnostic: macro-error +// +// This diagnostic is shown for macro expansion errors. +pub(super) fn macro_error(ctx: &DiagnosticsContext<'_>, d: &hir::MacroError) -> Diagnostic { + Diagnostic::new( + "macro-error", + d.message.clone(), + ctx.sema.diagnostics_display_range(d.node.clone()).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 { () => {} } + + include!("doesntexist"); +//^^^^^^^^^^^^^^^^^^^^^^^^ failed to load file `doesntexist` + "#, + ); + } + + #[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")); +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `OUT_DIR` not set, enable "run 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); + //^^^^^^^^^^^^^^^^^ could not convert tokens + include!("does not exist"); + //^^^^^^^^^^^^^^^^^^^^^^^^^^ failed to load file `does not exist` + + env!(invalid); + //^^^^^^^^^^^^^ could not convert tokens + + env!("OUT_DIR"); + //^^^^^^^^^^^^^^^ `OUT_DIR` not set, enable "run build scripts" to fix + + compile_error!("compile_error works"); + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ compile_error works + + // Lazy: + + format_args!(); + //^^^^^^^^^^^^^^ no rule matches input tokens +} +"#, + ); + } + + #[test] + fn macro_rules_diag() { + check_diagnostics( + r#" +macro_rules! m { + () => {}; +} +fn f() { + m!(); + + m!(hi); + //^^^^^^ 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!(); +} //^^^^^^^^ leftover tokens +"#, + ) + } +} diff --git a/crates/ide_diagnostics/src/mismatched_arg_count.rs b/crates/ide_diagnostics/src/mismatched_arg_count.rs new file mode 100644 index 000000000..c5749c8a6 --- /dev/null +++ b/crates/ide_diagnostics/src/mismatched_arg_count.rs @@ -0,0 +1,272 @@ +use crate::{Diagnostic, DiagnosticsContext}; + +// Diagnostic: mismatched-arg-count +// +// This diagnostic is triggered if a function is invoked with an incorrect amount of arguments. +pub(super) 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, + ctx.sema.diagnostics_display_range(d.call_expr.clone().map(|it| it.into())).range, + ) +} + +#[cfg(test)] +mod tests { + use crate::tests::check_diagnostics; + + #[test] + fn simple_free_fn_zero() { + check_diagnostics( + r#" +fn zero() {} +fn f() { zero(1); } + //^^^^^^^ 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(); } + //^^^^^ 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(); +} //^^^^^^^^^^^ 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(); + } //^^^^^^^^^^ 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); +} //^^^^^^ expected 2 arguments, found 1 +"#, + ) + } + + #[test] + fn enum_variant() { + check_diagnostics( + r#" +enum En { Variant(u8, u16), } +fn f() { + En::Variant(0); +} //^^^^^^^^^^^^^^ 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); + //^^^^^^^^^^^^^^ expected 1 argument, found 2 + Foo::Bar(); + //^^^^^^^^^^ 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); + //^^^^^^^^^^^ 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(); + //^^^ expected 1 argument, found 0 + f(()); + f((), ()); + //^^^^^^^^^ 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); + } +} + "#, + ) + } +} diff --git a/crates/ide_diagnostics/src/missing_fields.rs b/crates/ide_diagnostics/src/missing_fields.rs new file mode 100644 index 000000000..f242ee481 --- /dev/null +++ b/crates/ide_diagnostics/src/missing_fields.rs @@ -0,0 +1,327 @@ +use either::Either; +use hir::{db::AstDatabase, InFile}; +use ide_assists::Assist; +use ide_db::source_change::SourceChange; +use stdx::format_to; +use syntax::{algo, ast::make, AstNode, 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(super) 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> { + // 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 field_list_parent = match &d.field_list_parent { + Either::Left(record_expr) => record_expr.to_node(&root), + // FIXE: patterns should be fixable as well. + Either::Right(_) => return None, + }; + let old_field_list = field_list_parent.record_expr_field_list()?; + let new_field_list = old_field_list.clone_for_update(); + for f in d.missed_fields.iter() { + let field = + make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())) + .clone_for_update(); + new_field_list.add_field(field); + } + + let edit = { + let mut builder = TextEdit::builder(); + algo::diff(old_field_list.syntax(), new_field_list.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(field_list_parent.syntax()).range, + )]) +} + +#[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; + //^ 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 } +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 range_mapping_out_of_macros() { + // FIXME: this is very wrong, but somewhat tricky to fix. + 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(, b: () ) {} +fn items() {} +fn here() {} + +macro_rules! id { ($($tt:tt)*) => { $($tt)*}; } + +fn main() { + let _x = id![Foo { a: 42 }]; +} + +pub struct Foo { pub a: i32, pub b: i32 } +"#, + ); + } + + #[test] + fn test_fill_struct_fields_empty() { + check_fix( + r#" +struct TestStruct { one: i32, two: i64 } + +fn test_fn() { + let s = TestStruct {$0}; +} +"#, + r#" +struct TestStruct { one: i32, two: i64 } + +fn test_fn() { + let s = TestStruct { one: (), two: () }; +} +"#, + ); + } + + #[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: () }; } +} +"#, + ); + } + + #[test] + fn test_fill_struct_fields_enum() { + check_fix( + r#" +enum Expr { + Bin { lhs: Box, rhs: Box } +} + +impl Expr { + fn new_bin(lhs: Box, rhs: Box) -> Expr { + Expr::Bin {$0 } + } +} +"#, + r#" +enum Expr { + Bin { lhs: Box, rhs: Box } +} + +impl Expr { + fn new_bin(lhs: Box, rhs: Box) -> 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: () }; +} +", + ); + } + + #[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: () }; +} +", + ); + } + + #[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: (), + b: (), + }; +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/missing_match_arms.rs b/crates/ide_diagnostics/src/missing_match_arms.rs new file mode 100644 index 000000000..c83155d2f --- /dev/null +++ b/crates/ide_diagnostics/src/missing_match_arms.rs @@ -0,0 +1,929 @@ +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(super) fn missing_match_arms( + ctx: &DiagnosticsContext<'_>, + d: &hir::MissingMatchArms, +) -> Diagnostic { + Diagnostic::new( + "missing-match-arm", + "missing match arm", + ctx.sema.diagnostics_display_range(InFile::new(d.file, d.match_expr.clone().into())).range, + ) +} + +#[cfg(test)] +pub(super) 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 () { } + //^^ missing match arm + match (()) { } + //^^^^ missing match arm + + match () { _ => (), } + match () { () => (), } + match (()) { (()) => (), } +} +"#, + ); + } + + #[test] + fn tuple_of_two_empty_tuple() { + check_diagnostics_no_bails( + r#" +fn main() { + match ((), ()) { } + //^^^^^^^^ missing match arm + + match ((), ()) { ((), ()) => (), } +} +"#, + ); + } + + #[test] + fn boolean() { + check_diagnostics_no_bails( + r#" +fn test_main() { + match false { } + //^^^^^ missing match arm + match false { true => (), } + //^^^^^ missing match arm + match (false, true) {} + //^^^^^^^^^^^^^ missing match arm + match (false, true) { (true, true) => (), } + //^^^^^^^^^^^^^ missing match arm + match (false, true) { + //^^^^^^^^^^^^^ missing match arm + (false, true) => (), + (false, false) => (), + (true, false) => (), + } + match (false, true) { (true, _x) => (), } + //^^^^^^^^^^^^^ missing match arm + + 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)) {} + //^^^^^^^^^^^^^^^^^^^^ missing match arm + match (false, ((), false)) { (true, ((), true)) => (), } + //^^^^^^^^^^^^^^^^^^^^ missing match arm + match (false, ((), false)) { (true, _) => (), } + //^^^^^^^^^^^^^^^^^^^^ missing match arm + + 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 { } + //^^^^^^^^^ missing match arm + match Either::B { Either::A => (), } + //^^^^^^^^^ missing match arm + + match &Either::B { + //^^^^^^^^^^ missing match arm + 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 { } + //^^^^^^^^^ missing match arm + match Either::B { + //^^^^^^^^^ missing match arm + 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) { + //^^^^^^^^^^^^^^^^ missing match arm + 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) => (), + } + 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 } { + //^^^^^^^^^^^^^^^^^^^^^ missing match arm + Either::A => (), + } + match loop { break Foo::A } { + Either::A => (), + Either::B => (), + } +} +"#, + ); + } + + #[test] + fn expr_partially_diverges() { + check_diagnostics_no_bails( + r#" +enum Either { 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 { } + //^ missing match arm + match a { Either::A { foo: true } => () } + //^ missing match arm + match a { + Either::A { } => (), + //^^^^^^^^^ Missing structure fields: + // | - foo + Either::B => (), + } + match a { + //^ missing match arm + Either::A { } => (), + } //^^^^^^^^^ 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 { + //^ missing match arm + 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 { + //^ missing match arm + Either::A { foo: true, .. } => (), + Either::B => (), + } + match a { + //^ missing match arm + 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 { + //^^^^^^^^^ missing match arm + Either::A(true, .., true) => (), + Either::A(true, .., false) => (), + Either::A(false, .., false) => (), + Either::B => (), + } + match Either::B { + //^^^^^^^^^ missing match arm + 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 {} + //^^^^^ missing match arm +} +fn bang(never: !) { + match never {} +} +"#, + ); + } + + #[test] + fn unknown_type() { + cov_mark::check_count!(validate_match_bailed_out, 1); + + check_diagnostics( + r#" +enum Option { Some(T), None } + +fn main() { + // `Never` is deliberately not defined so that it's an uninferred type. + match Option::::None { + None => (), + Some(never) => match never {}, + } + match Option::::None { + //^^^^^^^^^^^^^^^^^^^^^ missing match arm + 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) { + //^^^^^^^^^^^^^^^^^^^^ missing match arm + (false, ..) => (), + } +}"#, + ); + } + + #[test] + fn tuple_of_bools_with_ellipsis_at_beginning_missing_arm() { + check_diagnostics_no_bails( + r#" +fn main() { + match (false, true, false) { + //^^^^^^^^^^^^^^^^^^^^ missing match arm + (.., false) => (), + } +}"#, + ); + } + + #[test] + fn tuple_of_bools_with_ellipsis_in_middle_missing_arm() { + check_diagnostics_no_bails( + r#" +fn main() { + match (false, true, false) { + //^^^^^^^^^^^^^^^^^^^^ missing match arm + (true, .., false) => (), + } +}"#, + ); + } + + #[test] + fn record_struct() { + check_diagnostics_no_bails( + r#"struct Foo { a: bool } +fn main(f: Foo) { + match f {} + //^ missing match arm + match f { Foo { a: true } => () } + //^ missing match arm + match &f { Foo { a: true } => () } + //^^ missing match arm + 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 {} + //^ missing match arm + match f { Foo(true) => () } + //^ missing match arm + match f { + Foo(true) => (), + Foo(false) => (), + } +} +"#, + ); + } + + #[test] + fn unit_struct() { + check_diagnostics_no_bails( + r#"struct Foo; +fn main(f: Foo) { + match f {} + //^ missing match arm + 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, .. } => () } + //^ missing match arm + match f { + //^ missing match arm + 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 { + //^^^^^^^^^ missing match arm + 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 { } + //^ missing match arm +} +"#, + ); + } + + #[test] + fn binding() { + check_diagnostics_no_bails( + r#" +fn main() { + match true { + _x @ true => {} + false => {} + } + match true { _x @ true => {} } + //^^^^ missing match arm +} +"#, + ); + } + + #[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 { + //^^^^ missing match arm + E::A => {} + E::B => {} + } + match E::A { + //^^^^ missing match arm + E::A | E::B => {} + } +} +"#, + ); + } + + #[test] + fn match_guard() { + check_diagnostics_no_bails( + r#" +fn main() { + match true { + true if false => {} + true => {} + false => {} + } + match true { + //^^^^ missing match arm + true if false => {} + false => {} + } +} +"#, + ); + } + + #[test] + fn pattern_type_is_of_substitution() { + cov_mark::check!(match_check_wildcard_expanded_to_substitutions); + check_diagnostics_no_bails( + r#" +struct Foo(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 { A(T) } +fn main() { + match &Foo::A(true) { + _ => {} + Foo::A(_) => {} + } +} +"#, + ); + } + + 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/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs b/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs new file mode 100644 index 000000000..9e36ca296 --- /dev/null +++ b/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs @@ -0,0 +1,230 @@ +use hir::db::AstDatabase; +use ide_assists::Assist; +use ide_db::source_change::SourceChange; +use syntax::AstNode; +use text_edit::TextEdit; + +use crate::{fix, Diagnostic, DiagnosticsContext}; + +// Diagnostic: missing-ok-or-some-in-tail-expr +// +// This diagnostic is triggered if a block that should return `Result` returns a value not wrapped in `Ok`, +// or if a block that should return `Option` returns a value not wrapped in `Some`. +// +// Example: +// +// ```rust +// fn foo() -> Result { +// 10 +// } +// ``` +pub(super) fn missing_ok_or_some_in_tail_expr( + ctx: &DiagnosticsContext<'_>, + d: &hir::MissingOkOrSomeInTailExpr, +) -> Diagnostic { + Diagnostic::new( + "missing-ok-or-some-in-tail-expr", + format!("wrap return expression in {}", d.required), + ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, + ) + .with_fixes(fixes(ctx, d)) +} + +fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingOkOrSomeInTailExpr) -> Option> { + let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?; + let tail_expr = d.expr.value.to_node(&root); + let tail_expr_range = tail_expr.syntax().text_range(); + let replacement = format!("{}({})", d.required, tail_expr.syntax()); + let edit = TextEdit::replace(tail_expr_range, replacement); + let source_change = + SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit); + let name = if d.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; + Some(vec![fix("wrap_tail_expr", name, source_change, tail_expr_range)]) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_diagnostics, check_fix}; + + #[test] + fn test_wrap_return_type_option() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::option::Option::{self, Some, None}; + +fn div(x: i32, y: i32) -> Option { + if y == 0 { + return None; + } + x / y$0 +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::option::Option::{self, Some, None}; + +fn div(x: i32, y: i32) -> Option { + if y == 0 { + return None; + } + Some(x / y) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +fn div(x: i32, y: i32) -> Result { + if y == 0 { + return Err(()); + } + x / y$0 +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::result::Result::{self, Ok, Err}; + +fn div(x: i32, y: i32) -> Result { + if y == 0 { + return Err(()); + } + Ok(x / y) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_handles_generic_functions() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +fn div(x: T) -> Result { + if x == 0 { + return Err(7); + } + $0x +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::result::Result::{self, Ok, Err}; + +fn div(x: T) -> Result { + if x == 0 { + return Err(7); + } + Ok(x) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_handles_type_aliases() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +type MyResult = Result; + +fn div(x: i32, y: i32) -> MyResult { + if y == 0 { + return Err(()); + } + x $0/ y +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::result::Result::{self, Ok, Err}; + +type MyResult = Result; + +fn div(x: i32, y: i32) -> MyResult { + if y == 0 { + return Err(()); + } + Ok(x / y) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { + check_diagnostics( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +fn foo() -> Result<(), i32> { 0 } + +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() { + check_diagnostics( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +enum SomeOtherEnum { Ok(i32), Err(String) } + +fn foo() -> SomeOtherEnum { 0 } + +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/missing_unsafe.rs b/crates/ide_diagnostics/src/missing_unsafe.rs new file mode 100644 index 000000000..f5f38a0d3 --- /dev/null +++ b/crates/ide_diagnostics/src/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(super) 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; +} //^^ 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(); + //^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block + HasUnsafe.unsafe_fn(); + //^^^^^^^^^^^^^^^^^^^^^ 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; + //^^^^^^^^^^ 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); + //^^^^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/no_such_field.rs b/crates/ide_diagnostics/src/no_such_field.rs new file mode 100644 index 000000000..c4fa387ca --- /dev/null +++ b/crates/ide_diagnostics/src/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(super) 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> { + 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, + usage_file_id: FileId, + record_expr_field: &ast::RecordExprField, +) -> Option> { + 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()?)?; + 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 { + 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 { + //^ Missing structure fields: + //| - bar + foo: 92, + baz: 62, + //^^^^^^^ 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/crates/ide_diagnostics/src/remove_this_semicolon.rs b/crates/ide_diagnostics/src/remove_this_semicolon.rs new file mode 100644 index 000000000..dc6c9c083 --- /dev/null +++ b/crates/ide_diagnostics/src/remove_this_semicolon.rs @@ -0,0 +1,61 @@ +use hir::db::AstDatabase; +use ide_db::source_change::SourceChange; +use syntax::{ast, AstNode}; +use text_edit::TextEdit; + +use crate::{fix, Assist, Diagnostic, DiagnosticsContext}; + +// Diagnostic: remove-this-semicolon +// +// This diagnostic is triggered when there's an erroneous `;` at the end of the block. +pub(super) fn remove_this_semicolon( + ctx: &DiagnosticsContext<'_>, + d: &hir::RemoveThisSemicolon, +) -> Diagnostic { + Diagnostic::new( + "remove-this-semicolon", + "remove this semicolon", + ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, + ) + .with_fixes(fixes(ctx, d)) +} + +fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::RemoveThisSemicolon) -> Option> { + let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?; + + let semicolon = d + .expr + .value + .to_node(&root) + .syntax() + .parent() + .and_then(ast::ExprStmt::cast) + .and_then(|expr| expr.semicolon_token())? + .text_range(); + + let edit = TextEdit::delete(semicolon); + let source_change = + SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit); + + Some(vec![fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)]) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_diagnostics, check_fix}; + + #[test] + fn missing_semicolon() { + check_diagnostics( + r#" +fn test() -> i32 { 123; } + //^^^ remove this semicolon +"#, + ); + } + + #[test] + fn remove_semicolon() { + check_fix(r#"fn f() -> i32 { 92$0; }"#, r#"fn f() -> i32 { 92 }"#); + } +} diff --git a/crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs b/crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs new file mode 100644 index 000000000..775c350d2 --- /dev/null +++ b/crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs @@ -0,0 +1,179 @@ +use hir::{db::AstDatabase, InFile}; +use ide_db::source_change::SourceChange; +use syntax::{ + ast::{self, ArgListOwner}, + 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(super) 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> { + 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_fix; + + // Register the required standard library types to make the tests work + #[track_caller] + fn check_diagnostics(ra_fixture: &str) { + let prefix = r#" +//- /main.rs crate:main deps:core +use core::iter::Iterator; +use core::option::Option::{self, Some, None}; +"#; + let suffix = r#" +//- /core/lib.rs crate:core +pub mod option { + pub enum Option { Some(T), None } +} +pub mod iter { + pub trait Iterator { + type Item; + fn filter_map(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option { FilterMap } + fn next(&mut self) -> Option; + } + pub struct FilterMap {} + impl Iterator for FilterMap { + type Item = i32; + fn next(&mut self) -> i32 { 7 } + } +} +"#; + crate::tests::check_diagnostics(&format!("{}{}{}", prefix, ra_fixture, suffix)) + } + + #[test] + fn replace_filter_map_next_with_find_map2() { + check_diagnostics( + r#" + fn foo() { + let m = [1, 2, 3].iter().filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); + } //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ replace filter_map(..).next() with find_map(..) +"#, + ); + } + + #[test] + fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() { + check_diagnostics( + r#" +fn foo() { + let m = [1, 2, 3] + .iter() + .filter_map(|x| if *x == 2 { Some (4) } else { None }) + .len(); +} +"#, + ); + } + + #[test] + fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() { + check_diagnostics( + r#" +fn foo() { + let m = [1, 2, 3] + .iter() + .filter_map(|x| if *x == 2 { Some (4) } else { None }) + .map(|x| x + 2) + .len(); +} +"#, + ); + } + + #[test] + fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() { + check_diagnostics( + r#" +fn foo() { + let m = [1, 2, 3] + .iter() + .filter_map(|x| if *x == 2 { Some (4) } else { None }); + let n = m.next(); +} +"#, + ); + } + + #[test] + fn replace_with_wind_map() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::iter::Iterator; +use core::option::Option::{self, Some, None}; +fn foo() { + let m = [1, 2, 3].iter().$0filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); +} +//- /core/lib.rs crate:core +pub mod option { + pub enum Option { Some(T), None } +} +pub mod iter { + pub trait Iterator { + type Item; + fn filter_map(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option { FilterMap } + fn next(&mut self) -> Option; + } + pub struct FilterMap {} + impl Iterator for FilterMap { + type Item = i32; + fn next(&mut self) -> i32 { 7 } + } +} +"#, + r#" +use core::iter::Iterator; +use core::option::Option::{self, Some, None}; +fn foo() { + let m = [1, 2, 3].iter().find_map(|x| if *x == 2 { Some (4) } else { None }); +} +"#, + ) + } +} diff --git a/crates/ide_diagnostics/src/unimplemented_builtin_macro.rs b/crates/ide_diagnostics/src/unimplemented_builtin_macro.rs new file mode 100644 index 000000000..a600544f0 --- /dev/null +++ b/crates/ide_diagnostics/src/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(super) 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/crates/ide_diagnostics/src/unlinked_file.rs b/crates/ide_diagnostics/src/unlinked_file.rs new file mode 100644 index 000000000..424532e3a --- /dev/null +++ b/crates/ide_diagnostics/src/unlinked_file.rs @@ -0,0 +1,301 @@ +//! 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, ModuleItemOwner, NameOwner}, + AstNode, TextRange, TextSize, +}; +use text_edit::TextEdit; + +use crate::{fix, Assist, Diagnostic, DiagnosticsContext}; + +#[derive(Debug)] +pub(crate) struct UnlinkedFile { + pub(crate) file: FileId, +} + +// 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(super) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic { + // 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(d.file).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); + + Diagnostic::new("unlinked-file", "file not included in module tree", range) + .with_fixes(fixes(ctx, d)) +} + +fn fixes(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Option> { + // 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(d.file)); + let our_path = source_root.path_for_file(&d.file)?; + let module_name = our_path.name_and_extension()?.0; + + // Candidates to look for: + // - `mod.rs` in the same folder + // - we also check `main.rs` and `lib.rs` + // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id` + let parent = our_path.parent()?; + let mut paths = vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?]; + + // `submod/bla.rs` -> `submod.rs` + if let Some(newmod) = (|| { + let name = parent.name_and_extension()?.0; + parent.parent()?.join(&format!("{}.rs", name)) + })() { + paths.push(newmod); + } + + for path in &paths { + if let Some(parent_id) = 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, d.file); + } + } + } + } + } + + None +} + +fn make_fixes( + db: &RootDatabase, + parent_file_id: FileId, + new_mod_name: &str, + added_file_id: FileId, +) -> Option> { + 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(|item| is_outline_mod(item)) + .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_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/crates/ide_diagnostics/src/unresolved_extern_crate.rs b/crates/ide_diagnostics/src/unresolved_extern_crate.rs new file mode 100644 index 000000000..69f07d0b0 --- /dev/null +++ b/crates/ide_diagnostics/src/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(super) 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; +//^^^^^^^^^^^^^^^^^^^^^^^^^^ 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; +//^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate +// Should not error. +extern crate self as foo; +struct Foo; +use foo::Foo as Bar; +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/unresolved_import.rs b/crates/ide_diagnostics/src/unresolved_import.rs new file mode 100644 index 000000000..7779033d4 --- /dev/null +++ b/crates/ide_diagnostics/src/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(super) 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 = "../"]` 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; + //^^^^^^^^^^^^^^ 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}; + //^^^^^^^^^^^ unresolved import + +use {does_not_exist::*, does_exist}; + //^^^^^^^^^^^^^^^^^ unresolved import + +use does_not_exist::{ + a, + //^ unresolved import + b, + //^ unresolved import + c, + //^ 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; + //^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate + + // Should not error, since we already errored for the missing crate. + use doesnotexist::{self, bla, *}; + + use crate::doesnotexist; + //^^^^^^^^^^^^^^^^^^^ unresolved import +} + +mod m { + use super::doesnotexist; + //^^^^^^^^^^^^^^^^^^^ unresolved import +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/unresolved_macro_call.rs b/crates/ide_diagnostics/src/unresolved_macro_call.rs new file mode 100644 index 000000000..88133d0f3 --- /dev/null +++ b/crates/ide_diagnostics/src/unresolved_macro_call.rs @@ -0,0 +1,84 @@ +use hir::{db::AstDatabase, InFile}; +use syntax::{AstNode, SyntaxNodePtr}; + +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(super) fn unresolved_macro_call( + ctx: &DiagnosticsContext<'_>, + d: &hir::UnresolvedMacroCall, +) -> Diagnostic { + let last_path_segment = ctx.sema.db.parse_or_expand(d.macro_call.file_id).and_then(|root| { + d.macro_call + .value + .to_node(&root) + .path() + .and_then(|it| it.segment()) + .and_then(|it| it.name_ref()) + .map(|it| InFile::new(d.macro_call.file_id, SyntaxNodePtr::new(it.syntax()))) + }); + let diagnostics = last_path_segment.unwrap_or_else(|| d.macro_call.clone().map(|it| it.into())); + + Diagnostic::new( + "unresolved-macro-call", + format!("unresolved macro `{}!`", d.path), + ctx.sema.diagnostics_display_range(diagnostics).range, + ) + .experimental() +} + +#[cfg(test)] +mod tests { + use crate::tests::check_diagnostics; + + #[test] + fn unresolved_macro_diag() { + check_diagnostics( + r#" +fn f() { + m!(); +} //^ unresolved macro `m!` + +"#, + ); + } + + #[test] + fn test_unresolved_macro_range() { + check_diagnostics( + r#" +foo::bar!(92); + //^^^ unresolved macro `foo::bar!` +"#, + ); + } + + #[test] + fn unresolved_legacy_scope_macro() { + check_diagnostics( + r#" +macro_rules! m { () => {} } + +m!(); m2!(); + //^^ unresolved macro `self::m2!` +"#, + ); + } + + #[test] + fn unresolved_module_scope_macro() { + check_diagnostics( + r#" +mod mac { +#[macro_export] +macro_rules! m { () => {} } } + +self::m!(); self::m2!(); + //^^ unresolved macro `self::m2!` +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/unresolved_module.rs b/crates/ide_diagnostics/src/unresolved_module.rs new file mode 100644 index 000000000..b11e71b3e --- /dev/null +++ b/crates/ide_diagnostics/src/unresolved_module.rs @@ -0,0 +1,111 @@ +use hir::db::AstDatabase; +use ide_assists::Assist; +use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit}; +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(super) fn unresolved_module( + ctx: &DiagnosticsContext<'_>, + d: &hir::UnresolvedModule, +) -> Diagnostic { + Diagnostic::new( + "unresolved-module", + "unresolved module", + 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> { + let root = ctx.sema.db.parse_or_expand(d.decl.file_id)?; + let unresolved_module = d.decl.value.to_node(&root); + Some(vec![fix( + "create_module", + "Create module", + FileSystemEdit::CreateFile { + dst: AnchoredPathBuf { + anchor: d.decl.file_id.original_file(ctx.sema.db), + path: d.candidate.clone(), + }, + initial_contents: "".to_string(), + } + .into(), + unresolved_module.syntax().text_range(), + )]) +} + +#[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; +//^^^^^^^^ unresolved module +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", + range: 0..8, + severity: Error, + unused: false, + experimental: false, + fixes: Some( + [ + Assist { + id: AssistId( + "create_module", + QuickFix, + ), + label: "Create module", + 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, + }, + ), + }, + ], + ), + }, + ] + "#]], + ); + } +} diff --git a/crates/ide_diagnostics/src/unresolved_proc_macro.rs b/crates/ide_diagnostics/src/unresolved_proc_macro.rs new file mode 100644 index 000000000..744cce508 --- /dev/null +++ b/crates/ide_diagnostics/src/unresolved_proc_macro.rs @@ -0,0 +1,27 @@ +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.enable`). +pub(super) fn unresolved_proc_macro( + ctx: &DiagnosticsContext<'_>, + d: &hir::UnresolvedProcMacro, +) -> 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); + // FIXME: it would be nice to tell the user whether proc macros are currently disabled + let message = match &d.macro_name { + Some(name) => format!("proc macro `{}` not expanded", name), + None => "proc macro not expanded".to_string(), + }; + + Diagnostic::new("unresolved-proc-macro", message, display_range).severity(Severity::WeakWarning) +} -- cgit v1.2.3 From 2e8dab631b4ab429eeade7f5302e8de9dcd0b398 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 14 Jun 2021 13:18:03 +0300 Subject: internal: prepare to move assist definitions --- crates/ide_diagnostics/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'crates/ide_diagnostics') diff --git a/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs index a104a702d..0d98307a2 100644 --- a/crates/ide_diagnostics/src/lib.rs +++ b/crates/ide_diagnostics/src/lib.rs @@ -27,7 +27,6 @@ mod unresolved_proc_macro; mod field_shorthand; use hir::{diagnostics::AnyDiagnostic, Semantics}; -use ide_assists::AssistResolveStrategy; use ide_db::{ base_db::{FileId, SourceDatabase}, label::Label, @@ -43,7 +42,7 @@ use syntax::{ use text_edit::TextEdit; use unlinked_file::UnlinkedFile; -use ide_assists::{Assist, AssistId, AssistKind}; +use ide_assists::{Assist, AssistId, AssistKind, AssistResolveStrategy}; #[derive(Copy, Clone, Debug, PartialEq)] pub struct DiagnosticCode(pub &'static str); -- cgit v1.2.3 From a91071b57be6e64ad2fd277998ada0ae6206457b Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 14 Jun 2021 13:27:11 +0300 Subject: internal: cut deps between assists and diagnostics --- crates/ide_diagnostics/Cargo.toml | 1 - crates/ide_diagnostics/src/incorrect_case.rs | 3 +-- crates/ide_diagnostics/src/lib.rs | 5 ++--- crates/ide_diagnostics/src/missing_fields.rs | 3 +-- crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs | 3 +-- crates/ide_diagnostics/src/unresolved_module.rs | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) (limited to 'crates/ide_diagnostics') diff --git a/crates/ide_diagnostics/Cargo.toml b/crates/ide_diagnostics/Cargo.toml index 738fca14e..fa2adf212 100644 --- a/crates/ide_diagnostics/Cargo.toml +++ b/crates/ide_diagnostics/Cargo.toml @@ -22,7 +22,6 @@ 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" } -ide_assists = { path = "../ide_assists", version = "0.0.0" } [dev-dependencies] expect-test = "1.1" diff --git a/crates/ide_diagnostics/src/incorrect_case.rs b/crates/ide_diagnostics/src/incorrect_case.rs index 04fc779ce..8e1a93aa7 100644 --- a/crates/ide_diagnostics/src/incorrect_case.rs +++ b/crates/ide_diagnostics/src/incorrect_case.rs @@ -1,6 +1,5 @@ use hir::{db::AstDatabase, InFile}; -use ide_assists::Assist; -use ide_db::base_db::FilePosition; +use ide_db::{assists::Assist, base_db::FilePosition}; use syntax::AstNode; use crate::{ diff --git a/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs index 0d98307a2..2a16c73a8 100644 --- a/crates/ide_diagnostics/src/lib.rs +++ b/crates/ide_diagnostics/src/lib.rs @@ -28,6 +28,7 @@ mod field_shorthand; use hir::{diagnostics::AnyDiagnostic, Semantics}; use ide_db::{ + assists::{Assist, AssistId, AssistKind, AssistResolveStrategy}, base_db::{FileId, SourceDatabase}, label::Label, source_change::SourceChange, @@ -42,8 +43,6 @@ use syntax::{ use text_edit::TextEdit; use unlinked_file::UnlinkedFile; -use ide_assists::{Assist, AssistId, AssistKind, AssistResolveStrategy}; - #[derive(Copy, Clone, Debug, PartialEq)] pub struct DiagnosticCode(pub &'static str); @@ -265,8 +264,8 @@ fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist { #[cfg(test)] mod tests { use expect_test::Expect; - use ide_assists::AssistResolveStrategy; use ide_db::{ + assists::AssistResolveStrategy, base_db::{fixture::WithFixture, SourceDatabaseExt}, RootDatabase, }; diff --git a/crates/ide_diagnostics/src/missing_fields.rs b/crates/ide_diagnostics/src/missing_fields.rs index f242ee481..5af67f461 100644 --- a/crates/ide_diagnostics/src/missing_fields.rs +++ b/crates/ide_diagnostics/src/missing_fields.rs @@ -1,7 +1,6 @@ use either::Either; use hir::{db::AstDatabase, InFile}; -use ide_assists::Assist; -use ide_db::source_change::SourceChange; +use ide_db::{assists::Assist, source_change::SourceChange}; use stdx::format_to; use syntax::{algo, ast::make, AstNode, SyntaxNodePtr}; use text_edit::TextEdit; diff --git a/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs b/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs index 9e36ca296..01c79b6f5 100644 --- a/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs +++ b/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs @@ -1,6 +1,5 @@ use hir::db::AstDatabase; -use ide_assists::Assist; -use ide_db::source_change::SourceChange; +use ide_db::{assists::Assist, source_change::SourceChange}; use syntax::AstNode; use text_edit::TextEdit; diff --git a/crates/ide_diagnostics/src/unresolved_module.rs b/crates/ide_diagnostics/src/unresolved_module.rs index b11e71b3e..5aa9dae17 100644 --- a/crates/ide_diagnostics/src/unresolved_module.rs +++ b/crates/ide_diagnostics/src/unresolved_module.rs @@ -1,6 +1,5 @@ use hir::db::AstDatabase; -use ide_assists::Assist; -use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit}; +use ide_db::{assists::Assist, base_db::AnchoredPathBuf, source_change::FileSystemEdit}; use syntax::AstNode; use crate::{fix, Diagnostic, DiagnosticsContext}; -- cgit v1.2.3 From 26c978f258ed2af45a6979eefea9860c1eaeacda Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 14 Jun 2021 18:46:27 +0300 Subject: internal: adapt diagnostics to the new rename API --- crates/ide_diagnostics/src/incorrect_case.rs | 46 +++++++++------------------- 1 file changed, 15 insertions(+), 31 deletions(-) (limited to 'crates/ide_diagnostics') diff --git a/crates/ide_diagnostics/src/incorrect_case.rs b/crates/ide_diagnostics/src/incorrect_case.rs index 8e1a93aa7..2cf232d56 100644 --- a/crates/ide_diagnostics/src/incorrect_case.rs +++ b/crates/ide_diagnostics/src/incorrect_case.rs @@ -1,5 +1,5 @@ use hir::{db::AstDatabase, InFile}; -use ide_db::{assists::Assist, base_db::FilePosition}; +use ide_db::{assists::Assist, defs::NameClass}; use syntax::AstNode; use crate::{ @@ -27,35 +27,26 @@ pub(super) fn incorrect_case(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCas } fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option> { - if true { - return None; - } - 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(ctx.sema.db)?; let name_node = InFile::new(d.file, name_node.syntax()); let frange = name_node.original_file_range(ctx.sema.db); - let _file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; let label = format!("Rename to {}", d.suggested_text); - let res = unresolved_fix("change_case", &label, frange.range); + let mut res = unresolved_fix("change_case", &label, frange.range); if ctx.resolve.should_resolve(&res.id) { - //let source_change = rename_with_semantics(&ctx.sema, file_position, &d.suggested_text); - //res.source_change = Some(source_change.ok().unwrap_or_default()); - todo!() + let source_change = def.rename(&ctx.sema, &d.suggested_text); + res.source_change = Some(source_change.ok().unwrap_or_default()); } Some(vec![res]) } -#[cfg(TODO)] +#[cfg(test)] mod change_case { - use crate::{ - fixture, - tests::{check_diagnostics, check_fix}, - AssistResolveStrategy, DiagnosticsConfig, - }; + use crate::tests::{check_diagnostics, check_fix}; #[test] fn test_rename_incorrect_case() { @@ -123,7 +114,7 @@ fn some_fn() { check_diagnostics( r#" fn foo() { - const ANOTHER_ITEM$0: &str = "some_item"; + const ANOTHER_ITEM: &str = "some_item"; } "#, ); @@ -155,20 +146,13 @@ impl TestStruct { #[test] fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() { - let input = r#"fn FOO$0() {}"#; - let expected = r#"fn foo() {}"#; - - let (analysis, file_position) = fixture::position(input); - let diagnostics = analysis - .diagnostics( - &DiagnosticsConfig::default(), - AssistResolveStrategy::All, - file_position.file_id, - ) - .unwrap(); - assert_eq!(diagnostics.len(), 1); - - check_fix(input, expected); + check_diagnostics( + r#" +fn FOO() {} +// ^^^ Function `FOO` should have snake_case name, e.g. `foo` +"#, + ); + check_fix(r#"fn FOO$0() {}"#, r#"fn foo() {}"#); } #[test] -- cgit v1.2.3 From 4768e5fb23c058eba90f0a1dcd6e9d5c0ecdee1b Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Mon, 14 Jun 2021 19:32:39 +0300 Subject: internal: document diagnostics crate --- .../ide_diagnostics/src/break_outside_of_loop.rs | 30 - .../src/handlers/break_outside_of_loop.rs | 30 + .../ide_diagnostics/src/handlers/inactive_code.rs | 116 +++ .../ide_diagnostics/src/handlers/incorrect_case.rs | 479 +++++++++++ crates/ide_diagnostics/src/handlers/macro_error.rs | 173 ++++ .../src/handlers/mismatched_arg_count.rs | 272 ++++++ .../ide_diagnostics/src/handlers/missing_fields.rs | 326 ++++++++ .../src/handlers/missing_match_arms.rs | 929 +++++++++++++++++++++ .../handlers/missing_ok_or_some_in_tail_expr.rs | 229 +++++ .../ide_diagnostics/src/handlers/missing_unsafe.rs | 101 +++ .../ide_diagnostics/src/handlers/no_such_field.rs | 283 +++++++ .../src/handlers/remove_this_semicolon.rs | 61 ++ .../replace_filter_map_next_with_find_map.rs | 179 ++++ .../src/handlers/unimplemented_builtin_macro.rs | 16 + .../ide_diagnostics/src/handlers/unlinked_file.rs | 301 +++++++ .../src/handlers/unresolved_extern_crate.rs | 49 ++ .../src/handlers/unresolved_import.rs | 90 ++ .../src/handlers/unresolved_macro_call.rs | 84 ++ .../src/handlers/unresolved_module.rs | 110 +++ .../src/handlers/unresolved_proc_macro.rs | 27 + crates/ide_diagnostics/src/inactive_code.rs | 116 --- crates/ide_diagnostics/src/incorrect_case.rs | 479 ----------- crates/ide_diagnostics/src/lib.rs | 112 ++- crates/ide_diagnostics/src/macro_error.rs | 173 ---- crates/ide_diagnostics/src/mismatched_arg_count.rs | 272 ------ crates/ide_diagnostics/src/missing_fields.rs | 326 -------- crates/ide_diagnostics/src/missing_match_arms.rs | 929 --------------------- .../src/missing_ok_or_some_in_tail_expr.rs | 229 ----- crates/ide_diagnostics/src/missing_unsafe.rs | 101 --- crates/ide_diagnostics/src/no_such_field.rs | 283 ------- .../ide_diagnostics/src/remove_this_semicolon.rs | 61 -- .../src/replace_filter_map_next_with_find_map.rs | 179 ---- .../src/unimplemented_builtin_macro.rs | 16 - crates/ide_diagnostics/src/unlinked_file.rs | 301 ------- .../ide_diagnostics/src/unresolved_extern_crate.rs | 49 -- crates/ide_diagnostics/src/unresolved_import.rs | 90 -- .../ide_diagnostics/src/unresolved_macro_call.rs | 84 -- crates/ide_diagnostics/src/unresolved_module.rs | 110 --- .../ide_diagnostics/src/unresolved_proc_macro.rs | 27 - 39 files changed, 3922 insertions(+), 3900 deletions(-) delete mode 100644 crates/ide_diagnostics/src/break_outside_of_loop.rs create mode 100644 crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs create mode 100644 crates/ide_diagnostics/src/handlers/inactive_code.rs create mode 100644 crates/ide_diagnostics/src/handlers/incorrect_case.rs create mode 100644 crates/ide_diagnostics/src/handlers/macro_error.rs create mode 100644 crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs create mode 100644 crates/ide_diagnostics/src/handlers/missing_fields.rs create mode 100644 crates/ide_diagnostics/src/handlers/missing_match_arms.rs create mode 100644 crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs create mode 100644 crates/ide_diagnostics/src/handlers/missing_unsafe.rs create mode 100644 crates/ide_diagnostics/src/handlers/no_such_field.rs create mode 100644 crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs create mode 100644 crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs create mode 100644 crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs create mode 100644 crates/ide_diagnostics/src/handlers/unlinked_file.rs create mode 100644 crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs create mode 100644 crates/ide_diagnostics/src/handlers/unresolved_import.rs create mode 100644 crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs create mode 100644 crates/ide_diagnostics/src/handlers/unresolved_module.rs create mode 100644 crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs delete mode 100644 crates/ide_diagnostics/src/inactive_code.rs delete mode 100644 crates/ide_diagnostics/src/incorrect_case.rs delete mode 100644 crates/ide_diagnostics/src/macro_error.rs delete mode 100644 crates/ide_diagnostics/src/mismatched_arg_count.rs delete mode 100644 crates/ide_diagnostics/src/missing_fields.rs delete mode 100644 crates/ide_diagnostics/src/missing_match_arms.rs delete mode 100644 crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs delete mode 100644 crates/ide_diagnostics/src/missing_unsafe.rs delete mode 100644 crates/ide_diagnostics/src/no_such_field.rs delete mode 100644 crates/ide_diagnostics/src/remove_this_semicolon.rs delete mode 100644 crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs delete mode 100644 crates/ide_diagnostics/src/unimplemented_builtin_macro.rs delete mode 100644 crates/ide_diagnostics/src/unlinked_file.rs delete mode 100644 crates/ide_diagnostics/src/unresolved_extern_crate.rs delete mode 100644 crates/ide_diagnostics/src/unresolved_import.rs delete mode 100644 crates/ide_diagnostics/src/unresolved_macro_call.rs delete mode 100644 crates/ide_diagnostics/src/unresolved_module.rs delete mode 100644 crates/ide_diagnostics/src/unresolved_proc_macro.rs (limited to 'crates/ide_diagnostics') diff --git a/crates/ide_diagnostics/src/break_outside_of_loop.rs b/crates/ide_diagnostics/src/break_outside_of_loop.rs deleted file mode 100644 index 79e8cea37..000000000 --- a/crates/ide_diagnostics/src/break_outside_of_loop.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{Diagnostic, DiagnosticsContext}; - -// Diagnostic: break-outside-of-loop -// -// This diagnostic is triggered if the `break` keyword is used outside of a loop. -pub(super) 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; } - //^^^^^ break outside of loop -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs b/crates/ide_diagnostics/src/handlers/break_outside_of_loop.rs new file mode 100644 index 000000000..5ad0fbd1b --- /dev/null +++ b/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; } + //^^^^^ break outside of loop +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/inactive_code.rs b/crates/ide_diagnostics/src/handlers/inactive_code.rs new file mode 100644 index 000000000..4b722fd64 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/inactive_code.rs @@ -0,0 +1,116 @@ +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 { + // If there's inactive code somewhere in a macro, don't propagate to the call-site. + if d.node.file_id.expansion_info(ctx.sema.db).is_some() { + 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 { + 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 + //^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + #[cfg(a)] {} // Expression statement + //^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + #[cfg(a)] let x = 0; // let statement + //^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + + abc(#[cfg(a)] 0); + //^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + let x = Struct { + #[cfg(a)] f: 0, + //^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + }; + match () { + () => (), + #[cfg(a)] () => (), + //^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled + } + + #[cfg(a)] 0 // Trailing expression of block + //^^^^^^^^^^^ 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() {} + //^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no is disabled + + #[cfg(no)] #[cfg(no2)] mod m; + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no and no2 are disabled + + #[cfg(all(not(a), b))] enum E {} + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: b is disabled + + #[cfg(feature = "std")] use std; + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: feature = "std" is disabled +"#, + ); + } + + /// 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() {} + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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() {} + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no is disabled +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/incorrect_case.rs b/crates/ide_diagnostics/src/handlers/incorrect_case.rs new file mode 100644 index 000000000..3a33029cf --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/incorrect_case.rs @@ -0,0 +1,479 @@ +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> { + 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(ctx.sema.db)?; + + 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_a_weird_formatting = 10; + another_func(what_a_weird_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() {} +// ^^^ 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() {} +// ^^^^^^^^^^^^^^^^ 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) {} + // ^^^^^^^^^ Parameter `SomeParam` should have snake_case name, e.g. `some_param` + +fn foo2(ok_param: &str, CAPS_PARAM: u8) {} + // ^^^^^^^^^^ 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; + // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value` + let AnotherValue = 20; + // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value` +} +"#, + ); + } + + #[test] + fn incorrect_struct_names() { + check_diagnostics( + r#" +struct non_camel_case_name {} + // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName` + +struct SCREAMING_CASE {} + // ^^^^^^^^^^^^^^ 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 } + // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field` +"#, + ); + } + + #[test] + fn incorrect_enum_names() { + check_diagnostics( + r#" +enum some_enum { Val(u8) } + // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum` + +enum SOME_ENUM {} + // ^^^^^^^^^ 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) } + // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant` +"#, + ); + } + + #[test] + fn incorrect_const_name() { + check_diagnostics( + r#" +const some_weird_const: u8 = 10; + // ^^^^^^^^^^^^^^^^ 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; + // ^^^^^^^^^^^^^^^^ 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; + // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct` + +impl someStruct { + fn SomeFunc(&self) { + // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func` + let WHY_VAR_IS_CAPS = 10; + // ^^^^^^^^^^^^^^^ 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 => (), + // ^^^^^^^^ 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)] + +mod F { + fn CheckItWorksWithCrateAttr(BAD_NAME_HI: u8) {} +} + "#, + ); + } + + #[test] + #[ignore] + fn bug_trait_inside_fn() { + // FIXME: + // This is broken, and in fact, should not even be looked at by this + // lint in the first place. There's weird stuff going on in the + // collection phase. + // It's currently being brought in by: + // * validate_func on `a` recursing into modules + // * then it finds the trait and then the function while iterating + // through modules + // * then validate_func is called on Dirty + // * ... which then proceeds to look at some unknown module taking no + // attrs from either the impl or the fn a, and then finally to the root + // module + // + // It should find the attribute on the trait, but it *doesn't even see + // the trait* as far as I can tell. + + check_diagnostics( + r#" +trait T { fn a(); } +struct U {} +impl T for U { + fn a() { + // this comes out of bitflags, mostly + #[allow(non_snake_case)] + trait __BitFlags { + const HiImAlsoBad: u8 = 2; + #[inline] + 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] + #[ignore] + 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 { + // ^^^^^^^^^ Trait `BAD_TRAIT` should have CamelCase name, e.g. `BadTrait` + fn BAD_FUNCTION(); + // ^^^^^^^^^^^^ Function `BAD_FUNCTION` should have snake_case name, e.g. `bad_function` + fn BadFunction(); + // ^^^^^^^^^^^^ Function `BadFunction` should have snake_case name, e.g. `bad_function` +} + "#, + ); + } + + #[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/crates/ide_diagnostics/src/handlers/macro_error.rs b/crates/ide_diagnostics/src/handlers/macro_error.rs new file mode 100644 index 000000000..d4d928ad1 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/macro_error.rs @@ -0,0 +1,173 @@ +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 { + Diagnostic::new( + "macro-error", + d.message.clone(), + ctx.sema.diagnostics_display_range(d.node.clone()).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 { () => {} } + + include!("doesntexist"); +//^^^^^^^^^^^^^^^^^^^^^^^^ failed to load file `doesntexist` + "#, + ); + } + + #[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")); +//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `OUT_DIR` not set, enable "run 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); + //^^^^^^^^^^^^^^^^^ could not convert tokens + include!("does not exist"); + //^^^^^^^^^^^^^^^^^^^^^^^^^^ failed to load file `does not exist` + + env!(invalid); + //^^^^^^^^^^^^^ could not convert tokens + + env!("OUT_DIR"); + //^^^^^^^^^^^^^^^ `OUT_DIR` not set, enable "run build scripts" to fix + + compile_error!("compile_error works"); + //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ compile_error works + + // Lazy: + + format_args!(); + //^^^^^^^^^^^^^^ no rule matches input tokens +} +"#, + ); + } + + #[test] + fn macro_rules_diag() { + check_diagnostics( + r#" +macro_rules! m { + () => {}; +} +fn f() { + m!(); + + m!(hi); + //^^^^^^ 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!(); +} //^^^^^^^^ leftover tokens +"#, + ) + } +} diff --git a/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs b/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs new file mode 100644 index 000000000..ce313b2cc --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/mismatched_arg_count.rs @@ -0,0 +1,272 @@ +use crate::{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, + ctx.sema.diagnostics_display_range(d.call_expr.clone().map(|it| it.into())).range, + ) +} + +#[cfg(test)] +mod tests { + use crate::tests::check_diagnostics; + + #[test] + fn simple_free_fn_zero() { + check_diagnostics( + r#" +fn zero() {} +fn f() { zero(1); } + //^^^^^^^ 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(); } + //^^^^^ 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(); +} //^^^^^^^^^^^ 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(); + } //^^^^^^^^^^ 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); +} //^^^^^^ expected 2 arguments, found 1 +"#, + ) + } + + #[test] + fn enum_variant() { + check_diagnostics( + r#" +enum En { Variant(u8, u16), } +fn f() { + En::Variant(0); +} //^^^^^^^^^^^^^^ 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); + //^^^^^^^^^^^^^^ expected 1 argument, found 2 + Foo::Bar(); + //^^^^^^^^^^ 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); + //^^^^^^^^^^^ 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(); + //^^^ expected 1 argument, found 0 + f(()); + f((), ()); + //^^^^^^^^^ 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); + } +} + "#, + ) + } +} diff --git a/crates/ide_diagnostics/src/handlers/missing_fields.rs b/crates/ide_diagnostics/src/handlers/missing_fields.rs new file mode 100644 index 000000000..bc82c0e4a --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/missing_fields.rs @@ -0,0 +1,326 @@ +use either::Either; +use hir::{db::AstDatabase, InFile}; +use ide_db::{assists::Assist, source_change::SourceChange}; +use stdx::format_to; +use syntax::{algo, ast::make, AstNode, 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> { + // 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 field_list_parent = match &d.field_list_parent { + Either::Left(record_expr) => record_expr.to_node(&root), + // FIXE: patterns should be fixable as well. + Either::Right(_) => return None, + }; + let old_field_list = field_list_parent.record_expr_field_list()?; + let new_field_list = old_field_list.clone_for_update(); + for f in d.missed_fields.iter() { + let field = + make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())) + .clone_for_update(); + new_field_list.add_field(field); + } + + let edit = { + let mut builder = TextEdit::builder(); + algo::diff(old_field_list.syntax(), new_field_list.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(field_list_parent.syntax()).range, + )]) +} + +#[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; + //^ 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 } +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 range_mapping_out_of_macros() { + // FIXME: this is very wrong, but somewhat tricky to fix. + 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(, b: () ) {} +fn items() {} +fn here() {} + +macro_rules! id { ($($tt:tt)*) => { $($tt)*}; } + +fn main() { + let _x = id![Foo { a: 42 }]; +} + +pub struct Foo { pub a: i32, pub b: i32 } +"#, + ); + } + + #[test] + fn test_fill_struct_fields_empty() { + check_fix( + r#" +struct TestStruct { one: i32, two: i64 } + +fn test_fn() { + let s = TestStruct {$0}; +} +"#, + r#" +struct TestStruct { one: i32, two: i64 } + +fn test_fn() { + let s = TestStruct { one: (), two: () }; +} +"#, + ); + } + + #[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: () }; } +} +"#, + ); + } + + #[test] + fn test_fill_struct_fields_enum() { + check_fix( + r#" +enum Expr { + Bin { lhs: Box, rhs: Box } +} + +impl Expr { + fn new_bin(lhs: Box, rhs: Box) -> Expr { + Expr::Bin {$0 } + } +} +"#, + r#" +enum Expr { + Bin { lhs: Box, rhs: Box } +} + +impl Expr { + fn new_bin(lhs: Box, rhs: Box) -> 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: () }; +} +", + ); + } + + #[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: () }; +} +", + ); + } + + #[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: (), + b: (), + }; +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/missing_match_arms.rs b/crates/ide_diagnostics/src/handlers/missing_match_arms.rs new file mode 100644 index 000000000..9ea533d74 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/missing_match_arms.rs @@ -0,0 +1,929 @@ +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", + "missing match arm", + 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 () { } + //^^ missing match arm + match (()) { } + //^^^^ missing match arm + + match () { _ => (), } + match () { () => (), } + match (()) { (()) => (), } +} +"#, + ); + } + + #[test] + fn tuple_of_two_empty_tuple() { + check_diagnostics_no_bails( + r#" +fn main() { + match ((), ()) { } + //^^^^^^^^ missing match arm + + match ((), ()) { ((), ()) => (), } +} +"#, + ); + } + + #[test] + fn boolean() { + check_diagnostics_no_bails( + r#" +fn test_main() { + match false { } + //^^^^^ missing match arm + match false { true => (), } + //^^^^^ missing match arm + match (false, true) {} + //^^^^^^^^^^^^^ missing match arm + match (false, true) { (true, true) => (), } + //^^^^^^^^^^^^^ missing match arm + match (false, true) { + //^^^^^^^^^^^^^ missing match arm + (false, true) => (), + (false, false) => (), + (true, false) => (), + } + match (false, true) { (true, _x) => (), } + //^^^^^^^^^^^^^ missing match arm + + 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)) {} + //^^^^^^^^^^^^^^^^^^^^ missing match arm + match (false, ((), false)) { (true, ((), true)) => (), } + //^^^^^^^^^^^^^^^^^^^^ missing match arm + match (false, ((), false)) { (true, _) => (), } + //^^^^^^^^^^^^^^^^^^^^ missing match arm + + 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 { } + //^^^^^^^^^ missing match arm + match Either::B { Either::A => (), } + //^^^^^^^^^ missing match arm + + match &Either::B { + //^^^^^^^^^^ missing match arm + 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 { } + //^^^^^^^^^ missing match arm + match Either::B { + //^^^^^^^^^ missing match arm + 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) { + //^^^^^^^^^^^^^^^^ missing match arm + 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) => (), + } + 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 } { + //^^^^^^^^^^^^^^^^^^^^^ missing match arm + Either::A => (), + } + match loop { break Foo::A } { + Either::A => (), + Either::B => (), + } +} +"#, + ); + } + + #[test] + fn expr_partially_diverges() { + check_diagnostics_no_bails( + r#" +enum Either { 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 { } + //^ missing match arm + match a { Either::A { foo: true } => () } + //^ missing match arm + match a { + Either::A { } => (), + //^^^^^^^^^ Missing structure fields: + // | - foo + Either::B => (), + } + match a { + //^ missing match arm + Either::A { } => (), + } //^^^^^^^^^ 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 { + //^ missing match arm + 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 { + //^ missing match arm + Either::A { foo: true, .. } => (), + Either::B => (), + } + match a { + //^ missing match arm + 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 { + //^^^^^^^^^ missing match arm + Either::A(true, .., true) => (), + Either::A(true, .., false) => (), + Either::A(false, .., false) => (), + Either::B => (), + } + match Either::B { + //^^^^^^^^^ missing match arm + 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 {} + //^^^^^ missing match arm +} +fn bang(never: !) { + match never {} +} +"#, + ); + } + + #[test] + fn unknown_type() { + cov_mark::check_count!(validate_match_bailed_out, 1); + + check_diagnostics( + r#" +enum Option { Some(T), None } + +fn main() { + // `Never` is deliberately not defined so that it's an uninferred type. + match Option::::None { + None => (), + Some(never) => match never {}, + } + match Option::::None { + //^^^^^^^^^^^^^^^^^^^^^ missing match arm + 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) { + //^^^^^^^^^^^^^^^^^^^^ missing match arm + (false, ..) => (), + } +}"#, + ); + } + + #[test] + fn tuple_of_bools_with_ellipsis_at_beginning_missing_arm() { + check_diagnostics_no_bails( + r#" +fn main() { + match (false, true, false) { + //^^^^^^^^^^^^^^^^^^^^ missing match arm + (.., false) => (), + } +}"#, + ); + } + + #[test] + fn tuple_of_bools_with_ellipsis_in_middle_missing_arm() { + check_diagnostics_no_bails( + r#" +fn main() { + match (false, true, false) { + //^^^^^^^^^^^^^^^^^^^^ missing match arm + (true, .., false) => (), + } +}"#, + ); + } + + #[test] + fn record_struct() { + check_diagnostics_no_bails( + r#"struct Foo { a: bool } +fn main(f: Foo) { + match f {} + //^ missing match arm + match f { Foo { a: true } => () } + //^ missing match arm + match &f { Foo { a: true } => () } + //^^ missing match arm + 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 {} + //^ missing match arm + match f { Foo(true) => () } + //^ missing match arm + match f { + Foo(true) => (), + Foo(false) => (), + } +} +"#, + ); + } + + #[test] + fn unit_struct() { + check_diagnostics_no_bails( + r#"struct Foo; +fn main(f: Foo) { + match f {} + //^ missing match arm + 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, .. } => () } + //^ missing match arm + match f { + //^ missing match arm + 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 { + //^^^^^^^^^ missing match arm + 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 { } + //^ missing match arm +} +"#, + ); + } + + #[test] + fn binding() { + check_diagnostics_no_bails( + r#" +fn main() { + match true { + _x @ true => {} + false => {} + } + match true { _x @ true => {} } + //^^^^ missing match arm +} +"#, + ); + } + + #[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 { + //^^^^ missing match arm + E::A => {} + E::B => {} + } + match E::A { + //^^^^ missing match arm + E::A | E::B => {} + } +} +"#, + ); + } + + #[test] + fn match_guard() { + check_diagnostics_no_bails( + r#" +fn main() { + match true { + true if false => {} + true => {} + false => {} + } + match true { + //^^^^ missing match arm + true if false => {} + false => {} + } +} +"#, + ); + } + + #[test] + fn pattern_type_is_of_substitution() { + cov_mark::check!(match_check_wildcard_expanded_to_substitutions); + check_diagnostics_no_bails( + r#" +struct Foo(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 { A(T) } +fn main() { + match &Foo::A(true) { + _ => {} + Foo::A(_) => {} + } +} +"#, + ); + } + + 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/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs b/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs new file mode 100644 index 000000000..63de54570 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/missing_ok_or_some_in_tail_expr.rs @@ -0,0 +1,229 @@ +use hir::db::AstDatabase; +use ide_db::{assists::Assist, source_change::SourceChange}; +use syntax::AstNode; +use text_edit::TextEdit; + +use crate::{fix, Diagnostic, DiagnosticsContext}; + +// Diagnostic: missing-ok-or-some-in-tail-expr +// +// This diagnostic is triggered if a block that should return `Result` returns a value not wrapped in `Ok`, +// or if a block that should return `Option` returns a value not wrapped in `Some`. +// +// Example: +// +// ```rust +// fn foo() -> Result { +// 10 +// } +// ``` +pub(crate) fn missing_ok_or_some_in_tail_expr( + ctx: &DiagnosticsContext<'_>, + d: &hir::MissingOkOrSomeInTailExpr, +) -> Diagnostic { + Diagnostic::new( + "missing-ok-or-some-in-tail-expr", + format!("wrap return expression in {}", d.required), + ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, + ) + .with_fixes(fixes(ctx, d)) +} + +fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingOkOrSomeInTailExpr) -> Option> { + let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?; + let tail_expr = d.expr.value.to_node(&root); + let tail_expr_range = tail_expr.syntax().text_range(); + let replacement = format!("{}({})", d.required, tail_expr.syntax()); + let edit = TextEdit::replace(tail_expr_range, replacement); + let source_change = + SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit); + let name = if d.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; + Some(vec![fix("wrap_tail_expr", name, source_change, tail_expr_range)]) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_diagnostics, check_fix}; + + #[test] + fn test_wrap_return_type_option() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::option::Option::{self, Some, None}; + +fn div(x: i32, y: i32) -> Option { + if y == 0 { + return None; + } + x / y$0 +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::option::Option::{self, Some, None}; + +fn div(x: i32, y: i32) -> Option { + if y == 0 { + return None; + } + Some(x / y) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +fn div(x: i32, y: i32) -> Result { + if y == 0 { + return Err(()); + } + x / y$0 +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::result::Result::{self, Ok, Err}; + +fn div(x: i32, y: i32) -> Result { + if y == 0 { + return Err(()); + } + Ok(x / y) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_handles_generic_functions() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +fn div(x: T) -> Result { + if x == 0 { + return Err(7); + } + $0x +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::result::Result::{self, Ok, Err}; + +fn div(x: T) -> Result { + if x == 0 { + return Err(7); + } + Ok(x) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_handles_type_aliases() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +type MyResult = Result; + +fn div(x: i32, y: i32) -> MyResult { + if y == 0 { + return Err(()); + } + x $0/ y +} +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + r#" +use core::result::Result::{self, Ok, Err}; + +type MyResult = Result; + +fn div(x: i32, y: i32) -> MyResult { + if y == 0 { + return Err(()); + } + Ok(x / y) +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { + check_diagnostics( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +fn foo() -> Result<(), i32> { 0 } + +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + ); + } + + #[test] + fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() { + check_diagnostics( + r#" +//- /main.rs crate:main deps:core +use core::result::Result::{self, Ok, Err}; + +enum SomeOtherEnum { Ok(i32), Err(String) } + +fn foo() -> SomeOtherEnum { 0 } + +//- /core/lib.rs crate:core +pub mod result { + pub enum Result { Ok(T), Err(E) } +} +pub mod option { + pub enum Option { Some(T), None } +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/missing_unsafe.rs b/crates/ide_diagnostics/src/handlers/missing_unsafe.rs new file mode 100644 index 000000000..62d8687ba --- /dev/null +++ b/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; +} //^^ 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(); + //^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block + HasUnsafe.unsafe_fn(); + //^^^^^^^^^^^^^^^^^^^^^ 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; + //^^^^^^^^^^ 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); + //^^^^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/no_such_field.rs b/crates/ide_diagnostics/src/handlers/no_such_field.rs new file mode 100644 index 000000000..e4cc8a840 --- /dev/null +++ b/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> { + 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, + usage_file_id: FileId, + record_expr_field: &ast::RecordExprField, +) -> Option> { + 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()?)?; + 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 { + 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 { + //^ Missing structure fields: + //| - bar + foo: 92, + baz: 62, + //^^^^^^^ 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/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs b/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs new file mode 100644 index 000000000..b52e4dc84 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/remove_this_semicolon.rs @@ -0,0 +1,61 @@ +use hir::db::AstDatabase; +use ide_db::source_change::SourceChange; +use syntax::{ast, AstNode}; +use text_edit::TextEdit; + +use crate::{fix, Assist, Diagnostic, DiagnosticsContext}; + +// Diagnostic: remove-this-semicolon +// +// This diagnostic is triggered when there's an erroneous `;` at the end of the block. +pub(crate) fn remove_this_semicolon( + ctx: &DiagnosticsContext<'_>, + d: &hir::RemoveThisSemicolon, +) -> Diagnostic { + Diagnostic::new( + "remove-this-semicolon", + "remove this semicolon", + ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, + ) + .with_fixes(fixes(ctx, d)) +} + +fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::RemoveThisSemicolon) -> Option> { + let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?; + + let semicolon = d + .expr + .value + .to_node(&root) + .syntax() + .parent() + .and_then(ast::ExprStmt::cast) + .and_then(|expr| expr.semicolon_token())? + .text_range(); + + let edit = TextEdit::delete(semicolon); + let source_change = + SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit); + + Some(vec![fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)]) +} + +#[cfg(test)] +mod tests { + use crate::tests::{check_diagnostics, check_fix}; + + #[test] + fn missing_semicolon() { + check_diagnostics( + r#" +fn test() -> i32 { 123; } + //^^^ remove this semicolon +"#, + ); + } + + #[test] + fn remove_semicolon() { + check_fix(r#"fn f() -> i32 { 92$0; }"#, r#"fn f() -> i32 { 92 }"#); + } +} diff --git a/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs b/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs new file mode 100644 index 000000000..10d5da15d --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/replace_filter_map_next_with_find_map.rs @@ -0,0 +1,179 @@ +use hir::{db::AstDatabase, InFile}; +use ide_db::source_change::SourceChange; +use syntax::{ + ast::{self, ArgListOwner}, + 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> { + 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_fix; + + // Register the required standard library types to make the tests work + #[track_caller] + fn check_diagnostics(ra_fixture: &str) { + let prefix = r#" +//- /main.rs crate:main deps:core +use core::iter::Iterator; +use core::option::Option::{self, Some, None}; +"#; + let suffix = r#" +//- /core/lib.rs crate:core +pub mod option { + pub enum Option { Some(T), None } +} +pub mod iter { + pub trait Iterator { + type Item; + fn filter_map(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option { FilterMap } + fn next(&mut self) -> Option; + } + pub struct FilterMap {} + impl Iterator for FilterMap { + type Item = i32; + fn next(&mut self) -> i32 { 7 } + } +} +"#; + crate::tests::check_diagnostics(&format!("{}{}{}", prefix, ra_fixture, suffix)) + } + + #[test] + fn replace_filter_map_next_with_find_map2() { + check_diagnostics( + r#" + fn foo() { + let m = [1, 2, 3].iter().filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); + } //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ replace filter_map(..).next() with find_map(..) +"#, + ); + } + + #[test] + fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() { + check_diagnostics( + r#" +fn foo() { + let m = [1, 2, 3] + .iter() + .filter_map(|x| if *x == 2 { Some (4) } else { None }) + .len(); +} +"#, + ); + } + + #[test] + fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() { + check_diagnostics( + r#" +fn foo() { + let m = [1, 2, 3] + .iter() + .filter_map(|x| if *x == 2 { Some (4) } else { None }) + .map(|x| x + 2) + .len(); +} +"#, + ); + } + + #[test] + fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() { + check_diagnostics( + r#" +fn foo() { + let m = [1, 2, 3] + .iter() + .filter_map(|x| if *x == 2 { Some (4) } else { None }); + let n = m.next(); +} +"#, + ); + } + + #[test] + fn replace_with_wind_map() { + check_fix( + r#" +//- /main.rs crate:main deps:core +use core::iter::Iterator; +use core::option::Option::{self, Some, None}; +fn foo() { + let m = [1, 2, 3].iter().$0filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); +} +//- /core/lib.rs crate:core +pub mod option { + pub enum Option { Some(T), None } +} +pub mod iter { + pub trait Iterator { + type Item; + fn filter_map(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option { FilterMap } + fn next(&mut self) -> Option; + } + pub struct FilterMap {} + impl Iterator for FilterMap { + type Item = i32; + fn next(&mut self) -> i32 { 7 } + } +} +"#, + r#" +use core::iter::Iterator; +use core::option::Option::{self, Some, None}; +fn foo() { + let m = [1, 2, 3].iter().find_map(|x| if *x == 2 { Some (4) } else { None }); +} +"#, + ) + } +} diff --git a/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs b/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs new file mode 100644 index 000000000..e879de75c --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/unimplemented_builtin_macro.rs @@ -0,0 +1,16 @@ +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/crates/ide_diagnostics/src/handlers/unlinked_file.rs b/crates/ide_diagnostics/src/handlers/unlinked_file.rs new file mode 100644 index 000000000..8921ddde2 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/unlinked_file.rs @@ -0,0 +1,301 @@ +//! 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, ModuleItemOwner, NameOwner}, + AstNode, TextRange, TextSize, +}; +use text_edit::TextEdit; + +use crate::{fix, Assist, Diagnostic, DiagnosticsContext}; + +#[derive(Debug)] +pub(crate) struct UnlinkedFile { + pub(crate) file: FileId, +} + +// 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, d: &UnlinkedFile) -> Diagnostic { + // 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(d.file).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); + + Diagnostic::new("unlinked-file", "file not included in module tree", range) + .with_fixes(fixes(ctx, d)) +} + +fn fixes(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Option> { + // 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(d.file)); + let our_path = source_root.path_for_file(&d.file)?; + let module_name = our_path.name_and_extension()?.0; + + // Candidates to look for: + // - `mod.rs` in the same folder + // - we also check `main.rs` and `lib.rs` + // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id` + let parent = our_path.parent()?; + let mut paths = vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?]; + + // `submod/bla.rs` -> `submod.rs` + if let Some(newmod) = (|| { + let name = parent.name_and_extension()?.0; + parent.parent()?.join(&format!("{}.rs", name)) + })() { + paths.push(newmod); + } + + for path in &paths { + if let Some(parent_id) = 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, d.file); + } + } + } + } + } + + None +} + +fn make_fixes( + db: &RootDatabase, + parent_file_id: FileId, + new_mod_name: &str, + added_file_id: FileId, +) -> Option> { + 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(|item| is_outline_mod(item)) + .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_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/crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs b/crates/ide_diagnostics/src/handlers/unresolved_extern_crate.rs new file mode 100644 index 000000000..f5313cc0c --- /dev/null +++ b/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; +//^^^^^^^^^^^^^^^^^^^^^^^^^^ 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; +//^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate +// Should not error. +extern crate self as foo; +struct Foo; +use foo::Foo as Bar; +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/unresolved_import.rs b/crates/ide_diagnostics/src/handlers/unresolved_import.rs new file mode 100644 index 000000000..f30051c12 --- /dev/null +++ b/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 = "../"]` 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; + //^^^^^^^^^^^^^^ 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}; + //^^^^^^^^^^^ unresolved import + +use {does_not_exist::*, does_exist}; + //^^^^^^^^^^^^^^^^^ unresolved import + +use does_not_exist::{ + a, + //^ unresolved import + b, + //^ unresolved import + c, + //^ 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; + //^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate + + // Should not error, since we already errored for the missing crate. + use doesnotexist::{self, bla, *}; + + use crate::doesnotexist; + //^^^^^^^^^^^^^^^^^^^ unresolved import +} + +mod m { + use super::doesnotexist; + //^^^^^^^^^^^^^^^^^^^ unresolved import +} +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs b/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs new file mode 100644 index 000000000..4c3c1c19a --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/unresolved_macro_call.rs @@ -0,0 +1,84 @@ +use hir::{db::AstDatabase, InFile}; +use syntax::{AstNode, SyntaxNodePtr}; + +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 { + let last_path_segment = ctx.sema.db.parse_or_expand(d.macro_call.file_id).and_then(|root| { + d.macro_call + .value + .to_node(&root) + .path() + .and_then(|it| it.segment()) + .and_then(|it| it.name_ref()) + .map(|it| InFile::new(d.macro_call.file_id, SyntaxNodePtr::new(it.syntax()))) + }); + let diagnostics = last_path_segment.unwrap_or_else(|| d.macro_call.clone().map(|it| it.into())); + + Diagnostic::new( + "unresolved-macro-call", + format!("unresolved macro `{}!`", d.path), + ctx.sema.diagnostics_display_range(diagnostics).range, + ) + .experimental() +} + +#[cfg(test)] +mod tests { + use crate::tests::check_diagnostics; + + #[test] + fn unresolved_macro_diag() { + check_diagnostics( + r#" +fn f() { + m!(); +} //^ unresolved macro `m!` + +"#, + ); + } + + #[test] + fn test_unresolved_macro_range() { + check_diagnostics( + r#" +foo::bar!(92); + //^^^ unresolved macro `foo::bar!` +"#, + ); + } + + #[test] + fn unresolved_legacy_scope_macro() { + check_diagnostics( + r#" +macro_rules! m { () => {} } + +m!(); m2!(); + //^^ unresolved macro `self::m2!` +"#, + ); + } + + #[test] + fn unresolved_module_scope_macro() { + check_diagnostics( + r#" +mod mac { +#[macro_export] +macro_rules! m { () => {} } } + +self::m!(); self::m2!(); + //^^ unresolved macro `self::m2!` +"#, + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/unresolved_module.rs b/crates/ide_diagnostics/src/handlers/unresolved_module.rs new file mode 100644 index 000000000..17166a0c6 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/unresolved_module.rs @@ -0,0 +1,110 @@ +use hir::db::AstDatabase; +use ide_db::{assists::Assist, base_db::AnchoredPathBuf, source_change::FileSystemEdit}; +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", + "unresolved module", + 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> { + let root = ctx.sema.db.parse_or_expand(d.decl.file_id)?; + let unresolved_module = d.decl.value.to_node(&root); + Some(vec![fix( + "create_module", + "Create module", + FileSystemEdit::CreateFile { + dst: AnchoredPathBuf { + anchor: d.decl.file_id.original_file(ctx.sema.db), + path: d.candidate.clone(), + }, + initial_contents: "".to_string(), + } + .into(), + unresolved_module.syntax().text_range(), + )]) +} + +#[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; +//^^^^^^^^ unresolved module +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", + range: 0..8, + severity: Error, + unused: false, + experimental: false, + fixes: Some( + [ + Assist { + id: AssistId( + "create_module", + QuickFix, + ), + label: "Create module", + 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, + }, + ), + }, + ], + ), + }, + ] + "#]], + ); + } +} diff --git a/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs b/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs new file mode 100644 index 000000000..fde1d1323 --- /dev/null +++ b/crates/ide_diagnostics/src/handlers/unresolved_proc_macro.rs @@ -0,0 +1,27 @@ +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.enable`). +pub(crate) fn unresolved_proc_macro( + ctx: &DiagnosticsContext<'_>, + d: &hir::UnresolvedProcMacro, +) -> 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); + // FIXME: it would be nice to tell the user whether proc macros are currently disabled + let message = match &d.macro_name { + Some(name) => format!("proc macro `{}` not expanded", name), + None => "proc macro not expanded".to_string(), + }; + + Diagnostic::new("unresolved-proc-macro", message, display_range).severity(Severity::WeakWarning) +} diff --git a/crates/ide_diagnostics/src/inactive_code.rs b/crates/ide_diagnostics/src/inactive_code.rs deleted file mode 100644 index 34837cc0d..000000000 --- a/crates/ide_diagnostics/src/inactive_code.rs +++ /dev/null @@ -1,116 +0,0 @@ -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(super) fn inactive_code( - ctx: &DiagnosticsContext<'_>, - d: &hir::InactiveCode, -) -> Option { - // If there's inactive code somewhere in a macro, don't propagate to the call-site. - if d.node.file_id.expansion_info(ctx.sema.db).is_some() { - 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 { - 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 - //^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled - #[cfg(a)] {} // Expression statement - //^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled - #[cfg(a)] let x = 0; // let statement - //^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled - - abc(#[cfg(a)] 0); - //^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled - let x = Struct { - #[cfg(a)] f: 0, - //^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled - }; - match () { - () => (), - #[cfg(a)] () => (), - //^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: a is disabled - } - - #[cfg(a)] 0 // Trailing expression of block - //^^^^^^^^^^^ 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() {} - //^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no is disabled - - #[cfg(no)] #[cfg(no2)] mod m; - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no and no2 are disabled - - #[cfg(all(not(a), b))] enum E {} - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: b is disabled - - #[cfg(feature = "std")] use std; - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: feature = "std" is disabled -"#, - ); - } - - /// 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() {} - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 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() {} - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ code is inactive due to #[cfg] directives: no is disabled -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/incorrect_case.rs b/crates/ide_diagnostics/src/incorrect_case.rs deleted file mode 100644 index 2cf232d56..000000000 --- a/crates/ide_diagnostics/src/incorrect_case.rs +++ /dev/null @@ -1,479 +0,0 @@ -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(super) 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> { - 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(ctx.sema.db)?; - - 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_a_weird_formatting = 10; - another_func(what_a_weird_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() {} -// ^^^ 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() {} -// ^^^^^^^^^^^^^^^^ 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) {} - // ^^^^^^^^^ Parameter `SomeParam` should have snake_case name, e.g. `some_param` - -fn foo2(ok_param: &str, CAPS_PARAM: u8) {} - // ^^^^^^^^^^ 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; - // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value` - let AnotherValue = 20; - // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value` -} -"#, - ); - } - - #[test] - fn incorrect_struct_names() { - check_diagnostics( - r#" -struct non_camel_case_name {} - // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName` - -struct SCREAMING_CASE {} - // ^^^^^^^^^^^^^^ 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 } - // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field` -"#, - ); - } - - #[test] - fn incorrect_enum_names() { - check_diagnostics( - r#" -enum some_enum { Val(u8) } - // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum` - -enum SOME_ENUM {} - // ^^^^^^^^^ 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) } - // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant` -"#, - ); - } - - #[test] - fn incorrect_const_name() { - check_diagnostics( - r#" -const some_weird_const: u8 = 10; - // ^^^^^^^^^^^^^^^^ 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; - // ^^^^^^^^^^^^^^^^ 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; - // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct` - -impl someStruct { - fn SomeFunc(&self) { - // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func` - let WHY_VAR_IS_CAPS = 10; - // ^^^^^^^^^^^^^^^ 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 => (), - // ^^^^^^^^ 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)] - -mod F { - fn CheckItWorksWithCrateAttr(BAD_NAME_HI: u8) {} -} - "#, - ); - } - - #[test] - #[ignore] - fn bug_trait_inside_fn() { - // FIXME: - // This is broken, and in fact, should not even be looked at by this - // lint in the first place. There's weird stuff going on in the - // collection phase. - // It's currently being brought in by: - // * validate_func on `a` recursing into modules - // * then it finds the trait and then the function while iterating - // through modules - // * then validate_func is called on Dirty - // * ... which then proceeds to look at some unknown module taking no - // attrs from either the impl or the fn a, and then finally to the root - // module - // - // It should find the attribute on the trait, but it *doesn't even see - // the trait* as far as I can tell. - - check_diagnostics( - r#" -trait T { fn a(); } -struct U {} -impl T for U { - fn a() { - // this comes out of bitflags, mostly - #[allow(non_snake_case)] - trait __BitFlags { - const HiImAlsoBad: u8 = 2; - #[inline] - 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] - #[ignore] - 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 { - // ^^^^^^^^^ Trait `BAD_TRAIT` should have CamelCase name, e.g. `BadTrait` - fn BAD_FUNCTION(); - // ^^^^^^^^^^^^ Function `BAD_FUNCTION` should have snake_case name, e.g. `bad_function` - fn BadFunction(); - // ^^^^^^^^^^^^ Function `BadFunction` should have snake_case name, e.g. `bad_function` -} - "#, - ); - } - - #[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/crates/ide_diagnostics/src/lib.rs b/crates/ide_diagnostics/src/lib.rs index 2a16c73a8..88037be5a 100644 --- a/crates/ide_diagnostics/src/lib.rs +++ b/crates/ide_diagnostics/src/lib.rs @@ -1,28 +1,49 @@ -//! Collects diagnostics & fixits for a single file. +//! Diagnostics rendering and fixits. //! -//! The tricky bit here is that diagnostics are produced by hir in terms of -//! macro-expanded files, but we need to present them to the users in terms of -//! original files. So we need to map the ranges. - -mod break_outside_of_loop; -mod inactive_code; -mod incorrect_case; -mod macro_error; -mod mismatched_arg_count; -mod missing_fields; -mod missing_match_arms; -mod missing_ok_or_some_in_tail_expr; -mod missing_unsafe; -mod no_such_field; -mod remove_this_semicolon; -mod replace_filter_map_next_with_find_map; -mod unimplemented_builtin_macro; -mod unlinked_file; -mod unresolved_extern_crate; -mod unresolved_import; -mod unresolved_macro_call; -mod unresolved_module; -mod unresolved_proc_macro; +//! 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. + +mod handlers { + pub(crate) mod break_outside_of_loop; + pub(crate) mod inactive_code; + pub(crate) mod incorrect_case; + pub(crate) mod macro_error; + pub(crate) mod mismatched_arg_count; + pub(crate) mod missing_fields; + pub(crate) mod missing_match_arms; + pub(crate) mod missing_ok_or_some_in_tail_expr; + pub(crate) mod missing_unsafe; + pub(crate) mod no_such_field; + pub(crate) mod remove_this_semicolon; + pub(crate) mod replace_filter_map_next_with_find_map; + pub(crate) mod unimplemented_builtin_macro; + pub(crate) mod unlinked_file; + 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; +} mod field_shorthand; @@ -41,7 +62,8 @@ use syntax::{ SyntaxNode, TextRange, }; use text_edit::TextEdit; -use unlinked_file::UnlinkedFile; + +use crate::handlers::unlinked_file::UnlinkedFile; #[derive(Copy, Clone, Debug, PartialEq)] pub struct DiagnosticCode(pub &'static str); @@ -148,32 +170,32 @@ pub fn diagnostics( let ctx = DiagnosticsContext { config, sema, resolve }; if module.is_none() { let d = UnlinkedFile { file: file_id }; - let d = unlinked_file::unlinked_file(&ctx, &d); + let d = handlers::unlinked_file::unlinked_file(&ctx, &d); res.push(d) } for diag in diags { #[rustfmt::skip] let d = match diag { - AnyDiagnostic::BreakOutsideOfLoop(d) => break_outside_of_loop::break_outside_of_loop(&ctx, &d), - AnyDiagnostic::IncorrectCase(d) => incorrect_case::incorrect_case(&ctx, &d), - AnyDiagnostic::MacroError(d) => macro_error::macro_error(&ctx, &d), - AnyDiagnostic::MismatchedArgCount(d) => mismatched_arg_count::mismatched_arg_count(&ctx, &d), - AnyDiagnostic::MissingFields(d) => missing_fields::missing_fields(&ctx, &d), - AnyDiagnostic::MissingMatchArms(d) => missing_match_arms::missing_match_arms(&ctx, &d), - AnyDiagnostic::MissingOkOrSomeInTailExpr(d) => missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&ctx, &d), - AnyDiagnostic::MissingUnsafe(d) => missing_unsafe::missing_unsafe(&ctx, &d), - AnyDiagnostic::NoSuchField(d) => no_such_field::no_such_field(&ctx, &d), - AnyDiagnostic::RemoveThisSemicolon(d) => remove_this_semicolon::remove_this_semicolon(&ctx, &d), - AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d), - AnyDiagnostic::UnimplementedBuiltinMacro(d) => unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d), - AnyDiagnostic::UnresolvedExternCrate(d) => unresolved_extern_crate::unresolved_extern_crate(&ctx, &d), - AnyDiagnostic::UnresolvedImport(d) => unresolved_import::unresolved_import(&ctx, &d), - AnyDiagnostic::UnresolvedMacroCall(d) => unresolved_macro_call::unresolved_macro_call(&ctx, &d), - AnyDiagnostic::UnresolvedModule(d) => unresolved_module::unresolved_module(&ctx, &d), - AnyDiagnostic::UnresolvedProcMacro(d) => unresolved_proc_macro::unresolved_proc_macro(&ctx, &d), - - AnyDiagnostic::InactiveCode(d) => match inactive_code::inactive_code(&ctx, &d) { + 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::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::MissingOkOrSomeInTailExpr(d) => handlers::missing_ok_or_some_in_tail_expr::missing_ok_or_some_in_tail_expr(&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::RemoveThisSemicolon(d) => handlers::remove_this_semicolon::remove_this_semicolon(&ctx, &d), + AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&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), + + AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) { Some(it) => it, None => continue, } diff --git a/crates/ide_diagnostics/src/macro_error.rs b/crates/ide_diagnostics/src/macro_error.rs deleted file mode 100644 index 180f297eb..000000000 --- a/crates/ide_diagnostics/src/macro_error.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::{Diagnostic, DiagnosticsContext}; - -// Diagnostic: macro-error -// -// This diagnostic is shown for macro expansion errors. -pub(super) fn macro_error(ctx: &DiagnosticsContext<'_>, d: &hir::MacroError) -> Diagnostic { - Diagnostic::new( - "macro-error", - d.message.clone(), - ctx.sema.diagnostics_display_range(d.node.clone()).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 { () => {} } - - include!("doesntexist"); -//^^^^^^^^^^^^^^^^^^^^^^^^ failed to load file `doesntexist` - "#, - ); - } - - #[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")); -//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `OUT_DIR` not set, enable "run 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); - //^^^^^^^^^^^^^^^^^ could not convert tokens - include!("does not exist"); - //^^^^^^^^^^^^^^^^^^^^^^^^^^ failed to load file `does not exist` - - env!(invalid); - //^^^^^^^^^^^^^ could not convert tokens - - env!("OUT_DIR"); - //^^^^^^^^^^^^^^^ `OUT_DIR` not set, enable "run build scripts" to fix - - compile_error!("compile_error works"); - //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ compile_error works - - // Lazy: - - format_args!(); - //^^^^^^^^^^^^^^ no rule matches input tokens -} -"#, - ); - } - - #[test] - fn macro_rules_diag() { - check_diagnostics( - r#" -macro_rules! m { - () => {}; -} -fn f() { - m!(); - - m!(hi); - //^^^^^^ 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!(); -} //^^^^^^^^ leftover tokens -"#, - ) - } -} diff --git a/crates/ide_diagnostics/src/mismatched_arg_count.rs b/crates/ide_diagnostics/src/mismatched_arg_count.rs deleted file mode 100644 index c5749c8a6..000000000 --- a/crates/ide_diagnostics/src/mismatched_arg_count.rs +++ /dev/null @@ -1,272 +0,0 @@ -use crate::{Diagnostic, DiagnosticsContext}; - -// Diagnostic: mismatched-arg-count -// -// This diagnostic is triggered if a function is invoked with an incorrect amount of arguments. -pub(super) 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, - ctx.sema.diagnostics_display_range(d.call_expr.clone().map(|it| it.into())).range, - ) -} - -#[cfg(test)] -mod tests { - use crate::tests::check_diagnostics; - - #[test] - fn simple_free_fn_zero() { - check_diagnostics( - r#" -fn zero() {} -fn f() { zero(1); } - //^^^^^^^ 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(); } - //^^^^^ 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(); -} //^^^^^^^^^^^ 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(); - } //^^^^^^^^^^ 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); -} //^^^^^^ expected 2 arguments, found 1 -"#, - ) - } - - #[test] - fn enum_variant() { - check_diagnostics( - r#" -enum En { Variant(u8, u16), } -fn f() { - En::Variant(0); -} //^^^^^^^^^^^^^^ 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); - //^^^^^^^^^^^^^^ expected 1 argument, found 2 - Foo::Bar(); - //^^^^^^^^^^ 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); - //^^^^^^^^^^^ 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(); - //^^^ expected 1 argument, found 0 - f(()); - f((), ()); - //^^^^^^^^^ 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); - } -} - "#, - ) - } -} diff --git a/crates/ide_diagnostics/src/missing_fields.rs b/crates/ide_diagnostics/src/missing_fields.rs deleted file mode 100644 index 5af67f461..000000000 --- a/crates/ide_diagnostics/src/missing_fields.rs +++ /dev/null @@ -1,326 +0,0 @@ -use either::Either; -use hir::{db::AstDatabase, InFile}; -use ide_db::{assists::Assist, source_change::SourceChange}; -use stdx::format_to; -use syntax::{algo, ast::make, AstNode, 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(super) 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> { - // 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 field_list_parent = match &d.field_list_parent { - Either::Left(record_expr) => record_expr.to_node(&root), - // FIXE: patterns should be fixable as well. - Either::Right(_) => return None, - }; - let old_field_list = field_list_parent.record_expr_field_list()?; - let new_field_list = old_field_list.clone_for_update(); - for f in d.missed_fields.iter() { - let field = - make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit())) - .clone_for_update(); - new_field_list.add_field(field); - } - - let edit = { - let mut builder = TextEdit::builder(); - algo::diff(old_field_list.syntax(), new_field_list.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(field_list_parent.syntax()).range, - )]) -} - -#[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; - //^ 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 } -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 range_mapping_out_of_macros() { - // FIXME: this is very wrong, but somewhat tricky to fix. - 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(, b: () ) {} -fn items() {} -fn here() {} - -macro_rules! id { ($($tt:tt)*) => { $($tt)*}; } - -fn main() { - let _x = id![Foo { a: 42 }]; -} - -pub struct Foo { pub a: i32, pub b: i32 } -"#, - ); - } - - #[test] - fn test_fill_struct_fields_empty() { - check_fix( - r#" -struct TestStruct { one: i32, two: i64 } - -fn test_fn() { - let s = TestStruct {$0}; -} -"#, - r#" -struct TestStruct { one: i32, two: i64 } - -fn test_fn() { - let s = TestStruct { one: (), two: () }; -} -"#, - ); - } - - #[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: () }; } -} -"#, - ); - } - - #[test] - fn test_fill_struct_fields_enum() { - check_fix( - r#" -enum Expr { - Bin { lhs: Box, rhs: Box } -} - -impl Expr { - fn new_bin(lhs: Box, rhs: Box) -> Expr { - Expr::Bin {$0 } - } -} -"#, - r#" -enum Expr { - Bin { lhs: Box, rhs: Box } -} - -impl Expr { - fn new_bin(lhs: Box, rhs: Box) -> 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: () }; -} -", - ); - } - - #[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: () }; -} -", - ); - } - - #[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: (), - b: (), - }; -} -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/missing_match_arms.rs b/crates/ide_diagnostics/src/missing_match_arms.rs deleted file mode 100644 index c83155d2f..000000000 --- a/crates/ide_diagnostics/src/missing_match_arms.rs +++ /dev/null @@ -1,929 +0,0 @@ -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(super) fn missing_match_arms( - ctx: &DiagnosticsContext<'_>, - d: &hir::MissingMatchArms, -) -> Diagnostic { - Diagnostic::new( - "missing-match-arm", - "missing match arm", - ctx.sema.diagnostics_display_range(InFile::new(d.file, d.match_expr.clone().into())).range, - ) -} - -#[cfg(test)] -pub(super) 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 () { } - //^^ missing match arm - match (()) { } - //^^^^ missing match arm - - match () { _ => (), } - match () { () => (), } - match (()) { (()) => (), } -} -"#, - ); - } - - #[test] - fn tuple_of_two_empty_tuple() { - check_diagnostics_no_bails( - r#" -fn main() { - match ((), ()) { } - //^^^^^^^^ missing match arm - - match ((), ()) { ((), ()) => (), } -} -"#, - ); - } - - #[test] - fn boolean() { - check_diagnostics_no_bails( - r#" -fn test_main() { - match false { } - //^^^^^ missing match arm - match false { true => (), } - //^^^^^ missing match arm - match (false, true) {} - //^^^^^^^^^^^^^ missing match arm - match (false, true) { (true, true) => (), } - //^^^^^^^^^^^^^ missing match arm - match (false, true) { - //^^^^^^^^^^^^^ missing match arm - (false, true) => (), - (false, false) => (), - (true, false) => (), - } - match (false, true) { (true, _x) => (), } - //^^^^^^^^^^^^^ missing match arm - - 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)) {} - //^^^^^^^^^^^^^^^^^^^^ missing match arm - match (false, ((), false)) { (true, ((), true)) => (), } - //^^^^^^^^^^^^^^^^^^^^ missing match arm - match (false, ((), false)) { (true, _) => (), } - //^^^^^^^^^^^^^^^^^^^^ missing match arm - - 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 { } - //^^^^^^^^^ missing match arm - match Either::B { Either::A => (), } - //^^^^^^^^^ missing match arm - - match &Either::B { - //^^^^^^^^^^ missing match arm - 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 { } - //^^^^^^^^^ missing match arm - match Either::B { - //^^^^^^^^^ missing match arm - 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) { - //^^^^^^^^^^^^^^^^ missing match arm - 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) => (), - } - 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 } { - //^^^^^^^^^^^^^^^^^^^^^ missing match arm - Either::A => (), - } - match loop { break Foo::A } { - Either::A => (), - Either::B => (), - } -} -"#, - ); - } - - #[test] - fn expr_partially_diverges() { - check_diagnostics_no_bails( - r#" -enum Either { 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 { } - //^ missing match arm - match a { Either::A { foo: true } => () } - //^ missing match arm - match a { - Either::A { } => (), - //^^^^^^^^^ Missing structure fields: - // | - foo - Either::B => (), - } - match a { - //^ missing match arm - Either::A { } => (), - } //^^^^^^^^^ 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 { - //^ missing match arm - 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 { - //^ missing match arm - Either::A { foo: true, .. } => (), - Either::B => (), - } - match a { - //^ missing match arm - 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 { - //^^^^^^^^^ missing match arm - Either::A(true, .., true) => (), - Either::A(true, .., false) => (), - Either::A(false, .., false) => (), - Either::B => (), - } - match Either::B { - //^^^^^^^^^ missing match arm - 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 {} - //^^^^^ missing match arm -} -fn bang(never: !) { - match never {} -} -"#, - ); - } - - #[test] - fn unknown_type() { - cov_mark::check_count!(validate_match_bailed_out, 1); - - check_diagnostics( - r#" -enum Option { Some(T), None } - -fn main() { - // `Never` is deliberately not defined so that it's an uninferred type. - match Option::::None { - None => (), - Some(never) => match never {}, - } - match Option::::None { - //^^^^^^^^^^^^^^^^^^^^^ missing match arm - 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) { - //^^^^^^^^^^^^^^^^^^^^ missing match arm - (false, ..) => (), - } -}"#, - ); - } - - #[test] - fn tuple_of_bools_with_ellipsis_at_beginning_missing_arm() { - check_diagnostics_no_bails( - r#" -fn main() { - match (false, true, false) { - //^^^^^^^^^^^^^^^^^^^^ missing match arm - (.., false) => (), - } -}"#, - ); - } - - #[test] - fn tuple_of_bools_with_ellipsis_in_middle_missing_arm() { - check_diagnostics_no_bails( - r#" -fn main() { - match (false, true, false) { - //^^^^^^^^^^^^^^^^^^^^ missing match arm - (true, .., false) => (), - } -}"#, - ); - } - - #[test] - fn record_struct() { - check_diagnostics_no_bails( - r#"struct Foo { a: bool } -fn main(f: Foo) { - match f {} - //^ missing match arm - match f { Foo { a: true } => () } - //^ missing match arm - match &f { Foo { a: true } => () } - //^^ missing match arm - 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 {} - //^ missing match arm - match f { Foo(true) => () } - //^ missing match arm - match f { - Foo(true) => (), - Foo(false) => (), - } -} -"#, - ); - } - - #[test] - fn unit_struct() { - check_diagnostics_no_bails( - r#"struct Foo; -fn main(f: Foo) { - match f {} - //^ missing match arm - 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, .. } => () } - //^ missing match arm - match f { - //^ missing match arm - 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 { - //^^^^^^^^^ missing match arm - 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 { } - //^ missing match arm -} -"#, - ); - } - - #[test] - fn binding() { - check_diagnostics_no_bails( - r#" -fn main() { - match true { - _x @ true => {} - false => {} - } - match true { _x @ true => {} } - //^^^^ missing match arm -} -"#, - ); - } - - #[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 { - //^^^^ missing match arm - E::A => {} - E::B => {} - } - match E::A { - //^^^^ missing match arm - E::A | E::B => {} - } -} -"#, - ); - } - - #[test] - fn match_guard() { - check_diagnostics_no_bails( - r#" -fn main() { - match true { - true if false => {} - true => {} - false => {} - } - match true { - //^^^^ missing match arm - true if false => {} - false => {} - } -} -"#, - ); - } - - #[test] - fn pattern_type_is_of_substitution() { - cov_mark::check!(match_check_wildcard_expanded_to_substitutions); - check_diagnostics_no_bails( - r#" -struct Foo(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 { A(T) } -fn main() { - match &Foo::A(true) { - _ => {} - Foo::A(_) => {} - } -} -"#, - ); - } - - 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/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs b/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs deleted file mode 100644 index 01c79b6f5..000000000 --- a/crates/ide_diagnostics/src/missing_ok_or_some_in_tail_expr.rs +++ /dev/null @@ -1,229 +0,0 @@ -use hir::db::AstDatabase; -use ide_db::{assists::Assist, source_change::SourceChange}; -use syntax::AstNode; -use text_edit::TextEdit; - -use crate::{fix, Diagnostic, DiagnosticsContext}; - -// Diagnostic: missing-ok-or-some-in-tail-expr -// -// This diagnostic is triggered if a block that should return `Result` returns a value not wrapped in `Ok`, -// or if a block that should return `Option` returns a value not wrapped in `Some`. -// -// Example: -// -// ```rust -// fn foo() -> Result { -// 10 -// } -// ``` -pub(super) fn missing_ok_or_some_in_tail_expr( - ctx: &DiagnosticsContext<'_>, - d: &hir::MissingOkOrSomeInTailExpr, -) -> Diagnostic { - Diagnostic::new( - "missing-ok-or-some-in-tail-expr", - format!("wrap return expression in {}", d.required), - ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, - ) - .with_fixes(fixes(ctx, d)) -} - -fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::MissingOkOrSomeInTailExpr) -> Option> { - let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?; - let tail_expr = d.expr.value.to_node(&root); - let tail_expr_range = tail_expr.syntax().text_range(); - let replacement = format!("{}({})", d.required, tail_expr.syntax()); - let edit = TextEdit::replace(tail_expr_range, replacement); - let source_change = - SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit); - let name = if d.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; - Some(vec![fix("wrap_tail_expr", name, source_change, tail_expr_range)]) -} - -#[cfg(test)] -mod tests { - use crate::tests::{check_diagnostics, check_fix}; - - #[test] - fn test_wrap_return_type_option() { - check_fix( - r#" -//- /main.rs crate:main deps:core -use core::option::Option::{self, Some, None}; - -fn div(x: i32, y: i32) -> Option { - if y == 0 { - return None; - } - x / y$0 -} -//- /core/lib.rs crate:core -pub mod result { - pub enum Result { Ok(T), Err(E) } -} -pub mod option { - pub enum Option { Some(T), None } -} -"#, - r#" -use core::option::Option::{self, Some, None}; - -fn div(x: i32, y: i32) -> Option { - if y == 0 { - return None; - } - Some(x / y) -} -"#, - ); - } - - #[test] - fn test_wrap_return_type() { - check_fix( - r#" -//- /main.rs crate:main deps:core -use core::result::Result::{self, Ok, Err}; - -fn div(x: i32, y: i32) -> Result { - if y == 0 { - return Err(()); - } - x / y$0 -} -//- /core/lib.rs crate:core -pub mod result { - pub enum Result { Ok(T), Err(E) } -} -pub mod option { - pub enum Option { Some(T), None } -} -"#, - r#" -use core::result::Result::{self, Ok, Err}; - -fn div(x: i32, y: i32) -> Result { - if y == 0 { - return Err(()); - } - Ok(x / y) -} -"#, - ); - } - - #[test] - fn test_wrap_return_type_handles_generic_functions() { - check_fix( - r#" -//- /main.rs crate:main deps:core -use core::result::Result::{self, Ok, Err}; - -fn div(x: T) -> Result { - if x == 0 { - return Err(7); - } - $0x -} -//- /core/lib.rs crate:core -pub mod result { - pub enum Result { Ok(T), Err(E) } -} -pub mod option { - pub enum Option { Some(T), None } -} -"#, - r#" -use core::result::Result::{self, Ok, Err}; - -fn div(x: T) -> Result { - if x == 0 { - return Err(7); - } - Ok(x) -} -"#, - ); - } - - #[test] - fn test_wrap_return_type_handles_type_aliases() { - check_fix( - r#" -//- /main.rs crate:main deps:core -use core::result::Result::{self, Ok, Err}; - -type MyResult = Result; - -fn div(x: i32, y: i32) -> MyResult { - if y == 0 { - return Err(()); - } - x $0/ y -} -//- /core/lib.rs crate:core -pub mod result { - pub enum Result { Ok(T), Err(E) } -} -pub mod option { - pub enum Option { Some(T), None } -} -"#, - r#" -use core::result::Result::{self, Ok, Err}; - -type MyResult = Result; - -fn div(x: i32, y: i32) -> MyResult { - if y == 0 { - return Err(()); - } - Ok(x / y) -} -"#, - ); - } - - #[test] - fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { - check_diagnostics( - r#" -//- /main.rs crate:main deps:core -use core::result::Result::{self, Ok, Err}; - -fn foo() -> Result<(), i32> { 0 } - -//- /core/lib.rs crate:core -pub mod result { - pub enum Result { Ok(T), Err(E) } -} -pub mod option { - pub enum Option { Some(T), None } -} -"#, - ); - } - - #[test] - fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() { - check_diagnostics( - r#" -//- /main.rs crate:main deps:core -use core::result::Result::{self, Ok, Err}; - -enum SomeOtherEnum { Ok(i32), Err(String) } - -fn foo() -> SomeOtherEnum { 0 } - -//- /core/lib.rs crate:core -pub mod result { - pub enum Result { Ok(T), Err(E) } -} -pub mod option { - pub enum Option { Some(T), None } -} -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/missing_unsafe.rs b/crates/ide_diagnostics/src/missing_unsafe.rs deleted file mode 100644 index f5f38a0d3..000000000 --- a/crates/ide_diagnostics/src/missing_unsafe.rs +++ /dev/null @@ -1,101 +0,0 @@ -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(super) 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; -} //^^ 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(); - //^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block - HasUnsafe.unsafe_fn(); - //^^^^^^^^^^^^^^^^^^^^^ 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; - //^^^^^^^^^^ 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); - //^^^^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block -} -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/no_such_field.rs b/crates/ide_diagnostics/src/no_such_field.rs deleted file mode 100644 index c4fa387ca..000000000 --- a/crates/ide_diagnostics/src/no_such_field.rs +++ /dev/null @@ -1,283 +0,0 @@ -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(super) 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> { - 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, - usage_file_id: FileId, - record_expr_field: &ast::RecordExprField, -) -> Option> { - 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()?)?; - 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 { - 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 { - //^ Missing structure fields: - //| - bar - foo: 92, - baz: 62, - //^^^^^^^ 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/crates/ide_diagnostics/src/remove_this_semicolon.rs b/crates/ide_diagnostics/src/remove_this_semicolon.rs deleted file mode 100644 index dc6c9c083..000000000 --- a/crates/ide_diagnostics/src/remove_this_semicolon.rs +++ /dev/null @@ -1,61 +0,0 @@ -use hir::db::AstDatabase; -use ide_db::source_change::SourceChange; -use syntax::{ast, AstNode}; -use text_edit::TextEdit; - -use crate::{fix, Assist, Diagnostic, DiagnosticsContext}; - -// Diagnostic: remove-this-semicolon -// -// This diagnostic is triggered when there's an erroneous `;` at the end of the block. -pub(super) fn remove_this_semicolon( - ctx: &DiagnosticsContext<'_>, - d: &hir::RemoveThisSemicolon, -) -> Diagnostic { - Diagnostic::new( - "remove-this-semicolon", - "remove this semicolon", - ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, - ) - .with_fixes(fixes(ctx, d)) -} - -fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::RemoveThisSemicolon) -> Option> { - let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?; - - let semicolon = d - .expr - .value - .to_node(&root) - .syntax() - .parent() - .and_then(ast::ExprStmt::cast) - .and_then(|expr| expr.semicolon_token())? - .text_range(); - - let edit = TextEdit::delete(semicolon); - let source_change = - SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit); - - Some(vec![fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)]) -} - -#[cfg(test)] -mod tests { - use crate::tests::{check_diagnostics, check_fix}; - - #[test] - fn missing_semicolon() { - check_diagnostics( - r#" -fn test() -> i32 { 123; } - //^^^ remove this semicolon -"#, - ); - } - - #[test] - fn remove_semicolon() { - check_fix(r#"fn f() -> i32 { 92$0; }"#, r#"fn f() -> i32 { 92 }"#); - } -} diff --git a/crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs b/crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs deleted file mode 100644 index 775c350d2..000000000 --- a/crates/ide_diagnostics/src/replace_filter_map_next_with_find_map.rs +++ /dev/null @@ -1,179 +0,0 @@ -use hir::{db::AstDatabase, InFile}; -use ide_db::source_change::SourceChange; -use syntax::{ - ast::{self, ArgListOwner}, - 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(super) 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> { - 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_fix; - - // Register the required standard library types to make the tests work - #[track_caller] - fn check_diagnostics(ra_fixture: &str) { - let prefix = r#" -//- /main.rs crate:main deps:core -use core::iter::Iterator; -use core::option::Option::{self, Some, None}; -"#; - let suffix = r#" -//- /core/lib.rs crate:core -pub mod option { - pub enum Option { Some(T), None } -} -pub mod iter { - pub trait Iterator { - type Item; - fn filter_map(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option { FilterMap } - fn next(&mut self) -> Option; - } - pub struct FilterMap {} - impl Iterator for FilterMap { - type Item = i32; - fn next(&mut self) -> i32 { 7 } - } -} -"#; - crate::tests::check_diagnostics(&format!("{}{}{}", prefix, ra_fixture, suffix)) - } - - #[test] - fn replace_filter_map_next_with_find_map2() { - check_diagnostics( - r#" - fn foo() { - let m = [1, 2, 3].iter().filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); - } //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ replace filter_map(..).next() with find_map(..) -"#, - ); - } - - #[test] - fn replace_filter_map_next_with_find_map_no_diagnostic_without_next() { - check_diagnostics( - r#" -fn foo() { - let m = [1, 2, 3] - .iter() - .filter_map(|x| if *x == 2 { Some (4) } else { None }) - .len(); -} -"#, - ); - } - - #[test] - fn replace_filter_map_next_with_find_map_no_diagnostic_with_intervening_methods() { - check_diagnostics( - r#" -fn foo() { - let m = [1, 2, 3] - .iter() - .filter_map(|x| if *x == 2 { Some (4) } else { None }) - .map(|x| x + 2) - .len(); -} -"#, - ); - } - - #[test] - fn replace_filter_map_next_with_find_map_no_diagnostic_if_not_in_chain() { - check_diagnostics( - r#" -fn foo() { - let m = [1, 2, 3] - .iter() - .filter_map(|x| if *x == 2 { Some (4) } else { None }); - let n = m.next(); -} -"#, - ); - } - - #[test] - fn replace_with_wind_map() { - check_fix( - r#" -//- /main.rs crate:main deps:core -use core::iter::Iterator; -use core::option::Option::{self, Some, None}; -fn foo() { - let m = [1, 2, 3].iter().$0filter_map(|x| if *x == 2 { Some (4) } else { None }).next(); -} -//- /core/lib.rs crate:core -pub mod option { - pub enum Option { Some(T), None } -} -pub mod iter { - pub trait Iterator { - type Item; - fn filter_map(self, f: F) -> FilterMap where F: FnMut(Self::Item) -> Option { FilterMap } - fn next(&mut self) -> Option; - } - pub struct FilterMap {} - impl Iterator for FilterMap { - type Item = i32; - fn next(&mut self) -> i32 { 7 } - } -} -"#, - r#" -use core::iter::Iterator; -use core::option::Option::{self, Some, None}; -fn foo() { - let m = [1, 2, 3].iter().find_map(|x| if *x == 2 { Some (4) } else { None }); -} -"#, - ) - } -} diff --git a/crates/ide_diagnostics/src/unimplemented_builtin_macro.rs b/crates/ide_diagnostics/src/unimplemented_builtin_macro.rs deleted file mode 100644 index a600544f0..000000000 --- a/crates/ide_diagnostics/src/unimplemented_builtin_macro.rs +++ /dev/null @@ -1,16 +0,0 @@ -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(super) 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/crates/ide_diagnostics/src/unlinked_file.rs b/crates/ide_diagnostics/src/unlinked_file.rs deleted file mode 100644 index 424532e3a..000000000 --- a/crates/ide_diagnostics/src/unlinked_file.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! 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, ModuleItemOwner, NameOwner}, - AstNode, TextRange, TextSize, -}; -use text_edit::TextEdit; - -use crate::{fix, Assist, Diagnostic, DiagnosticsContext}; - -#[derive(Debug)] -pub(crate) struct UnlinkedFile { - pub(crate) file: FileId, -} - -// 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(super) fn unlinked_file(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Diagnostic { - // 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(d.file).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); - - Diagnostic::new("unlinked-file", "file not included in module tree", range) - .with_fixes(fixes(ctx, d)) -} - -fn fixes(ctx: &DiagnosticsContext, d: &UnlinkedFile) -> Option> { - // 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(d.file)); - let our_path = source_root.path_for_file(&d.file)?; - let module_name = our_path.name_and_extension()?.0; - - // Candidates to look for: - // - `mod.rs` in the same folder - // - we also check `main.rs` and `lib.rs` - // - `$dir.rs` in the parent folder, where `$dir` is the directory containing `self.file_id` - let parent = our_path.parent()?; - let mut paths = vec![parent.join("mod.rs")?, parent.join("lib.rs")?, parent.join("main.rs")?]; - - // `submod/bla.rs` -> `submod.rs` - if let Some(newmod) = (|| { - let name = parent.name_and_extension()?.0; - parent.parent()?.join(&format!("{}.rs", name)) - })() { - paths.push(newmod); - } - - for path in &paths { - if let Some(parent_id) = 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, d.file); - } - } - } - } - } - - None -} - -fn make_fixes( - db: &RootDatabase, - parent_file_id: FileId, - new_mod_name: &str, - added_file_id: FileId, -) -> Option> { - 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(|item| is_outline_mod(item)) - .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_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/crates/ide_diagnostics/src/unresolved_extern_crate.rs b/crates/ide_diagnostics/src/unresolved_extern_crate.rs deleted file mode 100644 index 69f07d0b0..000000000 --- a/crates/ide_diagnostics/src/unresolved_extern_crate.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::{Diagnostic, DiagnosticsContext}; - -// Diagnostic: unresolved-extern-crate -// -// This diagnostic is triggered if rust-analyzer is unable to discover referred extern crate. -pub(super) 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; -//^^^^^^^^^^^^^^^^^^^^^^^^^^ 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; -//^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate -// Should not error. -extern crate self as foo; -struct Foo; -use foo::Foo as Bar; -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/unresolved_import.rs b/crates/ide_diagnostics/src/unresolved_import.rs deleted file mode 100644 index 7779033d4..000000000 --- a/crates/ide_diagnostics/src/unresolved_import.rs +++ /dev/null @@ -1,90 +0,0 @@ -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(super) 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 = "../"]` 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; - //^^^^^^^^^^^^^^ 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}; - //^^^^^^^^^^^ unresolved import - -use {does_not_exist::*, does_exist}; - //^^^^^^^^^^^^^^^^^ unresolved import - -use does_not_exist::{ - a, - //^ unresolved import - b, - //^ unresolved import - c, - //^ 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; - //^^^^^^^^^^^^^^^^^^^^^^^^^^ unresolved extern crate - - // Should not error, since we already errored for the missing crate. - use doesnotexist::{self, bla, *}; - - use crate::doesnotexist; - //^^^^^^^^^^^^^^^^^^^ unresolved import -} - -mod m { - use super::doesnotexist; - //^^^^^^^^^^^^^^^^^^^ unresolved import -} -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/unresolved_macro_call.rs b/crates/ide_diagnostics/src/unresolved_macro_call.rs deleted file mode 100644 index 88133d0f3..000000000 --- a/crates/ide_diagnostics/src/unresolved_macro_call.rs +++ /dev/null @@ -1,84 +0,0 @@ -use hir::{db::AstDatabase, InFile}; -use syntax::{AstNode, SyntaxNodePtr}; - -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(super) fn unresolved_macro_call( - ctx: &DiagnosticsContext<'_>, - d: &hir::UnresolvedMacroCall, -) -> Diagnostic { - let last_path_segment = ctx.sema.db.parse_or_expand(d.macro_call.file_id).and_then(|root| { - d.macro_call - .value - .to_node(&root) - .path() - .and_then(|it| it.segment()) - .and_then(|it| it.name_ref()) - .map(|it| InFile::new(d.macro_call.file_id, SyntaxNodePtr::new(it.syntax()))) - }); - let diagnostics = last_path_segment.unwrap_or_else(|| d.macro_call.clone().map(|it| it.into())); - - Diagnostic::new( - "unresolved-macro-call", - format!("unresolved macro `{}!`", d.path), - ctx.sema.diagnostics_display_range(diagnostics).range, - ) - .experimental() -} - -#[cfg(test)] -mod tests { - use crate::tests::check_diagnostics; - - #[test] - fn unresolved_macro_diag() { - check_diagnostics( - r#" -fn f() { - m!(); -} //^ unresolved macro `m!` - -"#, - ); - } - - #[test] - fn test_unresolved_macro_range() { - check_diagnostics( - r#" -foo::bar!(92); - //^^^ unresolved macro `foo::bar!` -"#, - ); - } - - #[test] - fn unresolved_legacy_scope_macro() { - check_diagnostics( - r#" -macro_rules! m { () => {} } - -m!(); m2!(); - //^^ unresolved macro `self::m2!` -"#, - ); - } - - #[test] - fn unresolved_module_scope_macro() { - check_diagnostics( - r#" -mod mac { -#[macro_export] -macro_rules! m { () => {} } } - -self::m!(); self::m2!(); - //^^ unresolved macro `self::m2!` -"#, - ); - } -} diff --git a/crates/ide_diagnostics/src/unresolved_module.rs b/crates/ide_diagnostics/src/unresolved_module.rs deleted file mode 100644 index 5aa9dae17..000000000 --- a/crates/ide_diagnostics/src/unresolved_module.rs +++ /dev/null @@ -1,110 +0,0 @@ -use hir::db::AstDatabase; -use ide_db::{assists::Assist, base_db::AnchoredPathBuf, source_change::FileSystemEdit}; -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(super) fn unresolved_module( - ctx: &DiagnosticsContext<'_>, - d: &hir::UnresolvedModule, -) -> Diagnostic { - Diagnostic::new( - "unresolved-module", - "unresolved module", - 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> { - let root = ctx.sema.db.parse_or_expand(d.decl.file_id)?; - let unresolved_module = d.decl.value.to_node(&root); - Some(vec![fix( - "create_module", - "Create module", - FileSystemEdit::CreateFile { - dst: AnchoredPathBuf { - anchor: d.decl.file_id.original_file(ctx.sema.db), - path: d.candidate.clone(), - }, - initial_contents: "".to_string(), - } - .into(), - unresolved_module.syntax().text_range(), - )]) -} - -#[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; -//^^^^^^^^ unresolved module -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", - range: 0..8, - severity: Error, - unused: false, - experimental: false, - fixes: Some( - [ - Assist { - id: AssistId( - "create_module", - QuickFix, - ), - label: "Create module", - 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, - }, - ), - }, - ], - ), - }, - ] - "#]], - ); - } -} diff --git a/crates/ide_diagnostics/src/unresolved_proc_macro.rs b/crates/ide_diagnostics/src/unresolved_proc_macro.rs deleted file mode 100644 index 744cce508..000000000 --- a/crates/ide_diagnostics/src/unresolved_proc_macro.rs +++ /dev/null @@ -1,27 +0,0 @@ -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.enable`). -pub(super) fn unresolved_proc_macro( - ctx: &DiagnosticsContext<'_>, - d: &hir::UnresolvedProcMacro, -) -> 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); - // FIXME: it would be nice to tell the user whether proc macros are currently disabled - let message = match &d.macro_name { - Some(name) => format!("proc macro `{}` not expanded", name), - None => "proc macro not expanded".to_string(), - }; - - Diagnostic::new("unresolved-proc-macro", message, display_range).severity(Severity::WeakWarning) -} -- cgit v1.2.3