Repository: rwjblue/ember-template-lint
Pull request #88 (2016-06-12 20:25:31)

Lint unused block params
### Overview:

Adds linting of unused block params. Traverses the template AST adding "frames" which know if a block param is in scope. If a block param that is in scope isn't used, a linter warning occurs.
### Outstanding Issues:

This implementation only checks to see if the last block param is unused. This is because you may only want use the last param, and therefore would need to define the ones before e.g.

``` hbs
{{#each cats as |unused, index|}}
 {{index}}
{{/each}}
```

A better implementation may be to force the use an underscore to indicate an intentionally unused parameter e.g. 

``` hbs
{{#each cats as |_, index|}}
 {{index}}
{{/each}}
```

This simpler implementation is still a solid first step to doing the more complex underscore version. 
@mmun 


matteddy: Whoops, I will fix the specs tonight. 
mmun: Paging señor @rwjblue.
rwjblue: Oh, no! Sorry y'all.  Will try to review tonight.

rwjblue: This looks great (though I'm a horrible maintainer and have taken forever to give feedback).  

@matteddy - Can you rebase?

Patch:
diff --git a/lib/config/recommended.js b/lib/config/recommended.js
index 629fb17..f116c7f 100644
--- a/lib/config/recommended.js
+++ b/lib/config/recommended.js
@@ -12,7 +12,8 @@ module.exports = {
     'invalid-interactive': true,
     'img-alt-attributes': true,
     'style-concatenation': true,
-    'deprecated-inline-view-helper': true
+    'deprecated-inline-view-helper': true,
+    'unused-block-params': true
   },
 
   pending: [],
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 3b14e22..446c4bf 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -13,5 +13,6 @@ module.exports = {
   'deprecated-each-syntax': require('./deprecations/lint-deprecated-each-syntax'),
   'deprecated-inline-view-helper': require('./deprecations/lint-deprecated-inline-view-helper'),
   'invalid-interactive': require('./lint-invalid-interactive'),
-  'style-concatenation': require('./lint-style-concatenation')
+  'style-concatenation': require('./lint-style-concatenation'),
+  'unused-block-params': require('./lint-unused-block-params')
 };
diff --git a/lib/rules/lint-unused-block-params.js b/lib/rules/lint-unused-block-params.js
new file mode 100644
index 0000000..37a47e3
--- /dev/null
+++ b/lib/rules/lint-unused-block-params.js
@@ -0,0 +1,91 @@
+'use strict';
+
+var buildPlugin = require('./base');
+
+module.exports = function(addonContext) {
+  var UnusedBlockParams = buildPlugin(addonContext, 'unused-block-params');
+
+  UnusedBlockParams.prototype.visitors = function() {
+    var scope = new Scope();
+
+    return {
+      Program: {
+        enter: function(node) {
+          scope.pushFrame(node);
+        },
+        exit: function(node) {
+          var 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: function(node) {
+        scope.usePath(node);
+      }
+    };
+  };
+
+  return UnusedBlockParams;
+};
+
+function Frame(locals) {
+  this.locals = locals;
+  this.usedLocals = {};
+
+  for (var i = 0; i < locals.length; i++) {
+    this.usedLocals[locals[i]] = false;
+  }
+}
+
+Frame.prototype.useLocal = function(name) {
+  if (name in this.usedLocals) {
+    this.usedLocals[name] = true;
+    return true;
+  } else {
+    return false;
+  }
+};
+
+Frame.prototype.unusedLocals = function() {
+  if (this.locals.length > 0) {
+    if (!this.usedLocals[this.locals[this.locals.length - 1]]) {
+      return this.locals[this.locals.length - 1];
+    }
+  } else {
+    return false;
+  }
+};
+
+function Scope() {
+  this.frames = [];
+}
+
+Scope.prototype.pushFrame = function(program) {
+  this.frames.push(new Frame(program.blockParams));
+};
+
+Scope.prototype.popFrame = function() {
+  this.frames.pop();
+};
+
+Scope.prototype.frameHasUnusedBlockParams = function() {
+  return this.frames[this.frames.length - 1].unusedLocals();
+};
+
+Scope.prototype.usePath = function(path) {
+  var name = path.parts[0];
+
+  for (var i = this.frames.length - 1; i >= 0; i--) {
+    if (this.frames[i].useLocal(name)) {
+      break;
+    }
+  }
+};


Test patch:
diff --git a/test/unit/rules/lint-unused-block-params-test.js b/test/unit/rules/lint-unused-block-params-test.js
new file mode 100644
index 0000000..8522abb
--- /dev/null
+++ b/test/unit/rules/lint-unused-block-params-test.js
@@ -0,0 +1,64 @@
+'use strict';
+
+var generateRuleTests = require('../../helpers/rule-test-harness');
+
+generateRuleTests({
+  name: 'unused-block-params',
+
+  config: true,
+
+  good: [
+    '{{cat}}',
+    '{{#each cats as |cat|}}{{cat}}{{/each}}',
+    '{{#each cats as |cat|}}{{cat.name}}{{/each}}',
+    '{{#each cats as |cat|}}{{meow cat}}{{/each}}',
+    '{{#each cats as |cat index|}}{{index}}{{/each}}',
+    '{{#each cats as |cat index|}}' +
+      '{{#each cat.lives as |life|}}' +
+        '{{index}}: {{life}}' +
+      '{{/each}}' +
+    '{{/each}}'
+  ],
+
+  bad: [
+    {
+      template: '{{#each cats as |cat|}}Dogs{{/each}}',
+
+      result: {
+        rule: 'unused-block-params',
+        message: '\'cat\' is defined but never used',
+        moduleId: 'layout.hbs',
+        source: '{{#each cats as |cat|}}Dogs',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template: '{{#each cats as |cat index|}}{{cat}}{{/each}}',
+      result: {
+        rule: 'unused-block-params',
+        message: '\'index\' is defined but never used',
+        moduleId: 'layout.hbs',
+        source: '{{#each cats as |cat index|}}{{cat}}',
+        line: 1,
+        column: 0
+      }
+    },
+    {
+      template:
+        '{{#each cats as |cat index|}}' +
+          '{{#each cat.lives as |life index|}}' +
+            '{{index}}: {{life}}' +
+          '{{/each}}' +
+        '{{/each}}',
+      result: {
+        rule: 'unused-block-params',
+        message: '\'index\' is defined but never used',
+        moduleId: 'layout.hbs',
+        source: '{{#each cats as |cat index|}}{{#each cat.lives as |life index|}}{{index}}: {{life}}{{/each}}',
+        line: 1,
+        column: 0
+      }
+    }
+  ]
+});