From 9c64b1296307cfc39972a20768c7c97b287469c9 Mon Sep 17 00:00:00 2001 From: Burlin Date: Fri, 16 Aug 2024 09:24:08 +0800 Subject: [PATCH] fix(linter): improve no-zero-fractions rule for member expressions and scientific notation (#4793) --- .../src/rules/unicorn/no_zero_fractions.rs | 73 +++++++++++++++---- .../src/snapshots/no_zero_fractions.snap | 6 +- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs b/crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs index da07fe5bddb82..5ceb10aabbd95 100644 --- a/crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs +++ b/crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs @@ -66,12 +66,50 @@ impl Rule for NoZeroFractions { } else { zero_fraction(number_literal.span, &fmt) }, - |fixer| fixer.replace(number_literal.span, fmt), + |fixer| { + let mut fixed = fmt.clone(); + let is_decimal_integer = fmt.parse::().is_ok(); + let is_member_expression = + ctx.nodes().parent_node(node.id()).map_or(false, |parent_node| { + matches!(parent_node.kind(), AstKind::MemberExpression(_)) + }); + + if is_member_expression && is_decimal_integer { + fixed = format!("({fixed})"); + // TODO: checks the type and value of tokenBefore: + // If tokenBefore is a Punctuator (e.g., a symbol like ;, ], or )), it determines whether a semicolon is necessary based on the context (e.g., the type of the last block node). + // If the token type is in tokenTypesNeedsSemicolon, it returns true (semicolon needed). + // Special cases like Template strings, ObjectExpression blocks, and certain Identifier cases are handled explicitly. + // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/77f32e5a6b2df542cf50dfbd371054f2cd8ce2d6/rules/no-zero-fractions.js#L56 + } + + // Handle special cases where a space is needed after certain keywords + // to prevent the number from being interpreted as a property access + let start = number_literal.span.start.saturating_sub(6); + let end = number_literal.span.start; + let token = ctx.source_range(oxc_span::Span::new(start, end)).to_string(); + if token.ends_with("return") + || token.ends_with("throw") + || token.ends_with("typeof") + || token.ends_with("void") + { + fixed = format!(" {fixed}"); + } + + fixer.replace(number_literal.span, fixed) + }, ); } } fn format_raw(raw: &str) -> Option<(String, bool)> { + // Check if the string contains 'e' or 'E' (scientific notation) + if let Some((base, exp)) = raw.split_once(['e', 'E']) { + // Process the base part + let (formatted_base, has_fraction) = format_raw(base)?; + // Recombine the scientific notation + return Some((format!("{formatted_base}e{exp}"), has_fraction)); + } let (before, after_and_dot) = raw.split_once('.')?; let mut after_parts = after_and_dot.splitn(2, |c: char| !c.is_ascii_digit() && c != '_'); let dot_and_fractions = after_parts.next()?; @@ -146,22 +184,29 @@ fn test() { (r"const foo = 1.", r"const foo = 1"), (r"const foo = +1.", r"const foo = +1"), (r"const foo = -1.", r"const foo = -1"), - // maybe todo - // In the following tests, the comments did not pass the fixer. - - // (r"const foo = 1.e10", r"const foo = 1e10"), - // (r"const foo = +1.e-10", r"const foo = +1e-10"), - // (r"const foo = -1.e+10", r"const foo = -1e+10"), + (r"const foo = 1.e10", r"const foo = 1e10"), + (r"const foo = +1.e-10", r"const foo = +1e-10"), + (r"const foo = -1.e+10", r"const foo = -1e+10"), (r"const foo = (1.).toString()", r"const foo = (1).toString()"), - // (r"1.00.toFixed(2)", r"(1).toFixed(2)"), - // (r"1.00 .toFixed(2)", r"(1) .toFixed(2)"), + (r"1.00.toFixed(2)", r"(1).toFixed(2)"), + (r"1.010.toFixed(2)", r"1.01.toFixed(2)"), + (r"1.00 .toFixed(2)", r"(1) .toFixed(2)"), (r"(1.00).toFixed(2)", r"(1).toFixed(2)"), - // (r"1.00?.toFixed(2)", r"(1)?.toFixed(2)"), + (r"1.00?.toFixed(2)", r"(1)?.toFixed(2)"), (r"a = .0;", r"a = 0;"), - // (r"a = .0.toString()", r"a = (0).toString()"), - // (r"function foo(){return.0}", r"function foo(){return 0}"), - // (r"function foo(){return.0.toString()}", r"function foo(){return (0).toString()}"), - // (r"function foo(){return.0+.1}", r"function foo(){return 0+.1}"), + (r"a = .0.toString()", r"a = (0).toString()"), + (r"function foo(){return.0}", r"function foo(){return 0}"), + (r"function foo(){return.0.toString()}", r"function foo(){return (0).toString()}"), + (r"function foo(){return.0+.1}", r"function foo(){return 0+.1}"), + (r"typeof.0", r"typeof 0"), + (r"function foo(){typeof.0.toString()}", r"function foo(){typeof (0).toString()}"), + (r"typeof.0+.1", r"typeof 0+.1"), + (r"function foo(){throw.0;}", r"function foo(){throw 0;}"), + (r"function foo(){typeof.0.toString()}", r"function foo(){typeof (0).toString()}"), + (r"function foo(){throw.0+.1;}", r"function foo(){throw 0+.1;}"), + (r"void.0", r"void 0"), + (r"function foo(){void.0.toString()}", r"function foo(){void (0).toString()}"), + (r"function foo(){void.0+.1;}", r"function foo(){void 0+.1;}"), ]; Tester::new(NoZeroFractions::NAME, pass, fail).expect_fix(fix).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/no_zero_fractions.snap b/crates/oxc_linter/src/snapshots/no_zero_fractions.snap index f15723687116b..9dbb67fb685f1 100644 --- a/crates/oxc_linter/src/snapshots/no_zero_fractions.snap +++ b/crates/oxc_linter/src/snapshots/no_zero_fractions.snap @@ -83,21 +83,21 @@ source: crates/oxc_linter/src/tester.rs 1 │ const foo = 1.e10 · ───── ╰──── - help: Replace the number literal with `110` + help: Replace the number literal with `1e10` ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. ╭─[no_zero_fractions.tsx:1:14] 1 │ const foo = +1.e-10 · ────── ╰──── - help: Replace the number literal with `1-10` + help: Replace the number literal with `1e-10` ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. ╭─[no_zero_fractions.tsx:1:14] 1 │ const foo = -1.e+10 · ────── ╰──── - help: Replace the number literal with `1+10` + help: Replace the number literal with `1e+10` ⚠ eslint-plugin-unicorn(no-zero-fractions): Don't use a dangling dot in the number. ╭─[no_zero_fractions.tsx:1:14]