Skip to content

Commit 67212c0

Browse files
committed
unnecessary_ip_addr_parse: new lint
This lint detects parsing of string literals into IP addresses when they are known correct.
1 parent 3da4c10 commit 67212c0

File tree

9 files changed

+266
-1
lines changed

9 files changed

+266
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6155,6 +6155,7 @@ Released 2018-09-13
61556155
[`panic_params`]: https://rust-lang.github.io/rust-clippy/master/index.html#panic_params
61566156
[`panicking_overflow_checks`]: https://rust-lang.github.io/rust-clippy/master/index.html#panicking_overflow_checks
61576157
[`panicking_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#panicking_unwrap
6158+
[`parsed_string_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#parsed_string_literals
61586159
[`partial_pub_fields`]: https://rust-lang.github.io/rust-clippy/master/index.html#partial_pub_fields
61596160
[`partialeq_ne_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl
61606161
[`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
@@ -437,6 +437,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
437437
crate::methods::OPTION_MAP_OR_NONE_INFO,
438438
crate::methods::OR_FUN_CALL_INFO,
439439
crate::methods::OR_THEN_UNWRAP_INFO,
440+
crate::methods::PARSED_STRING_LITERALS_INFO,
440441
crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO,
441442
crate::methods::PATH_ENDS_WITH_EXT_INFO,
442443
crate::methods::RANGE_ZIP_WITH_LEN_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#![feature(array_windows)]
22
#![feature(box_patterns)]
3+
#![feature(cow_is_borrowed)]
34
#![feature(macro_metavar_expr_concat)]
45
#![feature(f128)]
56
#![feature(f16)]
67
#![feature(if_let_guard)]
8+
#![feature(ip_as_octets)]
79
#![feature(iter_intersperse)]
810
#![feature(iter_partition_in_place)]
911
#![feature(never_type)]

clippy_lints/src/methods/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ mod option_map_or_none;
8888
mod option_map_unwrap_or;
8989
mod or_fun_call;
9090
mod or_then_unwrap;
91+
mod parsed_string_literals;
9192
mod path_buf_push_overwrite;
9293
mod path_ends_with_ext;
9394
mod range_zip_with_len;
@@ -4528,6 +4529,34 @@ declare_clippy_lint! {
45284529
"detect swap with a temporary value"
45294530
}
45304531

4532+
declare_clippy_lint! {
4533+
/// ### What it does
4534+
/// Checks for parsing IPv4/IPv6 string literals
4535+
///
4536+
/// ### Why is this bad?
4537+
/// Parsing known-correct IP address at runtime consumes resources and forces to
4538+
/// handle the (non-existing) errors.
4539+
///
4540+
/// ### Example
4541+
/// ```no_run
4542+
/// use std::net::Ipv4Addr;
4543+
///
4544+
/// let addr1: Ipv4Addr = "10.2.3.4".parse().unwrap();
4545+
/// let addr2: Ipv4Addr = "127.0.0.1".parse().unwrap();
4546+
/// ```
4547+
/// Use instead:
4548+
/// ```no_run
4549+
/// use std::net::Ipv4Addr;
4550+
///
4551+
/// let addr1: Ipv4Addr = Ipv4Addr::new(10, 2, 3, 4);
4552+
/// let addr2: Ipv4Addr = Ipv4Addr::LOCALHOST;
4553+
/// ```
4554+
#[clippy::version = "1.89.0"]
4555+
pub PARSED_STRING_LITERALS,
4556+
complexity,
4557+
"known-correct literal IP address parsing"
4558+
}
4559+
45314560
#[expect(clippy::struct_excessive_bools)]
45324561
pub struct Methods {
45334562
avoid_breaking_exported_api: bool,
@@ -4706,6 +4735,7 @@ impl_lint_pass!(Methods => [
47064735
MANUAL_CONTAINS,
47074736
IO_OTHER_ERROR,
47084737
SWAP_WITH_TEMPORARY,
4738+
PARSED_STRING_LITERALS,
47094739
]);
47104740

47114741
/// Extracts a method call name, args, and `Span` of the method name.
@@ -5420,6 +5450,9 @@ impl Methods {
54205450
Some((sym::or, recv, [or_arg], or_span, _)) => {
54215451
or_then_unwrap::check(cx, expr, recv, or_arg, or_span);
54225452
},
5453+
Some((sym::parse, recv, [], _, _)) => {
5454+
parsed_string_literals::check(cx, expr, recv, self.msrv);
5455+
},
54235456
_ => {},
54245457
}
54255458
unnecessary_literal_unwrap::check(cx, expr, recv, name, args);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use std::borrow::Cow;
2+
use std::net::{Ipv4Addr, Ipv6Addr};
3+
4+
use clippy_utils::diagnostics::span_lint_and_sugg;
5+
use clippy_utils::msrvs::{self, Msrv};
6+
use clippy_utils::source::SpanRangeExt;
7+
use clippy_utils::sym;
8+
use clippy_utils::ty::is_type_diagnostic_item;
9+
use rustc_ast::LitKind;
10+
use rustc_errors::Applicability;
11+
use rustc_hir::{Expr, ExprKind};
12+
use rustc_lint::LateContext;
13+
use rustc_span::Symbol;
14+
15+
use super::PARSED_STRING_LITERALS;
16+
17+
pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, msrv: Msrv) {
18+
if let ExprKind::Lit(lit) = recv.kind
19+
&& let LitKind::Str(str, _) = lit.node
20+
{
21+
let consts_available = || msrv.meets(cx, msrvs::IPADDR_CONSTANTS);
22+
let ty = cx.typeck_results().expr_ty(expr);
23+
if is_type_diagnostic_item(cx, ty, sym::Ipv4Addr)
24+
&& let Some(sugg) = ipv4_subst(str, consts_available())
25+
{
26+
maybe_emit_lint(cx, expr, sugg.is_borrowed(), sugg);
27+
} else if is_type_diagnostic_item(cx, ty, sym::Ipv6Addr)
28+
&& let Some(sugg) = ipv6_subst(str, consts_available())
29+
{
30+
maybe_emit_lint(cx, expr, sugg.is_borrowed(), sugg);
31+
} else if is_type_diagnostic_item(cx, ty, sym::IpAddr) {
32+
let with_consts = consts_available();
33+
if let Some(sugg) = ipv4_subst(str, with_consts) {
34+
maybe_emit_lint(cx, expr, sugg.is_borrowed(), format!("IpAddr::V4({sugg})").into());
35+
} else if let Some(sugg) = ipv6_subst(str, with_consts) {
36+
maybe_emit_lint(cx, expr, sugg.is_borrowed(), format!("IpAddr::V6({sugg})").into());
37+
}
38+
}
39+
}
40+
}
41+
42+
/// Suggests a replacement if `addr` is a correct IPv4 address
43+
fn ipv4_subst(addr: Symbol, with_consts: bool) -> Option<Cow<'static, str>> {
44+
addr.as_str().parse().ok().map(|ipv4: Ipv4Addr| {
45+
if with_consts && ipv4.as_octets() == &[127, 0, 0, 1] {
46+
"Ipv4Addr::LOCALHOST".into()
47+
} else if with_consts && ipv4.is_broadcast() {
48+
"Ipv4Addr::BROADCAST".into()
49+
} else if with_consts && ipv4.is_unspecified() {
50+
"Ipv4Addr::UNSPECIFIED".into()
51+
} else {
52+
let ipv4 = ipv4.as_octets();
53+
format!("Ipv4Addr::new({}, {}, {}, {})", ipv4[0], ipv4[1], ipv4[2], ipv4[3]).into()
54+
}
55+
})
56+
}
57+
58+
/// Suggests a replacement if `addr` is a correct IPv6 address
59+
fn ipv6_subst(addr: Symbol, with_consts: bool) -> Option<Cow<'static, str>> {
60+
addr.as_str().parse().ok().map(|ipv6: Ipv6Addr| {
61+
if with_consts && ipv6.is_loopback() {
62+
"Ipv6Addr::LOCALHOST".into()
63+
} else if with_consts && ipv6.is_unspecified() {
64+
"Ipv6Addr::UNSPECIFIED".into()
65+
} else {
66+
format!(
67+
"Ipv6Addr::new([{}])",
68+
ipv6.segments()
69+
.map(|n| if n < 2 { n.to_string() } else { format!("{n:#x}") })
70+
.join(", ")
71+
)
72+
.into()
73+
}
74+
})
75+
}
76+
77+
/// Emit the lint if the length of `sugg` is no longer than the original `expr` span, or if `force`
78+
/// is set.
79+
fn maybe_emit_lint(cx: &LateContext<'_>, expr: &Expr<'_>, force: bool, sugg: Cow<'_, str>) {
80+
if force || expr.span.check_source_text(cx, |snip| snip.len() >= sugg.len()) {
81+
span_lint_and_sugg(
82+
cx,
83+
PARSED_STRING_LITERALS,
84+
expr.span,
85+
"unnecessary runtime parsing of IP address",
86+
"use",
87+
sugg.into(),
88+
Applicability::MaybeIncorrect,
89+
);
90+
}
91+
}

clippy_utils/src/msrvs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ msrv_aliases! {
6868
1,33,0 { UNDERSCORE_IMPORTS }
6969
1,32,0 { CONST_IS_POWER_OF_TWO }
7070
1,31,0 { OPTION_REPLACE }
71-
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES }
71+
1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES, IPADDR_CONSTANTS }
7272
1,29,0 { ITER_FLATTEN }
7373
1,28,0 { FROM_BOOL, REPEAT_WITH, SLICE_FROM_REF }
7474
1,27,0 { ITERATOR_TRY_FOLD }

tests/ui/parsed_string_literals.fixed

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#![warn(clippy::parsed_string_literals)]
2+
3+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4+
5+
fn main() {
6+
_ = Ipv4Addr::new(137, 194, 161, 2);
7+
//~^ parsed_string_literals
8+
9+
_ = Ipv4Addr::LOCALHOST;
10+
//~^ parsed_string_literals
11+
12+
_ = Ipv4Addr::BROADCAST;
13+
//~^ parsed_string_literals
14+
15+
_ = Ipv4Addr::UNSPECIFIED;
16+
//~^ parsed_string_literals
17+
18+
// Wrong address family
19+
_ = "::1".parse::<Ipv4Addr>().unwrap();
20+
_ = "127.0.0.1".parse::<Ipv6Addr>().unwrap();
21+
22+
_ = Ipv6Addr::LOCALHOST;
23+
//~^ parsed_string_literals
24+
25+
_ = Ipv6Addr::UNSPECIFIED;
26+
//~^ parsed_string_literals
27+
28+
_ = IpAddr::V6(Ipv6Addr::LOCALHOST);
29+
//~^ parsed_string_literals
30+
31+
_ = IpAddr::V6(Ipv6Addr::UNSPECIFIED);
32+
//~^ parsed_string_literals
33+
34+
// The substition text would be larger than the original and wouldn't use constants
35+
_ = "2a04:8ec0:0:47::131".parse::<Ipv6Addr>().unwrap();
36+
_ = "2a04:8ec0:0:47::131".parse::<IpAddr>().unwrap();
37+
}
38+
39+
#[clippy::msrv = "1.29"]
40+
fn msrv_under() {
41+
_ = "::".parse::<IpAddr>().unwrap();
42+
}

tests/ui/parsed_string_literals.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#![warn(clippy::parsed_string_literals)]
2+
3+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4+
5+
fn main() {
6+
_ = "137.194.161.2".parse::<Ipv4Addr>().unwrap();
7+
//~^ parsed_string_literals
8+
9+
_ = "127.0.0.1".parse::<Ipv4Addr>().unwrap();
10+
//~^ parsed_string_literals
11+
12+
_ = "255.255.255.255".parse::<Ipv4Addr>().unwrap();
13+
//~^ parsed_string_literals
14+
15+
_ = "0.0.0.0".parse::<Ipv4Addr>().unwrap();
16+
//~^ parsed_string_literals
17+
18+
// Wrong address family
19+
_ = "::1".parse::<Ipv4Addr>().unwrap();
20+
_ = "127.0.0.1".parse::<Ipv6Addr>().unwrap();
21+
22+
_ = "::1".parse::<Ipv6Addr>().unwrap();
23+
//~^ parsed_string_literals
24+
25+
_ = "::".parse::<Ipv6Addr>().unwrap();
26+
//~^ parsed_string_literals
27+
28+
_ = "::1".parse::<IpAddr>().unwrap();
29+
//~^ parsed_string_literals
30+
31+
_ = "::".parse::<IpAddr>().unwrap();
32+
//~^ parsed_string_literals
33+
34+
// The substition text would be larger than the original and wouldn't use constants
35+
_ = "2a04:8ec0:0:47::131".parse::<Ipv6Addr>().unwrap();
36+
_ = "2a04:8ec0:0:47::131".parse::<IpAddr>().unwrap();
37+
}
38+
39+
#[clippy::msrv = "1.29"]
40+
fn msrv_under() {
41+
_ = "::".parse::<IpAddr>().unwrap();
42+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
error: unnecessary runtime parsing of IP address
2+
--> tests/ui/parsed_string_literals.rs:6:9
3+
|
4+
LL | _ = "137.194.161.2".parse::<Ipv4Addr>().unwrap();
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv4Addr::new(137, 194, 161, 2)`
6+
|
7+
= note: `-D clippy::parsed-string-literals` implied by `-D warnings`
8+
= help: to override `-D warnings` add `#[allow(clippy::parsed_string_literals)]`
9+
10+
error: unnecessary runtime parsing of IP address
11+
--> tests/ui/parsed_string_literals.rs:9:9
12+
|
13+
LL | _ = "127.0.0.1".parse::<Ipv4Addr>().unwrap();
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv4Addr::LOCALHOST`
15+
16+
error: unnecessary runtime parsing of IP address
17+
--> tests/ui/parsed_string_literals.rs:12:9
18+
|
19+
LL | _ = "255.255.255.255".parse::<Ipv4Addr>().unwrap();
20+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv4Addr::BROADCAST`
21+
22+
error: unnecessary runtime parsing of IP address
23+
--> tests/ui/parsed_string_literals.rs:15:9
24+
|
25+
LL | _ = "0.0.0.0".parse::<Ipv4Addr>().unwrap();
26+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv4Addr::UNSPECIFIED`
27+
28+
error: unnecessary runtime parsing of IP address
29+
--> tests/ui/parsed_string_literals.rs:22:9
30+
|
31+
LL | _ = "::1".parse::<Ipv6Addr>().unwrap();
32+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv6Addr::LOCALHOST`
33+
34+
error: unnecessary runtime parsing of IP address
35+
--> tests/ui/parsed_string_literals.rs:25:9
36+
|
37+
LL | _ = "::".parse::<Ipv6Addr>().unwrap();
38+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv6Addr::UNSPECIFIED`
39+
40+
error: unnecessary runtime parsing of IP address
41+
--> tests/ui/parsed_string_literals.rs:28:9
42+
|
43+
LL | _ = "::1".parse::<IpAddr>().unwrap();
44+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `IpAddr::V6(Ipv6Addr::LOCALHOST)`
45+
46+
error: unnecessary runtime parsing of IP address
47+
--> tests/ui/parsed_string_literals.rs:31:9
48+
|
49+
LL | _ = "::".parse::<IpAddr>().unwrap();
50+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `IpAddr::V6(Ipv6Addr::UNSPECIFIED)`
51+
52+
error: aborting due to 8 previous errors
53+

0 commit comments

Comments
 (0)