Repository: rust-lang/rustfmt (rust)

Issue:
Add leading/trailing zeros to floats
Essentially, these reformats should be done for consistency and readability:

```
x. -> x.0
.x -> 0.x
```

So `1.` would become `1.0` and `.1` would become `0.1`.

Pull request description:
Add option to control trailing zero in floating-point literals
This is my first contribution to rustfmt. Feel free to nitpick :)

Also please tell me if I did the tests right. Three separate test files seem like a lot for such a small feature, but I don't see how else I could check all configurations.

Closes #3187

Patch:
diff --git a/Configurations.md b/Configurations.md
index 61b9995e..e27c40ff 100644
--- a/Configurations.md
+++ b/Configurations.md
@@ -1256,6 +1256,56 @@ Control the case of the letters in hexadecimal literal values
 - **Possible values**: `Preserve`, `Upper`, `Lower`
 - **Stable**: No (tracking issue: [#5081](https://github.com/rust-lang/rustfmt/issues/5081))
 
+## `float_literal_trailing_zero`
+
+Control the presence of trailing zero in floating-point literal values
+
+- **Default value**: `Preserve`
+- **Possible values**: `Preserve`, `Always`, `IfNoPostfix`, `Never`
+- **Stable**: No (tracking issue: [#6471](https://github.com/rust-lang/rustfmt/issues/6471))
+
+#### `Preserve` (default):
+
+Leave the literal as-is.
+
+```rust
+fn main() {
+    let values = [1.0, 2., 3.0e10, 4f32];
+}
+```
+
+#### `Always`:
+
+Add a trailing zero to the literal:
+
+```rust
+fn main() {
+    let values = [1.0, 2.0, 3.0e10, 4.0f32];
+}
+```
+
+#### `IfNoPostfix`:
+
+Add a trailing zero by default. If the literal contains an exponent or a suffix, the zero
+and the preceding period are removed:
+
+```rust
+fn main() {
+    let values = [1.0, 2.0, 3e10, 4f32];
+}
+```
+
+#### `Never`:
+
+Remove the trailing zero. If the literal contains an exponent or a suffix, the preceding
+period is also removed:
+
+```rust
+fn main() {
+    let values = [1., 2., 3e10, 4f32];
+}
+```
+
 ## `hide_parse_errors`
 
 This option is deprecated and has been renamed to `show_parse_errors` to avoid confusion around the double negative default of `hide_parse_errors=false`.
diff --git a/src/chains.rs b/src/chains.rs
index acd0c296..50a129b6 100644
--- a/src/chains.rs
+++ b/src/chains.rs
@@ -264,7 +264,7 @@ impl ChainItemKind {
                 return (
                     ChainItemKind::Parent {
                         expr: expr.clone(),
-                        parens: is_method_call_receiver && should_add_parens(expr),
+                        parens: is_method_call_receiver && should_add_parens(expr, context),
                     },
                     expr.span,
                 );
@@ -1049,12 +1049,12 @@ fn trim_tries(s: &str) -> String {
 /// 1. .method();
 /// ```
 /// Which all need parenthesis or a space before `.method()`.
-fn should_add_parens(expr: &ast::Expr) -> bool {
+fn should_add_parens(expr: &ast::Expr, context: &RewriteContext<'_>) -> bool {
     match expr.kind {
-        ast::ExprKind::Lit(ref lit) => crate::expr::lit_ends_in_dot(lit),
+        ast::ExprKind::Lit(ref lit) => crate::expr::lit_ends_in_dot(lit, context),
         ast::ExprKind::Closure(ref cl) => match cl.body.kind {
             ast::ExprKind::Range(_, _, ast::RangeLimits::HalfOpen) => true,
-            ast::ExprKind::Lit(ref lit) => crate::expr::lit_ends_in_dot(lit),
+            ast::ExprKind::Lit(ref lit) => crate::expr::lit_ends_in_dot(lit, context),
             _ => false,
         },
         _ => false,
diff --git a/src/config/mod.rs b/src/config/mod.rs
index 7355adc9..439a5765 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -79,6 +79,8 @@ create_config! {
     skip_macro_invocations: SkipMacroInvocations, false,
         "Skip formatting the bodies of macros invoked with the following names.";
     hex_literal_case: HexLiteralCaseConfig, false, "Format hexadecimal integer literals";
+    float_literal_trailing_zero: FloatLiteralTrailingZeroConfig, false,
+        "Add or remove trailing zero in floating-point literals";
 
     // Single line expressions and items
     empty_item_single_line: EmptyItemSingleLine, false,
@@ -739,6 +741,7 @@ format_macro_matchers = false
 format_macro_bodies = true
 skip_macro_invocations = []
 hex_literal_case = "Preserve"
+float_literal_trailing_zero = "Preserve"
 empty_item_single_line = true
 struct_lit_single_line = true
 fn_single_line = false
@@ -829,6 +832,7 @@ format_macro_matchers = false
 format_macro_bodies = true
 skip_macro_invocations = []
 hex_literal_case = "Preserve"
+float_literal_trailing_zero = "Preserve"
 empty_item_single_line = true
 struct_lit_single_line = true
 fn_single_line = false
diff --git a/src/config/options.rs b/src/config/options.rs
index b9b47130..3f86999c 100644
--- a/src/config/options.rs
+++ b/src/config/options.rs
@@ -146,6 +146,21 @@ pub enum HexLiteralCase {
     Lower,
 }
 
+/// How to treat trailing zeros in floating-point literals.
+#[config_type]
+pub enum FloatLiteralTrailingZero {
+    /// Leave the literal as-is.
+    Preserve,
+    /// Add a trailing zero to the literal.
+    Always,
+    /// Add a trailing zero by default. If the literal contains an exponent or a suffix, the zero
+    /// and the preceding period are removed.
+    IfNoPostfix,
+    /// Remove the trailing zero. If the literal contains an exponent or a suffix, the preceding
+    /// period is also removed.
+    Never,
+}
+
 #[config_type]
 pub enum ReportTactic {
     Always,
@@ -613,6 +628,8 @@ config_option_with_style_edition_default!(
     FormatMacroBodies, bool, _ => true;
     SkipMacroInvocations, MacroSelectors, _ => MacroSelectors::default();
     HexLiteralCaseConfig, HexLiteralCase, _ => HexLiteralCase::Preserve;
+    FloatLiteralTrailingZeroConfig, FloatLiteralTrailingZero, _ =>
+        FloatLiteralTrailingZero::Preserve;
 
     // Single line expressions and items
     EmptyItemSingleLine, bool, _ => true;
diff --git a/src/expr.rs b/src/expr.rs
index 8031ab68..0c575238 100644
--- a/src/expr.rs
+++ b/src/expr.rs
@@ -13,8 +13,8 @@ use crate::comment::{
     CharClasses, FindUncommented, combine_strs_with_missing_comments, contains_comment,
     recover_comment_removed, rewrite_comment, rewrite_missing_comment,
 };
-use crate::config::lists::*;
 use crate::config::{Config, ControlBraceStyle, HexLiteralCase, IndentStyle, StyleEdition};
+use crate::config::{FloatLiteralTrailingZero, lists::*};
 use crate::lists::{
     ListFormatting, Separator, definitive_tactic, itemize_list, shape_for_tactic,
     struct_lit_formatting, struct_lit_shape, struct_lit_tactic, write_list,
@@ -54,8 +54,32 @@ pub(crate) enum ExprType {
     SubExpression,
 }
 
-pub(crate) fn lit_ends_in_dot(lit: &Lit) -> bool {
-    matches!(lit, Lit { kind: LitKind::Float, suffix: None, symbol } if symbol.as_str().ends_with('.'))
+pub(crate) fn lit_ends_in_dot(lit: &Lit, context: &RewriteContext<'_>) -> bool {
+    match lit.kind {
+        LitKind::Float => float_lit_ends_in_dot(
+            lit.symbol.as_str(),
+            lit.suffix.as_ref().map(|s| s.as_str()),
+            context.config.float_literal_trailing_zero(),
+        ),
+        _ => false,
+    }
+}
+
+pub(crate) fn float_lit_ends_in_dot(
+    symbol: &str,
+    suffix: Option<&str>,
+    float_literal_trailing_zero: FloatLiteralTrailingZero,
+) -> bool {
+    match float_literal_trailing_zero {
+        FloatLiteralTrailingZero::Preserve => symbol.ends_with('.') && suffix.is_none(),
+        FloatLiteralTrailingZero::IfNoPostfix | FloatLiteralTrailingZero::Always => false,
+        FloatLiteralTrailingZero::Never => {
+            let float_parts = parse_float_symbol(symbol).unwrap();
+            let has_postfix = float_parts.exponent.is_some() || suffix.is_some();
+            let fractional_part_zero = float_parts.is_fractional_part_zero();
+            !has_postfix && fractional_part_zero
+        }
+    }
 }
 
 pub(crate) fn format_expr(
@@ -297,7 +321,7 @@ pub(crate) fn format_expr(
 
             fn needs_space_before_range(context: &RewriteContext<'_>, lhs: &ast::Expr) -> bool {
                 match lhs.kind {
-                    ast::ExprKind::Lit(token_lit) => lit_ends_in_dot(&token_lit),
+                    ast::ExprKind::Lit(token_lit) => lit_ends_in_dot(&token_lit, context),
                     ast::ExprKind::Unary(_, ref expr) => needs_space_before_range(context, expr),
                     ast::ExprKind::Binary(_, _, ref rhs_expr) => {
                         needs_space_before_range(context, rhs_expr)
@@ -1276,6 +1300,7 @@ pub(crate) fn rewrite_literal(
     match token_lit.kind {
         token::LitKind::Str => rewrite_string_lit(context, span, shape),
         token::LitKind::Integer => rewrite_int_lit(context, token_lit, span, shape),
+        token::LitKind::Float => rewrite_float_lit(context, token_lit, span, shape),
         _ => wrap_str(
             context.snippet(span).to_owned(),
             context.config.max_width(),
@@ -1319,6 +1344,10 @@ fn rewrite_int_lit(
     span: Span,
     shape: Shape,
 ) -> RewriteResult {
+    if token_lit.is_semantic_float() {
+        return rewrite_float_lit(context, token_lit, span, shape);
+    }
+
     let symbol = token_lit.symbol.as_str();
 
     if let Some(symbol_stripped) = symbol.strip_prefix("0x") {
@@ -1349,6 +1378,72 @@ fn rewrite_int_lit(
     .max_width_error(shape.width, span)
 }
 
+fn rewrite_float_lit(
+    context: &RewriteContext<'_>,
+    token_lit: token::Lit,
+    span: Span,
+    shape: Shape,
+) -> RewriteResult {
+    if matches!(
+        context.config.float_literal_trailing_zero(),
+        FloatLiteralTrailingZero::Preserve
+    ) {
+        return wrap_str(
+            context.snippet(span).to_owned(),
+            context.config.max_width(),
+            shape,
+        )
+        .max_width_error(shape.width, span);
+    }
+
+    let symbol = token_lit.symbol.as_str();
+    let suffix = token_lit.suffix.as_ref().map(|s| s.as_str());
+
+    let float_parts = parse_float_symbol(symbol).unwrap();
+    let FloatSymbolParts {
+        integer_part,
+        fractional_part,
+        exponent,
+    } = float_parts;
+
+    let has_postfix = exponent.is_some() || suffix.is_some();
+    let fractional_part_nonzero = !float_parts.is_fractional_part_zero();
+
+    let (include_period, include_fractional_part) =
+        match context.config.float_literal_trailing_zero() {
+            FloatLiteralTrailingZero::Preserve => unreachable!("handled above"),
+            FloatLiteralTrailingZero::Always => (true, true),
+            FloatLiteralTrailingZero::IfNoPostfix => (
+                fractional_part_nonzero || !has_postfix,
+                fractional_part_nonzero || !has_postfix,
+            ),
+            FloatLiteralTrailingZero::Never => (
+                fractional_part_nonzero || !has_postfix,
+                fractional_part_nonzero,
+            ),
+        };
+
+    let period = if include_period { "." } else { "" };
+    let fractional_part = if include_fractional_part {
+        fractional_part.unwrap_or("0")
+    } else {
+        ""
+    };
+    wrap_str(
+        format!(
+            "{}{}{}{}{}",
+            integer_part,
+            period,
+            fractional_part,
+            exponent.unwrap_or(""),
+            suffix.unwrap_or(""),
+        ),
+        context.config.max_width(),
+        shape,
+    )
+    .max_width_error(shape.width, span)
+}
+
 fn choose_separator_tactic(context: &RewriteContext<'_>, span: Span) -> Option<SeparatorTactic> {
     if context.inside_macro() {
         if span_ends_with_comma(context, span) {
@@ -2274,9 +2369,45 @@ pub(crate) fn is_method_call(expr: &ast::Expr) -> bool {
     }
 }
 
+/// Indicates the parts of a float literal specified as a string.
+struct FloatSymbolParts<'a> {
+    /// The integer part, e.g. `123` in `123.456e789`.
+    /// Always non-empty, because in Rust `.1` is not a valid floating-point literal:
+    /// <https://doc.rust-lang.org/reference/tokens.html#floating-point-literals>
+    integer_part: &'a str,
+    /// The fractional part excluding the decimal point, e.g. `456` in `123.456e789`.
+    fractional_part: Option<&'a str>,
+    /// The exponent part including the `e` or `E`, e.g. `e789` in `123.456e789`.
+    exponent: Option<&'a str>,
+}
+
+impl FloatSymbolParts<'_> {
+    fn is_fractional_part_zero(&self) -> bool {
+        let zero_literal_regex = static_regex!(r"^[0_]+$");
+        self.fractional_part
+            .is_none_or(|s| zero_literal_regex.is_match(s))
+    }
+}
+
+/// Parses a float literal. The `symbol` must be a valid floating point literal without a type
+/// suffix. Otherwise the function may panic or return wrong result.
+fn parse_float_symbol(symbol: &str) -> Result<FloatSymbolParts<'_>, &'static str> {
+    // This regex may accept invalid float literals (such as `1`, `_` or `2.e3`). That's ok.
+    // We only use it to parse literals whose validity has already been established.
+    let float_literal_regex = static_regex!(r"^([0-9_]+)(?:\.([0-9_]+)?)?([eE][+-]?[0-9_]+)?$");
+    let caps = float_literal_regex
+        .captures(symbol)
+        .ok_or("invalid float literal")?;
+    Ok(FloatSymbolParts {
+        integer_part: caps.get(1).ok_or("missing integer part")?.as_str(),
+        fractional_part: caps.get(2).map(|m| m.as_str()),
+        exponent: caps.get(3).map(|m| m.as_str()),
+    })
+}
+
 #[cfg(test)]
 mod test {
-    use super::last_line_offsetted;
+    use super::*;
 
     #[test]
     fn test_last_line_offsetted() {
@@ -2298,4 +2429,93 @@ mod test {
         let lines = "one\n two      three";
         assert_eq!(last_line_offsetted(2, lines), false);
     }
+
+    #[test]
+    fn test_parse_float_symbol() {
+        let parts = parse_float_symbol("123.456e789").unwrap();
+        assert_eq!(parts.integer_part, "123");
+        assert_eq!(parts.fractional_part, Some("456"));
+        assert_eq!(parts.exponent, Some("e789"));
+
+        let parts = parse_float_symbol("123.456e+789").unwrap();
+        assert_eq!(parts.integer_part, "123");
+        assert_eq!(parts.fractional_part, Some("456"));
+        assert_eq!(parts.exponent, Some("e+789"));
+
+        let parts = parse_float_symbol("123.456e-789").unwrap();
+        assert_eq!(parts.integer_part, "123");
+        assert_eq!(parts.fractional_part, Some("456"));
+        assert_eq!(parts.exponent, Some("e-789"));
+
+        let parts = parse_float_symbol("123e789").unwrap();
+        assert_eq!(parts.integer_part, "123");
+        assert_eq!(parts.fractional_part, None);
+        assert_eq!(parts.exponent, Some("e789"));
+
+        let parts = parse_float_symbol("123E789").unwrap();
+        assert_eq!(parts.integer_part, "123");
+        assert_eq!(parts.fractional_part, None);
+        assert_eq!(parts.exponent, Some("E789"));
+
+        let parts = parse_float_symbol("123.").unwrap();
+        assert_eq!(parts.integer_part, "123");
+        assert_eq!(parts.fractional_part, None);
+        assert_eq!(parts.exponent, None);
+    }
+
+    #[test]
+    fn test_parse_float_symbol_with_underscores() {
+        let parts = parse_float_symbol("_123._456e_789").unwrap();
+        assert_eq!(parts.integer_part, "_123");
+        assert_eq!(parts.fractional_part, Some("_456"));
+        assert_eq!(parts.exponent, Some("e_789"));
+
+        let parts = parse_float_symbol("123_.456_e789_").unwrap();
+        assert_eq!(parts.integer_part, "123_");
+        assert_eq!(parts.fractional_part, Some("456_"));
+        assert_eq!(parts.exponent, Some("e789_"));
+
+        let parts = parse_float_symbol("1_23.4_56e7_89").unwrap();
+        assert_eq!(parts.integer_part, "1_23");
+        assert_eq!(parts.fractional_part, Some("4_56"));
+        assert_eq!(parts.exponent, Some("e7_89"));
+
+        let parts = parse_float_symbol("_1_23_._4_56_e_7_89_").unwrap();
+        assert_eq!(parts.integer_part, "_1_23_");
+        assert_eq!(parts.fractional_part, Some("_4_56_"));
+        assert_eq!(parts.exponent, Some("e_7_89_"));
+    }
+
+    #[test]
+    fn test_float_lit_ends_in_dot() {
+        type TZ = FloatLiteralTrailingZero;
+
+        assert!(float_lit_ends_in_dot("1.", None, TZ::Preserve));
+        assert!(!float_lit_ends_in_dot("1.0", None, TZ::Preserve));
+        assert!(!float_lit_ends_in_dot("1.e2", None, TZ::Preserve));
+        assert!(!float_lit_ends_in_dot("1.0e2", None, TZ::Preserve));
+        assert!(!float_lit_ends_in_dot("1.", Some("f32"), TZ::Preserve));
+        assert!(!float_lit_ends_in_dot("1.0", Some("f32"), TZ::Preserve));
+
+        assert!(!float_lit_ends_in_dot("1.", None, TZ::Always));
+        assert!(!float_lit_ends_in_dot("1.0", None, TZ::Always));
+        assert!(!float_lit_ends_in_dot("1.e2", None, TZ::Always));
+        assert!(!float_lit_ends_in_dot("1.0e2", None, TZ::Always));
+        assert!(!float_lit_ends_in_dot("1.", Some("f32"), TZ::Always));
+        assert!(!float_lit_ends_in_dot("1.0", Some("f32"), TZ::Always));
+
+        assert!(!float_lit_ends_in_dot("1.", None, TZ::IfNoPostfix));
+        assert!(!float_lit_ends_in_dot("1.0", None, TZ::IfNoPostfix));
+        assert!(!float_lit_ends_in_dot("1.e2", None, TZ::IfNoPostfix));
+        assert!(!float_lit_ends_in_dot("1.0e2", None, TZ::IfNoPostfix));
+        assert!(!float_lit_ends_in_dot("1.", Some("f32"), TZ::IfNoPostfix));
+        assert!(!float_lit_ends_in_dot("1.0", Some("f32"), TZ::IfNoPostfix));
+
+        assert!(float_lit_ends_in_dot("1.", None, TZ::Never));
+        assert!(float_lit_ends_in_dot("1.0", None, TZ::Never));
+        assert!(!float_lit_ends_in_dot("1.e2", None, TZ::Never));
+        assert!(!float_lit_ends_in_dot("1.0e2", None, TZ::Never));
+        assert!(!float_lit_ends_in_dot("1.", Some("f32"), TZ::Never));
+        assert!(!float_lit_ends_in_dot("1.0", Some("f32"), TZ::Never));
+    }
 }


Test patch:
diff --git a/tests/source/configs/float_literal_trailing_zero/always.rs b/tests/source/configs/float_literal_trailing_zero/always.rs
new file mode 100644
index 00000000..47b04431
--- /dev/null
+++ b/tests/source/configs/float_literal_trailing_zero/always.rs
@@ -0,0 +1,45 @@
+// rustfmt-float_literal_trailing_zero: Always
+
+fn float_literals() {
+    let a = 0.;
+    let b = 0.0;
+    let c = 100.;
+    let d = 100.0;
+    let e = 5e3;
+    let f = 5.0e3;
+    let g = 5e+3;
+    let h = 5.0e+3;
+    let i = 5e-3;
+    let j = 5.0e-3;
+    let k = 5E3;
+    let l = 5.0E3;
+    let m = 7f32;
+    let n = 7.0f32;
+    let o = 9e3f32;
+    let p = 9.0e3f32;
+    let q = 1000.00;
+    let r = 1_000_.;
+    let s = 1_000_.000_000;
+}
+
+fn range_bounds() {
+    if (1.0..2.0).contains(&1.0) {}
+    if (1.1..2.2).contains(&1.1) {}
+    if (1.0e1..2.0e1).contains(&1.0e1) {}
+    let _binop_range = 3.0 / 2.0..4.0;
+}
+
+fn method_calls() {
+    let x = 1.0.neg();
+    let y = 2.3.neg();
+    let z = (4.).neg();
+    let u = 5.0f32.neg();
+    let v = -6.0.neg();
+}
+
+fn line_wrapping() {
+    let array = [
+        1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18.,
+    ];
+    println!("This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}", 10e3);
+}
diff --git a/tests/source/configs/float_literal_trailing_zero/if-no-postfix.rs b/tests/source/configs/float_literal_trailing_zero/if-no-postfix.rs
new file mode 100644
index 00000000..45e0b87b
--- /dev/null
+++ b/tests/source/configs/float_literal_trailing_zero/if-no-postfix.rs
@@ -0,0 +1,48 @@
+// rustfmt-float_literal_trailing_zero: IfNoPostfix
+
+fn float_literals() {
+    let a = 0.;
+    let b = 0.0;
+    let c = 100.;
+    let d = 100.0;
+    let e = 5e3;
+    let f = 5.0e3;
+    let g = 5e+3;
+    let h = 5.0e+3;
+    let i = 5e-3;
+    let j = 5.0e-3;
+    let k = 5E3;
+    let l = 5.0E3;
+    let m = 7f32;
+    let n = 7.0f32;
+    let o = 9e3f32;
+    let p = 9.0e3f32;
+    let q = 1000.00;
+    let r = 1_000_.;
+    let s = 1_000_.000_000;
+}
+
+fn range_bounds() {
+    if (1.0..2.0).contains(&1.0) {}
+    if (1.1..2.2).contains(&1.1) {}
+    if (1.0e1..2.0e1).contains(&1.0e1) {}
+    let _binop_range = 3.0 / 2.0..4.0;
+}
+
+fn method_calls() {
+    let x = 1.0.neg();
+    let y = 2.3.neg();
+    let z = (4.).neg();
+    let u = 5.0f32.neg();
+    let v = -6.0.neg();
+}
+
+fn line_wrapping() {
+    let array = [
+        1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11., 12., 13., 14., 15., 16., 17., 18.,
+    ];
+    println!(
+        "This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}",
+        10.0e3
+    );
+}
diff --git a/tests/source/configs/float_literal_trailing_zero/never.rs b/tests/source/configs/float_literal_trailing_zero/never.rs
new file mode 100644
index 00000000..2fe5fe2f
--- /dev/null
+++ b/tests/source/configs/float_literal_trailing_zero/never.rs
@@ -0,0 +1,49 @@
+// rustfmt-float_literal_trailing_zero: Never
+
+fn float_literals() {
+    let a = 0.;
+    let b = 0.0;
+    let c = 100.;
+    let d = 100.0;
+    let e = 5e3;
+    let f = 5.0e3;
+    let g = 5e+3;
+    let h = 5.0e+3;
+    let i = 5e-3;
+    let j = 5.0e-3;
+    let k = 5E3;
+    let l = 5.0E3;
+    let m = 7f32;
+    let n = 7.0f32;
+    let o = 9e3f32;
+    let p = 9.0e3f32;
+    let q = 1000.00;
+    let r = 1_000_.;
+    let s = 1_000_.000_000;
+}
+
+fn range_bounds() {
+    if (1.0..2.0).contains(&1.0) {}
+    if (1.1..2.2).contains(&1.1) {}
+    if (1.0e1..2.0e1).contains(&1.0e1) {}
+    let _binop_range = 3.0 / 2.0..4.0;
+}
+
+fn method_calls() {
+    let x = 1.0.neg();
+    let y = 2.3.neg();
+    let z = (4.).neg();
+    let u = 5.0f32.neg();
+    let v = -6.0.neg();
+}
+
+fn line_wrapping() {
+    let array = [
+        1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
+        17.0, 18.0,
+    ];
+    println!(
+        "This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}",
+        10.0e3
+    );
+}
diff --git a/tests/target/configs/float_literal_trailing_zero/always.rs b/tests/target/configs/float_literal_trailing_zero/always.rs
new file mode 100644
index 00000000..e6d643ad
--- /dev/null
+++ b/tests/target/configs/float_literal_trailing_zero/always.rs
@@ -0,0 +1,49 @@
+// rustfmt-float_literal_trailing_zero: Always
+
+fn float_literals() {
+    let a = 0.0;
+    let b = 0.0;
+    let c = 100.0;
+    let d = 100.0;
+    let e = 5.0e3;
+    let f = 5.0e3;
+    let g = 5.0e+3;
+    let h = 5.0e+3;
+    let i = 5.0e-3;
+    let j = 5.0e-3;
+    let k = 5.0E3;
+    let l = 5.0E3;
+    let m = 7.0f32;
+    let n = 7.0f32;
+    let o = 9.0e3f32;
+    let p = 9.0e3f32;
+    let q = 1000.00;
+    let r = 1_000_.0;
+    let s = 1_000_.000_000;
+}
+
+fn range_bounds() {
+    if (1.0..2.0).contains(&1.0) {}
+    if (1.1..2.2).contains(&1.1) {}
+    if (1.0e1..2.0e1).contains(&1.0e1) {}
+    let _binop_range = 3.0 / 2.0..4.0;
+}
+
+fn method_calls() {
+    let x = 1.0.neg();
+    let y = 2.3.neg();
+    let z = (4.0).neg();
+    let u = 5.0f32.neg();
+    let v = -6.0.neg();
+}
+
+fn line_wrapping() {
+    let array = [
+        1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
+        17.0, 18.0,
+    ];
+    println!(
+        "This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}",
+        10.0e3
+    );
+}
diff --git a/tests/target/configs/float_literal_trailing_zero/if-no-postfix.rs b/tests/target/configs/float_literal_trailing_zero/if-no-postfix.rs
new file mode 100644
index 00000000..d81f5c07
--- /dev/null
+++ b/tests/target/configs/float_literal_trailing_zero/if-no-postfix.rs
@@ -0,0 +1,46 @@
+// rustfmt-float_literal_trailing_zero: IfNoPostfix
+
+fn float_literals() {
+    let a = 0.0;
+    let b = 0.0;
+    let c = 100.0;
+    let d = 100.0;
+    let e = 5e3;
+    let f = 5e3;
+    let g = 5e+3;
+    let h = 5e+3;
+    let i = 5e-3;
+    let j = 5e-3;
+    let k = 5E3;
+    let l = 5E3;
+    let m = 7f32;
+    let n = 7f32;
+    let o = 9e3f32;
+    let p = 9e3f32;
+    let q = 1000.00;
+    let r = 1_000_.0;
+    let s = 1_000_.000_000;
+}
+
+fn range_bounds() {
+    if (1.0..2.0).contains(&1.0) {}
+    if (1.1..2.2).contains(&1.1) {}
+    if (1e1..2e1).contains(&1e1) {}
+    let _binop_range = 3.0 / 2.0..4.0;
+}
+
+fn method_calls() {
+    let x = 1.0.neg();
+    let y = 2.3.neg();
+    let z = (4.0).neg();
+    let u = 5f32.neg();
+    let v = -6.0.neg();
+}
+
+fn line_wrapping() {
+    let array = [
+        1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
+        17.0, 18.0,
+    ];
+    println!("This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}", 10e3);
+}
diff --git a/tests/target/configs/float_literal_trailing_zero/never.rs b/tests/target/configs/float_literal_trailing_zero/never.rs
new file mode 100644
index 00000000..6391890e
--- /dev/null
+++ b/tests/target/configs/float_literal_trailing_zero/never.rs
@@ -0,0 +1,45 @@
+// rustfmt-float_literal_trailing_zero: Never
+
+fn float_literals() {
+    let a = 0.;
+    let b = 0.;
+    let c = 100.;
+    let d = 100.;
+    let e = 5e3;
+    let f = 5e3;
+    let g = 5e+3;
+    let h = 5e+3;
+    let i = 5e-3;
+    let j = 5e-3;
+    let k = 5E3;
+    let l = 5E3;
+    let m = 7f32;
+    let n = 7f32;
+    let o = 9e3f32;
+    let p = 9e3f32;
+    let q = 1000.;
+    let r = 1_000_.;
+    let s = 1_000_.;
+}
+
+fn range_bounds() {
+    if (1. ..2.).contains(&1.) {}
+    if (1.1..2.2).contains(&1.1) {}
+    if (1e1..2e1).contains(&1e1) {}
+    let _binop_range = 3. / 2. ..4.;
+}
+
+fn method_calls() {
+    let x = (1.).neg();
+    let y = 2.3.neg();
+    let z = (4.).neg();
+    let u = 5f32.neg();
+    let v = -(6.).neg();
+}
+
+fn line_wrapping() {
+    let array = [
+        1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12., 13., 14., 15., 16., 17., 18.,
+    ];
+    println!("This is floaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaat {}", 10e3);
+}
diff --git a/tests/target/configs/float_literal_trailing_zero/preserve.rs b/tests/target/configs/float_literal_trailing_zero/preserve.rs
new file mode 100644
index 00000000..3f0f7647
--- /dev/null
+++ b/tests/target/configs/float_literal_trailing_zero/preserve.rs
@@ -0,0 +1,38 @@
+// rustfmt-float_literal_trailing_zero: Preserve
+
+fn float_literals() {
+    let a = 0.;
+    let b = 0.0;
+    let c = 100.;
+    let d = 100.0;
+    let e = 5e3;
+    let f = 5.0e3;
+    let g = 5e+3;
+    let h = 5.0e+3;
+    let i = 5e-3;
+    let j = 5.0e-3;
+    let k = 5E3;
+    let l = 5.0E3;
+    let m = 7f32;
+    let n = 7.0f32;
+    let o = 9e3f32;
+    let p = 9.0e3f32;
+    let q = 1000.00;
+    let r = 1_000_.;
+    let s = 1_000_.000_000;
+}
+
+fn range_bounds() {
+    if (1.0..2.0).contains(&1.0) {}
+    if (1.1..2.2).contains(&1.1) {}
+    if (1.0e1..2.0e1).contains(&1.0e1) {}
+    let _binop_range = 3.0 / 2.0..4.0;
+}
+
+fn method_calls() {
+    let x = 1.0.neg();
+    let y = 2.3.neg();
+    let z = (4.).neg();
+    let u = 5.0f32.neg();
+    let v = -6.0.neg();
+}