diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 7df0414b90b..eee26f894dc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -301,6 +301,7 @@ namespace ts { getESSymbolType: () => esSymbolType, getNeverType: () => neverType, isSymbolAccessible, + getObjectFlags, isArrayLikeType, isTypeInvalidDueToUnionDiscriminant, getAllPossiblePropertiesOfTypes, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 1e592ad5886..a39d098443c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3173,6 +3173,8 @@ namespace ts { /* @internal */ getTypeCount(): number; /* @internal */ isArrayLikeType(type: Type): boolean; + /* @internal */ getObjectFlags(type: Type): ObjectFlags; + /** * True if `contextualType` should not be considered for completions because * e.g. it specifies `kind: "a"` and obj has `kind: "b"`. diff --git a/src/services/codefixes/inferFromUsage.ts b/src/services/codefixes/inferFromUsage.ts index 12a0aeff730..d8e0bd08ebc 100644 --- a/src/services/codefixes/inferFromUsage.ts +++ b/src/services/codefixes/inferFromUsage.ts @@ -369,7 +369,9 @@ namespace ts.codefix { interface UsageContext { isNumber?: boolean; isString?: boolean; - isNumberOrString?: boolean; + hasNonVacuousType?: boolean; + hasNonVacuousNonAnonymousType?: boolean; + candidateTypes?: Type[]; properties?: UnderscoreEscapedMap; callContexts?: CallContext[]; @@ -384,7 +386,7 @@ namespace ts.codefix { cancellationToken.throwIfCancellationRequested(); inferTypeFromContext(reference, checker, usageContext); } - return getTypeFromUsageContext(usageContext, checker) || checker.getAnyType(); + return unifyFromContext(inferFromContext(usageContext, checker), checker); } export function inferTypeForParametersFromReferences(references: ReadonlyArray, declaration: FunctionLikeDeclaration, program: Program, cancellationToken: CancellationToken): ParameterInference[] | undefined { @@ -411,6 +413,7 @@ namespace ts.codefix { for (const callContext of callContexts) { if (callContext.argumentTypes.length <= parameterIndex) { isOptional = isInJSFile(declaration); + types.push(checker.getUndefinedType()); continue; } @@ -423,14 +426,10 @@ namespace ts.codefix { types.push(checker.getBaseTypeOfLiteralType(callContext.argumentTypes[parameterIndex])); } } - - let type = types.length && checker.getWidenedType(checker.getUnionType(types, UnionReduction.Subtype)); - if ((!type || type.flags & TypeFlags.Any) && isIdentifier(parameter.name)) { + let type = unifyFromContext(types, checker); + if (type.flags & TypeFlags.Any && isIdentifier(parameter.name)) { type = inferTypeForVariableFromUsage(parameter.name, program, cancellationToken); } - if (!type) { - type = checker.getAnyType(); - } return { type: isRest ? checker.createArrayType(type) : type, isOptional: isOptional && !isRest, @@ -504,7 +503,8 @@ namespace ts.codefix { break; case SyntaxKind.PlusToken: - usageContext.isNumberOrString = true; + usageContext.isNumber = true; + usageContext.isString = true; break; // case SyntaxKind.ExclamationToken: @@ -575,7 +575,8 @@ namespace ts.codefix { usageContext.isString = true; } else { - usageContext.isNumberOrString = true; + usageContext.isNumber = true; + usageContext.isString = true; } break; @@ -649,7 +650,8 @@ namespace ts.codefix { function inferTypeFromPropertyElementExpressionContext(parent: ElementAccessExpression, node: Expression, checker: TypeChecker, usageContext: UsageContext): void { if (node === parent.argumentExpression) { - usageContext.isNumberOrString = true; + usageContext.isNumber = true; + usageContext.isString = true; return; } else { @@ -665,29 +667,83 @@ namespace ts.codefix { } } - function getTypeFromUsageContext(usageContext: UsageContext, checker: TypeChecker): Type | undefined { - if (usageContext.isNumberOrString && !usageContext.isNumber && !usageContext.isString) { - return checker.getUnionType([checker.getNumberType(), checker.getStringType()]); + function unifyFromContext(inferences: ReadonlyArray, checker: TypeChecker, fallback = checker.getAnyType()): Type { + if (!inferences.length) return fallback; + const hasNonVacuousType = inferences.some(i => !(i.flags & (TypeFlags.Any | TypeFlags.Void))); + const hasNonVacuousNonAnonymousType = inferences.some( + i => !(i.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(checker.getObjectFlags(i) & ObjectFlags.Anonymous)); + const anons = inferences.filter(i => checker.getObjectFlags(i) & ObjectFlags.Anonymous) as AnonymousType[]; + const good = []; + if (!hasNonVacuousNonAnonymousType && anons.length) { + good.push(unifyAnonymousTypes(anons, checker)); } - else if (usageContext.isNumber) { - return checker.getNumberType(); + good.push(...inferences.filter(i => !(checker.getObjectFlags(i) & ObjectFlags.Anonymous) && !(hasNonVacuousType && i.flags & (TypeFlags.Any | TypeFlags.Void)))); + return checker.getWidenedType(checker.getUnionType(good)); + } + + function unifyAnonymousTypes(anons: AnonymousType[], checker: TypeChecker) { + if (anons.length === 1) { + return anons[0]; } - else if (usageContext.isString) { - return checker.getStringType(); + const calls = []; + const constructs = []; + const stringIndices = []; + const numberIndices = []; + let stringIndexReadonly = false; + let numberIndexReadonly = false; + const props = createMultiMap(); + for (const anon of anons) { + for (const p of checker.getPropertiesOfType(anon)) { + props.add(p.name, checker.getTypeOfSymbolAtLocation(p, p.valueDeclaration)); + } + calls.push(...checker.getSignaturesOfType(anon, SignatureKind.Call)); + constructs.push(...checker.getSignaturesOfType(anon, SignatureKind.Construct)); + if (anon.stringIndexInfo) { + stringIndices.push(anon.stringIndexInfo.type); + stringIndexReadonly = stringIndexReadonly || anon.stringIndexInfo.isReadonly; + } + if (anon.numberIndexInfo) { + numberIndices.push(anon.numberIndexInfo.type); + numberIndexReadonly = numberIndexReadonly || anon.numberIndexInfo.isReadonly; + } } - else if (usageContext.candidateTypes) { - return checker.getWidenedType(checker.getUnionType(usageContext.candidateTypes.map(t => checker.getBaseTypeOfLiteralType(t)), UnionReduction.Subtype)); + const members = mapEntries(props, (name, types) => { + const isOptional = types.length < anons.length ? SymbolFlags.Optional : 0; + const s = checker.createSymbol(SymbolFlags.Property | isOptional, name as __String); + s.type = checker.getUnionType(types); + return [name, s]; + }); + return checker.createAnonymousType( + anons[0].symbol, + members as UnderscoreEscapedMap, + calls, + constructs, + stringIndices.length ? checker.createIndexInfo(checker.getUnionType(stringIndices), stringIndexReadonly) : undefined, + numberIndices.length ? checker.createIndexInfo(checker.getUnionType(numberIndices), numberIndexReadonly) : undefined); + } + + function inferFromContext(usageContext: UsageContext, checker: TypeChecker) { + const types = []; + if (usageContext.isNumber) { + types.push(checker.getNumberType()); } - else if (usageContext.properties && hasCallContext(usageContext.properties.get("then" as __String))) { + if (usageContext.isString) { + types.push(checker.getStringType()); + } + + types.push(...(usageContext.candidateTypes || []).map(t => checker.getBaseTypeOfLiteralType(t))); + + if (usageContext.properties && hasCallContext(usageContext.properties.get("then" as __String))) { const paramType = getParameterTypeFromCallContexts(0, usageContext.properties.get("then" as __String)!.callContexts!, /*isRestParameter*/ false, checker)!; // TODO: GH#18217 const types = paramType.getCallSignatures().map(c => c.getReturnType()); - return checker.createPromiseType(types.length ? checker.getUnionType(types, UnionReduction.Subtype) : checker.getAnyType()); + types.push(checker.createPromiseType(types.length ? checker.getUnionType(types, UnionReduction.Subtype) : checker.getAnyType())); } else if (usageContext.properties && hasCallContext(usageContext.properties.get("push" as __String))) { - return checker.createArrayType(getParameterTypeFromCallContexts(0, usageContext.properties.get("push" as __String)!.callContexts!, /*isRestParameter*/ false, checker)!); + types.push(checker.createArrayType(getParameterTypeFromCallContexts(0, usageContext.properties.get("push" as __String)!.callContexts!, /*isRestParameter*/ false, checker)!)); } - else if (usageContext.numberIndexContext) { - return checker.createArrayType(recur(usageContext.numberIndexContext)); + + if (usageContext.numberIndexContext) { + return [checker.createArrayType(recur(usageContext.numberIndexContext))]; } else if (usageContext.properties || usageContext.callContexts || usageContext.constructContexts || usageContext.stringIndexContext) { const members = createUnderscoreEscapedMap(); @@ -719,14 +775,12 @@ namespace ts.codefix { stringIndexInfo = checker.createIndexInfo(recur(usageContext.stringIndexContext), /*isReadonly*/ false); } - return checker.createAnonymousType(/*symbol*/ undefined!, members, callSignatures, constructSignatures, stringIndexInfo, /*numberIndexInfo*/ undefined); // TODO: GH#18217 - } - else { - return undefined; + types.push(checker.createAnonymousType(/*symbol*/ undefined!, members, callSignatures, constructSignatures, stringIndexInfo, /*numberIndexInfo*/ undefined)); // TODO: GH#18217 } + return types; function recur(innerContext: UsageContext): Type { - return getTypeFromUsageContext(innerContext, checker) || checker.getAnyType(); + return unifyFromContext(inferFromContext(innerContext, checker), checker); } } @@ -759,7 +813,7 @@ namespace ts.codefix { symbol.type = checker.getWidenedType(checker.getBaseTypeOfLiteralType(callContext.argumentTypes[i])); parameters.push(symbol); } - const returnType = getTypeFromUsageContext(callContext.returnType, checker) || checker.getVoidType(); + const returnType = unifyFromContext(inferFromContext(callContext.returnType, checker), checker, checker.getVoidType()); // TODO: GH#18217 return checker.createSignature(/*declaration*/ undefined!, /*typeParameters*/ undefined, /*thisParameter*/ undefined, parameters, returnType, /*typePredicate*/ undefined, callContext.argumentTypes.length, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); } diff --git a/tests/cases/fourslash/codeFixInferFromUsageEmptyTypePriority.ts b/tests/cases/fourslash/codeFixInferFromUsageEmptyTypePriority.ts new file mode 100644 index 00000000000..e9288303781 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageEmptyTypePriority.ts @@ -0,0 +1,13 @@ +/// +// @strict: true +// based on acorn, translated to TS + +////function TokenType([|label, conf |]) { +//// if ( conf === void 0 ) conf = {}; +//// +//// var l = label; +//// var keyword = conf.keyword; +//// var beforeExpr = !!conf.beforeExpr; +////}; + +verify.rangeAfterCodeFix("label: any, conf: { keyword?: any; beforeExpr?: any; } | undefined",/*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 0); diff --git a/tests/cases/fourslash/codeFixInferFromUsageMemberJS.ts b/tests/cases/fourslash/codeFixInferFromUsageMemberJS.ts index 2a97b3dca32..f53c89b6fcc 100644 --- a/tests/cases/fourslash/codeFixInferFromUsageMemberJS.ts +++ b/tests/cases/fourslash/codeFixInferFromUsageMemberJS.ts @@ -27,10 +27,10 @@ verify.codeFixAll({ constructor() { /** * this is fine - * @type {undefined} + * @type {number[] | undefined} */ this.p = undefined; - /** @type {undefined} */ + /** @type {number[] | undefined} */ this.q = undefined } method() { diff --git a/tests/cases/fourslash/codeFixInferFromUsageUnifyAnonymousType.ts b/tests/cases/fourslash/codeFixInferFromUsageUnifyAnonymousType.ts new file mode 100644 index 00000000000..50a9fcd071e --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageUnifyAnonymousType.ts @@ -0,0 +1,19 @@ +/// +// @strict: true +// based on acorn, translated to TS + +////function kw([|name, options |]) { +//// if ( options === void 0 ) options = {}; +//// +//// options.keyword = name; +//// return keywords$1[name] = new TokenType(name, options) +////} +////kw("1") +////kw("2", { startsExpr: true }) +////kw("3", { beforeExpr: false }) +////kw("4", { isLoop: false }) +////kw("5", { beforeExpr: true, startsExpr: true }) +////kw("6", { beforeExpr: true, prefix: true, startsExpr: true }) + + +verify.rangeAfterCodeFix("name: string, options: { startsExpr?: boolean; beforeExpr?: boolean; isLoop?: boolean; prefix?: boolean; } | undefined",/*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 0);