Skip to content

Widen widening literal types through compound-like assignments #52493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 15, 2023
10 changes: 6 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ import {
isImportOrExportSpecifier,
isImportSpecifier,
isImportTypeNode,
isInCompoundLikeAssignment,
isIndexedAccessTypeNode,
isInExpressionContext,
isInfinityOrNaNString,
Expand Down Expand Up @@ -26729,10 +26730,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const assignedType = getWidenedLiteralType(getInitialOrAssignedType(flow));
return isTypeAssignableTo(assignedType, declaredType) ? assignedType : anyArrayType;
}
if (declaredType.flags & TypeFlags.Union) {
return getAssignmentReducedType(declaredType as UnionType, getInitialOrAssignedType(flow));
const t = isInCompoundLikeAssignment(node) ? getBaseTypeOfLiteralType(declaredType) : declaredType;
if (t.flags & TypeFlags.Union) {
return getAssignmentReducedType(t as UnionType, getInitialOrAssignedType(flow));
}
return declaredType;
return t;
}
// We didn't have a direct match. However, if the reference is a dotted name, this
// may be an assignment to a left hand part of the reference. For example, for a
Expand Down Expand Up @@ -28075,7 +28077,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// entities we simply return the declared type.
if (localOrExportSymbol.flags & SymbolFlags.Variable) {
if (assignmentKind === AssignmentKind.Definite) {
return type;
return isInCompoundLikeAssignment(node) ? getBaseTypeOfLiteralType(type) : type;
}
}
else if (isAlias) {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/factory/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1222,7 +1222,8 @@ function isShiftOperator(kind: SyntaxKind): kind is ShiftOperator {
|| kind === SyntaxKind.GreaterThanGreaterThanGreaterThanToken;
}

function isShiftOperatorOrHigher(kind: SyntaxKind): kind is ShiftOperatorOrHigher {
/** @internal */
export function isShiftOperatorOrHigher(kind: SyntaxKind): kind is ShiftOperatorOrHigher {
return isShiftOperator(kind)
|| isAdditiveOperatorOrHigher(kind);
}
Expand Down
78 changes: 57 additions & 21 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ import {
forEachChild,
forEachChildRecursively,
ForInOrOfStatement,
ForInStatement,
ForOfStatement,
ForStatement,
FunctionBody,
FunctionDeclaration,
Expand Down Expand Up @@ -331,6 +329,7 @@ import {
isQualifiedName,
isRootedDiskPath,
isSetAccessorDeclaration,
isShiftOperatorOrHigher,
isShorthandPropertyAssignment,
isSourceFile,
isString,
Expand Down Expand Up @@ -3418,9 +3417,9 @@ export function isInExpressionContext(node: Node): boolean {
forStatement.incrementor === node;
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
const forInStatement = parent as ForInStatement | ForOfStatement;
return (forInStatement.initializer === node && forInStatement.initializer.kind !== SyntaxKind.VariableDeclarationList) ||
forInStatement.expression === node;
const forInOrOfStatement = parent as ForInOrOfStatement;
return (forInOrOfStatement.initializer === node && forInOrOfStatement.initializer.kind !== SyntaxKind.VariableDeclarationList) ||
forInOrOfStatement.expression === node;
case SyntaxKind.TypeAssertionExpression:
case SyntaxKind.AsExpression:
return node === (parent as AssertionExpression).expression;
Expand Down Expand Up @@ -4468,23 +4467,29 @@ export const enum AssignmentKind {
None, Definite, Compound
}

/** @internal */
export function getAssignmentTargetKind(node: Node): AssignmentKind {
type AssignmentTarget =
| BinaryExpression
| PrefixUnaryExpression
| PostfixUnaryExpression
| ForInOrOfStatement;

function getAssignmentTarget(node: Node): AssignmentTarget | undefined {
let parent = node.parent;
while (true) {
switch (parent.kind) {
case SyntaxKind.BinaryExpression:
const binaryOperator = (parent as BinaryExpression).operatorToken.kind;
return isAssignmentOperator(binaryOperator) && (parent as BinaryExpression).left === node ?
binaryOperator === SyntaxKind.EqualsToken || isLogicalOrCoalescingAssignmentOperator(binaryOperator) ? AssignmentKind.Definite : AssignmentKind.Compound :
AssignmentKind.None;
const binaryExpression = parent as BinaryExpression;
const binaryOperator = binaryExpression.operatorToken.kind;
return isAssignmentOperator(binaryOperator) && binaryExpression.left === node ? binaryExpression : undefined;
case SyntaxKind.PrefixUnaryExpression:
case SyntaxKind.PostfixUnaryExpression:
const unaryOperator = (parent as PrefixUnaryExpression | PostfixUnaryExpression).operator;
return unaryOperator === SyntaxKind.PlusPlusToken || unaryOperator === SyntaxKind.MinusMinusToken ? AssignmentKind.Compound : AssignmentKind.None;
const unaryExpression = (parent as PrefixUnaryExpression | PostfixUnaryExpression);
const unaryOperator = unaryExpression.operator;
return unaryOperator === SyntaxKind.PlusPlusToken || unaryOperator === SyntaxKind.MinusMinusToken ? unaryExpression : undefined;
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
return (parent as ForInOrOfStatement).initializer === node ? AssignmentKind.Definite : AssignmentKind.None;
const forInOrOfStatement = parent as ForInOrOfStatement;
return forInOrOfStatement.initializer === node ? forInOrOfStatement : undefined;
case SyntaxKind.ParenthesizedExpression:
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.SpreadElement:
Expand All @@ -4496,30 +4501,62 @@ export function getAssignmentTargetKind(node: Node): AssignmentKind {
break;
case SyntaxKind.ShorthandPropertyAssignment:
if ((parent as ShorthandPropertyAssignment).name !== node) {
return AssignmentKind.None;
return undefined;
}
node = parent.parent;
break;
case SyntaxKind.PropertyAssignment:
if ((parent as ShorthandPropertyAssignment).name === node) {
return AssignmentKind.None;
if ((parent as PropertyAssignment).name === node) {
return undefined;
}
node = parent.parent;
break;
default:
return AssignmentKind.None;
return undefined;
}
parent = node.parent;
}
}

/** @internal */
export function getAssignmentTargetKind(node: Node): AssignmentKind {
const target = getAssignmentTarget(node);
if (!target) {
return AssignmentKind.None;
}
switch (target.kind) {
case SyntaxKind.BinaryExpression:
const binaryOperator = target.operatorToken.kind;
return binaryOperator === SyntaxKind.EqualsToken || isLogicalOrCoalescingAssignmentOperator(binaryOperator) ?
AssignmentKind.Definite :
AssignmentKind.Compound;
case SyntaxKind.PrefixUnaryExpression:
case SyntaxKind.PostfixUnaryExpression:
return AssignmentKind.Compound;
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
return AssignmentKind.Definite;
}
}

// A node is an assignment target if it is on the left hand side of an '=' token, if it is parented by a property
// assignment in an object literal that is an assignment target, or if it is parented by an array literal that is
// an assignment target. Examples include 'a = xxx', '{ p: a } = xxx', '[{ a }] = xxx'.
// (Note that `p` is not a target in the above examples, only `a`.)
/** @internal */
export function isAssignmentTarget(node: Node): boolean {
return getAssignmentTargetKind(node) !== AssignmentKind.None;
return !!getAssignmentTarget(node);
}

function isCompoundLikeAssignment(assignment: AssignmentExpression<EqualsToken>): boolean {
const right = skipParentheses(assignment.right);
return right.kind === SyntaxKind.BinaryExpression && isShiftOperatorOrHigher((right as BinaryExpression).operatorToken.kind);
}

/** @internal */
export function isInCompoundLikeAssignment(node: Node): boolean {
const target = getAssignmentTarget(node);
return !!target && isAssignmentExpression(target, /*excludeCompoundAssignment*/ true) && isCompoundLikeAssignment(target);
}

/** @internal */
Expand All @@ -4534,8 +4571,7 @@ export type NodeWithPossibleHoistedDeclaration =
| DefaultClause
| LabeledStatement
| ForStatement
| ForInStatement
| ForOfStatement
| ForInOrOfStatement
| DoStatement
| WhileStatement
| TryStatement
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
//// [tests/cases/compiler/literalWideningWithCompoundLikeAssignments.ts] ////

=== literalWideningWithCompoundLikeAssignments.ts ===
// repro from #13865

const empty: "" = "";
>empty : Symbol(empty, Decl(literalWideningWithCompoundLikeAssignments.ts, 2, 5))

let foo = empty;
>foo : Symbol(foo, Decl(literalWideningWithCompoundLikeAssignments.ts, 3, 3))
>empty : Symbol(empty, Decl(literalWideningWithCompoundLikeAssignments.ts, 2, 5))

foo = foo + "bar"
>foo : Symbol(foo, Decl(literalWideningWithCompoundLikeAssignments.ts, 3, 3))
>foo : Symbol(foo, Decl(literalWideningWithCompoundLikeAssignments.ts, 3, 3))

foo // string
>foo : Symbol(foo, Decl(literalWideningWithCompoundLikeAssignments.ts, 3, 3))

declare const numLiteral: 0;
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

let t1 = numLiteral;
>t1 : Symbol(t1, Decl(literalWideningWithCompoundLikeAssignments.ts, 9, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t1 = t1 + 42
>t1 : Symbol(t1, Decl(literalWideningWithCompoundLikeAssignments.ts, 9, 3))
>t1 : Symbol(t1, Decl(literalWideningWithCompoundLikeAssignments.ts, 9, 3))

t1 // number
>t1 : Symbol(t1, Decl(literalWideningWithCompoundLikeAssignments.ts, 9, 3))

let t2 = numLiteral;
>t2 : Symbol(t2, Decl(literalWideningWithCompoundLikeAssignments.ts, 13, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t2 = t2 - 42
>t2 : Symbol(t2, Decl(literalWideningWithCompoundLikeAssignments.ts, 13, 3))
>t2 : Symbol(t2, Decl(literalWideningWithCompoundLikeAssignments.ts, 13, 3))

t2 // number
>t2 : Symbol(t2, Decl(literalWideningWithCompoundLikeAssignments.ts, 13, 3))

let t3 = numLiteral;
>t3 : Symbol(t3, Decl(literalWideningWithCompoundLikeAssignments.ts, 17, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t3 = t3 * 42
>t3 : Symbol(t3, Decl(literalWideningWithCompoundLikeAssignments.ts, 17, 3))
>t3 : Symbol(t3, Decl(literalWideningWithCompoundLikeAssignments.ts, 17, 3))

t3 // number
>t3 : Symbol(t3, Decl(literalWideningWithCompoundLikeAssignments.ts, 17, 3))

let t4 = numLiteral;
>t4 : Symbol(t4, Decl(literalWideningWithCompoundLikeAssignments.ts, 21, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t4 = t4 ** 42
>t4 : Symbol(t4, Decl(literalWideningWithCompoundLikeAssignments.ts, 21, 3))
>t4 : Symbol(t4, Decl(literalWideningWithCompoundLikeAssignments.ts, 21, 3))

t4 // number
>t4 : Symbol(t4, Decl(literalWideningWithCompoundLikeAssignments.ts, 21, 3))

let t5 = numLiteral;
>t5 : Symbol(t5, Decl(literalWideningWithCompoundLikeAssignments.ts, 25, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t5 = t5 / 42
>t5 : Symbol(t5, Decl(literalWideningWithCompoundLikeAssignments.ts, 25, 3))
>t5 : Symbol(t5, Decl(literalWideningWithCompoundLikeAssignments.ts, 25, 3))

t5 // number
>t5 : Symbol(t5, Decl(literalWideningWithCompoundLikeAssignments.ts, 25, 3))

let t6 = numLiteral;
>t6 : Symbol(t6, Decl(literalWideningWithCompoundLikeAssignments.ts, 29, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t6 = t6 % 42
>t6 : Symbol(t6, Decl(literalWideningWithCompoundLikeAssignments.ts, 29, 3))
>t6 : Symbol(t6, Decl(literalWideningWithCompoundLikeAssignments.ts, 29, 3))

t6 // number
>t6 : Symbol(t6, Decl(literalWideningWithCompoundLikeAssignments.ts, 29, 3))

let t7 = numLiteral;
>t7 : Symbol(t7, Decl(literalWideningWithCompoundLikeAssignments.ts, 33, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t7 = t7 >> 0
>t7 : Symbol(t7, Decl(literalWideningWithCompoundLikeAssignments.ts, 33, 3))
>t7 : Symbol(t7, Decl(literalWideningWithCompoundLikeAssignments.ts, 33, 3))

t7 // number
>t7 : Symbol(t7, Decl(literalWideningWithCompoundLikeAssignments.ts, 33, 3))

let t8 = numLiteral;
>t8 : Symbol(t8, Decl(literalWideningWithCompoundLikeAssignments.ts, 37, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t8 = t8 >>> 0
>t8 : Symbol(t8, Decl(literalWideningWithCompoundLikeAssignments.ts, 37, 3))
>t8 : Symbol(t8, Decl(literalWideningWithCompoundLikeAssignments.ts, 37, 3))

t8 // number
>t8 : Symbol(t8, Decl(literalWideningWithCompoundLikeAssignments.ts, 37, 3))

let t9 = numLiteral;
>t9 : Symbol(t9, Decl(literalWideningWithCompoundLikeAssignments.ts, 41, 3))
>numLiteral : Symbol(numLiteral, Decl(literalWideningWithCompoundLikeAssignments.ts, 7, 13))

t9 = t9 << 0
>t9 : Symbol(t9, Decl(literalWideningWithCompoundLikeAssignments.ts, 41, 3))
>t9 : Symbol(t9, Decl(literalWideningWithCompoundLikeAssignments.ts, 41, 3))

t9 // number
>t9 : Symbol(t9, Decl(literalWideningWithCompoundLikeAssignments.ts, 41, 3))

declare const literalUnion: "a" | 0;
>literalUnion : Symbol(literalUnion, Decl(literalWideningWithCompoundLikeAssignments.ts, 45, 13))

let t10 = literalUnion;
>t10 : Symbol(t10, Decl(literalWideningWithCompoundLikeAssignments.ts, 46, 3))
>literalUnion : Symbol(literalUnion, Decl(literalWideningWithCompoundLikeAssignments.ts, 45, 13))

t10 = t10 + 'b'
>t10 : Symbol(t10, Decl(literalWideningWithCompoundLikeAssignments.ts, 46, 3))
>t10 : Symbol(t10, Decl(literalWideningWithCompoundLikeAssignments.ts, 46, 3))

t10 // string
>t10 : Symbol(t10, Decl(literalWideningWithCompoundLikeAssignments.ts, 46, 3))

Loading