Skip to content

Ensure math operators are surrounded by whitespace in arbitrary values#20011

Merged
RobinMalfait merged 3 commits into
mainfrom
fix/issue-20010
May 5, 2026
Merged

Ensure math operators are surrounded by whitespace in arbitrary values#20011
RobinMalfait merged 3 commits into
mainfrom
fix/issue-20010

Conversation

@RobinMalfait
Copy link
Copy Markdown
Member

@RobinMalfait RobinMalfait commented May 5, 2026

This PR fixes an issue where some calc(…) expressions become invalid after we canonicalize them to a different syntax.

Let's say you start with: left-[calc(-1*(var(--my-var1)+var(--my-var2)))], then the produced AST for this candidate looks like this:

[
  {
    "kind": "functional",
    "root": "left",
    "modifier": null,
    "value": {
      "kind": "arbitrary",
      "dataType": null,
      "value": "calc(-1 * (var(--my-var1) + var(--my-var2)))"
    },
    "variants": [],
    "important": false,
    "raw": "left-[calc(-1*(var(--my-var1)+var(--my-var2)))]"
  }
]

Notice that the + in between the vars already contain spaces.

And the generated CSS looks like this:

.left-\[calc\(-1\*\(var\(--my-var1\)\+var\(--my-var2\)\)\)\] {
  left: calc(-1 * (var(--my-var1) + var(--my-var2)));
}

Again, the + has spaces aroudn it.

However, we canoncialize this syntax where we remove the calc(-1 * <value>) and move the - in front: -left-[(var(--my-var1)+var(--my-var2))], which should behave the same, but it did not. The parsed value for this looks like:

[
  {
    "kind": "functional",
    "root": "-left",
    "modifier": null,
    "value": {
      "kind": "arbitrary",
      "dataType": null,
      "value": "(var(--my-var1)+var(--my-var2))"
    },
    "variants": [],
    "important": false,
    "raw": "-left-[(var(--my-var1)+var(--my-var2))]"
  }
]

Notice that the + does not contain spaces around it. That's because we add them when we parse arbitrary values and when we are in a calc(…) expression. But the calc(<value> * -1) is added later, so at this point, no spaces are added yet.
This also means that the generated CSS currently looks like:

.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
  left: calc((var(--my-var1)+var(--my-var2)) * -1);
}

Which is invalid.

To solve this, we will make sure to add the whitespace around operators when we re-insert the calc(<value> * -1). With this fix, the CSS looks like this:

.-left-\[\(var\(--my-var1\)\+var\(--my-var2\)\)\] {
  left: calc((var(--my-var1) + var(--my-var2)) * -1);
}

Which is correct again.


There are a few other issues that need a bit more work, but are not required for this fix.

  1. Can we get rid of the additional ( and ) parens? E.g.:
    - left-[calc(-1*(var(--my-var1)+var(--my-var2)))]
    - -left-[(var(--my-var1)+var(--my-var2))]
    + -left-[var(--my-var1)+var(--my-var2)]
    Right now this means that we should use calc((<value>) * -1) instead of
    calc(<value> * -1) since <value> can be an expression on its own. This
    could lead to unwanted behavior, but this will be a follow up PR if it's
    worth it.
  2. Why did we even allow this canoncialization from A to B if it's not the same result?

This last question is a bit more scary, but it has to do with how we compare results. We create a signature where we normalize a bunch of values to make sure that we can compare them. As a silly example calc(var(--a) + var(--b)) and calc(var(--b) + var(--a)) will result in the same value, therefor should have the same signature and should be swappable.
But what's happening is that the signature of left-[calc(-1*(var(--my-var1)+var(--my-var2)))] and -left-[(var(--my-var1)+var(--my-var2))] result in:

/* Signature of: left-[calc(-1*(var(--my-var1)+var(--my-var2)))] */
.x {
  left: calc((var(--my-var1)+var(--my-var2))*-1);
}

/* Signature of: -left-[(var(--my-var1)+var(--my-var2))] */
.x {
  left: calc((var(--my-var1)+var(--my-var2))*-1);
}

This gets rid of whitespace to store less data, but this is obviously wrong now, so we need to improve the signatures around this. That said, that will be a follow up PR as well because this requires much more testing.
But this PR at least fixes the #20010 issue because we will properly insert the whitespace around the math operators.

Fixes: #20010

Test plan

  1. Added a regression test to make sure that the new canonicalized syntax results in the correct CSS. Before the fix, the test would fail: image
  2. Existing tests still pass

When we use `-foo-[…]`, where the contents of `…` happens to contain a
binary expression, then we have to make sure that the operators there
receive whitespace around them.

We already handle this whitespace when we parse arbitrary values.
However, we only do that when we're dealing with a math-like function
such as `calc`. But when you just have `-inset-[(var(--a)+var(--b))]`,
then we see `(var(--a)+var(--b))`, not `calc(var(--a)+var(--b))`.
You could assume that that's the case, but then we have to deal with
edge cases where it could later be used in a spot that is not math-like
@RobinMalfait RobinMalfait requested a review from a team as a code owner May 5, 2026 14:59
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Confidence Score: 5/5

Safe to merge; the fix is narrow and well-tested, with no regressions introduced.

The change is a single-line call-site fix backed by an existing, well-exercised utility (addWhitespaceAroundMathOperators). The regression test correctly captures the before/after CSS. The utility already handles edge cases (scientific notation, leading unary operators, consecutive whitespace), and the * -1 suffix in calc(${value} * -1) is handled safely because - preceded by * is recognized as a unary sign and no extra space is inserted.

No files require special attention.

Reviews (2): Last reviewed commit: "update changelog" | Re-trigger Greptile

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f708192d-d939-4788-aaf6-bf9b93948d5f

📥 Commits

Reviewing files that changed from the base of the PR and between d6ab294 and 2ff732a.

📒 Files selected for processing (1)
  • CHANGELOG.md

Walkthrough

The tests were updated to expect a new negative arbitrary-value case using CSS variables (-left-[(var(--my-var1)+var(--my-var2))]) and the corresponding emitted CSS selector and left: calc(...) formatting. The implementation now imports addWhitespaceAroundMathOperators and uses it when emitting negated calc(${value} * -1) for negative functional utilities, producing whitespace around math operators in the generated calc(...) expressions.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: ensuring math operators in arbitrary values have whitespace surrounding them.
Description check ✅ Passed The description thoroughly explains the bug, root cause, the fix implementation, and provides test plan details.
Linked Issues check ✅ Passed The PR directly addresses issue #20010 by adding whitespace around operators when negating calc expressions, matching the stated objective.
Out of Scope Changes check ✅ Passed All changes focus on fixing the whitespace issue in calc expressions; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@RobinMalfait RobinMalfait merged commit 8c77989 into main May 5, 2026
7 checks passed
@RobinMalfait RobinMalfait deleted the fix/issue-20010 branch May 5, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Canonicalization of calc classes that multiply by -1 break by removing whitespace.

1 participant