From cb5fefe02367fcb0d81e7ded6ba3df643783ce5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Mon, 13 Apr 2026 13:23:40 +0200 Subject: [PATCH 1/2] Fix template-no-inline-linkto false positive in GJS/GTS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The angle-bracket visitor checks `node.tag === 'LinkTo'` by bare name, which causes two GJS/GTS bugs (the same class as PR #2652 fixed for template-no-link-to-tagname): - A user-authored `` from any non-`@ember/routing` source is falsely flagged when childless. - A renamed framework import (`import { LinkTo as Link } from '@ember/routing'`) used as `` is missed entirely. Adopt the same `@ember/routing` import-tracker pattern: in strict mode, only flag elements whose tag is in the tracked map (covers renamed imports). HBS path uses bare-name match (no imports possible). Curly `{{link-to ...}}` handler is gated to HBS only — `link-to` is not a valid JS identifier, so it cannot be a user binding in strict mode, and the strict-mode compiler would already reject the source. Tests: 21 (was 14) — adds 4 new valid GJS/GTS cases (no-import bare `` in .gjs and .gts, with-import block-form `...`, with-renamed-import block-form `...`) and 3 new invalid cases (with-import childless `` in .gjs and .gts, renamed childless ``). Docs updated to document the strict-mode behavior. --- docs/rules/template-no-inline-linkto.md | 13 ++++++ lib/rules/template-no-inline-linkto.js | 43 +++++++++++++++++- tests/lib/rules/template-no-inline-linkto.js | 47 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/docs/rules/template-no-inline-linkto.md b/docs/rules/template-no-inline-linkto.md index 11fbd15df2..79ebcc8345 100644 --- a/docs/rules/template-no-inline-linkto.md +++ b/docs/rules/template-no-inline-linkto.md @@ -62,6 +62,19 @@ Examples of **correct** code for this rule: ``` +```gjs +// User-authored `` (not from `@ember/routing`) is not flagged in +// strict mode, even when childless. +import LinkTo from './my-link-to-component'; + +``` + +## Strict-mode behavior + +In `.gjs`/`.gts` strict mode, `` only refers to Ember's router link when explicitly imported from `@ember/routing` (this also covers renamed imports such as `import { LinkTo as Link } from '@ember/routing'`). Without that import, `` is treated as a user-authored component and the rule does not fire. The curly `{{link-to ...}}` form is unreachable in strict mode (`link-to` cannot be a JS identifier) and the autofix is skipped there. + ## References - [eslint-plugin-ember template-no-inline-link-to](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-inline-link-to.md) diff --git a/lib/rules/template-no-inline-linkto.js b/lib/rules/template-no-inline-linkto.js index 4faed353be..69460a39db 100644 --- a/lib/rules/template-no-inline-linkto.js +++ b/lib/rules/template-no-inline-linkto.js @@ -23,10 +23,42 @@ module.exports = { create(context) { const sourceCode = context.sourceCode; + const filename = context.filename; + const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts'); + + // In HBS, `` always refers to Ember's router link component. + // In GJS/GTS, `` must be explicitly imported from '@ember/routing' + // (and may be renamed, e.g. `import { LinkTo as Link } from '@ember/routing'`). + // local alias → true + const importedLinkComponents = new Map(); + + function isLinkToComponent(node) { + if (isStrictMode) { + return importedLinkComponents.has(node.tag); + } + return node.tag === 'LinkTo'; + } return { + ImportDeclaration(node) { + if (!isStrictMode) { + return; + } + if (node.source.value !== '@ember/routing') { + return; + } + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'LinkTo') { + importedLinkComponents.set(specifier.local.name, true); + } + } + }, + GlimmerElementNode(node) { - if (node.tag === 'LinkTo' && node.children && node.children.length === 0) { + if (!isLinkToComponent(node)) { + return; + } + if (node.children && node.children.length === 0) { context.report({ node, messageId: 'noInlineLinkTo', @@ -34,8 +66,15 @@ module.exports = { } }, - // {{link-to 'text' 'route'}} inline curly form + // {{link-to 'text' 'route'}} inline curly form — HBS-only. + // The `link-to` kebab path is not a valid JS identifier, so it cannot + // be a user binding in strict mode; the strict-mode compiler would + // already reject the source. Skip the curly handler in strict mode to + // avoid emitting a fix that produces also-broken `{{#link-to ...}}`. GlimmerMustacheStatement(node) { + if (isStrictMode) { + return; + } if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'link-to') { const titleNode = node.params[0]; const isFixable = diff --git a/tests/lib/rules/template-no-inline-linkto.js b/tests/lib/rules/template-no-inline-linkto.js index ffe73743af..ee709572e8 100644 --- a/tests/lib/rules/template-no-inline-linkto.js +++ b/tests/lib/rules/template-no-inline-linkto.js @@ -27,6 +27,30 @@ ruleTester.run('template-no-inline-linkto', rule, { ``, + + // GJS/GTS: without an `@ember/routing` import, `` is a + // user-authored component — flagging it would corrupt the user's intent. + { + filename: 'test.gjs', + code: '', + }, + { + filename: 'test.gts', + code: '', + }, + + // GJS/GTS with the canonical `@ember/routing` import: still allow when + // the LinkTo has children (block form). + { + filename: 'test.gjs', + code: 'import { LinkTo } from \'@ember/routing\';\n', + }, + + // Renamed import: also allowed when the renamed LinkTo has children. + { + filename: 'test.gjs', + code: 'import { LinkTo as Link } from \'@ember/routing\';\n', + }, ], invalid: [ @@ -66,6 +90,29 @@ ruleTester.run('template-no-inline-linkto', rule, { }, ], }, + + // GJS/GTS with `@ember/routing` import: childless LinkTo is flagged. + { + filename: 'test.gjs', + code: 'import { LinkTo } from \'@ember/routing\';\n', + output: null, + errors: [{ messageId: 'noInlineLinkTo', type: 'GlimmerElementNode' }], + }, + { + filename: 'test.gts', + code: 'import { LinkTo } from \'@ember/routing\';\n', + output: null, + errors: [{ messageId: 'noInlineLinkTo', type: 'GlimmerElementNode' }], + }, + + // Renamed import: childless `` is flagged because it resolves to + // the framework `LinkTo`. + { + filename: 'test.gjs', + code: 'import { LinkTo as Link } from \'@ember/routing\';\n', + output: null, + errors: [{ messageId: 'noInlineLinkTo', type: 'GlimmerElementNode' }], + }, ], }); From c689d14e18d1022cb4ffda2585eee4c7cfbad4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Mon, 13 Apr 2026 13:37:57 +0200 Subject: [PATCH 2/2] Review feedback: Set, inline sourceCode, document namespace-import limitation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - importedLinkComponents: Map → Set - Move const sourceCode inside the curly-fixer (only user) - Comment: namespace imports (import * as routing from '@ember/routing') are not tracked; dotted tag paths are not a realistic usage pattern. --- lib/rules/template-no-inline-linkto.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/rules/template-no-inline-linkto.js b/lib/rules/template-no-inline-linkto.js index 69460a39db..6aef35ba99 100644 --- a/lib/rules/template-no-inline-linkto.js +++ b/lib/rules/template-no-inline-linkto.js @@ -22,15 +22,16 @@ module.exports = { }, create(context) { - const sourceCode = context.sourceCode; const filename = context.filename; const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts'); // In HBS, `` always refers to Ember's router link component. // In GJS/GTS, `` must be explicitly imported from '@ember/routing' // (and may be renamed, e.g. `import { LinkTo as Link } from '@ember/routing'`). - // local alias → true - const importedLinkComponents = new Map(); + // Limitation: namespace imports (`import * as routing from '@ember/routing'` + // → ``) are not tracked — dotted tag paths would need a + // separate match and are not a realistic usage pattern for this component. + const importedLinkComponents = new Set(); function isLinkToComponent(node) { if (isStrictMode) { @@ -49,7 +50,7 @@ module.exports = { } for (const specifier of node.specifiers) { if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'LinkTo') { - importedLinkComponents.set(specifier.local.name, true); + importedLinkComponents.add(specifier.local.name); } } }, @@ -76,6 +77,7 @@ module.exports = { return; } if (node.path?.type === 'GlimmerPathExpression' && node.path.original === 'link-to') { + const sourceCode = context.sourceCode; const titleNode = node.params[0]; const isFixable = titleNode &&