This repository has been archived by the owner on Jul 15, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 198
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Issue #211] Add a rule a11y-tabindex-no-positive
- Loading branch information
Showing
56 changed files
with
3,726 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
)); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface IAria { | ||
type: string; | ||
values: string[]; | ||
allowUndefined: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export interface IRole { | ||
requiredProps: string[]; | ||
supportedProps: string[]; | ||
isAbstract: boolean; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.