Match transformForLint masking to ember-estree's toPlaceholderJS (fixes per-file program rebuilds)#233
Merged
NullVoxPopuli merged 1 commit intoJul 3, 2026
Conversation
typescript-eslint hashes the code the parser passes in against what the watch program last read from disk (via the patched ts.sys.readFile). The two texts came from different masking strategies: toPlaceholderJS keeps template content and blanks only backticks/dollars, while transformForLint blanked every non-newline character. Any .gts/.gjs file with a non-empty template therefore hashed differently, which fired the file-changed watch callback and silently rebuilt the entire TS program inside getProgram() — once per file linted. On large codebases that is hour-long lint runs and tens of GB of memory (ember-tooling#229). Blank only backticks/dollars, byte-matching ember-estree 0.6.10, and add a parity regression test so the two implementations cannot drift again. This masking is still overflow-safe for the ember-tooling#226 case: replacement is 1:1, so the placeholder never outgrows the original region. Fixes ember-tooling#229 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NullVoxPopuli
approved these changes
Jul 3, 2026
NullVoxPopuli
left a comment
Member
There was a problem hiding this comment.
good news is that all the other tests added over the last few days still pass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #229 (the performance/memory half, with a strong lead on the rest).
Root cause
For type-aware linting, every
.gts/.gjsfile gets a JS placeholder text twice, by two different implementations:toPlaceholderJSkeeps template content and blanks only`/$(since ember-estree 0.6.10, thetypescript-eslinterror on a file #230 fix)transformForLintin the patchedts.sys.readFileblanked every non-newline character (since v0.14.1, the Parser breaks on a specific template #226 fix)typescript-estree hashes the code passed to
parseForESLintagainst what the watch program last read from disk (getWatchProgramsForProjects). Since the two maskings differ, every file with a non-empty template hashes differently — which fires the file-changed watch callback and silently rebuilds the entire TS program insidegetProgram(), once per.gts/.gjsfile linted. It's invisible in debug logs ("Found existing program for file" prints either way); the tell is thatservices.programis a new object on every parse.On a large codebase that's an O(project) rebuild × thousands of files — hour-long runs — and the allocation churn (×
--concurrencyworkers) accounts for the tens-of-GB memory. It also plausibly feeds the non-deterministic type errors, since checker state now depends on per-worker lint order.How the two fixes diverged
#226 and #230 were the same escape-overflow bug in the two copies of this logic. Through v0.14.0 both sides escaped
`/$identically, so bytes matched except on overflow files. The fixes chose different maskings (blank-all vs blank-`$), so as of v0.14.1/v0.14.2 every templated file mismatches.Fix
Blank only
`/$inmaskTemplateContentForLint, byte-matching ember-estree 0.6.10. This remains overflow-safe for the #226 case (1:1 replacement, placeholder never outgrows the region).Verified against a minimal 3-file typed-lint project: before,
services.programwas a distinct object after every parse; after, all parses share one program.Regression test
tests/placeholder-parity.test.jsassertstransformForLint(code).outputis byte-identical to thetoTreeplaceholder across the #226 snippet, dollar-heavy expression templates, emoji/CJK, CRLF, and multi-template modules — so the two implementations can't silently drift again. Longer-term it may be worth exportingtoPlaceholderJSfrom ember-estree and deleting the copy here.🤖 Generated with Claude Code