⬆️ rust-analyzer

This commit is contained in:
Laurențiu Nicola 2023-03-13 10:42:24 +02:00
parent 15b867b5db
commit b2f6fd4f96
217 changed files with 12639 additions and 3059 deletions

View file

@ -7,10 +7,15 @@ pub(crate) fn break_outside_of_loop(
ctx: &DiagnosticsContext<'_>,
d: &hir::BreakOutsideOfLoop,
) -> Diagnostic {
let construct = if d.is_break { "break" } else { "continue" };
let message = if d.bad_value_break {
"can't break with a value in this position".to_owned()
} else {
let construct = if d.is_break { "break" } else { "continue" };
format!("{construct} outside of loop")
};
Diagnostic::new(
"break-outside-of-loop",
format!("{construct} outside of loop"),
message,
ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
)
}
@ -132,6 +137,20 @@ fn foo() {
//^^^^^^^^^^^ error: continue outside of loop
}
}
"#,
);
}
#[test]
fn value_break_in_for_loop() {
check_diagnostics(
r#"
fn test() {
for _ in [()] {
break 3;
// ^^^^^^^ error: can't break with a value in this position
}
}
"#,
);
}

View file

@ -0,0 +1,39 @@
use hir::HirDisplay;
use crate::{Diagnostic, DiagnosticsContext};
// Diagnostic: expected-function
//
// This diagnostic is triggered if a call is made on something that is not callable.
pub(crate) fn expected_function(
ctx: &DiagnosticsContext<'_>,
d: &hir::ExpectedFunction,
) -> Diagnostic {
Diagnostic::new(
"expected-function",
format!("expected function, found {}", d.found.display(ctx.sema.db)),
ctx.sema.diagnostics_display_range(d.call.clone().map(|it| it.into())).range,
)
.experimental()
}
#[cfg(test)]
mod tests {
use crate::tests::check_diagnostics;
#[test]
fn smoke_test() {
check_diagnostics(
r#"
fn foo() {
let x = 3;
x();
// ^^^ error: expected function, found i32
""();
// ^^^^ error: expected function, found &str
foo();
}
"#,
);
}
}

View file

@ -5,7 +5,7 @@ use hir::{
};
use ide_db::{
assists::Assist, famous_defs::FamousDefs, imports::import_assets::item_for_path_search,
source_change::SourceChange, use_trivial_contructor::use_trivial_constructor, FxHashMap,
source_change::SourceChange, use_trivial_constructor::use_trivial_constructor, FxHashMap,
};
use stdx::format_to;
use syntax::{

View file

@ -1,5 +1,3 @@
use hir::InFile;
use crate::{Diagnostic, DiagnosticsContext};
// Diagnostic: missing-match-arm
@ -12,7 +10,7 @@ pub(crate) fn missing_match_arms(
Diagnostic::new(
"missing-match-arm",
format!("missing match arm: {}", d.uncovered_patterns),
ctx.sema.diagnostics_display_range(InFile::new(d.file, d.match_expr.clone().into())).range,
ctx.sema.diagnostics_display_range(d.scrutinee_expr.clone().map(Into::into)).range,
)
}
@ -1038,7 +1036,6 @@ fn main() {
#[test]
fn reference_patterns_in_fields() {
cov_mark::check_count!(validate_match_bailed_out, 2);
check_diagnostics(
r#"
fn main() {

View file

@ -0,0 +1,625 @@
use ide_db::source_change::SourceChange;
use syntax::{AstNode, SyntaxKind, SyntaxNode, SyntaxToken, T};
use text_edit::TextEdit;
use crate::{fix, Diagnostic, DiagnosticsContext, Severity};
// Diagnostic: need-mut
//
// This diagnostic is triggered on mutating an immutable variable.
pub(crate) fn need_mut(ctx: &DiagnosticsContext<'_>, d: &hir::NeedMut) -> Diagnostic {
let fixes = (|| {
if d.local.is_ref(ctx.sema.db) {
// There is no simple way to add `mut` to `ref x` and `ref mut x`
return None;
}
let file_id = d.span.file_id.file_id()?;
let mut edit_builder = TextEdit::builder();
let use_range = d.span.value.text_range();
for source in d.local.sources(ctx.sema.db) {
let Some(ast) = source.name() else { continue };
edit_builder.insert(ast.syntax().text_range().start(), "mut ".to_string());
}
let edit = edit_builder.finish();
Some(vec![fix(
"add_mut",
"Change it to be mutable",
SourceChange::from_text_edit(file_id, edit),
use_range,
)])
})();
Diagnostic::new(
"need-mut",
format!("cannot mutate immutable variable `{}`", d.local.name(ctx.sema.db)),
ctx.sema.diagnostics_display_range(d.span.clone()).range,
)
.with_fixes(fixes)
}
// Diagnostic: unused-mut
//
// This diagnostic is triggered when a mutable variable isn't actually mutated.
pub(crate) fn unused_mut(ctx: &DiagnosticsContext<'_>, d: &hir::UnusedMut) -> Diagnostic {
let ast = d.local.primary_source(ctx.sema.db).syntax_ptr();
let fixes = (|| {
let file_id = ast.file_id.file_id()?;
let mut edit_builder = TextEdit::builder();
let use_range = ast.value.text_range();
for source in d.local.sources(ctx.sema.db) {
let ast = source.syntax();
let Some(mut_token) = token(ast, T![mut]) else { continue };
edit_builder.delete(mut_token.text_range());
if let Some(token) = mut_token.next_token() {
if token.kind() == SyntaxKind::WHITESPACE {
edit_builder.delete(token.text_range());
}
}
}
let edit = edit_builder.finish();
Some(vec![fix(
"remove_mut",
"Remove unnecessary `mut`",
SourceChange::from_text_edit(file_id, edit),
use_range,
)])
})();
let ast = d.local.primary_source(ctx.sema.db).syntax_ptr();
Diagnostic::new(
"unused-mut",
"variable does not need to be mutable",
ctx.sema.diagnostics_display_range(ast).range,
)
.severity(Severity::WeakWarning)
.experimental() // Not supporting `#[allow(unused_mut)]` leads to false positive.
.with_fixes(fixes)
}
pub(super) fn token(parent: &SyntaxNode, kind: SyntaxKind) -> Option<SyntaxToken> {
parent.children_with_tokens().filter_map(|it| it.into_token()).find(|it| it.kind() == kind)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_diagnostics, check_fix};
#[test]
fn unused_mut_simple() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = 2;
//^^^^^ 💡 weak: variable does not need to be mutable
f(x);
}
"#,
);
}
#[test]
fn no_false_positive_simple() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let x = 2;
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = 2;
x = 5;
f(x);
}
"#,
);
}
#[test]
fn multiple_errors_for_single_variable() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let x = 2;
x = 10;
//^^^^^^ 💡 error: cannot mutate immutable variable `x`
x = 5;
//^^^^^ 💡 error: cannot mutate immutable variable `x`
&mut x;
//^^^^^^ 💡 error: cannot mutate immutable variable `x`
f(x);
}
"#,
);
}
#[test]
fn unused_mut_fix() {
check_fix(
r#"
fn f(_: i32) {}
fn main() {
let mu$0t x = 2;
f(x);
}
"#,
r#"
fn f(_: i32) {}
fn main() {
let x = 2;
f(x);
}
"#,
);
check_fix(
r#"
fn f(_: i32) {}
fn main() {
let ((mu$0t x, _) | (_, mut x)) = (2, 3);
f(x);
}
"#,
r#"
fn f(_: i32) {}
fn main() {
let ((x, _) | (_, x)) = (2, 3);
f(x);
}
"#,
);
}
#[test]
fn need_mut_fix() {
check_fix(
r#"
fn f(_: i32) {}
fn main() {
let x = 2;
x$0 = 5;
f(x);
}
"#,
r#"
fn f(_: i32) {}
fn main() {
let mut x = 2;
x = 5;
f(x);
}
"#,
);
check_fix(
r#"
fn f(_: i32) {}
fn main() {
let ((x, _) | (_, x)) = (2, 3);
x =$0 4;
f(x);
}
"#,
r#"
fn f(_: i32) {}
fn main() {
let ((mut x, _) | (_, mut x)) = (2, 3);
x = 4;
f(x);
}
"#,
);
check_fix(
r#"
struct Foo(i32);
impl Foo {
fn foo(self) {
self = Fo$0o(5);
}
}
"#,
r#"
struct Foo(i32);
impl Foo {
fn foo(mut self) {
self = Foo(5);
}
}
"#,
);
}
#[test]
fn need_mut_fix_not_applicable_on_ref() {
check_diagnostics(
r#"
fn main() {
let ref x = 2;
x = &5;
//^^^^^^ error: cannot mutate immutable variable `x`
}
"#,
);
check_diagnostics(
r#"
fn main() {
let ref mut x = 2;
x = &mut 5;
//^^^^^^^^^^ error: cannot mutate immutable variable `x`
}
"#,
);
}
#[test]
fn field_mutate() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = (2, 7);
//^^^^^ 💡 weak: variable does not need to be mutable
f(x.1);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x = (2, 7);
x.0 = 5;
f(x.1);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let x = (2, 7);
x.0 = 5;
//^^^^^^^ 💡 error: cannot mutate immutable variable `x`
f(x.1);
}
"#,
);
}
#[test]
fn mutable_reference() {
check_diagnostics(
r#"
fn main() {
let mut x = &mut 2;
//^^^^^ 💡 weak: variable does not need to be mutable
*x = 5;
}
"#,
);
check_diagnostics(
r#"
fn main() {
let x = 2;
&mut x;
//^^^^^^ 💡 error: cannot mutate immutable variable `x`
}
"#,
);
check_diagnostics(
r#"
fn main() {
let x_own = 2;
let ref mut x_ref = x_own;
//^^^^^^^^^^^^^ 💡 error: cannot mutate immutable variable `x_own`
}
"#,
);
check_diagnostics(
r#"
struct Foo;
impl Foo {
fn method(&mut self, x: i32) {}
}
fn main() {
let x = Foo;
x.method(2);
//^ 💡 error: cannot mutate immutable variable `x`
}
"#,
);
}
#[test]
fn regression_14310() {
check_diagnostics(
r#"
fn clone(mut i: &!) -> ! {
//^^^^^ 💡 weak: variable does not need to be mutable
*i
}
"#,
);
}
#[test]
fn match_bindings() {
check_diagnostics(
r#"
fn main() {
match (2, 3) {
(x, mut y) => {
//^^^^^ 💡 weak: variable does not need to be mutable
x = 7;
//^^^^^ 💡 error: cannot mutate immutable variable `x`
}
}
}
"#,
);
}
#[test]
fn mutation_in_dead_code() {
// This one is interesting. Dead code is not represented at all in the MIR, so
// there would be no mutablility error for locals in dead code. Rustc tries to
// not emit `unused_mut` in this case, but since it works without `mut`, and
// special casing it is not trivial, we emit it.
check_diagnostics(
r#"
fn main() {
return;
let mut x = 2;
//^^^^^ 💡 weak: variable does not need to be mutable
&mut x;
}
"#,
);
check_diagnostics(
r#"
fn main() {
loop {}
let mut x = 2;
//^^^^^ 💡 weak: variable does not need to be mutable
&mut x;
}
"#,
);
check_diagnostics(
r#"
enum X {}
fn g() -> X {
loop {}
}
fn f() -> ! {
loop {}
}
fn main(b: bool) {
if b {
f();
} else {
g();
}
let mut x = 2;
//^^^^^ 💡 weak: variable does not need to be mutable
&mut x;
}
"#,
);
check_diagnostics(
r#"
fn main(b: bool) {
if b {
loop {}
} else {
return;
}
let mut x = 2;
//^^^^^ 💡 weak: variable does not need to be mutable
&mut x;
}
"#,
);
}
#[test]
fn initialization_is_not_mutation() {
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let mut x;
//^^^^^ 💡 weak: variable does not need to be mutable
x = 5;
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main(b: bool) {
let mut x;
//^^^^^ 💡 weak: variable does not need to be mutable
if b {
x = 1;
} else {
x = 3;
}
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main(b: bool) {
let x;
if b {
x = 1;
}
x = 3;
//^^^^^ 💡 error: cannot mutate immutable variable `x`
f(x);
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
let x;
loop {
x = 1;
//^^^^^ 💡 error: cannot mutate immutable variable `x`
f(x);
}
}
"#,
);
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
loop {
let mut x = 1;
//^^^^^ 💡 weak: variable does not need to be mutable
f(x);
if let mut y = 2 {
//^^^^^ 💡 weak: variable does not need to be mutable
f(y);
}
match 3 {
mut z => f(z),
//^^^^^ 💡 weak: variable does not need to be mutable
}
}
}
"#,
);
}
#[test]
fn function_arguments_are_initialized() {
check_diagnostics(
r#"
fn f(mut x: i32) {
//^^^^^ 💡 weak: variable does not need to be mutable
}
"#,
);
check_diagnostics(
r#"
fn f(x: i32) {
x = 5;
//^^^^^ 💡 error: cannot mutate immutable variable `x`
}
"#,
);
}
#[test]
fn for_loop() {
check_diagnostics(
r#"
//- minicore: iterators
fn f(x: [(i32, u8); 10]) {
for (a, mut b) in x {
//^^^^^ 💡 weak: variable does not need to be mutable
a = 2;
//^^^^^ 💡 error: cannot mutate immutable variable `a`
}
}
"#,
);
}
#[test]
fn overloaded_deref() {
// FIXME: check for false negative
check_diagnostics(
r#"
//- minicore: deref_mut
use core::ops::{Deref, DerefMut};
struct Foo;
impl Deref for Foo {
type Target = i32;
fn deref(&self) -> &i32 {
&5
}
}
impl DerefMut for Foo {
fn deref_mut(&mut self) -> &mut i32 {
&mut 5
}
}
fn f() {
let x = Foo;
let y = &*x;
let x = Foo;
let mut x = Foo;
let y: &mut i32 = &mut x;
}
"#,
);
}
#[test]
fn or_pattern() {
check_diagnostics(
r#"
//- minicore: option
fn f(_: i32) {}
fn main() {
let ((Some(mut x), None) | (_, Some(mut x))) = (None, Some(7));
//^^^^^ 💡 weak: variable does not need to be mutable
f(x);
}
"#,
);
}
#[test]
fn or_pattern_no_terminator() {
check_diagnostics(
r#"
enum Foo {
A, B, C, D
}
use Foo::*;
fn f(inp: (Foo, Foo, Foo, Foo)) {
let ((A, B, _, x) | (B, C | D, x, _)) = inp else {
return;
};
x = B;
//^^^^^ 💡 error: cannot mutate immutable variable `x`
}
"#,
);
}
#[test]
fn respect_allow_unused_mut() {
// FIXME: respect
check_diagnostics(
r#"
fn f(_: i32) {}
fn main() {
#[allow(unused_mut)]
let mut x = 2;
//^^^^^ 💡 weak: variable does not need to be mutable
f(x);
}
"#,
);
}
}

View file

@ -115,6 +115,44 @@ mod module {
fn main(s: module::Struct) {
s.method();
}
"#,
);
}
#[test]
fn can_see_through_top_level_anonymous_const() {
// regression test for #14046.
check_diagnostics(
r#"
struct S;
mod m {
const _: () = {
impl crate::S {
pub(crate) fn method(self) {}
pub(crate) const A: usize = 42;
}
};
mod inner {
const _: () = {
impl crate::S {
pub(crate) fn method2(self) {}
pub(crate) const B: usize = 42;
pub(super) fn private(self) {}
pub(super) const PRIVATE: usize = 42;
}
};
}
}
fn main() {
S.method();
S::A;
S.method2();
S::B;
S.private();
//^^^^^^^^^^^ error: function `private` is private
S::PRIVATE;
//^^^^^^^^^^ error: const `PRIVATE` is private
}
"#,
);
}

View file

@ -55,7 +55,18 @@ fn fixes(
#[cfg(test)]
mod tests {
use crate::tests::{check_diagnostics, check_fix};
use crate::{
tests::{check_diagnostics_with_config, check_fix},
DiagnosticsConfig,
};
#[track_caller]
pub(crate) fn check_diagnostics(ra_fixture: &str) {
let mut config = DiagnosticsConfig::test_sample();
config.disabled.insert("inactive-code".to_string());
config.disabled.insert("unresolved-method".to_string());
check_diagnostics_with_config(config, ra_fixture)
}
#[test]
fn replace_filter_map_next_with_find_map2() {

View file

@ -1,8 +1,9 @@
use hir::{db::AstDatabase, HirDisplay, Type};
use either::Either;
use hir::{db::AstDatabase, HirDisplay, InFile, Type};
use ide_db::{famous_defs::FamousDefs, source_change::SourceChange};
use syntax::{
ast::{self, BlockExpr, ExprStmt},
AstNode,
AstNode, AstPtr,
};
use text_edit::TextEdit;
@ -10,19 +11,23 @@ use crate::{adjusted_display_range, fix, Assist, Diagnostic, DiagnosticsContext}
// Diagnostic: type-mismatch
//
// This diagnostic is triggered when the type of an expression does not match
// This diagnostic is triggered when the type of an expression or pattern does not match
// the expected type.
pub(crate) fn type_mismatch(ctx: &DiagnosticsContext<'_>, d: &hir::TypeMismatch) -> Diagnostic {
let display_range = adjusted_display_range::<ast::BlockExpr>(
ctx,
d.expr.clone().map(|it| it.into()),
&|block| {
let r_curly_range = block.stmt_list()?.r_curly_token()?.text_range();
cov_mark::hit!(type_mismatch_on_block);
Some(r_curly_range)
},
);
let display_range = match &d.expr_or_pat {
Either::Left(expr) => adjusted_display_range::<ast::BlockExpr>(
ctx,
expr.clone().map(|it| it.into()),
&|block| {
let r_curly_range = block.stmt_list()?.r_curly_token()?.text_range();
cov_mark::hit!(type_mismatch_on_block);
Some(r_curly_range)
},
),
Either::Right(pat) => {
ctx.sema.diagnostics_display_range(pat.clone().map(|it| it.into())).range
}
};
let mut diag = Diagnostic::new(
"type-mismatch",
format!(
@ -42,10 +47,15 @@ pub(crate) fn type_mismatch(ctx: &DiagnosticsContext<'_>, d: &hir::TypeMismatch)
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::TypeMismatch) -> Option<Vec<Assist>> {
let mut fixes = Vec::new();
add_reference(ctx, d, &mut fixes);
add_missing_ok_or_some(ctx, d, &mut fixes);
remove_semicolon(ctx, d, &mut fixes);
str_ref_to_owned(ctx, d, &mut fixes);
match &d.expr_or_pat {
Either::Left(expr_ptr) => {
add_reference(ctx, d, expr_ptr, &mut fixes);
add_missing_ok_or_some(ctx, d, expr_ptr, &mut fixes);
remove_semicolon(ctx, d, expr_ptr, &mut fixes);
str_ref_to_owned(ctx, d, expr_ptr, &mut fixes);
}
Either::Right(_pat_ptr) => {}
}
if fixes.is_empty() {
None
@ -57,9 +67,10 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::TypeMismatch) -> Option<Vec<Assi
fn add_reference(
ctx: &DiagnosticsContext<'_>,
d: &hir::TypeMismatch,
expr_ptr: &InFile<AstPtr<ast::Expr>>,
acc: &mut Vec<Assist>,
) -> Option<()> {
let range = ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range;
let range = ctx.sema.diagnostics_display_range(expr_ptr.clone().map(|it| it.into())).range;
let (_, mutability) = d.expected.as_reference()?;
let actual_with_ref = Type::reference(&d.actual, mutability);
@ -71,7 +82,7 @@ fn add_reference(
let edit = TextEdit::insert(range.start(), ampersands);
let source_change =
SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
SourceChange::from_text_edit(expr_ptr.file_id.original_file(ctx.sema.db), edit);
acc.push(fix("add_reference_here", "Add reference here", source_change, range));
Some(())
}
@ -79,10 +90,11 @@ fn add_reference(
fn add_missing_ok_or_some(
ctx: &DiagnosticsContext<'_>,
d: &hir::TypeMismatch,
expr_ptr: &InFile<AstPtr<ast::Expr>>,
acc: &mut Vec<Assist>,
) -> Option<()> {
let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
let expr = d.expr.value.to_node(&root);
let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id)?;
let expr = expr_ptr.value.to_node(&root);
let expr_range = expr.syntax().text_range();
let scope = ctx.sema.scope(expr.syntax())?;
@ -109,7 +121,7 @@ fn add_missing_ok_or_some(
builder.insert(expr.syntax().text_range().start(), format!("{variant_name}("));
builder.insert(expr.syntax().text_range().end(), ")".to_string());
let source_change =
SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), builder.finish());
SourceChange::from_text_edit(expr_ptr.file_id.original_file(ctx.sema.db), builder.finish());
let name = format!("Wrap in {variant_name}");
acc.push(fix("wrap_in_constructor", &name, source_change, expr_range));
Some(())
@ -118,10 +130,11 @@ fn add_missing_ok_or_some(
fn remove_semicolon(
ctx: &DiagnosticsContext<'_>,
d: &hir::TypeMismatch,
expr_ptr: &InFile<AstPtr<ast::Expr>>,
acc: &mut Vec<Assist>,
) -> Option<()> {
let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
let expr = d.expr.value.to_node(&root);
let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id)?;
let expr = expr_ptr.value.to_node(&root);
if !d.actual.is_unit() {
return None;
}
@ -136,7 +149,7 @@ fn remove_semicolon(
let edit = TextEdit::delete(semicolon_range);
let source_change =
SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
SourceChange::from_text_edit(expr_ptr.file_id.original_file(ctx.sema.db), edit);
acc.push(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon_range));
Some(())
@ -145,24 +158,26 @@ fn remove_semicolon(
fn str_ref_to_owned(
ctx: &DiagnosticsContext<'_>,
d: &hir::TypeMismatch,
expr_ptr: &InFile<AstPtr<ast::Expr>>,
acc: &mut Vec<Assist>,
) -> Option<()> {
let expected = d.expected.display(ctx.sema.db);
let actual = d.actual.display(ctx.sema.db);
// FIXME do this properly
if expected.to_string() != "String" || actual.to_string() != "&str" {
return None;
}
let root = ctx.sema.db.parse_or_expand(d.expr.file_id)?;
let expr = d.expr.value.to_node(&root);
let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id)?;
let expr = expr_ptr.value.to_node(&root);
let expr_range = expr.syntax().text_range();
let to_owned = format!(".to_owned()");
let edit = TextEdit::insert(expr.syntax().text_range().end(), to_owned);
let source_change =
SourceChange::from_text_edit(d.expr.file_id.original_file(ctx.sema.db), edit);
SourceChange::from_text_edit(expr_ptr.file_id.original_file(ctx.sema.db), edit);
acc.push(fix("str_ref_to_owned", "Add .to_owned() here", source_change, expr_range));
Some(())
@ -592,6 +607,21 @@ fn f() -> i32 {
let _ = x + y;
}
//^ error: expected i32, found ()
"#,
);
}
#[test]
fn type_mismatch_pat_smoke_test() {
check_diagnostics(
r#"
fn f() {
let &() = &mut ();
match &() {
&9 => ()
//^ error: expected (), found i32
}
}
"#,
);
}

View file

@ -0,0 +1,148 @@
use hir::{db::AstDatabase, HirDisplay, InFile};
use ide_db::{
assists::{Assist, AssistId, AssistKind},
base_db::FileRange,
label::Label,
source_change::SourceChange,
};
use syntax::{ast, AstNode, AstPtr};
use text_edit::TextEdit;
use crate::{Diagnostic, DiagnosticsContext};
// Diagnostic: unresolved-field
//
// This diagnostic is triggered if a field does not exist on a given type.
pub(crate) fn unresolved_field(
ctx: &DiagnosticsContext<'_>,
d: &hir::UnresolvedField,
) -> Diagnostic {
let method_suffix = if d.method_with_same_name_exists {
", but a method with a similar name exists"
} else {
""
};
Diagnostic::new(
"unresolved-field",
format!(
"no field `{}` on type `{}`{method_suffix}",
d.name,
d.receiver.display(ctx.sema.db)
),
ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
)
.with_fixes(fixes(ctx, d))
.experimental()
}
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedField) -> Option<Vec<Assist>> {
if d.method_with_same_name_exists {
method_fix(ctx, &d.expr)
} else {
// FIXME: add quickfix
None
}
}
// FIXME: We should fill out the call here, mvoe the cursor and trigger signature help
fn method_fix(
ctx: &DiagnosticsContext<'_>,
expr_ptr: &InFile<AstPtr<ast::Expr>>,
) -> Option<Vec<Assist>> {
let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id)?;
let expr = expr_ptr.value.to_node(&root);
let FileRange { range, file_id } = ctx.sema.original_range_opt(expr.syntax())?;
Some(vec![Assist {
id: AssistId("expected-field-found-method-call-fix", AssistKind::QuickFix),
label: Label::new("Use parentheses to call the method".to_string()),
group: None,
target: range,
source_change: Some(SourceChange::from_text_edit(
file_id,
TextEdit::insert(range.end(), "()".to_owned()),
)),
trigger_signature_help: false,
}])
}
#[cfg(test)]
mod tests {
use crate::tests::check_diagnostics;
#[test]
fn smoke_test() {
check_diagnostics(
r#"
fn main() {
().foo;
// ^^^^^^ error: no field `foo` on type `()`
}
"#,
);
}
#[test]
fn method_clash() {
check_diagnostics(
r#"
struct Foo;
impl Foo {
fn bar(&self) {}
}
fn foo() {
Foo.bar;
// ^^^^^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
}
"#,
);
}
#[test]
fn method_trait_() {
check_diagnostics(
r#"
struct Foo;
trait Bar {
fn bar(&self) {}
}
impl Bar for Foo {}
fn foo() {
Foo.bar;
// ^^^^^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
}
"#,
);
}
#[test]
fn method_trait_2() {
check_diagnostics(
r#"
struct Foo;
trait Bar {
fn bar(&self);
}
impl Bar for Foo {
fn bar(&self) {}
}
fn foo() {
Foo.bar;
// ^^^^^^^ 💡 error: no field `bar` on type `Foo`, but a method with a similar name exists
}
"#,
);
}
#[test]
fn no_diagnostic_on_unknown() {
check_diagnostics(
r#"
fn foo() {
x.foo;
(&x).foo;
(&((x,),),).foo;
}
"#,
);
}
}

View file

@ -0,0 +1,131 @@
use hir::{db::AstDatabase, HirDisplay};
use ide_db::{
assists::{Assist, AssistId, AssistKind},
base_db::FileRange,
label::Label,
source_change::SourceChange,
};
use syntax::{ast, AstNode, TextRange};
use text_edit::TextEdit;
use crate::{Diagnostic, DiagnosticsContext};
// Diagnostic: unresolved-method
//
// This diagnostic is triggered if a method does not exist on a given type.
pub(crate) fn unresolved_method(
ctx: &DiagnosticsContext<'_>,
d: &hir::UnresolvedMethodCall,
) -> Diagnostic {
let field_suffix = if d.field_with_same_name.is_some() {
", but a field with a similar name exists"
} else {
""
};
Diagnostic::new(
"unresolved-method",
format!(
"no method `{}` on type `{}`{field_suffix}",
d.name,
d.receiver.display(ctx.sema.db)
),
ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range,
)
.with_fixes(fixes(ctx, d))
.experimental()
}
fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::UnresolvedMethodCall) -> Option<Vec<Assist>> {
if let Some(ty) = &d.field_with_same_name {
field_fix(ctx, d, ty)
} else {
// FIXME: add quickfix
None
}
}
fn field_fix(
ctx: &DiagnosticsContext<'_>,
d: &hir::UnresolvedMethodCall,
ty: &hir::Type,
) -> Option<Vec<Assist>> {
if !ty.impls_fnonce(ctx.sema.db) {
return None;
}
let expr_ptr = &d.expr;
let root = ctx.sema.db.parse_or_expand(expr_ptr.file_id)?;
let expr = expr_ptr.value.to_node(&root);
let (file_id, range) = match expr {
ast::Expr::MethodCallExpr(mcall) => {
let FileRange { range, file_id } =
ctx.sema.original_range_opt(mcall.receiver()?.syntax())?;
let FileRange { range: range2, file_id: file_id2 } =
ctx.sema.original_range_opt(mcall.name_ref()?.syntax())?;
if file_id != file_id2 {
return None;
}
(file_id, TextRange::new(range.start(), range2.end()))
}
_ => return None,
};
Some(vec![Assist {
id: AssistId("expected-method-found-field-fix", AssistKind::QuickFix),
label: Label::new("Use parentheses to call the value of the field".to_string()),
group: None,
target: range,
source_change: Some(SourceChange::from_iter([
(file_id, TextEdit::insert(range.start(), "(".to_owned())),
(file_id, TextEdit::insert(range.end(), ")".to_owned())),
])),
trigger_signature_help: false,
}])
}
#[cfg(test)]
mod tests {
use crate::tests::{check_diagnostics, check_fix};
#[test]
fn smoke_test() {
check_diagnostics(
r#"
fn main() {
().foo();
// ^^^^^^^^ error: no method `foo` on type `()`
}
"#,
);
}
#[test]
fn field() {
check_diagnostics(
r#"
struct Foo { bar: i32 }
fn foo() {
Foo { bar: i32 }.bar();
// ^^^^^^^^^^^^^^^^^^^^^^ error: no method `bar` on type `Foo`, but a field with a similar name exists
}
"#,
);
}
#[test]
fn callable_field() {
check_fix(
r#"
//- minicore: fn
struct Foo { bar: fn() }
fn foo() {
Foo { bar: foo }.b$0ar();
}
"#,
r#"
struct Foo { bar: fn() }
fn foo() {
(Foo { bar: foo }.bar)();
}
"#,
);
}
}

View file

@ -27,6 +27,7 @@
mod handlers {
pub(crate) mod break_outside_of_loop;
pub(crate) mod expected_function;
pub(crate) mod inactive_code;
pub(crate) mod incorrect_case;
pub(crate) mod invalid_derive_target;
@ -36,6 +37,7 @@ mod handlers {
pub(crate) mod missing_fields;
pub(crate) mod missing_match_arms;
pub(crate) mod missing_unsafe;
pub(crate) mod mutability_errors;
pub(crate) mod no_such_field;
pub(crate) mod private_assoc_item;
pub(crate) mod private_field;
@ -43,6 +45,8 @@ mod handlers {
pub(crate) mod type_mismatch;
pub(crate) mod unimplemented_builtin_macro;
pub(crate) mod unresolved_extern_crate;
pub(crate) mod unresolved_field;
pub(crate) mod unresolved_method;
pub(crate) mod unresolved_import;
pub(crate) mod unresolved_macro_call;
pub(crate) mod unresolved_module;
@ -248,6 +252,7 @@ pub fn diagnostics(
#[rustfmt::skip]
let d = match diag {
AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d),
AnyDiagnostic::ExpectedFunction(d) => handlers::expected_function::expected_function(&ctx, &d),
AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d),
AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d),
AnyDiagnostic::MalformedDerive(d) => handlers::malformed_derive::malformed_derive(&ctx, &d),
@ -267,7 +272,10 @@ pub fn diagnostics(
AnyDiagnostic::UnresolvedModule(d) => handlers::unresolved_module::unresolved_module(&ctx, &d),
AnyDiagnostic::UnresolvedProcMacro(d) => handlers::unresolved_proc_macro::unresolved_proc_macro(&ctx, &d, config.proc_macros_enabled, config.proc_attr_macros_enabled),
AnyDiagnostic::InvalidDeriveTarget(d) => handlers::invalid_derive_target::invalid_derive_target(&ctx, &d),
AnyDiagnostic::UnresolvedField(d) => handlers::unresolved_field::unresolved_field(&ctx, &d),
AnyDiagnostic::UnresolvedMethodCall(d) => handlers::unresolved_method::unresolved_method(&ctx, &d),
AnyDiagnostic::NeedMut(d) => handlers::mutability_errors::need_mut(&ctx, &d),
AnyDiagnostic::UnusedMut(d) => handlers::mutability_errors::unused_mut(&ctx, &d),
AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
Some(it) => it,
None => continue,