Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a diagnostic with a fix to remove return at the end of functions #11020

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/hir/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ diagnostics![
UnresolvedMacroCall,
UnresolvedModule,
UnresolvedProcMacro,
RemoveTrailingReturn,
];

#[derive(Debug)]
Expand Down Expand Up @@ -174,3 +175,9 @@ pub struct AddReferenceHere {
}

pub use hir_ty::diagnostics::IncorrectCase;

#[derive(Debug)]
pub struct RemoveTrailingReturn {
pub file: HirFileId,
pub return_expr: AstPtr<ast::Expr>,
}
16 changes: 13 additions & 3 deletions crates/hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ pub use crate::{
AddReferenceHere, AnyDiagnostic, BreakOutsideOfLoop, InactiveCode, IncorrectCase,
InvalidDeriveTarget, MacroError, MalformedDerive, MismatchedArgCount, MissingFields,
MissingMatchArms, MissingOkOrSomeInTailExpr, MissingUnsafe, NoSuchField,
RemoveThisSemicolon, ReplaceFilterMapNextWithFindMap, UnimplementedBuiltinMacro,
UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall, UnresolvedModule,
UnresolvedProcMacro,
RemoveThisSemicolon, RemoveTrailingReturn, ReplaceFilterMapNextWithFindMap,
UnimplementedBuiltinMacro, UnresolvedExternCrate, UnresolvedImport, UnresolvedMacroCall,
UnresolvedModule, UnresolvedProcMacro,
},
has_source::HasSource,
semantics::{PathResolution, Semantics, SemanticsScope, TypeInfo},
Expand Down Expand Up @@ -1232,6 +1232,16 @@ impl DefWithBody {
);
}
}

BodyValidationDiagnostic::RemoveTrailingReturn { return_expr } => {
if let Ok(expr) = source_map.expr_syntax(return_expr) {
acc.push(
RemoveTrailingReturn { file: expr.file_id, return_expr: expr.value }
.into(),
);
}
}

BodyValidationDiagnostic::MismatchedArgCount { call_expr, expected, found } => {
match source_map.expr_syntax(call_expr) {
Ok(source_ptr) => acc.push(
Expand Down
28 changes: 28 additions & 0 deletions crates/hir_ty/src/diagnostics/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ pub enum BodyValidationDiagnostic {
arg_expr: ExprId,
mutability: Mutability,
},
RemoveTrailingReturn {
return_expr: ExprId,
},
}

impl BodyValidationDiagnostic {
Expand All @@ -80,6 +83,7 @@ impl ExprValidator {

fn validate_body(&mut self, db: &dyn HirDatabase) {
self.check_for_filter_map_next(db);
self.check_for_trailing_return(db);

let body = db.body(self.owner);

Expand Down Expand Up @@ -141,6 +145,30 @@ impl ExprValidator {
});
}

fn check_for_trailing_return(&mut self, db: &dyn HirDatabase) {
// todo: handle inner functions and lambdas.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inner functions I believe should already be covered by this as they are their own DefWithBodyId again.

For lambdas you will just have to go iterate through all expressions of the body and search for a lambda expression, then do the logic you already do here for it's body expression again

if !matches!(self.owner, DefWithBodyId::FunctionId(_)) {
return;
}

let body = db.body(self.owner);

let return_expr = match &body.exprs[body.body_expr] {
Expr::Block { statements, tail, .. } => tail
.or_else(|| match statements.last()? {
Statement::Expr { expr, .. } => Some(*expr),
_ => None,
})
.filter(|expr| matches!(body.exprs[*expr], Expr::Return { .. })),
Expr::Return { .. } => Some(body.body_expr),
_ => return,
};

if let Some(return_expr) = return_expr {
self.diagnostics.push(BodyValidationDiagnostic::RemoveTrailingReturn { return_expr })
}
}

fn check_for_filter_map_next(&mut self, db: &dyn HirDatabase) {
// Find the FunctionIds for Iterator::filter_map and Iterator::next
let iterator_path = path![core::iter::Iterator];
Expand Down
149 changes: 149 additions & 0 deletions crates/ide_diagnostics/src/handlers/remove_trailing_return.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use hir::{db::AstDatabase, diagnostics::RemoveTrailingReturn, InFile};
use ide_db::{assists::Assist, source_change::SourceChange};
use syntax::{ast, AstNode};
use text_edit::TextEdit;

use crate::{fix, Diagnostic, DiagnosticsContext, Severity};

// Diagnostic: remove-trailing-return
//
// This diagnostic is triggered when there is a redundant `return` at the end of a function.
pub(crate) fn remove_trailing_return(
ctx: &DiagnosticsContext<'_>,
d: &RemoveTrailingReturn,
) -> Diagnostic {
Diagnostic::new(
"remove-trailing-return",
"replace return <expr>; with <expr>",
ctx.sema.diagnostics_display_range(InFile::new(d.file, d.return_expr.clone().into())).range,
)
.severity(Severity::WeakWarning)
.with_fixes(fixes(ctx, d))
}

fn fixes(ctx: &DiagnosticsContext<'_>, d: &RemoveTrailingReturn) -> Option<Vec<Assist>> {
let root = ctx.sema.db.parse_or_expand(d.file)?;

let return_expr = d.return_expr.to_node(&root);

let return_expr = ast::ReturnExpr::cast(return_expr.syntax().clone())?;

let stmt = return_expr.syntax().parent().and_then(ast::ExprStmt::cast);
let range_to_replace = match stmt {
Some(stmt) => stmt.syntax().text_range(),
None => return_expr.syntax().text_range(),
};

let replacement =
return_expr.expr().map_or_else(String::new, |expr| format!("{}", expr.syntax().text()));

// this *seems* like a reasonable range to trigger in?
let trigger_range = range_to_replace;

let edit = TextEdit::replace(range_to_replace, replacement);

let source_change = SourceChange::from_text_edit(d.file.original_file(ctx.sema.db), edit);

Some(vec![fix(
"replace_with_inner",
"Replace return <expr>; with <expr>",
source_change,
trigger_range,
)])
}

#[cfg(test)]
mod tests {
use crate::tests::{check_diagnostics, check_fix};

#[test]
fn remove_trailing_return() {
// fixme: this should include the semi.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm ye, this one is tricky as diagnostics_display_range doesn't really offer you a nice way to go to the parent as is for checking the semi, leaving the FIXME here for the time being is fine I think, since its not a severe problem.

check_diagnostics(
r#"
fn foo() -> i8 {
return 2;
} //^^^^^^^^ 💡 weak: replace return <expr>; with <expr>
"#,
);
}

// fixme: implement this for lambdas and inner functions.
#[test]
fn remove_trailing_return_no_lambda() {
// fixme: this should include the semi.
check_diagnostics(
r#"
fn foo() -> i8 {
let bar = || return 2;
bar()
}
"#,
);
}

#[test]
fn remove_trailing_return_unit() {
check_diagnostics(
r#"
fn foo() -> i8 {
return
} //^^^^^^ 💡 weak: replace return <expr>; with <expr>
"#,
);
}

#[test]
fn remove_trailing_return_no_diagnostic_if_no_return_keyword() {
check_diagnostics(
r#"
fn foo() -> i8 {
3
}
"#,
);
}

#[test]
fn remove_trailing_return_no_diagnostic_if_not_at_and() {
check_diagnostics(
r#"
fn foo() -> i8 {
if true { return 2; }
3
}
"#,
);
}

#[test]
fn replace_with_expr() {
check_fix(
r#"
fn foo() -> i8 {
return$0 2;
}
"#,
r#"
fn foo() -> i8 {
2
}
"#,
);
}
#[test]
fn replace_with_unit() {
check_fix(
r#"
fn foo() {
return$0/*ensure tidy is happy*/
}
"#,
r#"
fn foo() {
/*ensure tidy is happy*/
}
"#,
);
}
}
3 changes: 3 additions & 0 deletions crates/ide_diagnostics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ mod handlers {
pub(crate) mod missing_unsafe;
pub(crate) mod no_such_field;
pub(crate) mod remove_this_semicolon;
pub(crate) mod remove_trailing_return;
pub(crate) mod replace_filter_map_next_with_find_map;
pub(crate) mod unimplemented_builtin_macro;
pub(crate) mod unresolved_extern_crate;
Expand Down Expand Up @@ -204,6 +205,8 @@ pub fn diagnostics(
Some(it) => it,
None => continue,
}

AnyDiagnostic::RemoveTrailingReturn(d) => handlers::remove_trailing_return::remove_trailing_return(&ctx, &d),
};
res.push(d)
}
Expand Down