refactor(compiler-cli): produce binding access when checkTypeOfOutputEvents is false (#39515)

When `checkTypeOfOutputEvents` is `false`, we still need to produce the access
to the `EventEmitter` so the Language Service can still get the
type information about the field. That is, in a template `<div
(output)="handle($event)"`, we still want to be able to grab information
when the cursor is inside the "output" parens. The flag is intended only
to affect whether the compiler produces diagnostics for the inferred
type of the `$event`.

PR Close #39515
This commit is contained in:
Andrew Scott 2020-11-17 13:00:22 -08:00 committed by Alex Rickabaugh
parent 702d6bfe8f
commit 269a775287
5 changed files with 36 additions and 14 deletions

View File

@ -165,9 +165,11 @@ export class SymbolBuilder {
} }
private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null { private getSymbolOfBoundEvent(eventBinding: TmplAstBoundEvent): OutputBindingSymbol|null {
// Outputs are a `ts.CallExpression` that look like one of the two: // Outputs in the TCB look like one of the two:
// * _outputHelper(_t1["outputField"]).subscribe(handler); // * _outputHelper(_t1["outputField"]).subscribe(handler);
// * _t1.addEventListener(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.
const outputFieldAccess = findFirstMatchingNode( const outputFieldAccess = findFirstMatchingNode(
this.typeCheckBlock, {withSpan: eventBinding.keySpan, filter: isAccessExpression}); this.typeCheckBlock, {withSpan: eventBinding.keySpan, filter: isAccessExpression});
if (outputFieldAccess === null) { if (outputFieldAccess === null) {

View File

@ -865,6 +865,11 @@ export class TcbDirectiveOutputsOp extends TcbOp {
// TODO(alxhub): consider supporting multiple fields with the same property name for outputs. // TODO(alxhub): consider supporting multiple fields with the same property name for outputs.
const field = outputs.getByBindingPropertyName(output.name)![0].classPropertyName; const field = outputs.getByBindingPropertyName(output.name)![0].classPropertyName;
if (dirId === null) {
dirId = this.scope.resolve(this.node, this.dir);
}
const outputField = ts.createElementAccess(dirId, ts.createStringLiteral(field));
addParseSpanInfo(outputField, output.keySpan);
if (this.tcb.env.config.checkTypeOfOutputEvents) { if (this.tcb.env.config.checkTypeOfOutputEvents) {
// For strict checking of directive events, generate a call to the `subscribe` method // 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 // on the directive's output field to let type information flow into the handler function's
@ -877,12 +882,6 @@ export class TcbDirectiveOutputsOp extends TcbOp {
// specially crafted set of signatures, to effectively cast `EventEmitter<T>` to something // 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. // 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 handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Infer);
if (dirId === null) {
dirId = this.scope.resolve(this.node, this.dir);
}
const outputField = ts.createElementAccess(dirId, ts.createStringLiteral(field));
addParseSpanInfo(outputField, output.keySpan);
const outputHelper = const outputHelper =
ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]); ts.createCall(this.tcb.env.declareOutputHelper(), undefined, [outputField]);
const subscribeFn = ts.createPropertyAccess(outputHelper, 'subscribe'); const subscribeFn = ts.createPropertyAccess(outputHelper, 'subscribe');
@ -890,8 +889,13 @@ export class TcbDirectiveOutputsOp extends TcbOp {
addParseSpanInfo(call, output.sourceSpan); addParseSpanInfo(call, output.sourceSpan);
this.scope.addStatement(ts.createExpressionStatement(call)); this.scope.addStatement(ts.createExpressionStatement(call));
} else { } else {
// If strict checking of directive events is disabled, emit a handler function where the // If strict checking of directive events is disabled:
// `$event` parameter has an explicit `any` type. //
// * We still generate the access to the output field as a statement in the TCB so consumers
// of the `TemplateTypeChecker` can still find the node for the class member for the
// output.
// * Emit a handler function where the `$event` parameter has an explicit `any` type.
this.scope.addStatement(ts.createExpressionStatement(outputField));
const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any); const handler = tcbCreateEventHandler(output, this.tcb, this.scope, EventParamType.Any);
this.scope.addStatement(ts.createExpressionStatement(handler)); this.scope.addStatement(ts.createExpressionStatement(handler));
} }

View File

@ -839,7 +839,7 @@ describe('type check blocks', () => {
expect(block).toContain('function ($event: any): any { (ctx).foo($event); }'); expect(block).toContain('function ($event: any): any { (ctx).foo($event); }');
// Note that DOM events are still checked, that is controlled by `checkTypeOfDomEvents` // Note that DOM events are still checked, that is controlled by `checkTypeOfDomEvents`
expect(block).toContain( expect(block).toContain(
'_t1.addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });'); 'addEventListener("nonDirOutput", function ($event): any { (ctx).foo($event); });');
}); });
}); });

View File

@ -1271,7 +1271,7 @@ runInEachFileSystem(() => {
assertExpressionSymbol(eventSymbol); assertExpressionSymbol(eventSymbol);
}); });
it('returns empty list when checkTypeOfOutputEvents is false', () => { it('still returns binding when checkTypeOfOutputEvents is false', () => {
const fileName = absoluteFrom('/main.ts'); const fileName = absoluteFrom('/main.ts');
const dirFile = absoluteFrom('/dir.ts'); const dirFile = absoluteFrom('/dir.ts');
const {program, templateTypeChecker} = setup( const {program, templateTypeChecker} = setup(
@ -1302,9 +1302,14 @@ runInEachFileSystem(() => {
const nodes = templateTypeChecker.getTemplate(cmp)!; const nodes = templateTypeChecker.getTemplate(cmp)!;
const outputABinding = (nodes[0] as TmplAstElement).outputs[0]; const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp); const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!;
// TODO(atscott): should type checker still generate the subscription in this case? assertOutputBindingSymbol(symbol);
expect(symbol).toBeNull(); expect(
(symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration).name.getText())
.toEqual('outputA');
expect((symbol.bindings[0].tsSymbol!.declarations[0] as ts.PropertyDeclaration)
.parent.name?.text)
.toEqual('TestDir');
}); });
}); });

View File

@ -496,6 +496,17 @@ describe('quick info', () => {
expectedDisplayString: '(property) TestComponent.name: string' expectedDisplayString: '(property) TestComponent.name: string'
}); });
}); });
it('can still get quick info when strictOutputEventTypes is false', () => {
initMockFileSystem('Native');
env = LanguageServiceTestEnvironment.setup(
quickInfoSkeleton(), {strictOutputEventTypes: false});
expectQuickInfo({
templateOverride: `<test-comp (te¦st)="myClick($event)"></test-comp>`,
expectedSpanText: 'test',
expectedDisplayString: '(event) TestComponent.testEvent: EventEmitter<string>'
});
});
}); });
function expectQuickInfo( function expectQuickInfo(