Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 762f67f

Browse files
mohammedzamakhanmgechev
authored andcommitted
feat(rule): use valid aria rules (#746)
1 parent 6ff8c56 commit 762f67f

7 files changed

+179
-0
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@
101101
},
102102
"dependencies": {
103103
"app-root-path": "^2.1.0",
104+
"aria-query": "^3.0.0",
104105
"css-selector-tokenizer": "^0.7.0",
105106
"cssauron": "^1.4.0",
107+
"damerau-levenshtein": "^1.0.4",
106108
"semver-dsl": "^1.0.1",
107109
"source-map": "^0.5.7",
108110
"sprintf-js": "^1.1.1"

src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export { Rule as TemplateConditionalComplexityRule } from './templateConditional
3030
export { Rule as TemplateCyclomaticComplexityRule } from './templateCyclomaticComplexityRule';
3131
export { Rule as TemplateAccessibilityTabindexNoPositiveRule } from './templateAccessibilityTabindexNoPositiveRule';
3232
export { Rule as TemplateAccessibilityLabelForVisitor } from './templateAccessibilityLabelForRule';
33+
export { Rule as TemplateAccessibilityValidAriaRule } from './templateAccessibilityValidAriaRule';
3334
export { Rule as TemplatesAccessibilityAnchorContentRule } from './templateAccessibilityAnchorContentRule';
3435
export { Rule as TemplatesNoNegatedAsync } from './templatesNoNegatedAsyncRule';
3536
export { Rule as TemplateNoAutofocusRule } from './templateNoAutofocusRule';
+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { AttrAst, BoundElementPropertyAst } from '@angular/compiler';
2+
import { IRuleMetadata, RuleFailure, Rules } from 'tslint/lib';
3+
import { SourceFile } from 'typescript/lib/typescript';
4+
import { NgWalker } from './angular/ngWalker';
5+
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
6+
import { aria } from 'aria-query';
7+
import { getSuggestion } from './util/getSuggestion';
8+
9+
const ariaAttributes: string[] = [...(<string[]>Array.from(aria.keys()))];
10+
11+
export class Rule extends Rules.AbstractRule {
12+
static readonly metadata: IRuleMetadata = {
13+
description: 'Ensures that the correct ARIA attributes are used',
14+
options: null,
15+
optionsDescription: 'Not configurable.',
16+
rationale: 'Elements should not use invalid aria attributes (AX_ARIA_11)',
17+
ruleName: 'template-accessibility-valid-aria',
18+
type: 'functionality',
19+
typescriptOnly: true
20+
};
21+
22+
apply(sourceFile: SourceFile): RuleFailure[] {
23+
return this.applyWithWalker(
24+
new NgWalker(sourceFile, this.getOptions(), {
25+
templateVisitorCtrl: TemplateAccessibilityValidAriaVisitor
26+
})
27+
);
28+
}
29+
}
30+
31+
export const getFailureMessage = (name: string): string => {
32+
const suggestions = getSuggestion(name, ariaAttributes);
33+
const message = `${name}: This attribute is an invalid ARIA attribute.`;
34+
35+
if (suggestions.length > 0) {
36+
return `${message} Did you mean to use ${suggestions}?`;
37+
}
38+
39+
return message;
40+
};
41+
42+
class TemplateAccessibilityValidAriaVisitor extends BasicTemplateAstVisitor {
43+
visitAttr(ast: AttrAst, context: any) {
44+
this.validateAttribute(ast);
45+
super.visitAttr(ast, context);
46+
}
47+
48+
visitElementProperty(ast: BoundElementPropertyAst) {
49+
this.validateAttribute(ast);
50+
super.visitElementProperty(ast, context);
51+
}
52+
53+
private validateAttribute(ast: AttrAst | BoundElementPropertyAst) {
54+
if (ast.name.indexOf('aria-') !== 0) return;
55+
const isValid = ariaAttributes.indexOf(ast.name) > -1;
56+
if (isValid) return;
57+
58+
const {
59+
sourceSpan: {
60+
end: { offset: endOffset },
61+
start: { offset: startOffset }
62+
}
63+
} = ast;
64+
this.addFailureFromStartToEnd(startOffset, endOffset, getFailureMessage(ast.name));
65+
}
66+
}

src/util/getSuggestion.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as editDistance from 'damerau-levenshtein';
2+
3+
const THRESHOLD = 2;
4+
5+
export const getSuggestion = (word: string, dictionary: string[] = [], limit = 2) => {
6+
const distances = dictionary.reduce((suggestions, dictionaryWord: string) => {
7+
const distance = editDistance(word.toUpperCase(), dictionaryWord.toUpperCase());
8+
const { steps } = distance;
9+
suggestions[dictionaryWord] = steps;
10+
return suggestions;
11+
}, {});
12+
13+
return Object.keys(distances)
14+
.filter(suggestion => distances[suggestion] <= THRESHOLD)
15+
.sort((a, b) => distances[a] - distances[b])
16+
.slice(0, limit);
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { getFailureMessage, Rule } from '../src/templateAccessibilityValidAriaRule';
2+
import { assertAnnotated, assertSuccess } from './testHelper';
3+
4+
const {
5+
metadata: { ruleName }
6+
} = Rule;
7+
8+
describe(ruleName, () => {
9+
describe('failure', () => {
10+
it('should fail when aria attributes are misspelled or if they does not exist', () => {
11+
const source = `
12+
@Component({
13+
template: \`
14+
<input aria-labelby="text">
15+
~~~~~~~~~~~~~~~~~~~
16+
\`
17+
})
18+
class Bar {}
19+
`;
20+
assertAnnotated({
21+
message: getFailureMessage('aria-labelby'),
22+
ruleName,
23+
source
24+
});
25+
});
26+
27+
it('should fail when using wrong aria attributes with inputs', () => {
28+
const source = `
29+
@Component({
30+
template: \`
31+
<input [attr.aria-labelby]="text">
32+
~~~~~~~~~~~~~~~~~~~~~~~~~~
33+
\`
34+
})
35+
class Bar {}
36+
`;
37+
assertAnnotated({
38+
message: getFailureMessage('aria-labelby'),
39+
ruleName,
40+
source
41+
});
42+
});
43+
});
44+
45+
describe('success', () => {
46+
it('should work when the aria attributes are correctly named', () => {
47+
const source = `
48+
@Component({
49+
template: \`
50+
<input aria-labelledby="Text">
51+
<input [attr.aria-labelledby]="text">
52+
\`
53+
})
54+
class Bar {}
55+
`;
56+
assertSuccess(ruleName, source);
57+
});
58+
});
59+
});

test/util/getSuggestion.spec.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { expect } from 'chai';
2+
3+
import { getSuggestion } from '../../src/util/getSuggestion';
4+
5+
describe('getSuggestion', () => {
6+
it('should suggest based on dictionary', () => {
7+
const suggestion = getSuggestion('wordd', ['word', 'words', 'wording'], 2);
8+
expect(suggestion).to.deep.equals(['word', 'words']);
9+
});
10+
11+
it("should not suggest if the dictionary doesn't have any similar words", () => {
12+
const suggestion = getSuggestion('ink', ['word', 'words', 'wording'], 2);
13+
expect(suggestion).to.deep.equals([]);
14+
});
15+
});

yarn.lock

+19
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,13 @@ argparse@^1.0.7:
269269
dependencies:
270270
sprintf-js "~1.0.2"
271271

272+
aria-query@^3.0.0:
273+
version "3.0.0"
274+
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-3.0.0.tgz#65b3fcc1ca1155a8c9ae64d6eee297f15d5133cc"
275+
dependencies:
276+
ast-types-flow "0.0.7"
277+
commander "^2.11.0"
278+
272279
arr-diff@^4.0.0:
273280
version "4.0.0"
274281
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -324,6 +331,10 @@ assign-symbols@^1.0.0:
324331
version "1.0.0"
325332
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
326333

334+
335+
version "0.0.7"
336+
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
337+
327338
async-foreach@^0.1.3:
328339
version "0.1.3"
329340
resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
@@ -673,6 +684,10 @@ [email protected], commander@^2.12.1, commander@^2.14.1:
673684
version "2.15.1"
674685
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
675686

687+
commander@^2.11.0:
688+
version "2.19.0"
689+
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
690+
676691
commander@^2.9.0:
677692
version "2.9.0"
678693
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
@@ -944,6 +959,10 @@ d@^0.1.1, d@~0.1.1:
944959
dependencies:
945960
es5-ext "~0.10.2"
946961

962+
damerau-levenshtein@^1.0.4:
963+
version "1.0.4"
964+
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz#03191c432cb6eea168bb77f3a55ffdccb8978514"
965+
947966
dargs@^4.0.1:
948967
version "4.1.0"
949968
resolved "https://registry.yarnpkg.com/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17"

0 commit comments

Comments
 (0)