-
Notifications
You must be signed in to change notification settings - Fork 234
/
noInputRenameRule.ts
109 lines (96 loc) · 3.66 KB
/
noInputRenameRule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import { sprintf } from 'sprintf-js';
import { IRuleMetadata, RuleFailure } from 'tslint';
import { AbstractRule } from 'tslint/lib/rules';
import { dedent } from 'tslint/lib/utils';
import { Decorator, PropertyDeclaration, SourceFile } from 'typescript';
import { DirectiveMetadata } from './angular/metadata';
import { NgWalker } from './angular/ngWalker';
import { getClassName, kebabToCamelCase, toTitleCase } from './util/utils';
export class Rule extends AbstractRule {
static readonly metadata: IRuleMetadata = {
description: 'Disallows renaming directive inputs by providing a string to the decorator.',
descriptionDetails: 'See more at https://angular.io/styleguide#style-05-13.',
options: null,
optionsDescription: 'Not configurable.',
rationale: 'Two names for the same property (one private, one public) is inherently confusing.',
ruleName: 'no-input-rename',
type: 'maintainability',
typescriptOnly: true
};
static readonly FAILURE_STRING = dedent`
In the class "%s", the directive input property "%s" should not be renamed.
However, you should use an alias when the directive name is also an input property, and the directive name
doesn't describe the property. In this last case, you can disable this rule with \`tslint:disable-next-line:no-input-rename\`.
`;
apply(sourceFile: SourceFile): RuleFailure[] {
const walker = new Walker(sourceFile, this.getOptions());
return this.applyWithWalker(walker);
}
}
export const getFailureMessage = (className: string, propertyName: string): string => {
return sprintf(Rule.FAILURE_STRING, className, propertyName);
};
// source: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques
const whiteListAliases = new Set<string>([
'aria-activedescendant',
'aria-atomic',
'aria-autocomplete',
'aria-busy',
'aria-checked',
'aria-controls',
'aria-current',
'aria-describedby',
'aria-disabled',
'aria-dragged',
'aria-dropeffect',
'aria-expanded',
'aria-flowto',
'aria-haspopup',
'aria-hidden',
'aria-invalid',
'aria-label',
'aria-labelledby',
'aria-level',
'aria-live',
'aria-multiline',
'aria-multiselectable',
'aria-orientation',
'aria-owns',
'aria-posinset',
'aria-pressed',
'aria-readonly',
'aria-relevant',
'aria-required',
'aria-selected',
'aria-setsize',
'aria-sort',
'aria-valuemax',
'aria-valuemin',
'aria-valuenow',
'aria-valuetext'
]);
class Walker extends NgWalker {
private directiveSelectors!: ReadonlyArray<DirectiveMetadata['selector']>;
protected visitNgDirective(metadata: DirectiveMetadata): void {
this.directiveSelectors = Array.from(new Set((metadata.selector || '').replace(/[\[\]\s]/g, '').split(',')));
super.visitNgDirective(metadata);
}
protected visitNgInput(property: PropertyDeclaration, input: Decorator, args: string[]): void {
this.validateInput(property, args);
super.visitNgInput(property, input, args);
}
private canPropertyBeAliased(propertyAlias: string, propertyName: string): boolean {
return !!(
(propertyAlias !== propertyName &&
this.directiveSelectors &&
this.directiveSelectors.some(x => new RegExp(`^${x}((${toTitleCase(propertyName)}$)|(?=$))`).test(propertyAlias))) ||
(whiteListAliases.has(propertyAlias) && propertyName === kebabToCamelCase(propertyAlias))
);
}
private validateInput(property: PropertyDeclaration, args: string[]): void {
const className = getClassName(property)!;
const memberName = property.name.getText();
if (args.length === 0 || this.canPropertyBeAliased(args[0], memberName)) return;
this.addFailureAtNode(property, getFailureMessage(className, memberName));
}
}