From f67d898d74f3106f8cf1c60ba44500d8bc69a783 Mon Sep 17 00:00:00 2001 From: Noam Yogev Date: Sat, 19 Jan 2019 22:52:14 +0200 Subject: [PATCH] Implement react-a11y-iframes rule (#692) * implement reactA11yIFramesRule add unit tests * generate README and tslint.json * generate README and tslint.json * Rename reactA11yIFramesRule.ts to reactA11yIframesRule.ts * Rename reactA11yIFramesRuleTests.ts to reactA11yIframesRuleTests.ts * add call to super, fix README.md, lowercase tsUtils * add tslint tests fix build error (String -> string) * update tslint-warnings after npm install * remove mocha test add comma to rule description fix function names in test.tsx.lint * refactor reactA11yIframsRule to use walk function add unit tests for class and function rule enforcement * remove resetting previousTiltles when JsxFragment is enountered * add some test cases --- README.md | 9 +++ src/reactA11yIframesRule.ts | 82 ++++++++++++++++++++++++++ tests/react-a11y-iframes/test.tsx.lint | 67 +++++++++++++++++++++ tests/react-a11y-iframes/tslint.json | 5 ++ tslint-warnings.csv | 1 + tslint.json | 1 + 6 files changed, 165 insertions(+) create mode 100644 src/reactA11yIframesRule.ts create mode 100644 tests/react-a11y-iframes/test.tsx.lint create mode 100644 tests/react-a11y-iframes/tslint.json diff --git a/README.md b/README.md index 96f4fd4..bd9d161 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,15 @@ We recommend you specify exact versions of lint libraries, including `tslint-mic 6.0.0 + + + react-a11y-iframes + + + Enforce that iframe elements are not empty, have title, and are unique. + + 6.1.0 + informative-docs diff --git a/src/reactA11yIframesRule.ts b/src/reactA11yIframesRule.ts new file mode 100644 index 0000000..e884d5b --- /dev/null +++ b/src/reactA11yIframesRule.ts @@ -0,0 +1,82 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint'; +import * as tsutils from 'tsutils'; + +import { ExtendedMetadata } from './utils/ExtendedMetadata'; +import { getJsxAttributesFromJsxElement } from './utils/JsxAttribute'; + +const IFRAME_ELEMENT_NAME: string = 'iframe'; +const TITLE_ATTRIBUTE_NAME: string = 'title'; +const SRC_ATTRIBUTE_NAME: string = 'src'; +const HIDDEN_ATTRIBUTE_NAME: string = 'hidden'; +const IFRAME_EMPTY_TITLE_ERROR_STRING: string = 'An iframe element must have a non-empty title.'; +const IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING: string = 'An iframe element should not be hidden or empty.'; +const IFRAME_UNIQUE_TITLE_ERROR_STRING: string = 'An iframe element must have a unique title.'; + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: ExtendedMetadata = { + ruleName: 'react-a11y-iframes', + type: 'functionality', + description: 'Enforce that iframe elements are not empty, have title, and are unique.', + options: null, // tslint:disable-line:no-null-keyword + optionsDescription: '', + typescriptOnly: false, + issueClass: 'Non-SDL', + issueType: 'Error', + severity: 'Important', + level: 'Opportunity for Excellence', + group: 'Accessibility' + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return sourceFile.languageVariant === ts.LanguageVariant.JSX ? this.applyWithFunction(sourceFile, walk) : []; + } +} + +function walk(ctx: Lint.WalkContext) { + const previousTitles: Set = new Set(); + function cb(node: ts.Node): void { + if (tsutils.isVariableDeclaration(node) || tsutils.isMethodDeclaration(node) || tsutils.isFunctionDeclaration(node)) { + previousTitles.clear(); + } + if (tsutils.isJsxOpeningElement(node) || tsutils.isJsxSelfClosingElement(node)) { + if (node.tagName.getText() === IFRAME_ELEMENT_NAME) { + const attributes = getJsxAttributesFromJsxElement(node); + // Validate that iframe has a non-empty title + const titleAttribute = attributes[TITLE_ATTRIBUTE_NAME]; + const titleAttributeText = getAttributeText(titleAttribute); + if (!titleAttribute || !titleAttributeText) { + ctx.addFailureAtNode(node.tagName, IFRAME_EMPTY_TITLE_ERROR_STRING); + } + + // Validate the iframe title is unique + if (titleAttributeText && previousTitles.has(titleAttributeText)) { + ctx.addFailureAtNode(node.tagName, IFRAME_UNIQUE_TITLE_ERROR_STRING); + } else if (titleAttributeText) { + previousTitles.add(titleAttributeText); + } + + // Validate that iframe is not empty or hidden + const hiddenAttribute = attributes[HIDDEN_ATTRIBUTE_NAME]; + const srcAttribute = attributes[SRC_ATTRIBUTE_NAME]; + if (hiddenAttribute || !srcAttribute || !getAttributeText(srcAttribute)) { + ctx.addFailureAtNode(node.tagName, IFRAME_EMPTY_OR_HIDDEN_ERROR_STRING); + } + } + } + return ts.forEachChild(node, cb); + } + return ts.forEachChild(ctx.sourceFile, cb); +} + +function getAttributeText(attribute: ts.JsxAttribute): string | undefined { + if (attribute && attribute.initializer) { + if (tsutils.isJsxExpression(attribute.initializer)) { + return attribute.initializer.expression ? attribute.initializer.expression.getText() : undefined; + } + if (tsutils.isStringLiteral(attribute.initializer)) { + return attribute.initializer.text; + } + } + return undefined; +} diff --git a/tests/react-a11y-iframes/test.tsx.lint b/tests/react-a11y-iframes/test.tsx.lint new file mode 100644 index 0000000..55f8a20 --- /dev/null +++ b/tests/react-a11y-iframes/test.tsx.lint @@ -0,0 +1,67 @@ + +const SomeComponent = () => + +const SomeComponent = () => + +const SomeComponent = () => ; + ~~~~~~ [An iframe element should not be hidden or empty.] +const SomeComponent = () => ; + ~~~~~~ [An iframe element should not be hidden or empty.] +const SomeComponent = () => + <> + + + +const SomeComponent = () => + <> + + + ~~~~~~ [An iframe element must have a unique title.] + +class AmazingComponent extends React.Component { + render() { + return ( + <> + + ~~~~~~ [An iframe element should not be hidden or empty.] + + ~~~~~~ [An iframe element should not be hidden or empty.] + + ~~~~~~ [An iframe element should not be hidden or empty.] +