Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,7 @@ import {
isTypeReferenceType,
isTypeUsableAsPropertyName,
isUMDExportSymbol,
isUnaryTupleTypeNode,
isValidBigIntString,
isValidESSymbolDeclaration,
isValidTypeOnlyAliasUseSite,
Expand Down Expand Up @@ -16709,17 +16710,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isNoInferType(substitutionType) ? substitutionType.baseType : getIntersectionType([substitutionType.constraint, substitutionType.baseType]);
}

function isUnaryTupleTypeNode(node: TypeNode) {
return node.kind === SyntaxKind.TupleType && (node as TupleTypeNode).elements.length === 1;
}

function getImpliedConstraint(type: Type, checkNode: TypeNode, extendsNode: TypeNode): Type | undefined {
return isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode) ? getImpliedConstraint(type, (checkNode as TupleTypeNode).elements[0], (extendsNode as TupleTypeNode).elements[0]) :
getActualTypeVariable(getTypeFromTypeNode(checkNode)) === getActualTypeVariable(type) ? getTypeFromTypeNode(extendsNode) :
undefined;
}

function getConditionalFlowTypeOfType(type: Type, node: Node) {
function getFlowTypeOfType(type: Type, node: Node) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the rename?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally this was handling flow types in conditional types alone. Later on, this function was expanded with some logic for mapped types but the function's name was never updated. I was on the fence about renaming it but it felt like it's too specific today when perhaps it shouldn't mention conditional types given its expanded scope

let constraints: Type[] | undefined;
let covariant = true;
while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDoc) {
Expand Down Expand Up @@ -19893,7 +19890,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getTypeFromTypeNode(node: TypeNode): Type {
return getConditionalFlowTypeOfType(getTypeFromTypeNodeWorker(node), node);
return getFlowTypeOfType(getTypeFromTypeNodeWorker(node), node);
}

function getTypeFromTypeNodeWorker(node: TypeNode): Type {
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11918,3 +11918,8 @@ export const nodeCoreModules = new Set([
...unprefixedNodeCoreModulesList.map(name => `node:${name}`),
...exclusivelyPrefixedNodeCoreModules,
]);

/** @internal */
export function isUnaryTupleTypeNode(node: TypeNode) {
return node.kind === SyntaxKind.TupleType && (node as TupleTypeNode).elements.length === 1;
}
81 changes: 76 additions & 5 deletions src/services/stringCompletions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import {
addToSeen,
altDirectorySeparator,
append,
arrayFrom,
CallLikeExpression,
CancellationToken,
Expand All @@ -27,6 +28,8 @@ import {
CompletionEntry,
CompletionEntryDetails,
CompletionInfo,
concatenate,
ConditionalTypeNode,
contains,
containsPath,
ContextFlags,
Expand All @@ -41,6 +44,7 @@ import {
endsWith,
ensureTrailingDirectorySeparator,
equateStringsCaseSensitive,
escapeLeadingUnderscores,
escapeString,
Extension,
fileExtensionIsOneOf,
Expand Down Expand Up @@ -95,9 +99,11 @@ import {
isPatternMatch,
isPrivateIdentifierClassElementDeclaration,
isRootedDiskPath,
isStatement,
isString,
isStringLiteral,
isStringLiteralLike,
isUnaryTupleTypeNode,
isUrl,
JsxAttribute,
LanguageServiceHost,
Expand Down Expand Up @@ -140,6 +146,7 @@ import {
stripQuotes,
supportedTSImplementationExtensions,
Symbol,
SymbolFlags,
SyntaxKind,
textPart,
TextSpan,
Expand All @@ -152,9 +159,11 @@ import {
tryReadDirectory,
tryRemoveDirectoryPrefix,
tryRemovePrefix,
TupleTypeNode,
Type,
TypeChecker,
TypeFlags,
TypeNode,
UnionTypeNode,
unmangleScopedPackageName,
UserPreferences,
Expand Down Expand Up @@ -497,11 +506,31 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
// bar: string;
// }
// let x: Foo["/*completion position*/"]
const { indexType, objectType } = grandParent as IndexedAccessTypeNode;
if (!rangeContainsPosition(indexType, position)) {
if (!rangeContainsPosition((grandParent as IndexedAccessTypeNode).indexType, position)) {
return undefined;
}
return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType));
const objectType = typeChecker.getTypeFromTypeNode((grandParent as IndexedAccessTypeNode).objectType);
const completions = stringLiteralCompletionsFromProperties(objectType);
const flowPropertyNames = getFlowPropertyNamesOfType(typeChecker, objectType, (grandParent as IndexedAccessTypeNode).objectType);
if (flowPropertyNames) {
const existing = new Set(completions.symbols.map(s => s.escapedName));
let extraSymbols: Symbol[] | undefined;
for (const name of flowPropertyNames) {
const escapedName = escapeLeadingUnderscores(name);
if (!existing.has(escapedName)) {
existing.add(escapedName);
const symbol = typeChecker.createSymbol(SymbolFlags.Property, escapedName);
symbol.links.type = typeChecker.getAnyType();
extraSymbols = append(extraSymbols, symbol);
}
}
return {
kind: StringLiteralCompletionKind.Properties,
symbols: concatenate(completions.symbols, extraSymbols),
hasIndexSignature: completions.hasIndexSignature,
};
}
return completions;
case SyntaxKind.UnionType: {
const result = fromUnionableLiteralType(walkUpParentheses(grandParent.parent));
if (!result) {
Expand Down Expand Up @@ -529,6 +558,48 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
}
}

// this is a reverse of `getFlowTypeOfType` from the checker
function getFlowPropertyNamesOfType(typeChecker: TypeChecker, type: Type, node: Node) {
const indexType = typeChecker.getIndexType(type);

if (!indexType.isIndexType()) {
// if the index type is not deferred then there is no reason to check for flow property names
// as accessing such properties resuls in an error anyway:
//
// type A = { foo: string };
// type B = "bar" extends keyof A ? A["bar"]/*error*/ : never;
return;
}

let flowPropertyNames: string[] | undefined;

while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDoc) {
const parent = node.parent;
if (parent.kind === SyntaxKind.ConditionalType && node === (parent as ConditionalTypeNode).trueType) {
const propertyName = getImpliedPropertyName((parent as ConditionalTypeNode).checkType, (parent as ConditionalTypeNode).extendsType);
flowPropertyNames = append(flowPropertyNames, propertyName);
}
node = parent;
}

return flowPropertyNames;

function getImpliedPropertyName(checkNode: TypeNode, extendsNode: TypeNode) {
if (isUnaryTupleTypeNode(checkNode) && isUnaryTupleTypeNode(extendsNode)) {
return getImpliedPropertyName((checkNode as TupleTypeNode).elements[0], (extendsNode as TupleTypeNode).elements[0]);
}
const checkType = typeChecker.getTypeFromTypeNode(checkNode);
if (!checkType.isStringLiteral()) {
return;
}
const extendsType = typeChecker.getTypeFromTypeNode(extendsNode);
if (!typeChecker.isTypeAssignableTo(extendsType, indexType)) {
return;
}
return checkType.value;
}
}

function walkUpParentheses(node: Node) {
switch (node.kind) {
case SyntaxKind.ParenthesizedType:
Expand Down Expand Up @@ -564,8 +635,8 @@ function getStringLiteralCompletionsFromSignature(call: CallLikeExpression, arg:
return length(types) ? { kind: StringLiteralCompletionKind.Types, types, isNewIdentifier } : undefined;
}

function stringLiteralCompletionsFromProperties(type: Type | undefined): StringLiteralCompletionsFromProperties | undefined {
return type && {
function stringLiteralCompletionsFromProperties(type: Type): StringLiteralCompletionsFromProperties {
return {
kind: StringLiteralCompletionKind.Properties,
symbols: filter(type.getApparentProperties(), prop => !(prop.valueDeclaration && isPrivateIdentifierClassElementDeclaration(prop.valueDeclaration))),
hasIndexSignature: hasIndexSignature(type),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// <reference path="fourslash.ts" />

//// type Test1<T> = "foo" extends keyof T ? T["/*1*/"] : never;
////
//// type A = { bar: string };
//// type Test2 = "foo" extends keyof A ? A["/*2*/"] : never;
////
//// type Test3<T> = "foo" extends keyof T ? "bar" extends keyof T ? T["/*3*/"] : never : never;
////
//// type Test4<T extends { foo?: string }> = "foo" extends keyof T ? T["/*4*/"] : never;
////
//// type Test5<T> = "foo" extends keyof T ? "foo" extends keyof T ? T["/*5*/"] : never : never;
////
//// type Test6<T> = ["foo"] extends [keyof T] ? T["/*6*/"] : never;
////
//// type Test7<T extends { foo: string }> = "bar" extends keyof T ? T["/*7*/"] : never;
////
//// type Test8<T> = "foo" | "bar" extends keyof T ? T["/*8*/"] : never;
////
//// type Test9<T, T2> = "foo" & T2 extends keyof T ? T["/*9*/"] : never;
////
//// type Test10<T, T2> = "foo" extends keyof T & T2 ? T["/*10*/"] : never;
////
//// type Test11<T, T2> = "foo" extends keyof T | T2 ? T["/*11*/"] : never;
////
//// type Test12<T, T2> = "foo" extends keyof T2 ? T["/*12*/"] : never;

verify.completions({ marker: ["1"], exact: ["foo"] });
verify.completions({ marker: ["2"], excludes: ["foo"] });
verify.completions({ marker: ["3"], exact: ["bar", "foo"] });
verify.completions({ marker: ["4"], exact: ["foo"] });
verify.completions({ marker: ["5"], exact: ["foo"] });
verify.completions({ marker: ["6"], exact: ["foo"] });
verify.completions({ marker: ["7"], exact: ["bar", "foo"] });
verify.completions({ marker: ["8"], excludes: ["bar", "foo"] });
verify.completions({ marker: ["9"], excludes: ["foo"] });
verify.completions({ marker: ["10"], exact: ["foo"] });
verify.completions({ marker: ["11"], excludes: ["foo"] });
verify.completions({ marker: ["12"], excludes: ["foo"] });