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 lint for unnecessary lifetime bounded &str return #13395

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -6025,6 +6025,7 @@ Released 2018-09-13
[`unnecessary_get_then_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_get_then_check
[`unnecessary_join`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_join
[`unnecessary_lazy_evaluations`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
[`unnecessary_literal_bound`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_literal_bound
[`unnecessary_literal_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_literal_unwrap
[`unnecessary_map_on_constructor`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_map_on_constructor
[`unnecessary_min_or_max`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_min_or_max
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
crate::unit_types::UNIT_CMP_INFO,
crate::unnamed_address::FN_ADDRESS_COMPARISONS_INFO,
crate::unnecessary_box_returns::UNNECESSARY_BOX_RETURNS_INFO,
crate::unnecessary_literal_bound::UNNECESSARY_LITERAL_BOUND_INFO,
crate::unnecessary_map_on_constructor::UNNECESSARY_MAP_ON_CONSTRUCTOR_INFO,
crate::unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS_INFO,
crate::unnecessary_self_imports::UNNECESSARY_SELF_IMPORTS_INFO,
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ mod unit_return_expecting_ord;
mod unit_types;
mod unnamed_address;
mod unnecessary_box_returns;
mod unnecessary_literal_bound;
mod unnecessary_map_on_constructor;
mod unnecessary_owned_empty_strings;
mod unnecessary_self_imports;
Expand Down Expand Up @@ -942,5 +943,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
store.register_late_pass(move |_| Box::new(manual_div_ceil::ManualDivCeil::new(conf)));
store.register_late_pass(|_| Box::new(manual_is_power_of_two::ManualIsPowerOfTwo));
store.register_late_pass(|_| Box::new(non_zero_suggestions::NonZeroSuggestions));
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
// add lints here, do not remove this comment, it's used in `new_lint`
}
160 changes: 160 additions & 0 deletions clippy_lints/src/unnecessary_literal_bound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use clippy_utils::diagnostics::span_lint_and_sugg;
use rustc_ast::ast::LitKind;
use rustc_errors::Applicability;
use rustc_hir::intravisit::{FnKind, Visitor};
use rustc_hir::{intravisit, Body, Expr, ExprKind, FnDecl, FnRetTy, Lit, MutTy, Mutability, Ty, TyKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::def_id::LocalDefId;
use rustc_span::Span;

declare_clippy_lint! {
/// ### What it does
///
/// Detects functions that are written to return `&str` that could return `&'static str` but instead return a `&'a str`.
///
/// ### Why is this bad?
///
/// This leaves the caller unable to use the `&str` as `&'static str`, causing unneccessary allocations or confusion.
/// This is also most likely what you meant to write.
///
/// ### Example
/// ```no_run
/// # struct MyType;
/// impl MyType {
/// fn returns_literal(&self) -> &str {
/// "Literal"
/// }
/// }
/// ```
/// Use instead:
/// ```no_run
/// # struct MyType;
/// impl MyType {
/// fn returns_literal(&self) -> &'static str {
/// "Literal"
/// }
/// }
/// ```
/// Or, in case you may return a non-literal `str` in future:
/// ```no_run
/// # struct MyType;
/// impl MyType {
/// fn returns_literal<'a>(&'a self) -> &'a str {
/// "Literal"
/// }
/// }
/// ```
#[clippy::version = "1.83.0"]
pub UNNECESSARY_LITERAL_BOUND,
pedantic,
"detects &str that could be &'static str in function return types"
}

declare_lint_pass!(UnnecessaryLiteralBound => [UNNECESSARY_LITERAL_BOUND]);

fn extract_anonymous_ref<'tcx>(hir_ty: &Ty<'tcx>) -> Option<&'tcx Ty<'tcx>> {
let TyKind::Ref(lifetime, MutTy { ty, mutbl }) = hir_ty.kind else {
return None;
};

if !lifetime.is_anonymous() || !matches!(mutbl, Mutability::Not) {
return None;
}

Some(ty)
}

fn is_str_literal(expr: &Expr<'_>) -> bool {
matches!(
expr.kind,
ExprKind::Lit(Lit {
node: LitKind::Str(..),
..
}),
)
}

struct FindNonLiteralReturn {
poison: bool,
}

impl<'hir> Visitor<'hir> for FindNonLiteralReturn {
type NestedFilter = intravisit::nested_filter::None;

fn visit_expr(&mut self, expr: &'hir Expr<'hir>) {
match expr {
Expr {
kind: ExprKind::Ret(Some(ret_val_expr)),
..
} if !is_str_literal(ret_val_expr) => {
self.poison = true;
},
expr => intravisit::walk_expr(self, expr),
}
}
}

fn check_implicit_returns_static_str(body: &Body<'_>) -> bool {
// TODO: Improve this to the same complexity as the Visitor to catch more implicit return cases.
if let ExprKind::Block(block, _) = body.value.kind
&& let Some(implicit_ret) = block.expr
{
return is_str_literal(implicit_ret);
}

false
}

fn check_explicit_returns_static_str(expr: &Expr<'_>) -> bool {
let mut visitor = FindNonLiteralReturn { poison: false };
visitor.visit_expr(expr);
!visitor.poison
}

impl<'tcx> LateLintPass<'tcx> for UnnecessaryLiteralBound {
fn check_fn(
&mut self,
cx: &LateContext<'tcx>,
kind: FnKind<'tcx>,
decl: &'tcx FnDecl<'_>,
body: &'tcx Body<'_>,
span: Span,
_: LocalDefId,
) {
if span.from_expansion() {
return;
}

// Checking closures would be a little silly
if matches!(kind, FnKind::Closure) {
return;
}

// Check for `-> &str`
let FnRetTy::Return(ret_hir_ty) = decl.output else {
return;
};

let Some(inner_hir_ty) = extract_anonymous_ref(ret_hir_ty) else {
return;
};

if !rustc_hir_analysis::lower_ty(cx.tcx, inner_hir_ty).is_str() {
GnomedDev marked this conversation as resolved.
Show resolved Hide resolved
return;
}

// Check for all return statements returning literals
if check_explicit_returns_static_str(body.value) && check_implicit_returns_static_str(body) {
span_lint_and_sugg(
cx,
UNNECESSARY_LITERAL_BOUND,
ret_hir_ty.span,
"returning a `str` unnecessarily tied to the lifetime of arguments",
"try",
"&'static str".into(), // how ironic, a lint about `&'static str` requiring a `String` alloc...
Applicability::MachineApplicable,
);
}
}
}
65 changes: 65 additions & 0 deletions tests/ui/unnecessary_literal_bound.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#![warn(clippy::unnecessary_literal_bound)]

struct Struct<'a> {
not_literal: &'a str,
}

impl Struct<'_> {
// Should warn
fn returns_lit(&self) -> &'static str {
"Hello"
}

// Should NOT warn
fn returns_non_lit(&self) -> &str {
self.not_literal
}

// Should warn, does not currently
fn conditionally_returns_lit(&self, cond: bool) -> &str {
if cond { "Literal" } else { "also a literal" }
}

// Should NOT warn
fn conditionally_returns_non_lit(&self, cond: bool) -> &str {
if cond { "Literal" } else { self.not_literal }
}

// Should warn
fn contionally_returns_literals_explicit(&self, cond: bool) -> &'static str {
if cond {
return "Literal";
}

"also a literal"
}

// Should NOT warn
fn conditionally_returns_non_lit_explicit(&self, cond: bool) -> &str {
if cond {
return self.not_literal;
}

"Literal"
}
}

trait ReturnsStr {
fn trait_method(&self) -> &str;
}

impl ReturnsStr for u8 {
// Should warn, even though not useful without trait refinement
fn trait_method(&self) -> &'static str {
"Literal"
}
}

impl ReturnsStr for Struct<'_> {
// Should NOT warn
fn trait_method(&self) -> &str {
self.not_literal
}
}

fn main() {}
65 changes: 65 additions & 0 deletions tests/ui/unnecessary_literal_bound.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#![warn(clippy::unnecessary_literal_bound)]

struct Struct<'a> {
not_literal: &'a str,
}

impl Struct<'_> {
// Should warn
fn returns_lit(&self) -> &str {
"Hello"
}

// Should NOT warn
fn returns_non_lit(&self) -> &str {
self.not_literal
}

// Should warn, does not currently
fn conditionally_returns_lit(&self, cond: bool) -> &str {
if cond { "Literal" } else { "also a literal" }
}

// Should NOT warn
fn conditionally_returns_non_lit(&self, cond: bool) -> &str {
if cond { "Literal" } else { self.not_literal }
}

// Should warn
fn contionally_returns_literals_explicit(&self, cond: bool) -> &str {
if cond {
return "Literal";
}

"also a literal"
}

// Should NOT warn
fn conditionally_returns_non_lit_explicit(&self, cond: bool) -> &str {
if cond {
return self.not_literal;
}

"Literal"
}
}

trait ReturnsStr {
fn trait_method(&self) -> &str;
}

impl ReturnsStr for u8 {
// Should warn, even though not useful without trait refinement
fn trait_method(&self) -> &str {
"Literal"
}
}

impl ReturnsStr for Struct<'_> {
// Should NOT warn
fn trait_method(&self) -> &str {
self.not_literal
}
}

fn main() {}
23 changes: 23 additions & 0 deletions tests/ui/unnecessary_literal_bound.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
error: returning a `str` unnecessarily tied to the lifetime of arguments
--> tests/ui/unnecessary_literal_bound.rs:9:30
|
LL | fn returns_lit(&self) -> &str {
| ^^^^ help: try: `&'static str`
|
= note: `-D clippy::unnecessary-literal-bound` implied by `-D warnings`
= help: to override `-D warnings` add `#[allow(clippy::unnecessary_literal_bound)]`

error: returning a `str` unnecessarily tied to the lifetime of arguments
--> tests/ui/unnecessary_literal_bound.rs:29:68
|
LL | fn contionally_returns_literals_explicit(&self, cond: bool) -> &str {
| ^^^^ help: try: `&'static str`

error: returning a `str` unnecessarily tied to the lifetime of arguments
--> tests/ui/unnecessary_literal_bound.rs:53:31
|
LL | fn trait_method(&self) -> &str {
| ^^^^ help: try: `&'static str`

error: aborting due to 3 previous errors

Loading