Skip to content

Commit

Permalink
Fix microsoft#394: Add ignore-case and ignore-whitespace options to r…
Browse files Browse the repository at this point in the history
…eact-a11y-anchors (microsoft#524)

* Add ignore-case and ignore-whitespace options to react-a11y-anchors

* Add unit tests for react-a11y-anchor new options

* Add documentation for react-a11y-anchor new options

* Add new unit tests with no options enabled for react-a11y-anchors

* Update ignore-whitespace to allow trim or all whitespace

* Add and update ignore-whitespace unit tests

* Update ignore-whitespace documentation
  • Loading branch information
nadyafebi authored and Josh Goldberg committed Oct 7, 2018
1 parent 7574010 commit 76d0853
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 7 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>. 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 } ]`<br/>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 `<string>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). <br/><br/>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 #. <br/>References:<br/>[WCAG Rule 38: Link text should be as least four 4 characters long](http://oaa-accessibility.org/wcag20/rule/38/)<br/>[WCAG Rule 39: Links with the same HREF should have the same link text](http://oaa-accessibility.org/wcag20/rule/39/)<br/>[WCAG Rule 41: Links that point to different HREFs should have different link text](http://oaa-accessibility.org/wcag20/rule/41/)<br/>[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/)<br/> | 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 #. <br/>References:<br/>[WCAG Rule 38: Link text should be as least four 4 characters long](http://oaa-accessibility.org/wcag20/rule/38/)<br/>[WCAG Rule 39: Links with the same HREF should have the same link text](http://oaa-accessibility.org/wcag20/rule/39/)<br/>[WCAG Rule 41: Links that point to different HREFs should have different link text](http://oaa-accessibility.org/wcag20/rule/41/)<br/>[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/)<br/> | 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.<br/>References:<br/>[WCAG Rule 94](http://oaa-accessibility.org/wcag20/rule/94/)<br/>[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
Expand Down
76 changes: 70 additions & 6 deletions src/reactA11yAnchorsRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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',
Expand All @@ -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[] = [];
Expand All @@ -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...
Expand All @@ -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...
Expand All @@ -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();
Expand Down
94 changes: 94 additions & 0 deletions src/tests/ReactA11yAnchorsRuleTests.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 = <a href="someRef1">someTitle1</a>;
const anchor2 = <a href="someRef2">someTitle2</a>;
const anchor3 = <a href="someRef1">SomeTitle1</a>;
const anchor4 = <a href="someRef2">sometitle2</a>;
`;

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 = <a href="someRef1">someTitle1</a>;
const anchor2 = <a href="someRef2"><span>someTitle</span><img alt="someAlt2" /></a>;
const anchor3 = <a href="someRef1">someTitle1 </a>;
const anchor4 = <a href="someRef2"><span>someTitle </span><img alt="someAlt2" /></a>
`;

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 = <a href="someRef1">someTitle1</a>;
const anchor2 = <a href="someRef2"><span>someTitle</span><img alt="someAlt2" /></a>;
const anchor3 = <a href="someRef1">s o m e T i t l e 1</a>;
const anchor4 = <a href="someRef2"><span>some Title</span><img alt="someAlt2" /></a>
`;

TestHelper.assertNoViolationWithOptions(ruleName, [true, opt], script);
});

it('should fail when identical hrefs have different texts', () : void => {
const script : string = `
import React = require('react');
Expand All @@ -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 = <a href="someRef">someTitle1</a>;
const anchor2 = <a href="someRef1">someTitle2</a>;
const anchor3 = <a href="someRef">SomeTitle1</a>; // should fail with line 3
const anchor4 = <a href="someRef1">sometitle2</a>; // 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 = <a href="someRef1">someTitle1</a>;
const anchor2 = <a href="someRef2"><span>someTitle</span><img alt="someAlt2" /></a>;
const anchor3 = <a href="someRef1">someTitle1 </a>; // should fail with line 3
const anchor4 = <a href="someRef2"><span>some Title</span><img alt="someAlt2" /></a> // 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');
Expand Down

0 comments on commit 76d0853

Please sign in to comment.