Skip to content

Commit 204b70d

Browse files
author
Andy
authored
Don't add completions from a discriminated union type when the discriminant doesn't match (microsoft#24770)
* Don't add completions from a discriminated union type when the discriminant doesn't match * Move code to checker * Update API (microsoft#24966) * Use isTypeIdenticalTo
1 parent eb7ff43 commit 204b70d

File tree

5 files changed

+55
-12
lines changed

5 files changed

+55
-12
lines changed

src/compiler/checker.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ namespace ts {
109109
getDeclaredTypeOfSymbol,
110110
getPropertiesOfType,
111111
getPropertyOfType: (type, name) => getPropertyOfType(type, escapeLeadingUnderscores(name)),
112+
getTypeOfPropertyOfType: (type, name) => getTypeOfPropertyOfType(type, escapeLeadingUnderscores(name)),
112113
getIndexInfoOfType,
113114
getSignaturesOfType,
114115
getIndexTypeOfType,
@@ -290,6 +291,7 @@ namespace ts {
290291
getNeverType: () => neverType,
291292
isSymbolAccessible,
292293
isArrayLikeType,
294+
isTypeInvalidDueToUnionDiscriminant,
293295
getAllPossiblePropertiesOfTypes,
294296
getSuggestionForNonexistentProperty: (node, type) => getSuggestionForNonexistentProperty(node, type),
295297
getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning),
@@ -6687,6 +6689,18 @@ namespace ts {
66876689
getPropertiesOfObjectType(type);
66886690
}
66896691

6692+
function isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression): boolean {
6693+
return obj.properties.some(property => {
6694+
const name = property.name && getTextOfPropertyName(property.name);
6695+
const expected = name === undefined ? undefined : getTypeOfPropertyOfType(contextualType, name);
6696+
if (expected && typeIsLiteralType(expected)) {
6697+
const actual = getTypeOfNode(property);
6698+
return !!actual && !isTypeIdenticalTo(actual, expected);
6699+
}
6700+
return false;
6701+
});
6702+
}
6703+
66906704
function getAllPossiblePropertiesOfTypes(types: ReadonlyArray<Type>): Symbol[] {
66916705
const unionType = getUnionType(types);
66926706
if (!(unionType.flags & TypeFlags.Union)) {
@@ -29082,4 +29096,8 @@ namespace ts {
2908229096
export const LibraryManagedAttributes = "LibraryManagedAttributes" as __String;
2908329097
// tslint:enable variable-name
2908429098
}
29099+
29100+
function typeIsLiteralType(type: Type): type is LiteralType {
29101+
return !!(type.flags & TypeFlags.Literal);
29102+
}
2908529103
}

src/compiler/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2883,6 +2883,7 @@ namespace ts {
28832883
getDeclaredTypeOfSymbol(symbol: Symbol): Type;
28842884
getPropertiesOfType(type: Type): Symbol[];
28852885
getPropertyOfType(type: Type, propertyName: string): Symbol | undefined;
2886+
/* @internal */ getTypeOfPropertyOfType(type: Type, propertyName: string): Type | undefined;
28862887
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined;
28872888
getSignaturesOfType(type: Type, kind: SignatureKind): ReadonlyArray<Signature>;
28882889
getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined;
@@ -3045,6 +3046,11 @@ namespace ts {
30453046
/* @internal */ getTypeCount(): number;
30463047

30473048
/* @internal */ isArrayLikeType(type: Type): boolean;
3049+
/**
3050+
* True if `contextualType` should not be considered for completions because
3051+
* e.g. it specifies `kind: "a"` and obj has `kind: "b"`.
3052+
*/
3053+
/* @internal */ isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression): boolean;
30483054
/**
30493055
* For a union, will include a property if it's defined in *any* of the member types.
30503056
* So for `{ a } | { b }`, this will include both `a` and `b`.

src/services/completions.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,7 @@ namespace ts.Completions {
11011101
// each individual type has. This is because we're going to add all identifiers
11021102
// anyways. So we might as well elevate the members that were at least part
11031103
// of the individual types to a higher status since we know what they are.
1104-
symbols.push(...getPropertiesForCompletion(type, typeChecker, /*isForAccess*/ true));
1104+
symbols.push(...getPropertiesForCompletion(type, typeChecker));
11051105
}
11061106
else {
11071107
for (const symbol of type.getApparentProperties()) {
@@ -1221,7 +1221,7 @@ namespace ts.Completions {
12211221
if (preferences.includeCompletionsWithInsertText && scopeNode.kind !== SyntaxKind.SourceFile) {
12221222
const thisType = typeChecker.tryGetThisTypeAt(scopeNode);
12231223
if (thisType) {
1224-
for (const symbol of getPropertiesForCompletion(thisType, typeChecker, /*isForAccess*/ true)) {
1224+
for (const symbol of getPropertiesForCompletion(thisType, typeChecker)) {
12251225
symbolToOriginInfoMap[getSymbolId(symbol)] = { type: "this-type" };
12261226
symbols.push(symbol);
12271227
}
@@ -1538,7 +1538,7 @@ namespace ts.Completions {
15381538
const typeForObject = typeChecker.getContextualType(objectLikeContainer);
15391539
if (!typeForObject) return GlobalsSearch.Fail;
15401540
isNewIdentifierLocation = hasIndexSignature(typeForObject);
1541-
typeMembers = getPropertiesForCompletion(typeForObject, typeChecker, /*isForAccess*/ false);
1541+
typeMembers = getPropertiesForObjectExpression(typeForObject, objectLikeContainer, typeChecker);
15421542
existingMembers = objectLikeContainer.properties;
15431543
}
15441544
else {
@@ -2159,19 +2159,25 @@ namespace ts.Completions {
21592159
return jsdoc && jsdoc.tags && (rangeContainsPosition(jsdoc, position) ? findLast(jsdoc.tags, tag => tag.pos < position) : undefined);
21602160
}
21612161

2162+
function getPropertiesForObjectExpression(contextualType: Type, obj: ObjectLiteralExpression, checker: TypeChecker): Symbol[] {
2163+
return contextualType.isUnion()
2164+
? checker.getAllPossiblePropertiesOfTypes(contextualType.types.filter(memberType =>
2165+
// If we're providing completions for an object literal, skip primitive, array-like, or callable types since those shouldn't be implemented by object literals.
2166+
!(memberType.flags & TypeFlags.Primitive ||
2167+
checker.isArrayLikeType(memberType) ||
2168+
typeHasCallOrConstructSignatures(memberType, checker) ||
2169+
checker.isTypeInvalidDueToUnionDiscriminant(memberType, obj))))
2170+
: contextualType.getApparentProperties();
2171+
}
2172+
21622173
/**
21632174
* Gets all properties on a type, but if that type is a union of several types,
21642175
* excludes array-like types or callable/constructable types.
21652176
*/
2166-
function getPropertiesForCompletion(type: Type, checker: TypeChecker, isForAccess: boolean): Symbol[] {
2167-
if (!(type.isUnion())) {
2168-
return Debug.assertEachDefined(type.getApparentProperties(), "getApparentProperties() should all be defined");
2169-
}
2170-
2171-
// If we're providing completions for an object literal, skip primitive, array-like, or callable types since those shouldn't be implemented by object literals.
2172-
const filteredTypes = isForAccess ? type.types : type.types.filter(memberType =>
2173-
!(memberType.flags & TypeFlags.Primitive || checker.isArrayLikeType(memberType) || typeHasCallOrConstructSignatures(memberType, checker)));
2174-
return Debug.assertEachDefined(checker.getAllPossiblePropertiesOfTypes(filteredTypes), "getAllPossiblePropertiesOfTypes() should all be defined");
2177+
function getPropertiesForCompletion(type: Type, checker: TypeChecker): Symbol[] {
2178+
return type.isUnion()
2179+
? Debug.assertEachDefined(checker.getAllPossiblePropertiesOfTypes(type.types), "getAllPossiblePropertiesOfTypes() should all be defined")
2180+
: Debug.assertEachDefined(type.getApparentProperties(), "getApparentProperties() should all be defined");
21752181
}
21762182

21772183
/**

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2570,6 +2570,7 @@ declare namespace ts {
25702570
getDeclaredTypeOfSymbol(symbol: Symbol): Type;
25712571
getPropertiesOfType(type: Type): Symbol[];
25722572
getPropertyOfType(type: Type, propertyName: string): Symbol | undefined;
2573+
getTypeOfPropertyOfType(type: Type, propertyName: string): Type | undefined;
25732574
getIndexInfoOfType(type: Type, kind: IndexKind): IndexInfo | undefined;
25742575
getSignaturesOfType(type: Type, kind: SignatureKind): ReadonlyArray<Signature>;
25752576
getIndexTypeOfType(type: Type, kind: IndexKind): Type | undefined;
@@ -2707,6 +2708,11 @@ declare namespace ts {
27072708
getSymbolCount(): number;
27082709
getTypeCount(): number;
27092710
isArrayLikeType(type: Type): boolean;
2711+
/**
2712+
* True if `contextualType` should not be considered for completions because
2713+
* e.g. it specifies `kind: "a"` and obj has `kind: "b"`.
2714+
*/
2715+
isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression): boolean;
27102716
/**
27112717
* For a union, will include a property if it's defined in *any* of the member types.
27122718
* So for `{ a } | { b }`, this will include both `a` and `b`.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface A { kind: "a"; a: number; }
4+
////interface B { kind: "b"; b: number; }
5+
////const c: A | B = { kind: "a", /**/ };
6+
7+
verify.completions({ marker: "", exact: ["a"] });

0 commit comments

Comments
 (0)