Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/rules/template-no-inline-linkto.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ Examples of **correct** code for this rule:
</template>
```

```gjs
// User-authored `<LinkTo>` (not from `@ember/routing`) is not flagged in
// strict mode, even when childless.
import LinkTo from './my-link-to-component';
<template>
<LinkTo />
</template>
```

## Strict-mode behavior

In `.gjs`/`.gts` strict mode, `<LinkTo>` 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, `<LinkTo>` 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)
47 changes: 44 additions & 3 deletions lib/rules/template-no-inline-linkto.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,62 @@ module.exports = {
},

create(context) {
const sourceCode = context.sourceCode;
const filename = context.filename;
const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');

// In HBS, `<LinkTo>` always refers to Ember's router link component.
// In GJS/GTS, `<LinkTo>` 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'`
// → `<routing.LinkTo />`) 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',
});
}
},

// {{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 &&
Expand Down
47 changes: 47 additions & 0 deletions tests/lib/rules/template-no-inline-linkto.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@ ruleTester.run('template-no-inline-linkto', rule, {
`<template>
<div></div>
</template>`,

// GJS/GTS: without an `@ember/routing` import, `<LinkTo>` is a
// user-authored component — flagging it would corrupt the user's intent.
{
filename: 'test.gjs',
code: '<template><LinkTo @route="index" /></template>',
},
{
filename: 'test.gts',
code: '<template><LinkTo /></template>',
},

// 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<template><LinkTo @route="index">Home</LinkTo></template>',
},

// Renamed import: also allowed when the renamed LinkTo has children.
{
filename: 'test.gjs',
code: 'import { LinkTo as Link } from \'@ember/routing\';\n<template><Link @route="index">Home</Link></template>',
},
],

invalid: [
Expand Down Expand Up @@ -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<template><LinkTo @route="index" /></template>',
output: null,
errors: [{ messageId: 'noInlineLinkTo', type: 'GlimmerElementNode' }],
},
{
filename: 'test.gts',
code: 'import { LinkTo } from \'@ember/routing\';\n<template><LinkTo @route="contact"></LinkTo></template>',
output: null,
errors: [{ messageId: 'noInlineLinkTo', type: 'GlimmerElementNode' }],
},

// Renamed import: childless `<Link>` is flagged because it resolves to
// the framework `LinkTo`.
{
filename: 'test.gjs',
code: 'import { LinkTo as Link } from \'@ember/routing\';\n<template><Link @route="index" /></template>',
output: null,
errors: [{ messageId: 'noInlineLinkTo', type: 'GlimmerElementNode' }],
},
],
});

Expand Down
Loading