Repository: ember-template-lint/ember-template-lint
Pull request #753 (2019-06-24 20:44:51)

Add `no-unnecessary-component-helper` rule
@Turbo87 @rwjblue 

Turbo87: is that officially supported? I don't think I've ever seen that documented anywhere 🤔 
rwjblue: > is that officially supported?

Dunno, but _I_ consider it supported (and know of a number of projects that rely on it). There are also tests in ember-cli/ember-resolver that ensure it continues to work.

FWIW, I'm **totally** happy to just decide that if you need those exotic types of things that you'd have to disable this rule. I just wanted to make sure we are explicit about it (and possibly mention it in the docs for the rule).

Patch:
diff --git a/docs/rule/no-unnecessary-component-helper.md b/docs/rule/no-unnecessary-component-helper.md
new file mode 100644
index 00000000..b1a2ff0f
--- /dev/null
+++ b/docs/rule/no-unnecessary-component-helper.md
@@ -0,0 +1,35 @@
+## no-unnecessary-component-helper
+
+The `component` template helper can be used to dynamically pick the component being rendered based on the provided property. But if the component name is passed as a string because it's already known, then the component should be invoked directly, instead of using the `component` helper.
+
+### Examples
+
+This rule **forbids** the following:
+
+```hbs
+{{component "my-component"}}
+```
+
+This rule **allows** the following:
+
+```hbs
+{{component SOME_COMPONENT_NAME}}
+```
+
+```hbs
+{{! the `component` helper is needed to invoke this }}
+{{component "addon-name@component-name"}}
+```
+
+```hbs
+{{my-component}}
+```
+
+```hbs
+{{my-component close=(component "link-to" "index")}}
+```
+
+### References
+
+* [component helper guide](https://guides.emberjs.com/release/components/defining-a-component/#toc_dynamically-rendering-a-component)
+* [component helper spec](https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/component?anchor=component)
diff --git a/lib/helpers/ast-node-info.js b/lib/helpers/ast-node-info.js
index aafd31d6..ec4dce92 100644
--- a/lib/helpers/ast-node-info.js
+++ b/lib/helpers/ast-node-info.js
@@ -52,6 +52,10 @@ function isBlockStatement(node) {
   return node.type === 'BlockStatement';
 }
 
+function isStringLiteral(node) {
+  return node.type === 'StringLiteral';
+}
+
 function isIf(node) {
   return node.path && node.path.original === 'if';
 }
@@ -155,6 +159,7 @@ module.exports = {
   attributeValue: attributeValue,
   elementAttributeValue: elementAttributeValue,
   isBlockStatement: isBlockStatement,
+  isStringLiteral: isStringLiteral,
   isIf: isIf,
   isUnless: isUnless,
   isCommentStatement: isCommentStatement,
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 90fef389..6e0e1f2a 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -40,6 +40,7 @@ module.exports = {
   'no-trailing-spaces': require('./lint-no-trailing-spaces'),
   'no-triple-curlies': require('./lint-no-triple-curlies'),
   'no-unbound': require('./lint-no-unbound'),
+  'no-unnecessary-component-helper': require('./lint-no-unnecessary-component-helper'),
   'no-unnecessary-concat': require('./lint-no-unnecessary-concat'),
   'no-unused-block-params': require('./lint-no-unused-block-params'),
   quotes: require('./lint-quotes'),
diff --git a/lib/rules/lint-no-unnecessary-component-helper.js b/lib/rules/lint-no-unnecessary-component-helper.js
new file mode 100644
index 00000000..667267af
--- /dev/null
+++ b/lib/rules/lint-no-unnecessary-component-helper.js
@@ -0,0 +1,34 @@
+'use strict';
+
+const Rule = require('./base');
+const AstNodeInfo = require('../helpers/ast-node-info');
+
+const ERROR_MESSAGE = 'Invoke component directly instead of using `component` helper';
+
+module.exports = class NoUnnecessaryComponentHelper extends Rule {
+  visitor() {
+    function checkNode(node) {
+      if (
+        AstNodeInfo.isPathExpression(node.path) &&
+        node.path.original === 'component' &&
+        node.params.length > 0 &&
+        AstNodeInfo.isStringLiteral(node.params[0]) &&
+        !node.params[0].value.includes('@')
+      ) {
+        this.log({
+          message: ERROR_MESSAGE,
+          line: node.loc && node.loc.start.line,
+          column: node.loc && node.loc.start.column,
+          source: this.sourceForNode(node),
+        });
+      }
+    }
+
+    return {
+      BlockStatement: checkNode,
+      MustacheStatement: checkNode,
+    };
+  }
+};
+
+module.exports.ERROR_MESSAGE = ERROR_MESSAGE;


Test patch:
diff --git a/test/unit/rules/lint-no-unnecessary-component-helper-test.js b/test/unit/rules/lint-no-unnecessary-component-helper-test.js
new file mode 100644
index 00000000..36a9cbf3
--- /dev/null
+++ b/test/unit/rules/lint-no-unnecessary-component-helper-test.js
@@ -0,0 +1,65 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+const rule = require('../../../lib/rules/lint-no-unnecessary-component-helper');
+
+const { ERROR_MESSAGE } = rule;
+
+generateRuleTests({
+  name: 'no-unnecessary-component-helper',
+
+  config: true,
+
+  good: [
+    // MustacheStatement
+    '{{component SOME_COMPONENT_NAME}}',
+    '{{component SOME_COMPONENT_NAME SOME_ARG}}',
+    '{{component SOME_COMPONENT_NAME "Hello World"}}',
+    '{{my-component}}',
+    '{{my-component "Hello world"}}',
+    '{{my-component "Hello world" 123}}',
+
+    // BlockStatement:
+    '{{#component SOME_COMPONENT_NAME}}{{/component}}',
+    '{{#component SOME_COMPONENT_NAME SOME_ARG}}{{/component}}',
+    '{{#component SOME_COMPONENT_NAME "Hello World"}}{{/component}}',
+    '{{#my-component}}{{/my-component}}',
+    '{{#my-component "Hello world"}}{{/my-component}}',
+    '{{#my-component "Hello world" 123}}{{/my-component}}',
+
+    // Inline usage is not affected by this rule:
+    '(component SOME_COMPONENT_NAME)',
+    '(component "my-component")',
+
+    // Component names of the form `addon-name@component-name` are exempt:
+    "{{component 'addon-name@component-name'}}",
+    "{{#component 'addon-name@component-name'}}{{/component}}",
+  ],
+
+  bad: [
+    // MustacheStatement
+    {
+      template: '{{component "my-component-name"}}',
+
+      result: {
+        message: ERROR_MESSAGE,
+        moduleId: 'layout.hbs',
+        source: '{{component "my-component-name"}}',
+        line: 1,
+        column: 0,
+      },
+    },
+    // BlockStatement:
+    {
+      template: '{{#component "my-component-name"}}{{/component}}',
+
+      result: {
+        message: ERROR_MESSAGE,
+        moduleId: 'layout.hbs',
+        source: '{{#component "my-component-name"}}{{/component}}',
+        line: 1,
+        column: 0,
+      },
+    },
+  ],
+});