Repository: ember-template-lint/ember-template-lint
Pull request #430 (2018-06-15 03:12:53)

Initial updates to account for angle bracket invocation.
* Fixup no-unused-block-params to properly track a block param that is used in angle bracket invocation
* Fixup invocation-blacklist to properly account for angle bracket invocation


Patch:
diff --git a/lib/helpers/dasherize-component-name.js b/lib/helpers/dasherize-component-name.js
new file mode 100644
index 00000000..dfa4877e
--- /dev/null
+++ b/lib/helpers/dasherize-component-name.js
@@ -0,0 +1,18 @@
+'use strict';
+
+/*
+  This diverges from `Ember.String.dasherize` so that`<XFoo />` can resolve to `x-foo`.
+  `Ember.String.dasherize` would resolve it to `xfoo`..
+*/
+const SIMPLE_DASHERIZE_REGEXP = /[A-Z]/g;
+const ALPHA = /[A-Za-z]/;
+
+module.exports = function(key) {
+  return key.replace(SIMPLE_DASHERIZE_REGEXP, (char, index) => {
+    if (index === 0 || !ALPHA.test(key[index - 1])) {
+      return char.toLowerCase();
+    }
+
+    return `-${char.toLowerCase()}`;
+  });
+};
diff --git a/lib/rules/internal/frame.js b/lib/rules/internal/frame.js
deleted file mode 100644
index 8bc3c106..00000000
--- a/lib/rules/internal/frame.js
+++ /dev/null
@@ -1,42 +0,0 @@
-'use strict';
-
-class Frame {
-  constructor(locals) {
-    this.locals = locals;
-    this.hasPartial = false;
-    this.usedLocals = {};
-
-    for (let i = 0; i < locals.length; i++) {
-      this.usedLocals[locals[i]] = false;
-    }
-  }
-
-  useLocal(name) {
-    if (name in this.usedLocals) {
-      this.usedLocals[name] = true;
-      return true;
-    } else {
-      return false;
-    }
-  }
-
-  usePartial() {
-    this.hasPartial = true;
-  }
-
-  unusedLocals() {
-    if (!this.hasPartial && this.locals.length > 0) {
-      if (!this.usedLocals[this.locals[this.locals.length - 1]]) {
-        return this.locals[this.locals.length - 1];
-      }
-    } else {
-      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 08064e84..8863fb3b 100644
--- a/lib/rules/internal/scope.js
+++ b/lib/rules/internal/scope.js
@@ -1,14 +1,64 @@
 'use strict';
 
-const Frame = require('./frame');
+function getLocalName(node) {
+
+  switch (node.type) {
+  case 'ElementNode':
+    // unfortunately the ElementNode stores `tag` as a string
+    // if that changes in glimmer-vm this will need to be updated
+    return node.tag.split('.')[0];
+
+  default:
+    return node.parts[0];
+  }
+}
+
+class Frame {
+  constructor(locals) {
+    this.locals = locals;
+    this.hasPartial = false;
+    this.usedLocals = {};
+
+    for (let i = 0; i < locals.length; i++) {
+      this.usedLocals[locals[i]] = false;
+    }
+  }
+
+  useLocal(name) {
+    if (name in this.usedLocals) {
+      this.usedLocals[name] = true;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  usePartial() {
+    this.hasPartial = true;
+  }
+
+  unusedLocals() {
+    if (!this.hasPartial && this.locals.length > 0) {
+      if (!this.usedLocals[this.locals[this.locals.length - 1]]) {
+        return this.locals[this.locals.length - 1];
+      }
+    } else {
+      return false;
+    }
+  }
+
+  isLocal(name) {
+    return this.locals.indexOf(name) !== -1;
+  }
+}
 
 class Scope {
   constructor() {
     this.frames = [];
   }
 
-  pushFrame(block) {
-    this.frames.push(new Frame(block.program.blockParams));
+  pushFrame(locals) {
+    this.frames.push(new Frame(locals));
   }
 
   popFrame() {
@@ -19,8 +69,8 @@ class Scope {
     return this.frames[this.frames.length - 1].unusedLocals();
   }
 
-  usePath(path) {
-    let name = path.parts[0];
+  useLocal(node) {
+    let name = getLocalName(node);
 
     for (let i = this.frames.length - 1; i >= 0; i--) {
       if (this.frames[i].useLocal(name)) {
@@ -36,8 +86,10 @@ class Scope {
   }
 
   isLocal(node) {
+    let name = getLocalName(node);
+
     for (let i = this.frames.length - 1; i >= 0; i--) {
-      if (this.frames[i].isLocal(node.path.original)) {
+      if (this.frames[i].isLocal(name)) {
         return true;
       }
     }
diff --git a/lib/rules/lint-invocable-blacklist.js b/lib/rules/lint-invocable-blacklist.js
index be956655..a048152d 100644
--- a/lib/rules/lint-invocable-blacklist.js
+++ b/lib/rules/lint-invocable-blacklist.js
@@ -3,6 +3,7 @@
 const Rule = require('./base');
 const Scope = require('./internal/scope');
 const createErrorMessage = require('../helpers/create-error-message');
+const dasherize = require('../helpers/dasherize-component-name');
 
 module.exports = class InvocableBlacklist extends Rule {
   parseConfig(config) {
@@ -34,7 +35,7 @@ module.exports = class InvocableBlacklist extends Rule {
     return {
       BlockStatement: {
         enter(node) {
-          this._scope.pushFrame(node);
+          this._scope.pushFrame(node.program.blockParams);
           this._checkBlacklist(node);
         },
         exit() {
@@ -42,6 +43,16 @@ module.exports = class InvocableBlacklist extends Rule {
         }
       },
 
+      ElementNode: {
+        enter(node) {
+          this._checkBlacklist(node);
+          this._scope.pushFrame(node.blockParams);
+        },
+        exit() {
+          this._scope.popFrame();
+        }
+      },
+
       MustacheStatement(node) {
         this._checkBlacklist(node);
       },
@@ -60,16 +71,26 @@ module.exports = class InvocableBlacklist extends Rule {
   }
 
   _checkNode(node, name) {
-    if ((node.path.original === name ||
+    if (node.type === 'ElementNode') {
+      if (this._scope.isLocal(node)) { return; }
+
+      if (dasherize(node.tag) === name || node.tag === name) {
+        this._logNode(node, `<${node.tag} />`);
+      }
+    } else {
+      if (this._scope.isLocal(node.path)) { return; }
+
+      if ((node.path.original === name ||
         checkForComponentHelper(node, name)) &&
-        !this._scope.isLocal(node)) {
-      this._logNode(node, name);
+        !this._scope.isLocal(node.path)) {
+        this._logNode(node, `{{${name}}}`);
+      }
     }
   }
 
   _logNode(node, name) {
     this.log({
-      message: `Cannot use blacklisted helper or component '{{${name}}}'`,
+      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)
diff --git a/lib/rules/lint-no-unused-block-params.js b/lib/rules/lint-no-unused-block-params.js
index 626d1005..970d8eaf 100644
--- a/lib/rules/lint-no-unused-block-params.js
+++ b/lib/rules/lint-no-unused-block-params.js
@@ -50,7 +50,7 @@ module.exports = class UnusedBlockParams extends Rule {
     return {
       BlockStatement: {
         enter(node) {
-          scope.pushFrame(node);
+          scope.pushFrame(node.program.blockParams);
         },
         exit(node) {
           let unusedLocal = scope.frameHasUnusedBlockParams();
@@ -66,8 +66,28 @@ module.exports = class UnusedBlockParams extends Rule {
         }
       },
 
+      ElementNode: {
+        enter(node) {
+          scope.useLocal(node);
+          scope.pushFrame(node.blockParams);
+        },
+
+        exit(node) {
+          let unusedLocal = scope.frameHasUnusedBlockParams();
+          if (unusedLocal) {
+            this.log({
+              message: `'${unusedLocal}' is defined but never used`,
+              line: node.loc && node.loc.start.line,
+              column: node.loc && node.loc.start.column,
+              source: this.sourceForNode(node)
+            });
+          }
+          scope.popFrame();
+        }
+      },
+
       PathExpression(node) {
-        scope.usePath(node);
+        scope.useLocal(node);
       },
 
       MustacheStatement(node) {


Test patch:
diff --git a/test/unit/rules/lint-invocable-blacklist-test.js b/test/unit/rules/lint-invocable-blacklist-test.js
index 91549163..146cec3b 100644
--- a/test/unit/rules/lint-invocable-blacklist-test.js
+++ b/test/unit/rules/lint-invocable-blacklist-test.js
@@ -29,7 +29,8 @@ generateRuleTests({
     '{{yield (baz (baz (baz) bar))}}',
     '{{yield (baz (baz (baz) (baz)))}}',
     '{{yield (baz (baz (baz) foo=(baz)))}}',
-    '{{#baz as |foo|}}{{foo}}{{/baz}}'
+    '{{#baz as |foo|}}{{foo}}{{/baz}}',
+    '{{#with (component "blah") as |Foo|}} <Foo /> {{/with}}',
   ],
 
   bad: [
@@ -44,6 +45,17 @@ generateRuleTests({
         column: 0
       }
     },
+    {
+      template: '<Foo />',
+
+      result: {
+        message: 'Cannot use blacklisted helper or component \'<Foo />\'',
+        moduleId: 'layout.hbs',
+        source: '<Foo />',
+        line: 1,
+        column: 0
+      }
+    },
     {
       template: '{{foo foo=bar}}',
 
diff --git a/test/unit/rules/lint-no-unused-block-params-test.js b/test/unit/rules/lint-no-unused-block-params-test.js
index f8a1abbc..a8395243 100644
--- a/test/unit/rules/lint-no-unused-block-params-test.js
+++ b/test/unit/rules/lint-no-unused-block-params-test.js
@@ -20,6 +20,7 @@ generateRuleTests({
       '{{/each}}' +
     '{{/each}}',
     '{{! template-lint-disable unused-block-params}}{{#each cats as |cat|}}Dogs{{/each}}',
+    '{{#with (component "foo-bar") as |FooBar|}}<FooBar />{{/with}}',
   ],
 
   bad: [