Skip to content

Commit f328623

Browse files
committed
Auto merge of rust-lang#13395 - GnomedDev:unnecessary-lit-bound, r=Manishearth
Add lint for unnecessary lifetime bounded &str return Closes rust-lang#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 rust-lang#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

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: 158 additions & 0 deletions
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+
}
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)