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

Add unneeded_try_convert lint #4759

Closed
wants to merge 1 commit into from
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,7 @@ Released 2018-09-13
[`unnecessary_operation`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_operation
[`unnecessary_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_unwrap
[`unneeded_field_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#unneeded_field_pattern
[`unneeded_try_convert`]: https://rust-lang.github.io/rust-clippy/master/index.html#unneeded_try_convert
[`unneeded_wildcard_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#unneeded_wildcard_pattern
[`unreachable`]: https://rust-lang.github.io/rust-clippy/master/index.html#unreachable
[`unreadable_literal`]: https://rust-lang.github.io/rust-clippy/master/index.html#unreadable_literal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.

[There are 333 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
[There are 334 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)

We have a bunch of lint categories to allow you to choose how much Clippy is supposed to ~~annoy~~ help you:

Expand Down
5 changes: 5 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ pub mod trivially_copy_pass_by_ref;
pub mod try_err;
pub mod types;
pub mod unicode;
pub mod unneeded_try_convert;
pub mod unsafe_removed_from_name;
pub mod unused_io_amount;
pub mod unused_label;
Expand Down Expand Up @@ -757,6 +758,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
&unicode::NON_ASCII_LITERAL,
&unicode::UNICODE_NOT_NFC,
&unicode::ZERO_WIDTH_SPACE,
&unneeded_try_convert::UNNEEDED_TRY_CONVERT,
&unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME,
&unused_io_amount::UNUSED_IO_AMOUNT,
&unused_label::UNUSED_LABEL,
Expand Down Expand Up @@ -949,6 +951,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
store.register_late_pass(|| box mutable_debug_assertion::DebugAssertWithMutCall);
store.register_late_pass(|| box exit::Exit);
store.register_late_pass(|| box to_digit_is_some::ToDigitIsSome);
store.register_late_pass(|| box unneeded_try_convert::UnneededTryConvert);

store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
LintId::of(&arithmetic::FLOAT_ARITHMETIC),
Expand Down Expand Up @@ -1274,6 +1277,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
LintId::of(&types::UNNECESSARY_CAST),
LintId::of(&types::VEC_BOX),
LintId::of(&unicode::ZERO_WIDTH_SPACE),
LintId::of(&unneeded_try_convert::UNNEEDED_TRY_CONVERT),
LintId::of(&unsafe_removed_from_name::UNSAFE_REMOVED_FROM_NAME),
LintId::of(&unused_io_amount::UNUSED_IO_AMOUNT),
LintId::of(&unused_label::UNUSED_LABEL),
Expand Down Expand Up @@ -1455,6 +1459,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
LintId::of(&types::UNIT_ARG),
LintId::of(&types::UNNECESSARY_CAST),
LintId::of(&types::VEC_BOX),
LintId::of(&unneeded_try_convert::UNNEEDED_TRY_CONVERT),
LintId::of(&unused_label::UNUSED_LABEL),
LintId::of(&unwrap::UNNECESSARY_UNWRAP),
LintId::of(&zero_div_zero::ZERO_DIVIDED_BY_ZERO),
Expand Down
235 changes: 235 additions & 0 deletions clippy_lints/src/unneeded_try_convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use crate::utils::{
is_ctor_or_promotable_const_function, match_def_path, match_type, paths, snippet_with_applicability,
span_lint_and_then,
};
use if_chain::if_chain;
use rustc::hir::intravisit::Visitor;
use rustc::hir::{self, *};
use rustc::lint::{LateContext, LateLintPass, LintArray, LintPass};
use rustc::ty::Ty;
use rustc::{declare_lint_pass, declare_tool_lint};
use rustc_errors::Applicability;
use syntax_pos::Span;

declare_clippy_lint! {
/// **What it does:** Checks for usages of `option.ok_or_else(|| <..>::from(..))?` or
/// `result.map_err(|x| <..>::from(..))`.
///
/// **Why is this bad?** The `?` operator will call `from` in the `Err` case,
/// so calling it manually is redundant.
///
/// **Known problems:** The suggested fix does not correct any explicitly provided
/// type arguments in `ok_or_else` or `map_err`.
///
/// **Example:**
/// ```rust
/// fn bar() -> Result<i32, String> {
/// let x = Some(52.3).ok_or_else(|| String::from("foo"))?;
/// Ok(42)
/// }
/// ```
/// Could be written:
///
/// ```rust
/// fn bar() -> Result<i32, String> {
/// let x = Some(52.3).ok_or("foo")?;
/// Ok(42)
/// }
/// ```
pub UNNEEDED_TRY_CONVERT,
complexity,
"unneeded conversion inside `?`"
}

declare_lint_pass!(UnneededTryConvert => [UNNEEDED_TRY_CONVERT]);

impl<'a, 'tcx> LateLintPass<'a, 'tcx> for UnneededTryConvert {
fn check_expr(&mut self, cx: &LateContext<'a, 'tcx>, expr: &'tcx Expr) {
if_chain! {
if let ExprKind::Match(match_arg, arms, MatchSource::TryDesugar) = &expr.kind;
if let ExprKind::Call(_, args) = &match_arg.kind;
if let [try_arg] = &**args;
if let Some(fn_error_ty) = get_try_err_ty(cx, arms);
then {
check_option_ok_or_else(cx, fn_error_ty, try_arg);
check_result_map_err(cx, fn_error_ty, try_arg);
}
}
}
}

/// Given the arms of a `match` expr from desugaring a `?`, return the error type of the `Try` type
fn get_try_err_ty<'tcx>(cx: &LateContext<'_, 'tcx>, match_arms: &[Arm]) -> Option<Ty<'tcx>> {
if_chain! {
if let [err_arm, _] = match_arms;
if let ExprKind::Ret(Some(ret_expr)) = &err_arm.body.kind;
if let ExprKind::Call(_, try_from_error_args) = &ret_expr.kind;
if let [try_from_error_arg] = &**try_from_error_args;
then {
return Some(cx.tables.expr_ty(try_from_error_arg));
}
}
None
}

fn check_option_ok_or_else<'tcx>(cx: &LateContext<'_, 'tcx>, fn_error_ty: Ty<'tcx>, expr: &Expr) {
if_chain! {
if let ExprKind::MethodCall(call_path, _, call_args) = &expr.kind;
if call_path.ident.as_str() == "ok_or_else";
if let [receiver, closure_expr] = &**call_args;
if match_type(cx, cx.tables.expr_ty(receiver), &paths::OPTION);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if match_type(cx, cx.tables.expr_ty(receiver), &paths::OPTION);
if is_type_diagnostic_item(cx, cx.tables.expr_ty(receiver), sym!(option_type));

if let Some((closure_body_span, conv_arg)) = check_closure_expr(cx, fn_error_ty, closure_expr);
then {
let mut applicability = Applicability::MachineApplicable;
let conv_arg_snip = snippet_with_applicability(cx, conv_arg.span, "..", &mut applicability);
let (sugg_span, sugg_snip) = if is_trivial_expr(cx, conv_arg) {
// suggest inlining the closure and using `ok_or`
let receiver_snip = snippet_with_applicability(cx, receiver.span, "..", &mut applicability);
(expr.span, format!("{}.ok_or({})", receiver_snip, conv_arg_snip))
} else {
// suggest removing the conversion in the closure
(closure_body_span, conv_arg_snip.into_owned())
};
emit_lint(cx, closure_body_span, sugg_span, sugg_snip, applicability);
}
}
}

fn check_result_map_err<'tcx>(cx: &LateContext<'_, 'tcx>, fn_error_ty: Ty<'tcx>, expr: &Expr) {
if_chain! {
if let ExprKind::MethodCall(call_path, _, call_args) = &expr.kind;
if call_path.ident.as_str() == "map_err";
if let [receiver, mapper_expr] = &**call_args;
let receiver_ty = cx.tables.expr_ty(receiver);
if match_type(cx, receiver_ty, &paths::RESULT);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if match_type(cx, receiver_ty, &paths::RESULT);
if is_type_diagnostic_item(cx, receiver_ty, sym!(result_type));

then {
if let Some((closure_body_span, conv_arg)) = check_closure_expr(cx, fn_error_ty, mapper_expr) {
// suggest removing just the conversion in the closure
let mut applicability = Applicability::MachineApplicable;
let conv_arg_snip = snippet_with_applicability(cx, conv_arg.span, "..", &mut applicability);
emit_lint(
cx,
closure_body_span,
closure_body_span,
conv_arg_snip.into_owned(),
applicability,
);
return;
}
if_chain! {
if let ExprKind::Path(qpath) = &mapper_expr.kind;
if let def::Res::Def(_, def_id) = cx.tables.qpath_res(qpath, mapper_expr.hir_id);
if match_def_path(cx, def_id, &paths::FROM_FROM)
|| match_def_path(cx, def_id, &paths::INTO_INTO);
if *cx.tables.expr_ty(mapper_expr).fn_sig(cx.tcx).output().skip_binder() == fn_error_ty;
then {
// suggest removing the entire `map_err(..)` call
let mut applicability = Applicability::MachineApplicable;
let receiver_snip = snippet_with_applicability(cx, receiver.span, "..", &mut applicability);
emit_lint(
cx,
mapper_expr.span,
expr.span,
receiver_snip.into_owned(),
applicability,
);
}
}
}
}
}

fn emit_lint(cx: &LateContext<'_, '_>, lint_span: Span, sugg_span: Span, sugg: String, applicability: Applicability) {
span_lint_and_then(
cx,
UNNEEDED_TRY_CONVERT,
lint_span,
"unneeded conversion inside `?`",
move |db| {
db.note("the `?` operator will automatically call `from` in the `Err` case");
db.span_suggestion(sugg_span, "remove the conversion", sugg, applicability);
},
);
}

/// If `closure_expr` is a closure whose body is a conversion to `fn_error_ty`,
/// return (the span of the conversion call, the argument of the conversion call)
fn check_closure_expr<'tcx>(
cx: &LateContext<'_, 'tcx>,
fn_error_ty: Ty<'tcx>,
closure_expr: &Expr,
) -> Option<(Span, &'tcx Expr)> {
if_chain! {
if let ExprKind::Closure(_, _, body_id, _, _) = closure_expr.kind;
let closure_body = &cx.tcx.hir().body(body_id).value;
if let Some(conv_arg) = conversion_subject(cx, closure_body);
if cx.tables.expr_ty(closure_body) == fn_error_ty;
then {
return Some((closure_body.span, conv_arg));
}
}
None
}

/// If `expr` is `From::from(<inner>)` or `(<inner>).into()`, returns `<inner>`.
fn conversion_subject<'tcx>(cx: &LateContext<'_, 'tcx>, expr: &'tcx Expr) -> Option<&'tcx Expr> {
if_chain! {
if let ExprKind::Call(fn_expr, from_args) = &expr.kind;
if let ExprKind::Path(fn_qpath) = &fn_expr.kind;
if let def::Res::Def(def::DefKind::Method, fn_did) = cx.tables.qpath_res(fn_qpath, fn_expr.hir_id);
if match_def_path(cx, fn_did, &paths::FROM_FROM);
if let [from_arg] = &**from_args;
then {
return Some(from_arg);
}
}
if_chain! {
if let ExprKind::MethodCall(_, _, args) = &expr.kind;
if let Some(call_did) = cx.tables.type_dependent_def_id(expr.hir_id);
if match_def_path(cx, call_did, &paths::INTO_INTO);
if let [receiver] = &**args;
then {
return Some(receiver);
}
}
None
}

/// Is this expression "trivial" such that a closure containing it could be inlined?
/// (currently very conservative)
fn is_trivial_expr<'tcx>(cx: &LateContext<'_, 'tcx>, expr: &'tcx Expr) -> bool {
struct TrivialVisitor<'a, 'tcx> {
cx: &'a LateContext<'a, 'tcx>,
trivial: bool,
}

impl<'a, 'tcx> intravisit::Visitor<'tcx> for TrivialVisitor<'a, 'tcx> {
fn visit_expr(&mut self, expr: &'tcx hir::Expr) {
// whitelist of definitely trivial expressions
self.trivial &= match &expr.kind {
hir::ExprKind::Call(..) => is_ctor_or_promotable_const_function(self.cx, expr),
hir::ExprKind::Tup(..)
| hir::ExprKind::Lit(..)
| hir::ExprKind::Cast(..)
| hir::ExprKind::Field(..)
| hir::ExprKind::Index(..)
| hir::ExprKind::Path(..)
| hir::ExprKind::AddrOf(..)
| hir::ExprKind::Struct(..) => true,
_ => false,
};

if self.trivial {
intravisit::walk_expr(self, expr);
}
}

fn nested_visit_map<'this>(&'this mut self) -> intravisit::NestedVisitorMap<'this, 'tcx> {
intravisit::NestedVisitorMap::None
}
}

let mut visitor = TrivialVisitor { cx, trivial: true };
visitor.visit_expr(expr);
visitor.trivial
}
1 change: 1 addition & 0 deletions clippy_lints/src/utils/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub const HASHSET: [&str; 5] = ["std", "collections", "hash", "set", "HashSet"];
pub const INDEX: [&str; 3] = ["core", "ops", "Index"];
pub const INDEX_MUT: [&str; 3] = ["core", "ops", "IndexMut"];
pub const INTO: [&str; 3] = ["core", "convert", "Into"];
pub const INTO_INTO: [&str; 4] = ["core", "convert", "Into", "into"];
pub const INTO_ITERATOR: [&str; 5] = ["core", "iter", "traits", "collect", "IntoIterator"];
pub const IO_READ: [&str; 3] = ["std", "io", "Read"];
pub const IO_WRITE: [&str; 3] = ["std", "io", "Write"];
Expand Down
9 changes: 8 additions & 1 deletion src/lintlist/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub use lint::Lint;
pub use lint::LINT_LEVELS;

// begin lint list, do not remove this comment, it’s used in `update_lints`
pub const ALL_LINTS: [Lint; 333] = [
pub const ALL_LINTS: [Lint; 334] = [
Lint {
name: "absurd_extreme_comparisons",
group: "correctness",
Expand Down Expand Up @@ -2086,6 +2086,13 @@ pub const ALL_LINTS: [Lint; 333] = [
deprecation: None,
module: "misc_early",
},
Lint {
name: "unneeded_try_convert",
group: "complexity",
desc: "unneeded conversion inside `?`",
deprecation: None,
module: "unneeded_try_convert",
},
Lint {
name: "unneeded_wildcard_pattern",
group: "complexity",
Expand Down
63 changes: 63 additions & 0 deletions tests/ui/unneeded_try_convert.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// run-rustfix
#![deny(clippy::unneeded_try_convert)]
#![allow(dead_code, unused_imports, clippy::redundant_closure)]

use std::convert::Into;

fn result_string() -> Result<(), String> {
let option = Some(3);
option.ok_or("foo")?;
option.ok_or_else(|| complex_computation())?;
// type arg not fixed
// option.ok_or_else::<String, _>(|| From::from(complex_computation()))?;
// type arg not fixed
// option.ok_or_else::<String, _>(|| "foo".into())?;

let result: Result<_, &'static str> = Ok(3);
result.map_err(|_| "foo")?;
result.map_err(|_| complex_computation())?;
// type arg not fixed
// result.map_err::<String, _>(|_| "foo".into())?;
result.map_err(|x| x)?;
result.map_err(|x| x.trim())?;
result?;
result?;
result?;

Ok(())
}

fn in_closure() {
let option = Some(3);
let _ = || -> Result<_, String> { Ok(option.ok_or("foo")?) };
}

#[allow(clippy::option_option)]
fn trivial_closure() {
let option = Some(3);
let _ = || -> Result<_, i32> { Ok(option.ok_or(0_u8)?) };
let x: u8 = 0;
let _ = || -> Result<_, i32> { Ok(option.ok_or(x)?) };
const X: u8 = 0;
let _ = || -> Result<_, i32> { Ok(option.ok_or(X)?) };
let _ =
|| -> Result<_, Option<Option<i32>>> { Ok(option.ok_or(Some(x as i32))?) };
}

fn result_opt_string() -> Result<(), Option<String>> {
// can't convert &str -> Option<String> in one step
let option = Some(3);
option.ok_or_else(|| String::from("foo"))?;

let result: Result<_, &'static str> = Ok(3);
result.map_err(|_| String::from("foo"))?;
result.map_err(String::from)?;

Ok(())
}

fn complex_computation() -> &'static str {
"bar"
}

fn main() {}
Loading