Skip to content

Refactor block parsing and improve whitespace handling - #278

Merged
bartveneman merged 1 commit into
mainfrom
claude/code-review-refactoring-dw1sip
Jul 29, 2026
Merged

Refactor block parsing and improve whitespace handling#278
bartveneman merged 1 commit into
mainfrom
claude/code-review-refactoring-dw1sip

Conversation

@bartveneman

Copy link
Copy Markdown
Member

Summary

This PR refactors the CSS parser to eliminate code duplication in block parsing logic and improves whitespace handling in at-rule prelude parsing. The changes consolidate three separate block-parsing implementations into a single reusable method and fix a regression where newlines in at-rule prelude tokens could cause incorrect AST structure.

Key Changes

  • Extract parse_block_children() method: Consolidated duplicated block parsing logic from parse_style_rule() and parse_atrule() into a single private method that handles declarations, nested rules, and at-rules with a declarations_only parameter to control parsing behavior.

  • Fix whitespace handling in at-rule prelude parsing: Introduced scan_matching_paren() helper method to consistently handle parenthesis matching across multiple at-rule prelude parsing contexts (media features, supports queries, layer functions, etc.), fixing a regression where newlines between tokens could desync the whitespace-skip helper from the tokenizer's newline handling.

  • Simplify declaration parsing logic: Refactored parse_declaration() to use clearer control flow by consolidating token type checks and reducing nested conditionals.

  • Improve selector parser: Consolidated create_node() and create_node_at() methods into a single create_node() with optional line/column parameters that default to the current token position, reducing code duplication.

  • Simplify walk/traverse functions: Refactored walk() and traverse() in walk.ts to delegate to their internal _walk() and _traverse() implementations, eliminating duplicated logic.

  • Remove unused code: Removed unused is_whitespace() export from char-types.ts and unused create_operator_node() from value-node-parser.ts.

  • Add regression tests: Added test cases for newline handling in @media queries and @supports conditions, and expanded has_declarations property tests to cover at-rules with direct declarations.

Implementation Details

The parse_block_children() method accepts an owner node and declarations_only flag, properly setting FLAG_HAS_DECLARATIONS on the owner when direct declaration children are found. This flag is now correctly set for both style rules and at-rules (including nested conditional at-rules under CSS Nesting).

The scan_matching_paren() helper returns a tuple of [content_end, close_end, matched] to provide consistent parenthesis matching across all at-rule prelude contexts, with sensible fallback values when EOF is encountered before a matching closing paren.

https://claude.ai/code/session_017XjfDK7or6VrnJ41vBhxrK

…s, dedupe parser internals

Two real bugs surfaced while reviewing the parser for duplication:

- Lexer.skip_whitespace_in_range() used char-types.ts's narrower
  is_whitespace() (space/tab only, no newlines), while tokenize.ts's own
  hot-path whitespace skip correctly includes newlines. Any at-rule
  prelude or nth-expression relying on skip_whitespace_in_range() to
  jump straight to the next real token (AtRulePreludeParser,
  ANplusBParser) could silently split a media/supports query in two
  when a newline fell between components, e.g. "@media screen\nand
  (min-width: 768px)" produced two MediaQuery nodes instead of one.
  Fixed by checking CHAR_WHITESPACE | CHAR_NEWLINE directly, matching
  the rest of the file; removed the now-dead char-types.ts is_whitespace.
- FLAG_HAS_DECLARATIONS was only ever set on STYLE_RULE nodes, even
  though the public Atrule type promises `has_declarations: boolean`.
  At-rules with direct declarations (@font-face, @page, or a nested
  @media under CSS Nesting) always reported has_declarations: false.

Also removed duplication that had built up across the parser:

- parse.ts: style rules and at-rule bodies each had their own ~35-line
  copy of the "declaration, nested rule, or nested at-rule" loop;
  extracted into parse_block_children() (which also carries the
  has_declarations fix to at-rules).
- parse-atrule-prelude.ts: the "scan to matching closing paren" loop was
  copy-pasted across 7 call sites (function conditions, media/supports
  features, @import's url()/layer()/supports(), @scope); extracted into
  scan_matching_paren().
- parse-selector.ts: skip_whitespace() had its own from-scratch copy of
  the whitespace/comment skip that Lexer.skip_whitespace_in_range()
  already implements (correctly) elsewhere; now delegates to it. Merged
  create_node/create_node_at into one method with defaulted line/column.
- walk.ts: walk()/traverse() each duplicated their recursive helper's
  body just to avoid one extra node-wrapper allocation for the root
  node; now delegate directly.
- value-node-parser.ts: dropped a one-line-only wrapper.

None of tokenize.ts's character-level hot loops (next_token_fast,
consume_number, consume_ident_or_function, etc.) were touched — those
are deliberately inlined for performance. Verified no regression with a
before/after timing comparison parsing bootstrap.css (100 iterations):
~57-58ms/parse on both sides.

All 1382 existing tests plus 4 new regression tests pass; typecheck,
lint, and knip are clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XjfDK7or6VrnJ41vBhxrK
@github-actions

Copy link
Copy Markdown
Contributor

🎉 Package Size Decrease

📦 Package 📏 Base Size 📏 Source Size 📈 Size Change
@projectwallace/css-parser 39.3 kB 39 kB -347 B

Copy link
Copy Markdown
Member Author

CI is green except Audit packages, which is unrelated to this PR: this branch doesn't touch package.json or pnpm-lock.yaml, and the two high findings are a transitive brace-expansion DoS advisory pulled in via tailwindcss > purgecss > glob > minimatch. It's already being tracked separately in #258, which drops tailwindcss as a devDependency. Not fixing it here to keep this PR scoped to the parser refactor.

All other checks (unit tests, types, lint, knip, dependency diff, pack) pass.


Generated by Claude Code

@bartveneman
bartveneman merged commit a9633da into main Jul 29, 2026
7 of 8 checks passed
@bartveneman
bartveneman deleted the claude/code-review-refactoring-dw1sip branch July 29, 2026 07:01
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.

2 participants