diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2c1a6e3e820..591002244a8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8758,7 +8758,7 @@ namespace ts { // The location isn't a reference to the given symbol, meaning we're being asked // a hypothetical question of what type the symbol would have if there was a reference // to it at the given location. Since we have no control flow information for the - // hypotherical reference (control flow information is created and attached by the + // hypothetical reference (control flow information is created and attached by the // binder), we simply return the declared type of the symbol. return getTypeOfSymbol(symbol); } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 9a951243e0b..eb177402771 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1535,5 +1535,4 @@ namespace ts { ? ((fileName) => fileName) : ((fileName) => fileName.toLowerCase()); } - } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index af156c8f5f9..637f3629cfb 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2671,10 +2671,17 @@ namespace ts { return token >= SyntaxKind.FirstAssignment && token <= SyntaxKind.LastAssignment; } - export function isExpressionWithTypeArgumentsInClassExtendsClause(node: Node): boolean { - return node.kind === SyntaxKind.ExpressionWithTypeArguments && + /** Get `C` given `N` if `N` is in the position `class C extends N` where `N` is an ExpressionWithTypeArguments. */ + export function tryGetClassExtendingExpressionWithTypeArguments(node: Node): ClassLikeDeclaration | undefined { + if (node.kind === SyntaxKind.ExpressionWithTypeArguments && (node.parent).token === SyntaxKind.ExtendsKeyword && - isClassLike(node.parent.parent); + isClassLike(node.parent.parent)) { + return node.parent.parent; + } + } + + export function isExpressionWithTypeArgumentsInClassExtendsClause(node: Node): boolean { + return tryGetClassExtendingExpressionWithTypeArguments(node) !== undefined; } export function isEntityNameExpression(node: Expression): node is EntityNameExpression { diff --git a/src/services/services.ts b/src/services/services.ts index 050960a0da8..c97455c3244 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -2796,18 +2796,26 @@ namespace ts { return isRightSideOfPropertyAccess(node) ? node.parent : node; } - function climbPastManyPropertyAccesses(node: Node): Node { - return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; + /** Get `C` given `N` if `N` is in the position `class C extends N` or `class C extends foo.N` where `N` is an identifier. */ + function tryGetClassByExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { + return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); } function isCallExpressionTarget(node: Node): boolean { - node = climbPastPropertyAccess(node); - return node && node.parent && node.parent.kind === SyntaxKind.CallExpression && (node.parent).expression === node; + return isCallOrNewExpressionTarget(node, SyntaxKind.CallExpression); } function isNewExpressionTarget(node: Node): boolean { - node = climbPastPropertyAccess(node); - return node && node.parent && node.parent.kind === SyntaxKind.NewExpression && (node.parent).expression === node; + return isCallOrNewExpressionTarget(node, SyntaxKind.NewExpression); + } + + function isCallOrNewExpressionTarget(node: Node, kind: SyntaxKind) { + const target = climbPastPropertyAccess(node); + return target && target.parent && target.parent.kind === kind && (target.parent).expression === target; + } + + function climbPastManyPropertyAccesses(node: Node): Node { + return isRightSideOfPropertyAccess(node) ? climbPastManyPropertyAccesses(node.parent) : node; } /** Returns a CallLikeExpression where `node` is the target being invoked. */ @@ -4625,7 +4633,7 @@ namespace ts { const symbolFlags = symbol.flags; let symbolKind = getSymbolKindOfConstructorPropertyMethodAccessorFunctionOrVar(symbol, symbolFlags, location); let hasAddedSymbolInfo: boolean; - const isThisExpression: boolean = location.kind === SyntaxKind.ThisKeyword && isExpression(location); + const isThisExpression = location.kind === SyntaxKind.ThisKeyword && isExpression(location); let type: Type; // Class at constructor site need to be shown as constructor apart from property,method, vars @@ -6045,6 +6053,7 @@ namespace ts { case SyntaxKind.Identifier: case SyntaxKind.ThisKeyword: // case SyntaxKind.SuperKeyword: TODO:GH#9268 + case SyntaxKind.ConstructorKeyword: case SyntaxKind.StringLiteral: return getReferencedSymbolsForNode(node, program.getSourceFiles(), findInStrings, findInComments); } @@ -6089,6 +6098,8 @@ namespace ts { return getReferencesForSuperKeyword(node); } + // `getSymbolAtLocation` normally returns the symbol of the class when given the constructor keyword, + // so we have to specify that we want the constructor symbol. const symbol = typeChecker.getSymbolAtLocation(node); if (!symbol && node.kind === SyntaxKind.StringLiteral) { @@ -6163,7 +6174,7 @@ namespace ts { }; } - function getAliasSymbolForPropertyNameSymbol(symbol: Symbol, location: Node): Symbol { + function getAliasSymbolForPropertyNameSymbol(symbol: Symbol, location: Node): Symbol | undefined { if (symbol.flags & SymbolFlags.Alias) { // Default import get alias const defaultImport = getDeclarationOfKind(symbol, SyntaxKind.ImportClause); @@ -6189,6 +6200,10 @@ namespace ts { return undefined; } + function followAliasIfNecessary(symbol: Symbol, location: Node): Symbol { + return getAliasSymbolForPropertyNameSymbol(symbol, location) || symbol; + } + function getPropertySymbolOfDestructuringAssignment(location: Node) { return isArrayLiteralOrObjectLiteralDestructuringPattern(location.parent.parent) && typeChecker.getPropertySymbolOfDestructuringAssignment(location); @@ -6453,7 +6468,8 @@ namespace ts { if (referenceSymbol) { const referenceSymbolDeclaration = referenceSymbol.valueDeclaration; const shorthandValueSymbol = typeChecker.getShorthandAssignmentValueSymbol(referenceSymbolDeclaration); - const relatedSymbol = getRelatedSymbol(searchSymbols, referenceSymbol, referenceLocation); + const relatedSymbol = getRelatedSymbol(searchSymbols, referenceSymbol, referenceLocation, + /*searchLocationIsConstructor*/ searchLocation.kind === SyntaxKind.ConstructorKeyword); if (relatedSymbol) { const referencedSymbol = getReferencedSymbol(relatedSymbol); @@ -6469,12 +6485,94 @@ namespace ts { const referencedSymbol = getReferencedSymbol(shorthandValueSymbol); referencedSymbol.references.push(getReferenceEntryFromNode(referenceSymbolDeclaration.name)); } + else if (searchLocation.kind === SyntaxKind.ConstructorKeyword) { + findAdditionalConstructorReferences(referenceSymbol, referenceLocation); + } } }); } return; + /** Adds references when a constructor is used with `new this()` in its own class and `super()` calls in subclasses. */ + function findAdditionalConstructorReferences(referenceSymbol: Symbol, referenceLocation: Node): void { + Debug.assert(isClassLike(searchSymbol.valueDeclaration)); + + const referenceClass = referenceLocation.parent; + if (referenceSymbol === searchSymbol && isClassLike(referenceClass)) { + Debug.assert(referenceClass.name === referenceLocation); + // This is the class declaration containing the constructor. + addReferences(findOwnConstructorCalls(searchSymbol)); + } + else { + // If this class appears in `extends C`, then the extending class' "super" calls are references. + const classExtending = tryGetClassByExtendingIdentifier(referenceLocation); + if (classExtending && isClassLike(classExtending) && followAliasIfNecessary(referenceSymbol, referenceLocation) === searchSymbol) { + addReferences(superConstructorAccesses(classExtending)); + } + } + } + + function addReferences(references: Node[]): void { + if (references.length) { + const referencedSymbol = getReferencedSymbol(searchSymbol); + addRange(referencedSymbol.references, map(references, getReferenceEntryFromNode)); + } + } + + /** `classSymbol` is the class where the constructor was defined. + * Reference the constructor and all calls to `new this()`. + */ + function findOwnConstructorCalls(classSymbol: Symbol): Node[] { + const result: Node[] = []; + + for (const decl of classSymbol.members["__constructor"].declarations) { + Debug.assert(decl.kind === SyntaxKind.Constructor); + const ctrKeyword = decl.getChildAt(0); + Debug.assert(ctrKeyword.kind === SyntaxKind.ConstructorKeyword); + result.push(ctrKeyword); + } + + forEachProperty(classSymbol.exports, member => { + const decl = member.valueDeclaration; + if (decl && decl.kind === SyntaxKind.MethodDeclaration) { + const body = (decl).body; + if (body) { + forEachDescendantOfKind(body, SyntaxKind.ThisKeyword, thisKeyword => { + if (isNewExpressionTarget(thisKeyword)) { + result.push(thisKeyword); + } + }); + } + } + }); + + return result; + } + + /** Find references to `super` in the constructor of an extending class. */ + function superConstructorAccesses(cls: ClassLikeDeclaration): Node[] { + const symbol = cls.symbol; + const ctr = symbol.members["__constructor"]; + if (!ctr) { + return []; + } + + const result: Node[] = []; + for (const decl of ctr.declarations) { + Debug.assert(decl.kind === SyntaxKind.Constructor); + const body = (decl).body; + if (body) { + forEachDescendantOfKind(body, SyntaxKind.SuperKeyword, node => { + if (isCallExpressionTarget(node)) { + result.push(node); + } + }); + } + }; + return result; + } + function getReferencedSymbol(symbol: Symbol): ReferencedSymbol { const symbolId = getSymbolId(symbol); let index = symbolToIndex[symbolId]; @@ -6855,16 +6953,17 @@ namespace ts { } } - function getRelatedSymbol(searchSymbols: Symbol[], referenceSymbol: Symbol, referenceLocation: Node): Symbol { - if (searchSymbols.indexOf(referenceSymbol) >= 0) { - return referenceSymbol; + function getRelatedSymbol(searchSymbols: Symbol[], referenceSymbol: Symbol, referenceLocation: Node, searchLocationIsConstructor: boolean): Symbol | undefined { + if (contains(searchSymbols, referenceSymbol)) { + // If we are searching for constructor uses, they must be 'new' expressions. + return (!searchLocationIsConstructor || isNewExpressionTarget(referenceLocation)) && referenceSymbol; } // If the reference symbol is an alias, check if what it is aliasing is one of the search // symbols but by looking up for related symbol of this alias so it can handle multiple level of indirectness. const aliasSymbol = getAliasSymbolForPropertyNameSymbol(referenceSymbol, referenceLocation); if (aliasSymbol) { - return getRelatedSymbol(searchSymbols, aliasSymbol, referenceLocation); + return getRelatedSymbol(searchSymbols, aliasSymbol, referenceLocation, searchLocationIsConstructor); } // If the reference location is in an object literal, try to get the contextual type for the @@ -8388,6 +8487,15 @@ namespace ts { }; } + function forEachDescendantOfKind(node: Node, kind: SyntaxKind, action: (node: Node) => void) { + forEachChild(node, child => { + if (child.kind === kind) { + action(child); + } + forEachDescendantOfKind(child, kind, action); + }); + } + /* @internal */ export function getNameTable(sourceFile: SourceFile): Map { if (!sourceFile.nameTable) { diff --git a/tests/cases/fourslash/findAllReferencesOfConstructor.ts b/tests/cases/fourslash/findAllReferencesOfConstructor.ts new file mode 100644 index 00000000000..b25508a60db --- /dev/null +++ b/tests/cases/fourslash/findAllReferencesOfConstructor.ts @@ -0,0 +1,46 @@ +/// + +// @Filename: a.ts +////export class C { +//// [|constructor|](n: number); +//// [|constructor|](); +//// [|constructor|](n?: number){} +//// static f() { +//// this.f(); +//// new [|this|](); +//// } +////} +////new [|C|](); +// Does not handle alias. +////const D = C; +////new D(); + +// @Filename: b.ts +////import { C } from "./a"; +////new [|C|](); + +// @Filename: c.ts +////import { C } from "./a"; +////class D extends C { +//// constructor() { +//// [|super|](); +//// super.method(); +//// } +//// method() { super(); } +////} +// Does not find 'super()' calls for a class that merely implements 'C', +// since those must be calling a different constructor. +////class E implements C { +//// constructor() { super(); } +////} + +// Works with qualified names too +// @Filename: d.ts +////import * as a from "./a"; +////new a.[|C|](); +////class d extends a.C { constructor() { [|super|](); } + +const ranges = test.ranges(); +for (const ctr of ranges.slice(0, 3)) { + verify.referencesOf(ctr, ranges); +} diff --git a/tests/cases/fourslash/findAllReferencesOfConstructor_badOverload.ts b/tests/cases/fourslash/findAllReferencesOfConstructor_badOverload.ts new file mode 100644 index 00000000000..77479bad113 --- /dev/null +++ b/tests/cases/fourslash/findAllReferencesOfConstructor_badOverload.ts @@ -0,0 +1,8 @@ +/// + +////class C { +//// [|constructor|](n: number); +//// [|constructor|](){} +////} + +verify.rangesReferenceEachOther();