Skip to content

Commit ae2950d

Browse files
committed
parsed_string_literals: new lint
This lint detects parsing of string literals into primitive types or IP addresses when they are known correct. Apply review comments
1 parent 3e218d1 commit ae2950d

18 files changed

+924
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6496,6 +6496,7 @@ Released 2018-09-13
64966496
[`panic_params`]: https://rust-lang.github.io/rust-clippy/master/index.html#panic_params
64976497
[`panicking_overflow_checks`]: https://rust-lang.github.io/rust-clippy/master/index.html#panicking_overflow_checks
64986498
[`panicking_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#panicking_unwrap
6499+
[`parsed_string_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#parsed_string_literals
64996500
[`partial_pub_fields`]: https://rust-lang.github.io/rust-clippy/master/index.html#partial_pub_fields
65006501
[`partialeq_ne_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl
65016502
[`partialeq_to_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_to_none

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[
444444
crate::methods::OPTION_MAP_OR_NONE_INFO,
445445
crate::methods::OR_FUN_CALL_INFO,
446446
crate::methods::OR_THEN_UNWRAP_INFO,
447+
crate::methods::PARSED_STRING_LITERALS_INFO,
447448
crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO,
448449
crate::methods::PATH_ENDS_WITH_EXT_INFO,
449450
crate::methods::PTR_OFFSET_WITH_CAST_INFO,

clippy_lints/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#![feature(f128)]
55
#![feature(f16)]
66
#![feature(if_let_guard)]
7+
#![feature(ip_as_octets)]
78
#![feature(iter_intersperse)]
89
#![feature(iter_partition_in_place)]
910
#![feature(never_type)]

clippy_lints/src/methods/mod.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ mod option_map_or_none;
8989
mod option_map_unwrap_or;
9090
mod or_fun_call;
9191
mod or_then_unwrap;
92+
mod parsed_string_literals;
9293
mod path_buf_push_overwrite;
9394
mod path_ends_with_ext;
9495
mod ptr_offset_with_cast;
@@ -4637,6 +4638,36 @@ declare_clippy_lint! {
46374638
"detects redundant calls to `Iterator::cloned`"
46384639
}
46394640

4641+
declare_clippy_lint! {
4642+
/// ### What it does
4643+
/// Checks for parsing string literals into types from the standard library
4644+
///
4645+
/// ### Why is this bad?
4646+
/// Parsing known values at runtime consumes resources and forces to
4647+
/// unwrap the `Ok()` variant returned by `parse()`.
4648+
///
4649+
/// ### Example
4650+
/// ```no_run
4651+
/// use std::net::Ipv4Addr;
4652+
///
4653+
/// let number = "123".parse::<u32>().unwrap();
4654+
/// let addr1: Ipv4Addr = "10.2.3.4".parse().unwrap();
4655+
/// let addr2: Ipv4Addr = "127.0.0.1".parse().unwrap();
4656+
/// ```
4657+
/// Use instead:
4658+
/// ```no_run
4659+
/// use std::net::Ipv4Addr;
4660+
///
4661+
/// let number = 123_u32;
4662+
/// let addr1: Ipv4Addr = Ipv4Addr::new(10, 2, 3, 4);
4663+
/// let addr2: Ipv4Addr = Ipv4Addr::LOCALHOST;
4664+
/// ```
4665+
#[clippy::version = "1.92.0"]
4666+
pub PARSED_STRING_LITERALS,
4667+
perf,
4668+
"known-correct literal IP address parsing"
4669+
}
4670+
46404671
#[expect(clippy::struct_excessive_bools)]
46414672
pub struct Methods {
46424673
avoid_breaking_exported_api: bool,
@@ -4818,6 +4849,7 @@ impl_lint_pass!(Methods => [
48184849
SWAP_WITH_TEMPORARY,
48194850
IP_CONSTANT,
48204851
REDUNDANT_ITER_CLONED,
4852+
PARSED_STRING_LITERALS,
48214853
]);
48224854

48234855
/// Extracts a method call name, args, and `Span` of the method name.
@@ -5542,6 +5574,9 @@ impl Methods {
55425574
Some((sym::or, recv, [or_arg], or_span, _)) => {
55435575
or_then_unwrap::check(cx, expr, recv, or_arg, or_span);
55445576
},
5577+
Some((sym::parse, inner_recv, [], _, _)) => {
5578+
parsed_string_literals::check(cx, expr, inner_recv, recv, self.msrv);
5579+
},
55455580
_ => {},
55465581
}
55475582
unnecessary_literal_unwrap::check(cx, expr, recv, name, args);
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use std::net::{Ipv4Addr, Ipv6Addr};
2+
3+
use clippy_utils::msrvs::{self, Msrv};
4+
use clippy_utils::source::snippet_with_applicability;
5+
use clippy_utils::{std_or_core, sym};
6+
use rustc_errors::Applicability;
7+
use rustc_hir::{self as hir, AmbigArg, Expr};
8+
use rustc_lint::LateContext;
9+
use rustc_span::Symbol;
10+
11+
use super::{maybe_emit_lint, warn_about};
12+
13+
static IPV4_ENTITY: &str = "an IPv4 address";
14+
static IPV6_ENTITY: &str = "an IPv6 address";
15+
16+
pub(super) fn check(
17+
cx: &LateContext<'_>,
18+
expr: &Expr<'_>,
19+
lit: Symbol,
20+
method: Symbol,
21+
explicit_type: Option<hir::Ty<'_, AmbigArg>>,
22+
msrv: Msrv,
23+
) {
24+
let ipaddr_consts_available = msrv.meets(cx, msrvs::IPADDR_CONSTANTS);
25+
match method {
26+
sym::Ipv4Addr => {
27+
// Only use constants such as `Ipv4Addr::LOCALHOST` when the type has been explicitly given
28+
if let Some((sugg, typed_const, app)) = ipv4_subst(cx, lit, ipaddr_consts_available, explicit_type) {
29+
maybe_emit_lint(cx, expr, typed_const, IPV4_ENTITY, sugg, app);
30+
}
31+
},
32+
sym::Ipv6Addr => {
33+
// Only use constants such as `Ipv4Addr::LOCALHOST` when the type has been explicitly given
34+
if let Some((sugg, typed_const, app)) = ipv6_subst(cx, lit, ipaddr_consts_available, explicit_type) {
35+
maybe_emit_lint(cx, expr, typed_const, IPV6_ENTITY, sugg, app);
36+
}
37+
},
38+
sym::IpAddr => {
39+
if let Some(explicit_type) = explicit_type
40+
&& let Some((sugg, typed_const, entity, app)) = ip_subst(cx, lit, explicit_type)
41+
{
42+
maybe_emit_lint(cx, expr, typed_const, entity, sugg, app);
43+
} else {
44+
warn_about(
45+
cx,
46+
expr,
47+
"an IP address",
48+
format!(
49+
"use a variant of the `{}::net::IpAddr` enum instead",
50+
std_or_core(cx).unwrap_or("std")
51+
),
52+
);
53+
}
54+
},
55+
_ => unreachable!(),
56+
}
57+
}
58+
59+
/// Suggests a replacement if `addr` is a correct IPv4 address, with:
60+
/// - the replacement string
61+
/// - a boolean that indicates if a typed constant is used
62+
/// - the applicability
63+
///
64+
/// The replacement will be `T::CONSTANT` if a constant is detected,
65+
/// where `T` is either `explicit_type` if provided, or `Ipv4Addr`
66+
/// otherwise.
67+
///
68+
/// In other cases, when the type has been explicitly given as `T`, the
69+
/// `T::new()` constructor will be used. If no type has been explicitly
70+
/// given, then `[u8; 4].into()` will be used as the context should
71+
/// already provide the proper information. This allows us not to use
72+
/// a type name which might not be available in the current scope.
73+
fn ipv4_subst(
74+
cx: &LateContext<'_>,
75+
addr: Symbol,
76+
with_consts: bool,
77+
explicit_type: Option<hir::Ty<'_, AmbigArg>>,
78+
) -> Option<(String, bool, Applicability)> {
79+
addr.as_str().parse().ok().map(|ipv4: Ipv4Addr| {
80+
let bytes = ipv4.as_octets();
81+
if let Some(ty) = explicit_type {
82+
let mut app = Applicability::MachineApplicable;
83+
let ty = snippet_with_applicability(cx, ty.span, "_", &mut app);
84+
if with_consts && bytes == &[127, 0, 0, 1] {
85+
(format!("{ty}::LOCALHOST"), true, app)
86+
} else if with_consts && ipv4.is_broadcast() {
87+
(format!("{ty}::BROADCAST"), true, app)
88+
} else if with_consts && ipv4.is_unspecified() {
89+
(format!("{ty}::UNSPECIFIED"), true, app)
90+
} else {
91+
(
92+
format!("{ty}::new({}, {}, {}, {})", bytes[0], bytes[1], bytes[2], bytes[3]),
93+
false,
94+
app,
95+
)
96+
}
97+
} else {
98+
(
99+
format!("[{}, {}, {}, {}].into()", bytes[0], bytes[1], bytes[2], bytes[3]),
100+
false,
101+
Applicability::MachineApplicable,
102+
)
103+
}
104+
})
105+
}
106+
107+
/// Suggests a replacement if `addr` is a correct IPv6 address, with:
108+
/// - the replacement string
109+
/// - a boolean that indicates if a typed constant is used
110+
/// - the applicability
111+
///
112+
/// Replacement will either be:
113+
/// - `T::CONSTANT`
114+
/// - `Ipv6Addr::CONSTANT` if no `explicit_type` is defined
115+
/// - `T::new(…)`
116+
/// - `[u16; 8].into()` if no `explicit_type` is defined
117+
///
118+
/// See [`ipv4_subst()`] for more details.
119+
fn ipv6_subst(
120+
cx: &LateContext<'_>,
121+
addr: Symbol,
122+
with_consts: bool,
123+
explicit_type: Option<hir::Ty<'_, AmbigArg>>,
124+
) -> Option<(String, bool, Applicability)> {
125+
addr.as_str().parse().ok().map(|ipv6: Ipv6Addr| {
126+
let addr = || {
127+
ipv6.segments()
128+
.map(|n| if n < 2 { n.to_string() } else { format!("{n:#x}") })
129+
.join(", ")
130+
};
131+
if let Some(ty) = explicit_type {
132+
let mut app = Applicability::MachineApplicable;
133+
let ty = snippet_with_applicability(cx, ty.span, "_", &mut app);
134+
if with_consts && ipv6.is_loopback() {
135+
(format!("{ty}::LOCALHOST"), true, app)
136+
} else if with_consts && explicit_type.is_some() && ipv6.is_unspecified() {
137+
(format!("{ty}::UNSPECIFIED"), true, app)
138+
} else {
139+
(format!("{ty}::new({})", addr()), false, app)
140+
}
141+
} else {
142+
(format!("[{}].into()", addr()), false, Applicability::MachineApplicable)
143+
}
144+
})
145+
}
146+
147+
/// Suggests a replacement if `addr` is a correct IPv6 address, with:
148+
/// - the replacement string
149+
/// - a boolean that indicates if a typed constant is used
150+
/// - the entity that was detected
151+
/// - the applicability
152+
///
153+
/// `explicit_type` refers to `IpAddr`, and not to the content of one of the variants
154+
/// (`IpAddr::V4` or `IpAddr::V6`). The use of constants from `Ipv4Addr` or `Ipv6Addr`
155+
/// will not be proposed because we do not know if those types are imported in the scope.
156+
fn ip_subst(
157+
cx: &LateContext<'_>,
158+
addr: Symbol,
159+
explicit_type: hir::Ty<'_, AmbigArg>,
160+
) -> Option<(String, bool, &'static str, Applicability)> {
161+
let (sugg, typed_const, entity, variant) = if let Some((sugg, typed_const, _)) = ipv4_subst(cx, addr, false, None) {
162+
(sugg, typed_const, IPV4_ENTITY, "V4")
163+
} else if let Some((sugg, typed_const, _)) = ipv6_subst(cx, addr, false, None) {
164+
(sugg, typed_const, IPV6_ENTITY, "V6")
165+
} else {
166+
return None;
167+
};
168+
// If a typed constant has been used, we cannot know for sure that the `Ipv4Addr`/`Ipv6Addr` is
169+
// present in the current scope because the explicit type if present would be `IpAddr`.
170+
let mut app = if typed_const {
171+
Applicability::MaybeIncorrect
172+
} else {
173+
Applicability::MachineApplicable
174+
};
175+
Some((
176+
format!(
177+
"{}::{variant}({sugg})",
178+
snippet_with_applicability(cx, explicit_type.span, "_", &mut app)
179+
),
180+
typed_const,
181+
entity,
182+
app,
183+
))
184+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
2+
use clippy_utils::msrvs::Msrv;
3+
use clippy_utils::source::SpanRangeExt as _;
4+
use clippy_utils::sym;
5+
use clippy_utils::ty::get_type_diagnostic_name;
6+
use rustc_ast::LitKind;
7+
use rustc_errors::{Applicability, SubdiagMessage};
8+
use rustc_hir::{self as hir, AmbigArg, Expr, ExprKind, GenericArg, Node};
9+
use rustc_lint::LateContext;
10+
11+
mod ip_addresses;
12+
mod primitive_types;
13+
14+
use super::PARSED_STRING_LITERALS;
15+
16+
/// Detects instances of `"literal".parse().unwrap()`:
17+
/// - `expr` is the whole expression
18+
/// - `recv` is the receiver of `parse()`
19+
/// - `parse_call` is the `parse()` method call
20+
/// - `msrv` is used for Rust version checking
21+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, parse_call: &Expr<'_>, msrv: Msrv) {
22+
if let ExprKind::Lit(lit) = recv.kind
23+
&& let LitKind::Str(lit, _) = lit.node
24+
{
25+
let ty = cx.typeck_results().expr_ty(expr);
26+
match get_type_diagnostic_name(cx, ty) {
27+
_ if ty.is_primitive() => primitive_types::check(cx, expr, lit, ty, recv),
28+
Some(method @ (sym::IpAddr | sym::Ipv4Addr | sym::Ipv6Addr)) => {
29+
ip_addresses::check(cx, expr, lit, method, get_explicit_type(cx, expr, parse_call), msrv);
30+
},
31+
_ => (),
32+
}
33+
}
34+
}
35+
36+
/// Emit the lint if the length of `sugg` is no longer than the original `expr` span, or if `force`
37+
/// is set.
38+
fn maybe_emit_lint(
39+
cx: &LateContext<'_>,
40+
expr: &Expr<'_>,
41+
force: bool,
42+
entity: &str,
43+
sugg: String,
44+
applicability: Applicability,
45+
) {
46+
if force || expr.span.check_source_text(cx, |snip| snip.len() >= sugg.len()) {
47+
span_lint_and_sugg(
48+
cx,
49+
PARSED_STRING_LITERALS,
50+
expr.span,
51+
format!("unnecessary runtime parsing of {entity}"),
52+
"use",
53+
sugg,
54+
applicability,
55+
);
56+
}
57+
}
58+
59+
/// Warn and help about an expression.
60+
fn warn_about(cx: &LateContext<'_>, expr: &Expr<'_>, entity: &str, help: impl Into<SubdiagMessage>) {
61+
span_lint_and_help(
62+
cx,
63+
PARSED_STRING_LITERALS,
64+
expr.span,
65+
format!("unnecessary runtime parsing of {entity}"),
66+
None,
67+
help,
68+
);
69+
}
70+
71+
/// Returns `T` from the `parse::<T>(…)` call if present, or from a `let _: T =
72+
/// "…".parse().unwrap();` otherwise if it can be found. The `let` statement has been chosen because
73+
/// it is quite common to assign the result of parsing to a variable.
74+
fn get_explicit_type<'hir>(
75+
cx: &'hir LateContext<'_>,
76+
expr: &'hir Expr<'_>,
77+
parse_call: &'hir Expr<'_>,
78+
) -> Option<hir::Ty<'hir, AmbigArg>> {
79+
if let ExprKind::MethodCall(parse, _, _, _) = parse_call.kind
80+
&& let [GenericArg::Type(ty)] = parse.args().args
81+
&& matches!(ty.kind, hir::TyKind::Path(_))
82+
{
83+
Some(**ty)
84+
} else if let Node::LetStmt(let_stmt) = cx.tcx.parent_hir_node(expr.hir_id)
85+
&& let Some(ty) = let_stmt.ty
86+
&& let Some(ty) = ty.try_as_ambig_ty()
87+
&& matches!(ty.kind, hir::TyKind::Path(_))
88+
{
89+
Some(*ty)
90+
} else {
91+
None
92+
}
93+
}

0 commit comments

Comments
 (0)