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:
committed by
Misko Hevery
parent
634c393d35
commit
2f8a42036a
@@ -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|
|
||||
|
||||
+21
-30
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user