refactor(compiler-cli): Return addEventListener symbol for native output bindings (#39312)

Rather than returning `null`, we can provide some useful information to the Language Service
by returning a symbol for the `addEventListener` function call when the consumer
of a binding as an element.

PR Close #39312
This commit is contained in:
Andrew Scott
2020-10-16 17:17:15 -07:00
committed by Misko Hevery
parent 634c393d35
commit 2f8a42036a
5 changed files with 148 additions and 88 deletions
@@ -176,41 +176,67 @@ export class SymbolBuilder {
}
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
if (consumer === null || consumer instanceof TmplAstTemplate ||
consumer instanceof TmplAstElement) {
// Bindings to element or template events produce `addEventListener` which
// we cannot get the field for.
return null;
}
const outputFieldAccess = TcbDirectiveOutputsOp.decodeOutputCallExpression(node);
if (outputFieldAccess === null) {
if (consumer === null) {
return null;
}
const tsSymbol =
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
if (tsSymbol === undefined) {
return null;
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
if (!ts.isPropertyAccessExpression(node.expression) ||
node.expression.name.text !== 'addEventListener') {
return null;
}
const addEventListener = node.expression.name;
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
const positionInShimFile = this.getShimPositionForNode(addEventListener);
const target = this.getSymbol(consumer);
if (target === null || tsSymbol === undefined) {
return null;
}
return {
kind: SymbolKind.Output,
bindings: [{
kind: SymbolKind.Binding,
tsSymbol,
tsType,
target,
shimLocation: {shimPath: this.shimPath, positionInShimFile},
}],
};
} else {
const outputFieldAccess = TcbDirectiveOutputsOp.decodeOutputCallExpression(node);
if (outputFieldAccess === null) {
return null;
}
const tsSymbol =
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
if (tsSymbol === undefined) {
return null;
}
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
if (target === null) {
return null;
}
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
const tsType = this.getTypeChecker().getTypeAtLocation(node);
return {
kind: SymbolKind.Output,
bindings: [{
kind: SymbolKind.Binding,
tsSymbol,
tsType,
target,
shimLocation: {shimPath: this.shimPath, positionInShimFile},
}],
};
}
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
if (target === null) {
return null;
}
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
const tsType = this.getTypeChecker().getTypeAtLocation(node);
return {
kind: SymbolKind.Output,
bindings: [{
kind: SymbolKind.Binding,
tsSymbol,
tsType,
target,
shimLocation: {shimPath: this.shimPath, positionInShimFile},
}],
};
}
private getSymbolOfInputBinding(binding: TmplAstBoundAttribute|
@@ -1247,38 +1247,29 @@ runInEachFileSystem(() => {
.toEqual('TestDir');
});
it('returns empty list when binding does not match any directive output', () => {
const fileName = absoluteFrom('/main.ts');
const dirFile = absoluteFrom('/dir.ts');
const {program, templateTypeChecker} = setup([
{
fileName,
templates: {'Cmp': `<div dir (doesNotExist)="handle($event)"></div>`},
declarations: [
{
name: 'TestDir',
selector: '[dir]',
file: dirFile,
type: 'directive',
outputs: {outputA: 'outputA'},
},
]
},
{
fileName: dirFile,
source: `export class TestDir {outputA!: EventEmitter<string>;}`,
templates: {},
}
]);
const sf = getSourceFileOrError(program, fileName);
const cmp = getClass(sf, 'Cmp');
it('returns addEventListener binding to native element when no match to any directive output',
() => {
const fileName = absoluteFrom('/main.ts');
const {program, templateTypeChecker} = setup([
{
fileName,
templates: {'Cmp': `<div (click)="handle($event)"></div>`},
},
]);
const sf = getSourceFileOrError(program, fileName);
const cmp = getClass(sf, 'Cmp');
const nodes = templateTypeChecker.getTemplate(cmp)!;
const nodes = templateTypeChecker.getTemplate(cmp)!;
const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp);
expect(symbol).toBeNull();
});
const outputABinding = (nodes[0] as TmplAstElement).outputs[0];
const symbol = templateTypeChecker.getSymbolOfNode(outputABinding, cmp)!;
assertOutputBindingSymbol(symbol);
expect(program.getTypeChecker().symbolToString(symbol.bindings[0].tsSymbol!))
.toEqual('addEventListener');
const eventSymbol = templateTypeChecker.getSymbolOfNode(outputABinding.handler, cmp)!;
assertExpressionSymbol(eventSymbol);
});
it('returns empty list when checkTypeOfOutputEvents is false', () => {
const fileName = absoluteFrom('/main.ts');