Skip to content

Commit f328623

Browse files
committed
Auto merge of #13395 - GnomedDev:unnecessary-lit-bound, r=Manishearth
Add lint for unnecessary lifetime bounded &str return Closes #305. Currently implemented with a pretty strong limitation that it can only see the most basic implicit return, but this should be fixable by something with more time and brain energy than me. Cavets from #13388 apply, as I have not had a review on my clippy lints yet so am pretty new to this. ``` changelog: [`unnecessary_literal_bound`]: Add lint for unnecessary lifetime bounded &str return. ```
2 parents 1563ce5 + b62d262 commit f328623

7 files changed

+315
-0
lines changed

CHANGELOG.md

+1
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

+1
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

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

+65
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() {}
+23
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)