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: [
+ "{{#link-to 'routeName' prop}}Link text{{/link-to}}",
+ "{{#link-to 'routeName'}}Link text{{/link-to}}",
+ ],
+ invalid: [
+ {
+ code: "{{link-to 'Link text' 'routeName'}}",
+ output: "{{#link-to 'routeName'}}Link text{{/link-to}}",
+ errors: [
+ {
+ message: 'The inline form of link-to is not allowed. Use the block form instead.',
+ },
+ ],
+ },
+ {
+ code: "{{link-to 'Link text' 'routeName' one two}}",
+ output: "{{#link-to 'routeName' one two}}Link text{{/link-to}}",
+ errors: [
+ {
+ message: 'The inline form of link-to is not allowed. Use the block form instead.',
+ },
+ ],
+ },
+ {
+ code: "{{link-to (concat 'Hello' @username) 'routeName' one two}}",
+ output:
+ "{{#link-to 'routeName' one two}}{{concat 'Hello' @username}}{{/link-to}}",
+ errors: [
+ {
+ message: 'The inline form of link-to is not allowed. Use the block form instead.',
+ },
+ ],
+ },
+ {
+ code: "{{link-to 1234 'routeName' one two}}",
+ output: null,
+ errors: [
+ {
+ message: 'The inline form of link-to is not allowed. Use the block form instead.',
+ },
+ ],
+ },
+ ],
+});