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..6aef35ba99 100644 --- a/lib/rules/template-no-inline-linkto.js +++ b/lib/rules/template-no-inline-linkto.js @@ -22,11 +22,44 @@ 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'`). + // 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) { + 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.add(specifier.local.name); + } + } + }, + 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,9 +67,17 @@ 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 sourceCode = context.sourceCode; const titleNode = node.params[0]; const isFixable = titleNode && 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' }], + }, ], });