From e12b708221ab7abe22e841d6b77a51d3d238c993 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 2 May 2017 15:33:13 -0700 Subject: [PATCH] For completions of union type, get all possible properties --- src/compiler/checker.ts | 19 ++++++++++++++++- src/compiler/types.ts | 5 +++++ src/services/completions.ts | 21 +++++++------------ .../cases/fourslash/completionListOfUnion.ts | 18 ++++++++++++++++ 4 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 tests/cases/fourslash/completionListOfUnion.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ca5d65068a0..3c45942fd62 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -204,7 +204,8 @@ namespace ts { // since we are only interested in declarations of the module itself return tryFindAmbientModule(moduleName, /*withAugmentations*/ false); }, - getApparentType + getApparentType, + getAllPossiblePropertiesOfType, }; const tupleTypes: GenericType[] = []; @@ -5648,6 +5649,22 @@ namespace ts { getPropertiesOfObjectType(type); } + function getAllPossiblePropertiesOfType(type: Type): Symbol[] { + if (type.flags & TypeFlags.Union) { + const props = createMap(); + for (const memberType of (type as UnionType).types) { + for (const { name } of getPropertiesOfType(memberType)) { + if (!props.has(name)) { + props.set(name, createUnionOrIntersectionProperty(type as UnionType, name)); + } + } + } + return arrayFrom(props.values()); + } else { + return getPropertiesOfType(type); + } + } + function getConstraintOfType(type: TypeVariable | UnionOrIntersectionType): Type { return type.flags & TypeFlags.TypeParameter ? getConstraintOfTypeParameter(type) : type.flags & TypeFlags.IndexedAccess ? getConstraintOfIndexedAccess(type) : diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d9b4ca192f4..aad4f2680e3 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2564,6 +2564,11 @@ namespace ts { /* @internal */ getIdentifierCount(): number; /* @internal */ getSymbolCount(): number; /* @internal */ getTypeCount(): number; + + /** For a union, will include a property if it's defined in *any* of the member types. + * So for `{ a } | { b }`, this will include both `a` and `b`. + */ + /* @internal */ getAllPossiblePropertiesOfType(type: Type): Symbol[]; } export enum NodeBuilderFlags { diff --git a/src/services/completions.ts b/src/services/completions.ts index 6ac3762b4c7..f429ed34b6b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -813,19 +813,16 @@ namespace ts.Completions { // We're looking up possible property names from contextual/inferred/declared type. isMemberCompletion = true; - let typeForObject: Type; + let typeMembers: Symbol[]; let existingMembers: Declaration[]; if (objectLikeContainer.kind === SyntaxKind.ObjectLiteralExpression) { // We are completing on contextual types, but may also include properties // other than those within the declared type. isNewIdentifierLocation = true; - - // If the object literal is being assigned to something of type 'null | { hello: string }', - // it clearly isn't trying to satisfy the 'null' type. So we grab the non-nullable type if possible. - typeForObject = typeChecker.getContextualType(objectLikeContainer); - typeForObject = typeForObject && typeForObject.getNonNullableType(); - + const typeForObject = typeChecker.getContextualType(objectLikeContainer); + if (!typeForObject) return false; + typeMembers = typeChecker.getAllPossiblePropertiesOfType(typeForObject); existingMembers = (objectLikeContainer).properties; } else if (objectLikeContainer.kind === SyntaxKind.ObjectBindingPattern) { @@ -849,7 +846,10 @@ namespace ts.Completions { } } if (canGetType) { - typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); + const typeForObject = typeChecker.getTypeAtLocation(objectLikeContainer); + if (!typeForObject) return false; + // In a binding pattern, get only known properties. Everywhere else we will get all possible properties. + typeMembers = typeChecker.getPropertiesOfType(typeForObject); existingMembers = (objectLikeContainer).elements; } } @@ -861,11 +861,6 @@ namespace ts.Completions { Debug.fail("Expected object literal or binding pattern, got " + objectLikeContainer.kind); } - if (!typeForObject) { - return false; - } - - const typeMembers = typeChecker.getPropertiesOfType(typeForObject); if (typeMembers && typeMembers.length > 0) { // Add filtered items to the completion list symbols = filterObjectMembersList(typeMembers, existingMembers); diff --git a/tests/cases/fourslash/completionListOfUnion.ts b/tests/cases/fourslash/completionListOfUnion.ts new file mode 100644 index 00000000000..6026615f8c1 --- /dev/null +++ b/tests/cases/fourslash/completionListOfUnion.ts @@ -0,0 +1,18 @@ +/// + +// @strictNullChecks: true + +// Non-objects should be skipped, so `| number | null` should have no effect on completions. +////const x: { a: number, b: number } | { a: string, c: string } | { b: boolean } | number | null = { /*x*/ }; + +////interface I { a: number; } +////function f(...args: Array) {} +////f({ /*f*/ }); + +goTo.marker("x"); +verify.completionListContains("a", "(property) a: string | number"); +verify.completionListContains("b", "(property) b: number | boolean"); +verify.completionListContains("c", "(property) c: string"); + +goTo.marker("f"); +verify.completionListContains("a", "(property) a: number");