diff --git a/src/parser/transforms.js b/src/parser/transforms.js index cf52e51..ba1efa5 100644 --- a/src/parser/transforms.js +++ b/src/parser/transforms.js @@ -279,8 +279,11 @@ export const replaceRange = function replaceRange(s, start, end, substitute) { }; function maskTemplateContentForLint(content) { - // Preserve line endings and UTF-16 length, but remove template syntax from the JS placeholder. - return content.replace(/[^\r\n]/g, ' '); + // Must produce byte-identical output to ember-estree's toPlaceholderJS + // masking, or typescript-eslint sees two different contents for the same + // .gts file (disk read vs lint parse) and invalidates + rebuilds the + // program on every file. + return content.replace(/[`$]/g, ' '); } const processor = new Preprocessor(); diff --git a/tests/placeholder-parity.test.js b/tests/placeholder-parity.test.js new file mode 100644 index 0000000..ea03919 --- /dev/null +++ b/tests/placeholder-parity.test.js @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; +import { toTree } from 'ember-estree'; +import { transformForLint } from '../src/parser/transforms.js'; + +/** + * transformForLint (used by the patched ts.sys.readFile for type-aware + * linting) must produce byte-identical output to the placeholder JS that + * ember-estree's toTree hands to the JS/TS parser at lint time. + * + * typescript-eslint hashes the code passed to parseForESLint and compares it + * against what the watch program last read off disk; any difference marks the + * file as changed and silently rebuilds the whole program inside + * getProgram() — once per .gts/.gjs file linted (#229). + */ + +function toTreePlaceholder(code) { + let placeholder; + try { + toTree(code, { + filePath: 'x.gts', + parser: (js) => { + placeholder = js; + // Only the placeholder is needed; abort before JS parsing. + throw new Error('stop'); + }, + }); + } catch { + // expected + } + return placeholder; +} + +const cases = { + 'backtick-heavy template comments (#226)': `import Component from '@glimmer/component'; + +export default class MyComponent extends Component { + +} +`, + 'expression template with dollar signs': `export const x = ; +`, + 'multibyte content (emoji, CJK)': `export const y = ; +`, + 'CRLF line endings': `export const z = ;\r\n`, + 'multiple templates in one module': `export const a = ; +export const b = ; +`, +}; + +describe('transformForLint matches toTree placeholder byte-for-byte', () => { + for (const [name, code] of Object.entries(cases)) { + it(name, () => { + expect(transformForLint(code).output).toBe(toTreePlaceholder(code)); + }); + } +});