fix: parse scientific notation in numbers - #4490
Conversation
The dimension rule matched a number without an exponent part, so the `e` in `1e3px` was taken as the unit and the rest as a separate value. Valid CSS was silently compiled to a different value — `scale(1e-2)` became `scale(-1e)` and `padding: .5e-2px` became `-1.5e` — or to output that is not CSS at all, and `(1e3px + 1px)` failed to parse. The exponent requires at least one digit after `e`, so units that begin with `e` keep parsing as units: `1em` and `2ex` are unchanged, while `1e2em` is now 100em.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe dimension parser now accepts scientific-notation numbers such as ChangesScientific-Notation Dimension Parsing
Estimated code review effort: 2 (Simple) | ~10 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
|
||
| const value = parserInput.$re(/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/i); | ||
| const value = parserInput.$re(/^([+-]?\d*\.?\d+(?:e[+-]?\d+)?)(%|[a-z_]+)?/i); | ||
| if (value) { |
There was a problem hiding this comment.
There was a problem hiding this comment.
The Infinitypx output is real, but it isn't reachable by the example given, and it isn't introduced here — it's a pre-existing overflow in Dimension.genCSS.
The stated repro doesn't reproduce. A declaration value isn't numerically re-serialized, so 1e309px passes through verbatim — on this branch and on master alike:
a { width: 1e309px; } /* both: width: 1e309px */Infinity appears only once something forces the Dimension back through genCSS: arithmetic, unary minus, a variable reference, or unit().
And it isn't about exponents. genCSS does String(this.fround(context, this.value)) over a value from parseFloat, so any literal past Number.MAX_VALUE (1.797e308) overflows. The defect reproduces on master with no exponent syntax anywhere — just a digit run:
input, on master |
output |
|---|---|
-1 followed by 308 zeros, then px |
-1e+308px |
-1 followed by 309 zeros, then px |
-Infinitypx |
One extra zero flips it, and nothing from this PR is on that path.
The 308-zero row is worth a second look, because it's this PR's own argument. master emits -1e+308px — notation master's parser cannot read back. Feeding that output in again:
a { width: -1e+308px + 0px; }
/* master: width: 307e; ← sign, mantissa and unit all gone */
/* branch: width: -1e+308px; */So the exponent gap was never only an input problem: less already produces exponent notation and then fails to round-trip it. That's the hole this change closes.
On fixing the overflow here — I'd rather not, and I don't think it belongs in this PR. The honest fix lives in Dimension.genCSS (reject or clamp a non-finite value), and it would change master's behaviour for the plain digit-run case above, which has nothing to do with parsing scientific notation. It's also a real decision rather than a mechanical one: throw "number too large" versus clamp to MAX_VALUE. Turning a one-line regex change into a numeric-output change seems like the wrong trade. Happy to file it separately and fix it there — just say which behaviour you'd want.
All seven test legs are green on 0f99a40.
There was a problem hiding this comment.
You're right on all counts. The overflow predates this PR, isn't introduced by the regex change, and the round-trip argument you've made is actually a stronger case for this fix — master already emits notation it can't re-parse.
The original concern doesn't hold up against the repro evidence. A separate issue/PR scoped to Dimension.genCSS is the right place to decide between throwing and clamping for non-finite values. That decision shouldn't block a correct parser fix.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
What: Teach the
dimensionparser rule about the exponent part of a CSS number, so1e3px,.5e-2pxand2e+2pxcompile to the value they mean.Why: CSS numbers may carry an exponent — css-syntax-3 §4.3.12 defines
<number-token>as[+-]? (\d+ | \d*\.\d+) ([eE][+-]?\d+)?, and a<dimension-token>is that number followed by an ident. The rule atpackages/less/lib/less/parser/parser.jsmatched the number without the exponent:/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/iso in
.5e-2pxit took.5as the number andeas the unit, leaving-2pxto be parsed as a separate term. The two then combine, and the result is neither an error nor the right value:padding: .5e-2px-1.5e0.005px0.005pxmargin: 2e+2px4e200px200pxpadding: 5e-1em4e0.5em0.5emopacity: 1e-10e0.10.1transform: scale(1e-2)scale(-1e)scale(0.01)scale(0.01)flex-basis: 1.5e2%1.5e 2%150%150%min-width: calc(1e3px + 1px)calc(1e 3px + 1px)calc(1000px + 1px)width: (1e3px + 1px)ParseError: Expected ')'1001pxThe last column is
el.style.widthafter assigning each value, read back from Chromium 133 and WebKit 18.2.A bare literal declaration such as
width: 1e3px;happens to survive today because it takes the verbatim fast path for simple values and is never parsed as a dimension. That protection disappears as soon as the value meets any Less feature, which is what makes this easy to miss:Exponents mostly reach Less from generated or minified CSS rather than from hand-written source, so the failure tends to show up as a rule the browser drops, or as a value that is quietly wrong, well away from the code that produced it.
The fix appends an optional
(?:e[+-]?\d+)?to the number group. The exponent needs at least one digit aftere, so a unit that merely begins withestill parses as a unit:1emand2exare unchanged.1e2emnow means 100em, which it did not before. No unit contains a digit, and[a-z_]+never matched one, so nothing that used to parse as a unit stops doing so.Tests: new fixture
packages/test-data/tests-unit/numbers-exponent, covering literal declarations, units that start withe, arithmetic in parens, variables, a mixin argument, a guard and an@mediaquery. Every expected value in the.csswas checked against the two browsers above. Revertingparser.jsand keeping the fixture makesgrunt test:nodeexit 6 withERROR: Expected ')'.pnpm testpasses on this branch:All Passed 211 run, including the headless-Chrome browser suite.pnpm --filter less typecheckis clean.pnpm lintreports one pre-existing parse error inbenchmark/benchmark-runner.jsthat is present onmasterand is unrelated to this change (#4453 appears to cover it).Checklist:
Summary by CodeRabbit
New Features
1e3px.emandex.Bug Fixes
Tests