about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2023-06-19 02:37:26 +0000
committerbors <bors@rust-lang.org>2023-06-19 02:37:26 +0000
commit06b444b2d10284d9be3db8b702654a74e2d2ac5f (patch)
tree7eb9086d502e28cea5004e3ca8014655086a2ed8
parent8c8ff5f31d163afbb9ae5e50a82a4e7439e00309 (diff)
parent2cd4a9182a4b4a1030594866414a3100b594343e (diff)
downloadrust-06b444b2d10284d9be3db8b702654a74e2d2ac5f.tar.gz
rust-06b444b2d10284d9be3db8b702654a74e2d2ac5f.zip
Auto merge of #10951 - Centri3:single_call_fn, r=giraffate
new lint [`single_call_fn`]

Closes #10861

changelog: New lint [`single_call_fn`]
-rw-r--r--CHANGELOG.md1
-rw-r--r--book/src/lint_configuration.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/lib.rs7
-rw-r--r--clippy_lints/src/single_call_fn.rs133
-rw-r--r--clippy_lints/src/utils/conf.rs2
-rw-r--r--tests/ui/single_call_fn.rs75
-rw-r--r--tests/ui/single_call_fn.stderr55
8 files changed, 274 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3eaa0d199fd..6fb7ac740b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5158,6 +5158,7 @@ Released 2018-09-13
 [`significant_drop_in_scrutinee`]: https://rust-lang.github.io/rust-clippy/master/index.html#significant_drop_in_scrutinee
 [`significant_drop_tightening`]: https://rust-lang.github.io/rust-clippy/master/index.html#significant_drop_tightening
 [`similar_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#similar_names
+[`single_call_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_call_fn
 [`single_char_add_str`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_char_add_str
 [`single_char_lifetime_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_char_lifetime_names
 [`single_char_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern
diff --git a/book/src/lint_configuration.md b/book/src/lint_configuration.md
index 78a5f9da461..cc4caae566f 100644
--- a/book/src/lint_configuration.md
+++ b/book/src/lint_configuration.md
@@ -94,6 +94,7 @@ Suppress lints whenever the suggested change would cause breakage for other crat
 * [`linkedlist`](https://rust-lang.github.io/rust-clippy/master/index.html#linkedlist)
 * [`rc_mutex`](https://rust-lang.github.io/rust-clippy/master/index.html#rc_mutex)
 * [`unnecessary_box_returns`](https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_box_returns)
+* [`single_call_fn`](https://rust-lang.github.io/rust-clippy/master/index.html#single_call_fn)
 
 
 ## `msrv`
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 7690e8f7247..9c1e1f6702d 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -570,6 +570,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
     crate::shadow::SHADOW_SAME_INFO,
     crate::shadow::SHADOW_UNRELATED_INFO,
     crate::significant_drop_tightening::SIGNIFICANT_DROP_TIGHTENING_INFO,
+    crate::single_call_fn::SINGLE_CALL_FN_INFO,
     crate::single_char_lifetime_names::SINGLE_CHAR_LIFETIME_NAMES_INFO,
     crate::single_component_path_imports::SINGLE_COMPONENT_PATH_IMPORTS_INFO,
     crate::single_range_in_vec_init::SINGLE_RANGE_IN_VEC_INIT_INFO,
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index 6ea329a9542..939b1136b46 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -287,6 +287,7 @@ mod semicolon_if_nothing_returned;
 mod serde_api;
 mod shadow;
 mod significant_drop_tightening;
+mod single_call_fn;
 mod single_char_lifetime_names;
 mod single_component_path_imports;
 mod single_range_in_vec_init;
@@ -1055,6 +1056,12 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
     store.register_late_pass(move |_| Box::new(large_stack_frames::LargeStackFrames::new(stack_size_threshold)));
     store.register_late_pass(|_| Box::new(single_range_in_vec_init::SingleRangeInVecInit));
     store.register_late_pass(|_| Box::new(incorrect_impls::IncorrectImpls));
+    store.register_late_pass(move |_| {
+        Box::new(single_call_fn::SingleCallFn {
+            avoid_breaking_exported_api,
+            def_id_to_usage: rustc_data_structures::fx::FxHashMap::default(),
+        })
+    });
     // add lints here, do not remove this comment, it's used in `new_lint`
 }
 
diff --git a/clippy_lints/src/single_call_fn.rs b/clippy_lints/src/single_call_fn.rs
new file mode 100644
index 00000000000..42753d2e951
--- /dev/null
+++ b/clippy_lints/src/single_call_fn.rs
@@ -0,0 +1,133 @@
+use clippy_utils::diagnostics::span_lint_and_help;
+use clippy_utils::{is_from_proc_macro, is_in_test_function};
+use rustc_data_structures::fx::FxHashMap;
+use rustc_hir::def_id::LocalDefId;
+use rustc_hir::intravisit::{walk_expr, Visitor};
+use rustc_hir::{intravisit::FnKind, Body, Expr, ExprKind, FnDecl};
+use rustc_lint::{LateContext, LateLintPass, LintContext};
+use rustc_middle::hir::nested_filter::OnlyBodies;
+use rustc_middle::lint::in_external_macro;
+use rustc_session::{declare_tool_lint, impl_lint_pass};
+use rustc_span::Span;
+
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks for functions that are only used once. Does not lint tests.
+    ///
+    /// ### Why is this bad?
+    /// It's usually not, splitting a function into multiple parts often improves readability and in
+    /// the case of generics, can prevent the compiler from duplicating the function dozens of
+    /// time; instead, only duplicating a thunk. But this can prevent segmentation across a
+    /// codebase, where many small functions are used only once.
+    ///
+    /// Note: If this lint is used, prepare to allow this a lot.
+    ///
+    /// ### Example
+    /// ```rust
+    /// pub fn a<T>(t: &T)
+    /// where
+    ///     T: AsRef<str>,
+    /// {
+    ///     a_inner(t.as_ref())
+    /// }
+    ///
+    /// fn a_inner(t: &str) {
+    ///     /* snip */
+    /// }
+    ///
+    /// ```
+    /// Use instead:
+    /// ```rust
+    /// pub fn a<T>(t: &T)
+    /// where
+    ///     T: AsRef<str>,
+    /// {
+    ///     let t = t.as_ref();
+    ///     /* snip */
+    /// }
+    ///
+    /// ```
+    #[clippy::version = "1.72.0"]
+    pub SINGLE_CALL_FN,
+    restriction,
+    "checks for functions that are only used once"
+}
+impl_lint_pass!(SingleCallFn => [SINGLE_CALL_FN]);
+
+#[derive(Clone)]
+pub struct SingleCallFn {
+    pub avoid_breaking_exported_api: bool,
+    pub def_id_to_usage: FxHashMap<LocalDefId, (Span, Vec<Span>)>,
+}
+
+impl<'tcx> LateLintPass<'tcx> for SingleCallFn {
+    fn check_fn(
+        &mut self,
+        cx: &LateContext<'tcx>,
+        kind: FnKind<'tcx>,
+        _: &'tcx FnDecl<'_>,
+        body: &'tcx Body<'_>,
+        span: Span,
+        def_id: LocalDefId,
+    ) {
+        if self.avoid_breaking_exported_api && cx.effective_visibilities.is_exported(def_id)
+            || in_external_macro(cx.sess(), span)
+            || is_from_proc_macro(cx, &(&kind, body, cx.tcx.local_def_id_to_hir_id(def_id), span))
+            || is_in_test_function(cx.tcx, body.value.hir_id)
+        {
+            return;
+        }
+
+        self.def_id_to_usage.insert(def_id, (span, vec![]));
+    }
+
+    fn check_crate_post(&mut self, cx: &LateContext<'tcx>) {
+        let mut v = FnUsageVisitor {
+            cx,
+            def_id_to_usage: &mut self.def_id_to_usage,
+        };
+        cx.tcx.hir().visit_all_item_likes_in_crate(&mut v);
+
+        for usage in self.def_id_to_usage.values() {
+            let single_call_fn_span = usage.0;
+            if let [caller_span] = *usage.1 {
+                span_lint_and_help(
+                    cx,
+                    SINGLE_CALL_FN,
+                    single_call_fn_span,
+                    "this function is only used once",
+                    Some(caller_span),
+                    "used here",
+                );
+            }
+        }
+    }
+}
+
+struct FnUsageVisitor<'a, 'tcx> {
+    cx: &'a LateContext<'tcx>,
+    def_id_to_usage: &'a mut FxHashMap<LocalDefId, (Span, Vec<Span>)>,
+}
+
+impl<'a, 'tcx> Visitor<'tcx> for FnUsageVisitor<'a, 'tcx> {
+    type NestedFilter = OnlyBodies;
+
+    fn nested_visit_map(&mut self) -> Self::Map {
+        self.cx.tcx.hir()
+    }
+
+    fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
+        let Self { cx, .. } = *self;
+
+        if let ExprKind::Path(qpath) = expr.kind
+            && let res = cx.qpath_res(&qpath, expr.hir_id)
+            && let Some(call_def_id) = res.opt_def_id()
+            && let Some(def_id) = call_def_id.as_local()
+            && let Some(usage) = self.def_id_to_usage.get_mut(&def_id)
+        {
+            usage.1.push(expr.span);
+        }
+
+        walk_expr(self, expr);
+    }
+}
diff --git a/clippy_lints/src/utils/conf.rs b/clippy_lints/src/utils/conf.rs
index e8c8d478ffc..987e2abec50 100644
--- a/clippy_lints/src/utils/conf.rs
+++ b/clippy_lints/src/utils/conf.rs
@@ -289,7 +289,7 @@ define_Conf! {
     /// arithmetic-side-effects-allowed-unary = ["SomeType", "AnotherType"]
     /// ```
     (arithmetic_side_effects_allowed_unary: rustc_data_structures::fx::FxHashSet<String> = <_>::default()),
-    /// Lint: ENUM_VARIANT_NAMES, LARGE_TYPES_PASSED_BY_VALUE, TRIVIALLY_COPY_PASS_BY_REF, UNNECESSARY_WRAPS, UNUSED_SELF, UPPER_CASE_ACRONYMS, WRONG_SELF_CONVENTION, BOX_COLLECTION, REDUNDANT_ALLOCATION, RC_BUFFER, VEC_BOX, OPTION_OPTION, LINKEDLIST, RC_MUTEX, UNNECESSARY_BOX_RETURNS.
+    /// Lint: ENUM_VARIANT_NAMES, LARGE_TYPES_PASSED_BY_VALUE, TRIVIALLY_COPY_PASS_BY_REF, UNNECESSARY_WRAPS, UNUSED_SELF, UPPER_CASE_ACRONYMS, WRONG_SELF_CONVENTION, BOX_COLLECTION, REDUNDANT_ALLOCATION, RC_BUFFER, VEC_BOX, OPTION_OPTION, LINKEDLIST, RC_MUTEX, UNNECESSARY_BOX_RETURNS, SINGLE_CALL_FN.
     ///
     /// Suppress lints whenever the suggested change would cause breakage for other crates.
     (avoid_breaking_exported_api: bool = true),
diff --git a/tests/ui/single_call_fn.rs b/tests/ui/single_call_fn.rs
new file mode 100644
index 00000000000..bfb773187fb
--- /dev/null
+++ b/tests/ui/single_call_fn.rs
@@ -0,0 +1,75 @@
+//@aux-build:proc_macros.rs
+//@compile-flags: --test
+#![allow(clippy::redundant_closure_call, unused)]
+#![warn(clippy::single_call_fn)]
+#![no_main]
+
+#[macro_use]
+extern crate proc_macros;
+
+// Do not lint since it's public
+pub fn f() {}
+
+fn i() {}
+fn j() {}
+
+fn h() {
+    // Linted
+    let a = i;
+    // Do not lint closures
+    let a = (|| {
+        // Not linted
+        a();
+        // Imo, it's reasonable to lint this as the function is still only being used once. Just in
+        // a closure.
+        j();
+    });
+    a();
+}
+
+fn g() {
+    f();
+}
+
+fn c() {
+    println!("really");
+    println!("long");
+    println!("function...");
+}
+
+fn d() {
+    c();
+}
+
+fn a() {}
+
+fn b() {
+    a();
+
+    external! {
+        fn lol() {
+            lol_inner();
+        }
+        fn lol_inner() {}
+    }
+    with_span! {
+        span
+        fn lol2() {
+            lol2_inner();
+        }
+        fn lol2_inner() {}
+    }
+}
+
+fn e() {
+    b();
+    b();
+}
+
+#[test]
+fn k() {}
+
+#[test]
+fn l() {
+    k();
+}
diff --git a/tests/ui/single_call_fn.stderr b/tests/ui/single_call_fn.stderr
new file mode 100644
index 00000000000..bb92e3cf71a
--- /dev/null
+++ b/tests/ui/single_call_fn.stderr
@@ -0,0 +1,55 @@
+error: this function is only used once
+  --> $DIR/single_call_fn.rs:34:1
+   |
+LL | / fn c() {
+LL | |     println!("really");
+LL | |     println!("long");
+LL | |     println!("function...");
+LL | | }
+   | |_^
+   |
+help: used here
+  --> $DIR/single_call_fn.rs:41:5
+   |
+LL |     c();
+   |     ^
+   = note: `-D clippy::single-call-fn` implied by `-D warnings`
+
+error: this function is only used once
+  --> $DIR/single_call_fn.rs:13:1
+   |
+LL | fn i() {}
+   | ^^^^^^^^^
+   |
+help: used here
+  --> $DIR/single_call_fn.rs:18:13
+   |
+LL |     let a = i;
+   |             ^
+
+error: this function is only used once
+  --> $DIR/single_call_fn.rs:44:1
+   |
+LL | fn a() {}
+   | ^^^^^^^^^
+   |
+help: used here
+  --> $DIR/single_call_fn.rs:47:5
+   |
+LL |     a();
+   |     ^
+
+error: this function is only used once
+  --> $DIR/single_call_fn.rs:14:1
+   |
+LL | fn j() {}
+   | ^^^^^^^^^
+   |
+help: used here
+  --> $DIR/single_call_fn.rs:25:9
+   |
+LL |         j();
+   |         ^
+
+error: aborting due to 4 previous errors
+