Skip to content

Commit 6037cf5

Browse files
authored
Provide member completions after dot in type locations using typeof (#54486)
1 parent 4320104 commit 6037cf5

File tree

4 files changed

+51
-23
lines changed

4 files changed

+51
-23
lines changed

src/compiler/checker.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ import {
564564
isInternalModuleImportEqualsDeclaration,
565565
isInTopLevelContext,
566566
isIntrinsicJsxName,
567+
isInTypeQuery,
567568
isIterationStatement,
568569
isJSDocAllType,
569570
isJSDocAugmentsTag,
@@ -25372,15 +25373,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2537225373
return links.resolvedSymbol;
2537325374
}
2537425375

25375-
function isInTypeQuery(node: Node): boolean {
25376-
// TypeScript 1.0 spec (April 2014): 3.6.3
25377-
// A type query consists of the keyword typeof followed by an expression.
25378-
// The expression is restricted to a single identifier or a sequence of identifiers separated by periods
25379-
return !!findAncestor(
25380-
node,
25381-
n => n.kind === SyntaxKind.TypeQuery ? true : n.kind === SyntaxKind.Identifier || n.kind === SyntaxKind.QualifiedName ? false : "quit");
25382-
}
25383-
2538425376
function isInAmbientOrTypeNode(node: Node): boolean {
2538525377
return !!(node.flags & NodeFlags.Ambient || findAncestor(node, n => isInterfaceDeclaration(n) || isTypeAliasDeclaration(n) || isTypeLiteralNode(n)));
2538625378
}

src/compiler/utilities.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6428,6 +6428,16 @@ export function isThisIdentifier(node: Node | undefined): boolean {
64286428
return !!node && node.kind === SyntaxKind.Identifier && identifierIsThisKeyword(node as Identifier);
64296429
}
64306430

6431+
/** @internal */
6432+
export function isInTypeQuery(node: Node): boolean {
6433+
// TypeScript 1.0 spec (April 2014): 3.6.3
6434+
// A type query consists of the keyword typeof followed by an expression.
6435+
// The expression is restricted to a single identifier or a sequence of identifiers separated by periods
6436+
return !!findAncestor(
6437+
node,
6438+
n => n.kind === SyntaxKind.TypeQuery ? true : n.kind === SyntaxKind.Identifier || n.kind === SyntaxKind.QualifiedName ? false : "quit");
6439+
}
6440+
64316441
/** @internal */
64326442
export function isThisInTypeQuery(node: Node): boolean {
64336443
if (!isThisIdentifier(node)) {

src/services/completions.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ import {
181181
isInString,
182182
isInterfaceDeclaration,
183183
isIntersectionTypeNode,
184+
isInTypeQuery,
184185
isJSDoc,
185186
isJSDocAugmentsTag,
186187
isJSDocImplementsTag,
@@ -3515,28 +3516,33 @@ function getCompletionData(
35153516
}
35163517
}
35173518

3518-
if (!isTypeLocation) {
3519+
if (!isTypeLocation || isInTypeQuery(node)) {
35193520
// GH#39946. Pulling on the type of a node inside of a function with a contextual `this` parameter can result in a circularity
35203521
// if the `node` is part of the exprssion of a `yield` or `return`. This circularity doesn't exist at compile time because
35213522
// we will check (and cache) the type of `this` *before* checking the type of the node.
35223523
typeChecker.tryGetThisTypeAt(node, /*includeGlobalThis*/ false);
3523-
35243524
let type = typeChecker.getTypeAtLocation(node).getNonOptionalType();
3525-
let insertQuestionDot = false;
3526-
if (type.isNullableType()) {
3527-
const canCorrectToQuestionDot =
3528-
isRightOfDot &&
3529-
!isRightOfQuestionDot &&
3530-
preferences.includeAutomaticOptionalChainCompletions !== false;
3531-
3532-
if (canCorrectToQuestionDot || isRightOfQuestionDot) {
3533-
type = type.getNonNullableType();
3534-
if (canCorrectToQuestionDot) {
3535-
insertQuestionDot = true;
3525+
3526+
if (!isTypeLocation) {
3527+
let insertQuestionDot = false;
3528+
if (type.isNullableType()) {
3529+
const canCorrectToQuestionDot =
3530+
isRightOfDot &&
3531+
!isRightOfQuestionDot &&
3532+
preferences.includeAutomaticOptionalChainCompletions !== false;
3533+
3534+
if (canCorrectToQuestionDot || isRightOfQuestionDot) {
3535+
type = type.getNonNullableType();
3536+
if (canCorrectToQuestionDot) {
3537+
insertQuestionDot = true;
3538+
}
35363539
}
35373540
}
3541+
addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot);
3542+
}
3543+
else {
3544+
addTypeProperties(type.getNonNullableType(), /*insertAwait*/ false, /*insertQuestionDot*/ false);
35383545
}
3539-
addTypeProperties(type, !!(node.flags & NodeFlags.AwaitContext), insertQuestionDot);
35403546
}
35413547
}
35423548

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @strict: true
4+
5+
// repro from https://github.com/microsoft/TypeScript/issues/54480
6+
7+
//// const languageService = { getCompletions() {} }
8+
//// type A = Parameters<typeof languageService./*1*/>
9+
////
10+
//// declare const obj: { dance: () => {} } | undefined
11+
//// type B = Parameters<typeof obj./*2*/>
12+
13+
verify.completions({
14+
marker: "1",
15+
includes: [{ name: "getCompletions", text: "(method) getCompletions(): void" }],
16+
});
17+
verify.completions({
18+
marker: "2",
19+
includes: [{ name: "dance", text: "(property) dance: () => {}" }],
20+
});

0 commit comments

Comments
 (0)