refactor(compiler-cli): Return symbols for all matching outputs (#40144)
This commit ensures that the template type checker returns symbols for all outputs if a template output listener binds to more than one. PR Close #40144
This commit is contained in:
parent
da2be4b710
commit
989b4a94d4
|
@ -173,72 +173,71 @@ export class SymbolBuilder {
|
|||
// * _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 outputFieldAccesses = findAllMatchingNodes(
|
||||
this.typeCheckBlock, {withSpan: eventBinding.keySpan, filter: isAccessExpression});
|
||||
if (outputFieldAccess === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
|
||||
if (consumer === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
|
||||
if (!ts.isPropertyAccessExpression(outputFieldAccess) ||
|
||||
outputFieldAccess.name.text !== 'addEventListener') {
|
||||
return null;
|
||||
const bindings: BindingSymbol[] = [];
|
||||
for (const outputFieldAccess of outputFieldAccesses) {
|
||||
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
|
||||
if (consumer === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const addEventListener = outputFieldAccess.name;
|
||||
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
|
||||
const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
|
||||
const positionInShimFile = this.getShimPositionForNode(addEventListener);
|
||||
const target = this.getSymbol(consumer);
|
||||
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
|
||||
if (!ts.isPropertyAccessExpression(outputFieldAccess) ||
|
||||
outputFieldAccess.name.text !== 'addEventListener') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (target === null || tsSymbol === undefined) {
|
||||
return null;
|
||||
}
|
||||
const addEventListener = outputFieldAccess.name;
|
||||
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
|
||||
const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
|
||||
const positionInShimFile = this.getShimPositionForNode(addEventListener);
|
||||
const target = this.getSymbol(consumer);
|
||||
|
||||
return {
|
||||
kind: SymbolKind.Output,
|
||||
bindings: [{
|
||||
if (target === null || tsSymbol === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bindings.push({
|
||||
kind: SymbolKind.Binding,
|
||||
tsSymbol,
|
||||
tsType,
|
||||
target,
|
||||
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
||||
}],
|
||||
};
|
||||
} else {
|
||||
if (!ts.isElementAccessExpression(outputFieldAccess)) {
|
||||
return null;
|
||||
}
|
||||
const tsSymbol =
|
||||
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
|
||||
if (tsSymbol === undefined) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (!ts.isElementAccessExpression(outputFieldAccess)) {
|
||||
continue;
|
||||
}
|
||||
const tsSymbol =
|
||||
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
|
||||
if (tsSymbol === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
|
||||
if (target === null) {
|
||||
return null;
|
||||
}
|
||||
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
|
||||
if (target === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
|
||||
const tsType = this.getTypeChecker().getTypeAtLocation(outputFieldAccess);
|
||||
return {
|
||||
kind: SymbolKind.Output,
|
||||
bindings: [{
|
||||
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
|
||||
const tsType = this.getTypeChecker().getTypeAtLocation(outputFieldAccess);
|
||||
bindings.push({
|
||||
kind: SymbolKind.Binding,
|
||||
tsSymbol,
|
||||
tsType,
|
||||
target,
|
||||
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
||||
}],
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
if (bindings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {kind: SymbolKind.Output, bindings};
|
||||
}
|
||||
|
||||
private getSymbolOfInputBinding(binding: TmplAstBoundAttribute|
|
||||
|
|
|
@ -87,6 +87,56 @@ describe('definitions', () => {
|
|||
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
|
||||
});
|
||||
|
||||
it('gets definitions for all outputs when attribute matches more than one', () => {
|
||||
initMockFileSystem('Native');
|
||||
const {cursor, text} = extractCursorInfo('<div dir (someEv¦ent)="doSomething()"></div>');
|
||||
const templateFile = {contents: text, name: absoluteFrom('/app.html')};
|
||||
const dirFile = {
|
||||
name: absoluteFrom('/dir.ts'),
|
||||
contents: `
|
||||
import {Directive, Output, EventEmitter} from '@angular/core';
|
||||
|
||||
@Directive({selector: '[dir]'})
|
||||
export class MyDir {
|
||||
@Output() someEvent = new EventEmitter<void>();
|
||||
}`,
|
||||
};
|
||||
const dirFile2 = {
|
||||
name: absoluteFrom('/dir2.ts'),
|
||||
contents: `
|
||||
import {Directive, Output, EventEmitter} from '@angular/core';
|
||||
|
||||
@Directive({selector: '[dir]'})
|
||||
export class MyDir2 {
|
||||
@Output() someEvent = new EventEmitter<void>();
|
||||
}`,
|
||||
};
|
||||
const appFile = {
|
||||
name: absoluteFrom('/app.ts'),
|
||||
contents: `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
@Component({templateUrl: 'app.html'})
|
||||
export class AppCmp {
|
||||
doSomething() {}
|
||||
}
|
||||
`
|
||||
};
|
||||
const env = createModuleWithDeclarations([appFile, dirFile, dirFile2], [templateFile]);
|
||||
const {textSpan, definitions} =
|
||||
getDefinitionsAndAssertBoundSpan(env, absoluteFrom('/app.html'), cursor);
|
||||
expect(text.substr(textSpan.start, textSpan.length)).toEqual('someEvent');
|
||||
|
||||
expect(definitions.length).toEqual(2);
|
||||
const [def, def2] = definitions;
|
||||
expect(def.textSpan).toContain('someEvent');
|
||||
expect(def2.textSpan).toContain('someEvent');
|
||||
// TODO(atscott): investigate why the text span includes more than just 'someEvent'
|
||||
// assertTextSpans([def, def2], ['someEvent']);
|
||||
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
|
||||
});
|
||||
|
||||
function getDefinitionsAndAssertBoundSpan(
|
||||
env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) {
|
||||
env.expectNoSourceDiagnostics();
|
||||
|
|
Loading…
Reference in New Issue