Skip to content

fix(lint): parse each style block once per lint instead of once per rule - #7593

Merged
miguel-heygen merged 1 commit into
mainfrom
fix/lint-parse-each-style-once
Nov 12, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix/lint-parse-each-style-once

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

What

The linter now parses each <style> block with postcss once per lint, and every rule shares that one tree. Before this change, ten rule passes each parsed the same block again.

Why

gsap.test.ts "scans a closed style block of many unclosed rules in linear time" lints 40 000 unclosed rules and checks a 2 s limit. On CI it took about 2 s and failed by chance, on main as well as on unrelated PRs. The cost came from ten parses and ten throwaway trees per block, which also made the garbage collector work harder. It was not slow regex scanning. Every lint of a large stylesheet paid the same extra cost.

Related work

None.

How

  • styleRoots.ts caches each block's parsed tree, or its parse error, in a WeakMap keyed by the block. This follows the per-block cache in scriptPositions.ts. The cache lasts as long as the lint context does.
  • The style parse sites in core.ts, fonts.ts, gsap.ts and textures.ts now read the shared tree. No rule modifies it, so sharing it is safe. A comment on styleRoot notes that rules must not modify it.
  • The timing tests are unchanged. They still guard against a slow scan with the same limits, and now have about 6x headroom.

Test plan

  • Unit tests added: styleRoots.test.ts. One lint calls postcss.parse once for a style block that several rules read; on main it is called 10 times, and that is how the test fails there. A malformed block is still reported once.
  • All lint package tests pass 3 runs in a row on Linux (1309 tests). tsc --noEmit passes for the lint package. oxfmt and oxlint are clean on the changed files.
  • One lint of 40 000 unclosed rules: about 400 ms before, about 60 ms after. 10 000 to 40 000 rules grew 6.8x before and 1.8x after.
  • fails without the fix: packages/lint/src/styleRoots.test.ts › "parses each style block once per lint, however many rules read it". On main it counts 10 postcss.parse calls for the one block instead of 1. Shown by running the test against main's rule files.
  • Comments follow CONTRIBUTING.md "Comments"
  • Local pre-commit lint, format, typecheck, fallow and commitlint were skipped because this checkout has no local install. The same checks ran on Linux as listed above, and CI runs them in full.

Size

This is a small, standalone lint fix. The flaky timing test fails on main and on any open PR, so it lands on its own rather than inside an unrelated studio change.

…rule

Ten rules each parsed the same <style> block with postcss, so a large
style block cost ten parses and ten trees for the garbage collector.
The 40 000-rule linear-time test ran close to its 2 s limit on CI and
failed there by chance. One shared parse per block brings that lint
from about 400 ms to about 60 ms.
@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 116 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head c00b7c4b

  • blocker (lane rule): this PR fixes a test that fails on main, so the body needs a fails without the fix: line naming a test. The Test plan describes styleRoots.test.ts failing on main, but not on that line. Add something like fails without the fix: packages/lint/src/styleRoots.test.ts › "parses each style block once per lint, however many rules read it" (it gets 10 postcss.parse calls on main).

I found no code blocker.

Scope

Read the full diff (6 files). Every postcss.parse(style.content) on a lint-context style block now goes through styleRoot. The parse sites still left are separate inputs: inline style="" attributes at core.ts:713 and project CSS files at project.ts:684,752. None of the rules that now share the tree modify it (I searched for remove, replaceWith, append, clone* and .value/.prop/.selector assignments). A parse error is cached, then thrown again to each caller, and every caller still catches it: the syntax rule reports it once, the others skip the block. The WeakMap is keyed by the block object, the same pattern as scriptPositions.ts. The spy test hooks the same postcss.parse the rules call, so it would really count 10 parses on main. CI, CodeQL, regression and Windows are green at this head.

— Genesis

@miguel-heygen miguel-heygen changed the title perf(lint): parse each style block once per lint instead of once per rule fix(lint): parse each style block once per lint instead of once per rule Nov 12, 2026

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 1 resolved, 0 open, 0 new · c00b7c4b → c00b7c4b · ✅ The body now has a fails without the fix: line naming styleRoots.test.ts. The code is unchanged.

— Genesis

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit 32cacab Nov 12, 2026
129 checks passed
@miguel-heygen
miguel-heygen deleted the fix/lint-parse-each-style-once branch November 12, 2026 06:42
@heygengenesis heygengenesis Bot mentioned this pull request Nov 12, 2026
4 of 6 tasks
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.

1 participant