Skip to content

Commit e93b01f

Browse files
NullVoxPopuliclaude
andcommitted
Add template-lint-disable pragma for template regions
Implements a `template-lint-disable` comment directive that suppresses lint errors on the next line in template regions of gjs/gts/hbs files. This works like `eslint-disable-next-line` but uses the template-lint naming convention familiar to ember-template-lint users. Supported comment formats: {{! template-lint-disable }} {{!-- template-lint-disable rule-name --}} <!-- template-lint-disable rule-name --> Rule names can be specified as either eslint rule IDs (e.g. `no-undef`) or with `ember/template-` prefix (e.g. `ember/template-no-bare-strings`). When no rule is specified, all rules are suppressed on the next line. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent a2c8796 commit e93b01f

3 files changed

Lines changed: 265 additions & 2 deletions

File tree

lib/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const requireIndex = require('requireindex');
4-
const noop = require('ember-eslint-parser/noop');
4+
const templateLintDisableProcessor = require('./processors/template-lint-disable');
55
const pkg = require('../package.json'); // eslint-disable-line import/extensions
66

77
module.exports = {
@@ -16,6 +16,6 @@ module.exports = {
1616
},
1717
processors: {
1818
// https://eslint.org/docs/developer-guide/working-with-plugins#file-extension-named-processor
19-
noop,
19+
noop: templateLintDisableProcessor,
2020
},
2121
};
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict';
2+
3+
const noop = require('ember-eslint-parser/noop');
4+
5+
/**
6+
* Regex patterns for template-lint-disable comments:
7+
* {{! template-lint-disable rule1 rule2 }}
8+
* {{!-- template-lint-disable rule1 rule2 --}}
9+
* <!-- template-lint-disable rule1 rule2 -->
10+
*/
11+
const TEMPLATE_LINT_DISABLE_REGEX =
12+
/(?:{{!-*\s*template-lint-disable\s*([\s\S]*?)-*}}|<!--\s*template-lint-disable\s*([\s\S]*?)-->)/g;
13+
14+
// Store disable directives per file
15+
const fileDisableDirectives = new Map();
16+
17+
/**
18+
* Parse template-lint-disable comments from source text and store
19+
* which lines/rules should be suppressed.
20+
*
21+
* template-lint-disable means "disable next line" (like eslint-disable-next-line).
22+
*/
23+
function parseDisableDirectives(text) {
24+
const directives = [];
25+
const lines = text.split('\n');
26+
27+
for (let i = 0; i < lines.length; i++) {
28+
const line = lines[i];
29+
TEMPLATE_LINT_DISABLE_REGEX.lastIndex = 0;
30+
let match;
31+
32+
while ((match = TEMPLATE_LINT_DISABLE_REGEX.exec(line)) !== null) {
33+
const rulesPart = (match[1] || match[2] || '').trim();
34+
// Strip trailing -- from mustache block comments
35+
const cleaned = rulesPart.replace(/-+$/, '').trim();
36+
37+
const rules = cleaned
38+
? cleaned.split(/[\s,]+/).filter(Boolean)
39+
: []; // empty = disable all
40+
41+
directives.push({
42+
// comment is on line i+1 (1-indexed), next line is i+2
43+
line: i + 2,
44+
rules,
45+
});
46+
}
47+
}
48+
49+
return directives;
50+
}
51+
52+
/**
53+
* Map a rule name from template-lint format to eslint-plugin-ember format.
54+
* e.g. "no-bare-strings" -> "ember/template-no-bare-strings"
55+
*
56+
* Also accepts already-qualified names like "ember/template-no-bare-strings".
57+
*/
58+
function matchesRule(ruleId, disableRuleName) {
59+
if (ruleId === disableRuleName) {
60+
return true;
61+
}
62+
// Map template-lint name to eslint-plugin-ember name
63+
if (ruleId === `ember/template-${disableRuleName}`) {
64+
return true;
65+
}
66+
return false;
67+
}
68+
69+
function shouldSuppressMessage(message, directives) {
70+
for (const directive of directives) {
71+
if (message.line !== directive.line) {
72+
continue;
73+
}
74+
// No rules specified = suppress all
75+
if (directive.rules.length === 0) {
76+
return true;
77+
}
78+
// Check if any specified rule matches this message's rule
79+
if (directive.rules.some((rule) => matchesRule(message.ruleId, rule))) {
80+
return true;
81+
}
82+
}
83+
return false;
84+
}
85+
86+
module.exports = {
87+
registerParsedFile: noop.registerParsedFile,
88+
89+
preprocess(text, fileName) {
90+
const directives = parseDisableDirectives(text);
91+
if (directives.length > 0) {
92+
fileDisableDirectives.set(fileName, directives);
93+
} else {
94+
fileDisableDirectives.delete(fileName);
95+
}
96+
// Return text as-is (single code block)
97+
return [text];
98+
},
99+
100+
postprocess(messages, fileName) {
101+
// First, apply noop's postprocess logic (config validation)
102+
const msgs = noop.postprocess(messages, fileName);
103+
104+
const directives = fileDisableDirectives.get(fileName);
105+
if (!directives) {
106+
return msgs;
107+
}
108+
109+
fileDisableDirectives.delete(fileName);
110+
return msgs.filter((message) => !shouldSuppressMessage(message, directives));
111+
},
112+
113+
supportsAutofix: true,
114+
};

tests/lib/rules-preprocessor/gjs-gts-parser-test.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,3 +885,152 @@ describe('multiple tokens in same file', () => {
885885
expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");
886886
});
887887
});
888+
889+
describe('supports template-lint-disable directive', () => {
890+
it('disables all rules on the next line with mustache comment', async () => {
891+
const eslint = initESLint();
892+
const code = `
893+
<template>
894+
<div>
895+
{{! template-lint-disable }}
896+
{{test}}
897+
</div>
898+
</template>
899+
`;
900+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
901+
const resultErrors = results.flatMap((result) => result.messages);
902+
// {{test}} would normally trigger no-undef, but should be suppressed
903+
expect(resultErrors).toHaveLength(0);
904+
});
905+
906+
it('disables all rules on the next line with mustache block comment', async () => {
907+
const eslint = initESLint();
908+
const code = `
909+
<template>
910+
<div>
911+
{{!-- template-lint-disable --}}
912+
{{test}}
913+
</div>
914+
</template>
915+
`;
916+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
917+
const resultErrors = results.flatMap((result) => result.messages);
918+
expect(resultErrors).toHaveLength(0);
919+
});
920+
921+
it('disables all rules on the next line with HTML comment', async () => {
922+
const eslint = initESLint();
923+
const code = `
924+
<template>
925+
<div>
926+
<!-- template-lint-disable -->
927+
{{test}}
928+
</div>
929+
</template>
930+
`;
931+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
932+
const resultErrors = results.flatMap((result) => result.messages);
933+
expect(resultErrors).toHaveLength(0);
934+
});
935+
936+
it('disables a specific rule by eslint rule name', async () => {
937+
const eslint = initESLint();
938+
const code = `
939+
<template>
940+
<div>
941+
{{! template-lint-disable no-undef }}
942+
{{test}}
943+
{{other}}
944+
</div>
945+
</template>
946+
`;
947+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
948+
const resultErrors = results.flatMap((result) => result.messages);
949+
// {{test}} on line after disable should be suppressed
950+
// {{other}} on the line after that should NOT be suppressed
951+
expect(resultErrors).toHaveLength(1);
952+
expect(resultErrors[0].message).toBe("'other' is not defined.");
953+
});
954+
955+
it('only disables the next line, not subsequent lines', async () => {
956+
const eslint = initESLint();
957+
const code = `
958+
<template>
959+
<div>
960+
{{! template-lint-disable }}
961+
{{test}}
962+
{{other}}
963+
</div>
964+
</template>
965+
`;
966+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
967+
const resultErrors = results.flatMap((result) => result.messages);
968+
// {{test}} suppressed, {{other}} NOT suppressed
969+
expect(resultErrors).toHaveLength(1);
970+
expect(resultErrors[0].message).toBe("'other' is not defined.");
971+
});
972+
973+
it('does not suppress unrelated rules when a specific rule is named', async () => {
974+
const eslint = initESLint();
975+
const code = `
976+
<template>
977+
<div>
978+
{{! template-lint-disable ember/template-no-bare-strings }}
979+
{{test}}
980+
</div>
981+
</template>
982+
`;
983+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
984+
const resultErrors = results.flatMap((result) => result.messages);
985+
// no-undef should still fire since we only disabled template-no-bare-strings
986+
expect(resultErrors).toHaveLength(1);
987+
expect(resultErrors[0].ruleId).toBe('no-undef');
988+
});
989+
990+
it('supports template-lint rule name format (maps to ember/ prefix)', async () => {
991+
const eslint = initESLint();
992+
const code = `
993+
<template>
994+
<div>
995+
{{! template-lint-disable no-undef }}
996+
{{test}}
997+
</div>
998+
</template>
999+
`;
1000+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
1001+
const resultErrors = results.flatMap((result) => result.messages);
1002+
expect(resultErrors).toHaveLength(0);
1003+
});
1004+
1005+
it('supports multiple rule names', async () => {
1006+
const eslint = initESLint();
1007+
const code = `
1008+
<template>
1009+
<div>
1010+
{{! template-lint-disable no-undef quotes }}
1011+
{{test}}
1012+
</div>
1013+
</template>
1014+
`;
1015+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
1016+
const resultErrors = results.flatMap((result) => result.messages);
1017+
expect(resultErrors).toHaveLength(0);
1018+
});
1019+
1020+
it('works with multiple disable comments in the same file', async () => {
1021+
const eslint = initESLint();
1022+
const code = `
1023+
<template>
1024+
<div>
1025+
{{! template-lint-disable }}
1026+
{{test}}
1027+
{{! template-lint-disable }}
1028+
{{other}}
1029+
</div>
1030+
</template>
1031+
`;
1032+
const results = await eslint.lintText(code, { filePath: 'my-component.gjs' });
1033+
const resultErrors = results.flatMap((result) => result.messages);
1034+
expect(resultErrors).toHaveLength(0);
1035+
});
1036+
});

0 commit comments

Comments
 (0)