Skip to content

Preserve formatter fidelity, and get the test suite running again - #3

Open
n-rodriguez wants to merge 11 commits into
yippee-fun:mainfrom
n-rodriguez:fix/formatter-fidelity
Open

n-rodriguez wants to merge 11 commits into
yippee-fun:mainfrom
n-rodriguez:fix/formatter-fidelity

Conversation

@n-rodriguez

@n-rodriguez n-rodriguez commented Sep 1, 2026 •

Copy link
Copy Markdown

Fixes found while rewriting Phlex templates with Refract, where the output is eval'd — so a formatting slip becomes a behaviour change rather than a cosmetic one. Rebased on main, which meanwhile fixed string escaping, destructuring parentheses, empty call parentheses and super(); what remains here is what main does not cover yet, plus the repairs needed to run the suite at all.

Formatter

A modifier guarding several statements. inline records how an if/unless/while/until was written, and a rewrite invalidates it: replacing the single statement of foo if bar with several emitted a\nb if bar, where only b stays guarded and the rest runs unconditionally. Phlex expands an element into three statements, so span(class: "x") { plain content } if flag opened the tag and ran content even when flag was false. A single modifier? predicate — inline and exactly one statement — now drives the four visit blocks and inline_statements?, which otherwise dropped the shareable_constant_value magic comment of a constant in that body.

super { … } came back as super{ … }. Valid Ruby, but every other call with a block, super() { … } included, is emitted with the space.

Test suite and CI

The suite had not run since the top-level test DSL disappeared from Quickdraw, and CI has been red on main since 22 July 2025. Three independent reasons, each hiding the next:

  • bundle exec qt -t 1 — -t is not a Quickdraw option, so the runner aborted before loading a file. No flag is needed: the process count comes from DEFAULT_PROCESSES.
  • the top-level test DSL — at the top level the name resolves to Kernel#test, the file-test builtin. Tests now live in Quickdraw::Test subclasses, test/corpus.test.rb included, which only failed once CORPUS was set.
  • assert_equal_ruby does not exist — the API is assert_equivalent_ruby. The assert_equal beside it is the strict identity check this suite is built on, which subsumes any equivalence check.

Also: bundle install --without fails on the Bundler that Ruby head now carries, replaced with bundle config set --local without development; CI runs on every branch, so a fork gets its own run without waiting on approval; and the matrix covers the same Ruby versions as Phlex.

Round-trip cases for string, symbol and assoc-key escaping, block-parameter destructuring and nested multiple-assignment targets pin behaviour main now has.

Evidence

650 tests pass locally on Ruby 4.0.7, none failing. RuboCop reports a single offense, Style/RedundantRegexpEscape in formatter.rb:16, already present on main.

🤖 Generated with Claude Code

@n-rodriguez n-rodriguez changed the title Fix/formatter fidelity Preserve formatter fidelity, and get the test suite running again Sep 1, 2026
@n-rodriguez
n-rodriguez force-pushed the fix/formatter-fidelity branch from 9640f7b to 5eb14b6 Compare September 1, 2026 20:02
n-rodriguez and others added 10 commits October 1, 2026 17:15
Both were ways the formatter silently changed the meaning of the code it
re-emitted, found by rewriting Phlex templates where the output is
eval'd: a parenthesised MultiTargetNode flattened into the surrounding
target list, and string literals whose `\` and `#{` were reinterpreted
by the parser. main now fixes both; these cases pin the behaviour.

- test/refract.test.rb: string, symbol and assoc key escaping, block
  parameter destructuring, nested multiple-assignment targets

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The suite had not run since the top-level `test` DSL disappeared from
Quickdraw: at the top level the name resolves to `Kernel#test`, the
file-test builtin, so every file failed to load with `unknown command
'a'` before a single assertion was reached. CI has been red on main
since 22 July 2025 for this reason alone.

- test/refract.test.rb: wrap the file in a `Quickdraw::Test` subclass,
  which is how Quickdraw collects tests now
- test/refract.test.rb: drop the `assert_equal_ruby` call: the method
  does not exist — the API is `assert_equivalent_ruby` — and it raised
  NoMethodError in every test once they could load. The `assert_equal`
  on the next line is the strict identity check the suite is built on,
  which subsumes any equivalence check anyway
- test/corpus.test.rb: same wrapping; it only loads its test when
  CORPUS is set, so the failure surfaced only on a corpus run

644 tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A branch pushed to a fork produced no run of its own: the workflow only
triggered on pushes to main and on pull requests targeting main, and a
pull request from a fork runs against the upstream repository, where it
waits for a maintainer to approve it.

`**` rather than `*`, which does not match a slash and so would miss
every `fix/…` branch. Tags are left out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`-t` is not a Quickdraw option, so `bundle exec qt -t 1` aborted before
loading a single test file, on every entry of the matrix. Together with
the top-level `test` DSL this made two independent reasons for the suite
never to run, each hiding the other, and CI has been red on main since
22 July 2025.

Passing no flag lets the process count come from `DEFAULT_PROCESSES`,
which is 1 where forking is unavailable and the core count otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--without` was removed from Bundler: it relied on being remembered
across invocations, which Bundler no longer does. The Ruby head jobs
carry a version that has dropped it, so both failed before installing a
single gem — the last thing standing between this branch and a green
matrix, now that the suite itself runs.

`bundle config set --local without development` is the documented
replacement and has been available since Bundler 2.1, so it covers every
entry of the matrix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The matrix stopped at 3.4 plus head, while Phlex — the main consumer of
this gem — also covers 4.0 and both TruffleRuby variants. A formatter
divergence on a runtime Phlex tests and this gem does not would surface
downstream instead of here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three majors behind, and still targeting Node 20: every job logged
`actions/checkout@v4 ... being forced to run on Node.js 24`. Version read
from the action's latest release, v7.0.1, rather than assumed.

`ruby/setup-ruby` is already on its current major.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`inline` records how the node was written, and a rewrite invalidates it.
Replacing the single statement of `foo if bar` with several emitted
`a\nb if bar`, where only the last one stays guarded — the rest runs
unconditionally.

Phlex expands an element into three statements, so a component writing
`span(class: "x") { plain content } if flag` opened the tag and called
`content` even when `flag` was false, leaving the tag unclosed and
running a side effect that should not have happened. 41 failures across
an application's suite came from this, second only to the buffer being
hoisted out of a capture.

All four nodes that carry `inline` have a modifier form and had the same
flaw — `if`, `unless`, `while` and `until` — so the check lives in one
`modifier?` predicate rather than being repeated and forgotten again:
the first version of this fix covered `IfNode` alone, and `unless` was
found afterwards in the same application.

The tests build the nodes by hand: Ruby cannot parse a modifier with
more than one statement, so the shape only ever comes from a rewrite.

`inline_statements?`, which decides whether a statement sits on lines of
its own, read `inline` too: once the modifier is emitted as a block, a
shareable constant among its statements lost its magic comment. It now
asks `modifier?` as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ForwardingSuperNode` pushed its block straight after the keyword, so
`super { … }` came back as `super{ … }`. Ruby accepts it, so this is
fidelity rather than correctness, but every other call with a block is
emitted with the space, `super() { … }` included.

- lib/refract/formatter.rb: emit a space before the block
- test/refract.test.rb: one case

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@n-rodriguez
n-rodriguez force-pushed the fix/formatter-fidelity branch from fa02f00 to 49dea4d Compare October 1, 2026 16:05
Four tests failed on TruffleRuby, for two reasons that CRuby hides.

- lib/refract.rb: `Node.attributes` took its order from
  `Method#parameters`, which lists required keywords first on CRuby and
  keeps declaration order on TruffleRuby. The order reaches `inspect` and
  the traversal of children in `shareable_constant_value_within`, so it is
  now fixed to CRuby's on both
- lib/refract/formatter.rb: `escape_string` relied on `[[:print:]]` to
  decide a high byte of a binary string. CRuby says it is not printable,
  TruffleRuby says it is, so `"\xFF"` was emitted raw — a source that
  does not parse. Such bytes are now always escaped
- lib/refract/formatter.rb: drop a redundant escape in `BARE_SYMBOL`
  flagged by Style/RedundantRegexpEscape

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joeldrapper joeldrapper mentioned this pull request Oct 13, 2026
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