Skip to content
This repository has been archived by the owner on Jul 15, 2023. It is now read-only.

Fix #434: react-a11y-anchors should warn when there is no href attribute #519

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,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 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. 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
19 changes: 13 additions & 6 deletions src/reactA11yAnchorsRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import {Utils} from './utils/Utils';
import {getImplicitRole} from './utils/getImplicitRole';
import {
getJsxAttributesFromJsxElement,
getStringLiteral
getStringLiteral,
isEmpty
} from './utils/JsxAttribute';

const ROLE_STRING: string = 'role';

export const NO_HASH_FAILURE_STRING: string =
'Do not use # as anchor href.';
export const MISSING_HREF_FAILURE_STRING: string =
'Do not leave href undefined or null';
export const LINK_TEXT_TOO_SHORT_FAILURE_STRING: string =
'Link text or the alt text of image in link should be at least 4 characters long. ' +
'If you are not using <a> element as anchor, please specify explicit role, e.g. role=\'button\'';
Expand Down Expand Up @@ -115,15 +118,20 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker {

private validateAnchor(parent: ts.Node, openingElement: ts.JsxOpeningLikeElement): void {
if (openingElement.tagName.getText() === 'a') {
const hrefAttribute = this.getAttribute(openingElement, 'href');

const anchorInfo: IAnchorInfo = {
href: this.getAttribute(openingElement, 'href') || '',
href: hrefAttribute ? getStringLiteral(hrefAttribute) || '' : '',
text: this.anchorText(parent),
altText: this.imageAlt(parent),
start: parent.getStart(),
width: parent.getWidth()
};

if (isEmpty(hrefAttribute)) {
this.addFailureAt(anchorInfo.start, anchorInfo.width, MISSING_HREF_FAILURE_STRING);
}

if (anchorInfo.href === '#') {
this.addFailureAt(anchorInfo.start, anchorInfo.width, NO_HASH_FAILURE_STRING);
}
Expand All @@ -147,10 +155,9 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker {
}
}

private getAttribute(openingElement: ts.JsxOpeningLikeElement, attributeName: string): string | undefined {
private getAttribute(openingElement: ts.JsxOpeningLikeElement, attributeName: string): ts.JsxAttribute {
const attributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(openingElement);
const attribute: ts.JsxAttribute = attributes[attributeName];
return attribute ? getStringLiteral(attribute) : '';
return attributes[attributeName];
}

/**
Expand Down Expand Up @@ -191,7 +198,7 @@ class ReactA11yAnchorsRuleWalker extends ErrorTolerantWalker {

private imageAltAttribute(openingElement: ts.JsxOpeningLikeElement): string {
if (openingElement.tagName.getText() === 'img') {
const altAttribute = this.getAttribute(openingElement, 'alt');
const altAttribute = getStringLiteral(this.getAttribute(openingElement, 'alt'));
return altAttribute === undefined ? '<unknown>' : altAttribute;
}

Expand Down
95 changes: 75 additions & 20 deletions src/tests/ReactA11yAnchorsRuleTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {TestHelper} from './TestHelper';
import {
MISSING_HREF_FAILURE_STRING,
NO_HASH_FAILURE_STRING,
LINK_TEXT_TOO_SHORT_FAILURE_STRING,
UNIQUE_ALT_FAILURE_STRING,
Expand Down Expand Up @@ -34,6 +35,43 @@ describe('reactA11yAnchorsRule', () : void => {
TestHelper.assertViolations(ruleName, script, [ ]);
});

it('should fail on null or undefined function calls', () : void => {
const script : string = `
import React = require('react');
const anchor1 = <a href={null}>someTitle</a>
const anchor2 = <a href={undefined}>someTitle</a>
const anchor3 = <a href={null} role="button"/>
const anchor4 = <a href={undefined} role="button"/>
`;

TestHelper.assertViolations(ruleName, script, [
{
"failure": MISSING_HREF_FAILURE_STRING,
"name": "file.tsx",
"ruleName": "react-a11y-anchors",
"startPosition": { "character": 29, "line": 3 }
},
{
"failure": MISSING_HREF_FAILURE_STRING,
"name": "file.tsx",
"ruleName": "react-a11y-anchors",
"startPosition": { "character": 29, "line": 4 }
},
{
"failure": MISSING_HREF_FAILURE_STRING,
"name": "file.tsx",
"ruleName": "react-a11y-anchors",
"startPosition": { "character": 29, "line": 5 }
},
{
"failure": MISSING_HREF_FAILURE_STRING,
"name": "file.tsx",
"ruleName": "react-a11y-anchors",
"startPosition": { "character": 29, "line": 6 }
}
]);
});

it('should fail on self closing anchor without link text', () : void => {
const script : string = `
import React = require('react');
Expand Down Expand Up @@ -88,6 +126,36 @@ describe('reactA11yAnchorsRule', () : void => {
]);
});

it('shoud fail when href is not defined', (): void => {
const script : string = `
import React = require('react');
const anchor1 = <a>someTitle1</a>;
const anchor2 = <a />;
const anchor3 = <a role="role1" />;
`;

TestHelper.assertViolations(ruleName, script, [
{
"failure": MISSING_HREF_FAILURE_STRING,
"name": "file.tsx",
"ruleName": "react-a11y-anchors",
"startPosition": {"character": 29, "line": 3}
},
{
"failure": MISSING_HREF_FAILURE_STRING,
"name": "file.tsx",
"ruleName": "react-a11y-anchors",
"startPosition": {"character": 29, "line": 4}
},
{
"failure": MISSING_HREF_FAILURE_STRING,
"name": "file.tsx",
"ruleName": "react-a11y-anchors",
"startPosition": {"character": 29, "line": 5}
}
]);
});

describe('Link text should be at least 4 characters long', (): void => {
it('should pass when length of text equals or larger than 4', () => {
const script: string = `
Expand All @@ -103,12 +171,11 @@ describe('reactA11yAnchorsRule', () : void => {
});

it('should pass when role is not link and length of text less than 4', () => {
// Anchor without 'href' attribute has no corresponding role.
const script: string = `
import React = require('react');
const anchor1 = <a role='button'>add</a>;
const anchor2 = <a role='button'><span className='iconClass'></span></a>;
const anchor3 = <a><img src='someURL' alt='someAlt' /></a>;
const anchor1 = <a href='href1' role='button'>add</a>;
const anchor2 = <a href='href2' role='button'><span className='iconClass'></span></a>;
const anchor3 = <a href='href3'><img src='someURL' alt='someAlt' /></a>;
`;

TestHelper.assertNoViolation(ruleName, script);
Expand Down Expand Up @@ -176,29 +243,17 @@ describe('reactA11yAnchorsRule', () : void => {
TestHelper.assertViolations(ruleName, scriptWithAltText, [ ]);
});

it('shoud pass when hrefs undefiend and texts are variant', (): void => {
const script : string = `
import React = require('react');
const anchor1 = <a>someTitle1</a>;
const anchor2 = <a>someTitle2</a>;
const anchor3 = <a>someTitle3</a>;
const anchor4 = <a>someTitle4</a>;
`;

TestHelper.assertViolations(ruleName, script, [ ]);
});

it('should pass when hrefs and texts both are different', () : 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="someRef3">someTitle3</a>;
const anchor4 = <a href="someRef4">someTitle4</a>;
const anchor5 = <a href="someRef5"><img alt="someAlt1" /><a>;
const anchor6 = <a href="someRef6"><img alt="someAlt2" /><a>;
const anchor7 = <a href="someRef7"><img alt="someAlt3" /><a>;
const anchor8 = <a href="someRef8"><img alt="someAlt4" /><a>;
const anchor5 = <a href="someRef5"><img alt="someAlt1" /></a>;
const anchor6 = <a href="someRef6"><img alt="someAlt2" /></a>;
const anchor7 = <a href="someRef7"><img alt="someAlt3" /></a>;
const anchor8 = <a href="someRef8"><img alt="someAlt4" /></a>;
`;

TestHelper.assertViolations(ruleName, script, [ ]);
Expand Down