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

Issue:
feat(lint-simple-unless): add ability to whitelist helpers
I would like to be able to allow some specific helpers when using unless.


Pull request description:
feat(lint-simple-unless): add whitelist
This PR adds two configuration options to the ```simple-unless``` rule:
- ```whitelist``` - an array of helpers to whitelist, e.g. ```'simple-unless': ['not', 'or']```.
- ```maxHelpers``` - number of maximum nested Helpers, defaults to 1 when at least one Helper is whitelisted. Setting maxHelpers without whitelisting at least one Helper will allow all Helpers and only apply the limit.
 
closes #353 


Patch:
diff --git a/docs/rule/simple-unless.md b/docs/rule/simple-unless.md
index d2a6256..77ea25f 100644
--- a/docs/rule/simple-unless.md
+++ b/docs/rule/simple-unless.md
@@ -35,3 +35,6 @@ logic within the template.
 The following values are valid configuration:
 
   * boolean -- `true` for enabled / `false` for disabled
+  * object --
+    * `whitelist` -- array - `['or']` for specific helpers / `[]` for wildcard
+    * `maxHelpers` -- number
diff --git a/lib/rules/lint-simple-unless.js b/lib/rules/lint-simple-unless.js
index 2d0e64d..2d6f884 100644
--- a/lib/rules/lint-simple-unless.js
+++ b/lib/rules/lint-simple-unless.js
@@ -1,6 +1,7 @@
 'use strict';
 
 const Rule = require('./base');
+const createErrorMessage = require('../helpers/create-error-message');
 
 const messages = {
   followingElseBlock: 'Using an {{else}} block with {{unless}} should be avoided.',
@@ -8,7 +9,53 @@ const messages = {
   withHelper: 'Using {{unless}} in combination with other helpers should be avoided.'
 };
 
+const DEFAULT_CONFIG = {
+  whitelist: [],
+  maxHelpers: 0
+};
+
+function isValidConfigObjectFormat(config) {
+  for (let key in DEFAULT_CONFIG) {
+    let value = config[key];
+    let valueIsArray = Array.isArray(value);
+
+    if (value === undefined) {
+      return false;
+    } else if (key === 'whitelist' && !valueIsArray){
+      return false;
+    }
+  }
+
+  return true;
+}
+
 module.exports = class LintSimpleUnless extends Rule {
+  parseConfig(config) {
+    let configType = typeof config;
+
+    switch (configType) {
+    case 'boolean':
+      // if `true` use `DEFAULT_CONFIG`
+      return config ? DEFAULT_CONFIG : false;
+    case 'object':
+      if (isValidConfigObjectFormat(config)) {
+        return config;
+      }
+      break;
+    case 'undefined':
+      return false;
+    }
+
+    let errorMessage = createErrorMessage(this.ruleName, [
+      '  * boolean -- `true` for enabled / `false` for disabled\n' +
+      '  * object --\n' +
+      '    *  `whitelist` -- array - `[\'or\']` for specific helpers / `[]` for wildcard\n' +
+      '    *  `maxHelpers` -- number'
+    ], config);
+
+    throw new Error(errorMessage);
+  }
+
   visitor() {
     return {
       MustacheStatement(node) {
@@ -65,18 +112,50 @@ module.exports = class LintSimpleUnless extends Rule {
   }
 
   _withHelper(node) {
-    let loc = node.params[0].loc.start;
-    let actual = `{{unless (${node.params[0].path.original} ...`;
+    const whitelist = this.config.whitelist; // let { whitelist, maxHelpers } = this.config;
+    const maxHelpers = this.config.maxHelpers;
+
+    let params;
+    let nextParams = node.params;
+    let helperCount = 0;
+    let containsSubexpression = false;
+
+    do {
+      params = nextParams;
+      nextParams = [];
+
+      params.forEach(param => {
+        if (param.type === 'SubExpression') {
+          if (++helperCount > maxHelpers) {
+            let loc = param.loc.start;
+            let actual = `{{unless ${helperCount > 1 ? '(... ' : ''}(${param.path.original} ...`;
+            let message = `${messages.withHelper} MaxHelpers: ${maxHelpers}`;
+
+            this._logMessage(message, loc.line, loc.column, actual);
+          }
+
+          if (whitelist.length > 0 && whitelist.indexOf(param.path.original) === -1) { // whitelist.includes(param.path.original)
+            let loc = param.loc.start;
+            let actual = `{{unless ${helperCount > 1 ? '(... ' : ''}(${param.path.original} ...`;
+            let message = `${messages.withHelper} Allowed helper${whitelist.length > 1 ? 's' : ''}: ${whitelist.toString()}`;
+
+            this._logMessage(message, loc.line, loc.column, actual);
+          }
+
+          param.params.forEach(param => nextParams.push(param)); // nextParams.push(...param.params);
+        }
+      });
 
-    this._logMessage(messages.withHelper, loc.line, loc.column, actual);
+      containsSubexpression = nextParams.some(param => param.type  === 'SubExpression');
+    } while (containsSubexpression);
   }
 
-  _logMessage(message, line, column, actual) {
+  _logMessage(message, line, column, source) {
     return this.log({
       message,
-      line: line,
-      column: column,
-      source: actual
+      line,
+      column,
+      source
     });
   }
 };


Test patch:
diff --git a/test/unit/rules/lint-simple-unless-test.js b/test/unit/rules/lint-simple-unless-test.js
index a923c53..b4abe9a 100644
--- a/test/unit/rules/lint-simple-unless-test.js
+++ b/test/unit/rules/lint-simple-unless-test.js
@@ -5,7 +5,10 @@ const messages = require('../../../lib/rules/lint-simple-unless').messages;
 
 generateRuleTests({
   name: 'simple-unless',
-  config: true,
+  config: {
+    whitelist: ['or', 'eq', 'not-eq'],
+    maxHelpers: 2
+  },
 
   good: [
     '{{#unless isRed}}I\'m blue, da ba dee da ba daa{{/unless}}',
@@ -13,19 +16,54 @@ generateRuleTests({
     '<div class="{{if foo \'foo\'}}"></div>',
     '{{unrelated-mustache-without-params}}',
     '{{#if foo}}{{else}}{{/if}}',
+    '{{#unless (or foo bar)}}order whiskey{{/unless}}',
+    '{{#unless (eq (or foo bar) baz)}}order whiskey{{/unless}}',
     [
       '{{#unless hamburger}}',
       '  HOT DOG!',
       '{{/unless}}'
-    ].join('\n')
+    ].join('\n'),
+    {
+      config: true,
+      template: '{{unless foo bar}}'
+    },
+    {
+      config: {
+        whitelist: ['or', 'eq', 'not-eq'],
+        maxHelpers: 2
+      },
+      template: '{{unless (eq foo bar) baz}}'
+    },
+    {
+      config: {
+        whitelist: [],
+        maxHelpers: 2
+      },
+      template: '{{unless (eq (not foo) bar) baz}}'
+    }
   ],
 
   bad: [
+    {
+      config: {
+        whitelist: ['or', 'eq', 'not-eq'],
+        maxHelpers: 2
+      },
+      template: '{{unless (if (or true))  \'Please no\'}}',
+
+      result: {
+        message: messages.withHelper + ' Allowed helpers: or,eq,not-eq',
+        moduleId: 'layout.hbs',
+        source: '{{unless (if ...',
+        line: 1,
+        column: 9
+      }
+    },
     {
       template: '{{unless (if true)  \'Please no\'}}',
 
       result: {
-        message: messages.withHelper,
+        message: messages.withHelper + ' Allowed helpers: or,eq,not-eq',
         moduleId: 'layout.hbs',
         source: '{{unless (if ...',
         line: 1,
@@ -36,7 +74,7 @@ generateRuleTests({
       template: '{{unless (and isBad isAwful)  \'notBadAndAwful\'}}',
 
       result: {
-        message: messages.withHelper,
+        message: messages.withHelper + ' Allowed helpers: or,eq,not-eq',
         moduleId: 'layout.hbs',
         source: '{{unless (and ...',
         line: 1,
@@ -140,7 +178,7 @@ generateRuleTests({
       ].join('\n'),
 
       result: {
-        message: messages.withHelper,
+        message: messages.withHelper + ' Allowed helpers: or,eq,not-eq',
         moduleId: 'layout.hbs',
         source: '{{unless (and ...',
         line: 1,
@@ -155,7 +193,7 @@ generateRuleTests({
       ].join('\n'),
 
       result: {
-        message: messages.withHelper,
+        message: messages.withHelper + ' Allowed helpers: or,eq,not-eq',
         moduleId: 'layout.hbs',
         source: '{{unless (not ...',
         line: 1,
@@ -164,7 +202,7 @@ generateRuleTests({
     },
     {
       template: [
-        '{{#unless (not isBrown isSticky)}}',
+        '{{#unless isSticky}}',
         '  I think I am a brown stick',
         '{{else}}',
         '  Not a brown stick',
@@ -178,6 +216,75 @@ generateRuleTests({
         line: 3,
         column: 0
       }
+    },{
+      template: [
+        '{{#unless (or (eq foo bar) (not-eq baz "beer"))}}',
+        '  MUCH HELPERS, VERY BAD',
+        '{{/unless}}'
+      ].join('\n'),
+
+      result: {
+        message: messages.withHelper + ' MaxHelpers: 2',
+        moduleId: 'layout.hbs',
+        source: '{{unless (... (not-eq ...',
+        line: 1,
+        column: 27
+      }
+    },{
+      config: true,
+      template: [
+        '{{#unless (concat "blue" "red")}}',
+        '  I think I am a brown stick',
+        '{{/unless}}'
+      ].join('\n'),
+
+      result: {
+        message: 'Using {{unless}} in combination with other helpers should be avoided. MaxHelpers: 0',
+        source: '{{unless (concat ...',
+        line: 1,
+        column: 10
+      }
+    },{
+      config: {
+        whitelist: ['test'],
+        maxHelpers: 1
+      },
+      template: [
+        '{{#unless (one (test power) two)}}',
+        '  I think I am a brown stick',
+        '{{/unless}}'
+      ].join('\n'),
+
+      results: [
+        {
+          message: 'Using {{unless}} in combination with other helpers should be avoided. Allowed helper: test',
+          source: '{{unless (one ...',
+          line: 1,
+          column: 10
+        },{
+          message: 'Using {{unless}} in combination with other helpers should be avoided. MaxHelpers: 1',
+          source: '{{unless (... (test ...',
+          line: 1,
+          column: 15
+        }
+      ]
+    },{
+      config: {
+        whitelist: [],
+        maxHelpers: 2
+      },
+      template: [
+        '{{#unless (one (two three) (four five))}}',
+        '  I think I am a brown stick',
+        '{{/unless}}'
+      ].join('\n'),
+
+      result: {
+        message: 'Using {{unless}} in combination with other helpers should be avoided. MaxHelpers: 2',
+        source: '{{unless (... (four ...',
+        line: 1,
+        column: 27
+      }
     }
   ]
 });