Repository: ember-template-lint/ember-template-lint
Pull request #433 (2018-06-16 16:11:49)

Add `no-shadowed-elements` rule.
As mentioned in the [Angle Bracket Invocation RFC](https://github.com/emberjs/rfcs/blob/master/text/0311-angle-bracket-invocation.md), this adds a rule to prevent usage of dynamic invocation of block params that are ambiguous with the normal HTML element naming space (including web components).

From the RFC:

> Instead, we recommend including a template linter in the default stack and defer to the linter to make such recommendations. At minimum, we recommend liniting against invoking local variables with lowercase names without a path segment, regarless of whether the name actually collide with a known HTML tag – human readers of an Ember template should be able to safely assume lowercase tags refer to HTML.


Patch:
diff --git a/lib/helpers/is-angle-bracket-component.js b/lib/helpers/is-angle-bracket-component.js
index 2cb42072..8b14f91d 100644
--- a/lib/helpers/is-angle-bracket-component.js
+++ b/lib/helpers/is-angle-bracket-component.js
@@ -1,19 +1,19 @@
 'use strict';
 
-function isDynamicComponent(element) {
+function isDynamicComponent(scope, element) {
   let open = element.tag.charAt(0);
 
-  let maybeLocal = element.tag.split('.').length > 1;
+  let isLocal = scope.isLocal(element);
   let isNamedArgument = open === '@';
   let isThisPath = element.tag.indexOf('this.') === 0;
-  return maybeLocal || isNamedArgument || isThisPath;
+  return isLocal || isNamedArgument || isThisPath;
 }
 
-module.exports = function isAngleBracketComponent(element) {
+module.exports = function isAngleBracketComponent(scope, element) {
   let open = element.tag.charAt(0);
   let isPath = element.tag.indexOf('.') > -1;
 
   let isUpperCase = open === open.toUpperCase() && open !== open.toLowerCase();
 
-  return (isUpperCase && !isPath) || isDynamicComponent(element);
+  return (isUpperCase && !isPath) || isDynamicComponent(scope, element);
 };
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 23a1cda6..e53018cc 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -38,4 +38,5 @@ module.exports = {
   'invocable-blacklist': require('./lint-invocable-blacklist'),
   'no-attrs-in-components': require('./lint-no-attrs-in-components'),
   'no-implicit-this': require('./lint-no-implicit-this'),
+  'no-shadowed-elements': require('./lint-no-shadowed-elements'),
 };
diff --git a/lib/rules/lint-no-invalid-interactive.js b/lib/rules/lint-no-invalid-interactive.js
index 24e615e4..9a3aae74 100644
--- a/lib/rules/lint-no-invalid-interactive.js
+++ b/lib/rules/lint-no-invalid-interactive.js
@@ -25,7 +25,9 @@ module.exports = class InvalidInteractive extends Rule {
 
     let visitor = {
       enter(node) {
-        let isInteractive = isInteractiveElement(node) || this.isCustomInteractiveElement(node) || isAngleBracketComponent(node);
+        let isInteractive = isInteractiveElement(node)
+          || this.isCustomInteractiveElement(node)
+          || isAngleBracketComponent(this.scope, node);
         this._element = !isInteractive ? node : null;
       },
 
diff --git a/lib/rules/lint-no-shadowed-elements.js b/lib/rules/lint-no-shadowed-elements.js
new file mode 100644
index 00000000..b80922a3
--- /dev/null
+++ b/lib/rules/lint-no-shadowed-elements.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const Rule = require('./base');
+const isAngleBracketComponent = require('../helpers/is-angle-bracket-component');
+
+module.exports = class NoShadowedElements extends Rule {
+
+  visitor() {
+    return {
+      ElementNode(node) {
+        // not a local, so cannot be shadowing native element
+        if (!this.isLocal(node)) {
+          return;
+        }
+
+        // not an angle bracket invocation at all, can't be shadowing
+        if (!isAngleBracketComponent(this.scope, node)) {
+          return;
+        }
+
+
+        let firstChar = node.tag.charAt(0);
+        let startsWithUpperCase = firstChar === firstChar.toUpperCase() && firstChar !== firstChar.toLowerCase();
+
+        if (!startsWithUpperCase) {
+          this.log({
+            message: `Ambiguous element used (\`${node.tag}\`)`,
+            line: node.loc && node.loc.start.line,
+            column: node.loc && node.loc.start.column,
+            source: this.sourceForNode(node)
+          });
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/test/unit/rules/lint-no-invalid-interactive-test.js b/test/unit/rules/lint-no-invalid-interactive-test.js
index c80e0fb8..6ad24a1c 100644
--- a/test/unit/rules/lint-no-invalid-interactive-test.js
+++ b/test/unit/rules/lint-no-invalid-interactive-test.js
@@ -17,7 +17,7 @@ generateRuleTests({
     '<form onreset={{action "foo"}}></form>',
     '<InputSearch @onInput={{action "foo"}} />',
     '<InputSearch @onInput={{action "foo"}}></InputSearch>',
-    '<foo.bar @onInput={{action "foo"}}></foo.bar>',
+    '{{#with (hash bar=(component "foo")) as |foo|}}<foo.bar @onInput={{action "foo"}}></foo.bar>{{/with}}',
     {
       config: { additionalInteractiveTags: ['div'] },
       template: '<div {{action "foo"}}></div>'
diff --git a/test/unit/rules/lint-no-shadowed-elements-test.js b/test/unit/rules/lint-no-shadowed-elements-test.js
new file mode 100644
index 00000000..c5f206ee
--- /dev/null
+++ b/test/unit/rules/lint-no-shadowed-elements-test.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+
+generateRuleTests({
+  name: 'no-shadowed-elements',
+
+  config: true,
+
+  good: [
+    '{{#foo-bar as |Baz|}}<Baz />{{/foo-bar}}',
+    '<FooBar as |Baz|><Baz /></FooBar>',
+    '{{#with foo=(component "blah-zorz") as |Div|}}<Div></Div>{{/with}}',
+  ],
+
+  bad: [
+    {
+      template: '<FooBar as |div|><div></div></FooBar>',
+
+      result: {
+        moduleId: 'layout.hbs',
+        message: 'Ambiguous element used (`div`)',
+        line: 1,
+        column: 17,
+        source: '<div></div>'
+      }
+    },
+  ],
+});