Skip to content

Commit 0f6f3b7

Browse files
authored
Fix find all references of inherited constructor (#30514)
* recursively look for inherited constructor references * add test * remove outdated comment * add tests * move function * improve tests * minor refactor * fix convert params refactoring to deal with inherited constructor calls * simplify refactor test
1 parent 1639a5a commit 0f6f3b7

7 files changed

+165
-48
lines changed

src/services/findAllReferences.ts

+60-37
Original file line numberDiff line numberDiff line change
@@ -586,25 +586,28 @@ namespace ts.FindAllReferences.Core {
586586
}
587587
else {
588588
const search = state.createSearch(node, symbol, /*comingFrom*/ undefined, { allSearchSymbols: node ? populateSearchSymbolSet(symbol, node, checker, !!options.isForRename, !!options.providePrefixAndSuffixTextForRename, !!options.implementations) : [symbol] });
589-
590-
// Try to get the smallest valid scope that we can limit our search to;
591-
// otherwise we'll need to search globally (i.e. include each file).
592-
const scope = getSymbolScope(symbol);
593-
if (scope) {
594-
getReferencesInContainer(scope, scope.getSourceFile(), search, state, /*addReferencesHere*/ !(isSourceFile(scope) && !contains(sourceFiles, scope)));
595-
}
596-
else {
597-
// Global search
598-
for (const sourceFile of state.sourceFiles) {
599-
state.cancellationToken.throwIfCancellationRequested();
600-
searchForName(sourceFile, search, state);
601-
}
602-
}
589+
getReferencesInContainerOrFiles(symbol, state, search);
603590
}
604591

605592
return result;
606593
}
607594

595+
function getReferencesInContainerOrFiles(symbol: Symbol, state: State, search: Search): void {
596+
// Try to get the smallest valid scope that we can limit our search to;
597+
// otherwise we'll need to search globally (i.e. include each file).
598+
const scope = getSymbolScope(symbol);
599+
if (scope) {
600+
getReferencesInContainer(scope, scope.getSourceFile(), search, state, /*addReferencesHere*/ !(isSourceFile(scope) && !contains(state.sourceFiles, scope)));
601+
}
602+
else {
603+
// Global search
604+
for (const sourceFile of state.sourceFiles) {
605+
state.cancellationToken.throwIfCancellationRequested();
606+
searchForName(sourceFile, search, state);
607+
}
608+
}
609+
}
610+
608611
function getSpecialSearchKind(node: Node): SpecialSearchKind {
609612
switch (node.kind) {
610613
case SyntaxKind.ConstructorKeyword:
@@ -707,7 +710,6 @@ namespace ts.FindAllReferences.Core {
707710
constructor(
708711
readonly sourceFiles: ReadonlyArray<SourceFile>,
709712
readonly sourceFilesSet: ReadonlyMap<true>,
710-
/** True if we're searching for constructor references. */
711713
readonly specialSearchKind: SpecialSearchKind,
712714
readonly checker: TypeChecker,
713715
readonly cancellationToken: CancellationToken,
@@ -1293,6 +1295,7 @@ namespace ts.FindAllReferences.Core {
12931295
const classExtending = tryGetClassByExtendingIdentifier(referenceLocation);
12941296
if (classExtending) {
12951297
findSuperConstructorAccesses(classExtending, pusher());
1298+
findInheritedConstructorReferences(classExtending, state);
12961299
}
12971300
}
12981301
}
@@ -1325,35 +1328,44 @@ namespace ts.FindAllReferences.Core {
13251328
* Reference the constructor and all calls to `new this()`.
13261329
*/
13271330
function findOwnConstructorReferences(classSymbol: Symbol, sourceFile: SourceFile, addNode: (node: Node) => void): void {
1328-
for (const decl of classSymbol.members!.get(InternalSymbolName.Constructor)!.declarations) {
1329-
const ctrKeyword = findChildOfKind(decl, SyntaxKind.ConstructorKeyword, sourceFile)!;
1330-
Debug.assert(decl.kind === SyntaxKind.Constructor && !!ctrKeyword);
1331-
addNode(ctrKeyword);
1332-
}
1333-
1334-
classSymbol.exports!.forEach(member => {
1335-
const decl = member.valueDeclaration;
1336-
if (decl && decl.kind === SyntaxKind.MethodDeclaration) {
1337-
const body = (<MethodDeclaration>decl).body;
1338-
if (body) {
1339-
forEachDescendantOfKind(body, SyntaxKind.ThisKeyword, thisKeyword => {
1340-
if (isNewExpressionTarget(thisKeyword)) {
1341-
addNode(thisKeyword);
1342-
}
1343-
});
1344-
}
1331+
const constructorSymbol = getClassConstructorSymbol(classSymbol);
1332+
if (constructorSymbol) {
1333+
for (const decl of constructorSymbol.declarations) {
1334+
const ctrKeyword = findChildOfKind(decl, SyntaxKind.ConstructorKeyword, sourceFile)!;
1335+
Debug.assert(decl.kind === SyntaxKind.Constructor && !!ctrKeyword);
1336+
addNode(ctrKeyword);
13451337
}
1346-
});
1338+
}
1339+
1340+
if (classSymbol.exports) {
1341+
classSymbol.exports.forEach(member => {
1342+
const decl = member.valueDeclaration;
1343+
if (decl && decl.kind === SyntaxKind.MethodDeclaration) {
1344+
const body = (<MethodDeclaration>decl).body;
1345+
if (body) {
1346+
forEachDescendantOfKind(body, SyntaxKind.ThisKeyword, thisKeyword => {
1347+
if (isNewExpressionTarget(thisKeyword)) {
1348+
addNode(thisKeyword);
1349+
}
1350+
});
1351+
}
1352+
}
1353+
});
1354+
}
1355+
}
1356+
1357+
function getClassConstructorSymbol(classSymbol: Symbol): Symbol | undefined {
1358+
return classSymbol.members && classSymbol.members.get(InternalSymbolName.Constructor);
13471359
}
13481360

13491361
/** Find references to `super` in the constructor of an extending class. */
1350-
function findSuperConstructorAccesses(cls: ClassLikeDeclaration, addNode: (node: Node) => void): void {
1351-
const ctr = cls.symbol.members!.get(InternalSymbolName.Constructor);
1352-
if (!ctr) {
1362+
function findSuperConstructorAccesses(classDeclaration: ClassLikeDeclaration, addNode: (node: Node) => void): void {
1363+
const constructor = getClassConstructorSymbol(classDeclaration.symbol);
1364+
if (!constructor) {
13531365
return;
13541366
}
13551367

1356-
for (const decl of ctr.declarations) {
1368+
for (const decl of constructor.declarations) {
13571369
Debug.assert(decl.kind === SyntaxKind.Constructor);
13581370
const body = (<ConstructorDeclaration>decl).body;
13591371
if (body) {
@@ -1366,6 +1378,17 @@ namespace ts.FindAllReferences.Core {
13661378
}
13671379
}
13681380

1381+
function hasOwnConstructor(classDeclaration: ClassLikeDeclaration): boolean {
1382+
return !!getClassConstructorSymbol(classDeclaration.symbol);
1383+
}
1384+
1385+
function findInheritedConstructorReferences(classDeclaration: ClassLikeDeclaration, state: State): void {
1386+
if (hasOwnConstructor(classDeclaration)) return;
1387+
const classSymbol = classDeclaration.symbol;
1388+
const search = state.createSearch(/*location*/ undefined, classSymbol, /*comingFrom*/ undefined);
1389+
getReferencesInContainerOrFiles(classSymbol, state, search);
1390+
}
1391+
13691392
function addImplementationReferences(refNode: Node, addReference: (node: Node) => void, state: State): void {
13701393
// Check if we found a function/propertyAssignment/method with an implementation or initializer
13711394
if (isDeclarationName(refNode) && isImplementation(refNode.parent)) {

src/services/refactors/convertParamsToDestructuredObject.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,19 @@ namespace ts.refactor.convertParamsToDestructuredObject {
9999
groupedReferences.valid = false;
100100
continue;
101101
}
102-
if (contains(functionSymbols, checker.getSymbolAtLocation(entry.node), symbolComparer)) {
102+
103+
/* We compare symbols because in some cases find all references wil return a reference that may or may not be to the refactored function.
104+
Example from the refactorConvertParamsToDestructuredObject_methodCallUnion.ts test:
105+
class A { foo(a: number, b: number) { return a + b; } }
106+
class B { foo(c: number, d: number) { return c + d; } }
107+
declare const ab: A | B;
108+
ab.foo(1, 2);
109+
Find all references will return `ab.foo(1, 2)` as a reference to A's `foo` but we could be calling B's `foo`.
110+
When looking for constructor calls, however, the symbol on the constructor call reference is going to be the corresponding class symbol.
111+
So we need to add a special case for this because when calling a constructor of a class through one of its subclasses,
112+
the symbols are going to be different.
113+
*/
114+
if (contains(functionSymbols, checker.getSymbolAtLocation(entry.node), symbolComparer) || isNewExpressionTarget(entry.node)) {
103115
const decl = entryToDeclaration(entry);
104116
if (decl) {
105117
groupedReferences.declarations.push(decl);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
4+
////class A {
5+
//// [|constructor|](s: string) {}
6+
////}
7+
////class B extends A { }
8+
////class C extends B {
9+
//// [|constructor|]() {
10+
//// [|super|]("");
11+
//// }
12+
////}
13+
////class D extends B { }
14+
////class E implements A { }
15+
////const a = new [|A|]("a");
16+
////const b = new [|B|]("b");
17+
////const c = new [|C|]();
18+
////const d = new [|D|]("d");
19+
////const e = new E();
20+
21+
verify.noErrors();
22+
const [aCtr, cCtr, cSuper, aNew, bNew, cNew, dNew] = test.ranges();
23+
verify.referenceGroups(aCtr, [
24+
{ definition: "class A", ranges: [aCtr, aNew] },
25+
{ definition: "class B", ranges: [cSuper, bNew]},
26+
{ definition: "class D", ranges: [dNew]}]);
27+
verify.referenceGroups(cCtr, [{ definition: "class C", ranges: [cCtr, cNew]}]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
4+
////class A {
5+
//// [|constructor|](s: string) {}
6+
////}
7+
////class B extends A {
8+
//// [|constructor|]() { [|super|](""); }
9+
////}
10+
////class C extends B {
11+
//// [|constructor|]() {
12+
//// [|super|]();
13+
//// }
14+
////}
15+
////class D extends B { }
16+
////const a = new [|A|]("a");
17+
////const b = new [|B|]();
18+
////const c = new [|C|]();
19+
////const d = new [|D|]();
20+
21+
verify.noErrors();
22+
const [aCtr, bCtr, bSuper, cCtr, cSuper, aNew, bNew, cNew, dNew] = test.ranges();
23+
verify.referenceGroups(aCtr, [{ definition: "class A", ranges: [aCtr, bSuper, aNew] }]);
24+
verify.referenceGroups(bCtr, [{ definition: "class B", ranges: [bCtr, cSuper, bNew]}, { definition: "class D", ranges: [dNew]}]);
25+
verify.referenceGroups(cCtr, [{ definition: "class C", ranges: [cCtr, cNew]}]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: f.ts
4+
5+
////class A {
6+
//// [|constructor|](s: string) {}
7+
////}
8+
////class B extends A { }
9+
////export { [|{| "isWriteAccess": true, "isDefinition": true |}A|], [|{| "isWriteAccess": true, "isDefinition": true |}B|] };
10+
11+
// @Filename: a.ts
12+
13+
////import { [|A|] as A1 } from "./f";
14+
////const a1 = new [|A1|]("a1");
15+
////export default class extends A1 { }
16+
////export { [|B|] as [|{| "isWriteAccess": true, "isDefinition": true |}B1|] } from "./f";
17+
18+
// @Filename: b.ts
19+
20+
////import [|B|], { B1 } from "./a";
21+
////const d = new [|B|]("b");
22+
////const d1 = new [|B1|]("b1");
23+
24+
verify.noErrors();
25+
const [aCtr, aExport, bExport, aImport, a1New, bReExport, b1Export, bDefault, bNew, b1New ] = test.ranges();
26+
verify.referenceGroups(aCtr, [
27+
{ definition: "class A", ranges: [aCtr, aExport] },
28+
{ definition: "class B", ranges: [bExport]},
29+
{ definition: "(alias) class B\nexport B", ranges: [bReExport]},
30+
{ definition: "(alias) class B1\nexport B1", ranges: [b1Export]},
31+
{ definition: "(alias) class B1\nimport B1", ranges: [b1New]},
32+
{ definition: "(alias) class A\nexport A", ranges: [aImport]},
33+
{ definition: "(alias) class A1\nimport A1", ranges: [a1New]},
34+
{ definition: "class default", ranges: []},
35+
{ definition: { text: "import B", range: bDefault }, ranges: [bNew]}]);

tests/cases/fourslash/refactorConvertParamsToDestructuredObject_inheritedConstructor.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
////var foo = new Foo("c", "d");
99

1010
goTo.select("a", "b");
11-
/* The expected new content is currently wrong.
12-
`new Bar("a", "b")` should be modified by the refactor to be `new Bar({ t: "a", s: "b" })`
13-
*/
1411
edit.applyRefactor({
1512
refactorName: "Convert parameters to destructured object",
1613
actionName: "Convert parameters to destructured object",
@@ -19,6 +16,6 @@ edit.applyRefactor({
1916
constructor({ t, s }: { t: string; s: string; }) { }
2017
}
2118
class Bar extends Foo { }
22-
var bar = new Bar("a", "b");
19+
var bar = new Bar({ t: "a", s: "b" });
2320
var foo = new Foo({ t: "c", s: "d" });`
2421
});

tests/cases/fourslash/refactorConvertParamsToDestructuredObject_methodCallUnion.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
////class B {
77
//// foo(c: number, d: number) { return c + d; }
88
////}
9-
////function foo(ab: A | B) {
10-
//// return ab.foo(1, 2);
11-
////}
9+
////declare var ab: A | B;
10+
////ab.foo(1, 2);
1211

1312

1413
goTo.select("a", "b");
@@ -23,7 +22,6 @@ edit.applyRefactor({
2322
class B {
2423
foo(c: number, d: number) { return c + d; }
2524
}
26-
function foo(ab: A | B) {
27-
return ab.foo(1, 2);
28-
}`
25+
declare var ab: A | B;
26+
ab.foo(1, 2);`
2927
});

0 commit comments

Comments
 (0)