Skip to content

fix: parse scientific notation in numbers - #4490

Open
Jaybhade wants to merge 1 commit into
less:masterfrom
Jaybhade:fix/dimension-exponent
Open

fix: parse scientific notation in numbers#4490
Jaybhade wants to merge 1 commit into
less:masterfrom
Jaybhade:fix/dimension-exponent

Conversation

@Jaybhade

@Jaybhade Jaybhade commented Aug 9, 2026

Copy link
Copy Markdown

What: Teach the dimension parser rule about the exponent part of a CSS number, so 1e3px, .5e-2px and 2e+2px compile 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 at packages/less/lib/less/parser/parser.js matched the number without the exponent:

/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/i

so in .5e-2px it took .5 as the number and e as the unit, leaving -2px to be parsed as a separate term. The two then combine, and the result is neither an error nor the right value:

input 4.8.1 this PR Chrome / Safari
padding: .5e-2px -1.5e 0.005px 0.005px
margin: 2e+2px 4e 200px 200px
padding: 5e-1em 4e 0.5em 0.5em
opacity: 1e-1 0e 0.1 0.1
transform: 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 ')' 1001px

The last column is el.style.width after 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:

@size: 1e2px;
.a { width: @size; }        // 4.8.1: width: 1e 2px
.b when (1e2px > 50px) { }  // 4.8.1: ParseError: expected condition
@media (min-width: 1e3px) { } // 4.8.1: @media (min-width: 1e 3px)

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 after e, so a unit that merely begins with e still parses as a unit: 1em and 2ex are unchanged. 1e2em now 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 with e, arithmetic in parens, variables, a mixin argument, a guard and an @media query. Every expected value in the .css was checked against the two browsers above. Reverting parser.js and keeping the fixture makes grunt test:node exit 6 with ERROR: Expected ')'.

pnpm test passes on this branch: All Passed 211 run, including the headless-Chrome browser suite. pnpm --filter less typecheck is clean. pnpm lint reports one pre-existing parse error in benchmark/benchmark-runner.js that is present on master and is unrelated to this change (#4453 appears to cover it).

Checklist:

  • Documentation — N/A, no documented behaviour changes
  • Added/updated unit tests
  • Code complete

Summary by CodeRabbit

  • New Features

    • Added support for scientific-notation dimension values, such as 1e3px.
    • Preserved correct handling of units beginning with “e,” including em and ex.
  • Bug Fixes

    • Improved exponent-number parsing across calculations, variables, mixins, guarded rules, and media queries.
  • Tests

    • Added comprehensive coverage for exponent notation and related unit-handling scenarios.

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.
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e4279db4-901c-4d50-ad0a-2823a0f1b8c2

📥 Commits

Reviewing files that changed from the base of the PR and between c303718 and 0f99a40.

📒 Files selected for processing (3)
  • packages/less/lib/less/parser/parser.js
  • packages/test-data/tests-unit/numbers-exponent/numbers-exponent.css
  • packages/test-data/tests-unit/numbers-exponent/numbers-exponent.less

📝 Walkthrough

Walkthrough

The dimension parser now accepts scientific-notation numbers such as 1e3px. Test fixtures cover literals, e-prefixed units, arithmetic, variables, mixins, guards, and media queries.

Changes

Scientific-Notation Dimension Parsing

Layer / File(s) Summary
Exponent-aware dimension grammar
packages/less/lib/less/parser/parser.js
The dimension token grammar accepts optional signed exponents followed by digits.
Literal and expression coverage
packages/test-data/tests-unit/numbers-exponent/numbers-exponent.less, packages/test-data/tests-unit/numbers-exponent/numbers-exponent.css
Fixtures cover exponent-form literals, em and ex units, and arithmetic results.
Contextual evaluation coverage
packages/test-data/tests-unit/numbers-exponent/numbers-exponent.less, packages/test-data/tests-unit/numbers-exponent/numbers-exponent.css
Fixtures cover variables, mixin arguments, guarded rules, and media-query conditions.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: parsing scientific notation in numbers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR extends Less dimension parsing to recognize CSS scientific notation and adds automatically discovered fixtures covering declarations, arithmetic, variables, mixins, guards, and media queries.

  • Adds optional signed exponent parsing to the dimension rule.
  • Verifies exponent handling across unitless and unit-bearing values.
  • Preserves common units beginning with e, such as em and ex.

Confidence Score: 4/5

The PR should not merge until exponent overflow is prevented from producing invalid Infinity dimensions.

Ordinary scientific notation is parsed as intended, but sufficiently large accepted exponents overflow during Dimension construction and are emitted as invalid CSS.

Files Needing Attention: packages/less/lib/less/parser/parser.js

Reviews (1): Last reviewed commit: "fix: parse scientific notation in number..." | Re-trigger Greptile


const value = parserInput.$re(/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/i);
const value = parserInput.$re(/^([+-]?\d*\.?\d+(?:e[+-]?\d+)?)(%|[a-z_]+)?/i);
if (value) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Exponent overflow emits Infinity

When a dimension contains a sufficiently large exponent such as 1e309px, the expanded regex passes 1e309 to Dimension, where parseFloat produces Infinity; serialization then emits invalid CSS such as Infinitypx instead of safely handling the numeric token.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant