-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Add lints for discouraging eager / lazy function arguments #17108
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| use clippy_utils::diagnostics::span_lint_and_note; | ||
| use clippy_utils::eager_or_lazy::switch_to_eager_eval; | ||
| use clippy_utils::{get_builtin_attr, is_from_proc_macro, sym, usage}; | ||
| use rustc_hir::def_id::LocalDefId; | ||
| use rustc_hir::intravisit::{FnKind, Visitor, walk_body, walk_expr}; | ||
| use rustc_hir::{Body, Closure, ClosureKind, Expr, ExprKind, FnDecl}; | ||
| use rustc_lint::{LateContext, LateLintPass, LintContext}; | ||
| use rustc_session::declare_lint_pass; | ||
| use rustc_span::Span; | ||
|
|
||
| declare_clippy_lint! { | ||
| /// ### What it does | ||
| /// | ||
| /// As the counterpart to `eager_fun_call`, this lint looks for unnecessarily lazily evaluated closures used to | ||
| /// produce arguments for functions marked with `#[clippy::optional_lazy_eval]`. | ||
| /// | ||
| /// ### Why is this bad? | ||
| /// | ||
| /// Using eager evaluation is shorter and simpler in some cases. | ||
| /// | ||
| /// ### Known Problems | ||
| /// | ||
| /// It is possible, but not recommended for `Deref` and `Index` to have side effects. Eagerly evaluating them can change the semantics of the program. | ||
| /// | ||
| /// ### Example | ||
| /// ```rust,ignore | ||
| /// fn foo(argument: String) { | ||
| /// // Perform some logic that only rarely involves the use of `argument` | ||
| /// } | ||
| /// | ||
| /// #[clippy::optional_lazy_eval = "If `argument` does not require evaluation, prefer using `foo` instead."] | ||
| /// fn lazy_foo<F>(argument: F) | ||
| /// where | ||
| /// F: FnOnce() -> String | ||
| /// { | ||
| /// /// Perform some logic and evaluate `argument` on the fly, only if it is needed. | ||
| /// } | ||
| /// | ||
| /// let s = String::new("bar"); | ||
| /// lazy_foo(move || s); | ||
| /// ``` | ||
| /// Use instead: | ||
| /// ```rust,ignore | ||
| /// let s = String::new("bar"); | ||
| /// foo(s); | ||
| /// ``` | ||
| #[clippy::version = "1.97.0"] | ||
| pub DISCOURAGED_LAZY_EVALUATION, | ||
| nursery, | ||
| "calling a function with an unnecessary closure used to produce an argument" | ||
| } | ||
|
|
||
| declare_lint_pass!(DiscouragedLazyEvaluation => [DISCOURAGED_LAZY_EVALUATION]); | ||
|
|
||
| impl<'tcx> LateLintPass<'tcx> for DiscouragedLazyEvaluation { | ||
| fn check_fn( | ||
| &mut self, | ||
| cx: &LateContext<'tcx>, | ||
| _: FnKind<'_>, | ||
| _: &FnDecl<'_>, | ||
| body: &'tcx Body<'_>, | ||
| _: Span, | ||
| _: LocalDefId, | ||
| ) { | ||
| let mut visitor = DiscouragedLazyEvaluationVisitor { cx }; | ||
| walk_body(&mut visitor, body); | ||
| } | ||
| } | ||
|
|
||
| struct DiscouragedLazyEvaluationVisitor<'cx, 'tcx> { | ||
| cx: &'cx LateContext<'tcx>, | ||
| } | ||
|
|
||
| impl<'tcx> Visitor<'tcx> for DiscouragedLazyEvaluationVisitor<'_, 'tcx> { | ||
| fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { | ||
| if let ExprKind::Call(func, args) = expr.kind | ||
| && let ExprKind::Path(ref qpath) = func.kind | ||
| && let Some(def_id) = self.cx.qpath_res(qpath, func.hir_id).opt_def_id() | ||
|
Comment on lines
+76
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't handle method calls. Calls to functions without arguments should also not be checked any further. |
||
| { | ||
| #[allow(deprecated)] | ||
| let attrs = self.cx.tcx.get_all_attrs(def_id); | ||
| let mut lazy_attrs = get_builtin_attr(self.cx.sess(), attrs, sym::optional_lazy_eval); | ||
| if let Some(attr) = lazy_attrs.next() { | ||
| let Some(message) = attr.value_str() else { | ||
| walk_expr(self, expr); | ||
| return; | ||
| }; | ||
|
|
||
| let lazy_args: Vec<_> = args | ||
| .iter() | ||
| .filter(|arg| { | ||
| let ExprKind::Closure(&Closure { | ||
| body, | ||
| kind: ClosureKind::Closure, | ||
| .. | ||
| }) = arg.kind | ||
| else { | ||
| return false; | ||
| }; | ||
|
|
||
| let body = self.cx.tcx.hir_body(body); | ||
| let body_expr = &body.value; | ||
|
|
||
| if usage::BindingUsageFinder::are_params_used(self.cx, body) | ||
| || is_from_proc_macro(self.cx, expr) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| switch_to_eager_eval(self.cx, body_expr) | ||
| }) | ||
| .collect(); | ||
|
|
||
| if !lazy_args.is_empty() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why collect just to check if it's empty. This is what |
||
| span_lint_and_note( | ||
| self.cx, | ||
| DISCOURAGED_LAZY_EVALUATION, | ||
| expr.span, | ||
| message.as_str().to_owned(), | ||
| Some(expr.span), | ||
| "unnecessary closure used to produce a function argument", | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| walk_expr(self, expr); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| use clippy_utils::diagnostics::span_lint_and_note; | ||
| use clippy_utils::eager_or_lazy::switch_to_lazy_eval; | ||
| use clippy_utils::{get_builtin_attr, sym}; | ||
| use rustc_hir::def_id::LocalDefId; | ||
| use rustc_hir::intravisit::{FnKind, Visitor, walk_body, walk_expr}; | ||
| use rustc_hir::{Body, Expr, ExprKind, FnDecl}; | ||
| use rustc_lint::{LateContext, LateLintPass, LintContext}; | ||
| use rustc_session::declare_lint_pass; | ||
| use rustc_span::Span; | ||
|
|
||
| declare_clippy_lint! { | ||
| /// ### What it does | ||
| /// | ||
| /// Searches for elements marked with `#[clippy::avoid_eager_arguments]` that are being called with eagerly | ||
| /// evaluated arguments. | ||
| /// | ||
| /// ### Why is this bad? | ||
| /// | ||
| /// If the value of an argument is expected to be rarely needed, it is usually better to evaluate that value lazily, | ||
| /// especially if the eager evaluation involves memory allocations or other non-trivial amounts of work. | ||
| /// | ||
| /// ### Known Problems | ||
| /// | ||
| /// If the function that is called to produce the argument has side-effects, not calling it will change the | ||
| /// semantics of the program, but this should not be relied on. | ||
| /// | ||
| /// The lint also cannot figure out whether the function that is being called is actually expensive to call or not. | ||
| /// | ||
| /// ### Example | ||
| /// ```rust,ignore | ||
| /// #[clippy::avoid_eager_arguments = "The value behind `argument` may not always be used, prefer using `lazy_foo` instead"] | ||
| /// fn foo(argument: String) { | ||
| /// // ... | ||
| /// } | ||
| /// | ||
| /// fn lazy_foo<F>(argument: F) | ||
| /// where | ||
| /// F: Fn() -> String | ||
| /// { | ||
| /// // ... | ||
| /// } | ||
| /// | ||
| /// foo(String::new()); | ||
| /// ``` | ||
| /// Use instead: | ||
| /// ```rust,ignore | ||
| /// lazy_foo(|| String::new()); | ||
| /// ``` | ||
| /// | ||
| /// ### Notes | ||
| /// | ||
| /// Library authors should provide an explanation as to why the eager evaluation is undesirable, and ideally offer | ||
| /// and mention an alternative function that accepts an argument that can be lazily evaluated. | ||
| #[clippy::version = "1.97.0"] | ||
| pub EAGER_FUN_CALL, | ||
| nursery, | ||
| "calling a function with eagerly evaluated arguments where it is discouraged to do so" | ||
| } | ||
|
|
||
| declare_lint_pass!(EagerFunCall => [EAGER_FUN_CALL]); | ||
|
|
||
| impl<'tcx> LateLintPass<'tcx> for EagerFunCall { | ||
| fn check_fn( | ||
| &mut self, | ||
| cx: &LateContext<'tcx>, | ||
| _: FnKind<'_>, | ||
| _: &FnDecl<'_>, | ||
| body: &'tcx Body<'_>, | ||
| _: Span, | ||
| _: LocalDefId, | ||
| ) { | ||
| let mut visitor = EagerFunCallVisitor { cx }; | ||
| walk_body(&mut visitor, body); | ||
| } | ||
| } | ||
|
|
||
| struct EagerFunCallVisitor<'cx, 'tcx> { | ||
| cx: &'cx LateContext<'tcx>, | ||
| } | ||
|
|
||
| impl<'tcx> Visitor<'tcx> for EagerFunCallVisitor<'_, 'tcx> { | ||
| fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) { | ||
| if let ExprKind::Call(func, args) = expr.kind | ||
| && let ExprKind::Path(ref qpath) = func.kind | ||
| && let Some(def_id) = self.cx.qpath_res(qpath, func.hir_id).opt_def_id() | ||
| { | ||
| #[allow(deprecated)] | ||
| let attrs = self.cx.tcx.get_all_attrs(def_id); | ||
| let mut lazy_attrs = get_builtin_attr(self.cx.sess(), attrs, sym::avoid_eager_arguments); | ||
| if let Some(attr) = lazy_attrs.next() { | ||
| let Some(message) = attr.value_str() else { | ||
| walk_expr(self, expr); | ||
| return; | ||
| }; | ||
|
|
||
| let lazy_args: Vec<_> = args.iter().filter(|arg| switch_to_lazy_eval(self.cx, arg)).collect(); | ||
|
|
||
| if !lazy_args.is_empty() { | ||
| span_lint_and_note( | ||
| self.cx, | ||
| EAGER_FUN_CALL, | ||
| expr.span, | ||
| message.as_str().to_owned(), | ||
| Some(expr.span), | ||
| "function call with eagerly evaluated arguments", | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| walk_expr(self, expr); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be
check_expr.