Skip to content

Commit 42511a3

Browse files
committed
Add lint for unnecessary lifetime bounded &str return
1 parent 7ce1eea commit 42511a3

7 files changed

+313
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6029,6 +6029,7 @@ Released 2018-09-13
60296029
[`unnecessary_get_then_check`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_get_then_check
60306030
[`unnecessary_join`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_join
60316031
[`unnecessary_lazy_evaluations`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_lazy_evaluations
6032+
[`unnecessary_literal_bound`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_literal_bound
60326033
[`unnecessary_literal_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_literal_unwrap
60336034
[`unnecessary_map_on_constructor`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_map_on_constructor
60346035
[`unnecessary_min_or_max`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_min_or_max

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
738738
crate::unit_types::UNIT_CMP_INFO,
739739
crate::unnamed_address::FN_ADDRESS_COMPARISONS_INFO,
740740
crate::unnecessary_box_returns::UNNECESSARY_BOX_RETURNS_INFO,
741+
crate::unnecessary_literal_bound::UNNECESSARY_LITERAL_BOUND_INFO,
741742
crate::unnecessary_map_on_constructor::UNNECESSARY_MAP_ON_CONSTRUCTOR_INFO,
742743
crate::unnecessary_owned_empty_strings::UNNECESSARY_OWNED_EMPTY_STRINGS_INFO,
743744
crate::unnecessary_self_imports::UNNECESSARY_SELF_IMPORTS_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ mod unit_return_expecting_ord;
366366
mod unit_types;
367367
mod unnamed_address;
368368
mod unnecessary_box_returns;
369+
mod unnecessary_literal_bound;
369370
mod unnecessary_map_on_constructor;
370371
mod unnecessary_owned_empty_strings;
371372
mod unnecessary_self_imports;
@@ -949,5 +950,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
949950
store.register_late_pass(|_| Box::new(non_zero_suggestions::NonZeroSuggestions));
950951
store.register_late_pass(move |_| Box::new(unused_trait_names::UnusedTraitNames::new(conf)));
951952
store.register_late_pass(|_| Box::new(manual_ignore_case_cmp::ManualIgnoreCaseCmp));
953+
store.register_late_pass(|_| Box::new(unnecessary_literal_bound::UnnecessaryLiteralBound));
952954
// add lints here, do not remove this comment, it's used in `new_lint`
953955
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use rustc_ast::ast::LitKind;
3+
use rustc_errors::Applicability;
4+
use rustc_hir::intravisit::{FnKind, Visitor};
5+
use rustc_hir::{Body, Expr, ExprKind, FnDecl, FnRetTy, Lit, MutTy, Mutability, Ty, TyKind, intravisit};
6+
use rustc_lint::{LateContext, LateLintPass};
7+
use rustc_session::declare_lint_pass;
8+
use rustc_span::Span;
9+
use rustc_span::def_id::LocalDefId;
10+
11+
declare_clippy_lint! {
12+
/// ### What it does
13+
///
14+
/// Detects functions that are written to return `&str` that could return `&'static str` but instead return a `&'a str`.
15+
///
16+
/// ### Why is this bad?
17+
///
18+
/// This leaves the caller unable to use the `&str` as `&'static str`, causing unneccessary allocations or confusion.
19+
/// This is also most likely what you meant to write.
20+
///
21+
/// ### Example
22+
/// ```no_run
23+
/// # struct MyType;
24+
/// impl MyType {
25+
/// fn returns_literal(&self) -> &str {
26+
/// "Literal"
27+
/// }
28+
/// }
29+
/// ```
30+
/// Use instead:
31+
/// ```no_run
32+
/// # struct MyType;
33+
/// impl MyType {
34+
/// fn returns_literal(&self) -> &'static str {
35+
/// "Literal"
36+
/// }
37+
/// }
38+
/// ```
39+
/// Or, in case you may return a non-literal `str` in future:
40+
/// ```no_run
41+
/// # struct MyType;
42+
/// impl MyType {
43+
/// fn returns_literal<'a>(&'a self) -> &'a str {
44+
/// "Literal"
45+
/// }
46+
/// }
47+
/// ```
48+
#[clippy::version = "1.83.0"]
49+
pub UNNECESSARY_LITERAL_BOUND,
50+
pedantic,
51+
"detects &str that could be &'static str in function return types"
52+
}
53+
54+
declare_lint_pass!(UnnecessaryLiteralBound => [UNNECESSARY_LITERAL_BOUND]);
55+
56+
fn extract_anonymous_ref<'tcx>(hir_ty: &Ty<'tcx>) -> Option<&'tcx Ty<'tcx>> {
57+
let TyKind::Ref(lifetime, MutTy { ty, mutbl }) = hir_ty.kind else {
58+
return None;
59+
};
60+
61+
if !lifetime.is_anonymous() || !matches!(mutbl, Mutability::Not) {
62+
return None;
63+
}
64+
65+
Some(ty)
66+
}
67+
68+
fn is_str_literal(expr: &Expr<'_>) -> bool {
69+
matches!(
70+
expr.kind,
71+
ExprKind::Lit(Lit {
72+
node: LitKind::Str(..),
73+
..
74+
}),
75+
)
76+
}
77+
78+
struct FindNonLiteralReturn;
79+
80+
impl<'hir> Visitor<'hir> for FindNonLiteralReturn {
81+
type Result = std::ops::ControlFlow<()>;
82+
type NestedFilter = intravisit::nested_filter::None;
83+
84+
fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> Self::Result {
85+
if let ExprKind::Ret(Some(ret_val_expr)) = expr.kind
86+
&& !is_str_literal(ret_val_expr)
87+
{
88+
Self::Result::Break(())
89+
} else {
90+
intravisit::walk_expr(self, expr)
91+
}
92+
}
93+
}
94+
95+
fn check_implicit_returns_static_str(body: &Body<'_>) -> bool {
96+
// TODO: Improve this to the same complexity as the Visitor to catch more implicit return cases.
97+
if let ExprKind::Block(block, _) = body.value.kind
98+
&& let Some(implicit_ret) = block.expr
99+
{
100+
return is_str_literal(implicit_ret);
101+
}
102+
103+
false
104+
}
105+
106+
fn check_explicit_returns_static_str(expr: &Expr<'_>) -> bool {
107+
let mut visitor = FindNonLiteralReturn;
108+
visitor.visit_expr(expr).is_continue()
109+
}
110+
111+
impl<'tcx> LateLintPass<'tcx> for UnnecessaryLiteralBound {
112+
fn check_fn(
113+
&mut self,
114+
cx: &LateContext<'tcx>,
115+
kind: FnKind<'tcx>,
116+
decl: &'tcx FnDecl<'_>,
117+
body: &'tcx Body<'_>,
118+
span: Span,
119+
_: LocalDefId,
120+
) {
121+
if span.from_expansion() {
122+
return;
123+
}
124+
125+
// Checking closures would be a little silly
126+
if matches!(kind, FnKind::Closure) {
127+
return;
128+
}
129+
130+
// Check for `-> &str`
131+
let FnRetTy::Return(ret_hir_ty) = decl.output else {
132+
return;
133+
};
134+
135+
let Some(inner_hir_ty) = extract_anonymous_ref(ret_hir_ty) else {
136+
return;
137+
};
138+
139+
if !rustc_hir_analysis::lower_ty(cx.tcx, inner_hir_ty).is_str() {
140+
return;
141+
}
142+
143+
// Check for all return statements returning literals
144+
if check_explicit_returns_static_str(body.value) && check_implicit_returns_static_str(body) {
145+
span_lint_and_sugg(
146+
cx,
147+
UNNECESSARY_LITERAL_BOUND,
148+
ret_hir_ty.span,
149+
"returning a `str` unnecessarily tied to the lifetime of arguments",
150+
"try",
151+
"&'static str".into(), // how ironic, a lint about `&'static str` requiring a `String` alloc...
152+
Applicability::MachineApplicable,
153+
);
154+
}
155+
}
156+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#![warn(clippy::unnecessary_literal_bound)]
2+
3+
struct Struct<'a> {
4+
not_literal: &'a str,
5+
}
6+
7+
impl Struct<'_> {
8+
// Should warn
9+
fn returns_lit(&self) -> &'static str {
10+
"Hello"
11+
}
12+
13+
// Should NOT warn
14+
fn returns_non_lit(&self) -> &str {
15+
self.not_literal
16+
}
17+
18+
// Should warn, does not currently
19+
fn conditionally_returns_lit(&self, cond: bool) -> &str {
20+
if cond { "Literal" } else { "also a literal" }
21+
}
22+
23+
// Should NOT warn
24+
fn conditionally_returns_non_lit(&self, cond: bool) -> &str {
25+
if cond { "Literal" } else { self.not_literal }
26+
}
27+
28+
// Should warn
29+
fn contionally_returns_literals_explicit(&self, cond: bool) -> &'static str {
30+
if cond {
31+
return "Literal";
32+
}
33+
34+
"also a literal"
35+
}
36+
37+
// Should NOT warn
38+
fn conditionally_returns_non_lit_explicit(&self, cond: bool) -> &str {
39+
if cond {
40+
return self.not_literal;
41+
}
42+
43+
"Literal"
44+
}
45+
}
46+
47+
trait ReturnsStr {
48+
fn trait_method(&self) -> &str;
49+
}
50+
51+
impl ReturnsStr for u8 {
52+
// Should warn, even though not useful without trait refinement
53+
fn trait_method(&self) -> &'static str {
54+
"Literal"
55+
}
56+
}
57+
58+
impl ReturnsStr for Struct<'_> {
59+
// Should NOT warn
60+
fn trait_method(&self) -> &str {
61+
self.not_literal
62+
}
63+
}
64+
65+
fn main() {}

tests/ui/unnecessary_literal_bound.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#![warn(clippy::unnecessary_literal_bound)]
2+
3+
struct Struct<'a> {
4+
not_literal: &'a str,
5+
}
6+
7+
impl Struct<'_> {
8+
// Should warn
9+
fn returns_lit(&self) -> &str {
10+
"Hello"
11+
}
12+
13+
// Should NOT warn
14+
fn returns_non_lit(&self) -> &str {
15+
self.not_literal
16+
}
17+
18+
// Should warn, does not currently
19+
fn conditionally_returns_lit(&self, cond: bool) -> &str {
20+
if cond { "Literal" } else { "also a literal" }
21+
}
22+
23+
// Should NOT warn
24+
fn conditionally_returns_non_lit(&self, cond: bool) -> &str {
25+
if cond { "Literal" } else { self.not_literal }
26+
}
27+
28+
// Should warn
29+
fn contionally_returns_literals_explicit(&self, cond: bool) -> &str {
30+
if cond {
31+
return "Literal";
32+
}
33+
34+
"also a literal"
35+
}
36+
37+
// Should NOT warn
38+
fn conditionally_returns_non_lit_explicit(&self, cond: bool) -> &str {
39+
if cond {
40+
return self.not_literal;
41+
}
42+
43+
"Literal"
44+
}
45+
}
46+
47+
trait ReturnsStr {
48+
fn trait_method(&self) -> &str;
49+
}
50+
51+
impl ReturnsStr for u8 {
52+
// Should warn, even though not useful without trait refinement
53+
fn trait_method(&self) -> &str {
54+
"Literal"
55+
}
56+
}
57+
58+
impl ReturnsStr for Struct<'_> {
59+
// Should NOT warn
60+
fn trait_method(&self) -> &str {
61+
self.not_literal
62+
}
63+
}
64+
65+
fn main() {}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
error: returning a `str` unnecessarily tied to the lifetime of arguments
2+
--> tests/ui/unnecessary_literal_bound.rs:9:30
3+
|
4+
LL | fn returns_lit(&self) -> &str {
5+
| ^^^^ help: try: `&'static str`
6+
|
7+
= note: `-D clippy::unnecessary-literal-bound` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::unnecessary_literal_bound)]`
9+
10+
error: returning a `str` unnecessarily tied to the lifetime of arguments
11+
--> tests/ui/unnecessary_literal_bound.rs:29:68
12+
|
13+
LL | fn contionally_returns_literals_explicit(&self, cond: bool) -> &str {
14+
| ^^^^ help: try: `&'static str`
15+
16+
error: returning a `str` unnecessarily tied to the lifetime of arguments
17+
--> tests/ui/unnecessary_literal_bound.rs:53:31
18+
|
19+
LL | fn trait_method(&self) -> &str {
20+
| ^^^^ help: try: `&'static str`
21+
22+
error: aborting due to 3 previous errors
23+

0 commit comments

Comments
 (0)