Skip to content

Commit

Permalink
Narrow on element access of literal (#26424)
Browse files Browse the repository at this point in the history
* Narrow literal element accesses

This means that, for example, the tuple `[number, string?]` allows its
second element to be narrowed with element access:

```ts
export function f(pair: [number, string?]): string {
  return pair[1] ? pair[1] : 'nope';
}
```

* Update baselines

* Cleanup

* More cleanup

* Test dashes in property names

* More cleanup

* Delete undead code
  • Loading branch information
sandersn committed Aug 15, 2018
1 parent b9bd0d9 commit 2bfd919
Show file tree
Hide file tree
Showing 10 changed files with 861 additions and 55 deletions.
11 changes: 7 additions & 4 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ namespace ts {
case SyntaxKind.Identifier:
case SyntaxKind.ThisKeyword:
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
return isNarrowableReference(expr);
case SyntaxKind.CallExpression:
return hasNarrowableArgument(<CallExpression>expr);
Expand All @@ -737,10 +738,11 @@ namespace ts {
}

function isNarrowableReference(expr: Expression): boolean {
return expr.kind === SyntaxKind.Identifier ||
expr.kind === SyntaxKind.ThisKeyword ||
expr.kind === SyntaxKind.SuperKeyword ||
expr.kind === SyntaxKind.PropertyAccessExpression && isNarrowableReference((<PropertyAccessExpression>expr).expression);
return expr.kind === SyntaxKind.Identifier || expr.kind === SyntaxKind.ThisKeyword || expr.kind === SyntaxKind.SuperKeyword ||
isPropertyAccessExpression(expr) && isNarrowableReference(expr.expression) ||
isElementAccessExpression(expr) && expr.argumentExpression &&
(isStringLiteral(expr.argumentExpression) || isNumericLiteral(expr.argumentExpression)) &&
isNarrowableReference(expr.expression);
}

function hasNarrowableArgument(expr: CallExpression) {
Expand Down Expand Up @@ -2066,6 +2068,7 @@ namespace ts {
}
return checkStrictModeIdentifier(<Identifier>node);
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
if (currentFlow && isNarrowableReference(<Expression>node)) {
node.flowNode = currentFlow;
}
Expand Down
54 changes: 39 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9158,7 +9158,8 @@ namespace ts {
getNodeLinks(accessNode!).resolvedSymbol = prop;
}
}
return getTypeOfSymbol(prop);
const propType = getTypeOfSymbol(prop);
return accessExpression ? getFlowTypeOfReference(accessExpression, propType) : propType;
}
if (isTupleType(objectType)) {
const restType = getRestTypeOfTupleType(objectType);
Expand Down Expand Up @@ -13778,9 +13779,10 @@ namespace ts {
case SyntaxKind.SuperKeyword:
return target.kind === SyntaxKind.SuperKeyword;
case SyntaxKind.PropertyAccessExpression:
return target.kind === SyntaxKind.PropertyAccessExpression &&
(<PropertyAccessExpression>source).name.escapedText === (<PropertyAccessExpression>target).name.escapedText &&
isMatchingReference((<PropertyAccessExpression>source).expression, (<PropertyAccessExpression>target).expression);
case SyntaxKind.ElementAccessExpression:
return (isPropertyAccessExpression(target) || isElementAccessExpression(target)) &&
getAccessedPropertyName(source as PropertyAccessExpression | ElementAccessExpression) === getAccessedPropertyName(target) &&
isMatchingReference((source as PropertyAccessExpression | ElementAccessExpression).expression, target.expression);
case SyntaxKind.BindingElement:
if (target.kind !== SyntaxKind.PropertyAccessExpression) return false;
const t = target as PropertyAccessExpression;
Expand All @@ -13796,6 +13798,12 @@ namespace ts {
return false;
}

function getAccessedPropertyName(access: PropertyAccessExpression | ElementAccessExpression): __String | undefined {
return isPropertyAccessExpression(access) ? access.name.escapedText :
isStringLiteral(access.argumentExpression) || isNumericLiteral(access.argumentExpression) ? escapeLeadingUnderscores(access.argumentExpression.text) :
undefined;
}

function containsMatchingReference(source: Node, target: Node) {
while (source.kind === SyntaxKind.PropertyAccessExpression) {
source = (<PropertyAccessExpression>source).expression;
Expand Down Expand Up @@ -14438,7 +14446,10 @@ namespace ts {
else if (flags & FlowFlags.Start) {
// Check if we should continue with the control flow of the containing function.
const container = (<FlowStart>flow).container;
if (container && container !== flowContainer && reference.kind !== SyntaxKind.PropertyAccessExpression && reference.kind !== SyntaxKind.ThisKeyword) {
if (container && container !== flowContainer &&
reference.kind !== SyntaxKind.PropertyAccessExpression &&
reference.kind !== SyntaxKind.ElementAccessExpression &&
reference.kind !== SyntaxKind.ThisKeyword) {
flow = container.flowNode!;
continue;
}
Expand Down Expand Up @@ -14555,7 +14566,10 @@ namespace ts {
type = narrowTypeBySwitchOnDiscriminant(type, flow.switchStatement, flow.clauseStart, flow.clauseEnd);
}
else if (isMatchingReferenceDiscriminant(expr, type)) {
type = narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
type = narrowTypeByDiscriminant(
type,
expr as PropertyAccessExpression | ElementAccessExpression,
t => narrowTypeBySwitchOnDiscriminant(t, flow.switchStatement, flow.clauseStart, flow.clauseEnd));
}
return createFlowType(type, isIncomplete(flowType));
}
Expand Down Expand Up @@ -14671,14 +14685,23 @@ namespace ts {
}

function isMatchingReferenceDiscriminant(expr: Expression, computedType: Type) {
return expr.kind === SyntaxKind.PropertyAccessExpression &&
computedType.flags & TypeFlags.Union &&
isMatchingReference(reference, (<PropertyAccessExpression>expr).expression) &&
isDiscriminantProperty(computedType, (<PropertyAccessExpression>expr).name.escapedText);
if (!(computedType.flags & TypeFlags.Union) ||
expr.kind !== SyntaxKind.PropertyAccessExpression && expr.kind !== SyntaxKind.ElementAccessExpression) {
return false;
}
const access = expr as PropertyAccessExpression | ElementAccessExpression;
const name = getAccessedPropertyName(access);
if (!name) {
return false;
}
return isMatchingReference(reference, access.expression) && isDiscriminantProperty(computedType, name);
}

function narrowTypeByDiscriminant(type: Type, propAccess: PropertyAccessExpression, narrowType: (t: Type) => Type): Type {
const propName = propAccess.name.escapedText;
function narrowTypeByDiscriminant(type: Type, access: PropertyAccessExpression | ElementAccessExpression, narrowType: (t: Type) => Type): Type {
const propName = getAccessedPropertyName(access);
if (!propName) {
return type;
}
const propType = getTypeOfPropertyOfType(type, propName);
const narrowedPropType = propType && narrowType(propType);
return propType === narrowedPropType ? type : filterType(type, t => isTypeComparableTo(getTypeOfPropertyOfType(t, propName)!, narrowedPropType!));
Expand All @@ -14689,7 +14712,7 @@ namespace ts {
return getTypeWithFacts(type, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy);
}
if (isMatchingReferenceDiscriminant(expr, declaredType)) {
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy));
return narrowTypeByDiscriminant(type, <PropertyAccessExpression | ElementAccessExpression>expr, t => getTypeWithFacts(t, assumeTrue ? TypeFacts.Truthy : TypeFacts.Falsy));
}
if (containsMatchingReferenceDiscriminant(reference, expr)) {
return declaredType;
Expand Down Expand Up @@ -14740,10 +14763,10 @@ namespace ts {
return narrowTypeByEquality(type, operator, left, assumeTrue);
}
if (isMatchingReferenceDiscriminant(left, declaredType)) {
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>left, t => narrowTypeByEquality(t, operator, right, assumeTrue));
return narrowTypeByDiscriminant(type, <PropertyAccessExpression | ElementAccessExpression>left, t => narrowTypeByEquality(t, operator, right, assumeTrue));
}
if (isMatchingReferenceDiscriminant(right, declaredType)) {
return narrowTypeByDiscriminant(type, <PropertyAccessExpression>right, t => narrowTypeByEquality(t, operator, left, assumeTrue));
return narrowTypeByDiscriminant(type, <PropertyAccessExpression | ElementAccessExpression>right, t => narrowTypeByEquality(t, operator, left, assumeTrue));
}
if (containsMatchingReferenceDiscriminant(reference, left) || containsMatchingReferenceDiscriminant(reference, right)) {
return declaredType;
Expand Down Expand Up @@ -14982,6 +15005,7 @@ namespace ts {
case SyntaxKind.ThisKeyword:
case SyntaxKind.SuperKeyword:
case SyntaxKind.PropertyAccessExpression:
case SyntaxKind.ElementAccessExpression:
return narrowTypeByTruthiness(type, expr, assumeTrue);
case SyntaxKind.CallExpression:
return narrowTypeByTypePredicate(type, <CallExpression>expr, assumeTrue);
Expand Down
34 changes: 17 additions & 17 deletions tests/baselines/reference/constDeclarations-access3.types
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ M["x"] = 0;
var a = M.x + 1;
>a : number
>M.x + 1 : number
>M.x : number
>M.x : 0
>M : typeof M
>x : number
>x : 0
>1 : 1

function f(v: number) { }
Expand All @@ -147,43 +147,43 @@ function f(v: number) { }
f(M.x);
>f(M.x) : void
>f : (v: number) => void
>M.x : number
>M.x : 0
>M : typeof M
>x : number
>x : 0

if (M.x) { }
>M.x : number
>M.x : 0
>M : typeof M
>x : number
>x : 0

M.x;
>M.x : number
>M.x : 0
>M : typeof M
>x : number
>x : 0

(M.x);
>(M.x) : number
>M.x : number
>(M.x) : 0
>M.x : 0
>M : typeof M
>x : number
>x : 0

-M.x;
>-M.x : number
>M.x : number
>M.x : 0
>M : typeof M
>x : number
>x : 0

+M.x;
>+M.x : number
>M.x : number
>M.x : 0
>M : typeof M
>x : number
>x : 0

M.x.toString();
>M.x.toString() : string
>M.x.toString : (radix?: number) => string
>M.x : number
>M.x : 0
>M : typeof M
>x : number
>x : 0
>toString : (radix?: number) => string

34 changes: 17 additions & 17 deletions tests/baselines/reference/constDeclarations-access5.types
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ m["x"] = 0;
var a = m.x + 1;
>a : number
>m.x + 1 : number
>m.x : number
>m.x : 0
>m : typeof m
>x : number
>x : 0
>1 : 1

function f(v: number) { }
Expand All @@ -146,44 +146,44 @@ function f(v: number) { }
f(m.x);
>f(m.x) : void
>f : (v: number) => void
>m.x : number
>m.x : 0
>m : typeof m
>x : number
>x : 0

if (m.x) { }
>m.x : number
>m.x : 0
>m : typeof m
>x : number
>x : 0

m.x;
>m.x : number
>m.x : 0
>m : typeof m
>x : number
>x : 0

(m.x);
>(m.x) : number
>m.x : number
>(m.x) : 0
>m.x : 0
>m : typeof m
>x : number
>x : 0

-m.x;
>-m.x : number
>m.x : number
>m.x : 0
>m : typeof m
>x : number
>x : 0

+m.x;
>+m.x : number
>m.x : number
>m.x : 0
>m : typeof m
>x : number
>x : 0

m.x.toString();
>m.x.toString() : string
>m.x.toString : (radix?: number) => string
>m.x : number
>m.x : 0
>m : typeof m
>x : number
>x : 0
>toString : (radix?: number) => string

=== tests/cases/compiler/constDeclarations_access_1.ts ===
Expand Down

0 comments on commit 2bfd919

Please sign in to comment.