diff --git a/CHANGELOG.md b/CHANGELOG.md index f7e7ed86eed8..5b8e4d20ddfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5213,6 +5213,7 @@ Released 2018-09-13 [`duplicate_underscore_argument`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_underscore_argument [`duplicated_attributes`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicated_attributes [`duration_subsec`]: https://rust-lang.github.io/rust-clippy/master/index.html#duration_subsec +[`duration_to_float_precision_loss`]: https://rust-lang.github.io/rust-clippy/master/index.html#duration_to_float_precision_loss [`eager_transmute`]: https://rust-lang.github.io/rust-clippy/master/index.html#eager_transmute [`else_if_without_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#else_if_without_else [`empty_docs`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_docs diff --git a/clippy_config/src/conf.rs b/clippy_config/src/conf.rs index 3b5de2c16ffa..6dc7e17cfa53 100644 --- a/clippy_config/src/conf.rs +++ b/clippy_config/src/conf.rs @@ -262,7 +262,7 @@ define_Conf! { /// /// Suppress lints whenever the suggested change would cause breakage for other crates. (avoid_breaking_exported_api: bool = true), - /// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, OPTION_MAP_UNWRAP_OR, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN, TYPE_REPETITION_IN_BOUNDS, TUPLE_ARRAY_CONVERSIONS, MANUAL_TRY_FOLD, MANUAL_HASH_ONE, ITER_KV_MAP, MANUAL_C_STR_LITERALS, ASSIGNING_CLONES, LEGACY_NUMERIC_CONSTANTS. + /// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, OPTION_MAP_UNWRAP_OR, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE, APPROX_CONSTANT, DEPRECATED_CFG_ATTR, INDEX_REFUTABLE_SLICE, MAP_CLONE, BORROW_AS_PTR, MANUAL_BITS, ERR_EXPECT, CAST_ABS_TO_UNSIGNED, UNINLINED_FORMAT_ARGS, MANUAL_CLAMP, MANUAL_LET_ELSE, UNCHECKED_DURATION_SUBTRACTION, COLLAPSIBLE_STR_REPLACE, SEEK_FROM_CURRENT, SEEK_REWIND, UNNECESSARY_LAZY_EVALUATIONS, TRANSMUTE_PTR_TO_REF, ALMOST_COMPLETE_RANGE, NEEDLESS_BORROW, DERIVABLE_IMPLS, MANUAL_IS_ASCII_CHECK, MANUAL_REM_EUCLID, MANUAL_RETAIN, TYPE_REPETITION_IN_BOUNDS, TUPLE_ARRAY_CONVERSIONS, MANUAL_TRY_FOLD, MANUAL_HASH_ONE, ITER_KV_MAP, MANUAL_C_STR_LITERALS, ASSIGNING_CLONES, DURATION_TO_FLOAT_PRECISION_LOSS, LEGACY_NUMERIC_CONSTANTS. /// /// The minimum rust version that the project supports. Defaults to the `rust-version` field in `Cargo.toml` #[default_text = ""] diff --git a/clippy_config/src/msrvs.rs b/clippy_config/src/msrvs.rs index 32107ded305e..1841377a1193 100644 --- a/clippy_config/src/msrvs.rs +++ b/clippy_config/src/msrvs.rs @@ -40,7 +40,7 @@ msrv_aliases! { 1,42,0 { MATCHES_MACRO, SLICE_PATTERNS, PTR_SLICE_RAW_PARTS } 1,41,0 { RE_REBALANCING_COHERENCE, RESULT_MAP_OR_ELSE } 1,40,0 { MEM_TAKE, NON_EXHAUSTIVE, OPTION_AS_DEREF } - 1,38,0 { POINTER_CAST, REM_EUCLID } + 1,38,0 { POINTER_CAST, REM_EUCLID, DURATION_AS_SECS_FLOAT } 1,37,0 { TYPE_ALIAS_ENUM_VARIANTS } 1,36,0 { ITERATOR_COPIED } 1,35,0 { OPTION_COPIED, RANGE_CONTAINS } diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs index 5ff7d8e51343..e5b61dd58042 100644 --- a/clippy_lints/src/declared_lints.rs +++ b/clippy_lints/src/declared_lints.rs @@ -155,6 +155,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[ crate::drop_forget_ref::FORGET_NON_DROP_INFO, crate::drop_forget_ref::MEM_FORGET_INFO, crate::duplicate_mod::DUPLICATE_MOD_INFO, + crate::duration_to_float_precision_loss::DURATION_TO_FLOAT_PRECISION_LOSS_INFO, crate::else_if_without_else::ELSE_IF_WITHOUT_ELSE_INFO, crate::empty_drop::EMPTY_DROP_INFO, crate::empty_enum::EMPTY_ENUM_INFO, diff --git a/clippy_lints/src/duration_to_float_precision_loss.rs b/clippy_lints/src/duration_to_float_precision_loss.rs new file mode 100644 index 000000000000..ae9c299dcc4b --- /dev/null +++ b/clippy_lints/src/duration_to_float_precision_loss.rs @@ -0,0 +1,393 @@ +use clippy_config::msrvs::{self, Msrv}; +use clippy_utils::consts::{constant, Constant}; +use clippy_utils::diagnostics::span_lint_and_sugg; +use clippy_utils::source::snippet_with_applicability; +use clippy_utils::ty::is_type_diagnostic_item; +use rustc_errors::Applicability; +use rustc_hir::{BinOpKind, Expr, ExprKind}; +use rustc_lint::{LateContext, LateLintPass, LintContext}; +use rustc_middle::lint::in_external_macro; +use rustc_middle::ty::{self, FloatTy}; +use rustc_span::{sym, Span}; + +declare_clippy_lint! { + /// ### What it does + /// Checks for conversions of a `Duration` to a floating point number where + /// precision is lost. + /// + /// ### Why is this bad? + /// This can be bad if the user wanted to retain the full precision of the duration. + /// + /// ### Example + /// ```no_run + /// # use std::time::Duration; + /// # let duration = Duration::from_nanos(1234500000); + /// let _ = duration.as_millis() as f64; + /// ``` + /// + /// Use instead: + /// + /// ```no_run + /// # use std::time::Duration; + /// # let duration = Duration::from_nanos(1234500000); + /// let _ = duration.as_secs_f64() * 1000.0; + /// ``` + /// + /// Another motivating example happens when calculating number of seconds as a float with millisecond precision: + /// + /// ```no_run + /// # use std::time::Duration; + /// # let duration = Duration::from_nanos(1234500000); + /// let _ = duration.as_millis() as f64 / 1000.0; + /// ``` + /// + /// Use instead: + /// + /// ```no_run + /// # use std::time::Duration; + /// # let duration = Duration::from_nanos(1234500000); + /// let _ = duration.as_secs_f64(); + /// ``` + #[clippy::version = "1.78.0"] + pub DURATION_TO_FLOAT_PRECISION_LOSS, + nursery, + "conversion from duration to float that cause loss of precision" +} + +/// This struct implements the logic needed to apply the lint +#[derive(Debug)] +pub struct DurationToFloatPrecisionLoss { + // This vector is used to prevent applying the lint to a sub-expression + lint_applications: Vec, + // `as_secs_f64` isn't applicable until 1.38.0 + msrv: Msrv, +} + +impl DurationToFloatPrecisionLoss { + /// Create a new instance of this lint + pub fn new(msrv: Msrv) -> Self { + Self { + lint_applications: Vec::new(), + msrv, + } + } + + fn should_emit_lint(&self, site: &LintApplicableSite) -> bool { + let Some(last) = self.lint_applications.last() else { + // if the stack is empty, then there is no outer expression + return true; + }; + + // don't emit this instance of the lint, if there is a previous instance + // which already covers this span + !last.contains(site.root_span) + } +} + +rustc_session::impl_lint_pass!(DurationToFloatPrecisionLoss => [DURATION_TO_FLOAT_PRECISION_LOSS]); + +impl<'tcx> LateLintPass<'tcx> for DurationToFloatPrecisionLoss { + fn check_expr(&mut self, cx: &LateContext<'tcx>, root: &'tcx Expr<'_>) { + if in_external_macro(cx.sess(), root.span) { + // We should ignore macro from a foreign crate. + return; + } + + let site = match root.kind { + ExprKind::Cast(duration_expr, _) => { + let Some(lint_site) = check_cast(cx, root, duration_expr) else { + // does not match criteria - cast didn't have right contents + return; + }; + + lint_site + }, + ExprKind::Binary(op, lhs, rhs) => { + if let ExprKind::Cast(duration_expr, _) = lhs.kind { + let Some(mut lint_site) = check_cast(cx, root, duration_expr) else { + // does not match criteria - cast didn't have right contents + return; + }; + + let constant_factor = constant(cx, cx.typeck_results(), rhs); + let Some(adjustment) = check_adjustment(op.node, constant_factor) else { + // does not match criteria - adjustment was not shaped correctly + return; + }; + + lint_site.adjustment = Some(adjustment); + lint_site + } else { + // does not match criteria - lhs of binary op was not a cast + return; + } + }, + _ => { + // does not match criteria - root expression was not a cast or a binary op + return; + }, + }; + + if !self.msrv.meets(msrvs::DURATION_AS_SECS_FLOAT) { + // rust version doesn't have required methods + return; + } + + // check to make sure this lint site is not already covered + if self.should_emit_lint(&site) { + site.emit_lint(cx); + self.lint_applications.push(site.root_span); + } + } + + fn check_expr_post(&mut self, _: &LateContext<'tcx>, root: &'tcx Expr<'tcx>) { + // clean up the stack + if self + .lint_applications + .last() + .is_some_and(|root_span| root_span == &root.span) + { + self.lint_applications.pop(); + } + } + + extract_msrv_attr!(LateContext); +} + +#[allow(clippy::enum_glob_use)] +fn check_adjustment( + bin_op: BinOpKind, + constant_factor: Option>, +) -> Option<(AdjustmentDirection, AdjustmentScale)> { + use AdjustmentDirection::*; + use AdjustmentScale::*; + + fn check_adjustment_f64(bin_op: BinOpKind, factor: f64) -> Option<(AdjustmentDirection, AdjustmentScale)> { + let scale = match factor { + // initially assuming multiply + 1e3 => Thousand, + 1e6 => Million, + 1e9 => Billion, + 1e-3 => Thousand, + 1e-6 => Million, + 1e-9 => Billion, + _ => return None, // does not match criteria - the factor is not in the predefined list + }; + + let direction = if factor > 1.0 { Positive } else { Negative }; + + let direction = match bin_op { + // swap direction when dividing + BinOpKind::Div => direction.negate(), + // keep mul the same + BinOpKind::Mul => direction, + _ => return None, // does not match criteria - the binary operation is not scaling the expression + }; + + Some((direction, scale)) + } + + match constant_factor? { + Constant::F32(val) => check_adjustment_f64(bin_op, f64::from(val)), + Constant::F64(val) => check_adjustment_f64(bin_op, val), + _ => None, // does not match criteria - constant is not a float of the expected sizes + } +} + +fn check_cast<'tcx>( + cx: &LateContext<'tcx>, + root: &'tcx Expr<'_>, + duration_expr: &'tcx Expr<'_>, +) -> Option { + if let ExprKind::MethodCall(method_path, method_receiver_expr, [], _) = duration_expr.kind { + let method_receiver_ty = cx.typeck_results().expr_ty(method_receiver_expr); + if is_type_diagnostic_item(cx, method_receiver_ty.peel_refs(), sym::Duration) { + let cast_expr_ty = cx.typeck_results().expr_ty(root); + + let precision = match cast_expr_ty.kind() { + ty::Float(FloatTy::F32) => FloatPrecision::F32, + ty::Float(FloatTy::F64) => FloatPrecision::F64, + _ => { + // does not match criteria - not the right kind of float + return None; + }, + }; + + let duration_method = match method_path.ident.as_str() { + "as_secs" => DurationMethod::AsSeconds, + "as_millis" => DurationMethod::AsMillis, + "as_micros" => DurationMethod::AsMicros, + "as_nanos" => DurationMethod::AsNanos, + _ => { + // does not match criteria - not the type of duration methods we're interested in + return None; + }, + }; + + Some(LintApplicableSite { + duration_method, + precision, + root_span: root.span, + adjustment: None, + duration_expr_span: method_receiver_expr.span, + }) + } else { + // does not match criteria - method receiver type is not Duration + None + } + } else { + // does not match criteria - not a method call expression in the expression of the cast + None + } +} + +#[derive(Debug, Copy, Clone)] +enum FloatPrecision { + F32, + F64, +} + +impl FloatPrecision { + fn as_secs_float_method(self) -> &'static str { + match self { + FloatPrecision::F32 => "as_secs_f32", + FloatPrecision::F64 => "as_secs_f64", + } + } +} + +#[derive(Debug, Copy, Clone)] +#[allow(clippy::enum_variant_names)] +enum DurationMethod { + AsSeconds, + AsMillis, + AsMicros, + AsNanos, +} + +#[derive(Debug, Copy, Clone)] +enum AdjustmentScale { + Thousand, + Million, + Billion, +} + +#[derive(Debug, Copy, Clone)] +enum AdjustmentDirection { + /// Decrease the number - multiply by factor < 1, or divide by factor > 1 + Negative, + /// Increase the number - multiply by factor > 1, or divide by factor < 1 + Positive, +} + +impl AdjustmentDirection { + fn negate(self) -> Self { + match self { + AdjustmentDirection::Positive => AdjustmentDirection::Negative, + AdjustmentDirection::Negative => AdjustmentDirection::Positive, + } + } +} + +#[derive(Debug, Clone)] +struct LintApplicableSite { + precision: FloatPrecision, + root_span: Span, + duration_expr_span: Span, + duration_method: DurationMethod, + // When the adjustment is missing from the site, set this to (`One`, whatever) + adjustment: Option<(AdjustmentDirection, AdjustmentScale)>, +} + +impl LintApplicableSite { + /// Return a string with the suggestion or None if this lint is not applicable. + #[allow(clippy::enum_glob_use)] + fn suggestion(&self) -> Option { + use AdjustmentScale::*; + + let (method_name, scale) = apply_adjustment(self.precision, self.duration_method, self.adjustment)?; + let scale = match scale { + None => "", + Some(Thousand) => " * 1e3", + Some(Million) => " * 1e6", + Some(Billion) => " * 1e9", + }; + + let suggestion = format!("{method_name}(){scale}"); + Some(suggestion) + } + + fn emit_lint(&self, cx: &LateContext<'_>) { + let mut applicability = Applicability::MachineApplicable; + let Some(suggested_expr) = self.suggestion() else { + // lint not applicable + return; + }; + + if !self.root_span.eq_ctxt(self.duration_expr_span) { + // different ctxt indicates a macro in the mix, can't apply lint + return; + } + + span_lint_and_sugg( + cx, + DURATION_TO_FLOAT_PRECISION_LOSS, + self.root_span, + &format!("calling `{suggested_expr}` is more precise than this calculation"), + "try", + format!( + "{}.{suggested_expr}", + snippet_with_applicability(cx, self.duration_expr_span, "_", &mut applicability) + ), + applicability, + ); + } +} + +#[allow(clippy::enum_glob_use, clippy::match_same_arms)] +fn apply_adjustment( + precision: FloatPrecision, + duration_method: DurationMethod, + adjustment: Option<(AdjustmentDirection, AdjustmentScale)>, +) -> Option<(&'static str, Option)> { + use AdjustmentDirection::*; + use AdjustmentScale::*; + use DurationMethod::*; + + let secs_method = precision.as_secs_float_method(); + + let result = match (duration_method, adjustment) { + // `as_{secs,millis,micros,nanos}() as f64` + (AsSeconds, None) => (secs_method, None), + (AsMillis, None) => (secs_method, Some(Thousand)), + (AsMicros, None) => (secs_method, Some(Million)), + (AsNanos, None) => return None, // `as_nanos() as f{32,64}` is already max precision + // `as_secs() as f64 * scale` + (AsSeconds, Some((Positive, scale))) => (secs_method, Some(scale)), + (AsSeconds, Some((Negative, _))) => return None, // lint isn't applicable to prefixes over unit (1) + // `as_millis() as f64 * scale` + (AsMillis, Some((Positive, Thousand))) => (secs_method, Some(Million)), + (AsMillis, Some((Positive, Million))) => (secs_method, Some(Billion)), + (AsMillis, Some((Positive, _))) => return None, // lint isn't applicable to prefixes below nanos + (AsMillis, Some((Negative, Thousand))) => (secs_method, None), + (AsMillis, Some((Negative, _))) => return None, // lint isn't applicable to prefixes over unit (1) + // `as_micros() as f64 * scale` + (AsMicros, Some((Positive, Thousand))) => (secs_method, Some(Billion)), + (AsMicros, Some((Positive, _))) => return None, // lint isn't applicable to prefixes below nanos + (AsMicros, Some((Negative, Thousand))) => (secs_method, Some(Thousand)), + (AsMicros, Some((Negative, Million))) => (secs_method, None), + (AsMicros, Some((Negative, _))) => return None, // lint isn't applicable to prefixes over unit (1) + // `as_nanos() as f64 * scale` + (AsNanos, Some((Positive, _))) => return None, // lint isn't applicable to prefixes below nanos + (AsNanos, Some((Negative, Thousand | Million))) => { + // if the expression is converting nanos to micros or millis, its already at max precision + return None; + }, + (AsNanos, Some((Negative, Billion))) => { + // this suggestion won't improve the precision, but it is more succint + (secs_method, None) + }, + }; + + Some(result) +} diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs index b92364a9d147..c7b2e8341779 100644 --- a/clippy_lints/src/lib.rs +++ b/clippy_lints/src/lib.rs @@ -122,6 +122,7 @@ mod doc; mod double_parens; mod drop_forget_ref; mod duplicate_mod; +mod duration_to_float_precision_loss; mod else_if_without_else; mod empty_drop; mod empty_enum; @@ -1131,6 +1132,11 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) { store.register_late_pass(|_| Box::new(zero_repeat_side_effects::ZeroRepeatSideEffects)); store.register_late_pass(|_| Box::new(manual_unwrap_or_default::ManualUnwrapOrDefault)); store.register_late_pass(|_| Box::new(integer_division_remainder_used::IntegerDivisionRemainderUsed)); + store.register_late_pass(move |_| { + Box::new(duration_to_float_precision_loss::DurationToFloatPrecisionLoss::new( + msrv(), + )) + }); // add lints here, do not remove this comment, it's used in `new_lint` } diff --git a/tests/ui/duration_to_float_precision_loss.fixed b/tests/ui/duration_to_float_precision_loss.fixed new file mode 100644 index 000000000000..ae5bbe8bddbf --- /dev/null +++ b/tests/ui/duration_to_float_precision_loss.fixed @@ -0,0 +1,59 @@ +#![warn(clippy::duration_to_float_precision_loss)] + +use std::time::Duration; + +const DURATION: Duration = Duration::from_nanos(1_234_567_890); + +fn main() { + let float_secs = DURATION.as_secs_f64(); + assert_eq!(float_secs, 1.234); + + macro_rules! m { + ($d:expr) => {{ + let float_secs = $d.as_secs() as f64; + assert_eq!(float_secs, 1.234); + }}; + } + + m!(DURATION); + + let float_secs = DURATION.as_secs_f32(); + assert_eq!(float_secs, 1.234); + + let float_secs = DURATION.as_secs_f64(); + assert_eq!(float_secs, 1.234); + + let float_secs = DURATION.as_secs_f64(); + assert_eq!(float_secs, 1.234); + + let float_secs = DURATION.as_secs_f64(); + assert_eq!(float_secs, 1.234); + + let float_millis = DURATION.as_secs_f64() * 1e3; + assert_eq!(float_millis, 1234.0); + + let float_micros = DURATION.as_secs_f64() * 1e6; + assert_eq!(float_micros, 1_234_567.0); + + // should not trigger on these conversion, since they are already the max precision + let float_nanos = DURATION.as_nanos() as f64; + assert_eq!(float_nanos, 1_234_567_890.0); + + let float_micros = DURATION.as_nanos() as f64 / 1_000.0; + assert_eq!(float_micros, 1_234_567.0); + + let float_millis = DURATION.as_nanos() as f64 / 1_000_000.0; + assert_eq!(float_millis, 1234.0); +} + +#[clippy::msrv = "1.37"] +fn msrv_1_37() { + let float_secs = DURATION.as_secs() as f64; // should not trigger lint because MSRV is below + assert_eq!(float_secs, 1.234); +} + +#[clippy::msrv = "1.38"] +fn msrv_1_38() { + let float_secs = DURATION.as_secs_f64(); // should trigger lint because MSRV is satisfied + assert_eq!(float_secs, 1.234); +} diff --git a/tests/ui/duration_to_float_precision_loss.rs b/tests/ui/duration_to_float_precision_loss.rs new file mode 100644 index 000000000000..3f560e673899 --- /dev/null +++ b/tests/ui/duration_to_float_precision_loss.rs @@ -0,0 +1,59 @@ +#![warn(clippy::duration_to_float_precision_loss)] + +use std::time::Duration; + +const DURATION: Duration = Duration::from_nanos(1_234_567_890); + +fn main() { + let float_secs = DURATION.as_secs() as f64; + assert_eq!(float_secs, 1.234); + + macro_rules! m { + ($d:expr) => {{ + let float_secs = $d.as_secs() as f64; + assert_eq!(float_secs, 1.234); + }}; + } + + m!(DURATION); + + let float_secs = DURATION.as_secs() as f32; + assert_eq!(float_secs, 1.234); + + let float_secs = DURATION.as_millis() as f64 / 1_000.0; + assert_eq!(float_secs, 1.234); + + let float_secs = DURATION.as_micros() as f64 / 1_000_000.0; + assert_eq!(float_secs, 1.234); + + let float_secs = DURATION.as_nanos() as f64 / 1_000_000_000.0; + assert_eq!(float_secs, 1.234); + + let float_millis = DURATION.as_millis() as f64; + assert_eq!(float_millis, 1234.0); + + let float_micros = DURATION.as_micros() as f64; + assert_eq!(float_micros, 1_234_567.0); + + // should not trigger on these conversion, since they are already the max precision + let float_nanos = DURATION.as_nanos() as f64; + assert_eq!(float_nanos, 1_234_567_890.0); + + let float_micros = DURATION.as_nanos() as f64 / 1_000.0; + assert_eq!(float_micros, 1_234_567.0); + + let float_millis = DURATION.as_nanos() as f64 / 1_000_000.0; + assert_eq!(float_millis, 1234.0); +} + +#[clippy::msrv = "1.37"] +fn msrv_1_37() { + let float_secs = DURATION.as_secs() as f64; // should not trigger lint because MSRV is below + assert_eq!(float_secs, 1.234); +} + +#[clippy::msrv = "1.38"] +fn msrv_1_38() { + let float_secs = DURATION.as_secs() as f64; // should trigger lint because MSRV is satisfied + assert_eq!(float_secs, 1.234); +} diff --git a/tests/ui/duration_to_float_precision_loss.stderr b/tests/ui/duration_to_float_precision_loss.stderr new file mode 100644 index 000000000000..2afa8b4d0ec9 --- /dev/null +++ b/tests/ui/duration_to_float_precision_loss.stderr @@ -0,0 +1,53 @@ +error: calling `as_secs_f64()` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:8:22 + | +LL | let float_secs = DURATION.as_secs() as f64; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f64()` + | + = note: `-D clippy::duration-to-float-precision-loss` implied by `-D warnings` + = help: to override `-D warnings` add `#[allow(clippy::duration_to_float_precision_loss)]` + +error: calling `as_secs_f32()` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:20:22 + | +LL | let float_secs = DURATION.as_secs() as f32; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f32()` + +error: calling `as_secs_f64()` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:23:22 + | +LL | let float_secs = DURATION.as_millis() as f64 / 1_000.0; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f64()` + +error: calling `as_secs_f64()` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:26:22 + | +LL | let float_secs = DURATION.as_micros() as f64 / 1_000_000.0; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f64()` + +error: calling `as_secs_f64()` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:29:22 + | +LL | let float_secs = DURATION.as_nanos() as f64 / 1_000_000_000.0; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f64()` + +error: calling `as_secs_f64() * 1e3` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:32:24 + | +LL | let float_millis = DURATION.as_millis() as f64; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f64() * 1e3` + +error: calling `as_secs_f64() * 1e6` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:35:24 + | +LL | let float_micros = DURATION.as_micros() as f64; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f64() * 1e6` + +error: calling `as_secs_f64()` is more precise than this calculation + --> tests/ui/duration_to_float_precision_loss.rs:57:22 + | +LL | let float_secs = DURATION.as_secs() as f64; // should trigger lint because MSRV is satisfied + | ^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `DURATION.as_secs_f64()` + +error: aborting due to 8 previous errors +