From 352dad51ba1519e67da5bee29ee7b94452739692 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:45:22 -0500 Subject: [PATCH] Extract rule: template-inline-link-to --- README.md | 13 ++-- docs/rules/template-inline-link-to.md | 36 ++++++++++ lib/rules/template-inline-link-to.js | 76 ++++++++++++++++++++++ tests/lib/rules/template-inline-link-to.js | 53 +++++++++++++++ 4 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 docs/rules/template-inline-link-to.md create mode 100644 lib/rules/template-inline-link-to.js create mode 100644 tests/lib/rules/template-inline-link-to.js diff --git a/README.md b/README.md index 1997fcd7ea..eeedee8cd4 100644 --- a/README.md +++ b/README.md @@ -315,12 +315,13 @@ rules in templates can be disabled with eslint directives with mustache or html ### Stylistic Issues -| Name | Description | 💼 | 🔧 | 💡 | -| :--------------------------------------------------------- | :------------------------------------------------ | :- | :- | :- | -| [order-in-components](docs/rules/order-in-components.md) | enforce proper order of properties in components | | 🔧 | | -| [order-in-controllers](docs/rules/order-in-controllers.md) | enforce proper order of properties in controllers | | 🔧 | | -| [order-in-models](docs/rules/order-in-models.md) | enforce proper order of properties in models | | 🔧 | | -| [order-in-routes](docs/rules/order-in-routes.md) | enforce proper order of properties in routes | | 🔧 | | +| Name | Description | 💼 | 🔧 | 💡 | +| :--------------------------------------------------------------- | :-------------------------------------------------- | :- | :- | :- | +| [order-in-components](docs/rules/order-in-components.md) | enforce proper order of properties in components | | 🔧 | | +| [order-in-controllers](docs/rules/order-in-controllers.md) | enforce proper order of properties in controllers | | 🔧 | | +| [order-in-models](docs/rules/order-in-models.md) | enforce proper order of properties in models | | 🔧 | | +| [order-in-routes](docs/rules/order-in-routes.md) | enforce proper order of properties in routes | | 🔧 | | +| [template-inline-link-to](docs/rules/template-inline-link-to.md) | disallow inline link-to, use the block form instead | | 🔧 | | ### Testing diff --git a/docs/rules/template-inline-link-to.md b/docs/rules/template-inline-link-to.md new file mode 100644 index 0000000000..3deb6c5d72 --- /dev/null +++ b/docs/rules/template-inline-link-to.md @@ -0,0 +1,36 @@ +# ember/template-inline-link-to + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +Disallows the inline form of the `link-to` component and enforces the block form instead. + +Ember's `link-to` component has both an inline form and a block form. This rule forbids the inline form. + +## Examples + +This rule **forbids** the following (inline form): + +```hbs +{{link-to 'Link text' 'routeName' prop1 prop2}} +``` + +This rule **allows** the following (block form): + +```hbs +{{#link-to 'routeName' prop1 prop2}}Link text{{/link-to}} +``` + +## Rationale + +The block form is a little longer but has advantages over the inline form: + +- It maps closer to the use of HTML anchor tags which wrap their inner content. +- It provides an obvious way for developers to put nested markup and components inside of their link. +- The block form's argument order is more direct: "link to route". The inline form's argument order is somewhat ambiguous (link text then link target). This is opposite of the order in HTML (`href` then link text). + +## References + +- [Ember guides/routing](https://guides.emberjs.com/release/routing/linking-between-routes/#toc_the-linkto--component) +- [Ember api/LinkTo component](https://api.emberjs.com/ember/release/classes/Ember.Templates.components/methods/LinkTo?anchor=LinkTo) diff --git a/lib/rules/template-inline-link-to.js b/lib/rules/template-inline-link-to.js new file mode 100644 index 0000000000..7284834ca8 --- /dev/null +++ b/lib/rules/template-inline-link-to.js @@ -0,0 +1,76 @@ +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow inline link-to, use the block form instead', + category: 'Stylistic Issues', + strictGjs: true, + strictGts: true, + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-inline-link-to.md', + }, + fixable: 'code', + schema: [], + messages: {}, + }, + + create(context) { + const MESSAGE = 'The inline form of link-to is not allowed. Use the block form instead.'; + + return { + GlimmerMustacheStatement(node) { + if ( + node.path && + node.path.type === 'GlimmerPathExpression' && + node.path.original === 'link-to' + ) { + const titleNode = node.params?.[0]; + const isFixable = + titleNode && + (titleNode.type === 'GlimmerSubExpression' || + titleNode.type === 'GlimmerStringLiteral'); + + context.report({ + node, + message: MESSAGE, + fix: isFixable + ? (fixer) => { + const sourceCode = context.getSourceCode(); + const text = sourceCode.getText(node); + + // Convert {{link-to 'text' 'route' ...}} to {{#link-to 'route' ...}}text{{/link-to}} + let blockBody; + if (titleNode.type === 'GlimmerSubExpression') { + // {{link-to (helper ...) 'route'}} -> {{#link-to 'route'}}{{helper ...}}{{/link-to}} + const helperText = sourceCode.getText(titleNode); + blockBody = helperText.replace(/^\(/, '{{').replace(/\)$/, '}}'); + } else if (titleNode.type === 'GlimmerStringLiteral') { + // {{link-to 'text' 'route'}} -> {{#link-to 'route'}}text{{/link-to}} + blockBody = titleNode.value; + } + + // Get remaining params (everything after the first param) + const remainingParams = node.params.slice(1); + const remainingParamsText = remainingParams + .map((param) => sourceCode.getText(param)) + .join(' '); + + // Get hash if present + const hashText = + node.hash && node.hash.pairs && node.hash.pairs.length > 0 + ? ` ${node.hash.pairs + .map((pair) => `${pair.key}=${sourceCode.getText(pair.value)}`) + .join(' ')}` + : ''; + + const fixedText = `{{#link-to ${remainingParamsText}${hashText}}}${blockBody}{{/link-to}}`; + + return fixer.replaceText(node, fixedText); + } + : null, + }); + } + }, + }; + }, +}; diff --git a/tests/lib/rules/template-inline-link-to.js b/tests/lib/rules/template-inline-link-to.js new file mode 100644 index 0000000000..6d13cedcf2 --- /dev/null +++ b/tests/lib/rules/template-inline-link-to.js @@ -0,0 +1,53 @@ +const rule = require('../../../lib/rules/template-inline-link-to'); +const RuleTester = require('eslint').RuleTester; + +const ruleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +ruleTester.run('template-inline-link-to', rule, { + valid: [ + "", + "", + ], + invalid: [ + { + code: "", + output: "", + errors: [ + { + message: 'The inline form of link-to is not allowed. Use the block form instead.', + }, + ], + }, + { + code: "", + output: "", + errors: [ + { + message: 'The inline form of link-to is not allowed. Use the block form instead.', + }, + ], + }, + { + code: "", + output: + "", + errors: [ + { + message: 'The inline form of link-to is not allowed. Use the block form instead.', + }, + ], + }, + { + code: "", + output: null, + errors: [ + { + message: 'The inline form of link-to is not allowed. Use the block form instead.', + }, + ], + }, + ], +});