diff --git a/README.md b/README.md index bd161e3d4..f2af75159 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Rule Name | Description | Since `prefer-array-literal` | Use array literal syntax when declaring or instantiating array types. For example, prefer the Javascript form of string[] to the TypeScript form Array. Prefer '[]' to 'new Array()'. Prefer '[4, 5]' to 'new Array(4, 5)'. Prefer '[undefined, undefined]' to 'new Array(4)'. Since 2.0.10, this rule can be configured to allow Array type parameters. To ignore type parameters, configure the rule with the values: `[ true, { 'allow-type-parameters': true } ]`
This rule has some overlap with the [TSLint array-type rule](https://palantir.github.io/tslint/rules/array-type), however, the version here catches more instances. | 1.0, 2.0.10 `prefer-type-cast` | Prefer the tradition type casts instead of the new 'as-cast' syntax. For example, prefer `myVariable` instead of `myVariable as string`. Rule ignores any file ending in .tsx. If you prefer the opposite and want to see the `as type` casts, then enable the tslint rule named 'no-angle-bracket-type-assertion'| 2.0.4 `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).

This rule has some overlap with the [tslint no-floating-promises rule](https://palantir.github.io/tslint/rules/no-floating-promises), but they are substantially different. | 1.0 -`react-a11y-anchors` | For accessibility of your website, anchor element link text should be at least 4 characters long. Links with the same HREF should have the same link text. Links that point to different HREFs should have different link text. Links with images and text content, the alt attribute should be unique to the text content or empty. An an anchor element's href prop value must not be undefined, null, or just #.
References:
[WCAG Rule 38: Link text should be as least four 4 characters long](http://oaa-accessibility.org/wcag20/rule/38/)
[WCAG Rule 39: Links with the same HREF should have the same link text](http://oaa-accessibility.org/wcag20/rule/39/)
[WCAG Rule 41: Links that point to different HREFs should have different link text](http://oaa-accessibility.org/wcag20/rule/41/)
[WCAG Rule 43: Links with images and text content, the alt attribute should be unique to the text content or empty](http://oaa-accessibility.org/wcag20/rule/43/)
| 2.0.11 +`react-a11y-anchors` | For accessibility of your website, anchor element link text should be at least 4 characters long. Links with the same HREF should have the same link text. Links that point to different HREFs should have different link text. This can be relaxed to allow differences in cases using `ignore-case` option. You can also allow differences in leading/trailing whitespace by adding `{"ignore-whitespace": "trim"}` option or all whitespace by adding `{"ignore-whitespace": "all"}` option. Links with images and text content, the alt attribute should be unique to the text content or empty. An an anchor element's href prop value must not be undefined, null, or just #.
References:
[WCAG Rule 38: Link text should be as least four 4 characters long](http://oaa-accessibility.org/wcag20/rule/38/)
[WCAG Rule 39: Links with the same HREF should have the same link text](http://oaa-accessibility.org/wcag20/rule/39/)
[WCAG Rule 41: Links that point to different HREFs should have different link text](http://oaa-accessibility.org/wcag20/rule/41/)
[WCAG Rule 43: Links with images and text content, the alt attribute should be unique to the text content or empty](http://oaa-accessibility.org/wcag20/rule/43/)
| 2.0.11 `react-a11y-aria-unsupported-elements` | For accessibility of your website, enforce that elements that do not support ARIA roles, states, and properties do not have those attributes. | 2.0.11 `react-a11y-event-has-role` | For accessibility of your website, Elements with event handlers must have explicit role or implicit role.
References:
[WCAG Rule 94](http://oaa-accessibility.org/wcag20/rule/94/)
[Using the button role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role) | 2.0.11 `react-a11y-image-button-has-alt` | For accessibility of your website, enforce that inputs element with `type="image"` must have non-empty alt attribute. | 2.0.11 diff --git a/src/reactA11yAnchorsRule.ts b/src/reactA11yAnchorsRule.ts index bf8fb4fe0..2f308b8a9 100644 --- a/src/reactA11yAnchorsRule.ts +++ b/src/reactA11yAnchorsRule.ts @@ -11,6 +11,9 @@ import { isEmpty } from './utils/JsxAttribute'; +export const OPTION_IGNORE_CASE: string = 'ignore-case'; +export const OPTION_IGNORE_WHITESPACE: string = 'ignore-whitespace'; + const ROLE_STRING: string = 'role'; export const NO_HASH_FAILURE_STRING: string = @@ -36,8 +39,21 @@ export class Rule extends Lint.Rules.AbstractRule { ruleName: 'react-a11y-anchors', type: 'functionality', description: 'For accessibility of your website, anchor elements must have a href different from # and a text longer than 4.', - options: null, - optionsDescription: '', + options: { + type: 'array', + items: { + type: 'string', + enum: [OPTION_IGNORE_CASE, OPTION_IGNORE_WHITESPACE] + }, + minLength: 0, + maxLength: 2 + }, + optionsDescription: Lint.Utils.dedent` + Optional arguments to relax the same HREF same link text rule are provided: + * \`${OPTION_IGNORE_CASE}\` ignore differences in cases. + * \`{"${OPTION_IGNORE_WHITESPACE}": "trim"}\` ignore differences only in leading/trailing whitespace. + * \`{"${OPTION_IGNORE_WHITESPACE}": "all"}\` ignore differences in all whitespace. + `, typescriptOnly: true, issueClass: 'Non-SDL', issueType: 'Warning', @@ -60,9 +76,27 @@ export class Rule extends Lint.Rules.AbstractRule { } class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker { - + private ignoreCase: boolean = false; + private ignoreWhitespace: string = ''; private anchorInfoList: IAnchorInfo[] = []; + constructor(sourceFile: ts.SourceFile, options: Lint.IOptions) { + super(sourceFile, options); + this.parseOptions(); + } + + private parseOptions(): void { + this.getOptions().forEach((opt: any) => { + if (typeof opt === 'string' && opt === OPTION_IGNORE_CASE) { + this.ignoreCase = true; + } + + if (typeof opt === 'object') { + this.ignoreWhitespace = opt[OPTION_IGNORE_WHITESPACE]; + } + }); + } + public validateAllAnchors(): void { const sameHrefDifferentTexts: IAnchorInfo[] = []; const differentHrefSameText: IAnchorInfo[] = []; @@ -72,7 +106,7 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker { this.anchorInfoList.forEach((anchorInfo: IAnchorInfo): void => { if (current.href && current.href === anchorInfo.href && - (current.text !== anchorInfo.text || current.altText !== anchorInfo.altText) && + !this.compareAnchorsText(current, anchorInfo) && !Utils.contains(sameHrefDifferentTexts, anchorInfo)) { // Same href - different text... @@ -82,8 +116,7 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker { } if (current.href !== anchorInfo.href && - current.text === anchorInfo.text && - current.altText === anchorInfo.altText && + this.compareAnchorsText(current, anchorInfo) && !Utils.contains(differentHrefSameText, anchorInfo)) { // Different href - same text... @@ -95,6 +128,37 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker { } } + private compareAnchorsText(anchor1: IAnchorInfo, anchor2: IAnchorInfo): boolean { + let text1: string = anchor1.text; + let text2: string = anchor2.text; + let altText1: string = anchor1.altText; + let altText2: string = anchor2.altText; + + if (this.ignoreCase) { + text1 = text1.toLowerCase(); + text2 = text2.toLowerCase(); + altText1 = altText1.toLowerCase(); + altText2 = altText2.toLowerCase(); + } + + if (this.ignoreWhitespace === 'trim') { + text1 = text1.trim(); + text2 = text2.trim(); + altText1 = altText1.trim(); + altText2 = altText2.trim(); + } + + if (this.ignoreWhitespace === 'all') { + const regex: RegExp = /\s/g; + text1 = text1.replace(regex, ''); + text2 = text2.replace(regex, ''); + altText1 = altText1.replace(regex, ''); + altText2 = altText2.replace(regex, ''); + } + + return text1 === text2 && altText1 === altText2; + } + private firstPosition(anchorInfo: IAnchorInfo): string { const startPosition: ts.LineAndCharacter = this.createFailure(anchorInfo.start, anchorInfo.width, '').getStartPosition().getLineAndCharacter(); diff --git a/src/tests/ReactA11yAnchorsRuleTests.ts b/src/tests/ReactA11yAnchorsRuleTests.ts index 0db6b31a7..2f4aca9ec 100644 --- a/src/tests/ReactA11yAnchorsRuleTests.ts +++ b/src/tests/ReactA11yAnchorsRuleTests.ts @@ -1,5 +1,7 @@ import {TestHelper} from './TestHelper'; import { + OPTION_IGNORE_CASE, + OPTION_IGNORE_WHITESPACE, MISSING_HREF_FAILURE_STRING, NO_HASH_FAILURE_STRING, LINK_TEXT_TOO_SHORT_FAILURE_STRING, @@ -259,6 +261,48 @@ describe('reactA11yAnchorsRule', () : void => { TestHelper.assertViolations(ruleName, script, [ ]); }); + it('should pass when identical hrefs have texts with different cases on ignore-case', () : void => { + const script : string = ` + import React = require('react); + const anchor1 = someTitle1; + const anchor2 = someTitle2; + const anchor3 = SomeTitle1; + const anchor4 = sometitle2; + `; + + TestHelper.assertNoViolationWithOptions(ruleName, [true, OPTION_IGNORE_CASE], script); + }); + + it('should pass when identical hrefs have texts with different leading/trailing whitespace on ignore-whitespace trim', () : void => { + const opt : any = {}; + opt[OPTION_IGNORE_WHITESPACE] = 'trim'; + + const script : string = ` + import React = require('react); + const anchor1 = someTitle1; + const anchor2 = someTitlesomeAlt2; + const anchor3 = someTitle1 ; + const anchor4 = someTitle someAlt2 + `; + + TestHelper.assertNoViolationWithOptions(ruleName, [true, opt], script); + }); + + it('should pass when identical hrefs have texts with different whitespace on ignore-whitespace all', () : void => { + const opt : any = {}; + opt[OPTION_IGNORE_WHITESPACE] = 'all'; + + const script : string = ` + import React = require('react); + const anchor1 = someTitle1; + const anchor2 = someTitlesomeAlt2; + const anchor3 = s o m e T i t l e 1; + const anchor4 = some TitlesomeAlt2 + `; + + TestHelper.assertNoViolationWithOptions(ruleName, [true, opt], script); + }); + it('should fail when identical hrefs have different texts', () : void => { const script : string = ` import React = require('react'); @@ -284,6 +328,56 @@ describe('reactA11yAnchorsRule', () : void => { ]); }); + it('should fail when identical hrefs have texts with different cases', () : void => { + const script : string = ` + import React = require('react'); + const anchor1 = someTitle1; + const anchor2 = someTitle2; + const anchor3 = SomeTitle1; // should fail with line 3 + const anchor4 = sometitle2; // should fail with line 4 + `; + + TestHelper.assertViolations(ruleName, script, [ + { + "failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 3`, + "name": "file.tsx", + "ruleName": "react-a11y-anchors", + "startPosition": { "character": 29, "line": 5 } + }, + { + "failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 4`, + "name": "file.tsx", + "ruleName": "react-a11y-anchors", + "startPosition": { "character": 29, "line": 6 } + } + ]); + }); + + it('should fail when identical hrefs have texts with different whitespace', () : void => { + const script : string = ` + import React = require('react); + const anchor1 = someTitle1; + const anchor2 = someTitlesomeAlt2; + const anchor3 = someTitle1 ; // should fail with line 3 + const anchor4 = some TitlesomeAlt2 // should fail with line 4 + `; + + TestHelper.assertViolations(ruleName, script, [ + { + "failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 3`, + "name": "file.tsx", + "ruleName": "react-a11y-anchors", + "startPosition": { "character": 29, "line": 5 } + }, + { + "failure": `${SAME_HREF_SAME_TEXT_FAILURE_STRING} First link at character: 29 line: 4`, + "name": "file.tsx", + "ruleName": "react-a11y-anchors", + "startPosition": { "character": 29, "line": 6 } + } + ]); + }); + it('should fail when identical hrefs have different alt texts', () : void => { const script : string = ` import React = require('react');