blob: 84276e321657399e2d8d1439835f95af6f3a9a13 [file] [log] [blame]
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::is_test_function;
use clippy_utils::visitors::for_each_expr;
use rustc_errors::Applicability;
use rustc_hir::intravisit::FnKind;
use rustc_hir::{self as hir, Body, ExprKind, FnDecl};
use rustc_lexer::is_ident;
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::def_id::LocalDefId;
use rustc_span::{Span, Symbol, edition};
use std::borrow::Cow;
use std::ops::ControlFlow;
declare_clippy_lint! {
/// ### What it does
/// Checks for test functions (functions annotated with `#[test]`) that are prefixed
/// with `test_` which is redundant.
///
/// ### Why is this bad?
/// This is redundant because test functions are already annotated with `#[test]`.
/// Moreover, it clutters the output of `cargo test` since test functions are expanded as
/// `module::tests::test_use_case` in the output. Without the redundant prefix, the output
/// becomes `module::tests::use_case`, which is more readable.
///
/// ### Example
/// ```no_run
/// #[cfg(test)]
/// mod tests {
/// use super::*;
///
/// #[test]
/// fn test_use_case() {
/// // test code
/// }
/// }
/// ```
/// Use instead:
/// ```no_run
/// #[cfg(test)]
/// mod tests {
/// use super::*;
///
/// #[test]
/// fn use_case() {
/// // test code
/// }
/// }
/// ```
#[clippy::version = "1.88.0"]
pub REDUNDANT_TEST_PREFIX,
restriction,
"redundant `test_` prefix in test function name"
}
declare_lint_pass!(RedundantTestPrefix => [REDUNDANT_TEST_PREFIX]);
impl<'tcx> LateLintPass<'tcx> for RedundantTestPrefix {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
kind: FnKind<'_>,
_decl: &FnDecl<'_>,
body: &'tcx Body<'_>,
_span: Span,
fn_def_id: LocalDefId,
) {
// Ignore methods and closures.
let FnKind::ItemFn(ref ident, ..) = kind else {
return;
};
// Skip the lint if the function is within a macro expansion.
if ident.span.from_expansion() {
return;
}
// Skip if the function name does not start with `test_`.
if !ident.as_str().starts_with("test_") {
return;
}
// If the function is not a test function, skip the lint.
if !is_test_function(cx.tcx, fn_def_id) {
return;
}
span_lint_and_then(
cx,
REDUNDANT_TEST_PREFIX,
ident.span,
"redundant `test_` prefix in test function name",
|diag| {
let non_prefixed = Symbol::intern(ident.as_str().trim_start_matches("test_"));
if is_invalid_ident(non_prefixed) {
// If the prefix-trimmed name is not a valid function name, do not provide an
// automatic fix, just suggest renaming the function.
diag.help(
"consider function renaming (just removing `test_` prefix will produce invalid function name)",
);
} else {
let (sugg, msg): (Cow<'_, str>, _) = if name_conflicts(cx, body, non_prefixed) {
// If `non_prefixed` conflicts with another function in the same module/scope,
// do not provide an automatic fix, but still emit a fix suggestion.
(
format!("{non_prefixed}_works").into(),
"consider function renaming (just removing `test_` prefix will cause a name conflict)",
)
} else {
// If `non_prefixed` is a valid identifier and does not conflict with another function,
// so we can suggest an auto-fix.
(non_prefixed.as_str().into(), "consider removing the `test_` prefix")
};
diag.span_suggestion(ident.span, msg, sugg, Applicability::MaybeIncorrect);
}
},
);
}
}
/// Checks whether removal of the `_test` prefix from the function name will cause a name conflict.
///
/// There should be no other function with the same name in the same module/scope. Also, there
/// should not be any function call with the same name within the body of the function, to avoid
/// recursion.
fn name_conflicts<'tcx>(cx: &LateContext<'tcx>, body: &'tcx Body<'_>, fn_name: Symbol) -> bool {
let tcx = cx.tcx;
let id = body.id().hir_id;
// Iterate over items in the same module/scope
let (module, _module_span, _module_hir) = tcx.hir_get_module(tcx.parent_module(id));
if module
.item_ids
.iter()
.any(|item| matches!(tcx.hir_item(*item).kind, hir::ItemKind::Fn { ident, .. } if ident.name == fn_name))
{
// Name conflict found
return true;
}
// Also check that within the body of the function there is also no function call
// with the same name (since it will result in recursion)
for_each_expr(cx, body, |expr| {
if let ExprKind::Path(qpath) = &expr.kind
&& let Some(def_id) = cx.qpath_res(qpath, expr.hir_id).opt_def_id()
&& let Some(name) = tcx.opt_item_name(def_id)
&& name == fn_name
{
// Function call with the same name found
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
})
.is_some()
}
fn is_invalid_ident(ident: Symbol) -> bool {
// The identifier is either a reserved keyword, or starts with an invalid sequence.
ident.is_reserved(|| edition::LATEST_STABLE_EDITION) || !is_ident(ident.as_str())
}