From 70cd124ededef29b8846145116795faf29ad037e Mon Sep 17 00:00:00 2001 From: Chuck Jazdzewski Date: Wed, 6 Dec 2017 10:32:39 -0800 Subject: [PATCH] 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 --- .../test/diagnostics/check_types_spec.ts | 118 +++++++++++++++++- .../src/compiler_util/expression_converter.ts | 9 ++ 2 files changed, 121 insertions(+), 6 deletions(-) diff --git a/packages/compiler-cli/test/diagnostics/check_types_spec.ts b/packages/compiler-cli/test/diagnostics/check_types_spec.ts index 0e634eef1a..bc1580515e 100644 --- a/packages/compiler-cli/test/diagnostics/check_types_spec.ts +++ b/packages/compiler-cli/test/diagnostics/check_types_spec.ts @@ -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,11 +63,13 @@ describe('ng type checker', () => { `Expected a diagnostics matching ${message}, received\n ${diagnostics.map(d => d.messageText).join('\n ')}`); } - const span = matchingDiagnostics[0].span; - if (!span) { - throw new Error('Expected a sourceSpan'); + if (location) { + const span = matchingDiagnostics[0].span; + if (!span) { + throw new Error('Expected a sourceSpan'); + } + expect(`${span.start.file.url}@${span.start.line}:${span.start.offset}`).toMatch(location); } - expect(`${span.start.file.url}@${span.start.line}:${span.start.offset}`).toMatch(location); } } @@ -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}); diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index 52fbb6faa6..a828cd75ca 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -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);