Skip to content

Commit 77a5e32

Browse files
rafaelss95mgechev
authored andcommitted
feat: add template-no-any rule (#755)
1 parent 0815ec5 commit 77a5e32

File tree

3 files changed

+261
-0
lines changed

3 files changed

+261
-0
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export { Rule as PreferOutputReadonlyRule } from './preferOutputReadonlyRule';
2929
export { Rule as TemplateConditionalComplexityRule } from './templateConditionalComplexityRule';
3030
export { Rule as TemplateCyclomaticComplexityRule } from './templateCyclomaticComplexityRule';
3131
export { Rule as TemplateAccessibilityTabindexNoPositiveRule } from './templateAccessibilityTabindexNoPositiveRule';
32+
export { Rule as TemplateNoAnyRule } from './templateNoAnyRule';
3233
export { Rule as TemplateAccessibilityLabelForVisitor } from './templateAccessibilityLabelForRule';
3334
export { Rule as TemplateAccessibilityValidAriaRule } from './templateAccessibilityValidAriaRule';
3435
export { Rule as TemplatesAccessibilityAnchorContentRule } from './templateAccessibilityAnchorContentRule';

src/templateNoAnyRule.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { MethodCall, PropertyRead } from '@angular/compiler';
2+
import { IRuleMetadata, RuleFailure } from 'tslint';
3+
import { AbstractRule } from 'tslint/lib/rules';
4+
import { dedent } from 'tslint/lib/utils';
5+
import { SourceFile } from 'typescript';
6+
import { NgWalker } from './angular/ngWalker';
7+
import { RecursiveAngularExpressionVisitor } from './angular/templates/recursiveAngularExpressionVisitor';
8+
9+
const ANY_TYPE_CAST_FUNCTION_NAME = '$any';
10+
11+
export class Rule extends AbstractRule {
12+
static readonly metadata: IRuleMetadata = {
13+
description: `Disallows using '${ANY_TYPE_CAST_FUNCTION_NAME}' in templates.`,
14+
options: null,
15+
optionsDescription: 'Not configurable.',
16+
rationale: dedent`
17+
The use of '${ANY_TYPE_CAST_FUNCTION_NAME}' nullifies the compile-time
18+
benefits of the Angular's type system.
19+
`,
20+
ruleName: 'template-no-any',
21+
type: 'functionality',
22+
typescriptOnly: true
23+
};
24+
25+
static readonly FAILURE_STRING = `Avoid using '${ANY_TYPE_CAST_FUNCTION_NAME}' in templates`;
26+
27+
apply(sourceFile: SourceFile): RuleFailure[] {
28+
return this.applyWithWalker(
29+
new NgWalker(sourceFile, this.getOptions(), {
30+
expressionVisitorCtrl: ExpressionVisitor
31+
})
32+
);
33+
}
34+
}
35+
36+
class ExpressionVisitor extends RecursiveAngularExpressionVisitor {
37+
visitMethodCall(ast: MethodCall, context: any): any {
38+
this.validateMethodCall(ast);
39+
super.visitMethodCall(ast, context);
40+
}
41+
42+
private generateFailure(ast: MethodCall): void {
43+
const {
44+
span: { end: endSpan, start: startSpan }
45+
} = ast;
46+
47+
this.addFailureFromStartToEnd(startSpan, endSpan, Rule.FAILURE_STRING);
48+
}
49+
50+
private validateMethodCall(ast: MethodCall): void {
51+
const isAnyTypeCastFunction = ast.name === ANY_TYPE_CAST_FUNCTION_NAME;
52+
const isAngularAnyTypeCastFunction = !(ast.receiver instanceof PropertyRead);
53+
54+
if (!isAnyTypeCastFunction || !isAngularAnyTypeCastFunction) return;
55+
56+
this.generateFailure(ast);
57+
}
58+
}

test/templateNoAnyRule.spec.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import { Rule } from '../src/templateNoAnyRule';
2+
import { assertAnnotated, assertMultipleAnnotated, assertSuccess } from './testHelper';
3+
4+
const {
5+
FAILURE_STRING,
6+
metadata: { ruleName }
7+
} = Rule;
8+
9+
describe(ruleName, () => {
10+
describe('failure', () => {
11+
it('should fail with call expression in expression binding', () => {
12+
const source = `
13+
@Component({
14+
template: '{{ $any(framework).name }}'
15+
~~~~~~~~~~~~~~~
16+
})
17+
export class Bar {}
18+
`;
19+
assertAnnotated({
20+
message: FAILURE_STRING,
21+
ruleName,
22+
source
23+
});
24+
});
25+
26+
it('should fail with call expression using "this"', () => {
27+
const source = `
28+
@Component({
29+
template: '{{ this.$any(framework).name }}'
30+
~~~~~~~~~~~~~~~~~~~~
31+
})
32+
class Bar {}
33+
`;
34+
assertAnnotated({
35+
message: FAILURE_STRING,
36+
ruleName,
37+
source
38+
});
39+
});
40+
41+
it('should fail with call expression in property binding', () => {
42+
const source = `
43+
@Component({
44+
template: '<a [href]="$any(getHref())">Click here</a>'
45+
~~~~~~~~~~~~~~~
46+
})
47+
class Bar {}
48+
`;
49+
assertAnnotated({
50+
message: FAILURE_STRING,
51+
ruleName,
52+
source
53+
});
54+
});
55+
56+
it('should fail with call expression in an output handler', () => {
57+
const source = `
58+
@Component({
59+
template: '<button type="button" (click)="$any(this).member = 2">Click here</button>'
60+
~~~~~~~~~~
61+
})
62+
class Bar {}
63+
`;
64+
assertAnnotated({
65+
message: FAILURE_STRING,
66+
ruleName,
67+
source
68+
});
69+
});
70+
71+
it('should fail for multiple cases', () => {
72+
const source = `
73+
@Component({
74+
template: \`
75+
{{ $any(framework).name }}
76+
~~~~~~~~~~~~~~~
77+
{{ this.$any(framework).name }}
78+
^^^^^^^^^^^^^^^^^^^^
79+
<a [href]="$any(getHref())">Click here</a>'
80+
###############
81+
<button type="button" (click)="$any(this).member = 2">Click here</button>
82+
%%%%%%%%%%
83+
\`
84+
})
85+
class Bar {}
86+
`;
87+
assertMultipleAnnotated({
88+
failures: [
89+
{
90+
char: '~',
91+
msg: FAILURE_STRING
92+
},
93+
{
94+
char: '^',
95+
msg: FAILURE_STRING
96+
},
97+
{
98+
char: '#',
99+
msg: FAILURE_STRING
100+
},
101+
{
102+
char: '%',
103+
msg: FAILURE_STRING
104+
}
105+
],
106+
ruleName,
107+
source
108+
});
109+
});
110+
});
111+
112+
describe('success', () => {
113+
it('should pass with no call expression', () => {
114+
const source = `
115+
@Component({
116+
template: '{{ $any }}'
117+
})
118+
class Bar {}
119+
`;
120+
assertSuccess(ruleName, source);
121+
});
122+
123+
it('should pass for an object containing a function called "$any"', () => {
124+
const source = `
125+
@Component({
126+
template: '{{ obj.$any() }}'
127+
})
128+
class Bar {
129+
readonly obj = {
130+
$any: () => '$any'
131+
};
132+
}
133+
`;
134+
assertSuccess(ruleName, source);
135+
});
136+
137+
it('should pass for a nested object containing a function called "$any"', () => {
138+
const source = `
139+
@Component({
140+
template: '{{ obj?.x?.y!.z!.$any() }}'
141+
})
142+
class Bar {
143+
readonly obj: Partial<Xyz> = {
144+
x: {
145+
y: {
146+
z: {
147+
$any: () => '$any'
148+
}
149+
}
150+
}
151+
};
152+
}
153+
`;
154+
assertSuccess(ruleName, source);
155+
});
156+
157+
it('should pass with call expression in property binding', () => {
158+
const source = `
159+
@Component({
160+
template: '<a [href]="$test()">Click here</a>'
161+
})
162+
class Bar {}
163+
`;
164+
assertSuccess(ruleName, source);
165+
});
166+
167+
it('should pass with call expression in an output handler', () => {
168+
const source = `
169+
@Component({
170+
template: '<button type="button" (click)="anyClick()">Click here</button>'
171+
})
172+
class Bar {}
173+
`;
174+
assertSuccess(ruleName, source);
175+
});
176+
177+
it('should pass for multiple cases', () => {
178+
const source = `
179+
@Component({
180+
template: \`
181+
{{ $any }}
182+
{{ obj?.x?.y!.z!.$any() }}
183+
<a [href]="$test()">Click here</a>
184+
<button type="button" (click)="anyClick()">Click here</button>
185+
\`
186+
})
187+
class Bar {
188+
readonly obj: Partial<Xyz> = {
189+
x: {
190+
y: {
191+
z: {
192+
$any: () => '$any'
193+
}
194+
}
195+
}
196+
};
197+
}
198+
`;
199+
assertSuccess(ruleName, source);
200+
});
201+
});
202+
});

0 commit comments

Comments
 (0)