Repository: ember-template-lint/ember-template-lint
Pull request #631 (2019-01-24 10:59:36)

Add rule: [a11y] iframe-has-title
https://dequeuniversity.com/rules/axe/1.1/frame-title

https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/iframe-has-title.md


Patch:
diff --git a/docs/rule/no-negated-condition.md b/docs/rule/no-negated-condition.md
index 3a67047b..4e629f02 100644
--- a/docs/rule/no-negated-condition.md
+++ b/docs/rule/no-negated-condition.md
@@ -33,6 +33,14 @@ This rule **forbids** the following:
 {{/unless}}
 ```
 
+```hbs
+{{#unless (not condition)}}
+  ...
+{{else}}
+  ...
+{{/unless}}
+```
+
 And similar examples with non-block forms like:
 
 ```hbs
diff --git a/docs/rule/require-iframe-title.md b/docs/rule/require-iframe-title.md
new file mode 100644
index 00000000..90921a6d
--- /dev/null
+++ b/docs/rule/require-iframe-title.md
@@ -0,0 +1,28 @@
+## require-iframe-title
+
+### `<iframe>`
+
+`<iframe>` elements must have a unique title property to indicate its content to the user.
+
+This rule takes no arguments.
+
+This rule **allows** the following:
+
+```hbs
+<iframe title="This is a unique title" />
+<iframe title={{someValue}} />
+```
+
+This rule **forbids** the following:
+
+```hbs
+<iframe />
+<iframe title="" />
+```
+
+### References
+
+- [Deque University](https://dequeuniversity.com/rules/axe/1.1/frame-title)
+- [Technique H65: Using the title attribute of the frame and iframe elements](https://www.w3.org/TR/2014/NOTE-WCAG20-TECHS-20140408/H64)
+- [WCAG Success Criterion 2.4.1 - Bypass Blocks](https://www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-skip.html)
+- [WCAG Success Criterion 4.1.2 - Name, Role, Value](https://www.w3.org/TR/UNDERSTANDING-WCAG20/ensure-compat-rsv.html)
\ No newline at end of file
diff --git a/docs/rule/simple-unless.md b/docs/rule/simple-unless.md
index 3c0181ce..b4fd23b4 100644
--- a/docs/rule/simple-unless.md
+++ b/docs/rule/simple-unless.md
@@ -1,22 +1,19 @@
 ## simple-unless
 
-This rule strongly advises against `{{unless}}` blocks in the following situations:
-
-* With other block helpers (e.g. `{{else}}`, `{{else if}}`)
-* With template helpers in the condition
+This rule strongly advises against `{{unless}}` blocks used in conjunction with other block helpers (e.g. `{{else}}`, `{{else if}}`), and template helpers.
 
 Common solutions are to use an `{{if}}` block, or refactor potentially confusing logic within the template.
 
 This rule **forbids** the following:
 
 ``` hbs
-{{! `unless` condition has a template helper }}
+{{! `if` template helper}}
 
 {{unless (if true) "This is not recommended"}}
 ```
 
 ``` hbs
-{{! `unless` statement has an `else` block }}
+{{! `else` block}}
 
 {{#unless bandwagoner}}
   Go Niners!
@@ -25,16 +22,6 @@ This rule **forbids** the following:
 {{/unless}}
 ```
 
-``` hbs
-{{! conditional statement has an `else unless` block }}
-
-{{#if condition}}
-  Hello
-{{else unless otherCondition}}
-  World
-{{/unless}}
-```
-
 This rule **allows** the following:
 
 ``` hbs
@@ -45,12 +32,6 @@ This rule **allows** the following:
 {{/if}}
 ```
 
-``` hbs
-{{#unless condition}}
-  Hello World
-{{/unless}}
-```
-
 ### Configuration
 
 The following values are valid configuration:
diff --git a/docs/rules.md b/docs/rules.md
index 0234df4c..dc699f2b 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -4,6 +4,7 @@
 * [block-indentation](rule/block-indentation.md)
 * [eol-last](rule/eol-last.md)
 * [img-alt-attributes](rule/img-alt-attributes.md)
+* [require-iframe-title](rule/require-iframe-title.md)
 * [inline-link-to](rule/inline-link-to.md)
 * [invocable-blacklist](rule/invocable-blacklist.md)
 * [linebreak-style](rule/linebreak-style.md)
diff --git a/lib/helpers/ast-node-info.js b/lib/helpers/ast-node-info.js
index 504a53e9..2c1aa212 100644
--- a/lib/helpers/ast-node-info.js
+++ b/lib/helpers/ast-node-info.js
@@ -48,14 +48,6 @@ function isBlockStatement(node) {
   return node.type === 'BlockStatement';
 }
 
-function isIf(node) {
-  return node.path && node.path.original === 'if';
-}
-
-function isUnless(node) {
-  return node.path && node.path.original === 'unless';
-}
-
 function hasAttribute(node, attributeName) {
   var attribute = findAttribute(node, attributeName);
 
@@ -106,8 +98,6 @@ module.exports = {
   hasAttribute: hasAttribute,
   hasChildren: hasChildren,
   isBlockStatement: isBlockStatement,
-  isIf: isIf,
-  isUnless: isUnless,
   isCommentStatement: isCommentStatement,
   isConcatStatement: isConcatStatement,
   isMustacheCommentStatement: isMustacheCommentStatement,
diff --git a/lib/rules/index.js b/lib/rules/index.js
index 296afac7..c3022d1e 100644
--- a/lib/rules/index.js
+++ b/lib/rules/index.js
@@ -8,6 +8,7 @@ module.exports = {
   'deprecated-render-helper': require('./deprecations/lint-deprecated-render-helper'),
   'eol-last': require('./lint-eol-last'),
   'img-alt-attributes': require('./lint-img-alt-attributes'),
+  'require-iframe-title': require('./lint-require-iframe-title'),
   'inline-link-to': require('./lint-inline-link-to'),
   'invocable-blacklist': require('./lint-invocable-blacklist'),
   'linebreak-style': require('./lint-linebreak-style'),
diff --git a/lib/rules/lint-no-negated-condition.js b/lib/rules/lint-no-negated-condition.js
index eec005f2..5091b5cc 100644
--- a/lib/rules/lint-no-negated-condition.js
+++ b/lib/rules/lint-no-negated-condition.js
@@ -10,27 +10,33 @@ const ERROR_MESSAGE_USE_UNLESS = 'Use `unless` instead of `if !condition`.';
 module.exports = class NoNegatedCondition extends Rule {
   visitor() {
     function checkNode(node) {
-      const isIf = AstNodeInfo.isIf(node);
-      const isUnless = AstNodeInfo.isUnless(node);
+      const isIf = node.path && node.path.original === 'if';
+      const isUnless = node.path && node.path.original === 'unless';
       if (!isIf && !isUnless) {
         // Not a conditional statement.
         return;
       }
 
       if (AstNodeInfo.isBlockStatement(node)) {
-        if (this.sourceForNode(node).startsWith('{{else ')) {
-          // We only care about the beginning of the overall `if` / `unless` statement so ignore the `else` parts.
-          return;
+        if (node.inverse && node.inverse.body.length > 0) {
+          // Conditional block statement has an `else` / `else if`
+
+          const firstNode = findFirstNonTextNonCommentNode(node.inverse.body);
+          if (firstNode && AstNodeInfo.isBlockStatement(firstNode)) {
+            // If there's BlockStatement directly inside the body of the `else`,
+            // we need to mark it so we can ignore it later,
+            // to avoid potentially suggesting an `unless` after an `else`.
+            firstNode.isAtBeginningOfElseBody = true;
+
+            if (isIf && firstNode.path.original === 'if') {
+              // Ignore `if ... else if ...` statements as there's nothing wrong with them.
+              return;
+            }
+          }
         }
 
-        if (
-          node.inverse &&
-          node.inverse.body.length > 0 &&
-          AstNodeInfo.isBlockStatement(node.inverse.body[0]) &&
-          isIf &&
-          AstNodeInfo.isIf(node.inverse.body[0])
-        ) {
-          // Ignore `if ... else if ...` statements as there may be no way to avoid negated conditions inside them.
+        if (isIf && node.isAtBeginningOfElseBody) {
+          // Ignore `if` statements directly following an `else` due to complexity.
           return;
         }
       }
@@ -41,7 +47,6 @@ module.exports = class NoNegatedCondition extends Rule {
         !AstNodeInfo.isPathExpression(node.params[0].path) ||
         node.params[0].path.original !== 'not'
       ) {
-        // No negation present.
         return;
       }
 
@@ -78,6 +83,19 @@ module.exports = class NoNegatedCondition extends Rule {
   }
 };
 
+function findFirstNonTextNonCommentNode(nodes) {
+  for (let i = 0; i < nodes.length; i++) {
+    const currentNode = nodes[i];
+    if (
+      !AstNodeInfo.isTextNode(currentNode) &&
+      !AstNodeInfo.isMustacheCommentStatement(currentNode)
+    ) {
+      return currentNode;
+    }
+  }
+  return null;
+}
+
 module.exports.ERROR_MESSAGE_FLIP_IF = ERROR_MESSAGE_FLIP_IF;
 module.exports.ERROR_MESSAGE_USE_IF = ERROR_MESSAGE_USE_IF;
 module.exports.ERROR_MESSAGE_USE_UNLESS = ERROR_MESSAGE_USE_UNLESS;
diff --git a/lib/rules/lint-require-iframe-title.js b/lib/rules/lint-require-iframe-title.js
new file mode 100644
index 00000000..0a0141ae
--- /dev/null
+++ b/lib/rules/lint-require-iframe-title.js
@@ -0,0 +1,99 @@
+'use strict';
+
+const AstNodeInfo = require('../helpers/ast-node-info');
+const Rule = require('./base');
+const errorMessage = '<iframe> elements must have a unique title property.';
+
+// iframes have to have titles for "Name,  Role, Value" - https://www.w3.org/TR/UNDERSTANDING-WCAG20/ensure-compat-rsv.html
+module.exports = class IframeHasTitle extends Rule {
+  logNode({ message, node }) {
+    this.log({
+      message,
+      line: node.loc && node.loc.start.line,
+      column: node.loc && node.loc.start.column,
+      source: this.sourceForNode(node),
+    });
+  }
+
+  visitor() {
+    this.knownTitles = [];
+    return {
+      ElementNode(node) {
+        if (node.tag === 'iframe') {
+          const ariaHidden = AstNodeInfo.hasAttribute(node, 'aria-hidden');
+          if (ariaHidden) {
+            return;
+          }
+          const hidden = AstNodeInfo.hasAttribute(node, 'hidden');
+          if (hidden) {
+            return;
+          }
+          const title = AstNodeInfo.findAttribute(node, 'title');
+          if (!title) {
+            this.logNode({
+              message: errorMessage,
+              node,
+            });
+          } else if (!title.value) {
+            return;
+          } else if (title.value.type === 'TextNode') {
+            let value = title.value.chars.trim();
+            let matchIndex = -1;
+            let [, existingNode] = this.knownTitles.filter(([val], index) => {
+              if (val === value) {
+                if (matchIndex === -1) {
+                  matchIndex = index + 1;
+                }
+                return true;
+              } else {
+                return false;
+              }
+            })[0] || [null, null];
+            if (value.length === 0) {
+              this.logNode({
+                message: errorMessage,
+                node,
+              });
+              return;
+            } else if (existingNode) {
+              this.logNode({
+                message: 'This title is not unique.' + ` #${matchIndex}`,
+                node: existingNode,
+              });
+              this.logNode({
+                message:
+                  errorMessage +
+                  ' ' +
+                  `Value title="${title.value.chars}" already used for different iframe.` +
+                  ` #${matchIndex}`,
+                node,
+              });
+              return;
+            } else {
+              this.knownTitles.push([value, title]);
+            }
+          } else if (title.value.type === 'MustacheStatement') {
+            if (title.value.path.type === 'BooleanLiteral') {
+              this.logNode({
+                message: errorMessage,
+                node,
+              });
+              return;
+            }
+          } else if (title.value.type === 'ConcatStatement') {
+            if (title.value.parts.length === 1) {
+              if (title.value.parts[0].type === 'MustacheStatement') {
+                if (title.value.parts[0].path.type === 'BooleanLiteral') {
+                  this.logNode({
+                    message: errorMessage,
+                    node,
+                  });
+                }
+              }
+            }
+          }
+        }
+      },
+    };
+  }
+};
diff --git a/lib/rules/lint-simple-unless.js b/lib/rules/lint-simple-unless.js
index 6786cdd9..dd8b8997 100644
--- a/lib/rules/lint-simple-unless.js
+++ b/lib/rules/lint-simple-unless.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const Rule = require('./base');
-const AstNodeInfo = require('../helpers/ast-node-info');
 const createErrorMessage = require('../helpers/create-error-message');
 
 const messages = {
@@ -70,19 +69,26 @@ module.exports = class LintSimpleUnless extends Rule {
       },
 
       BlockStatement(node) {
-        const nodeInverse = node.inverse;
+        let nodeInverse = node.inverse;
+        let nodePathOriginal = node.path.original;
 
         if (nodeInverse) {
-          if (AstNodeInfo.isUnless(node)) {
-            if (nodeInverse.body[0] && AstNodeInfo.isIf(nodeInverse.body[0])) {
+          if (nodePathOriginal === 'unless') {
+            if (
+              nodeInverse.body[0] &&
+              nodeInverse.body[0].path &&
+              nodeInverse.body[0].path.original === 'if'
+            ) {
               this._followingElseIfBlock(node);
             } else {
               this._followingElseBlock(node);
             }
-          } else if (this._isElseUnlessBlock(nodeInverse.body[0])) {
-            this._asElseUnlessBlock(node);
+          } else if (nodePathOriginal === 'if' && nodeInverse.body[0]) {
+            if (nodeInverse.body[0].path && nodeInverse.body[0].path.original === 'unless') {
+              this._asElseUnlessBlock(node);
+            }
           }
-        } else if (AstNodeInfo.isUnless(node) && node.params[0].path) {
+        } else if (nodePathOriginal === 'unless' && node.params[0].path) {
           this._withHelper(node);
         }
       },
@@ -155,15 +161,6 @@ module.exports = class LintSimpleUnless extends Rule {
     } while (containsSubexpression);
   }
 
-  _isElseUnlessBlock(node) {
-    return (
-      node &&
-      node.path &&
-      node.path.original === 'unless' &&
-      this.sourceForNode(node).startsWith('{{else ')
-    );
-  }
-
   _logMessage(message, line, column, source) {
     return this.log({
       message,


Test patch:
diff --git a/test/unit/rules/lint-duplicate-attributes-test.js b/test/unit/rules/lint-duplicate-attributes.js
similarity index 100%
rename from test/unit/rules/lint-duplicate-attributes-test.js
rename to test/unit/rules/lint-duplicate-attributes.js
diff --git a/test/unit/rules/lint-no-negated-condition-test.js b/test/unit/rules/lint-no-negated-condition.js
similarity index 87%
rename from test/unit/rules/lint-no-negated-condition-test.js
rename to test/unit/rules/lint-no-negated-condition.js
index 67f3d969..acaad51a 100644
--- a/test/unit/rules/lint-no-negated-condition-test.js
+++ b/test/unit/rules/lint-no-negated-condition.js
@@ -26,7 +26,6 @@ generateRuleTests({
 
     // if ... else if ...
     '{{#if condition}}<img>{{else if condition}}<img>{{/if}}',
-    '{{#if condition}}<img>{{else if (not condition2)}}<img>{{/if}}', // we ignore `if ... else if ...`
     '{{#if (not condition)}}<img>{{else if (not condition2)}}<img>{{/if}}', // we ignore `if ... else if ...`
 
     // if ... else if ... else ...
@@ -49,6 +48,19 @@ generateRuleTests({
     '{{#unless condition}}<img>{{else if condition}}<img>{{else}}<img>{{/unless}}',
     '{{#unless (or c1 c2)}}<img>{{else if (or c1 c2)}}<img>{{else}}<img>{{/unless}}',
 
+    // This is valid since we don't want to suggest `unless` directly after an `else`:
+    `{{#if condition}}
+     {{else}}
+       {{! some comment }}
+       {{#if (not condition)}}<img>{{/if}}
+     {{/if}}`,
+
+    // This is valid since we can't yet handle `if` statements directly after an `else`:
+    `{{#if (not condition)}}
+     {{else}}
+       {{#if (not condition)}}<img>{{else}}<img>{{/if}}
+     {{/if}}`,
+
     // ******************************************
     // MustacheStatement
     // ******************************************
@@ -82,14 +94,6 @@ generateRuleTests({
     // if ... else ...
     '{{input class=(if condition "some-class" "other-class")}}',
     '{{input class=(if (or c1 c2) "some-class" "other-class")}}',
-
-    // unless ...
-    '{{input class=(unless condition "some-class")}}',
-    '{{input class=(unless (or c1 c2) "some-class")}}',
-
-    // unless ... else ...
-    '{{input class=(unless condition "some-class" "other-class")}}',
-    '{{input class=(unless (or c1 c2) "some-class" "other-class")}}',
   ],
 
   bad: [
@@ -177,33 +181,6 @@ generateRuleTests({
       },
     },
 
-    // Nested inside the body of an `else` block (with preceding comment):
-    {
-      template:
-        '{{#if condition}}{{else}}{{! some comment }}{{#if (not condition)}}<img>{{/if}}{{/if}}',
-
-      result: {
-        message: ERROR_MESSAGE_USE_UNLESS,
-        moduleId: 'layout.hbs',
-        source: '{{#if (not condition)}}<img>{{/if}}',
-        line: 1,
-        column: 44,
-      },
-    },
-
-    // Nested inside the body of an `else` block (without preceding comment):
-    {
-      template: '{{#if condition}}{{else}}{{#if (not condition)}}<img>{{/if}}{{/if}}',
-
-      result: {
-        message: ERROR_MESSAGE_USE_UNLESS,
-        moduleId: 'layout.hbs',
-        source: '{{#if (not condition)}}<img>{{/if}}',
-        line: 1,
-        column: 25,
-      },
-    },
-
     // ******************************************
     // MustacheStatement
     // ******************************************
diff --git a/test/unit/rules/lint-require-iframe-title-test.js b/test/unit/rules/lint-require-iframe-title-test.js
new file mode 100644
index 00000000..64192d28
--- /dev/null
+++ b/test/unit/rules/lint-require-iframe-title-test.js
@@ -0,0 +1,121 @@
+'use strict';
+
+const generateRuleTests = require('../../helpers/rule-test-harness');
+
+generateRuleTests({
+  name: 'require-iframe-title',
+
+  config: true,
+
+  good: [
+    '<iframe title="Welcome to the Matrix!" />',
+    '<iframe title={{someValue}} />',
+    '<iframe title="" aria-hidden />',
+    '<iframe title="" hidden />',
+    '<iframe title="foo" /><iframe title="bar" />',
+  ],
+
+  bad: [
+    {
+      template: '<iframe title="foo" /><iframe title="foo" />',
+
+      results: [
+        {
+          message: 'This title is not unique. #1',
+          moduleId: 'layout.hbs',
+          source: 'title="foo"',
+          line: 1,
+          column: 8,
+        },
+        {
+          message:
+            '<iframe> elements must have a unique title property. Value title="foo" already used for different iframe. #1',
+          moduleId: 'layout.hbs',
+          source: '<iframe title="foo" />',
+          line: 1,
+          column: 22,
+        },
+      ],
+    },
+    {
+      template:
+        '<iframe title="foo" /><iframe title="boo" /><iframe title="foo" /><iframe title="boo" />',
+      results: [
+        {
+          message: 'This title is not unique. #1',
+          moduleId: 'layout.hbs',
+          source: 'title="foo"',
+          line: 1,
+          column: 8,
+        },
+        {
+          message:
+            '<iframe> elements must have a unique title property. Value title="foo" already used for different iframe. #1',
+          moduleId: 'layout.hbs',
+          source: '<iframe title="foo" />',
+          line: 1,
+          column: 44,
+        },
+        {
+          message: 'This title is not unique. #2',
+          moduleId: 'layout.hbs',
+          source: 'title="boo"',
+          line: 1,
+          column: 30,
+        },
+        {
+          message:
+            '<iframe> elements must have a unique title property. Value title="boo" already used for different iframe. #2',
+          moduleId: 'layout.hbs',
+          source: '<iframe title="boo" />',
+          line: 1,
+          column: 66,
+        },
+      ],
+    },
+    {
+      template: '<iframe src="12" />',
+
+      result: {
+        message: '<iframe> elements must have a unique title property.',
+        moduleId: 'layout.hbs',
+        source: '<iframe src="12" />',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<iframe src="12" title={{false}} />',
+
+      result: {
+        message: '<iframe> elements must have a unique title property.',
+        moduleId: 'layout.hbs',
+        source: '<iframe src="12" title={{false}} />',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<iframe src="12" title="{{false}}" />',
+
+      result: {
+        message: '<iframe> elements must have a unique title property.',
+        moduleId: 'layout.hbs',
+        source: '<iframe src="12" title="{{false}}" />',
+        line: 1,
+        column: 0,
+      },
+    },
+    {
+      template: '<iframe src="12" title="" />',
+
+      result: {
+        message: '<iframe> elements must have a unique title property.',
+        moduleId: 'layout.hbs',
+        source: '<iframe src="12" title="" />',
+        line: 1,
+        column: 0,
+      },
+    },
+  ],
+});
diff --git a/test/unit/rules/lint-simple-unless-test.js b/test/unit/rules/lint-simple-unless-test.js
index bb3a9078..e5d84ac0 100644
--- a/test/unit/rules/lint-simple-unless-test.js
+++ b/test/unit/rules/lint-simple-unless-test.js
@@ -16,8 +16,6 @@ generateRuleTests({
     '<div class="{{if foo \'foo\'}}"></div>',
     '{{unrelated-mustache-without-params}}',
     '{{#if foo}}{{else}}{{/if}}',
-    '{{#if foo}}{{else}}{{#unless bar}}{{/unless}}{{/if}}',
-    '{{#if foo}}{{else}}{{unless bar someProperty}}{{/if}}',
     '{{#unless (or foo bar)}}order whiskey{{/unless}}',
     '{{#unless (eq (or foo bar) baz)}}order whiskey{{/unless}}',
     ['{{#unless hamburger}}', '  HOT DOG!', '{{/unless}}'].join('\n'),