Repository: rwjblue/ember-template-lint
Pull request #342 (2018-01-21 16:32:16)

Don't allow trailing dots in a PathExpression
Implements https://github.com/rwjblue/ember-template-lint/issues/341


Patch:
diff --git a/docs/rule/no-trailing-dot-in-path-expression.md b/docs/rule/no-trailing-dot-in-path-expression.md
new file mode 100644
index 0000000..051b511
--- /dev/null
+++ b/docs/rule/no-trailing-dot-in-path-expression.md
@@ -0,0 +1,65 @@
+## no-trailing-dot-in-path-expression
+
+This rule doesn't allow trailing dot(s) on a path expression. Since the trailing dot is treated as a separate path expression which represents the context associated to the template.
+
+For instance:
+
+```hbs
+  /application.hbs
+
+  <span class={{if contact. 'bg-success'}}>{{contact.name}}</span>
+```
+
+is interpreted as
+
+```hbs
+  /application.hbs
+
+  <span class={{if contact . 'bg-success'}}>{{contact.name}}</span>
+```
+
+hence results in
+
+```html
+  <span class="<my-app@controller:application::ember223>">John</span>
+```
+
+
+
+Forbidden:
+
+```hbs
+
+{{contact.contact_name.}}
+
+{{#if contact.contact_name.}}
+  {{displayName}}
+{{/if}}
+
+{{if. contact.contact_name}}
+  {{displayName}}
+{{/if}}
+
+{{contact-details contact=(hash. name=name. age=age)}}
+```
+
+Allowed:
+
+```hbs
+
+{{contact.contact_name}}
+
+{{#if contact.contact_name}}
+  {{displayName}}
+{{/if}}
+
+{{if contact.contact_name}}
+  {{displayName}}
+{{/if}}
+
+{{contact-details contact=(hash name=name age=age)}}
+```
+
+This rule is configured with one boolean value:
+
+  * boolean - `true` for enabled / `false` for disabled
diff --git a/lib/rules/index.js b/lib/rules/index.js
index dd7be2a..83cbb30 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -27,5 +27,6 @@ module.exports = {
   'table-groups': require('./lint-table-groups'),
   'eol-last': require('./lint-eol-last'),
   'no-trailing-spaces': require('./lint-no-trailing-spaces'),
-  'quotes': require('./lint-quotes')
+  'quotes': require('./lint-quotes'),
+  'no-trailing-dot-in-path-expression': require('./lint-trailing-dot-in-path-expression'),
 };
diff --git a/lib/rules/lint-trailing-dot-in-path-expression.js b/lib/rules/lint-trailing-dot-in-path-expression.js
new file mode 100644
index 0000000..8d36b60
--- /dev/null
+++ b/lib/rules/lint-trailing-dot-in-path-expression.js
@@ -0,0 +1,89 @@
+/* eslint-env node */
+'use strict';
+
+const Rule = require('./base');
+
+/*
+  Don't allow dot(s) at the end of a Path expression.
+
+  Bad
+
+  {{contact.contact_name.}}
+
+  {{#if contact.contact_name.}}
+    {{displayName}}
+  {{/if}}
+
+  {{if contact.contact_name..}}
+    {{displayName}}
+  {{/if}}
+
+
+  Good
+
+  {{contact.contact_name}}
+
+  {{#if contact.contact_name}}
+    {{displayName}}
+  {{/if}}
+
+  {{if contact.contact_name}}
+    {{displayName}}
+  {{/if}}
+*/
+
+module.exports = class NoTrailingDotOnPathExpression extends Rule {
+
+  findPathExpressionWithTrailingDot(node) {
+    return node.params.find((param) => param.type === 'PathExpression' && param.original.endsWith('.')) ||
+    node.hash.pairs.find((param) => param.type === 'PathExpression' && param.original.endsWith('.'));
+  }
+
+  visitor() {
+    return {
+      MustacheStatement(node) {
+        let pathExpression = this.findPathExpressionWithTrailingDot(node);
+        if (pathExpression) {
+          let source = this.sourceForNode(node);
+          this.log({
+            message: 'A MustacheStatement should not have a Path Expression with a trialing dot.',
+            line: pathExpression.loc && pathExpression.loc.start.line,
+            column: pathExpression.loc && pathExpression.loc.start.column,
+            source
+          });
+        }
+      },
+
+      SubExpression(node) {
+        let pathExpression = this.findPathExpressionWithTrailingDot(node);
+        if (pathExpression) {
+          let source = this.sourceForNode(node);
+          this.log({
+            message: 'A SubExpression should not have a Path Expression with a trialing dot.',
+            line: pathExpression.loc && pathExpression.loc.start.line,
+            column: pathExpression.loc && pathExpression.loc.start.column,
+            source
+          });
+        }
+      },
+
+      BlockStatement(node) {
+        let pathExpression = this.findPathExpressionWithTrailingDot(node);
+        if (pathExpression) {
+          // Since we are processing only the hash pairs and params, its enough to display the open-invocation.
+          let sourceLoc = {
+            start: node.loc.start,
+            end: node.program.loc.start
+          };
+          let source = this.sourceForNode({ loc: sourceLoc });
+          this.log({
+            message: 'A BlockStatement should not have a Path Expression with a trialing dot.',
+            line: pathExpression.loc && pathExpression.loc.start.line,
+            column: pathExpression.loc && pathExpression.loc.start.column,
+            source
+          });
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/test/unit/rules/lint-trailing-dot-in-path-expression.js b/test/unit/rules/lint-trailing-dot-in-path-expression.js
new file mode 100644
index 0000000..f23a722
--- /dev/null
+++ b/test/unit/rules/lint-trailing-dot-in-path-expression.js
@@ -0,0 +1,83 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+
+generateRuleTests({
+  name: 'no-trailing-dot-in-path-expression',
+
+  config: true,
+
+  good: [
+    '{{contact}}',
+    '<span class={{if contact.is_new \'bg-success\'}}>{{contact.contact_name}}</span>',
+    '{{#if contact.contact_name}}\n' +
+    '   {{displayName}}\n' +
+    '{{/if}}',
+    '{{#contact-details contact=contact}}\n' +
+    '   {{contact.displayName}}\n' +
+    '{{/contact-details}}'
+  ],
+
+  bad: [{
+    template: '{{contact.}}',
+    result: {
+      rule: 'no-trailing-dot-in-path-expression',
+      moduleId: 'layout.hbs',
+      message: 'A MustacheStatement should not have a Path Expression with a trialing dot.',
+      line: 1,
+      column: 9,
+      source: '{{contact.}}'
+    }
+  }, {
+    template: '<span class={{if contact.is_new. \'bg-success\'}}>{{contact.contact_name}}</span>',
+    result: {
+      rule: 'no-trailing-dot-in-path-expression',
+      moduleId: 'layout.hbs',
+      message: 'A MustacheStatement should not have a Path Expression with a trialing dot.',
+      line: 1,
+      column: 31,
+      source: '{{if contact.is_new. \'bg-success\'}}'
+    }
+  }, {
+    template: '{{#if contact.contact_name.}}\n' +
+      '   {{displayName.}}\n' +
+      '{{/if}}',
+
+    results: [{
+      column: 26,
+      line: 1,
+      message: 'A BlockStatement should not have a Path Expression with a trialing dot.',
+      moduleId: 'layout.hbs',
+      rule: 'no-trailing-dot-in-path-expression',
+      source: '{{#if contact.contact_name.}}'
+    }, {
+      column: 16,
+      line: 2,
+      message: 'A MustacheStatement should not have a Path Expression with a trialing dot.',
+      moduleId: 'layout.hbs',
+      rule: 'no-trailing-dot-in-path-expression',
+      source: '{{displayName.}}'
+    }]
+  }, {
+    template: '{{if. contact \'bg-success\'}}',
+    results: [{
+      column: 4,
+      line: 1,
+      message: 'A MustacheStatement should not have a Path Expression with a trialing dot.',
+      moduleId: 'layout.hbs',
+      rule: 'no-trailing-dot-in-path-expression',
+      source: '{{if. contact \'bg-success\'}}',
+    }]
+  }, {
+    template: '{{contact-details contact=(hash. name=name age=age)}}',
+
+    results: [{
+      column: 31,
+      line: 1,
+      message: 'A SubExpression should not have a Path Expression with a trialing dot.',
+      moduleId: 'layout.hbs',
+      rule: 'no-trailing-dot-in-path-expression',
+      source: '(hash. name=name age=age)',
+    }]
+  }]
+});