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);
|
// * _t1.addEventListener(handler);
|
||||||
// Even with strict null checks disabled, we still produce the access as a separate statement
|
// Even with strict null checks disabled, we still produce the access as a separate statement
|
||||||
// so that it can be found here.
|
// so that it can be found here.
|
||||||
const outputFieldAccess = findFirstMatchingNode(
|
const outputFieldAccesses = findAllMatchingNodes(
|
||||||
this.typeCheckBlock, {withSpan: eventBinding.keySpan, filter: isAccessExpression});
|
this.typeCheckBlock, {withSpan: eventBinding.keySpan, filter: isAccessExpression});
|
||||||
if (outputFieldAccess === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
|
const bindings: BindingSymbol[] = [];
|
||||||
if (consumer === null) {
|
for (const outputFieldAccess of outputFieldAccesses) {
|
||||||
return null;
|
const consumer = this.templateData.boundTarget.getConsumerOfBinding(eventBinding);
|
||||||
}
|
if (consumer === null) {
|
||||||
|
continue;
|
||||||
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
|
|
||||||
if (!ts.isPropertyAccessExpression(outputFieldAccess) ||
|
|
||||||
outputFieldAccess.name.text !== 'addEventListener') {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addEventListener = outputFieldAccess.name;
|
if (consumer instanceof TmplAstTemplate || consumer instanceof TmplAstElement) {
|
||||||
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
|
if (!ts.isPropertyAccessExpression(outputFieldAccess) ||
|
||||||
const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
|
outputFieldAccess.name.text !== 'addEventListener') {
|
||||||
const positionInShimFile = this.getShimPositionForNode(addEventListener);
|
continue;
|
||||||
const target = this.getSymbol(consumer);
|
}
|
||||||
|
|
||||||
if (target === null || tsSymbol === undefined) {
|
const addEventListener = outputFieldAccess.name;
|
||||||
return null;
|
const tsSymbol = this.getTypeChecker().getSymbolAtLocation(addEventListener);
|
||||||
}
|
const tsType = this.getTypeChecker().getTypeAtLocation(addEventListener);
|
||||||
|
const positionInShimFile = this.getShimPositionForNode(addEventListener);
|
||||||
|
const target = this.getSymbol(consumer);
|
||||||
|
|
||||||
return {
|
if (target === null || tsSymbol === undefined) {
|
||||||
kind: SymbolKind.Output,
|
continue;
|
||||||
bindings: [{
|
}
|
||||||
|
|
||||||
|
bindings.push({
|
||||||
kind: SymbolKind.Binding,
|
kind: SymbolKind.Binding,
|
||||||
tsSymbol,
|
tsSymbol,
|
||||||
tsType,
|
tsType,
|
||||||
target,
|
target,
|
||||||
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
||||||
}],
|
});
|
||||||
};
|
} else {
|
||||||
} else {
|
if (!ts.isElementAccessExpression(outputFieldAccess)) {
|
||||||
if (!ts.isElementAccessExpression(outputFieldAccess)) {
|
continue;
|
||||||
return null;
|
}
|
||||||
}
|
const tsSymbol =
|
||||||
const tsSymbol =
|
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
|
||||||
this.getTypeChecker().getSymbolAtLocation(outputFieldAccess.argumentExpression);
|
if (tsSymbol === undefined) {
|
||||||
if (tsSymbol === undefined) {
|
continue;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
|
const target = this.getDirectiveSymbolForAccessExpression(outputFieldAccess, consumer);
|
||||||
if (target === null) {
|
if (target === null) {
|
||||||
return null;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
|
const positionInShimFile = this.getShimPositionForNode(outputFieldAccess);
|
||||||
const tsType = this.getTypeChecker().getTypeAtLocation(outputFieldAccess);
|
const tsType = this.getTypeChecker().getTypeAtLocation(outputFieldAccess);
|
||||||
return {
|
bindings.push({
|
||||||
kind: SymbolKind.Output,
|
|
||||||
bindings: [{
|
|
||||||
kind: SymbolKind.Binding,
|
kind: SymbolKind.Binding,
|
||||||
tsSymbol,
|
tsSymbol,
|
||||||
tsType,
|
tsType,
|
||||||
target,
|
target,
|
||||||
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
shimLocation: {shimPath: this.shimPath, positionInShimFile},
|
||||||
}],
|
});
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
if (bindings.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {kind: SymbolKind.Output, bindings};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSymbolOfInputBinding(binding: TmplAstBoundAttribute|
|
private getSymbolOfInputBinding(binding: TmplAstBoundAttribute|
|
||||||
|
|
|
@ -87,6 +87,56 @@ describe('definitions', () => {
|
||||||
assertFileNames([def, def2], ['dir2.ts', 'dir.ts']);
|
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(
|
function getDefinitionsAndAssertBoundSpan(
|
||||||
env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) {
|
env: LanguageServiceTestEnvironment, fileName: AbsoluteFsPath, cursor: number) {
|
||||||
env.expectNoSourceDiagnostics();
|
env.expectNoSourceDiagnostics();
|
||||||
|
|
Loading…
Reference in New Issue