Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/parser/transforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
59 changes: 59 additions & 0 deletions tests/placeholder-parity.test.js
Original file line number Diff line number Diff line change
@@ -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 {
<template>
{{! \`asd\` \`qwe\` \`zxc\` \`undefined\` \`asd\` }}
{{! \`@foo\` }}
</template>
}
`,
'expression template with dollar signs': `export const x = <template>costs \${{amount}} \`really\` $$$</template>;
`,
'multibyte content (emoji, CJK)': `export const y = <template>🎉 日本語 \` $ 🚀</template>;
`,
'CRLF line endings': `export const z = <template>\r\n hi \`there\`\r\n</template>;\r\n`,
'multiple templates in one module': `export const a = <template>one \`x\`</template>;
export const b = <template>two $y</template>;
`,
};

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));
});
}
});
Loading