Skip to content

Commit 7a7328a

Browse files
authored
string|number inferences are low priority (microsoft#28381)
* string|number inferences are low priority Also, refactor unifyFromContext to explicitly handle priorities * string/number/strnum are not mutually exclusive * Assert that high/low can't apply to same element
1 parent 2600250 commit 7a7328a

File tree

3 files changed

+66
-17
lines changed

3 files changed

+66
-17
lines changed

src/services/codefixes/inferFromUsage.ts

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ namespace ts.codefix {
379379
interface UsageContext {
380380
isNumber?: boolean;
381381
isString?: boolean;
382-
hasNonVacuousType?: boolean;
383-
hasNonVacuousNonAnonymousType?: boolean;
382+
/** Used ambiguously, eg x + ___ or object[___]; results in string | number if no other evidence exists */
383+
isNumberOrString?: boolean;
384384

385385
candidateTypes?: Type[];
386386
properties?: UnderscoreEscapedMap<UsageContext>;
@@ -510,8 +510,7 @@ namespace ts.codefix {
510510
break;
511511

512512
case SyntaxKind.PlusToken:
513-
usageContext.isNumber = true;
514-
usageContext.isString = true;
513+
usageContext.isNumberOrString = true;
515514
break;
516515

517516
// case SyntaxKind.ExclamationToken:
@@ -582,8 +581,7 @@ namespace ts.codefix {
582581
usageContext.isString = true;
583582
}
584583
else {
585-
usageContext.isNumber = true;
586-
usageContext.isString = true;
584+
usageContext.isNumberOrString = true;
587585
}
588586
break;
589587

@@ -657,8 +655,7 @@ namespace ts.codefix {
657655

658656
function inferTypeFromPropertyElementExpressionContext(parent: ElementAccessExpression, node: Expression, checker: TypeChecker, usageContext: UsageContext): void {
659657
if (node === parent.argumentExpression) {
660-
usageContext.isNumber = true;
661-
usageContext.isString = true;
658+
usageContext.isNumberOrString = true;
662659
return;
663660
}
664661
else {
@@ -674,17 +671,50 @@ namespace ts.codefix {
674671
}
675672
}
676673

674+
interface Priority {
675+
high: (t: Type) => boolean;
676+
low: (t: Type) => boolean;
677+
}
678+
679+
function removeLowPriorityInferences(inferences: ReadonlyArray<Type>, priorities: Priority[]): Type[] {
680+
const toRemove: ((t: Type) => boolean)[] = [];
681+
for (const i of inferences) {
682+
for (const { high, low } of priorities) {
683+
if (high(i)) {
684+
Debug.assert(!low(i));
685+
toRemove.push(low);
686+
}
687+
}
688+
}
689+
return inferences.filter(i => toRemove.every(f => !f(i)));
690+
}
691+
677692
export function unifyFromContext(inferences: ReadonlyArray<Type>, checker: TypeChecker, fallback = checker.getAnyType()): Type {
678693
if (!inferences.length) return fallback;
679-
const hasNonVacuousType = inferences.some(i => !(i.flags & (TypeFlags.Any | TypeFlags.Void)));
680-
const hasNonVacuousNonAnonymousType = inferences.some(
681-
i => !(i.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(checker.getObjectFlags(i) & ObjectFlags.Anonymous));
682-
const anons = inferences.filter(i => checker.getObjectFlags(i) & ObjectFlags.Anonymous) as AnonymousType[];
683-
const good = [];
684-
if (!hasNonVacuousNonAnonymousType && anons.length) {
694+
695+
// 1. string or number individually override string | number
696+
// 2. non-any, non-void overrides any or void
697+
// 3. non-nullable, non-any, non-void, non-anonymous overrides anonymous types
698+
const stringNumber = checker.getUnionType([checker.getStringType(), checker.getNumberType()]);
699+
const priorities: Priority[] = [
700+
{
701+
high: t => t === checker.getStringType() || t === checker.getNumberType(),
702+
low: t => t === stringNumber
703+
},
704+
{
705+
high: t => !(t.flags & (TypeFlags.Any | TypeFlags.Void)),
706+
low: t => !!(t.flags & (TypeFlags.Any | TypeFlags.Void))
707+
},
708+
{
709+
high: t => !(t.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(checker.getObjectFlags(t) & ObjectFlags.Anonymous),
710+
low: t => !!(checker.getObjectFlags(t) & ObjectFlags.Anonymous)
711+
}];
712+
let good = removeLowPriorityInferences(inferences, priorities);
713+
const anons = good.filter(i => checker.getObjectFlags(i) & ObjectFlags.Anonymous) as AnonymousType[];
714+
if (anons.length) {
715+
good = good.filter(i => !(checker.getObjectFlags(i) & ObjectFlags.Anonymous));
685716
good.push(unifyAnonymousTypes(anons, checker));
686717
}
687-
good.push(...inferences.filter(i => !(checker.getObjectFlags(i) & ObjectFlags.Anonymous) && !(hasNonVacuousType && i.flags & (TypeFlags.Any | TypeFlags.Void))));
688718
return checker.getWidenedType(checker.getUnionType(good));
689719
}
690720

@@ -731,12 +761,16 @@ namespace ts.codefix {
731761

732762
function inferFromContext(usageContext: UsageContext, checker: TypeChecker) {
733763
const types = [];
764+
734765
if (usageContext.isNumber) {
735766
types.push(checker.getNumberType());
736767
}
737768
if (usageContext.isString) {
738769
types.push(checker.getStringType());
739770
}
771+
if (usageContext.isNumberOrString) {
772+
types.push(checker.getUnionType([checker.getStringType(), checker.getNumberType()]));
773+
}
740774

741775
types.push(...(usageContext.candidateTypes || []).map(t => checker.getBaseTypeOfLiteralType(t)));
742776

@@ -750,7 +784,7 @@ namespace ts.codefix {
750784
}
751785

752786
if (usageContext.numberIndexContext) {
753-
return [checker.createArrayType(recur(usageContext.numberIndexContext))];
787+
types.push(checker.createArrayType(recur(usageContext.numberIndexContext)));
754788
}
755789
else if (usageContext.properties || usageContext.callContexts || usageContext.constructContexts || usageContext.stringIndexContext) {
756790
const members = createUnderscoreEscapedMap<Symbol>();
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////function f(x, y) {
4+
//// return x + y
5+
////}
6+
////f(1, 2)
7+
verify.codeFix({
8+
description: "Infer parameter types from usage",
9+
index: 0,
10+
newFileContent:
11+
`function f(x: number, y: number) {
12+
return x + y
13+
}
14+
f(1, 2)`,
15+
});

tests/cases/fourslash/codeFixInferFromUsageUnifyAnonymousType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
////kw("6", { beforeExpr: true, prefix: true, startsExpr: true })
1717

1818

19-
verify.rangeAfterCodeFix("name: string | number, options: { startsExpr?: boolean; beforeExpr?: boolean; isLoop?: boolean; prefix?: boolean; keyword?: any; } | undefined",/*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 0);
19+
verify.rangeAfterCodeFix("name: string, options: { startsExpr?: boolean; beforeExpr?: boolean; isLoop?: boolean; prefix?: boolean; keyword?: any; } | undefined",/*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 0);

0 commit comments

Comments
 (0)