Repository: ember-template-lint/ember-template-lint (js)

Issue:
Rule: Component/helper blacklist
Disallow certain components or helpers from being used. Use case is you bring in some addon like ember-composable-helpers, but your team deems one or many of the helpers not suitable and wants to guard against their usage.

Pull request description:
add helper-blacklist rule
Closes #401.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 204d6825..2c7bf9ef 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 Changelog
 =========
 
+- add invocable-blacklist rule [#404](https://github.com/ember-template-lint/ember-template-lint/pull/404)
 - extract scope and frame for reuse [#406](https://github.com/rwjblue/ember-template-lint/pull/406)
 
 ## 0.8.15
diff --git a/docs/rule/invocable-blacklist.md b/docs/rule/invocable-blacklist.md
new file mode 100644
index 00000000..f552fc00
--- /dev/null
+++ b/docs/rule/invocable-blacklist.md
@@ -0,0 +1,22 @@
+## invocable-blacklist
+
+Disallow certain components or helpers from being used. Use case is you bring in some addon like ember-composable-helpers, but your team deems one or many of the helpers not suitable and wants to guard against their usage.
+
+Given a config of
+
+```js
+'invocable-blacklist': ['foo']
+```
+
+#### Forbidden:
+
+```hbs
+{{foo}}
+```
+
+```hbs
+{{#foo}}{{/foo}}
+```
+
+#### Configuration:
+  * array of strings - helpers or components to blacklist
diff --git a/docs/rules.md b/docs/rules.md
index 88b208ca..1adc6baf 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -4,6 +4,7 @@
 * [bare-strings](rule/bare-strings.md)
 * [block-indentation](rule/block-indentation.md)
 * [eol-last](rule/eol-last.md)
+* [invocable-blacklist](rule/invocable-blacklist.md)
 * [html-comments](rule/html-comments.md)
 * [img-alt-attributes](rule/img-alt-attributes.md)
 * [inline-link-to](rule/inline-link-to.md)
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 728ed3f7..f57de625 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -35,4 +35,5 @@ module.exports = {
   'no-trailing-dot-in-path-expression': require('./lint-trailing-dot-in-path-expression'),
   'no-unnecessary-concat': require('./lint-no-unnecessary-concat'),
   'no-partial': require('./lint-no-partial'),
+  'invocable-blacklist': require('./lint-invocable-blacklist'),
 };
diff --git a/lib/rules/internal/frame.js b/lib/rules/internal/frame.js
index 3fa2dd9f..8bc3c106 100644
--- a/lib/rules/internal/frame.js
+++ b/lib/rules/internal/frame.js
@@ -33,6 +33,10 @@ class Frame {
       return false;
     }
   }
+
+  isLocal(name) {
+    return this.locals.indexOf(name) !== -1;
+  }
 }
 
 module.exports = Frame;
diff --git a/lib/rules/internal/scope.js b/lib/rules/internal/scope.js
index cda2b112..08064e84 100644
--- a/lib/rules/internal/scope.js
+++ b/lib/rules/internal/scope.js
@@ -34,6 +34,14 @@ class Scope {
       this.frames[i].usePartial();
     }
   }
+
+  isLocal(node) {
+    for (let i = this.frames.length - 1; i >= 0; i--) {
+      if (this.frames[i].isLocal(node.path.original)) {
+        return true;
+      }
+    }
+  }
 }
 
 module.exports = Scope;
diff --git a/lib/rules/lint-invocable-blacklist.js b/lib/rules/lint-invocable-blacklist.js
new file mode 100644
index 00000000..be956655
--- /dev/null
+++ b/lib/rules/lint-invocable-blacklist.js
@@ -0,0 +1,83 @@
+'use strict';
+
+const Rule = require('./base');
+const Scope = require('./internal/scope');
+const createErrorMessage = require('../helpers/create-error-message');
+
+module.exports = class InvocableBlacklist extends Rule {
+  parseConfig(config) {
+    switch (typeof config) {
+    case 'boolean':
+      if (!config) {
+        return false;
+      }
+      break;
+    case 'object':
+      if (Array.isArray(config)) {
+        return config;
+      }
+      break;
+    case 'undefined':
+      return false;
+    }
+
+    let errorMessage = createErrorMessage(this.ruleName, [
+      '  * array of strings - helpers or components to blacklist'
+    ], config);
+
+    throw new Error(errorMessage);
+  }
+
+  visitor() {
+    this._scope = new Scope();
+
+    return {
+      BlockStatement: {
+        enter(node) {
+          this._scope.pushFrame(node);
+          this._checkBlacklist(node);
+        },
+        exit() {
+          this._scope.popFrame();
+        }
+      },
+
+      MustacheStatement(node) {
+        this._checkBlacklist(node);
+      },
+
+      SubExpression(node) {
+        this._checkBlacklist(node);
+      }
+    };
+  }
+
+  _checkBlacklist(node) {
+    let blacklist = this.config;
+    for (let name of blacklist) {
+      this._checkNode(node, name);
+    }
+  }
+
+  _checkNode(node, name) {
+    if ((node.path.original === name ||
+        checkForComponentHelper(node, name)) &&
+        !this._scope.isLocal(node)) {
+      this._logNode(node, name);
+    }
+  }
+
+  _logNode(node, name) {
+    this.log({
+      message: `Cannot use blacklisted helper or component '{{${name}}}'`,
+      line: node.loc && node.loc.start.line,
+      column: node.loc && node.loc.start.column,
+      source: this.sourceForNode(node)
+    });
+  }
+};
+
+function checkForComponentHelper(node, name) {
+  return node.path.original === 'component' &&
+  node.params[0] && node.params[0].original === name;
+}


Test patch:
diff --git a/test/unit/rules/lint-invocable-blacklist-test.js b/test/unit/rules/lint-invocable-blacklist-test.js
new file mode 100644
index 00000000..91549163
--- /dev/null
+++ b/test/unit/rules/lint-invocable-blacklist-test.js
@@ -0,0 +1,279 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+
+generateRuleTests({
+  name: 'invocable-blacklist',
+
+  config: [
+    'foo',
+    'bar'
+  ],
+
+  good: [
+    '{{baz}}',
+    '{{baz foo=bar}}',
+    '{{baz foo=(baz)}}',
+    '{{#baz}}{{/baz}}',
+    '{{#baz foo=bar}}{{/baz}}',
+    '{{#baz foo=(baz)}}{{/baz}}',
+    '{{component "baz"}}',
+    '{{component "baz" foo=bar}}',
+    '{{component "baz" foo=(baz)}}',
+    '{{#component "baz"}}{{/component}}',
+    '{{#component "baz" foo=bar}}{{/component}}',
+    '{{#component "baz" foo=(baz)}}{{/component}}',
+    '{{yield (component "baz")}}',
+    '{{yield (component "baz" foo=bar)}}',
+    '{{yield (component "baz" foo=(baz))}}',
+    '{{yield (baz (baz (baz) bar))}}',
+    '{{yield (baz (baz (baz) (baz)))}}',
+    '{{yield (baz (baz (baz) foo=(baz)))}}',
+    '{{#baz as |foo|}}{{foo}}{{/baz}}'
+  ],
+
+  bad: [
+    {
+      template: '{{foo}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{foo}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{foo foo=bar}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{foo foo=bar}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{foo foo=(baz)}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{foo foo=(baz)}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{#foo}}{{/foo}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{#foo}}{{/foo}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{#foo foo=bar}}{{/foo}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{#foo foo=bar}}{{/foo}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{#foo foo=(baz)}}{{/foo}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{#foo foo=(baz)}}{{/foo}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{component "foo"}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{component "foo"}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{component "foo" foo=bar}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{component "foo" foo=bar}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{component "foo" foo=(baz)}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{component "foo" foo=(baz)}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{#component "foo"}}{{/component}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{#component "foo"}}{{/component}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{#component "foo" foo=bar}}{{/component}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{#component "foo" foo=bar}}{{/component}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{#component "foo" foo=(baz)}}{{/component}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '{{#component "foo" foo=(baz)}}{{/component}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{yield (component "foo")}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '(component "foo")',
+        line: 1,
+        column: 8
+      }
+    },
+    {
+      template: '{{yield (component "foo" foo=bar)}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '(component "foo" foo=bar)',
+        line: 1,
+        column: 8
+      }
+    },
+    {
+      template: '{{yield (component "foo" foo=(baz))}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '(component "foo" foo=(baz))',
+        line: 1,
+        column: 8
+      }
+    },
+    {
+      template: '{{yield (baz (foo (baz) bar))}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '(foo (baz) bar)',
+        line: 1,
+        column: 13
+      }
+    },
+    {
+      template: '{{yield (baz (baz (baz) (foo)))}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '(foo)',
+        line: 1,
+        column: 24
+      }
+    },
+    {
+      template: '{{yield (baz (baz (baz) foo=(foo)))}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '(foo)',
+        line: 1,
+        column: 28
+      }
+    },
+    {
+      template: '{{#baz as |bar|}}{{bar foo=(foo)}}{{/baz}}',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'{{foo}}\'',
+        moduleId: 'layout.hbs',
+        source: '(foo)',
+        line: 1,
+        column: 27
+      }
+    }
+  ],
+
+  error: [
+    {
+      config: 'sometimes',
+      template: 'test',
+
+      result: {
+        fatal: true,
+        moduleId: 'layout.hbs',
+        message: 'You specified `"sometimes"`'
+      }
+    },
+    {
+      config: true,
+      template: 'test',
+
+      result: {
+        fatal: true,
+        moduleId: 'layout.hbs',
+        message: 'You specified `true`'
+      }
+    },
+    {
+      config: {},
+      template: 'test',
+
+      result: {
+        fatal: true,
+        moduleId: 'layout.hbs',
+        message: 'You specified `{}`'
+      }
+    }
+  ]
+});