Skip to content

Commit

Permalink
feat: add template-no-any rule (#755)
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelss95 authored and mgechev committed Feb 13, 2019
1 parent 0815ec5 commit 77a5e32
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -29,6 +29,7 @@ export { Rule as PreferOutputReadonlyRule } from './preferOutputReadonlyRule';
export { Rule as TemplateConditionalComplexityRule } from './templateConditionalComplexityRule';
export { Rule as TemplateCyclomaticComplexityRule } from './templateCyclomaticComplexityRule';
export { Rule as TemplateAccessibilityTabindexNoPositiveRule } from './templateAccessibilityTabindexNoPositiveRule';
export { Rule as TemplateNoAnyRule } from './templateNoAnyRule';
export { Rule as TemplateAccessibilityLabelForVisitor } from './templateAccessibilityLabelForRule';
export { Rule as TemplateAccessibilityValidAriaRule } from './templateAccessibilityValidAriaRule';
export { Rule as TemplatesAccessibilityAnchorContentRule } from './templateAccessibilityAnchorContentRule';
Expand Down
58 changes: 58 additions & 0 deletions src/templateNoAnyRule.ts
@@ -0,0 +1,58 @@
import { MethodCall, PropertyRead } from '@angular/compiler';
import { IRuleMetadata, RuleFailure } from 'tslint';
import { AbstractRule } from 'tslint/lib/rules';
import { dedent } from 'tslint/lib/utils';
import { SourceFile } from 'typescript';
import { NgWalker } from './angular/ngWalker';
import { RecursiveAngularExpressionVisitor } from './angular/templates/recursiveAngularExpressionVisitor';

const ANY_TYPE_CAST_FUNCTION_NAME = '$any';

export class Rule extends AbstractRule {
static readonly metadata: IRuleMetadata = {
description: `Disallows using '${ANY_TYPE_CAST_FUNCTION_NAME}' in templates.`,
options: null,
optionsDescription: 'Not configurable.',
rationale: dedent`
The use of '${ANY_TYPE_CAST_FUNCTION_NAME}' nullifies the compile-time
benefits of the Angular's type system.
`,
ruleName: 'template-no-any',
type: 'functionality',
typescriptOnly: true
};

static readonly FAILURE_STRING = `Avoid using '${ANY_TYPE_CAST_FUNCTION_NAME}' in templates`;

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

class ExpressionVisitor extends RecursiveAngularExpressionVisitor {
visitMethodCall(ast: MethodCall, context: any): any {
this.validateMethodCall(ast);
super.visitMethodCall(ast, context);
}

private generateFailure(ast: MethodCall): void {
const {
span: { end: endSpan, start: startSpan }
} = ast;

this.addFailureFromStartToEnd(startSpan, endSpan, Rule.FAILURE_STRING);
}

private validateMethodCall(ast: MethodCall): void {
const isAnyTypeCastFunction = ast.name === ANY_TYPE_CAST_FUNCTION_NAME;
const isAngularAnyTypeCastFunction = !(ast.receiver instanceof PropertyRead);

if (!isAnyTypeCastFunction || !isAngularAnyTypeCastFunction) return;

this.generateFailure(ast);
}
}
202 changes: 202 additions & 0 deletions test/templateNoAnyRule.spec.ts
@@ -0,0 +1,202 @@
import { Rule } from '../src/templateNoAnyRule';
import { assertAnnotated, assertMultipleAnnotated, assertSuccess } from './testHelper';

const {
FAILURE_STRING,
metadata: { ruleName }
} = Rule;

describe(ruleName, () => {
describe('failure', () => {
it('should fail with call expression in expression binding', () => {
const source = `
@Component({
template: '{{ $any(framework).name }}'
~~~~~~~~~~~~~~~
})
export class Bar {}
`;
assertAnnotated({
message: FAILURE_STRING,
ruleName,
source
});
});

it('should fail with call expression using "this"', () => {
const source = `
@Component({
template: '{{ this.$any(framework).name }}'
~~~~~~~~~~~~~~~~~~~~
})
class Bar {}
`;
assertAnnotated({
message: FAILURE_STRING,
ruleName,
source
});
});

it('should fail with call expression in property binding', () => {
const source = `
@Component({
template: '<a [href]="$any(getHref())">Click here</a>'
~~~~~~~~~~~~~~~
})
class Bar {}
`;
assertAnnotated({
message: FAILURE_STRING,
ruleName,
source
});
});

it('should fail with call expression in an output handler', () => {
const source = `
@Component({
template: '<button type="button" (click)="$any(this).member = 2">Click here</button>'
~~~~~~~~~~
})
class Bar {}
`;
assertAnnotated({
message: FAILURE_STRING,
ruleName,
source
});
});

it('should fail for multiple cases', () => {
const source = `
@Component({
template: \`
{{ $any(framework).name }}
~~~~~~~~~~~~~~~
{{ this.$any(framework).name }}
^^^^^^^^^^^^^^^^^^^^
<a [href]="$any(getHref())">Click here</a>'
###############
<button type="button" (click)="$any(this).member = 2">Click here</button>
%%%%%%%%%%
\`
})
class Bar {}
`;
assertMultipleAnnotated({
failures: [
{
char: '~',
msg: FAILURE_STRING
},
{
char: '^',
msg: FAILURE_STRING
},
{
char: '#',
msg: FAILURE_STRING
},
{
char: '%',
msg: FAILURE_STRING
}
],
ruleName,
source
});
});
});

describe('success', () => {
it('should pass with no call expression', () => {
const source = `
@Component({
template: '{{ $any }}'
})
class Bar {}
`;
assertSuccess(ruleName, source);
});

it('should pass for an object containing a function called "$any"', () => {
const source = `
@Component({
template: '{{ obj.$any() }}'
})
class Bar {
readonly obj = {
$any: () => '$any'
};
}
`;
assertSuccess(ruleName, source);
});

it('should pass for a nested object containing a function called "$any"', () => {
const source = `
@Component({
template: '{{ obj?.x?.y!.z!.$any() }}'
})
class Bar {
readonly obj: Partial<Xyz> = {
x: {
y: {
z: {
$any: () => '$any'
}
}
}
};
}
`;
assertSuccess(ruleName, source);
});

it('should pass with call expression in property binding', () => {
const source = `
@Component({
template: '<a [href]="$test()">Click here</a>'
})
class Bar {}
`;
assertSuccess(ruleName, source);
});

it('should pass with call expression in an output handler', () => {
const source = `
@Component({
template: '<button type="button" (click)="anyClick()">Click here</button>'
})
class Bar {}
`;
assertSuccess(ruleName, source);
});

it('should pass for multiple cases', () => {
const source = `
@Component({
template: \`
{{ $any }}
{{ obj?.x?.y!.z!.$any() }}
<a [href]="$test()">Click here</a>
<button type="button" (click)="anyClick()">Click here</button>
\`
})
class Bar {
readonly obj: Partial<Xyz> = {
x: {
y: {
z: {
$any: () => '$any'
}
}
}
};
}
`;
assertSuccess(ruleName, source);
});
});
});

0 comments on commit 77a5e32

Please sign in to comment.