diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index d5fba6740fa..4172f6caedc 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -375,7 +375,7 @@ pub(crate) fn check_no_diagnostics(ra_fixture: &str) { assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics); } - fn check_expect(ra_fixture: &str, expect: Expect) { + pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) { let (analysis, file_id) = fixture::file(ra_fixture); let diagnostics = analysis .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id) @@ -383,374 +383,6 @@ fn check_expect(ra_fixture: &str, expect: Expect) { expect.assert_debug_eq(&diagnostics) } - #[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_no_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_no_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 } -} -"#, - ); - } - - #[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_no_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_no_diagnostics( - r" - struct TestStruct { one: i32, two: i64 } - - fn test_fn() { - let one = 1; - let s = TestStruct{ ..a }; - } - ", - ); - } - - #[test] - fn test_unresolved_module_diagnostic() { - check_expect( - r#"mod foo;"#, - expect![[r#" - [ - Diagnostic { - message: "unresolved module", - range: 0..8, - severity: Error, - fix: 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, - }, - ), - }, - ), - unused: false, - code: Some( - DiagnosticCode( - "unresolved-module", - ), - ), - }, - ] - "#]], - ); - } - #[test] fn test_unresolved_macro_range() { check_expect( @@ -890,53 +522,6 @@ mod a { mod c {} mod d { mod e {} } } ); } - #[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 -} -"#, - ) - } - #[test] fn test_disabled_diagnostics() { let mut config = DiagnosticsConfig::default(); @@ -954,120 +539,6 @@ fn test_disabled_diagnostics() { assert!(!diagnostics.is_empty()); } - #[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_no_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 unlinked_file_prepend_first_item() { cov_mark::check!(unlinked_file_prepend_before_first_item); diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs index 5330449f9b5..92b3f5a2d81 100644 --- a/crates/ide/src/diagnostics/fixes.rs +++ b/crates/ide/src/diagnostics/fixes.rs @@ -1,32 +1,18 @@ //! Provides a way to attach fixes to the diagnostics. //! The same module also has all curret custom fixes for the diagnostics implemented. +mod change_case; +mod create_field; mod fill_missing_fields; +mod remove_semicolon; +mod replace_with_find_map; +mod unresolved_module; +mod wrap_tail_expr; -use hir::{ - db::AstDatabase, - diagnostics::{ - Diagnostic, IncorrectCase, MissingOkOrSomeInTailExpr, NoSuchField, RemoveThisSemicolon, - ReplaceFilterMapNextWithFindMap, UnresolvedModule, - }, - HasSource, HirDisplay, InFile, Semantics, VariantDef, -}; +use hir::{diagnostics::Diagnostic, Semantics}; use ide_assists::AssistResolveStrategy; -use ide_db::{ - base_db::{AnchoredPathBuf, FileId}, - source_change::{FileSystemEdit, SourceChange}, - RootDatabase, -}; -use syntax::{ - ast::{self, edit::IndentLevel, make, ArgListOwner}, - AstNode, TextRange, -}; -use text_edit::TextEdit; +use ide_db::RootDatabase; -use crate::{ - diagnostics::{fix, unresolved_fix}, - references::rename::rename_with_semantics, - Assist, FilePosition, -}; +use crate::Assist; /// A [Diagnostic] that potentially has a fix available. /// @@ -43,216 +29,3 @@ fn fix( _resolve: &AssistResolveStrategy, ) -> Option; } - -impl DiagnosticWithFix for UnresolvedModule { - fn fix( - &self, - sema: &Semantics, - _resolve: &AssistResolveStrategy, - ) -> Option { - let root = sema.db.parse_or_expand(self.file)?; - let unresolved_module = self.decl.to_node(&root); - Some(fix( - "create_module", - "Create module", - FileSystemEdit::CreateFile { - dst: AnchoredPathBuf { - anchor: self.file.original_file(sema.db), - path: self.candidate.clone(), - }, - initial_contents: "".to_string(), - } - .into(), - unresolved_module.syntax().text_range(), - )) - } -} - -impl DiagnosticWithFix for NoSuchField { - fn fix( - &self, - sema: &Semantics, - _resolve: &AssistResolveStrategy, - ) -> Option { - let root = sema.db.parse_or_expand(self.file)?; - missing_record_expr_field_fix( - &sema, - self.file.original_file(sema.db), - &self.field.to_node(&root), - ) - } -} - -impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { - fn fix( - &self, - sema: &Semantics, - _resolve: &AssistResolveStrategy, - ) -> Option { - let root = sema.db.parse_or_expand(self.file)?; - let tail_expr = self.expr.to_node(&root); - let tail_expr_range = tail_expr.syntax().text_range(); - let replacement = format!("{}({})", self.required, tail_expr.syntax()); - let edit = TextEdit::replace(tail_expr_range, replacement); - let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); - let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; - Some(fix("wrap_tail_expr", name, source_change, tail_expr_range)) - } -} - -impl DiagnosticWithFix for RemoveThisSemicolon { - fn fix( - &self, - sema: &Semantics, - _resolve: &AssistResolveStrategy, - ) -> Option { - let root = sema.db.parse_or_expand(self.file)?; - - let semicolon = self - .expr - .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(self.file.original_file(sema.db), edit); - - Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)) - } -} - -impl DiagnosticWithFix for IncorrectCase { - fn fix( - &self, - sema: &Semantics, - resolve: &AssistResolveStrategy, - ) -> Option { - let root = sema.db.parse_or_expand(self.file)?; - let name_node = self.ident.to_node(&root); - - let name_node = InFile::new(self.file, name_node.syntax()); - let frange = name_node.original_file_range(sema.db); - let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; - - let label = format!("Rename to {}", self.suggested_text); - let mut res = unresolved_fix("change_case", &label, frange.range); - if resolve.should_resolve(&res.id) { - let source_change = rename_with_semantics(sema, file_position, &self.suggested_text); - res.source_change = Some(source_change.ok().unwrap_or_default()); - } - - Some(res) - } -} - -impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { - fn fix( - &self, - sema: &Semantics, - _resolve: &AssistResolveStrategy, - ) -> Option { - let root = sema.db.parse_or_expand(self.file)?; - let next_expr = self.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(self.file.original_file(sema.db), edit); - - Some(fix( - "replace_with_find_map", - "Replace filter_map(..).next() with find_map()", - source_change, - trigger_range, - )) - } -} - -fn missing_record_expr_field_fix( - 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 { - 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)? - } - 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()? - } - 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(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, - } - } -} diff --git a/crates/ide/src/diagnostics/fixes/change_case.rs b/crates/ide/src/diagnostics/fixes/change_case.rs new file mode 100644 index 00000000000..80aca58a1bb --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/change_case.rs @@ -0,0 +1,155 @@ +use hir::{db::AstDatabase, diagnostics::IncorrectCase, InFile, Semantics}; +use ide_assists::{Assist, AssistResolveStrategy}; +use ide_db::{base_db::FilePosition, RootDatabase}; +use syntax::AstNode; + +use crate::{ + diagnostics::{unresolved_fix, DiagnosticWithFix}, + references::rename::rename_with_semantics, +}; + +impl DiagnosticWithFix for IncorrectCase { + fn fix( + &self, + sema: &Semantics, + resolve: &AssistResolveStrategy, + ) -> Option { + let root = sema.db.parse_or_expand(self.file)?; + let name_node = self.ident.to_node(&root); + + let name_node = InFile::new(self.file, name_node.syntax()); + let frange = name_node.original_file_range(sema.db); + let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() }; + + let label = format!("Rename to {}", self.suggested_text); + let mut res = unresolved_fix("change_case", &label, frange.range); + if resolve.should_resolve(&res.id) { + let source_change = rename_with_semantics(sema, file_position, &self.suggested_text); + res.source_change = Some(source_change.ok().unwrap_or_default()); + } + + Some(res) + } +} + +#[cfg(test)] +mod change_case { + use crate::{ + diagnostics::tests::{check_fix, check_no_diagnostics}, + fixture, 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_no_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); + } +} diff --git a/crates/ide/src/diagnostics/fixes/create_field.rs b/crates/ide/src/diagnostics/fixes/create_field.rs new file mode 100644 index 00000000000..24e0fda5204 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/create_field.rs @@ -0,0 +1,157 @@ +use hir::{db::AstDatabase, diagnostics::NoSuchField, 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::{ + diagnostics::{fix, DiagnosticWithFix}, + Assist, AssistResolveStrategy, +}; + +impl DiagnosticWithFix for NoSuchField { + fn fix( + &self, + sema: &Semantics, + _resolve: &AssistResolveStrategy, + ) -> Option { + let root = sema.db.parse_or_expand(self.file)?; + missing_record_expr_field_fix( + &sema, + self.file.original_file(sema.db), + &self.field.to_node(&root), + ) + } +} + +fn missing_record_expr_field_fix( + 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(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::diagnostics::tests::check_fix; + + #[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/src/diagnostics/fixes/remove_semicolon.rs b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs new file mode 100644 index 00000000000..058002c697f --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs @@ -0,0 +1,31 @@ +use hir::{db::AstDatabase, diagnostics::RemoveThisSemicolon, Semantics}; +use ide_assists::{Assist, AssistResolveStrategy}; +use ide_db::{source_change::SourceChange, RootDatabase}; +use syntax::{ast, AstNode}; +use text_edit::TextEdit; + +use crate::diagnostics::{fix, DiagnosticWithFix}; + +impl DiagnosticWithFix for RemoveThisSemicolon { + fn fix( + &self, + sema: &Semantics, + _resolve: &AssistResolveStrategy, + ) -> Option { + let root = sema.db.parse_or_expand(self.file)?; + + let semicolon = self + .expr + .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(self.file.original_file(sema.db), edit); + + Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon)) + } +} diff --git a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs new file mode 100644 index 00000000000..5ddfd206483 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs @@ -0,0 +1,42 @@ +use hir::{db::AstDatabase, diagnostics::ReplaceFilterMapNextWithFindMap, Semantics}; +use ide_assists::{Assist, AssistResolveStrategy}; +use ide_db::{source_change::SourceChange, RootDatabase}; +use syntax::{ + ast::{self, ArgListOwner}, + AstNode, TextRange, +}; +use text_edit::TextEdit; + +use crate::diagnostics::{fix, DiagnosticWithFix}; + +impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap { + fn fix( + &self, + sema: &Semantics, + _resolve: &AssistResolveStrategy, + ) -> Option { + let root = sema.db.parse_or_expand(self.file)?; + let next_expr = self.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(self.file.original_file(sema.db), edit); + + Some(fix( + "replace_with_find_map", + "Replace filter_map(..).next() with find_map()", + source_change, + trigger_range, + )) + } +} diff --git a/crates/ide/src/diagnostics/fixes/unresolved_module.rs b/crates/ide/src/diagnostics/fixes/unresolved_module.rs new file mode 100644 index 00000000000..81244b29386 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/unresolved_module.rs @@ -0,0 +1,87 @@ +use hir::{db::AstDatabase, diagnostics::UnresolvedModule, Semantics}; +use ide_assists::{Assist, AssistResolveStrategy}; +use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase}; +use syntax::AstNode; + +use crate::diagnostics::{fix, DiagnosticWithFix}; + +impl DiagnosticWithFix for UnresolvedModule { + fn fix( + &self, + sema: &Semantics, + _resolve: &AssistResolveStrategy, + ) -> Option { + let root = sema.db.parse_or_expand(self.file)?; + let unresolved_module = self.decl.to_node(&root); + Some(fix( + "create_module", + "Create module", + FileSystemEdit::CreateFile { + dst: AnchoredPathBuf { + anchor: self.file.original_file(sema.db), + path: self.candidate.clone(), + }, + initial_contents: "".to_string(), + } + .into(), + unresolved_module.syntax().text_range(), + )) + } +} + +#[cfg(test)] +mod tests { + use expect_test::expect; + + use crate::diagnostics::tests::check_expect; + + #[test] + fn test_unresolved_module_diagnostic() { + check_expect( + r#"mod foo;"#, + expect![[r#" + [ + Diagnostic { + message: "unresolved module", + range: 0..8, + severity: Error, + fix: 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, + }, + ), + }, + ), + unused: false, + code: Some( + DiagnosticCode( + "unresolved-module", + ), + ), + }, + ] + "#]], + ); + } +} diff --git a/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs new file mode 100644 index 00000000000..66676064a52 --- /dev/null +++ b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs @@ -0,0 +1,211 @@ +use hir::{db::AstDatabase, diagnostics::MissingOkOrSomeInTailExpr, Semantics}; +use ide_assists::{Assist, AssistResolveStrategy}; +use ide_db::{source_change::SourceChange, RootDatabase}; +use syntax::AstNode; +use text_edit::TextEdit; + +use crate::diagnostics::{fix, DiagnosticWithFix}; + +impl DiagnosticWithFix for MissingOkOrSomeInTailExpr { + fn fix( + &self, + sema: &Semantics, + _resolve: &AssistResolveStrategy, + ) -> Option { + let root = sema.db.parse_or_expand(self.file)?; + let tail_expr = self.expr.to_node(&root); + let tail_expr_range = tail_expr.syntax().text_range(); + let replacement = format!("{}({})", self.required, tail_expr.syntax()); + let edit = TextEdit::replace(tail_expr_range, replacement); + let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit); + let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; + Some(fix("wrap_tail_expr", name, source_change, tail_expr_range)) + } +} + +#[cfg(test)] +mod tests { + use crate::diagnostics::tests::{check_fix, check_no_diagnostics}; + + #[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_no_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_no_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/xtask/src/tidy.rs b/xtask/src/tidy.rs index c3c785eff35..6c55823eb08 100644 --- a/xtask/src/tidy.rs +++ b/xtask/src/tidy.rs @@ -347,9 +347,8 @@ struct TidyDocs { impl TidyDocs { fn visit(&mut self, path: &Path, text: &str) { - // Test hopefully don't really need comments, and for assists we already - // have special comments which are source of doc tests and user docs. - if is_exclude_dir(path, &["tests", "test_data"]) { + // Tests and diagnostic fixes don't need module level comments. + if is_exclude_dir(path, &["tests", "test_data", "fixes"]) { return; }