-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Lint for if let Some(x) = ... instead of Option::map_or
- Loading branch information
1 parent
f1fb815
commit ff4da34
Showing
7 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
use crate::utils::sugg::Sugg; | ||
use crate::utils::{match_type, paths, span_lint_and_sugg}; | ||
use if_chain::if_chain; | ||
|
||
use rustc_errors::Applicability; | ||
use rustc_hir::intravisit::{NestedVisitorMap, Visitor}; | ||
use rustc_hir::*; | ||
use rustc_lint::{LateContext, LateLintPass}; | ||
use rustc_middle::hir::map::Map; | ||
use rustc_session::{declare_lint_pass, declare_tool_lint}; | ||
|
||
use std::marker::PhantomData; | ||
|
||
declare_clippy_lint! { | ||
/// **What it does:** | ||
/// Lints usage of `if let Some(v) = ... { y } else { x }` which is more | ||
/// idiomatically done with `Option::map_or` (if the else bit is a simple | ||
/// expression) or `Option::map_or_else` (if the else bit is a longer | ||
/// block). | ||
/// | ||
/// **Why is this bad?** | ||
/// Using the dedicated functions of the Option type is clearer and | ||
/// more concise than an if let expression. | ||
/// | ||
/// **Known problems:** | ||
/// This lint uses whether the block is just an expression or if it has | ||
/// more statements to decide whether to use `Option::map_or` or | ||
/// `Option::map_or_else`. If you have a single expression which calls | ||
/// an expensive function, then it would be more efficient to use | ||
/// `Option::map_or_else`, but this lint would suggest `Option::map_or`. | ||
/// | ||
/// Also, this lint uses a deliberately conservative metric for checking | ||
/// if the inside of either body contains breaks or continues which will | ||
/// cause it to not suggest a fix if either block contains a loop with | ||
/// continues or breaks contained within the loop. | ||
/// | ||
/// **Example:** | ||
/// | ||
/// ```rust | ||
/// # let optional: Option<u32> = Some(0); | ||
/// let _ = if let Some(foo) = optional { | ||
/// foo | ||
/// } else { | ||
/// 5 | ||
/// }; | ||
/// let _ = if let Some(foo) = optional { | ||
/// foo | ||
/// } else { | ||
/// let y = do_complicated_function(); | ||
/// y*y | ||
/// }; | ||
/// ``` | ||
/// | ||
/// should be | ||
/// | ||
/// ```rust | ||
/// # let optional: Option<u32> = Some(0); | ||
/// let _ = optional.map_or(5, |foo| foo); | ||
/// let _ = optional.map_or_else(||{ | ||
/// let y = do_complicated_function; | ||
/// y*y | ||
/// }, |foo| foo); | ||
/// ``` | ||
pub OPTION_IF_LET_ELSE, | ||
style, | ||
"reimplementation of Option::map_or" | ||
} | ||
|
||
declare_lint_pass!(OptionIfLetElse => [OPTION_IF_LET_ELSE]); | ||
|
||
/// Returns true iff the given expression is the result of calling Result::ok | ||
fn is_result_ok(cx: &LateContext<'_, '_>, expr: &'_ Expr<'_>) -> bool { | ||
if_chain! { | ||
if let ExprKind::MethodCall(ref path, _, &[ref receiver]) = &expr.kind; | ||
if path.ident.name.to_ident_string() == "ok"; | ||
if match_type(cx, &cx.tables.expr_ty(&receiver), &paths::RESULT); | ||
then { | ||
true | ||
} else { | ||
false | ||
} | ||
} | ||
} | ||
|
||
/// A struct containing information about occurences of the | ||
/// `if let Some(..) = .. else` construct that this lint detects. | ||
struct OptionIfLetElseOccurence { | ||
option: String, | ||
method_sugg: String, | ||
some_expr: String, | ||
none_expr: String, | ||
} | ||
|
||
struct ReturnBreakContinueVisitor<'tcx> { | ||
seen_return_break_continue: bool, | ||
phantom_data: PhantomData<&'tcx bool>, | ||
} | ||
impl<'tcx> ReturnBreakContinueVisitor<'tcx> { | ||
fn new() -> ReturnBreakContinueVisitor<'tcx> { | ||
ReturnBreakContinueVisitor { | ||
seen_return_break_continue: false, | ||
phantom_data: PhantomData, | ||
} | ||
} | ||
} | ||
impl<'tcx> Visitor<'tcx> for ReturnBreakContinueVisitor<'tcx> { | ||
type Map = Map<'tcx>; | ||
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> { | ||
NestedVisitorMap::None | ||
} | ||
|
||
fn visit_expr(&mut self, ex: &'tcx Expr<'tcx>) { | ||
if self.seen_return_break_continue { | ||
// No need to look farther if we've already seen one of them | ||
return; | ||
} | ||
match &ex.kind { | ||
ExprKind::Ret(..) | ExprKind::Break(..) | ExprKind::Continue(..) => { | ||
self.seen_return_break_continue = true; | ||
}, | ||
// Something special could be done here to handle while or for loop | ||
// desugaring, as this will detect a break if there's a while loop | ||
// or a for loop inside the expression. | ||
_ => { | ||
rustc_hir::intravisit::walk_expr(self, ex); | ||
}, | ||
} | ||
} | ||
} | ||
|
||
fn contains_return_break_continue<'tcx>(expression: &'tcx Expr<'tcx>) -> bool { | ||
let mut recursive_visitor: ReturnBreakContinueVisitor<'tcx> = ReturnBreakContinueVisitor::new(); | ||
recursive_visitor.visit_expr(expression); | ||
recursive_visitor.seen_return_break_continue | ||
} | ||
|
||
/// If this expression is the option if let/else construct we're detecting, then | ||
/// this function returns an OptionIfLetElseOccurence struct with details if | ||
/// this construct is found, or None if this construct is not found. | ||
fn detect_option_if_let_else<'a>(cx: &LateContext<'_, 'a>, expr: &'a Expr<'a>) -> Option<OptionIfLetElseOccurence> { | ||
//(String, String, String, String)> { | ||
if_chain! { | ||
if let ExprKind::Match(let_body, arms, MatchSource::IfLetDesugar{contains_else_clause: true}) = &expr.kind; | ||
if arms.len() == 2; | ||
if match_type(cx, &cx.tables.expr_ty(let_body), &paths::OPTION); | ||
if !is_result_ok(cx, let_body); // Don't lint on Result::ok because a different lint does it already | ||
if let PatKind::TupleStruct(_, &[inner_pat], _) = &arms[0].pat.kind; | ||
if let PatKind::Binding(_, _, id, _) = &inner_pat.kind; | ||
if !contains_return_break_continue(arms[0].body); | ||
if !contains_return_break_continue(arms[1].body); | ||
then { | ||
let some_body = if let ExprKind::Block(Block { stmts: statements, expr: Some(expr), .. }, _) | ||
= &arms[0].body.kind { | ||
if let &[] = &statements { | ||
expr | ||
} else { | ||
&arms[0].body | ||
} | ||
} else { | ||
return None; | ||
}; | ||
let (none_body, method_sugg) = if let ExprKind::Block(Block { stmts: statements, expr: Some(expr), .. }, _) | ||
= &arms[1].body.kind { | ||
if let &[] = &statements { | ||
(expr, "map_or") | ||
} else { | ||
(&arms[1].body, "map_or_else") | ||
} | ||
} else { | ||
return None; | ||
}; | ||
let capture_name = id.name.to_ident_string(); | ||
Some(OptionIfLetElseOccurence { | ||
option: format!("{}", Sugg::hir(cx, let_body, "..")), | ||
method_sugg: format!("{}", method_sugg), | ||
some_expr: format!("|{}| {}", capture_name, Sugg::hir(cx, some_body, "..")), | ||
none_expr: format!("{}{}", if method_sugg == "map_or" { "" } else { "|| " }, Sugg::hir(cx, none_body, "..")) | ||
}) | ||
} else { | ||
None | ||
} | ||
} | ||
} | ||
|
||
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for OptionIfLetElse { | ||
fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr<'_>) { | ||
if let Some(detection) = detect_option_if_let_else(cx, expr) { | ||
span_lint_and_sugg( | ||
cx, | ||
OPTION_IF_LET_ELSE, | ||
expr.span, | ||
format!("use Option::{} instead of an if let/else", detection.method_sugg).as_str(), | ||
"try", | ||
format!( | ||
"{}.{}({}, {})", | ||
detection.option, detection.method_sugg, detection.none_expr, detection.some_expr | ||
), | ||
Applicability::MachineApplicable, | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// run-rustfix | ||
#![warn(clippy::option_if_let_else)] | ||
|
||
fn bad1(string: Option<&str>) -> (bool, &str) { | ||
string.map_or((false, "hello"), |x| (true, x)) | ||
} | ||
|
||
fn longer_body(arg: Option<u32>) -> u32 { | ||
arg.map_or(13, |x| { | ||
let y = x * x; | ||
y * y | ||
}) | ||
} | ||
|
||
fn test_map_or_else(arg: Option<u32>) { | ||
let _ = arg.map_or_else(|| { | ||
let mut y = 1; | ||
y = (y + 2 / y) / 2; | ||
y = (y + 2 / y) / 2; | ||
y | ||
}, |x| x * x * x * x); | ||
} | ||
|
||
fn negative_tests(arg: Option<u32>) -> u32 { | ||
let _ = if let Some(13) = arg { "unlucky" } else { "lucky" }; | ||
for _ in 0..10 { | ||
let _ = if let Some(x) = arg { | ||
x | ||
} else { | ||
continue; | ||
}; | ||
} | ||
let _ = if let Some(x) = arg { | ||
return x; | ||
} else { | ||
5 | ||
}; | ||
7 | ||
} | ||
|
||
fn main() { | ||
let optional = Some(5); | ||
let _ = optional.map_or(5, |x| x + 2); | ||
let _ = bad1(None); | ||
let _ = longer_body(None); | ||
test_map_or_else(None); | ||
let _ = negative_tests(None); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
// run-rustfix | ||
#![warn(clippy::option_if_let_else)] | ||
|
||
fn bad1(string: Option<&str>) -> (bool, &str) { | ||
if let Some(x) = string { | ||
(true, x) | ||
} else { | ||
(false, "hello") | ||
} | ||
} | ||
|
||
fn longer_body(arg: Option<u32>) -> u32 { | ||
if let Some(x) = arg { | ||
let y = x * x; | ||
y * y | ||
} else { | ||
13 | ||
} | ||
} | ||
|
||
fn test_map_or_else(arg: Option<u32>) { | ||
let _ = if let Some(x) = arg { | ||
x * x * x * x | ||
} else { | ||
let mut y = 1; | ||
y = (y + 2 / y) / 2; | ||
y = (y + 2 / y) / 2; | ||
y | ||
}; | ||
} | ||
|
||
fn negative_tests(arg: Option<u32>) -> u32 { | ||
let _ = if let Some(13) = arg { "unlucky" } else { "lucky" }; | ||
for _ in 0..10 { | ||
let _ = if let Some(x) = arg { | ||
x | ||
} else { | ||
continue; | ||
}; | ||
} | ||
let _ = if let Some(x) = arg { | ||
return x; | ||
} else { | ||
5 | ||
}; | ||
7 | ||
} | ||
|
||
fn main() { | ||
let optional = Some(5); | ||
let _ = if let Some(x) = optional { x + 2 } else { 5 }; | ||
let _ = bad1(None); | ||
let _ = longer_body(None); | ||
test_map_or_else(None); | ||
let _ = negative_tests(None); | ||
} |
Oops, something went wrong.