Skip to content

Commit

Permalink
fix(linter): improve no-zero-fractions rule for member expressions an…
Browse files Browse the repository at this point in the history
…d scientific notation (#4793)
  • Loading branch information
cblh authored Aug 16, 2024
1 parent a0b1b86 commit 9c64b12
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 17 deletions.
73 changes: 59 additions & 14 deletions crates/oxc_linter/src/rules/unicorn/no_zero_fractions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<i64>().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()?;
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 3 additions & 3 deletions crates/oxc_linter/src/snapshots/no_zero_fractions.snap
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,21 @@ source: crates/oxc_linter/src/tester.rs
1const 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]
1const 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]
1const 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]
Expand Down

0 comments on commit 9c64b12

Please sign in to comment.