8862: internal: scalable module structure for fixits r=matklad a=matklad

bors r+
🤖

Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
bors[bot] 2021-05-17 09:04:57 +00:00 committed by GitHub
commit 72a694d577
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 890 additions and 812 deletions

View File

@ -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<i32> {
if y == 0 {
return None;
}
x / y$0
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::option::Option::{self, Some, None};
fn div(x: i32, y: i32) -> Option<i32> {
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<i32, ()> {
if y == 0 {
return Err(());
}
x / y$0
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::result::Result::{self, Ok, Err};
fn div(x: i32, y: i32) -> Result<i32, ()> {
if y == 0 {
return Err(());
}
Ok(x / y)
}
"#,
);
}
#[test]
fn test_wrap_return_type_handles_generic_functions() {
check_fix(
r#"
//- /main.rs crate:main deps:core
use core::result::Result::{self, Ok, Err};
fn div<T>(x: T) -> Result<T, i32> {
if x == 0 {
return Err(7);
}
$0x
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::result::Result::{self, Ok, Err};
fn div<T>(x: T) -> Result<T, i32> {
if x == 0 {
return Err(7);
}
Ok(x)
}
"#,
);
}
#[test]
fn test_wrap_return_type_handles_type_aliases() {
check_fix(
r#"
//- /main.rs crate:main deps:core
use core::result::Result::{self, Ok, Err};
type MyResult<T> = Result<T, ()>;
fn div(x: i32, y: i32) -> MyResult<i32> {
if y == 0 {
return Err(());
}
x $0/ y
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::result::Result::{self, Ok, Err};
type MyResult<T> = Result<T, ()>;
fn div(x: i32, y: i32) -> MyResult<i32> {
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<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { 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<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { 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<Expr>, rhs: Box<Expr> }
}
impl Expr {
fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
Expr::Bin {$0 }
}
}
"#,
r#"
enum Expr {
Bin { lhs: Box<Expr>, rhs: Box<Expr> }
}
impl Expr {
fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
Expr::Bin { lhs: (), rhs: () }
}
}
"#,
);
}
#[test]
fn test_fill_struct_fields_partial() {
check_fix(
r#"
struct TestStruct { one: i32, two: i64 }
fn test_fn() {
let s = TestStruct{ two: 2$0 };
}
"#,
r"
struct TestStruct { one: i32, two: i64 }
fn test_fn() {
let s = TestStruct{ two: 2, one: () };
}
",
);
}
#[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);

View File

@ -1,31 +1,18 @@
//! Provides a way to attach fixes to the diagnostics.
//! The same module also has all curret custom fixes for the diagnostics implemented.
use hir::{
db::AstDatabase,
diagnostics::{
Diagnostic, IncorrectCase, MissingFields, MissingOkOrSomeInTailExpr, NoSuchField,
RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap, UnresolvedModule,
},
HasSource, HirDisplay, InFile, Semantics, VariantDef,
};
use ide_assists::AssistResolveStrategy;
use ide_db::{
base_db::{AnchoredPathBuf, FileId},
source_change::{FileSystemEdit, SourceChange},
RootDatabase,
};
use syntax::{
algo,
ast::{self, edit::IndentLevel, make, ArgListOwner},
AstNode, TextRange,
};
use text_edit::TextEdit;
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 crate::{
diagnostics::{fix, unresolved_fix},
references::rename::rename_with_semantics,
Assist, FilePosition,
};
use hir::{diagnostics::Diagnostic, Semantics};
use ide_assists::AssistResolveStrategy;
use ide_db::RootDatabase;
use crate::Assist;
/// A [Diagnostic] that potentially has a fix available.
///
@ -42,257 +29,3 @@ fn fix(
_resolve: &AssistResolveStrategy,
) -> Option<Assist>;
}
impl DiagnosticWithFix for UnresolvedModule {
fn fix(
&self,
sema: &Semantics<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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 MissingFields {
fn fix(
&self,
sema: &Semantics<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
// Note that although we could add a diagnostics to
// fill the missing tuple field, e.g :
// `struct A(usize);`
// `let a = A { 0: () }`
// but it is uncommon usage and it should not be encouraged.
if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
return None;
}
let root = sema.db.parse_or_expand(self.file)?;
let field_list_parent = self.field_list_parent.to_node(&root);
let old_field_list = field_list_parent.record_expr_field_list()?;
let new_field_list = old_field_list.clone_for_update();
for f in self.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(fix(
"fill_missing_fields",
"Fill struct fields",
SourceChange::from_text_edit(self.file.original_file(sema.db), edit),
sema.original_range(&field_list_parent.syntax()).range,
))
}
}
impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
fn fix(
&self,
sema: &Semantics<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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<RootDatabase>,
resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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<RootDatabase>,
usage_file_id: FileId,
record_expr_field: &ast::RecordExprField,
) -> Option<Assist> {
let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
let def_id = sema.resolve_variant(record_lit)?;
let module;
let def_file_id;
let record_fields = match def_id {
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<ast::RecordFieldList> {
match field_def_list {
ast::FieldList::RecordFieldList(it) => Some(it),
ast::FieldList::TupleFieldList(_) => None,
}
}
}

View File

@ -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<RootDatabase>,
resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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);
}
}

View File

@ -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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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<RootDatabase>,
usage_file_id: FileId,
record_expr_field: &ast::RecordExprField,
) -> Option<Assist> {
let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
let def_id = sema.resolve_variant(record_lit)?;
let module;
let def_file_id;
let record_fields = match def_id {
hir::VariantDef::Struct(s) => {
module = s.module(sema.db);
let source = s.source(sema.db)?;
def_file_id = source.file_id;
let fields = source.value.field_list()?;
record_field_list(fields)?
}
hir::VariantDef::Union(u) => {
module = u.module(sema.db);
let source = u.source(sema.db)?;
def_file_id = source.file_id;
source.value.record_field_list()?
}
hir::VariantDef::Variant(e) => {
module = e.module(sema.db);
let source = e.source(sema.db)?;
def_file_id = source.file_id;
let fields = source.value.field_list()?;
record_field_list(fields)?
}
};
let def_file_id = def_file_id.original_file(sema.db);
let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
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<ast::RecordFieldList> {
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
}
"#,
)
}
}

View File

@ -0,0 +1,192 @@
use hir::{db::AstDatabase, diagnostics::MissingFields, Semantics};
use ide_assists::AssistResolveStrategy;
use ide_db::{source_change::SourceChange, RootDatabase};
use syntax::{algo, ast::make, AstNode};
use text_edit::TextEdit;
use crate::{
diagnostics::{fix, fixes::DiagnosticWithFix},
Assist,
};
impl DiagnosticWithFix for MissingFields {
fn fix(
&self,
sema: &Semantics<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
// Note that although we could add a diagnostics to
// fill the missing tuple field, e.g :
// `struct A(usize);`
// `let a = A { 0: () }`
// but it is uncommon usage and it should not be encouraged.
if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
return None;
}
let root = sema.db.parse_or_expand(self.file)?;
let field_list_parent = self.field_list_parent.to_node(&root);
let old_field_list = field_list_parent.record_expr_field_list()?;
let new_field_list = old_field_list.clone_for_update();
for f in self.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(fix(
"fill_missing_fields",
"Fill struct fields",
SourceChange::from_text_edit(self.file.original_file(sema.db), edit),
sema.original_range(&field_list_parent.syntax()).range,
))
}
}
#[cfg(test)]
mod tests {
use crate::diagnostics::tests::{check_fix, check_no_diagnostics};
#[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<Expr>, rhs: Box<Expr> }
}
impl Expr {
fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
Expr::Bin {$0 }
}
}
"#,
r#"
enum Expr {
Bin { lhs: Box<Expr>, rhs: Box<Expr> }
}
impl Expr {
fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
Expr::Bin { lhs: (), rhs: () }
}
}
"#,
);
}
#[test]
fn test_fill_struct_fields_partial() {
check_fix(
r#"
struct TestStruct { one: i32, two: i64 }
fn test_fn() {
let s = TestStruct{ two: 2$0 };
}
"#,
r"
struct TestStruct { one: i32, two: i64 }
fn test_fn() {
let s = TestStruct{ two: 2, one: () };
}
",
);
}
#[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 };
}
"#,
);
}
}

View File

@ -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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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))
}
}

View File

@ -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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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,
))
}
}

View File

@ -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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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",
),
),
},
]
"#]],
);
}
}

View File

@ -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<RootDatabase>,
_resolve: &AssistResolveStrategy,
) -> Option<Assist> {
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<i32> {
if y == 0 {
return None;
}
x / y$0
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::option::Option::{self, Some, None};
fn div(x: i32, y: i32) -> Option<i32> {
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<i32, ()> {
if y == 0 {
return Err(());
}
x / y$0
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::result::Result::{self, Ok, Err};
fn div(x: i32, y: i32) -> Result<i32, ()> {
if y == 0 {
return Err(());
}
Ok(x / y)
}
"#,
);
}
#[test]
fn test_wrap_return_type_handles_generic_functions() {
check_fix(
r#"
//- /main.rs crate:main deps:core
use core::result::Result::{self, Ok, Err};
fn div<T>(x: T) -> Result<T, i32> {
if x == 0 {
return Err(7);
}
$0x
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::result::Result::{self, Ok, Err};
fn div<T>(x: T) -> Result<T, i32> {
if x == 0 {
return Err(7);
}
Ok(x)
}
"#,
);
}
#[test]
fn test_wrap_return_type_handles_type_aliases() {
check_fix(
r#"
//- /main.rs crate:main deps:core
use core::result::Result::{self, Ok, Err};
type MyResult<T> = Result<T, ()>;
fn div(x: i32, y: i32) -> MyResult<i32> {
if y == 0 {
return Err(());
}
x $0/ y
}
//- /core/lib.rs crate:core
pub mod result {
pub enum Result<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
r#"
use core::result::Result::{self, Ok, Err};
type MyResult<T> = Result<T, ()>;
fn div(x: i32, y: i32) -> MyResult<i32> {
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<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { 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<T, E> { Ok(T), Err(E) }
}
pub mod option {
pub enum Option<T> { Some(T), None }
}
"#,
);
}
}

View File

@ -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;
}