From 365138c12de76fd75123b47b6ee82777586b572b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Thu, 12 Mar 2026 23:13:03 +0100 Subject: [PATCH 1/6] Fix template-no-autofocus-attribute: add MustacheStatement, remove autofix - Add GlimmerMustacheStatement visitor for {{input autofocus=true}} - Change fixable to null (matching original's deliberate design choice) - Add test cases for mustache form, div, h1, input autofocus="autofocus" --- lib/rules/template-no-autofocus-attribute.js | 27 ++++---- .../rules/template-no-autofocus-attribute.js | 61 +++++++++++++++++-- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/lib/rules/template-no-autofocus-attribute.js b/lib/rules/template-no-autofocus-attribute.js index b0d775d14a..db3465c1be 100644 --- a/lib/rules/template-no-autofocus-attribute.js +++ b/lib/rules/template-no-autofocus-attribute.js @@ -9,7 +9,7 @@ module.exports = { strictGts: true, url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-autofocus-attribute.md', }, - fixable: 'code', + fixable: null, schema: [], messages: { noAutofocus: @@ -26,20 +26,19 @@ module.exports = { context.report({ node: autofocusAttr, messageId: 'noAutofocus', - fix(fixer) { - // Remove the attribute including preceding whitespace - const sourceCode = context.sourceCode; - const text = sourceCode.getText(); - const attrStart = autofocusAttr.range[0]; - const attrEnd = autofocusAttr.range[1]; - - let removeStart = attrStart; - while (removeStart > 0 && /\s/.test(text[removeStart - 1])) { - removeStart--; - } + }); + } + }, - return fixer.removeRange([removeStart, attrEnd]); - }, + GlimmerMustacheStatement(node) { + if (!node.hash || !node.hash.pairs) { + return; + } + const autofocusPair = node.hash.pairs.find((pair) => pair.key === 'autofocus'); + if (autofocusPair) { + context.report({ + node: autofocusPair, + messageId: 'noAutofocus', }); } }, diff --git a/tests/lib/rules/template-no-autofocus-attribute.js b/tests/lib/rules/template-no-autofocus-attribute.js index afee5842d2..d3e6b38dac 100644 --- a/tests/lib/rules/template-no-autofocus-attribute.js +++ b/tests/lib/rules/template-no-autofocus-attribute.js @@ -29,9 +29,6 @@ ruleTester.run('template-no-autofocus-attribute', rule, { code: ``, - output: ``, errors: [ { message: @@ -44,11 +41,65 @@ ruleTester.run('template-no-autofocus-attribute', rule, { code: ``, - output: ``, + output: ``, errors: [ { messageId: 'noAutofocus', @@ -92,6 +96,10 @@ ruleTester.run('template-no-autofocus-attribute', rule, {

`, + output: ``, errors: [ { messageId: 'noAutofocus', @@ -103,6 +111,9 @@ ruleTester.run('template-no-autofocus-attribute', rule, { code: ``, + output: ``, errors: [ { messageId: 'noAutofocus', From 465571f9b6155def836c42a0b86414817f744f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Fri, 13 Mar 2026 07:58:41 +0100 Subject: [PATCH 4/6] Add output: null to test cases for consistent-output lint rule --- .claude/settings.local.json | 34 ++ FIX.md | 102 ++++ REVIEW.md | 470 ++++++++++++++++++ .../rules/template-no-autofocus-attribute.js | 2 + 4 files changed, 608 insertions(+) create mode 100644 .claude/settings.local.json create mode 100644 FIX.md create mode 100644 REVIEW.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..9138fee884 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,34 @@ +{ + "permissions": { + "allow": [ + "Bash(for branch:*)", + "Read(//Users/johanrd/fremby/ember-template-lint-main/.git/**)", + "Read(//Users/johanrd/fremby/ember-template-lint-main/**)", + "Bash(gh api:*)", + "Bash(git checkout:*)", + "Bash(npx jest:*)", + "Bash(cat jest.config.js 2>/dev/null || cat jest.config.cjs 2>/dev/null || echo \"no jest config\" ; ls package.json && node -e \"const p=require\\('./package.json'\\); console.log\\(JSON.stringify\\(p.scripts?.test || 'no test script'\\)\\)\")", + "Bash(npx vitest:*)", + "Bash(git add:*)", + "WebFetch(domain:www.w3.org)", + "Bash(git diff:*)", + "Bash(node -e \"\nconst regexp = /\\(.*\\\\s\\)?noopener\\\\s\\(.*\\\\s\\)?noreferrer\\(\\\\s.*\\)?|\\(.*\\\\s\\)?noreferrer\\\\s\\(.*\\\\s\\)?noopener\\(\\\\s.*\\)?/;\nconsole.log\\('noopener noreferrer:', regexp.test\\('noopener noreferrer'\\)\\);\nconsole.log\\('noreferrer noopener:', regexp.test\\('noreferrer noopener'\\)\\);\nconsole.log\\('noopener', regexp.test\\('noopener'\\)\\);\nconsole.log\\('noreferrer', regexp.test\\('noreferrer'\\)\\);\nconsole.log\\('xnoopener noreferrer', regexp.test\\('xnoopener noreferrer'\\)\\);\nconsole.log\\('noopenerx noreferrer', regexp.test\\('noopenerx noreferrer'\\)\\);\nconsole.log\\('nofollow noopener noreferrer:', regexp.test\\('nofollow noopener noreferrer'\\)\\);\n\")", + "Bash(node -e \"\nconst regexp = /\\(.*\\\\s\\)?noopener\\\\s\\(.*\\\\s\\)?noreferrer\\(\\\\s.*\\)?|\\(.*\\\\s\\)?noreferrer\\\\s\\(.*\\\\s\\)?noopener\\(\\\\s.*\\)?/;\n// Test edge cases\nconsole.log\\('foonoopener noreferrer:', regexp.test\\('foonoopener noreferrer'\\)\\); \nconsole.log\\('noopener foonoreferrer:', regexp.test\\('noopener foonoreferrer'\\)\\); \nconsole.log\\('foonoreferrer noopener:', regexp.test\\('foonoreferrer noopener'\\)\\); \nconsole.log\\('noreferrer foonoopener:', regexp.test\\('noreferrer foonoopener'\\)\\); \n\")", + "Bash(node -e \"\nconst AllowedPrefix = /[a-z]/;\n// Original uses: !AllowedPrefix.test\\(firstChar\\) || isReserved\n// So if first char is lowercase AND not reserved => allowed\n// If first char is NOT lowercase OR is reserved => flagged\n\n// Check _ prefix\nconsole.log\\('_ matches:', AllowedPrefix.test\\('_'\\)\\); // false => flagged\nconsole.log\\('A matches:', AllowedPrefix.test\\('A'\\)\\); // false => flagged\nconsole.log\\('a matches:', AllowedPrefix.test\\('a'\\)\\); // true => allowed\nconsole.log\\('1 matches:', AllowedPrefix.test\\('1'\\)\\); // false => flagged\n\")", + "Bash(node -e \"\n// The 'token' check does: convertToStrings\\(permittedValues\\).includes\\(value\\)\n// This is case-sensitive!\n// But 'tokenlist' does: token.toLowerCase\\(\\)\n// So tokenlist is case-insensitive but token is case-sensitive\n// Let's check aria-checked token values\nconsole.log\\('aria-checked values typically: true, false, mixed'\\);\n\n// The 'token' type does NOT lowercase the value before checking\n// So aria-checked='True' would fail, but probably should pass per WAI-ARIA spec\n// WAI-ARIA 1.2 says values are case-insensitive for 'token' type too\n\")", + "Bash(node -e \"\n// Check the original: token type doesn't do case-insensitive matching\n// but tokenlist does .toLowerCase\\(\\) on tokens\n\n// Token type check \\(line 90\\):\n// return typeof value === 'string' && convertToStrings\\(permittedValues\\).includes\\(value\\);\n// ^ case-sensitive\n\n// Tokenlist type check \\(line 95\\):\n// value.split\\(' '\\).every\\(\\(token\\) => permittedValues.includes\\(token.toLowerCase\\(\\)\\)\\)\n// ^ case-insensitive via toLowerCase\n\n// This is inconsistent! aria-live='POLITE' would fail \\(token type\\)\n// but aria-relevant='ADDITIONS REMOVALS' might pass \\(tokenlist type\\)\n\n// However... WAI-ARIA says attribute values are case-sensitive for token types\n// Actually the HTML spec says attribute values are generally case-insensitive for enumerated types\n// but ARIA values in practice must be lowercase. So this might actually be correct.\nconsole.log\\('tokenlist is case-insensitive, token is case-sensitive'\\);\nconsole.log\\('This is a potential inconsistency in the original, but arguable'\\);\n\")", + "Bash(cd /Users/johanrd/fremby/ember-template-lint-main && node -e \"\nimport\\('aria-query'\\).then\\(\\({aria}\\) => {\n const checked = aria.get\\('aria-checked'\\);\n console.log\\('aria-checked:', JSON.stringify\\(checked\\)\\);\n const relevant = aria.get\\('aria-relevant'\\);\n console.log\\('aria-relevant:', JSON.stringify\\(relevant\\)\\);\n const live = aria.get\\('aria-live'\\);\n console.log\\('aria-live:', JSON.stringify\\(live\\)\\);\n}\\);\n\")", + "Bash(node -e \"\nconst { aria } = require\\('aria-query'\\);\nconst checked = aria.get\\('aria-checked'\\);\nconsole.log\\('aria-checked:', JSON.stringify\\(checked\\)\\);\nconst relevant = aria.get\\('aria-relevant'\\);\nconsole.log\\('aria-relevant:', JSON.stringify\\(relevant\\)\\);\nconst live = aria.get\\('aria-live'\\);\nconsole.log\\('aria-live:', JSON.stringify\\(live\\)\\);\n\")", + "Bash(node -e \"\n// token: convertToStrings\\(permittedValues\\).includes\\(value\\) => case-SENSITIVE\n// tokenlist: permittedValues.includes\\(token.toLowerCase\\(\\)\\) => case-INSENSITIVE\n\n// So aria-live='POLITE' would fail \\(token, case-sensitive\\) -- strict but reasonable\n// But aria-relevant='ADDITIONS' would pass \\(tokenlist, case-insensitive\\) -- lenient\n\n// Per WAI-ARIA spec: attribute values for tokens should be compared case-insensitively\n// in HTML context. See: https://www.w3.org/TR/wai-aria-1.2/#propcharacteristic_value\n// 'Value type: token' - The token value is case-sensitive when specified in the\n// host language. But HTML is case-insensitive for attribute values.\n\n// This is an inconsistency in the original but subtle enough that it's arguable.\n// The main concern is: token is strict \\(case-sensitive\\), tokenlist is lenient.\nconsole.log\\('Inconsistency confirmed but arguable'\\);\n\")", + "Bash(node -e \"\n// ORIGINAL no-invalid-role.js:\n// Line 263: ['presentation', 'none'].includes\\(roleValue\\) <-- CASE-SENSITIVE \n// Line 272: !VALID_ROLES.has\\(roleValue.toLowerCase\\(\\)\\) <-- CASE-INSENSITIVE\n\n// So 'Presentation' would:\n// 1. NOT match the presentation/none check \\(line 263\\) -- allowed through\n// 2. .toLowerCase\\(\\) => 'presentation' => VALID_ROLES.has\\('presentation'\\) => true => not flagged\n\n// Result: 'Presentation' is silently accepted without checking against SEMANTIC_ELEMENTS\n// This is the confirmed case-sensitivity bug\n\n// BRANCH fix/2475:\n// Uses roleLower = role.toLowerCase\\(\\) for BOTH checks\n// So 'Presentation' on