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

Commit

Permalink
[Issue #211] Add a rule a11y-tabindex-no-positive
Browse files Browse the repository at this point in the history
  • Loading branch information
t-ligu authored and HamletDRC committed Sep 2, 2016
1 parent bd5e195 commit 3583502
Show file tree
Hide file tree
Showing 56 changed files with 3,726 additions and 0 deletions.
48 changes: 48 additions & 0 deletions src/a11yTabindexNoPositiveRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @copyright Microsoft Corporation. All rights reserved.
*
* @a11yTabindexNoPositiveRule tslint rule of accessibility.
*/

import * as ts from 'typescript';
import * as Lint from 'tslint/lib/lint';
import { getPropName, getStringLiteral, getNumericLiteral } from './utils/JsxAttribute';

export function getFailureString(): string {
return `The value of 'tabindex' attribute is invalid or undefined. It must be either -1 or 0.`;
}

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

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

if (!name || name.toLowerCase() !== 'tabindex') {
return;
}

const literalString: string = getNumericLiteral(node) || getStringLiteral(node);

// In case the attribute has no value of empty value.
if (literalString === '') {
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureString()
));
} else if (literalString && literalString !== '-1' && literalString !== '0') {
this.addFailure(this.createFailure(
node.getStart(),
node.getWidth(),
getFailureString()
));
}
}
}
106 changes: 106 additions & 0 deletions src/utils/JsxAttribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* @copyright Microsoft Corporation. All rights reserved.
*
* @JsxAttribute utilities for react rules.
*/

import * as ts from 'typescript';
import {
isJsxAttribute,
isJsxExpression,
isStringLiteral,
isNumericLiteral,
isJsxElement,
isJsxSelfClosingElement,
isJsxOpeningElement
} from './TypeGuard';

export function getPropName(node: ts.JsxAttribute): string {
if (!isJsxAttribute(node)) {
throw new Error('The node must be a JsxAttribute collected by the AST parser.');
}

return node.name
? node.name.text
: undefined;
}

/**
* Get the string literal in jsx attribute initializer with following format:
* @example
* <div attribute='StringLiteral' />
* @example
* <div attribute={ 'StringLiteral' } />
*/
export function getStringLiteral(node: ts.JsxAttribute): string {
if (!isJsxAttribute(node)) {
throw new Error('The node must be a JsxAttribute collected by the AST parser.');
}

const initializer: ts.Expression = node.initializer;

if (!initializer) { // <tag attribute/>
return '';
} else if (isStringLiteral(initializer)) { // <tag attribute='value' />
return initializer.text.trim();
} else if (isJsxExpression(initializer) && isStringLiteral(initializer.expression)) { // <tag attribute={'value'} />
return (initializer.expression as ts.StringLiteral).text;
} else if (isJsxExpression(initializer) && !initializer.expression) { // <tag attribute={} />
return '';
} else {
return undefined;
}
}

/**
* Get the numeric literal in jsx attribute initializer with following format:
* @example
* <div attribute={ 1 } />
*/
export function getNumericLiteral(node: ts.JsxAttribute): string {
if (!isJsxAttribute(node)) {
throw new Error('The node must be a JsxAttribute collected by the AST parser.');
}

const initializer: ts.Expression = node.initializer;

return isJsxExpression(initializer) && isNumericLiteral(initializer.expression)
? (initializer.expression as ts.LiteralExpression).text
: undefined;
}

/**
* Get an array of attributes in the given node.
* It contains JsxAttribute and JsxSpreadAttribute.
*/
export function getAllAttributesFromJsxElement(node: ts.Node): (ts.JsxAttribute | ts.JsxSpreadAttribute)[] {
let attributes: (ts.JsxAttribute | ts.JsxSpreadAttribute)[];

if (isJsxElement(node)) {
attributes = node.openingElement.attributes;
} else if (isJsxSelfClosingElement(node)) {
attributes = node.attributes;
} else if (isJsxOpeningElement(node)) {
attributes = node.attributes;
} else {
throw new Error('The node must be a JsxElement, JsxSelfClosingElement or JsxOpeningElement.');
}

return attributes;
}

/**
* Get a dictionary of JsxAttribute from a JsxElement, JsxSelfClosingElement or JsxOpeningElement.
* @returns { [propName: string]: ts.JsxAttribute } a dictionary has lowercase keys.
*/
export function getJsxAttributesFromJsxElement(node: ts.Node): { [propName: string]: ts.JsxAttribute } {
let attributesDictionary: { [propName: string]: ts.JsxAttribute } = {};

getAllAttributesFromJsxElement(node).forEach((attr) => {
if (isJsxAttribute(attr)) {
attributesDictionary[getPropName(attr).toLowerCase()] = attr;
}
});

return attributesDictionary;
}
42 changes: 42 additions & 0 deletions src/utils/TypeGuard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import * as ts from 'typescript';

/**
* TypeScript 2.0 will have more features to support type guard.
* https://www.typescriptlang.org/docs/handbook/advanced-types.html
* We could avoid 'as' cast if switching to 2.0
*/

export function isJsxAttribute(node: ts.Node): node is ts.JsxAttribute {
return node && node.kind === ts.SyntaxKind.JsxAttribute;
}

export function isJsxSpreadAttribute(node: ts.Node): node is ts.JsxSpreadAttribute {
return node && node.kind === ts.SyntaxKind.JsxSpreadAttribute;
}

export function isJsxExpression(node: ts.Node): node is ts.JsxExpression {
return node && node.kind === ts.SyntaxKind.JsxExpression;
}

/**
* There is no type of NumericLiteral in typescript, guarded as LiteralExpression.
*/
export function isNumericLiteral(node: ts.Node): node is ts.LiteralExpression {
return node && node.kind === ts.SyntaxKind.NumericLiteral;
}

export function isStringLiteral(node: ts.Node): node is ts.StringLiteral {
return node && node.kind === ts.SyntaxKind.StringLiteral;
}

export function isJsxElement(node: ts.Node): node is ts.JsxElement {
return node && node.kind === ts.SyntaxKind.JsxElement;
}

export function isJsxSelfClosingElement(node: ts.Node): node is ts.JsxSelfClosingElement {
return node && node.kind === ts.SyntaxKind.JsxSelfClosingElement;
}

export function isJsxOpeningElement(node: ts.Node): node is ts.JsxOpeningElement {
return node && node.kind === ts.SyntaxKind.JsxOpeningElement;
}
5 changes: 5 additions & 0 deletions src/utils/attributes/IAria.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IAria {
type: string;
values: string[];
allowUndefined: boolean;
}
5 changes: 5 additions & 0 deletions src/utils/attributes/IRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IRole {
requiredProps: string[];
supportedProps: string[];
isAbstract: boolean;
}
15 changes: 15 additions & 0 deletions src/utils/attributes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
This folder contains data from w3c specifications.

1. roleSchema.json
This file is composed of non-abstract role and abstract roles.
Each abstract role has only global props/states as its supported props.

The reference of non-abstract roles and it's props/states https://www.w3.org/TR/wai-aria/appendices#quickref
The reference of abstract roles https://www.w3.org/TR/wai-aria/roles#abstract_roles

2. ariaSchema.json
This file is composed of aria-* attributes as well as their types or limited values if any.
Each aria-* attribute has a required type.
Some attributes has a group of supported values, they MUST only use one of these values.

The reference of aria-* states and properties https://www.w3.org/TR/wai-aria/states_and_properties
117 changes: 117 additions & 0 deletions src/utils/attributes/ariaSchema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
{
"aria-activedescendant": {
"type": "string"
},
"aria-atomic": {
"type": "boolean"
},
"aria-autocomplete": {
"type": "token",
"values": [ "inline", "list", "both", "none" ]
},
"aria-busy": {
"type": "boolean"
},
"aria-checked": {
"type": "tristate"
},
"aria-controls": {
"type": "string"
},
"aria-describedby": {
"type": "string"
},
"aria-disabled": {
"type": "boolean"
},
"aria-dropeffect": {
"type": "tokenlist",
"values": [ "copy", "move", "link", "execute", "popup", "none" ]
},
"aria-expanded": {
"type": "boolean",
"allowUndefined": true
},
"aria-flowto": {
"type": "string"
},
"aria-grabbed": {
"type": "boolean",
"allowUndefined": true
},
"aria-haspopup": {
"type": "boolean"
},
"aria-hidden": {
"type": "boolean"
},
"aria-invalid": {
"type": "token",
"values": [ "grammar", "false", "spelling", "true" ]
},
"aria-label": {
"type": "string"
},
"aria-labelledby": {
"type": "string"
},
"aria-level": {
"type": "integer"
},
"aria-live": {
"type": "token",
"values": [ "off", "polite", "assertive" ]
},
"aria-multiline": {
"type": "boolean"
},
"aria-multiselectable": {
"type": "boolean"
},
"aria-orientation": {
"type": "token",
"values": [ "vertical", "horizontal" ]
},
"aria-owns": {
"type": "string"
},
"aria-posinset": {
"type": "integer"
},
"aria-pressed": {
"type": "tristate"
},
"aria-readonly": {
"type": "boolean"
},
"aria-relevant": {
"type": "tokenlist",
"values": [ "additions", "removals", "text", "all" ]
},
"aria-required": {
"type": "boolean"
},
"aria-selected": {
"type": "boolean",
"allowUndefined": true
},
"aria-setsize": {
"type": "integer"
},
"aria-sort": {
"type": "token",
"values": [ "ascending", "descending", "none", "other" ]
},
"aria-valuemax": {
"type": "number"
},
"aria-valuemin": {
"type": "number"
},
"aria-valuenow": {
"type": "number"
},
"aria-valuetext": {
"type": "string"
}
}
Loading

0 comments on commit 3583502

Please sign in to comment.