From f9db8710795edf5e5765b7b5a24dd78eb5963df1 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:41:18 -0400 Subject: [PATCH 01/19] 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 --}} 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) --- lib/index.js | 4 +- lib/processors/template-lint-disable.js | 114 ++++++++++++++ .../rules-preprocessor/gjs-gts-parser-test.js | 149 ++++++++++++++++++ 3 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 lib/processors/template-lint-disable.js diff --git a/lib/index.js b/lib/index.js index bfb49a4471..7bb1b2110e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,7 @@ 'use strict'; const requireIndex = require('requireindex'); -const noop = require('ember-eslint-parser/noop'); +const templateLintDisableProcessor = require('./processors/template-lint-disable'); const pkg = require('../package.json'); // eslint-disable-line import/extensions module.exports = { @@ -16,6 +16,6 @@ module.exports = { }, processors: { // https://eslint.org/docs/developer-guide/working-with-plugins#file-extension-named-processor - noop, + noop: templateLintDisableProcessor, }, }; diff --git a/lib/processors/template-lint-disable.js b/lib/processors/template-lint-disable.js new file mode 100644 index 0000000000..630e8b4d37 --- /dev/null +++ b/lib/processors/template-lint-disable.js @@ -0,0 +1,114 @@ +'use strict'; + +const noop = require('ember-eslint-parser/noop'); + +/** + * Regex patterns for template-lint-disable comments: + * {{! template-lint-disable rule1 rule2 }} + * {{!-- template-lint-disable rule1 rule2 --}} + * + */ +const TEMPLATE_LINT_DISABLE_REGEX = + /(?:{{!-*\s*template-lint-disable\s*([\s\S]*?)-*}}|)/g; + +// Store disable directives per file +const fileDisableDirectives = new Map(); + +/** + * Parse template-lint-disable comments from source text and store + * which lines/rules should be suppressed. + * + * template-lint-disable means "disable next line" (like eslint-disable-next-line). + */ +function parseDisableDirectives(text) { + const directives = []; + const lines = text.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + TEMPLATE_LINT_DISABLE_REGEX.lastIndex = 0; + let match; + + while ((match = TEMPLATE_LINT_DISABLE_REGEX.exec(line)) !== null) { + const rulesPart = (match[1] || match[2] || '').trim(); + // Strip trailing -- from mustache block comments + const cleaned = rulesPart.replace(/-+$/, '').trim(); + + const rules = cleaned + ? cleaned.split(/[\s,]+/).filter(Boolean) + : []; // empty = disable all + + directives.push({ + // comment is on line i+1 (1-indexed), next line is i+2 + line: i + 2, + rules, + }); + } + } + + return directives; +} + +/** + * Map a rule name from template-lint format to eslint-plugin-ember format. + * e.g. "no-bare-strings" -> "ember/template-no-bare-strings" + * + * Also accepts already-qualified names like "ember/template-no-bare-strings". + */ +function matchesRule(ruleId, disableRuleName) { + if (ruleId === disableRuleName) { + return true; + } + // Map template-lint name to eslint-plugin-ember name + if (ruleId === `ember/template-${disableRuleName}`) { + return true; + } + return false; +} + +function shouldSuppressMessage(message, directives) { + for (const directive of directives) { + if (message.line !== directive.line) { + continue; + } + // No rules specified = suppress all + if (directive.rules.length === 0) { + return true; + } + // Check if any specified rule matches this message's rule + if (directive.rules.some((rule) => matchesRule(message.ruleId, rule))) { + return true; + } + } + return false; +} + +module.exports = { + registerParsedFile: noop.registerParsedFile, + + preprocess(text, fileName) { + const directives = parseDisableDirectives(text); + if (directives.length > 0) { + fileDisableDirectives.set(fileName, directives); + } else { + fileDisableDirectives.delete(fileName); + } + // Return text as-is (single code block) + return [text]; + }, + + postprocess(messages, fileName) { + // First, apply noop's postprocess logic (config validation) + const msgs = noop.postprocess(messages, fileName); + + const directives = fileDisableDirectives.get(fileName); + if (!directives) { + return msgs; + } + + fileDisableDirectives.delete(fileName); + return msgs.filter((message) => !shouldSuppressMessage(message, directives)); + }, + + supportsAutofix: true, +}; diff --git a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js index cda7c36a34..eed175eca4 100644 --- a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js +++ b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js @@ -886,3 +886,152 @@ describe('multiple tokens in same file', () => { expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead."); }); }); + +describe('supports template-lint-disable directive', () => { + it('disables all rules on the next line with mustache comment', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + // {{test}} would normally trigger no-undef, but should be suppressed + expect(resultErrors).toHaveLength(0); + }); + + it('disables all rules on the next line with mustache block comment', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('disables all rules on the next line with HTML comment', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('disables a specific rule by eslint rule name', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + // {{test}} on line after disable should be suppressed + // {{other}} on the line after that should NOT be suppressed + expect(resultErrors).toHaveLength(1); + expect(resultErrors[0].message).toBe("'other' is not defined."); + }); + + it('only disables the next line, not subsequent lines', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + // {{test}} suppressed, {{other}} NOT suppressed + expect(resultErrors).toHaveLength(1); + expect(resultErrors[0].message).toBe("'other' is not defined."); + }); + + it('does not suppress unrelated rules when a specific rule is named', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + // no-undef should still fire since we only disabled template-no-bare-strings + expect(resultErrors).toHaveLength(1); + expect(resultErrors[0].ruleId).toBe('no-undef'); + }); + + it('supports template-lint rule name format (maps to ember/ prefix)', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('supports multiple rule names', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('works with multiple disable comments in the same file', async () => { + const eslint = initESLint(); + const code = ` + + `; + const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); +}); From cfc13ae7d3571b7a069f59ff6f44f69196589a03 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:49:03 -0400 Subject: [PATCH 02/19] Remove HTML comment support for template-lint-disable Only mustache comments ({{! ... }} and {{!-- ... --}}) are supported for the template-lint-disable directive. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/processors/template-lint-disable.js | 7 +++---- .../lib/rules-preprocessor/gjs-gts-parser-test.js | 15 --------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/lib/processors/template-lint-disable.js b/lib/processors/template-lint-disable.js index 630e8b4d37..b52686ed87 100644 --- a/lib/processors/template-lint-disable.js +++ b/lib/processors/template-lint-disable.js @@ -3,13 +3,12 @@ const noop = require('ember-eslint-parser/noop'); /** - * Regex patterns for template-lint-disable comments: + * Regex pattern for template-lint-disable mustache comments: * {{! template-lint-disable rule1 rule2 }} * {{!-- template-lint-disable rule1 rule2 --}} - * */ const TEMPLATE_LINT_DISABLE_REGEX = - /(?:{{!-*\s*template-lint-disable\s*([\s\S]*?)-*}}|)/g; + /{{!-*\s*template-lint-disable\s*([\s\S]*?)-*}}/g; // Store disable directives per file const fileDisableDirectives = new Map(); @@ -30,7 +29,7 @@ function parseDisableDirectives(text) { let match; while ((match = TEMPLATE_LINT_DISABLE_REGEX.exec(line)) !== null) { - const rulesPart = (match[1] || match[2] || '').trim(); + const rulesPart = (match[1] || '').trim(); // Strip trailing -- from mustache block comments const cleaned = rulesPart.replace(/-+$/, '').trim(); diff --git a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js index eed175eca4..2bbe811ad0 100644 --- a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js +++ b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js @@ -919,21 +919,6 @@ describe('supports template-lint-disable directive', () => { expect(resultErrors).toHaveLength(0); }); - it('disables all rules on the next line with HTML comment', async () => { - const eslint = initESLint(); - const code = ` - - `; - const results = await eslint.lintText(code, { filePath: 'my-component.gjs' }); - const resultErrors = results.flatMap((result) => result.messages); - expect(resultErrors).toHaveLength(0); - }); - it('disables a specific rule by eslint rule name', async () => { const eslint = initESLint(); const code = ` From 176831f7d9d8b0c0ff80ec0938ab3be7c9a838e1 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 22 Mar 2026 00:53:12 -0400 Subject: [PATCH 03/19] Add hbs file support for template-lint-disable and tests Apply the processor to hbs files in base configs so template-lint-disable works in standalone .hbs templates, not just gjs/gts files. Also handle template AST edge case where TextNodes start on the comment line. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/config-legacy/base.js | 5 + lib/config/base.js | 8 ++ lib/processors/template-lint-disable.js | 10 +- lib/recommended.mjs | 17 +++ .../rules-preprocessor/gjs-gts-parser-test.js | 122 ++++++++++++++++++ 5 files changed, 159 insertions(+), 3 deletions(-) diff --git a/lib/config-legacy/base.js b/lib/config-legacy/base.js index d27a5e8806..1dabbaf462 100644 --- a/lib/config-legacy/base.js +++ b/lib/config-legacy/base.js @@ -23,5 +23,10 @@ module.exports = { parser: 'ember-eslint-parser', processor: 'ember/noop', }, + { + files: ['**/*.hbs'], + parser: 'ember-eslint-parser/hbs', + processor: 'ember/noop', + }, ], }; diff --git a/lib/config/base.js b/lib/config/base.js index c6bdf52381..b7f5f7f0c1 100644 --- a/lib/config/base.js +++ b/lib/config/base.js @@ -1,5 +1,6 @@ const plugin = require('../index'); const emberEslintParser = require('ember-eslint-parser'); +const emberEslintParserHbs = require('ember-eslint-parser/hbs'); module.exports = [ { @@ -17,4 +18,11 @@ module.exports = [ }, processor: 'ember/noop', }, + { + files: ['**/*.hbs'], + languageOptions: { + parser: emberEslintParserHbs, + }, + processor: 'ember/noop', + }, ]; diff --git a/lib/processors/template-lint-disable.js b/lib/processors/template-lint-disable.js index b52686ed87..84b57996c5 100644 --- a/lib/processors/template-lint-disable.js +++ b/lib/processors/template-lint-disable.js @@ -37,9 +37,13 @@ function parseDisableDirectives(text) { ? cleaned.split(/[\s,]+/).filter(Boolean) : []; // empty = disable all + // Comment is on line i+1 (1-indexed), next line is i+2. + // In template ASTs, TextNodes can start on the same line as the comment + // (e.g. the newline after {{! template-lint-disable }} is part of the + // following TextNode), so we suppress both the comment line and the next. directives.push({ - // comment is on line i+1 (1-indexed), next line is i+2 - line: i + 2, + commentLine: i + 1, + nextLine: i + 2, rules, }); } @@ -67,7 +71,7 @@ function matchesRule(ruleId, disableRuleName) { function shouldSuppressMessage(message, directives) { for (const directive of directives) { - if (message.line !== directive.line) { + if (message.line !== directive.commentLine && message.line !== directive.nextLine) { continue; } // No rules specified = suppress all diff --git a/lib/recommended.mjs b/lib/recommended.mjs index 5daaf29ae4..029c15c953 100644 --- a/lib/recommended.mjs +++ b/lib/recommended.mjs @@ -3,9 +3,11 @@ import baseRules from './recommended-rules.js'; import gjsRules from './recommended-rules-gjs.js'; import gtsRules from './recommended-rules-gts.js'; import emberParser from 'ember-eslint-parser'; +import emberParserHbs from 'ember-eslint-parser/hbs'; export const plugin = emberPlugin; export const parser = emberParser; +export const hbsParser = emberParserHbs; export const base = { name: 'ember:base', @@ -53,14 +55,29 @@ export const gts = { }, }; +export const hbs = { + name: 'ember:hbs', + plugins: { ember: emberPlugin }, + files: ['**/*.hbs'], + languageOptions: { + parser: emberParserHbs, + }, + processor: 'ember/noop', + rules: { + ...base.rules, + }, +}; + export default { // Helpful utility exports plugin, parser, + hbsParser, // Recommended config sets configs: { base, gjs, gts, + hbs, }, }; diff --git a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js index 2bbe811ad0..6ce852db27 100644 --- a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js +++ b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js @@ -13,6 +13,7 @@ const { writeFileSync, readFileSync } = require('node:fs'); const { join } = require('node:path'); const gjsGtsParser = require.resolve('ember-eslint-parser'); +const hbsParser = require.resolve('ember-eslint-parser/hbs'); /** * Helper function which creates ESLint instance with enabled/disabled autofix feature. @@ -1020,3 +1021,124 @@ describe('supports template-lint-disable directive', () => { expect(resultErrors).toHaveLength(0); }); }); + +describe('supports template-lint-disable directive in hbs files', () => { + function initHbsESLint() { + return new ESLint({ + ignore: false, + useEslintrc: false, + plugins: { ember: plugin }, + overrideConfig: { + root: true, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + plugins: ['ember'], + overrides: [ + { + files: ['**/*.hbs'], + parser: hbsParser, + processor: 'ember/noop', + rules: { + 'ember/template-no-bare-strings': 'error', + }, + }, + ], + }, + }); + } + + it('disables all rules on the next line with mustache comment', async () => { + const eslint = initHbsESLint(); + const code = `
+ {{! template-lint-disable }} + Hello world +
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('disables all rules on the next line with mustache block comment', async () => { + const eslint = initHbsESLint(); + const code = `
+ {{!-- template-lint-disable --}} + Hello world +
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('only disables the next line, not subsequent lines', async () => { + const eslint = initHbsESLint(); + const code = `{{! template-lint-disable }} +
Hello world
+
Bare string here too
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + // Line 2 "Hello world" suppressed, but line 3 "Bare string here too" should still error + expect(resultErrors).toHaveLength(1); + expect(resultErrors[0].line).toBe(3); + }); + + it('disables a specific rule by name', async () => { + const eslint = initHbsESLint(); + const code = `
+ {{! template-lint-disable ember/template-no-bare-strings }} + Hello world +
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('supports template-lint rule name format (maps to ember/ prefix)', async () => { + const eslint = initHbsESLint(); + const code = `
+ {{! template-lint-disable no-bare-strings }} + Hello world +
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('does not suppress unrelated rules when a specific rule is named', async () => { + const eslint = initHbsESLint(); + const code = `
+ {{! template-lint-disable ember/template-no-html-comments }} + Hello world +
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + // no-bare-strings should still fire since we only disabled no-html-comments + expect(resultErrors).toHaveLength(1); + expect(resultErrors[0].ruleId).toBe('ember/template-no-bare-strings'); + }); + + it('works with multiple disable comments in the same file', async () => { + const eslint = initHbsESLint(); + const code = `
+ {{! template-lint-disable }} + Hello world + {{! template-lint-disable }} + Another bare string +
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(0); + }); + + it('bare strings without disable comment still trigger errors', async () => { + const eslint = initHbsESLint(); + const code = `
+ Hello world +
`; + const results = await eslint.lintText(code, { filePath: 'my-template.hbs' }); + const resultErrors = results.flatMap((result) => result.messages); + expect(resultErrors).toHaveLength(1); + expect(resultErrors[0].ruleId).toBe('ember/template-no-bare-strings'); + }); +}); From ed812261ede60ca0c85a004823ae6a5e3430edb7 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:02:56 -0400 Subject: [PATCH 04/19] Fix polynomial regex (ReDoS) flagged by CodeQL Split the single regex with overlapping quantifiers into two separate patterns for mustache comments and mustache block comments. Each uses a specific character class ([\w\s,/-]*) that cannot cause polynomial backtracking. Also removes the -+$ cleanup regex since the block comment pattern now captures only the rules content without trailing dashes. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/processors/template-lint-disable.js | 43 ++++++++++++++----------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/processors/template-lint-disable.js b/lib/processors/template-lint-disable.js index 84b57996c5..04c6fc0fd7 100644 --- a/lib/processors/template-lint-disable.js +++ b/lib/processors/template-lint-disable.js @@ -3,12 +3,15 @@ const noop = require('ember-eslint-parser/noop'); /** - * Regex pattern for template-lint-disable mustache comments: + * Regex patterns for template-lint-disable mustache comments. + * Two separate patterns to avoid polynomial backtracking (ReDoS): * {{! template-lint-disable rule1 rule2 }} * {{!-- template-lint-disable rule1 rule2 --}} */ -const TEMPLATE_LINT_DISABLE_REGEX = - /{{!-*\s*template-lint-disable\s*([\s\S]*?)-*}}/g; +const MUSTACHE_COMMENT_REGEX = + /{{!\s+template-lint-disable([\w\s,/-]*)}}/g; +const MUSTACHE_BLOCK_COMMENT_REGEX = + /{{!--\s*template-lint-disable([\w\s,/-]*)--}}/g; // Store disable directives per file const fileDisableDirectives = new Map(); @@ -19,35 +22,37 @@ const fileDisableDirectives = new Map(); * * template-lint-disable means "disable next line" (like eslint-disable-next-line). */ -function parseDisableDirectives(text) { - const directives = []; - const lines = text.split('\n'); - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - TEMPLATE_LINT_DISABLE_REGEX.lastIndex = 0; +function collectMatches(line, lineIndex, directives) { + for (const regex of [MUSTACHE_COMMENT_REGEX, MUSTACHE_BLOCK_COMMENT_REGEX]) { + regex.lastIndex = 0; let match; - while ((match = TEMPLATE_LINT_DISABLE_REGEX.exec(line)) !== null) { + while ((match = regex.exec(line)) !== null) { const rulesPart = (match[1] || '').trim(); - // Strip trailing -- from mustache block comments - const cleaned = rulesPart.replace(/-+$/, '').trim(); - - const rules = cleaned - ? cleaned.split(/[\s,]+/).filter(Boolean) + const rules = rulesPart + ? rulesPart.split(/[\s,]+/).filter(Boolean) : []; // empty = disable all - // Comment is on line i+1 (1-indexed), next line is i+2. + // Comment is on line lineIndex+1 (1-indexed), next line is lineIndex+2. // In template ASTs, TextNodes can start on the same line as the comment // (e.g. the newline after {{! template-lint-disable }} is part of the // following TextNode), so we suppress both the comment line and the next. directives.push({ - commentLine: i + 1, - nextLine: i + 2, + commentLine: lineIndex + 1, + nextLine: lineIndex + 2, rules, }); } } +} + +function parseDisableDirectives(text) { + const directives = []; + const lines = text.split('\n'); + + for (let i = 0; i < lines.length; i++) { + collectMatches(lines[i], i, directives); + } return directives; } From 51ff450a7689f13e7ffc5a6046d5d8820d8fdac9 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:14:19 -0400 Subject: [PATCH 05/19] Fix self-lint errors - Reorder regex char class per unicorn/better-regex - Use for-of loop per unicorn/no-for-loop - Move initHbsESLint to outer scope per unicorn/consistent-function-scoping - Disable import/no-unresolved for ember-eslint-parser/hbs (valid export) Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/config/base.js | 2 +- lib/processors/template-lint-disable.js | 8 +-- .../rules-preprocessor/gjs-gts-parser-test.js | 50 +++++++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/config/base.js b/lib/config/base.js index b7f5f7f0c1..70524e2a74 100644 --- a/lib/config/base.js +++ b/lib/config/base.js @@ -1,6 +1,6 @@ const plugin = require('../index'); const emberEslintParser = require('ember-eslint-parser'); -const emberEslintParserHbs = require('ember-eslint-parser/hbs'); +const emberEslintParserHbs = require('ember-eslint-parser/hbs'); // eslint-disable-line import/no-unresolved module.exports = [ { diff --git a/lib/processors/template-lint-disable.js b/lib/processors/template-lint-disable.js index 04c6fc0fd7..5cc9eacc27 100644 --- a/lib/processors/template-lint-disable.js +++ b/lib/processors/template-lint-disable.js @@ -9,9 +9,9 @@ const noop = require('ember-eslint-parser/noop'); * {{!-- template-lint-disable rule1 rule2 --}} */ const MUSTACHE_COMMENT_REGEX = - /{{!\s+template-lint-disable([\w\s,/-]*)}}/g; + /{{!\s+template-lint-disable([\s\w,/-]*)}}/g; const MUSTACHE_BLOCK_COMMENT_REGEX = - /{{!--\s*template-lint-disable([\w\s,/-]*)--}}/g; + /{{!--\s*template-lint-disable([\s\w,/-]*)--}}/g; // Store disable directives per file const fileDisableDirectives = new Map(); @@ -50,8 +50,8 @@ function parseDisableDirectives(text) { const directives = []; const lines = text.split('\n'); - for (let i = 0; i < lines.length; i++) { - collectMatches(lines[i], i, directives); + for (const [i, line] of lines.entries()) { + collectMatches(line, i, directives); } return directives; diff --git a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js index 6ce852db27..b9965a7c9a 100644 --- a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js +++ b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js @@ -1022,33 +1022,33 @@ describe('supports template-lint-disable directive', () => { }); }); -describe('supports template-lint-disable directive in hbs files', () => { - function initHbsESLint() { - return new ESLint({ - ignore: false, - useEslintrc: false, - plugins: { ember: plugin }, - overrideConfig: { - root: true, - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - plugins: ['ember'], - overrides: [ - { - files: ['**/*.hbs'], - parser: hbsParser, - processor: 'ember/noop', - rules: { - 'ember/template-no-bare-strings': 'error', - }, - }, - ], +function initHbsESLint() { + return new ESLint({ + ignore: false, + useEslintrc: false, + plugins: { ember: plugin }, + overrideConfig: { + root: true, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', }, - }); - } + plugins: ['ember'], + overrides: [ + { + files: ['**/*.hbs'], + parser: hbsParser, + processor: 'ember/noop', + rules: { + 'ember/template-no-bare-strings': 'error', + }, + }, + ], + }, + }); +} +describe('supports template-lint-disable directive in hbs files', () => { it('disables all rules on the next line with mustache comment', async () => { const eslint = initHbsESLint(); const code = `
From 83cbfa300da8126b9e48b34b50e1c5e710a33441 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:20:58 -0400 Subject: [PATCH 06/19] Address PR review feedback - Revert recommended.mjs to original (cannot change recommended config) - Keep noop processor unchanged; export template-lint-disable as a separate processor (ember/template-lint-disable) to avoid breaking existing gjs/gts users - Wire hbs files to use the new processor by default in base configs (hbs had no prior config, so this is additive) - Move hbs tests to a separate hbs-parser-test.js file - Update gjs template-lint-disable tests to explicitly use the new processor via initESLintWithTemplateLintDisable() Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/config-legacy/base.js | 2 +- lib/config/base.js | 2 +- lib/index.js | 4 +- lib/recommended.mjs | 17 -- .../rules-preprocessor/gjs-gts-parser-test.js | 163 ++++-------------- .../lib/rules-preprocessor/hbs-parser-test.js | 127 ++++++++++++++ 6 files changed, 166 insertions(+), 149 deletions(-) create mode 100644 tests/lib/rules-preprocessor/hbs-parser-test.js diff --git a/lib/config-legacy/base.js b/lib/config-legacy/base.js index 1dabbaf462..26472a9bdc 100644 --- a/lib/config-legacy/base.js +++ b/lib/config-legacy/base.js @@ -26,7 +26,7 @@ module.exports = { { files: ['**/*.hbs'], parser: 'ember-eslint-parser/hbs', - processor: 'ember/noop', + processor: 'ember/template-lint-disable', }, ], }; diff --git a/lib/config/base.js b/lib/config/base.js index 70524e2a74..143077f700 100644 --- a/lib/config/base.js +++ b/lib/config/base.js @@ -23,6 +23,6 @@ module.exports = [ languageOptions: { parser: emberEslintParserHbs, }, - processor: 'ember/noop', + processor: 'ember/template-lint-disable', }, ]; diff --git a/lib/index.js b/lib/index.js index 7bb1b2110e..638e4d840b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,7 @@ 'use strict'; const requireIndex = require('requireindex'); +const noop = require('ember-eslint-parser/noop'); const templateLintDisableProcessor = require('./processors/template-lint-disable'); const pkg = require('../package.json'); // eslint-disable-line import/extensions @@ -16,6 +17,7 @@ module.exports = { }, processors: { // https://eslint.org/docs/developer-guide/working-with-plugins#file-extension-named-processor - noop: templateLintDisableProcessor, + noop, + 'template-lint-disable': templateLintDisableProcessor, }, }; diff --git a/lib/recommended.mjs b/lib/recommended.mjs index 029c15c953..5daaf29ae4 100644 --- a/lib/recommended.mjs +++ b/lib/recommended.mjs @@ -3,11 +3,9 @@ import baseRules from './recommended-rules.js'; import gjsRules from './recommended-rules-gjs.js'; import gtsRules from './recommended-rules-gts.js'; import emberParser from 'ember-eslint-parser'; -import emberParserHbs from 'ember-eslint-parser/hbs'; export const plugin = emberPlugin; export const parser = emberParser; -export const hbsParser = emberParserHbs; export const base = { name: 'ember:base', @@ -55,29 +53,14 @@ export const gts = { }, }; -export const hbs = { - name: 'ember:hbs', - plugins: { ember: emberPlugin }, - files: ['**/*.hbs'], - languageOptions: { - parser: emberParserHbs, - }, - processor: 'ember/noop', - rules: { - ...base.rules, - }, -}; - export default { // Helpful utility exports plugin, parser, - hbsParser, // Recommended config sets configs: { base, gjs, gts, - hbs, }, }; diff --git a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js index b9965a7c9a..a1ccfcc2cf 100644 --- a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js +++ b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js @@ -13,7 +13,6 @@ const { writeFileSync, readFileSync } = require('node:fs'); const { join } = require('node:path'); const gjsGtsParser = require.resolve('ember-eslint-parser'); -const hbsParser = require.resolve('ember-eslint-parser/hbs'); /** * Helper function which creates ESLint instance with enabled/disabled autofix feature. @@ -888,9 +887,35 @@ describe('multiple tokens in same file', () => { }); }); +function initESLintWithTemplateLintDisable() { + return new ESLint({ + ignore: false, + useEslintrc: false, + plugins: { ember: plugin }, + overrideConfig: { + root: true, + env: { + browser: true, + }, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + parser: gjsGtsParser, + plugins: ['ember'], + processor: 'ember/template-lint-disable', + rules: { + 'no-undef': 'error', + 'no-unused-vars': 'error', + quotes: ['error', 'single'], + }, + }, + }); +} + describe('supports template-lint-disable directive', () => { it('disables all rules on the next line with mustache comment', async () => { - const eslint = initESLint(); + const eslint = initESLintWithTemplateLintDisable(); const code = `