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

Fixed for empty alt value in react-a11y-img-has-alt rule. #260

Closed
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
74 changes: 46 additions & 28 deletions src/reactA11yImgHasAltRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,23 @@ import {
} from './utils/JsxAttribute';
import { isJsxSpreadAttribute } from './utils/TypeGuard';

const roleString: string = 'role';
const altString: string = 'alt';
const ROLE_STRING: string = 'role';
const ALT_STRING: string = 'alt';

export function getFailureStringNoAlt(tagName: string): string {
return `<${tagName}> elements must have an alt attribute or use role='presentation' for presentational images. \
return `<${tagName}> elements must have an non-empty alt attribute or \
use empty alt attribute and role='presentation' for presentational images. \
A reference for the presentation role can be found at https://www.w3.org/TR/wai-aria/roles#presentation.`;
}

export function getFailureStringEmptyAlt(tagName: string): string {
return `The value of 'alt' attribute in <${tagName}> tag is undefined or empty. \
Add more details in 'alt' attribute or use role='presentation' for presentational images. \
A reference for the presentation role can be found at https://www.w3.org/TR/wai-aria/roles#presentation.`;
export function getFailureStringEmptyAltAndNotPresentationRole(tagName: string): string {
return `The value of alt attribute in <${tagName}> tag is empty and role value is not presentation. \
Add more details in alt attribute or specify role attribute to equal 'presentation' when 'alt' attribute is empty.`;
}

export function getFailureStringNonEmptyAltAndPresentationRole(tagName: string): string {
return `The value of alt attribute in <${tagName}> tag is non-empty and role value is presentation. \
Remove role='presentation' or specify 'alt' attributeto be empty when role attributes equals 'presentation'.`;
}

/**
Expand All @@ -31,7 +36,8 @@ export class Rule extends Lint.Rules.AbstractRule {
public static metadata: ExtendedMetadata = {
ruleName: 'react-a11y-img-has-alt',
type: 'maintainability',
description: 'Enforce that an `img` element contains the `alt` attribute or `role="presentation"` for decorative image.',
description: 'Enforce that an img element contains the non-empty alt attribute. ' +
'For decorative images, using empty alt attribute and role="presentation".',
options: 'string[]',
optionExamples: ['true', '[true, ["Image"]]'],
issueClass: 'Non-SDL',
Expand All @@ -49,9 +55,19 @@ export class Rule extends Lint.Rules.AbstractRule {
}

class ImgHasAltWalker extends Lint.RuleWalker {
public visitJsxElement(node: ts.JsxElement): void {
this.checkJsxOpeningElement(node.openingElement);
super.visitJsxElement(node);
}

public visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): void {
this.checkJsxOpeningElement(node);
super.visitJsxSelfClosingElement(node);
}

private checkJsxOpeningElement(node: ts.JsxOpeningElement): void {
// Tag name is sensitive on lowercase or uppercase, we shoudn't normalize tag names in this rule.
const tagName: string = node.tagName && node.tagName.getText();
const tagName: string = node.tagName.getText();
const options: any[] = this.getOptions(); // tslint:disable-line:no-any

// The additionalTagNames are specified by tslint config to check not only 'img' tag but also customized tag.
Expand All @@ -71,31 +87,33 @@ class ImgHasAltWalker extends Lint.RuleWalker {
}

const attributes: { [propName: string]: ts.JsxAttribute } = getJsxAttributesFromJsxElement(node);
const role: ts.JsxAttribute = attributes[roleString];
const roleValue: string = role && getStringLiteral(role);
const altAttribute: ts.JsxAttribute = attributes[ALT_STRING];

// if <img> element has role of 'presentation', it's presentational image, don't check it;
// @example <img role='presentation' />
if (roleValue && roleValue.match(/\bpresentation\b/)) {
return;
}

const altProp: ts.JsxAttribute = attributes[altString];

if (!altProp) {
if (!altAttribute) {
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureStringNoAlt(tagName)
));
} else if (isEmpty(altProp) || getStringLiteral(altProp) === '') {
this.addFailure(this.createFailure(
altProp.getStart(),
altProp.getWidth(),
getFailureStringEmptyAlt(tagName)
));
} else {
const roleAttribute: ts.JsxAttribute = attributes[ROLE_STRING];
const roleAttributeValue: string = roleAttribute ? getStringLiteral(roleAttribute) : '';
const isPresentationRole: boolean = !!roleAttributeValue.toLowerCase().match(/\bpresentation\b/);
const isEmptyAlt: boolean = isEmpty(altAttribute) || getStringLiteral(altAttribute) === '';

if (!isEmptyAlt && isPresentationRole) { // <img alt='altValue' role='presentation' />
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureStringNonEmptyAltAndPresentationRole(tagName)
));
} else if (isEmptyAlt && !isPresentationRole) { // <img alt='' />
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureStringEmptyAltAndNotPresentationRole(tagName)
));
}
}

super.visitJsxSelfClosingElement(node);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React = require('react');
let Picture;
const a = <Picture alt='' />
const b = <Picture alt role='button img' />
const c = <Picture alt={''} />
const c = <Picture alt={''} role='button' />
const d = <img alt='' />
const e = <img aLt />
const f = <img alt={''} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React = require('react');

let Picture;
let altValue;

const a = <Picture alt='altValue' role='presentation' />
const b = <Picture alt={ 'altValue' } role={ 'presentation' } />
const c = <Picture alt={ altValue } role='Presentation' />
const d = <img alt='altValue' role='presentation button' />
const e = <img aLt='altValue' role='presentation img' />
const f = <img alt={ altValue } role='presentation' />
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React = require('react');

let Picture;

const a = <Picture role='presentation' alt />
const b = <Picture role={'presentation'} alt='' />
const c = <Picture role='button presentation' alt={undefined} />
const d = <img role='presentation' alt={ null } />
const e = <img role={'presentation'} alt={ '' } />
const f = <img role='button presentation' alt />
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React = require('react');

let Picture, validAltValue;

const a = <Picture alt='validAltValue' />
const b = <Picture alt={validAltValue} />
const a = <Picture alt='validAltValue' role='button' />
const b = <Picture alt={validAltValue} role='link' />
const c = <Picture alt={'validAltValue'} />
const d = <img alt='validAltValue' />
const d = <img alt='validAltValue' role='button link' />
const e = <img aLt={validAltValue} />
const f = <img alt={'validAltValue'} />

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React = require('react');

const a = <img alt />
const a = <img alt role='button'/>
const b = <img Alt='' />
const c = <img ALT={undefined} />
const d = <img alt={''} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React = require('react');

let altValue;

const a = <img alt='altValue' role='presentation' />
const b = <img Alt='altValue' role='presentation button' />
const c = <img ALT={ altValue } role={ 'presentation' } />
const d = <img alt={'altValue'} role='Presentation' />
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React = require('react');

const a = <img role='presentation' alt />
const b = <img role='presentation' alt='' />
const c = <img role='button presentation' alt={ undefined } />
const d = <img role={'presentation button'} alt={''} />

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import React = require('react');

const a = <img />
const b = <img alt=''/>
let Img;
let IMG;

const a = <Img />
const b = <IMG alt=''/>
2 changes: 1 addition & 1 deletion tests/ReactA11yProptypesRuleTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ describe('reactA11yProptypesRule', () => {
const fileDirectory: string = 'test-data/ReactA11yProptypes/PassingTestInputs/';

it('when can not check the type of attribute value until running time', () => {
const fileName: string = fileDirectory + 'canNotCheckUtilRunTime';
const fileName: string = fileDirectory + 'canNotCheckUntilRunTime.tsx';
TestHelper.assertNoViolation(ruleName, fileName);
});

Expand Down
Loading