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:
parent
7363b3d4b5
commit
70cd124ede
|
@ -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});
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue