Skip to content

Commit 5092e7d

Browse files
Merge pull request #194 from johanrd/regression/eslint-directive-comments-in-templates
test: regression coverage for {{! eslint-disable-* }} directives in templates
2 parents 37b693b + ea768ae commit 5092e7d

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Regression tests for `{{! eslint-disable-* }}` directives inside
3+
* `<template>` blocks.
4+
*
5+
* ESLint's inline-config scanner (SourceCode#getInlineConfigNodes) only
6+
* reads `Program.comments`. Any template-comment handling that drops
7+
* those nodes, or leaves them in a shape the scanner rejects, silently
8+
* breaks `{{! eslint-disable-* }}` suppression in .gjs/.gts and .hbs
9+
* files. These tests pin the end-to-end contract by running a real
10+
* `Linter` pass against a stub rule — no downstream plugins required.
11+
*
12+
* Background: ember-estree 0.4.3 (NullVoxPopuli/ember-estree#31)
13+
* flipped the default so Glimmer comment nodes stay in
14+
* `GlimmerTemplate.body` and no longer flow into `Program.comments`,
15+
* silently regressing every consumer here. These tests exist to catch
16+
* that class of change before it ships.
17+
*/
18+
19+
import { describe, expect, it } from 'vitest';
20+
import { Linter } from 'eslint';
21+
import { parseForESLint as gjsParseForESLint } from '../src/parser/gjs-gts-parser.js';
22+
import { parseForESLint as hbsParseForESLint } from '../src/parser/hbs-parser.js';
23+
24+
function makeLinter({ parser, parserName }) {
25+
const linter = new Linter();
26+
linter.defineParser(parserName, { parseForESLint: parser });
27+
// Stub rule with a clearly identifiable message so the assertions
28+
// don't depend on any specific plugin being installed.
29+
linter.defineRule('regression/concat-flag', {
30+
create(context) {
31+
return {
32+
GlimmerConcatStatement(node) {
33+
context.report({ node, message: 'flagged concat' });
34+
},
35+
};
36+
},
37+
});
38+
return linter;
39+
}
40+
41+
function verify(linter, code, { parserName, filename }) {
42+
return linter.verify(
43+
code,
44+
{
45+
parser: parserName,
46+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
47+
rules: { 'regression/concat-flag': 'error' },
48+
},
49+
{ filename }
50+
);
51+
}
52+
53+
function flagged(messages) {
54+
return messages.filter((m) => m.ruleId === 'regression/concat-flag');
55+
}
56+
57+
describe('{{! eslint-disable-* }} directives inside <template> — .gts', () => {
58+
const linter = makeLinter({ parser: gjsParseForESLint, parserName: 'ember-eslint-parser' });
59+
const run = (code) =>
60+
verify(linter, code, { parserName: 'ember-eslint-parser', filename: 'test.gts' });
61+
62+
it('sanity: the stub rule fires when there is no directive', () => {
63+
const code = ['<template>', ' <li aria-current="{{foo}}"></li>', '</template>'].join('\n');
64+
expect(flagged(run(code))).toHaveLength(1);
65+
});
66+
67+
it('{{! eslint-disable-next-line regression/concat-flag }} suppresses the next line', () => {
68+
const code = [
69+
'<template>',
70+
' <li',
71+
' {{! eslint-disable-next-line regression/concat-flag }}',
72+
' aria-current="{{foo}}"',
73+
' ></li>',
74+
'</template>',
75+
].join('\n');
76+
expect(flagged(run(code))).toEqual([]);
77+
});
78+
79+
it('{{!-- eslint-disable-next-line regression/concat-flag --}} (long-form) suppresses the next line', () => {
80+
const code = [
81+
'<template>',
82+
' <li',
83+
' {{!-- eslint-disable-next-line regression/concat-flag --}}',
84+
' aria-current="{{foo}}"',
85+
' ></li>',
86+
'</template>',
87+
].join('\n');
88+
expect(flagged(run(code))).toEqual([]);
89+
});
90+
91+
it('{{! eslint-disable regression/concat-flag }} … {{! eslint-enable }} suppresses the block', () => {
92+
const code = [
93+
'<template>',
94+
' {{! eslint-disable regression/concat-flag }}',
95+
' <li aria-current="{{foo}}"></li>',
96+
' <li aria-current="{{bar}}"></li>',
97+
' {{! eslint-enable regression/concat-flag }}',
98+
' <li aria-current="{{baz}}"></li>',
99+
'</template>',
100+
].join('\n');
101+
const msgs = flagged(run(code));
102+
// Only the line after `eslint-enable` should still flag.
103+
expect(msgs).toHaveLength(1);
104+
});
105+
106+
it('directive before a <template> block suppresses a violation inside it', () => {
107+
// `{{! }}` lives inside the Glimmer region, but a standard JS block
108+
// comment just before the <template> expression should also reach
109+
// the same scanner — ensures the JS-side comment stream is intact.
110+
const code = [
111+
'const x = 1;',
112+
'/* eslint-disable-next-line regression/concat-flag */',
113+
'const y = <template><li aria-current="{{foo}}"></li></template>;',
114+
].join('\n');
115+
expect(flagged(run(code))).toEqual([]);
116+
});
117+
});
118+
119+
describe('{{! eslint-disable-* }} directives inside a template — .hbs', () => {
120+
const linter = makeLinter({ parser: hbsParseForESLint, parserName: 'ember-eslint-parser/hbs' });
121+
const run = (code) =>
122+
verify(linter, code, { parserName: 'ember-eslint-parser/hbs', filename: 'test.hbs' });
123+
124+
it('sanity: the stub rule fires when there is no directive', () => {
125+
const code = '<li aria-current="{{foo}}"></li>';
126+
expect(flagged(run(code))).toHaveLength(1);
127+
});
128+
129+
it('{{! eslint-disable-next-line regression/concat-flag }} suppresses the next line', () => {
130+
const code = [
131+
'<li',
132+
' {{! eslint-disable-next-line regression/concat-flag }}',
133+
' aria-current="{{foo}}"',
134+
'></li>',
135+
].join('\n');
136+
expect(flagged(run(code))).toEqual([]);
137+
});
138+
});
139+
140+
describe('Program.comments shape expected by ESLint consumers', () => {
141+
// ESLint's `SourceCode#getInlineConfigNodes` reads `ast.comments`,
142+
// filters out `type: 'Shebang'`, and for `eslint-disable-next-line`
143+
// accepts any comment type except `'Line'`. Most plugin rules that
144+
// iterate `sourceCode.getAllComments()` additionally filter on
145+
// `type === 'Block'` (e.g. eslint-plugin-ember's
146+
// template-no-html-comments). Pin both expectations at the parser
147+
// boundary so a future ember-estree shape change that silently loses
148+
// either trips a specific test rather than vague downstream fallout.
149+
150+
it('.gts: directive-shaped template comments surface in Program.comments', () => {
151+
const source = [
152+
'const X = <template>',
153+
' <li',
154+
' {{! eslint-disable-next-line regression/concat-flag }}',
155+
' aria-current="{{foo}}"',
156+
' ></li>',
157+
'</template>;',
158+
].join('\n');
159+
const { ast } = gjsParseForESLint(source, {
160+
filePath: 'test.gts',
161+
range: true,
162+
loc: true,
163+
comment: true,
164+
tokens: true,
165+
});
166+
const directives = (ast.comments || []).filter(
167+
(c) => typeof c.value === 'string' && /^\s*eslint-disable-next-line\s/.test(c.value)
168+
);
169+
expect(directives).toHaveLength(1);
170+
expect(directives[0].value.trim()).toBe('eslint-disable-next-line regression/concat-flag');
171+
// Most plugin rules iterate `getAllComments()` and filter on `Block`.
172+
expect(directives[0].type).toBe('Block');
173+
});
174+
175+
it('.hbs: directive-shaped template comments surface in Program.comments', () => {
176+
const source = [
177+
'<li',
178+
' {{! eslint-disable-next-line regression/concat-flag }}',
179+
' aria-current="{{foo}}"',
180+
'></li>',
181+
].join('\n');
182+
const { ast } = hbsParseForESLint(source, { filePath: 'test.hbs' });
183+
const directives = (ast.comments || []).filter(
184+
(c) => typeof c.value === 'string' && /^\s*eslint-disable-next-line\s/.test(c.value)
185+
);
186+
expect(directives).toHaveLength(1);
187+
expect(directives[0].type).toBe('Block');
188+
});
189+
});

0 commit comments

Comments
 (0)