From ef9156fd3fcb87d35a4e4efd759cfcce147b86f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 30 Aug 2024 23:54:46 +0200 Subject: [PATCH] Provide string literal completions for flow properties --- src/compiler/checker.ts | 9 +-- src/compiler/utilities.ts | 5 ++ src/services/stringCompletions.ts | 81 +++++++++++++++++-- ...CompletionsFlowPropertiesIndexedAccess1.ts | 39 +++++++++ 4 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 tests/cases/fourslash/stringLiteralCompletionsFlowPropertiesIndexedAccess1.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 614a958d618d4..1ef3dd0d44ab5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -769,6 +769,7 @@ import { isTypeReferenceType, isTypeUsableAsPropertyName, isUMDExportSymbol, + isUnaryTupleTypeNode, isValidBigIntString, isValidESSymbolDeclaration, isValidTypeOnlyAliasUseSite, @@ -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) { let constraints: Type[] | undefined; let covariant = true; while (node && !isStatement(node) && node.kind !== SyntaxKind.JSDoc) { @@ -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 { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 673c0b38c156d..63d61b3593fab 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -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; +} diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index 667cdf24ca8aa..2c0367052364b 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -11,6 +11,7 @@ import { import { addToSeen, altDirectorySeparator, + append, arrayFrom, CallLikeExpression, CancellationToken, @@ -27,6 +28,8 @@ import { CompletionEntry, CompletionEntryDetails, CompletionInfo, + concatenate, + ConditionalTypeNode, contains, containsPath, ContextFlags, @@ -41,6 +44,7 @@ import { endsWith, ensureTrailingDirectorySeparator, equateStringsCaseSensitive, + escapeLeadingUnderscores, escapeString, Extension, fileExtensionIsOneOf, @@ -95,9 +99,11 @@ import { isPatternMatch, isPrivateIdentifierClassElementDeclaration, isRootedDiskPath, + isStatement, isString, isStringLiteral, isStringLiteralLike, + isUnaryTupleTypeNode, isUrl, JsxAttribute, LanguageServiceHost, @@ -140,6 +146,7 @@ import { stripQuotes, supportedTSImplementationExtensions, Symbol, + SymbolFlags, SyntaxKind, textPart, TextSpan, @@ -152,9 +159,11 @@ import { tryReadDirectory, tryRemoveDirectoryPrefix, tryRemovePrefix, + TupleTypeNode, Type, TypeChecker, TypeFlags, + TypeNode, UnionTypeNode, unmangleScopedPackageName, UserPreferences, @@ -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) { @@ -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: @@ -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), diff --git a/tests/cases/fourslash/stringLiteralCompletionsFlowPropertiesIndexedAccess1.ts b/tests/cases/fourslash/stringLiteralCompletionsFlowPropertiesIndexedAccess1.ts new file mode 100644 index 0000000000000..30745a6ce8825 --- /dev/null +++ b/tests/cases/fourslash/stringLiteralCompletionsFlowPropertiesIndexedAccess1.ts @@ -0,0 +1,39 @@ +/// + +//// type Test1 = "foo" extends keyof T ? T["/*1*/"] : never; +//// +//// type A = { bar: string }; +//// type Test2 = "foo" extends keyof A ? A["/*2*/"] : never; +//// +//// type Test3 = "foo" extends keyof T ? "bar" extends keyof T ? T["/*3*/"] : never : never; +//// +//// type Test4 = "foo" extends keyof T ? T["/*4*/"] : never; +//// +//// type Test5 = "foo" extends keyof T ? "foo" extends keyof T ? T["/*5*/"] : never : never; +//// +//// type Test6 = ["foo"] extends [keyof T] ? T["/*6*/"] : never; +//// +//// type Test7 = "bar" extends keyof T ? T["/*7*/"] : never; +//// +//// type Test8 = "foo" | "bar" extends keyof T ? T["/*8*/"] : never; +//// +//// type Test9 = "foo" & T2 extends keyof T ? T["/*9*/"] : never; +//// +//// type Test10 = "foo" extends keyof T & T2 ? T["/*10*/"] : never; +//// +//// type Test11 = "foo" extends keyof T | T2 ? T["/*11*/"] : never; +//// +//// type Test12 = "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"] });