diff --git a/README.md b/README.md index a6057f329..dba355e90 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Rule Name | Description | Since `promise-must-complete` | When a Promise instance is created, then either the reject() or resolve() parameter must be called on it within all code branches in the scope. For more examples see the [feature request](https://github.com/Microsoft/tslint-microsoft-contrib/issues/34). | 1.0 `react-iframe-missing-sandbox` | React iframes must specify a sandbox attribute. If specified as an empty string, this attribute enables extra restrictions on the content that can appear in the inline frame. The value of the attribute can either be an empty string (all the restrictions are applied), or a space-separated list of tokens that lift particular restrictions. You many not use both allow-scripts and allow-same-origin at the same time, as that allows the embedded document to programmatically remove the sandbox attribute in some scenarios. | 2.0.10 `react-a11y-lang` | For accessibility of your website, html elements must have a lang attribute.
References:
* [H58: Using language attributes to identify changes in the human language](https://www.w3.org/TR/WCAG20-TECHS/H58.html)
* [lang attribute must have a valid value](https://dequeuniversity.com/rules/axe/1.1/valid-lang) | 2.0.11 -`react-a11y-meta` | ... todo | 2.0.11 +`react-a11y-meta` | For accessibility of your website, HTML meta elements must not have http-equiv="refresh". | 2.0.11 `react-a11y-titles` | For accessibility of your website, HTML title elements must not be empty.
References:
* [WCAG 2.0 - Requirement 2.4.2 Page Titled (Level A)](http://www.w3.org/TR/WCAG20/#navigation-mechanisms-title)
* [OAA-Accessibility Rule 13: Title element should not be empty](http://oaa-accessibility.org/wcag20/rule/13/) | 2.0.11 `react-no-dangerous-html` | Do not use React's dangerouslySetInnerHTML API. This rule finds usages of the dangerouslySetInnerHTML API (but not any JSX references). For more info see the [react-no-dangerous-html Rule wiki page](https://github.com/Microsoft/tslint-microsoft-contrib/wiki/react-no-dangerous-html-Rule). | 0.0.2 `react-this-binding-issue` | Several errors can occur when using React and React.Component subclasses. When using React components you must be careful to correctly bind the 'this' reference on any methods that you pass off to child components as callbacks. For example, it is common to define a private method called 'onClick' and then specify `onClick={this.onClick}` as a JSX attribute. If you do this then the 'this' reference will be undefined when your private method is invoked. The React documentation suggests that you bind the 'this' reference on all of your methods within the constructor: `this.onClick = this.onClick.bind(this);`. This rule will create a violation if 1) a method reference is passed to a JSX attribute without being bound in the constructor. And 2) a method is bound multiple times in the constructor. Another issue that can occur is binding the 'this' reference to a function within the render() method. For example, many people will create an anonymous lambda within the JSX attribute to avoid the 'this' binding issue: `onClick={() => { this.onClick(); }}`. The problem with this is that a new instance of an anonymous function is created every time render() is invoked. When React compares virutal DOM properties within shouldComponentUpdate() then the onClick property will look like a new property and force a re-render. You should avoid this pattern because creating function instances within render methods breaks any logic within shouldComponentUpdate() methods. This rule creates violations if 1) an anonymous function is passed as a JSX attribute. And 2) if a function instantiated in local scope is passed as a JSX attribute. This rule can be configured via the "allow-anonymous-listeners" parameter. If you want to suppress violations for the anonymous listener scenarios then configure that rule like this: `"react-this-binding-issue": [ true, { 'allow-anonymous-listeners': true } ]` | 2.0.8, 2.0.9 diff --git a/recommended_ruleset.js b/recommended_ruleset.js index 254d65d9e..22cf68325 100644 --- a/recommended_ruleset.js +++ b/recommended_ruleset.js @@ -137,6 +137,7 @@ module.exports = { * experience for keyboard and screen reader users. */ "react-a11y-lang": true, + "react-a11y-meta": true, "react-a11y-titles": true, /** diff --git a/src/reactA11yMetaRule.ts b/src/reactA11yMetaRule.ts new file mode 100644 index 000000000..565b1d177 --- /dev/null +++ b/src/reactA11yMetaRule.ts @@ -0,0 +1,74 @@ +import * as ts from 'typescript'; +import * as Lint from 'tslint/lib/lint'; + +import {ErrorTolerantWalker} from './utils/ErrorTolerantWalker'; +import {ExtendedMetadata} from './utils/ExtendedMetadata'; +import {SyntaxKind} from './utils/SyntaxKind'; + +const FAILURE_STRING: string = 'Do not use http-equiv="refresh"'; + +/** + * Implementation of the react-a11y-meta rule. + */ +export class Rule extends Lint.Rules.AbstractRule { + + public static metadata: ExtendedMetadata = { + ruleName: 'react-a11y-meta', + type: 'functionality', + description: '... add a meaningful one line description', + options: null, + issueClass: 'Ignored', + issueType: 'Warning', + severity: 'Low', + level: 'Opportunity for Excellence', + group: 'Accessibility' + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new ReactA11yMetaRuleWalker(sourceFile, this.getOptions())); + } +} + +class ReactA11yMetaRuleWalker extends ErrorTolerantWalker { + + + protected visitJsxElement(node: ts.JsxElement): void { + this.validateOpeningElement(node, node.openingElement); + super.visitJsxElement(node); + } + + protected visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void { + this.validateOpeningElement(node, node); + } + + private validateOpeningElement(parent: ts.Node, openElement: ts.JsxOpeningElement): void { + if (openElement.tagName.getText() === 'meta') { + const attributes: ts.NodeArray = openElement.attributes; + attributes.forEach((parameter: ts.JsxAttribute | ts.JsxSpreadAttribute): void => { + if (parameter.kind === SyntaxKind.current().JsxAttribute) { + const attribute: ts.JsxAttribute = parameter; + if (attribute.name.getText() === 'http-equiv') { + if (attribute.initializer != null) { + if (attribute.initializer.kind === SyntaxKind.current().StringLiteral) { + const value: string = (attribute.initializer).text; + if (value === 'refresh') { + this.addFailure(this.createFailure(parent.getStart(), openElement.getWidth(), FAILURE_STRING)); + } + } else if (attribute.initializer.kind === SyntaxKind.current().JsxExpression) { + const exp: ts.JsxExpression = attribute.initializer; + if (exp.expression.kind === SyntaxKind.current().StringLiteral) { + const value: string = (exp.expression).text; + if (value === 'refresh') { + this.addFailure( + this.createFailure(openElement.getStart(), openElement.getWidth(), FAILURE_STRING) + ); + } + } + } + } + } + } + }); + } + } +} diff --git a/tests/ReactA11yMetaRuleTests.ts b/tests/ReactA11yMetaRuleTests.ts new file mode 100644 index 000000000..5f0ce319a --- /dev/null +++ b/tests/ReactA11yMetaRuleTests.ts @@ -0,0 +1,74 @@ +/// +/// + +import {TestHelper} from './TestHelper'; + +/** + * Unit tests. + */ +describe('reactA11yMetaRule', () : void => { + + const ruleName : string = 'react-a11y-meta'; + + it('should pass on meta tags without refresh', () : void => { + const script : string = ` + import React = require('react'); + + const x = + `; + + TestHelper.assertViolations(ruleName, script, [ ]); + }); + + it('should fail on meta tags with refresh - self closing', () : void => { + const script : string = ` + import React = require('react'); + + const x = + `; + + TestHelper.assertViolations(ruleName, script, [ + { + "failure": "Do not use http-equiv=\"refresh\"", + "name": "file.tsx", + "ruleName": "react-a11y-meta", + "startPosition": { "character": 23, "line": 4 } + } + ]); + }); + + it('should fail on meta tags with refresh', () : void => { + const script : string = ` + import React = require('react'); + + const x = + `; + + TestHelper.assertViolations(ruleName, script, [ + { + "failure": "Do not use http-equiv=\"refresh\"", + "name": "file.tsx", + "ruleName": "react-a11y-meta", + "startPosition": { "character": 23, "line": 4 } + } + ]); + }); + + it('should fail on meta tags with refresh - self-closing', () : void => { + const script : string = ` + import React = require('react'); + + const x = + `; + + TestHelper.assertViolations(ruleName, script, [ + { + "failure": "Do not use http-equiv=\"refresh\"", + "name": "file.tsx", + "ruleName": "react-a11y-meta", + "startPosition": { "character": 23, "line": 4 } + } + ]); + }); + +}); diff --git a/tslint.json b/tslint.json index fd663d22c..3d8b58e05 100644 --- a/tslint.json +++ b/tslint.json @@ -203,6 +203,7 @@ "check-separator", "check-type" ], - "react-a11y-lang": true + "react-a11y-lang": true, + "react-a11y-meta": true } } \ No newline at end of file