Skip to content

Commit

Permalink
feat(rule): use valid aria rules (#746)
Browse files Browse the repository at this point in the history
  • Loading branch information
mohammedzamakhan authored and mgechev committed Feb 12, 2019
1 parent 6ff8c56 commit 762f67f
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 0 deletions.
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -101,8 +101,10 @@
},
"dependencies": {
"app-root-path": "^2.1.0",
"aria-query": "^3.0.0",
"css-selector-tokenizer": "^0.7.0",
"cssauron": "^1.4.0",
"damerau-levenshtein": "^1.0.4",
"semver-dsl": "^1.0.1",
"source-map": "^0.5.7",
"sprintf-js": "^1.1.1"
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -30,6 +30,7 @@ export { Rule as TemplateConditionalComplexityRule } from './templateConditional
export { Rule as TemplateCyclomaticComplexityRule } from './templateCyclomaticComplexityRule';
export { Rule as TemplateAccessibilityTabindexNoPositiveRule } from './templateAccessibilityTabindexNoPositiveRule';
export { Rule as TemplateAccessibilityLabelForVisitor } from './templateAccessibilityLabelForRule';
export { Rule as TemplateAccessibilityValidAriaRule } from './templateAccessibilityValidAriaRule';
export { Rule as TemplatesAccessibilityAnchorContentRule } from './templateAccessibilityAnchorContentRule';
export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule';
export { Rule as TemplateNoAutofocusRule } from './templateNoAutofocusRule';
Expand Down
66 changes: 66 additions & 0 deletions src/templateAccessibilityValidAriaRule.ts
@@ -0,0 +1,66 @@
import { AttrAst, BoundElementPropertyAst } from '@angular/compiler';
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
import { SourceFile } from 'typescript/lib/typescript';
import { NgWalker } from './angular/ngWalker';
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
import { aria } from 'aria-query';
import { getSuggestion } from './util/getSuggestion';

const ariaAttributes: string[] = [...(<string[]>Array.from(aria.keys()))];

export class Rule extends Rules.AbstractRule {
static readonly metadata: IRuleMetadata = {
description: 'Ensures that the correct ARIA attributes are used',
options: null,
optionsDescription: 'Not configurable.',
rationale: 'Elements should not use invalid aria attributes (AX_ARIA_11)',
ruleName: 'template-accessibility-valid-aria',
type: 'functionality',
typescriptOnly: true
};

apply(sourceFile: SourceFile): RuleFailure[] {
return this.applyWithWalker(
new NgWalker(sourceFile, this.getOptions(), {
templateVisitorCtrl: TemplateAccessibilityValidAriaVisitor
})
);
}
}

export const getFailureMessage = (name: string): string => {
const suggestions = getSuggestion(name, ariaAttributes);
const message = `${name}: This attribute is an invalid ARIA attribute.`;

if (suggestions.length > 0) {
return `${message} Did you mean to use ${suggestions}?`;
}

return message;
};

class TemplateAccessibilityValidAriaVisitor extends BasicTemplateAstVisitor {
visitAttr(ast: AttrAst, context: any) {
this.validateAttribute(ast);
super.visitAttr(ast, context);
}

visitElementProperty(ast: BoundElementPropertyAst) {
this.validateAttribute(ast);
super.visitElementProperty(ast, context);
}

private validateAttribute(ast: AttrAst | BoundElementPropertyAst) {
if (ast.name.indexOf('aria-') !== 0) return;
const isValid = ariaAttributes.indexOf(ast.name) > -1;
if (isValid) return;

const {
sourceSpan: {
end: { offset: endOffset },
start: { offset: startOffset }
}
} = ast;
this.addFailureFromStartToEnd(startOffset, endOffset, getFailureMessage(ast.name));
}
}
17 changes: 17 additions & 0 deletions src/util/getSuggestion.ts
@@ -0,0 +1,17 @@
import * as editDistance from 'damerau-levenshtein';

const THRESHOLD = 2;

export const getSuggestion = (word: string, dictionary: string[] = [], limit = 2) => {
const distances = dictionary.reduce((suggestions, dictionaryWord: string) => {
const distance = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase());
const { steps } = distance;
suggestions[dictionaryWord] = steps;
return suggestions;
}, {});

return Object.keys(distances)
.filter(suggestion => distances[suggestion] <= THRESHOLD)
.sort((a, b) => distances[a] - distances[b])
.slice(0, limit);
};
59 changes: 59 additions & 0 deletions test/templateAccessibilityValidAriaRule.spec.ts
@@ -0,0 +1,59 @@
import { getFailureMessage, Rule } from '../src/templateAccessibilityValidAriaRule';
import { assertAnnotated, assertSuccess } from './testHelper';

const {
metadata: { ruleName }
} = Rule;

describe(ruleName, () => {
describe('failure', () => {
it('should fail when aria attributes are misspelled or if they does not exist', () => {
const source = `
@Component({
template: \`
<input aria-labelby="text">
~~~~~~~~~~~~~~~~~~~
\`
})
class Bar {}
`;
assertAnnotated({
message: getFailureMessage('aria-labelby'),
ruleName,
source
});
});

it('should fail when using wrong aria attributes with inputs', () => {
const source = `
@Component({
template: \`
<input [attr.aria-labelby]="text">
~~~~~~~~~~~~~~~~~~~~~~~~~~
\`
})
class Bar {}
`;
assertAnnotated({
message: getFailureMessage('aria-labelby'),
ruleName,
source
});
});
});

describe('success', () => {
it('should work when the aria attributes are correctly named', () => {
const source = `
@Component({
template: \`
<input aria-labelledby="Text">
<input [attr.aria-labelledby]="text">
\`
})
class Bar {}
`;
assertSuccess(ruleName, source);
});
});
});
15 changes: 15 additions & 0 deletions test/util/getSuggestion.spec.ts
@@ -0,0 +1,15 @@
import { expect } from 'chai';

import { getSuggestion } from '../../src/util/getSuggestion';

describe('getSuggestion', () => {
it('should suggest based on dictionary', () => {
const suggestion = getSuggestion('wordd', ['word', 'words', 'wording'], 2);
expect(suggestion).to.deep.equals(['word', 'words']);
});

it("should not suggest if the dictionary doesn't have any similar words", () => {
const suggestion = getSuggestion('ink', ['word', 'words', 'wording'], 2);
expect(suggestion).to.deep.equals([]);
});
});
19 changes: 19 additions & 0 deletions yarn.lock
Expand Up @@ -269,6 +269,13 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"

aria-query@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc"
dependencies:
ast-types-flow "0.0.7"
commander "^2.11.0"

arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
Expand Down Expand Up @@ -324,6 +331,10 @@ assign-symbols@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"

ast-types-flow@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"

async-foreach@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
Expand Down Expand Up @@ -673,6 +684,10 @@ commander@2.15.1, commander@^2.12.1, commander@^2.14.1:
version "2.15.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"

commander@^2.11.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"

commander@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
Expand Down Expand Up @@ -944,6 +959,10 @@ d@^0.1.1, d@~0.1.1:
dependencies:
es5-ext "~0.10.2"

damerau-levenshtein@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"

dargs@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17"
Expand Down

0 comments on commit 762f67f

Please sign in to comment.