Repository: rwjblue/ember-template-lint
Pull request #361 (2018-03-21 00:34:13)

[FEAT] validate-input-helper
This PR contains two new rules for the `{{input` helper.

The first validates that it is not used via `{{#input` which would result in a runtime error. See: https://github.com/emberjs/ember.js/pull/15445 for context.

The second validates that `tagName` is not set. When `tagName` is set, and is set to something other than `input`, the helper becomes useless, and sometimes will not render at all.


Patch:
diff --git a/docs/rule/no-input-block.md b/docs/rule/no-input-block.md
new file mode 100644
index 0000000..5f28bb4
--- /dev/null
+++ b/docs/rule/no-input-block.md
@@ -0,0 +1,11 @@
+## no-input-block
+
+`{{#input}}Some Content{{/input}}` will result in an error during render. This rule
+provides a helpful error at build time that otherwise might not be caught quickly at
+runtime.
+
+This rule forbids usage of the following:
+
+```hbs
+{{#input}}{{/input}}
+```
diff --git a/docs/rule/no-input-tagname.md b/docs/rule/no-input-tagname.md
new file mode 100644
index 0000000..6c2301a
--- /dev/null
+++ b/docs/rule/no-input-tagname.md
@@ -0,0 +1,17 @@
+## no-input-tagname
+
+`{{input tagName=x}}` will result in obtuse errors. Typically the input will simply
+fail to render, whether used in block form or inline. The only valid tagName for the
+input helper is `input`. For `textarea`, `button` and other input-like elements create
+a new component or better use the DOM!
+
+This rule forbids usage of the following:
+
+```hbs
+{{input tagName="foo"}}
+{{input tagName=X}}
+{{component "input" tagName="foo"}}
+{{component "input" tagName=X}}
+{{yield (component "input" tagName="foo")}}
+{{yield (component "input" tagName=X)}}
+```
diff --git a/docs/rules.md b/docs/rules.md
index 74d90fa..e1c1de8 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -14,6 +14,8 @@
 * [no-debugger](rule/no-debugger.md)
 * [no-duplicate-attributes](rule/no-duplicate-attributes.md)
 * [no-log](rule/no-log.md)
+* [no-input-block](rule/no-input-block.md)
+* [no-input-tagname](rule/no-input-tagname.md)
 * [no-unbound](rule/no-unbound.md)
 * [no-trailing-spaces](rule/no-trailing-spaces.md)
 * [quotes](rule/quotes.md)
diff --git a/lib/config/recommended.js b/lib/config/recommended.js
index fda336e..f1f3ced 100644
--- a/lib/config/recommended.js
+++ b/lib/config/recommended.js
@@ -5,6 +5,8 @@ module.exports = {
     'block-indentation': 2,
     'no-debugger': true,
     'no-log': true,
+    'no-input-block': true,
+    'no-input-tagname': true,
     'no-unbound': true,
     'html-comments': true,
     'nested-interactive': true,
diff --git a/lib/rules/index.js b/lib/rules/index.js
index da12a57..53c5284 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -5,6 +5,8 @@ module.exports = {
   'block-indentation': require('./lint-block-indentation'),
   'no-debugger': require('./lint-no-debugger'),
   'no-log': require('./lint-no-log'),
+  'no-input-block': require('./lint-no-input-block'),
+  'no-input-tagname': require('./lint-no-input-tagname'),
   'no-unbound': require('./lint-no-unbound'),
   'html-comments': require('./lint-html-comments'),
   'img-alt-attributes': require('./lint-img-alt-attributes'),
diff --git a/lib/rules/lint-no-input-block.js b/lib/rules/lint-no-input-block.js
new file mode 100644
index 0000000..a865545
--- /dev/null
+++ b/lib/rules/lint-no-input-block.js
@@ -0,0 +1,27 @@
+'use strict';
+
+const Rule = require('./base');
+const message = 'Unexpected block usage. The {{input}} helper may only be used inline.';
+
+module.exports = class NoInputBlock extends Rule {
+  _checkForInput(node) {
+    if (node.path.original === 'input') {
+      this.log({
+        message,
+        line: node.loc && node.loc.start.line,
+        column: node.loc && node.loc.start.column,
+        source: this.sourceForNode(node)
+      });
+    }
+  }
+
+  visitor() {
+    return {
+      BlockStatement(node) {
+        this._checkForInput(node);
+      }
+    };
+  }
+};
+
+module.exports.message = message;
diff --git a/lib/rules/lint-no-input-tagname.js b/lib/rules/lint-no-input-tagname.js
new file mode 100644
index 0000000..3ea4819
--- /dev/null
+++ b/lib/rules/lint-no-input-tagname.js
@@ -0,0 +1,63 @@
+'use strict';
+
+const Rule = require('./base');
+const message = 'Unexpected `tagName` usage on {{input}} helper.';
+
+function firstComponentParamIsInput(node) {
+  return node &&
+    Array.isArray(node.params) &&
+    node.params[0] &&
+    node.params[0].original === 'input';
+}
+
+function hasTagNameAttr(attrs) {
+  for (let i = 0; i < attrs.length; i++) {
+    if (attrs[i].key === 'tagName') {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+
+module.exports = class NoInputTagname extends Rule {
+  _checkForInputTagName(node) {
+    let attrs = (node.hash || {}).pairs || [];
+
+    if (node.path.original === 'input' && hasTagNameAttr(attrs)) {
+      this.log({
+        message,
+        line: node.loc && node.loc.start.line,
+        column: node.loc && node.loc.start.column,
+        source: this.sourceForNode(node)
+      });
+
+    } else if (
+      node.path.original === 'component' &&
+      firstComponentParamIsInput(node) &&
+      hasTagNameAttr(attrs)
+    ) {
+      this.log({
+        message,
+        line: node.loc && node.loc.start.line,
+        column: node.loc && node.loc.start.column,
+        source: this.sourceForNode(node)
+      });
+    }
+  }
+
+  visitor() {
+    return {
+      MustacheStatement(node) {
+        this._checkForInputTagName(node);
+      },
+
+      SubExpression(node) {
+        this._checkForInputTagName(node);
+      }
+    };
+  }
+};
+
+module.exports.message = message;


Test patch:
diff --git a/test/unit/rules/lint-no-input-block-test.js b/test/unit/rules/lint-no-input-block-test.js
new file mode 100644
index 0000000..68eb017
--- /dev/null
+++ b/test/unit/rules/lint-no-input-block-test.js
@@ -0,0 +1,30 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+const message = require('../../../lib/rules/lint-no-input-block').message;
+
+generateRuleTests({
+  name: 'no-input-block',
+
+  config: true,
+
+  good: [
+    '{{button}}',
+    '{{#x-button}}{{/x-button}}',
+    '{{input}}',
+  ],
+
+  bad: [
+    {
+      template: '{{#input}}{{/input}}',
+
+      result: {
+        message,
+        moduleId: 'layout.hbs',
+        source: '{{#input}}{{/input}}',
+        line: 1,
+        column: 0
+      }
+    }
+  ]
+});
diff --git a/test/unit/rules/lint-no-input-tagname-test.js b/test/unit/rules/lint-no-input-tagname-test.js
new file mode 100644
index 0000000..be162b9
--- /dev/null
+++ b/test/unit/rules/lint-no-input-tagname-test.js
@@ -0,0 +1,85 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+const message = require('../../../lib/rules/lint-no-input-tagname').message;
+
+generateRuleTests({
+  name: 'no-input-tagname',
+
+  config: true,
+
+  good: [
+    '{{input type="text"}}',
+    '{{component "input" type="text"}}',
+    '{{yield (component "input" type="text")}}',
+  ],
+
+  bad: [
+    {
+      template: '{{input tagName="foo"}}',
+
+      result: {
+        message,
+        moduleId: 'layout.hbs',
+        source: '{{input tagName="foo"}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{input tagName=bar}}',
+
+      result: {
+        message,
+        moduleId: 'layout.hbs',
+        source: '{{input tagName=bar}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{component "input" tagName="foo"}}',
+
+      result: {
+        message,
+        moduleId: 'layout.hbs',
+        source: '{{component "input" tagName="foo"}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{component "input" tagName=bar}}',
+
+      result: {
+        message,
+        moduleId: 'layout.hbs',
+        source: '{{component "input" tagName=bar}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{yield (component "input" tagName="foo")}}',
+
+      result: {
+        message,
+        moduleId: 'layout.hbs',
+        source: '(component "input" tagName="foo")',
+        line: 1,
+        column: 8
+      }
+    },
+    {
+      template: '{{yield (component "input" tagName=bar)}}',
+
+      result: {
+        message,
+        moduleId: 'layout.hbs',
+        source: '(component "input" tagName=bar)',
+        line: 1,
+        column: 8
+      }
+    },
+  ]
+});