feat(compiler): add a pseudo $any() function to disable type checking (#20876)

`$any()` can now be used in a binding expression to disable type
checking for the rest of the expression. This similar to `as any` in
TypeScript and allows expression that work at runtime but do not
type-check.

PR Close #20876
This commit is contained in:
Chuck Jazdzewski 2017-12-06 10:32:39 -08:00 committed by Jason Aden
parent 7363b3d4b5
commit 70cd124ede
2 changed files with 121 additions and 6 deletions

View File

@ -48,11 +48,11 @@ describe('ng type checker', () => {
}
function reject(
message: string | RegExp, location: RegExp, files: MockFiles,
message: string | RegExp, location: RegExp | null, files: MockFiles,
overrideOptions: ng.CompilerOptions = {}) {
const diagnostics = compileAndCheck([QUICKSTART, files], overrideOptions);
if (!diagnostics || !diagnostics.length) {
throw new Error('Expected a diagnostic erorr message');
throw new Error('Expected a diagnostic error message');
} else {
const matches: (d: ng.Diagnostic) => boolean = typeof message === 'string' ?
d => ng.isNgDiagnostic(d)&& d.messageText == message :
@ -63,6 +63,7 @@ describe('ng type checker', () => {
`Expected a diagnostics matching ${message}, received\n ${diagnostics.map(d => d.messageText).join('\n ')}`);
}
if (location) {
const span = matchingDiagnostics[0].span;
if (!span) {
throw new Error('Expected a sourceSpan');
@ -70,6 +71,7 @@ describe('ng type checker', () => {
expect(`${span.start.file.url}@${span.start.line}:${span.start.offset}`).toMatch(location);
}
}
}
it('should accept unmodified QuickStart', () => { accept(); });
@ -216,6 +218,110 @@ describe('ng type checker', () => {
});
});
describe('casting $any', () => {
const a = (files: MockFiles, options: object = {}) => {
accept(
{'src/app.component.ts': '', 'src/lib.ts': '', ...files},
{fullTemplateTypeCheck: true, ...options});
};
const r =
(message: string | RegExp, location: RegExp | null, files: MockFiles,
options: object = {}) => {
reject(
message, location, {'src/app.component.ts': '', 'src/lib.ts': '', ...files},
{fullTemplateTypeCheck: true, ...options});
};
it('should allow member access of an expression', () => {
a({
'src/app.module.ts': `
import {NgModule, Component} from '@angular/core';
export interface Person {
name: string;
}
@Component({
selector: 'comp',
template: ' {{$any(person).address}}'
})
export class MainComp {
person: Person;
}
@NgModule({
declarations: [MainComp],
})
export class MainModule {
}`
});
});
it('should allow invalid this.member access', () => {
a({
'src/app.module.ts': `
import {NgModule, Component} from '@angular/core';
@Component({
selector: 'comp',
template: ' {{$any(this).missing}}'
})
export class MainComp { }
@NgModule({
declarations: [MainComp],
})
export class MainModule {
}`
});
});
it('should reject too few parameters to $any', () => {
r(/Invalid call to \$any, expected 1 argument but received none/, null, {
'src/app.module.ts': `
import {NgModule, Component} from '@angular/core';
@Component({
selector: 'comp',
template: ' {{$any().missing}}'
})
export class MainComp { }
@NgModule({
declarations: [MainComp],
})
export class MainModule {
}`
});
});
it('should reject too many parameters to $any', () => {
r(/Invalid call to \$any, expected 1 argument but received 2/, null, {
'src/app.module.ts': `
import {NgModule, Component} from '@angular/core';
export interface Person {
name: string;
}
@Component({
selector: 'comp',
template: ' {{$any(person, 12).missing}}'
})
export class MainComp {
person: Person;
}
@NgModule({
declarations: [MainComp],
})
export class MainModule {
}`
});
});
});
describe('regressions ', () => {
const a = (files: MockFiles, options: object = {}) => {
accept(files, {fullTemplateTypeCheck: true, ...options});

View File

@ -340,6 +340,15 @@ class _AstToIrVisitor implements cdAst.AstVisitor {
private _getLocal(name: string): o.Expression|null { return this._localResolver.getLocal(name); }
visitMethodCall(ast: cdAst.MethodCall, mode: _Mode): any {
if (ast.receiver instanceof cdAst.ImplicitReceiver && ast.name == '$any') {
const args = this.visitAll(ast.args, _Mode.Expression) as any[];
if (args.length != 1) {
throw new Error(
`Invalid call to $any, expected 1 argument but received ${args.length || 'none'}`);
}
return (args[0] as o.Expression).cast(o.DYNAMIC_TYPE);
}
const leftMostSafe = this.leftMostSafeNode(ast);
if (leftMostSafe) {
return this.convertSafeAccess(ast, leftMostSafe, mode);