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

Commit

Permalink
[Issue #212,#213,#214,#215,#216] Added five a11y rules.
Browse files Browse the repository at this point in the history
react-a11y-img-has-alt
react-a11y-props
react-a11y-role-has-required-aria-props
react-a11y-role
react-a11y-role-supports-aria-props
  • Loading branch information
t-ligu authored and HamletDRC committed Sep 2, 2016
1 parent 3583502 commit 82a9c39
Show file tree
Hide file tree
Showing 76 changed files with 2,887 additions and 2,800 deletions.
98 changes: 52 additions & 46 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

var _ = require('underscore');

module.exports = function(grunt) {
module.exports = function (grunt) {

let additionalMetadata;
let allCweDescriptions;
Expand Down Expand Up @@ -36,7 +36,7 @@ module.exports = function(grunt) {
}

function createCweDescription(metadata) {
allCweDescriptions = allCweDescriptions || grunt.file.readJSON('./cwe_descriptions.json', {encoding: 'UTF-8'});
allCweDescriptions = allCweDescriptions || grunt.file.readJSON('./cwe_descriptions.json', { encoding: 'UTF-8' });

const cwe = getMetadataValue(metadata, 'commonWeaknessEnumeration', true, true);
if (cwe === '') {
Expand All @@ -62,7 +62,7 @@ module.exports = function(grunt) {
}

function getMetadataValue(metadata, name, allowEmpty, doNotEscape) {
additionalMetadata = additionalMetadata || grunt.file.readJSON('./additional_rule_metadata.json', {encoding: 'UTF-8'});
additionalMetadata = additionalMetadata || grunt.file.readJSON('./additional_rule_metadata.json', { encoding: 'UTF-8' });

let value = metadata[name];
if (value == null) {
Expand Down Expand Up @@ -94,7 +94,7 @@ module.exports = function(grunt) {
}

function camelize(input) {
return _(input).reduce(function(memo, element) {
return _(input).reduce(function (memo, element) {
if (element.toLowerCase() === element) {
memo = memo + element;
} else {
Expand All @@ -107,7 +107,7 @@ module.exports = function(grunt) {
function getAllRuleNames(options) {
options = options || { skipTsLintRules: false }

var convertToRuleNames = function(filename) {
var convertToRuleNames = function (filename) {
filename = filename
.replace(/Rule\..*/, '') // file extension plus Rule name
.replace(/.*\//, ''); // leading path
Expand All @@ -126,7 +126,7 @@ module.exports = function(grunt) {

function getAllFormatterNames() {

var convertToRuleNames = function(filename) {
var convertToRuleNames = function (filename) {
filename = filename
.replace(/Formatter\..*/, '') // file extension plus Rule name
.replace(/.*\//, ''); // leading path
Expand All @@ -139,7 +139,7 @@ module.exports = function(grunt) {
}

function camelCase(input) {
return input.toLowerCase().replace(/-(.)/g, function(match, group1) {
return input.toLowerCase().replace(/-(.)/g, function (match, group1) {
return group1.toUpperCase();
});
}
Expand Down Expand Up @@ -185,6 +185,12 @@ module.exports = function(grunt) {
dest: 'dist/build'
}
]
},
json: {
expand: true,
cwd: '.',
src: ['src/**/*.json'],
dest: 'dist/'
}
},

Expand All @@ -193,7 +199,6 @@ module.exports = function(grunt) {
src: ['dist/tests/**/*.js']
}
},

ts: {
default: {
src: [
Expand Down Expand Up @@ -240,7 +245,7 @@ module.exports = function(grunt) {
},
tests: {
options: {
configuration: (function() {
configuration: (function () {
let tslintJson = grunt.file.readJSON("tslint.json", { encoding: 'UTF-8' });
tslintJson.rules['no-multiline-string'] = false;
tslintJson.rules['quotemark'] = false;
Expand Down Expand Up @@ -293,26 +298,26 @@ module.exports = function(grunt) {
grunt.registerTask('validate-documentation', 'A task that validates that all rules defined in src are documented in README.md\n' +
'and validates that the package.json version is the same version defined in README.md', function () {

var readmeText = grunt.file.read('README.md', { encoding: 'UTF-8' });
var packageJson = grunt.file.readJSON('package.json', { encoding: 'UTF-8' });
getAllRuleNames({ skipTsLintRules: true }).forEach(function(ruleName) {
if (readmeText.indexOf(ruleName) === -1) {
grunt.fail.warn('A rule was found that is not documented in README.md: ' + ruleName);
}
});
getAllFormatterNames().forEach(function(formatterName) {
if (readmeText.indexOf(formatterName) === -1) {
grunt.fail.warn('A formatter was found that is not documented in README.md: ' + formatterName);
var readmeText = grunt.file.read('README.md', { encoding: 'UTF-8' });
var packageJson = grunt.file.readJSON('package.json', { encoding: 'UTF-8' });
getAllRuleNames({ skipTsLintRules: true }).forEach(function (ruleName) {
if (readmeText.indexOf(ruleName) === -1) {
grunt.fail.warn('A rule was found that is not documented in README.md: ' + ruleName);
}
});
getAllFormatterNames().forEach(function (formatterName) {
if (readmeText.indexOf(formatterName) === -1) {
grunt.fail.warn('A formatter was found that is not documented in README.md: ' + formatterName);
}
});

if (readmeText.indexOf('\nVersion ' + packageJson.version + ' ') === -1) {
grunt.fail.warn('Version not documented in README.md correctly.\n' +
'package.json declares: ' + packageJson.version + '\n' +
'README.md declares something different.');
}
});

if (readmeText.indexOf('\nVersion ' + packageJson.version + ' ') === -1) {
grunt.fail.warn('Version not documented in README.md correctly.\n' +
'package.json declares: ' + packageJson.version + '\n' +
'README.md declares something different.');
}
});

grunt.registerTask('validate-config', 'A task that makes sure all the rules in the project are defined to run during the build.', function () {

var tslintConfig = grunt.file.readJSON('tslint.json', { encoding: 'UTF-8' });
Expand All @@ -323,7 +328,7 @@ module.exports = function(grunt) {
'no-empty-line-after-opening-brace': true
};
var errors = [];
getAllRuleNames().forEach(function(ruleName) {
getAllRuleNames().forEach(function (ruleName) {
if (rulesToSkip[ruleName]) {
return;
}
Expand All @@ -346,7 +351,7 @@ module.exports = function(grunt) {
const procedure = 'TSLint Procedure';
const header = 'Title,Description,ErrorID,Tool,IssueClass,IssueType,SDL Bug Bar Severity,' +
'SDL Level,Resolution,SDL Procedure,CWE,CWE Description';
getAllRules().forEach(function(ruleFile) {
getAllRules().forEach(function (ruleFile) {
const metadata = getMetadataFromFile(ruleFile);

const issueClass = getMetadataValue(metadata, 'issueClass');
Expand All @@ -367,15 +372,15 @@ module.exports = function(grunt) {
});
rows.sort();
rows.unshift(header);
grunt.file.write('tslint-warnings.csv', rows.join('\n'), {encoding: 'UTF-8'});
grunt.file.write('tslint-warnings.csv', rows.join('\n'), { encoding: 'UTF-8' });

});

grunt.registerTask('generate-recommendations', 'A task that generates the recommended_ruleset.js file', function () {

const groupedRows = {};

getAllRules().forEach(function(ruleFile) {
getAllRules().forEach(function (ruleFile) {
const metadata = getMetadataFromFile(ruleFile);

const groupName = getMetadataValue(metadata, 'group');
Expand All @@ -396,20 +401,20 @@ module.exports = function(grunt) {

_.values(groupedRows).forEach(function (element) { element.sort(); });

let data = grunt.file.read('./templates/recommended_ruleset.js.snippet', {encoding: 'UTF-8'});
data = data.replace('%security_rules%', groupedRows['Security'].join('\n'));
data = data.replace('%correctness_rules%', groupedRows['Correctness'].join('\n'));
data = data.replace('%clarity_rules%', groupedRows['Clarity'].join('\n'));
data = data.replace('%whitespace_rules%', groupedRows['Whitespace'].join('\n'));
data = data.replace('%configurable_rules%', groupedRows['Configurable'].join('\n'));
data = data.replace('%deprecated_rules%', groupedRows['Deprecated'].join('\n'));
let data = grunt.file.read('./templates/recommended_ruleset.js.snippet', { encoding: 'UTF-8' });
data = data.replace('%security_rules%', groupedRows['Security'].join('\n'));
data = data.replace('%correctness_rules%', groupedRows['Correctness'].join('\n'));
data = data.replace('%clarity_rules%', groupedRows['Clarity'].join('\n'));
data = data.replace('%whitespace_rules%', groupedRows['Whitespace'].join('\n'));
data = data.replace('%configurable_rules%', groupedRows['Configurable'].join('\n'));
data = data.replace('%deprecated_rules%', groupedRows['Deprecated'].join('\n'));
data = data.replace('%accessibilityy_rules%', groupedRows['Accessibility'].join('\n'));
grunt.file.write('recommended_ruleset.js', data, {encoding: 'UTF-8'});
grunt.file.write('recommended_ruleset.js', data, { encoding: 'UTF-8' });
});

grunt.registerTask('generate-default-tslint-json', 'A task that converts recommended_ruleset.js to ./dist/build/tslint.json', function () {
const data = require('./recommended_ruleset.js');
grunt.file.write('./dist/build/tslint.json', JSON.stringify(data, null, 2), {encoding: 'UTF-8'});
grunt.file.write('./dist/build/tslint.json', JSON.stringify(data, null, 2), { encoding: 'UTF-8' });
});

grunt.registerTask('create-rule', 'A task that creates a new rule from the rule templates. --rule-name parameter required', function () {
Expand All @@ -430,21 +435,22 @@ module.exports = function(grunt) {
var testFileName = './tests/' + ruleFile.charAt(0).toUpperCase() + ruleFile.substr(1) + 'Tests.ts';
var walkerName = ruleFile.charAt(0).toUpperCase() + ruleFile.substr(1) + 'Walker';

var ruleTemplateText = grunt.file.read('./templates/rule.snippet', {encoding: 'UTF-8'});
var testTemplateText = grunt.file.read('./templates/rule-tests.snippet', {encoding: 'UTF-8'});
var ruleTemplateText = grunt.file.read('./templates/rule.snippet', { encoding: 'UTF-8' });
var testTemplateText = grunt.file.read('./templates/rule-tests.snippet', { encoding: 'UTF-8' });

grunt.file.write(sourceFileName, applyTemplates(ruleTemplateText), {encoding: 'UTF-8'});
grunt.file.write(testFileName, applyTemplates(testTemplateText), {encoding: 'UTF-8'});
grunt.file.write(sourceFileName, applyTemplates(ruleTemplateText), { encoding: 'UTF-8' });
grunt.file.write(testFileName, applyTemplates(testTemplateText), { encoding: 'UTF-8' });

var currentRuleset = grunt.file.readJSON('./tslint.json', {encoding: 'UTF-8'});
var currentRuleset = grunt.file.readJSON('./tslint.json', { encoding: 'UTF-8' });
currentRuleset.rules[ruleName] = true;
grunt.file.write('./tslint.json', JSON.stringify(currentRuleset, null, 2), {encoding: 'UTF-8'});
grunt.file.write('./tslint.json', JSON.stringify(currentRuleset, null, 2), { encoding: 'UTF-8' });
}
});

grunt.registerTask('all', 'Performs a cleanup and a full build with all tasks', [
'clean',
'ts',
'copy:json',
'ts:default',
'mochaTest',
'tslint',
'validate-documentation',
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ Supported Rules

Rule Name | Description | Since
:---------- | :------------ | -------------
`a11y-img-has-alt` | Enforce that an `img` element contains the `alt` attribute or `role='presentation'` for decorative image. All images must have `alt` text to convey their purpose and meaning to **screen reader users**. Besides, the `alt` attribute specifies an alternate text for an image, if the image cannot be displayed. | 2.0.11
`a11y-props` | Enforce all `aria-*` attributes are valid. Elements cannot use an invalid `aria-*` attribute. This rule will fail if it finds an `aria-*` attribute that is not listed in [WAI-ARIA states and properties](https://www.w3.org/TR/wai-aria/states_and_properties#state_prop_def). | 2.0.11
`a11y-role-has-required-aria-props` | Elements with aria roles must have all required attributes according to the role. | 2.0.11
`a11y-role` | Elements with aria roles must use a **valid**, **non-abstract** aria role. A reference to role defintions can be found at [WAI-ARIA roles](https://www.w3.org/TR/wai-aria/roles#role_definitions). | 2.0.11
`a11y-role-supports-aria-props` | Enforce that elements with explicit or implicit roles defined contain only `aria-*` properties supported by that `role`. Many aria attributes (states and properties) can only be used on elements with particular roles. Some elements have implicit roles, such as `<a href='hrefValue' />`, which will be resolved to `role='link'`. A reference for the implicit roles can be found at [Default Implicit ARIA Semantics](https://www.w3.org/TR/html-aria/#sec-strong-native-semantics). | 2.0.11
`a11y-tabindex-no-positive` | Enforce tabindex value is **not greater than zero**. Avoid positive tabindex attribute values to synchronize the flow of the page with keyboard tab order. | 2.0.11
`chai-prefer-contains-to-index-of` | Avoid Chai assertions that invoke indexOf and compare for a -1 result. It is better to use the chai .contain() assertion API instead because the failure message will be more clearer if the test fails. | 2.0.10
`chai-vague-errors` | Avoid Chai assertions that result in vague errors. For example, asserting `expect(something).to.be.true` will result in the failure message "Expected true received false". This is a vague error message that does not reveal the underlying problem. It is especially vague in TypeScript because stack trace line numbers often do not match the source code. A better pattern to follow is the xUnit Patterns [Assertion Message](http://xunitpatterns.com/Assertion%20Message.html) pattern. The previous code sample could be better written as `expect(something).to.equal(true, 'expected something to have occurred');`| 1.0
`export-name` | The name of the exported module must match the filename of the source file. This is case-sensitive but ignores file extension. Since version 1.0, this rule takes a list of regular expressions as a parameter. Any export name matching that regular expression will be ignored. For example, to allow an exported name like myChartOptions, then configure the rule like this: "export-name": \[true, "myChartOptionsg"\]| 0.0.3
Expand Down
89 changes: 89 additions & 0 deletions src/a11yImgHasAltRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* @copyright Microsoft Corporation. All rights reserved.
*
* @a11yImgHasAltRule tslint rule for accessibility.
*/

import * as ts from 'typescript';
import * as Lint from 'tslint/lib/lint';

import {
getAllAttributesFromJsxElement,
getJsxAttributesFromJsxElement,
getStringLiteral
} from './utils/JsxAttribute';
import { isJsxSpreadAttribute } from './utils/TypeGuard';

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

export function getFailureStringNoAlt(tagName: string): string {
return `<${tagName}> elements must have an 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 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 class Rule extends Lint.Rules.AbstractRule {
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return sourceFile.languageVariant === ts.LanguageVariant.JSX
? this.applyWithWalker(new ImgHasAltWalker(sourceFile, this.getOptions()))
: [];
}
}

class ImgHasAltWalker extends Lint.RuleWalker {
public visitJsxSelfClosingElement(node: ts.JsxSelfClosingElement): 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 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.
// @example checking a customized component 'Image' which should require 'alt' attribute.
const additionalTagNames: string[] = options.length > 1 ? options[1] : [];

// The targetTagNames is the list of tag names we want to check.
const targetTagNames: string[] = ['img'].concat(additionalTagNames);

if (!tagName || targetTagNames.indexOf(tagName) === -1) {
return;
}

// If element contains JsxSpreadElement in which there could possibly be alt attribute, don't check it.
if (getAllAttributesFromJsxElement(node).some(isJsxSpreadAttribute)) {
return;
}

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

// 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) {
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureStringNoAlt(tagName)
));
} else if (getStringLiteral(altProp) === '') {
this.addFailure(this.createFailure(
altProp.getStart(),
altProp.getWidth(),
getFailureStringEmptyAlt(tagName)
));
}

super.visitJsxSelfClosingElement(node);
}
}
46 changes: 46 additions & 0 deletions src/a11yPropsRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* @copyright Microsoft Corporation. All rights reserved.
*
* @a11yPropsRule tslint rule for accessibility.
*/

import * as ts from 'typescript';
import * as Lint from 'tslint/lib/lint';

import { getPropName } from './utils/JsxAttribute';
import { IAria } from './utils/attributes/IAria';

// tslint:disable-next-line:no-require-imports no-var-requires
const aria: { [attributeName: string]: IAria } = require('./utils/attributes/ariaSchema.json');

export function getFailureString(name: string): string {
return `This attribute name '${name}' is an invalid ARIA attribute. \
A reference to valid ARIA attributes can be found at \
https://www.w3.org/TR/2014/REC-wai-aria-20140320/states_and_properties#state_prop_def `;
}

export class Rule extends Lint.Rules.AbstractRule {
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return sourceFile.languageVariant === ts.LanguageVariant.JSX
? this.applyWithWalker(new A11yPropsWalker(sourceFile, this.getOptions()))
: [];
}
}

class A11yPropsWalker extends Lint.RuleWalker {
public visitJsxAttribute(node: ts.JsxAttribute): void {
const name: string = getPropName(node);

if (!name || !name.match(/^aria-/i)) {
return;
}

if (!aria[name.toLowerCase()]) {
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureString(name)
));
}
}
}
Loading

0 comments on commit 82a9c39

Please sign in to comment.