Skip to content

Commit af36c10

Browse files
Merge pull request #233 from NullVoxPopuli-ai-agent/fix/placeholder-masking-parity
Match transformForLint masking to ember-estree's toPlaceholderJS (fixes per-file program rebuilds)
2 parents d94a3ba + 62d2a25 commit af36c10

2 files changed

Lines changed: 64 additions & 2 deletions

File tree

src/parser/transforms.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,11 @@ export const replaceRange = function replaceRange(s, start, end, substitute) {
279279
};
280280

281281
function maskTemplateContentForLint(content) {
282-
// Preserve line endings and UTF-16 length, but remove template syntax from the JS placeholder.
283-
return content.replace(/[^\r\n]/g, ' ');
282+
// Must produce byte-identical output to ember-estree's toPlaceholderJS
283+
// masking, or typescript-eslint sees two different contents for the same
284+
// .gts file (disk read vs lint parse) and invalidates + rebuilds the
285+
// program on every file.
286+
return content.replace(/[`$]/g, ' ');
284287
}
285288

286289
const processor = new Preprocessor();

tests/placeholder-parity.test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { toTree } from 'ember-estree';
3+
import { transformForLint } from '../src/parser/transforms.js';
4+
5+
/**
6+
* transformForLint (used by the patched ts.sys.readFile for type-aware
7+
* linting) must produce byte-identical output to the placeholder JS that
8+
* ember-estree's toTree hands to the JS/TS parser at lint time.
9+
*
10+
* typescript-eslint hashes the code passed to parseForESLint and compares it
11+
* against what the watch program last read off disk; any difference marks the
12+
* file as changed and silently rebuilds the whole program inside
13+
* getProgram() — once per .gts/.gjs file linted (#229).
14+
*/
15+
16+
function toTreePlaceholder(code) {
17+
let placeholder;
18+
try {
19+
toTree(code, {
20+
filePath: 'x.gts',
21+
parser: (js) => {
22+
placeholder = js;
23+
// Only the placeholder is needed; abort before JS parsing.
24+
throw new Error('stop');
25+
},
26+
});
27+
} catch {
28+
// expected
29+
}
30+
return placeholder;
31+
}
32+
33+
const cases = {
34+
'backtick-heavy template comments (#226)': `import Component from '@glimmer/component';
35+
36+
export default class MyComponent extends Component {
37+
<template>
38+
{{! \`asd\` \`qwe\` \`zxc\` \`undefined\` \`asd\` }}
39+
{{! \`@foo\` }}
40+
</template>
41+
}
42+
`,
43+
'expression template with dollar signs': `export const x = <template>costs \${{amount}} \`really\` $$$</template>;
44+
`,
45+
'multibyte content (emoji, CJK)': `export const y = <template>🎉 日本語 \` $ 🚀</template>;
46+
`,
47+
'CRLF line endings': `export const z = <template>\r\n hi \`there\`\r\n</template>;\r\n`,
48+
'multiple templates in one module': `export const a = <template>one \`x\`</template>;
49+
export const b = <template>two $y</template>;
50+
`,
51+
};
52+
53+
describe('transformForLint matches toTree placeholder byte-for-byte', () => {
54+
for (const [name, code] of Object.entries(cases)) {
55+
it(name, () => {
56+
expect(transformForLint(code).output).toBe(toTreePlaceholder(code));
57+
});
58+
}
59+
});

0 commit comments

Comments
 (0)