From 6257caf0eda344fe9afea61a4bd1a936af238a12 Mon Sep 17 00:00:00 2001 From: Samuel Tardieu Date: Tue, 13 May 2025 07:36:17 +0200 Subject: [PATCH] `parsed_string_literals`: new lint This lint detects parsing of string literals into primitive types or IP addresses when they are known correct. --- CHANGELOG.md | 1 + clippy_lints/src/declared_lints.rs | 1 + clippy_lints/src/lib.rs | 1 + clippy_lints/src/methods/mod.rs | 35 ++++ .../parsed_string_literals/ip_addresses.rs | 174 ++++++++++++++++++ .../src/methods/parsed_string_literals/mod.rs | 81 ++++++++ .../parsed_string_literals/primitive_types.rs | 102 ++++++++++ clippy_utils/src/msrvs.rs | 2 +- .../parsed_string_literals/ip_addresses.fixed | 121 ++++++++++++ .../ui/parsed_string_literals/ip_addresses.rs | 121 ++++++++++++ .../ip_addresses.stderr | 119 ++++++++++++ .../primitive_types.fixed | 66 +++++++ .../parsed_string_literals/primitive_types.rs | 66 +++++++ .../primitive_types.stderr | 95 ++++++++++ 14 files changed, 984 insertions(+), 1 deletion(-) create mode 100644 clippy_lints/src/methods/parsed_string_literals/ip_addresses.rs create mode 100644 clippy_lints/src/methods/parsed_string_literals/mod.rs create mode 100644 clippy_lints/src/methods/parsed_string_literals/primitive_types.rs create mode 100644 tests/ui/parsed_string_literals/ip_addresses.fixed create mode 100644 tests/ui/parsed_string_literals/ip_addresses.rs create mode 100644 tests/ui/parsed_string_literals/ip_addresses.stderr create mode 100644 tests/ui/parsed_string_literals/primitive_types.fixed create mode 100644 tests/ui/parsed_string_literals/primitive_types.rs create mode 100644 tests/ui/parsed_string_literals/primitive_types.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b374e26c96..957648f883ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6496,6 +6496,7 @@ Released 2018-09-13 [`panic_params`]: https://rust-lang.github.io/rust-clippy/master/index.html#panic_params [`panicking_overflow_checks`]: https://rust-lang.github.io/rust-clippy/master/index.html#panicking_overflow_checks [`panicking_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#panicking_unwrap +[`parsed_string_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#parsed_string_literals [`partial_pub_fields`]: https://rust-lang.github.io/rust-clippy/master/index.html#partial_pub_fields [`partialeq_ne_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_ne_impl [`partialeq_to_none`]: https://rust-lang.github.io/rust-clippy/master/index.html#partialeq_to_none diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index dd5c5dcf4d1f..842db55b10be 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -444,6 +444,7 @@ pub static LINTS: &[&::declare_clippy_lint::LintInfo] = &[ crate::methods::OPTION_MAP_OR_NONE_INFO, crate::methods::OR_FUN_CALL_INFO, crate::methods::OR_THEN_UNWRAP_INFO, + crate::methods::PARSED_STRING_LITERALS_INFO, crate::methods::PATH_BUF_PUSH_OVERWRITE_INFO, crate::methods::PATH_ENDS_WITH_EXT_INFO, crate::methods::PTR_OFFSET_WITH_CAST_INFO, diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index e3168b8cfe02..e5acb5c1ace0 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -4,6 +4,7 @@ #![feature(f128)] #![feature(f16)] #![feature(if_let_guard)] +#![feature(ip_as_octets)] #![feature(iter_intersperse)] #![feature(iter_partition_in_place)] #![feature(never_type)] diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs index b0b4e5eedb08..f972296fd034 100644 --- a/clippy_lints/src/methods/mod.rs +++ b/clippy_lints/src/methods/mod.rs @@ -91,6 +91,7 @@ mod option_map_or_none; mod option_map_unwrap_or; mod or_fun_call; mod or_then_unwrap; +mod parsed_string_literals; mod path_buf_push_overwrite; mod path_ends_with_ext; mod ptr_offset_with_cast; @@ -4639,6 +4640,36 @@ declare_clippy_lint! { "detects redundant calls to `Iterator::cloned`" } +declare_clippy_lint! { + /// ### What it does + /// Checks for parsing string literals into types from the standard library + /// + /// ### Why is this bad? + /// Parsing known values at runtime consumes resources and forces to + /// unwrap the `Ok()` variant returned by `parse()`. + /// + /// ### Example + /// ```no_run + /// use std::net::Ipv4Addr; + /// + /// let number = "123".parse::().unwrap(); + /// let addr1: Ipv4Addr = "10.2.3.4".parse().unwrap(); + /// let addr2: Ipv4Addr = "127.0.0.1".parse().unwrap(); + /// ``` + /// Use instead: + /// ```no_run + /// use std::net::Ipv4Addr; + /// + /// let number = 123_u32; + /// let addr1: Ipv4Addr = Ipv4Addr::new(10, 2, 3, 4); + /// let addr2: Ipv4Addr = Ipv4Addr::LOCALHOST; + /// ``` + #[clippy::version = "1.92.0"] + pub PARSED_STRING_LITERALS, + perf, + "known-correct literal IP address parsing" +} + #[expect(clippy::struct_excessive_bools)] pub struct Methods { avoid_breaking_exported_api: bool, @@ -4820,6 +4851,7 @@ impl_lint_pass!(Methods => [ SWAP_WITH_TEMPORARY, IP_CONSTANT, REDUNDANT_ITER_CLONED, + PARSED_STRING_LITERALS, ]); /// Extracts a method call name, args, and `Span` of the method name. @@ -5482,6 +5514,9 @@ impl Methods { Some((sym::or, recv, [or_arg], or_span, _)) => { or_then_unwrap::check(cx, expr, recv, or_arg, or_span); }, + Some((sym::parse, inner_recv, [], _, _)) => { + parsed_string_literals::check(cx, expr, inner_recv, recv, self.msrv); + }, _ => {}, } unnecessary_literal_unwrap::check(cx, expr, recv, name, args); diff --git a/clippy_lints/src/methods/parsed_string_literals/ip_addresses.rs b/clippy_lints/src/methods/parsed_string_literals/ip_addresses.rs new file mode 100644 index 000000000000..64e143605623 --- /dev/null +++ b/clippy_lints/src/methods/parsed_string_literals/ip_addresses.rs @@ -0,0 +1,174 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use clippy_utils::msrvs::{self, Msrv}; +use clippy_utils::source::SpanRangeExt as _; +use clippy_utils::sym; +use rustc_hir::{Expr, QPath}; +use rustc_lint::LateContext; +use rustc_span::Symbol; + +use super::maybe_emit_lint; + +static IPV4_ENTITY: &str = "an IPv4 address"; +static IPV6_ENTITY: &str = "an IPv6 address"; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &Expr<'_>, + lit: Symbol, + method: Symbol, + explicit_type: Option>, + msrv: Msrv, +) { + let ipaddr_consts_available = msrv.meets(cx, msrvs::IPADDR_CONSTANTS); + match method { + sym::Ipv4Addr => { + // Only use constants such as `Ipv4Addr::LOCALHOST` when the type has been explicitly given + if let Some((sugg, typed_const)) = ipv4_subst(cx, lit, ipaddr_consts_available, explicit_type) { + maybe_emit_lint(cx, expr, typed_const, IPV4_ENTITY, sugg); + } + }, + sym::Ipv6Addr => { + // Only use constants such as `Ipv4Addr::LOCALHOST` when the type has been explicitly given + if let Some((sugg, typed_const)) = ipv6_subst(cx, lit, ipaddr_consts_available, explicit_type) { + maybe_emit_lint(cx, expr, typed_const, IPV6_ENTITY, sugg); + } + }, + sym::IpAddr => { + if let Some((sugg, entity)) = ip_subst(cx, lit, explicit_type) { + maybe_emit_lint(cx, expr, false, entity, sugg); + } + }, + _ => unreachable!(), + } +} + +/// Suggests a replacement if `addr` is a correct IPv4 address, with: +/// - the replacement string +/// - a boolean that indicates if a typed constant is used +/// +/// The replacement will be `T::CONSTANT` if a constant is detected, +/// where `T` is either `explicit_type` if provided, or `Ipv4Addr` +/// otherwise. +/// +/// In other cases, when the type has been explicitly given as `T`, the +/// `T::new()` constructor will be used. If no type has been explicitly +/// given, then `[u8; 4].into()` will be used as the context should +/// already provide the proper information. This allows us not to use +/// a type name which might not be available in the current scope. +fn ipv4_subst( + cx: &LateContext<'_>, + addr: Symbol, + with_consts: bool, + explicit_type: Option>, +) -> Option<(String, bool)> { + as_ipv4_octets(addr).and_then(|bytes| { + if let Some(qpath) = explicit_type { + qpath.span().with_source_text(cx, |ty| { + if with_consts && &bytes == Ipv4Addr::LOCALHOST.as_octets() { + (format!("{ty}::LOCALHOST"), true) + } else if with_consts && &bytes == Ipv4Addr::BROADCAST.as_octets() { + (format!("{ty}::BROADCAST"), true) + } else if with_consts && &bytes == Ipv4Addr::UNSPECIFIED.as_octets() { + (format!("{ty}::UNSPECIFIED"), true) + } else { + ( + format!("{ty}::new({}, {}, {}, {})", bytes[0], bytes[1], bytes[2], bytes[3]), + false, + ) + } + }) + } else { + Some(( + format!("[{}, {}, {}, {}].into()", bytes[0], bytes[1], bytes[2], bytes[3]), + false, + )) + } + }) +} + +/// Try parsing `addr` as an IPv4 address and return its octets +fn as_ipv4_octets(addr: Symbol) -> Option<[u8; 4]> { + addr.as_str().parse::().ok().map(|addr| *addr.as_octets()) +} + +/// Suggests a replacement if `addr` is a correct IPv6 address, with: +/// - the replacement string +/// - a boolean that indicates if a typed constant is used +/// +/// Replacement will either be: +/// - `T::CONSTANT` +/// - `Ipv6Addr::CONSTANT` if no `explicit_type` is defined +/// - `T::new(…)` +/// - `[u16; 8].into()` if no `explicit_type` is defined +/// +/// See [`ipv4_subst()`] for more details. +fn ipv6_subst( + cx: &LateContext<'_>, + addr: Symbol, + with_consts: bool, + explicit_type: Option>, +) -> Option<(String, bool)> { + as_ipv6_segments(addr).and_then(|segments| { + if let Some(qpath) = explicit_type { + qpath.span().with_source_text(cx, |ty| { + if with_consts && segments == Ipv6Addr::LOCALHOST.segments() { + (format!("{ty}::LOCALHOST"), true) + } else if with_consts && explicit_type.is_some() && segments == Ipv6Addr::UNSPECIFIED.segments() { + (format!("{ty}::UNSPECIFIED"), true) + } else { + (format!("{ty}::new({})", segments_to_string(&segments)), false) + } + }) + } else { + Some((format!("[{}].into()", segments_to_string(&segments)), false)) + } + }) +} + +/// Try parsing `addr` as an IPv6 address and return its 16-bit segments +fn as_ipv6_segments(addr: Symbol) -> Option<[u16; 8]> { + addr.as_str().parse().ok().as_ref().map(Ipv6Addr::segments) +} + +/// Return the `segments` separated by commas, in a common format for IPv6 addresses +fn segments_to_string(segments: &[u16; 8]) -> String { + segments + .map(|n| if n < 2 { n.to_string() } else { format!("{n:#x}") }) + .join(", ") +} + +/// Suggests a replacement if `addr` is a correct IPv6 address, with: +/// - the replacement string +/// - the entity that was detected +/// +/// `explicit_type` refers to `IpAddr`, and not to the content of one of the variants +/// (`IpAddr::V4` or `IpAddr::V6`). The use of constants from `Ipv4Addr` or `Ipv6Addr` +/// will not be proposed because we do not know if those types are imported in the scope. +fn ip_subst(cx: &LateContext<'_>, addr: Symbol, explicit_type: Option>) -> Option<(String, &'static str)> { + if let Some([a0, a1, a2, a3]) = as_ipv4_octets(addr) { + Some(( + if let Some(qpath) = explicit_type { + qpath + .span() + .with_source_text(cx, |ty| format!("{ty}::V4([{a0}, {a1}, {a2}, {a3}].into())"))? + } else { + format!("[{a0}, {a1}, {a2}, {a3}].into()") + }, + IPV4_ENTITY, + )) + } else if let Some(segments) = as_ipv6_segments(addr) { + Some(( + if let Some(qpath) = explicit_type { + qpath + .span() + .with_source_text(cx, |ty| format!("{ty}::V6([{}].into())", segments_to_string(&segments)))? + } else { + format!("[{}].into()", segments_to_string(&segments)) + }, + IPV6_ENTITY, + )) + } else { + None + } +} diff --git a/clippy_lints/src/methods/parsed_string_literals/mod.rs b/clippy_lints/src/methods/parsed_string_literals/mod.rs new file mode 100644 index 000000000000..3bc9dbac2bef --- /dev/null +++ b/clippy_lints/src/methods/parsed_string_literals/mod.rs @@ -0,0 +1,81 @@ +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::msrvs::Msrv; +use clippy_utils::source::SpanRangeExt as _; +use clippy_utils::sym; +use clippy_utils::ty::get_type_diagnostic_name; +use rustc_ast::LitKind; +use rustc_errors::Applicability; +use rustc_hir::{self as hir, Expr, ExprKind, GenericArg, Node, QPath}; +use rustc_lint::LateContext; + +mod ip_addresses; +mod primitive_types; + +use super::PARSED_STRING_LITERALS; + +/// Detects instances of `"literal".parse().unwrap()`: +/// - `expr` is the whole expression +/// - `recv` is the receiver of `parse()` +/// - `parse_call` is the `parse()` method call +/// - `msrv` is used for Rust version checking +pub(super) fn check(cx: &LateContext<'_>, expr: &Expr<'_>, recv: &Expr<'_>, parse_call: &Expr<'_>, msrv: Msrv) { + if let ExprKind::Lit(lit) = recv.kind + && let LitKind::Str(lit, _) = lit.node + { + let ty = cx.typeck_results().expr_ty(expr); + match get_type_diagnostic_name(cx, ty) { + _ if ty.is_primitive() => primitive_types::check(cx, expr, lit, ty, recv, type_from_parse(parse_call)), + Some(method @ (sym::IpAddr | sym::Ipv4Addr | sym::Ipv6Addr)) => ip_addresses::check( + cx, + expr, + lit, + method, + type_from_parse(parse_call).or_else(|| type_from_let(cx, expr)), + msrv, + ), + _ => (), + } + } +} + +/// Emit the lint if the length of `sugg` is no longer than the original `expr` span, or if `force` +/// is set. +fn maybe_emit_lint(cx: &LateContext<'_>, expr: &Expr<'_>, force: bool, entity: &str, sugg: String) { + if force || expr.span.check_source_text(cx, |snip| snip.len() >= sugg.len()) { + span_lint_and_sugg( + cx, + PARSED_STRING_LITERALS, + expr.span, + format!("unnecessary runtime parsing of {entity}"), + "use", + sugg, + Applicability::MachineApplicable, + ); + } +} + +/// Returns `T` from the `parse::(…)` call if present. +fn type_from_parse<'hir>(parse_call: &'hir Expr<'_>) -> Option> { + if let ExprKind::MethodCall(parse, _, _, _) = parse_call.kind + && let [GenericArg::Type(ty)] = parse.args().args + && let hir::TyKind::Path(qpath) = ty.kind + { + Some(qpath) + } else { + None + } +} + +/// Returns `T` if `expr` is the initialization of `let …: T = expr`. This is used as an extra +/// opportunity to use variant constructors when `T` denotes an `enum`. +fn type_from_let<'hir>(cx: &'hir LateContext<'_>, expr: &'hir Expr<'_>) -> Option> { + if let Node::LetStmt(let_stmt) = cx.tcx.parent_hir_node(expr.hir_id) + && let Some(ty) = let_stmt.ty + && let Some(ty) = ty.try_as_ambig_ty() + && let hir::TyKind::Path(qpath) = ty.kind + { + Some(qpath) + } else { + None + } +} diff --git a/clippy_lints/src/methods/parsed_string_literals/primitive_types.rs b/clippy_lints/src/methods/parsed_string_literals/primitive_types.rs new file mode 100644 index 000000000000..20f8f8cafcd7 --- /dev/null +++ b/clippy_lints/src/methods/parsed_string_literals/primitive_types.rs @@ -0,0 +1,102 @@ +use std::fmt::Display; +use std::str::FromStr; + +use clippy_utils::source::{SpanRangeExt as _, str_literal_to_char_literal}; +use clippy_utils::{get_parent_expr, sym}; +use rustc_errors::Applicability; +use rustc_hir::{Expr, ExprKind, Path, QPath}; +use rustc_lint::LateContext; +use rustc_middle::ty::{self, Ty}; +use rustc_span::Symbol; + +pub(super) fn check( + cx: &LateContext<'_>, + expr: &Expr<'_>, + lit: Symbol, + ty: Ty<'_>, + strlit: &Expr<'_>, + explicit_type: Option>, +) { + macro_rules! number { + ($kind:ident, $expr:expr, $msg:expr, [$($subkind:ident => $ty:ident),*$(,)?]$(,)?) => {{ + match $expr { + // If the right type has been found, return the string to substitute the parsing + // call with, which will be the literal followed by a suffix if initially the + // parse call was qualified with the return type. Note that we use a canonical + // suffix, whereas the parse call might have been qualified with a type alias, + // because type aliases can't be used as suffixes. + $(ty::$kind::$subkind => + ( + try_parse::<$ty>( + cx, + lit, + Some(sym::$ty), + explicit_type, + ), + $msg + ),)* + #[allow(unreachable_patterns)] + _ => return, + } + }}; + } + + if let (Some(mut subst), entity) = match ty.kind() { + ty::Int(int_ty) => number!(IntTy, int_ty, "a signed integer", + [Isize => isize, I8 => i8, I16 => i16, I32 => i32, I64 => i64, I128 => i128]), + ty::Uint(uint_ty) => number!(UintTy, uint_ty, "an unsigned integer", + [Usize => usize, U8 => u8, U16 => u16, U32 => u32, U64 => u64, U128 => u128]), + // FIXME: ignore `f16` and `f128` for now as they cannot use the default formatter + ty::Float(float_ty) => number!(FloatTy, float_ty, "a real number", + [F32 => f32, F64 => f64]), + ty::Bool => (try_parse::(cx, lit, None, None), "a boolean"), + ty::Char => { + let mut app = Applicability::MachineApplicable; + let literal = str_literal_to_char_literal(cx, strlit, &mut app, false); + if app != Applicability::MachineApplicable { + return; + } + (literal, "a single character") + }, + _ => return, + } { + let contains_cast = subst.contains(" as "); + if subst.starts_with('+') { + subst.remove(0); + } + if (contains_cast || subst.starts_with('-')) + && let Some(parent_expr) = get_parent_expr(cx, expr) + { + match parent_expr.kind { + // Unary negation and cast must be parenthesized if they are receivers of a method call + ExprKind::MethodCall(_, recv, _, _) if expr.hir_id == recv.hir_id => { + subst = format!("({subst})"); + }, + // Cast must be parenthesized if it is the argument of a unary operator + ExprKind::Unary(_, arg) if contains_cast && expr.hir_id == arg.hir_id => { + subst = format!("({subst})"); + }, + _ => {}, + } + } + super::maybe_emit_lint(cx, expr, false, entity, subst); + } +} + +fn try_parse( + cx: &LateContext<'_>, + lit: Symbol, + suffix: Option, + explicit_type: Option>, +) -> Option { + lit.as_str().parse::().ok().and_then(|_| match explicit_type { + Some(QPath::Resolved( + None, + Path { + segments: [segment], .. + }, + )) if Some(segment.ident.name) == suffix => Some(format!("{lit}_{}", segment.ident.name)), + Some(qpath) => qpath.span().with_source_text(cx, |ty| format!("{lit} as {ty}")), + None => Some(format!("{lit}")), + }) +} diff --git a/clippy_utils/src/msrvs.rs b/clippy_utils/src/msrvs.rs index 62041fc631c0..88b82dc93e80 100644 --- a/clippy_utils/src/msrvs.rs +++ b/clippy_utils/src/msrvs.rs @@ -70,7 +70,7 @@ msrv_aliases! { 1,33,0 { UNDERSCORE_IMPORTS } 1,32,0 { CONST_IS_POWER_OF_TWO } 1,31,0 { OPTION_REPLACE } - 1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES } + 1,30,0 { ITERATOR_FIND_MAP, TOOL_ATTRIBUTES, IPADDR_CONSTANTS } 1,29,0 { ITER_FLATTEN } 1,28,0 { FROM_BOOL, REPEAT_WITH, SLICE_FROM_REF } 1,27,0 { ITERATOR_TRY_FOLD, DOUBLE_ENDED_ITERATOR_RFIND } diff --git a/tests/ui/parsed_string_literals/ip_addresses.fixed b/tests/ui/parsed_string_literals/ip_addresses.fixed new file mode 100644 index 000000000000..4c88e2919e28 --- /dev/null +++ b/tests/ui/parsed_string_literals/ip_addresses.fixed @@ -0,0 +1,121 @@ +#![warn(clippy::parsed_string_literals)] +#![expect(clippy::needless_late_init)] + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +fn v4() { + // Explicit type: use constructor + let original = Ipv4Addr::new(137, 194, 161, 2); + //~^ parsed_string_literals + let fixed = Ipv4Addr::new(137, 194, 161, 2); + + // Explicit type: ensure that the HIR type name is used + type V4 = Ipv4Addr; + let original = V4::new(137, 194, 161, 2); + //~^ parsed_string_literals + let fixed = V4::new(137, 194, 161, 2); + + // The type might also be extracted from an initializing + // `let` statement. + let original: V4 = V4::new(137, 194, 161, 2); + //~^ parsed_string_literals + let fixed = V4::new(137, 194, 161, 2); + + // Type from context: use `.into()` to avoid referencing + // a type which might not be imported + let original: Ipv4Addr; + original = [137, 194, 161, 2].into(); + //~^ parsed_string_literals + let fixed: Ipv4Addr = [137, 194, 161, 2].into(); + + // Explicit type: use constant + let original = V4::LOCALHOST; + //~^ parsed_string_literals + let fixed = V4::LOCALHOST; + + // Type from context: do not use constant + let original: V4; + original = [127, 0, 0, 1].into(); + //~^ parsed_string_literals + let fixed: V4 = [127, 0, 0, 1].into(); + + // Type from context: do not use constant + let original = IpAddr::V4([127, 0, 0, 1].into()); + //~^ parsed_string_literals + let fixed = IpAddr::V4([127, 0, 0, 1].into()); + + // Various constants + let cst_broadcast = V4::BROADCAST; + //~^ parsed_string_literals + let cst_unspecified = V4::UNSPECIFIED; + //~^ parsed_string_literals +} + +fn v6() { + // Explicit type: use constructor + let original = Ipv6Addr::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd); + //~^ parsed_string_literals + let fixed = Ipv6Addr::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd); + + // Explicit type: ensure that the HIR type name is used + type V6 = Ipv6Addr; + let original = V6::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd); + //~^ parsed_string_literals + let fixed = V6::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd); + + // Type from context: use `.into()` to avoid referencing + // a type which might not be imported + let original: V6; + original = [0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into(); + //~^ parsed_string_literals + let fixed: V6 = [0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into(); + + // Explicit type: use constant + let original = V6::LOCALHOST; + //~^ parsed_string_literals + let fixed = V6::LOCALHOST; + + // Type from context: do not use constant. Here, we have no lint because not + // using the constant makes it too long. + let do_not_lint: V6; + do_not_lint = "::1".parse().unwrap(); + + // Various constants + let cst_unspecified = V6::UNSPECIFIED; + //~^ parsed_string_literals +} + +fn ipaddr() { + // Explicit type is given: use proper constructor and `.into()` for the + // arg to avoid using `Ipv4Addr`/`Ipv6Addr` which might not be in scope. + let original = IpAddr::V4([137, 194, 161, 2].into()); + //~^ parsed_string_literals + let fixed = IpAddr::V4([137, 194, 161, 2].into()); + let original = IpAddr::V6([0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into()); + //~^ parsed_string_literals + let fixed = IpAddr::V6([0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into()); + + // Explicit type is given: use proper constructor and `.into()` instead + // of the constant which would need to be prefixed with its type which might + // not be imported. + let original = IpAddr::V4([127, 0, 0, 1].into()); + //~^ parsed_string_literals + let fixed = IpAddr::V4([127, 0, 0, 1].into()); + + // The absence of explicit type doesn't allow the use of the constructor. + let original: IpAddr; + original = [137, 194, 161, 2].into(); + //~^ parsed_string_literals + let fixed: IpAddr = [137, 194, 161, 2].into(); + + // `_` must not be considered an explicit type. + let original: IpAddr; + original = [137, 194, 161, 2].into(); + //~^ parsed_string_literals + let fixed: IpAddr = [137, 194, 161, 2].into(); +} + +#[clippy::msrv = "1.29"] +fn msrv_under() { + _ = "::".parse::().unwrap(); +} diff --git a/tests/ui/parsed_string_literals/ip_addresses.rs b/tests/ui/parsed_string_literals/ip_addresses.rs new file mode 100644 index 000000000000..825a1377617c --- /dev/null +++ b/tests/ui/parsed_string_literals/ip_addresses.rs @@ -0,0 +1,121 @@ +#![warn(clippy::parsed_string_literals)] +#![expect(clippy::needless_late_init)] + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +fn v4() { + // Explicit type: use constructor + let original = "137.194.161.2".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = Ipv4Addr::new(137, 194, 161, 2); + + // Explicit type: ensure that the HIR type name is used + type V4 = Ipv4Addr; + let original = "137.194.161.2".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = V4::new(137, 194, 161, 2); + + // The type might also be extracted from an initializing + // `let` statement. + let original: V4 = "137.194.161.2".parse().unwrap(); + //~^ parsed_string_literals + let fixed = V4::new(137, 194, 161, 2); + + // Type from context: use `.into()` to avoid referencing + // a type which might not be imported + let original: Ipv4Addr; + original = "137.194.161.2".parse().unwrap(); + //~^ parsed_string_literals + let fixed: Ipv4Addr = [137, 194, 161, 2].into(); + + // Explicit type: use constant + let original = "127.0.0.1".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = V4::LOCALHOST; + + // Type from context: do not use constant + let original: V4; + original = "127.0.0.1".parse().unwrap(); + //~^ parsed_string_literals + let fixed: V4 = [127, 0, 0, 1].into(); + + // Type from context: do not use constant + let original = IpAddr::V4("127.0.0.1".parse().unwrap()); + //~^ parsed_string_literals + let fixed = IpAddr::V4([127, 0, 0, 1].into()); + + // Various constants + let cst_broadcast = "255.255.255.255".parse::().unwrap(); + //~^ parsed_string_literals + let cst_unspecified = "0.0.0.0".parse::().unwrap(); + //~^ parsed_string_literals +} + +fn v6() { + // Explicit type: use constructor + let original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = Ipv6Addr::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd); + + // Explicit type: ensure that the HIR type name is used + type V6 = Ipv6Addr; + let original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = V6::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd); + + // Type from context: use `.into()` to avoid referencing + // a type which might not be imported + let original: V6; + original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse().unwrap(); + //~^ parsed_string_literals + let fixed: V6 = [0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into(); + + // Explicit type: use constant + let original = "::1".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = V6::LOCALHOST; + + // Type from context: do not use constant. Here, we have no lint because not + // using the constant makes it too long. + let do_not_lint: V6; + do_not_lint = "::1".parse().unwrap(); + + // Various constants + let cst_unspecified = "::".parse::().unwrap(); + //~^ parsed_string_literals +} + +fn ipaddr() { + // Explicit type is given: use proper constructor and `.into()` for the + // arg to avoid using `Ipv4Addr`/`Ipv6Addr` which might not be in scope. + let original = "137.194.161.2".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = IpAddr::V4([137, 194, 161, 2].into()); + let original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = IpAddr::V6([0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into()); + + // Explicit type is given: use proper constructor and `.into()` instead + // of the constant which would need to be prefixed with its type which might + // not be imported. + let original = "127.0.0.1".parse::().unwrap(); + //~^ parsed_string_literals + let fixed = IpAddr::V4([127, 0, 0, 1].into()); + + // The absence of explicit type doesn't allow the use of the constructor. + let original: IpAddr; + original = "137.194.161.2".parse().unwrap(); + //~^ parsed_string_literals + let fixed: IpAddr = [137, 194, 161, 2].into(); + + // `_` must not be considered an explicit type. + let original: IpAddr; + original = "137.194.161.2".parse::<_>().unwrap(); + //~^ parsed_string_literals + let fixed: IpAddr = [137, 194, 161, 2].into(); +} + +#[clippy::msrv = "1.29"] +fn msrv_under() { + _ = "::".parse::().unwrap(); +} diff --git a/tests/ui/parsed_string_literals/ip_addresses.stderr b/tests/ui/parsed_string_literals/ip_addresses.stderr new file mode 100644 index 000000000000..a7e9a3530486 --- /dev/null +++ b/tests/ui/parsed_string_literals/ip_addresses.stderr @@ -0,0 +1,119 @@ +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:8:20 + | +LL | let original = "137.194.161.2".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv4Addr::new(137, 194, 161, 2)` + | + = note: `-D clippy::parsed-string-literals` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::parsed_string_literals)]` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:14:20 + | +LL | let original = "137.194.161.2".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V4::new(137, 194, 161, 2)` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:20:24 + | +LL | let original: V4 = "137.194.161.2".parse().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V4::new(137, 194, 161, 2)` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:27:16 + | +LL | original = "137.194.161.2".parse().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `[137, 194, 161, 2].into()` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:32:20 + | +LL | let original = "127.0.0.1".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V4::LOCALHOST` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:38:16 + | +LL | original = "127.0.0.1".parse().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `[127, 0, 0, 1].into()` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:43:31 + | +LL | let original = IpAddr::V4("127.0.0.1".parse().unwrap()); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `[127, 0, 0, 1].into()` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:48:25 + | +LL | let cst_broadcast = "255.255.255.255".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V4::BROADCAST` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:50:27 + | +LL | let cst_unspecified = "0.0.0.0".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V4::UNSPECIFIED` + +error: unnecessary runtime parsing of an IPv6 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:56:20 + | +LL | let original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `Ipv6Addr::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd)` + +error: unnecessary runtime parsing of an IPv6 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:62:20 + | +LL | let original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V6::new(0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd)` + +error: unnecessary runtime parsing of an IPv6 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:69:16 + | +LL | original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `[0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into()` + +error: unnecessary runtime parsing of an IPv6 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:74:20 + | +LL | let original = "::1".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V6::LOCALHOST` + +error: unnecessary runtime parsing of an IPv6 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:84:27 + | +LL | let cst_unspecified = "::".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `V6::UNSPECIFIED` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:91:20 + | +LL | let original = "137.194.161.2".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `IpAddr::V4([137, 194, 161, 2].into())` + +error: unnecessary runtime parsing of an IPv6 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:94:20 + | +LL | let original = "2a04:0000:0000:0000:0000:0000:0000:abcd".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `IpAddr::V6([0x2a04, 0, 0, 0, 0, 0, 0, 0xabcd].into())` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:101:20 + | +LL | let original = "127.0.0.1".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `IpAddr::V4([127, 0, 0, 1].into())` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:107:16 + | +LL | original = "137.194.161.2".parse().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `[137, 194, 161, 2].into()` + +error: unnecessary runtime parsing of an IPv4 address + --> tests/ui/parsed_string_literals/ip_addresses.rs:113:16 + | +LL | original = "137.194.161.2".parse::<_>().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `[137, 194, 161, 2].into()` + +error: aborting due to 19 previous errors + diff --git a/tests/ui/parsed_string_literals/primitive_types.fixed b/tests/ui/parsed_string_literals/primitive_types.fixed new file mode 100644 index 000000000000..861b15fb833e --- /dev/null +++ b/tests/ui/parsed_string_literals/primitive_types.fixed @@ -0,0 +1,66 @@ +#![warn(clippy::parsed_string_literals)] + +use std::ffi::c_int; + +fn main() { + _ = 10_usize; + //~^ parsed_string_literals + _ = 1.23_f32; + //~^ parsed_string_literals + _ = 1.2300_f32; + //~^ parsed_string_literals + _ = 'c'; + //~^ parsed_string_literals + _ = '"'; + //~^ parsed_string_literals + _ = '\''; + //~^ parsed_string_literals + + // Since the context provides the type to use for the result of `parse()`, + // do not include a suffix when issuing the constant. + let _: i64 = -17; + //~^ parsed_string_literals + + // Check that the original form is preserved ('🦀' == '\u{1f980}') + _ = '\u{1f980}'; + //~^ parsed_string_literals + _ = '🦀'; + //~^ parsed_string_literals + + // Do not lint invalid values + _ = "-10".parse::().unwrap(); + + // Ensure that trailing `+` is removed + _ = 10_usize; + //~^ parsed_string_literals + + // Negative literals must be parenthesized when receivers of a method call + let _: usize = (-10_isize).unsigned_abs(); + //~^ parsed_string_literals + + let _: c_int = 10; + //~^ parsed_string_literals + _ = 10 as c_int; + //~^ parsed_string_literals + + // Casts must be parenthesized when receivers of a method call + type MySizedType = isize; + let _: usize = (-10 as MySizedType).unsigned_abs(); + //~^ parsed_string_literals + + // Casts must be parenthesized when arguments of a unary operator + _ = -(-10 as MySizedType); + //~^ parsed_string_literals + + // Do not lint content or code coming from macros + macro_rules! mac { + (str) => { + "10" + }; + (parse $l:literal) => { + $l.parse::().unwrap() + }; + } + _ = mac!(str).parse::().unwrap(); + _ = mac!(parse "10"); +} diff --git a/tests/ui/parsed_string_literals/primitive_types.rs b/tests/ui/parsed_string_literals/primitive_types.rs new file mode 100644 index 000000000000..4ac5847bb462 --- /dev/null +++ b/tests/ui/parsed_string_literals/primitive_types.rs @@ -0,0 +1,66 @@ +#![warn(clippy::parsed_string_literals)] + +use std::ffi::c_int; + +fn main() { + _ = "10".parse::().unwrap(); + //~^ parsed_string_literals + _ = "1.23".parse::().unwrap(); + //~^ parsed_string_literals + _ = "1.2300".parse::().unwrap(); + //~^ parsed_string_literals + _ = "c".parse::().unwrap(); + //~^ parsed_string_literals + _ = r#"""#.parse::().unwrap(); + //~^ parsed_string_literals + _ = "'".parse::().unwrap(); + //~^ parsed_string_literals + + // Since the context provides the type to use for the result of `parse()`, + // do not include a suffix when issuing the constant. + let _: i64 = "-17".parse().unwrap(); + //~^ parsed_string_literals + + // Check that the original form is preserved ('🦀' == '\u{1f980}') + _ = "\u{1f980}".parse::().unwrap(); + //~^ parsed_string_literals + _ = "🦀".parse::().unwrap(); + //~^ parsed_string_literals + + // Do not lint invalid values + _ = "-10".parse::().unwrap(); + + // Ensure that trailing `+` is removed + _ = "+10".parse::().unwrap(); + //~^ parsed_string_literals + + // Negative literals must be parenthesized when receivers of a method call + let _: usize = "-10".parse::().unwrap().unsigned_abs(); + //~^ parsed_string_literals + + let _: c_int = "10".parse().unwrap(); + //~^ parsed_string_literals + _ = "10".parse::().unwrap(); + //~^ parsed_string_literals + + // Casts must be parenthesized when receivers of a method call + type MySizedType = isize; + let _: usize = "-10".parse::().unwrap().unsigned_abs(); + //~^ parsed_string_literals + + // Casts must be parenthesized when arguments of a unary operator + _ = -"-10".parse::().unwrap(); + //~^ parsed_string_literals + + // Do not lint content or code coming from macros + macro_rules! mac { + (str) => { + "10" + }; + (parse $l:literal) => { + $l.parse::().unwrap() + }; + } + _ = mac!(str).parse::().unwrap(); + _ = mac!(parse "10"); +} diff --git a/tests/ui/parsed_string_literals/primitive_types.stderr b/tests/ui/parsed_string_literals/primitive_types.stderr new file mode 100644 index 000000000000..f57b691b0632 --- /dev/null +++ b/tests/ui/parsed_string_literals/primitive_types.stderr @@ -0,0 +1,95 @@ +error: unnecessary runtime parsing of an unsigned integer + --> tests/ui/parsed_string_literals/primitive_types.rs:6:9 + | +LL | _ = "10".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `10_usize` + | + = note: `-D clippy::parsed-string-literals` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::parsed_string_literals)]` + +error: unnecessary runtime parsing of a real number + --> tests/ui/parsed_string_literals/primitive_types.rs:8:9 + | +LL | _ = "1.23".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `1.23_f32` + +error: unnecessary runtime parsing of a real number + --> tests/ui/parsed_string_literals/primitive_types.rs:10:9 + | +LL | _ = "1.2300".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `1.2300_f32` + +error: unnecessary runtime parsing of a single character + --> tests/ui/parsed_string_literals/primitive_types.rs:12:9 + | +LL | _ = "c".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `'c'` + +error: unnecessary runtime parsing of a single character + --> tests/ui/parsed_string_literals/primitive_types.rs:14:9 + | +LL | _ = r#"""#.parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `'"'` + +error: unnecessary runtime parsing of a single character + --> tests/ui/parsed_string_literals/primitive_types.rs:16:9 + | +LL | _ = "'".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `'\''` + +error: unnecessary runtime parsing of a signed integer + --> tests/ui/parsed_string_literals/primitive_types.rs:21:18 + | +LL | let _: i64 = "-17".parse().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^ help: use: `-17` + +error: unnecessary runtime parsing of a single character + --> tests/ui/parsed_string_literals/primitive_types.rs:25:9 + | +LL | _ = "\u{1f980}".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `'\u{1f980}'` + +error: unnecessary runtime parsing of a single character + --> tests/ui/parsed_string_literals/primitive_types.rs:27:9 + | +LL | _ = "🦀".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `'🦀'` + +error: unnecessary runtime parsing of an unsigned integer + --> tests/ui/parsed_string_literals/primitive_types.rs:34:9 + | +LL | _ = "+10".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `10_usize` + +error: unnecessary runtime parsing of a signed integer + --> tests/ui/parsed_string_literals/primitive_types.rs:38:20 + | +LL | let _: usize = "-10".parse::().unwrap().unsigned_abs(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `(-10_isize)` + +error: unnecessary runtime parsing of a signed integer + --> tests/ui/parsed_string_literals/primitive_types.rs:41:20 + | +LL | let _: c_int = "10".parse().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^ help: use: `10` + +error: unnecessary runtime parsing of a signed integer + --> tests/ui/parsed_string_literals/primitive_types.rs:43:9 + | +LL | _ = "10".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `10 as c_int` + +error: unnecessary runtime parsing of a signed integer + --> tests/ui/parsed_string_literals/primitive_types.rs:48:20 + | +LL | let _: usize = "-10".parse::().unwrap().unsigned_abs(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `(-10 as MySizedType)` + +error: unnecessary runtime parsing of a signed integer + --> tests/ui/parsed_string_literals/primitive_types.rs:52:10 + | +LL | _ = -"-10".parse::().unwrap(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: use: `(-10 as MySizedType)` + +error: aborting due to 15 previous errors +