print: accept File nodes and weave comments into the output - #73
Conversation
print() previously threw on the File node that toTree returns, and comments -- which only live on File.comments -- were never printed, so reprinting a whole file silently dropped every comment. print(file) now prints file.program with the comments flushed back in: each comment is emitted immediately before the nearest printed node that follows it in the original source (matching source order), and comments after the last statement are appended at the end. Placement is approximate by design -- a same-line trailing comment becomes a leading comment of the following node -- so callers that care about exact layout should run a formatter over the result. Weaving state is module-level (print is synchronous), saved/restored in printFile, and cleared afterwards so unrelated print calls are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| const tree = toTree(source, { filePath: "m.js" }); | ||
| const output = print(tree); | ||
|
|
||
| expect(output).toContain(`/* setup */`); |
There was a problem hiding this comment.
use snapshot tests for this sort of assertion. as is, the assertions are hard to read, and could be incorrect (do this for all new tests in this file)
There was a problem hiding this comment.
Done — all nine tests now snapshot the full printed output via toMatchInlineSnapshot, so the expected text is visible verbatim in each test.
Per review: the toContain/indexOf assertions are replaced with toMatchInlineSnapshot of the full printed output, so each test shows exactly what print(File) produces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| const previousComments = fileComments; | ||
| const previousCursor = commentCursor; | ||
|
|
||
| fileComments = [...(file.comments ?? [])] |
There was a problem hiding this comment.
don't ever use [...arr] it's a huge performance footgun
There was a problem hiding this comment.
Gone in ee10c72 — filter() already yields a fresh array, so the spread was a pure waste; sorting happens in place on the filtered copy and the caller's file.comments is never mutated. Files with no comments skip weaving entirely.
That commit also includes a perf pass beyond this line (numbers in the updated PR body): the print/printNode split I'd introduced cost an extra call frame per node (~7–8% on standalone print(program) of a 4k-line file), so the switch is back inside print() as on main, with weaving as a top guard that short-circuits on one null test — standalone printing now measures at parity with main (−1%, noise).
Perf pass over the comment weaving: - The print/printNode split added an extra call frame to every node, measuring ~7-8% overhead on standalone print(program) of a 4k-line file. The switch is back inside print() (as on main); weaving is now a top guard that short-circuits on a single null test when no comment-carrying print(File) is in flight, and on a comment hit it flushes and re-enters (the cursor has advanced, so the recursion falls through). Standalone printing now measures at parity with main (-1%, within noise). - Comment flushing remains a monotone cursor over the sorted comments array: O(nodes + comments) total, each comment visited exactly once. - Per review: the [...arr] spread in printFile is gone -- filter() already yields a fresh array, so sorting in place is safe and the caller's file.comments is never mutated. Files without comments skip weaving entirely (fileComments stays null). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up on the benchmark question ("why is the medium tier slower on this PR?") — I pulled every Benchmark Comparison run whose logs are still available (June runs have expired) and tabulated the signed control-vs-experiment delta per tier (positive = experiment faster):
Medium-tier |Δ| across runs: #73 (3 runs × 3 tiers): max 6%, mean 2.8% — versus other runs: max 15%, mean 4.4%. Notably the docs-only PR (README generation, zero source changes) shows +6% swings on two medium tiers, and this PR's spread is run-to-run random rather than commit-correlated (gts-medium across #73's three runs: +1 / 0 / +6; two of those runs predate the perf restructuring, one follows it — same band). Mechanism check, in case the table wasn't enough:
So the medium numbers on this PR are inside the harness's normal cross-run variance (and slightly tighter than the repo average). The earlier absolute-time question — why medium takes ~70ms at all — is fixture composition: medium is ~100× the bytes of small (42KB vs 0.4KB), cost is linear in input, and ~60–80% of parse time is @glimmer/syntax parsing of the |
ember-estree 0.6.11 ships print(File) with comment weaving (NullVoxPopuli/ember-estree#73), so the rewrite is now the requested pipeline: mutate the Literal specifier nodes on the AST, print() the File, and run prettier over the result. The replaceAll step is gone. Files with no specifiers to rewrite are returned untouched. Prettier runs with the emitted app's config (printWidth 100, template tag plugin -- passed as a module since prettier resolves plugin names against the generated project's cwd, not this package). While here, babel-remove-types now also formats with the app's config instead of its singleQuote default, which previously made every converted file fail a generated JS project's own lint:prettier. Known cosmetic gap: print() collapses blank lines between statements, so a JS project with the eslint layer reports blank-line diagnostics on tests/test-helper.js (simple-import-sort group separation, newline-after-import, padding-line-between-statements). Blank-line preservation in ember-estree's printer is the follow-up. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up from ember.nvp#58 (comment):
print()threwunsupported node type 'File'on the nodetoTreereturns, and since comments only live onFile.comments, reprinting a file dropped every comment — which blocked usingprint()for ember.nvp's TS→JS import-specifier rewrite.What this does
print(file)now printsfile.programwithfile.commentswoven back in: each comment is emitted immediately before the nearest printed node that follows it in the original source (which matches source order for parsed trees), and comments after the last statement are appended at the end of the file. Works for comments nested inside functions/classes (they pick up block indentation naturally) and in gjs/gts files alongside<template>regions.let a = 1; // hi) becomes a leading comment of the next node. The intended pairing isprint()+ a formatter (prettier), per the ember.nvp use case, and the README documents this.print()exactly as before; weaving is a top guard that only engages while a comment-carryingprint(File)is in flight. On a comment hit it flushes the pending comments and re-enters (the cursor has advanced, so the recursion falls straight through to the switch). Weaving state is module-level (print is synchronous), saved/restored inprintFile, so standaloneprint(node)calls behave exactly as before.Performance
print(node)(the existing use case): pays onefileComments !== nullcheck and one extracase "File"arm; measured on a synthetic 4,000-line / 1,200-comment file (median of 50 runs, interleaved with main as control): main 0.50ms vs this branch 0.50ms (−1%, within noise). An earlier revision of this PR splitprintinto a wrapper +printNode, which cost an extra call frame per node (~7–8%); that structure is gone.print(File)with weaving (new capability): 0.69ms vs 0.50ms for the same file's bare program — the delta is the 1,200 comment emissions and concatenations themselves.parse()(gts/gjs/hbs fixtures) — it does not coverprint(), so the local numbers above are the relevant ones for this change. Parse deltas are all within ±5% in mixed directions (noise; this diff doesn't touch the parse path).filter()provides the fresh array, sorted in place; the caller'sfile.commentsis never mutated, and files without comments skip weaving entirely.Testing
tests/print-file.test.js(9 tests, inline snapshots of the full printed output per review): program printing, empty file, leading/block/doc comments, nested comments with indentation, trailing comments, gjs with templates, no state leakage between calls, File without acommentsproperty.pnpm test: 229 passing;pnpm lintclean.Once this is released, ember.nvp's rewrite can switch to mutate-AST +
print()+ prettier as requested in ember.nvp#58.🤖 Generated with Claude Code