Skip to content

Fix interpolated symbol shorthand hash keys producing invalid Ruby#858

Merged
reese merged 2 commits intofables-tales:trunkfrom
dduugg:fix/interpolated-symbol-shorthand-hash-key
May 6, 2026
Merged

Fix interpolated symbol shorthand hash keys producing invalid Ruby#858
reese merged 2 commits intofables-tales:trunkfrom
dduugg:fix/interpolated-symbol-shorthand-hash-key

Conversation

@dduugg
Copy link
Copy Markdown
Contributor

@dduugg dduugg commented May 6, 2026

Closes #857.

Bug

A hash with an interpolated shorthand symbol key was rewritten to invalid Ruby:

{ "#{x}": 1 }   # input
{:"#{x}": 1}    # rubyfmt output — invalid; second pass fails as a syntax error

Cause

format_interpolated_symbol_node hard-coded the opener as :" and the closer as ", ignoring prism's opening_loc / closing_loc. That works for the standalone literal :"#{x}" but corrupts the shorthand-key form, where prism reports opening_loc = " and closing_loc = ":. The function emitted :"#{x}", then format_assoc_node appended its own : separator.

Fix

Mirror the pattern that format_symbol_node already uses for plain SymbolNodes: emit the actual opening / closing locations and strip a trailing : from the closer (since format_assoc_node re-emits that as the key/value separator). A debug_assert! documents that the locations are always present in practice; the previous :" / " defaults remain as a graceful release-mode fallback.

Test

Fixtures cover:

  • { "#{x}": 1 } — bare hash, the original failure mode
  • foo("#{x}": 1) — keyword-arg call site (distinct caller of format_assoc_node)
  • { "#{x}": 1, "#{y}": 2 } — multi-entry, distinguishes a per-first-key fix from a general one
  • { a: 1, "#{x}": 2 } — mixed plain-shorthand and interpolated-shorthand

The existing :"#{name}=" literal coverage in string_first_item_is_embed still passes (regression check). cargo fmt --check and cargo clippy -D warnings are clean.

Follow-on refactor

Per the suggestion to consider centralizing the mirrored strip logic, commit 4d00438 extracts emit_symbol_key_closer next to format_assoc_node. Both format_symbol_node and format_interpolated_symbol_node now route through it, so the strip_suffix(b":") pattern lives in one place with a doc comment naming format_assoc_node as the owner of the : separator. No behavior change — verified by the full fixtures suite (379 tests) and an extra round-trip on the mixed-form path ({foo: 1, 'bar' => 2}{:foo => 1, "bar" => 2}), which exercises the strip in the rocket-rendering branch where as_symbol=false.

A hash with an interpolated shorthand symbol key like `{ "#{x}": 1 }`
was rewritten to `{:"#{x}": 1}`, which is invalid Ruby — a second pass
through rubyfmt rejects it as a syntax error.

`format_interpolated_symbol_node` was hard-coding the opener as `:"`
and the closer as `"`, ignoring prism's `opening_loc`/`closing_loc`.
That works for the standalone literal `:"#{x}"` but corrupts the
shorthand-key form, where prism reports `opening_loc=`"`` and
`closing_loc=`":``. The function emitted `:"#{x}"` and then
`format_assoc_node` appended its own `:` separator.

Fix this by mirroring the pattern `format_symbol_node` already uses
for plain `SymbolNode`s: emit the actual opening/closing locations and
strip a trailing `:` from the closer (since `format_assoc_node`
re-emits that as the key/value separator). A `debug_assert!` on the
locations being present documents the invariant; the previous `:"`/`"`
defaults are kept as a graceful fallback.

Fixtures cover the bare hash form, the keyword-arg call site
`foo("#{x}": 1)`, multi-entry hashes, and a mixed shorthand /
interpolated-shorthand form.

Closes fables-tales#857.
@dduugg dduugg force-pushed the fix/interpolated-symbol-shorthand-hash-key branch from f792b12 to 8b726ad Compare May 6, 2026 17:57
@dduugg dduugg marked this pull request as ready for review May 6, 2026 18:02
Copy link
Copy Markdown
Collaborator

@reese reese left a comment

Choose a reason for hiding this comment

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

LGTM. I think a refactor would also be fine if you wanted to move some of that mirrored logic into format_assoc_node (in this PR or in a follow-on), up to you.

`format_symbol_node` and `format_interpolated_symbol_node` each carried
a copy of the same `strip_suffix(b":")` logic for shorthand hash-key
closers. Extract it into a single helper (`emit_symbol_key_closer`)
placed next to `format_assoc_node`, with a doc comment that names
`format_assoc_node` as the owner of the `:` separator (re-emitted as
`:` for shorthand or replaced with ` =>` in the rocket branch on mixed
hashes).

No behavior change. All 379 fixture tests pass, including the
mixed-form path where `as_symbol=false` but source used shorthand
(`{foo: 1, 'bar' => 2}` → `{:foo => 1, "bar" => 2}`), which exercises
the strip in the rocket-rendering direction.
@reese reese merged commit 90cdb10 into fables-tales:trunk May 6, 2026
8 checks passed
@dduugg
Copy link
Copy Markdown
Contributor Author

dduugg commented May 6, 2026

@reese TY for the quick review. I added a second commit to de-duplicate the logic, PTaL.

@dduugg dduugg deleted the fix/interpolated-symbol-shorthand-hash-key branch May 6, 2026 21:30
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.

Hash with interpolated shorthand symbol key ({ "#{x}": 1 }) is rewritten to invalid Ruby {:"#{x}": 1}

2 participants