refactor(compiler-cli): remove event output helper from TCB (#40738)

In 5c547675b11a24b16c20df1718583a0e7ed49cbd the `EventEmitter.subscribe`
API was extended with a new signature that allows the emitter's generic
type `T` to flow into the subscribe callback. This new signature removes
the need for the special `_outputHelper` function that used to be
emitted into TCBs when `strictOutputEventTypes`/`strictTemplates` is
enabled.

PR Close #40738
This commit is contained in:
JoostK 2021-02-06 17:20:11 +01:00 committed by Alex Rickabaugh
parent e895166082
commit 94f4d5cba6
7 changed files with 7 additions and 114 deletions

View File

@ -41,9 +41,6 @@ export class Environment {
private pipeInsts = new Map<ClassDeclaration, ts.Expression>();
protected pipeInstStatements: ts.Statement[] = [];
private outputHelperIdent: ts.Identifier|null = null;
protected helperStatements: ts.Statement[] = [];
constructor(
readonly config: TypeCheckingConfig, protected importManager: ImportManager,
private refEmitter: ReferenceEmitter, private reflector: ReflectionHost,
@ -113,93 +110,6 @@ export class Environment {
return pipeInstId;
}
/**
* Declares a helper function to be able to cast directive outputs of type `EventEmitter<T>` to
* have an accurate `subscribe()` method that properly carries over the generic type `T` into the
* listener function passed as argument to `subscribe`. This is done to work around a typing
* deficiency in `EventEmitter.subscribe`, where the listener function is typed as any.
*/
declareOutputHelper(): ts.Expression {
if (this.outputHelperIdent !== null) {
return this.outputHelperIdent;
}
const outputHelperIdent = ts.createIdentifier('_outputHelper');
const genericTypeDecl = ts.createTypeParameterDeclaration('T');
const genericTypeRef = ts.createTypeReferenceNode('T', /* typeParameters */ undefined);
const eventEmitter = this.referenceExternalType(
'@angular/core', 'EventEmitter', [new ExpressionType(new WrappedNodeExpr(genericTypeRef))]);
// Declare a type that has a `subscribe` method that carries over type `T` as parameter
// into the callback. The below code generates the following type literal:
// `{subscribe(cb: (event: T) => any): void;}`
const observableLike = ts.createTypeLiteralNode([ts.createMethodSignature(
/* typeParameters */ undefined,
/* parameters */[ts.createParameter(
/* decorators */ undefined,
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'cb',
/* questionToken */ undefined,
/* type */
ts.createFunctionTypeNode(
/* typeParameters */ undefined,
/* parameters */[ts.createParameter(
/* decorators */ undefined,
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'event',
/* questionToken */ undefined,
/* type */ genericTypeRef)],
/* type */ ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)))],
/* type */ ts.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
/* name */ 'subscribe',
/* questionToken */ undefined)]);
// Declares the first signature of `_outputHelper` that matches arguments of type
// `EventEmitter`, to convert them into `observableLike` defined above. The following
// statement is generated:
// `declare function _outputHelper<T>(output: EventEmitter<T>): observableLike;`
this.helperStatements.push(ts.createFunctionDeclaration(
/* decorators */ undefined,
/* modifiers */[ts.createModifier(ts.SyntaxKind.DeclareKeyword)],
/* asteriskToken */ undefined,
/* name */ outputHelperIdent,
/* typeParameters */[genericTypeDecl],
/* parameters */[ts.createParameter(
/* decorators */ undefined,
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'output',
/* questionToken */ undefined,
/* type */ eventEmitter)],
/* type */ observableLike,
/* body */ undefined));
// Declares the second signature of `_outputHelper` that matches all other argument types,
// i.e. ensures type identity for output types other than `EventEmitter`. This corresponds
// with the following statement:
// `declare function _outputHelper<T>(output: T): T;`
this.helperStatements.push(ts.createFunctionDeclaration(
/* decorators */ undefined,
/* modifiers */[ts.createModifier(ts.SyntaxKind.DeclareKeyword)],
/* asteriskToken */ undefined,
/* name */ outputHelperIdent,
/* typeParameters */[genericTypeDecl],
/* parameters */[ts.createParameter(
/* decorators */ undefined,
/* modifiers */ undefined,
/* dotDotDotToken */ undefined,
/* name */ 'output',
/* questionToken */ undefined,
/* type */ genericTypeRef)],
/* type */ genericTypeRef,
/* body */ undefined));
return this.outputHelperIdent = outputHelperIdent;
}
/**
* Generate a `ts.Expression` that references the given node.
*
@ -250,7 +160,6 @@ export class Environment {
getPreludeStatements(): ts.Statement[] {
return [
...this.helperStatements,
...this.pipeInstStatements,
...this.typeCtorStatements,
];

View File

@ -174,7 +174,7 @@ export class SymbolBuilder {
}
// Outputs in the TCB look like one of the two:
// * _outputHelper(_t1["outputField"]).subscribe(handler);
// * _t1["outputField"].subscribe(handler);
// * _t1.addEventListener(handler);
// Even with strict null checks disabled, we still produce the access as a separate statement
// so that it can be found here.

View File

@ -874,17 +874,8 @@ export class TcbDirectiveOutputsOp extends TcbOp {
// For strict checking of directive events, generate a call to the `subscribe` method
// on the directive's output field to let type information flow into the handler function's
// `$event` parameter.
//
// Note that the `EventEmitter<T>` type from '@angular/core' that is typically used for
// outputs has a typings deficiency in its `subscribe` method. The generic type `T` is not
// carried into the handler function, which is vital for inference of the type of `$event`.
// As a workaround, the directive's field is passed into a helper function that has a
// specially crafted set of signatures, to effectively cast `EventEmitter<T>` to something
// that has a `subscribe` method that properly carries the `T` into the handler function.
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
const outputHelper =
ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]);
const subscribeFn = ts.createPropertyAccess(outputHelper, 'subscribe');
const subscribeFn = ts.createPropertyAccess(outputField, 'subscribe');
const call = ts.createCall(subscribeFn, /* typeArguments */ undefined, [handler]);
addParseSpanInfo(call, output.sourceSpan);
this.scope.addStatement(ts.createExpressionStatement(call));

View File

@ -56,9 +56,6 @@ export class TypeCheckFile extends Environment {
'\n\n';
const printer = ts.createPrinter();
source += '\n';
for (const stmt of this.helperStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}
for (const stmt of this.pipeInstStatements) {
source += printer.printNode(ts.EmitHint.Unspecified, stmt, this.contextFile) + '\n';
}

View File

@ -593,10 +593,6 @@ class FakeEnvironment /* implements Environment */ {
return ts.createParen(ts.createAsExpression(ts.createNull(), this.referenceType(ref)));
}
declareOutputHelper(): ts.Expression {
return ts.createIdentifier('_outputHelper');
}
reference(ref: Reference<ClassDeclaration<ts.ClassDeclaration>>): ts.Expression {
return ref.node.name;
}

View File

@ -666,7 +666,7 @@ describe('type check blocks', () => {
const TEMPLATE = `<div dir (dirOutput)="foo($event)"></div>`;
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
});
it('should emit a listener function with AnimationEvent for animation events', () => {
@ -828,7 +828,7 @@ describe('type check blocks', () => {
it('should check types of directive outputs when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
expect(block).toContain(
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
});
@ -865,7 +865,7 @@ describe('type check blocks', () => {
it('should check types of DOM events when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain(
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
expect(block).toContain(
'_t2.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
});
@ -875,7 +875,7 @@ describe('type check blocks', () => {
// Note that directive outputs are still checked, that is controlled by
// `checkTypeOfOutputEvents`
expect(block).toContain(
'_outputHelper(_t1["outputField"]).subscribe(function ($event): any { (ctx).foo($event); });');
'_t1["outputField"].subscribe(function ($event): any { (ctx).foo($event); });');
expect(block).toContain('function ($event: any): any { (ctx).foo($event); }');
});
});

View File

@ -48,7 +48,7 @@ export class DefinitionBuilder {
const definitions: ts.DefinitionInfo[] = [];
for (const definitionMeta of definitionMetas) {
// The `$event` of event handlers would point to the $event parameter in the shim file, as in
// `_outputHelper(_t3["x"]).subscribe(function ($event): any { $event }) ;`
// `_t3["x"].subscribe(function ($event): any { $event }) ;`
// If we wanted to return something for this, it would be more appropriate for something like
// `getTypeDefinition`.
if (isDollarEvent(definitionMeta.node)) {