diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 06c2808a393..918da100936 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -554,6 +554,7 @@ import { isImportOrExportSpecifier, isImportSpecifier, isImportTypeNode, + isInCompoundLikeAssignment, isIndexedAccessTypeNode, isInExpressionContext, isInfinityOrNaNString, @@ -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 @@ -28079,7 +28081,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) { diff --git a/src/compiler/factory/utilities.ts b/src/compiler/factory/utilities.ts index 3aa6f5e6548..221d4874d52 100644 --- a/src/compiler/factory/utilities.ts +++ b/src/compiler/factory/utilities.ts @@ -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); } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 4eb8cc9d52a..f00b10dbc7b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -152,8 +152,6 @@ import { forEachChild, forEachChildRecursively, ForInOrOfStatement, - ForInStatement, - ForOfStatement, ForStatement, FunctionBody, FunctionDeclaration, @@ -331,6 +329,7 @@ import { isQualifiedName, isRootedDiskPath, isSetAccessorDeclaration, + isShiftOperatorOrHigher, isShorthandPropertyAssignment, isSourceFile, isString, @@ -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; @@ -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: @@ -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): 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 */ @@ -4534,8 +4571,7 @@ export type NodeWithPossibleHoistedDeclaration = | DefaultClause | LabeledStatement | ForStatement - | ForInStatement - | ForOfStatement + | ForInOrOfStatement | DoStatement | WhileStatement | TryStatement diff --git a/tests/baselines/reference/literalWideningWithCompoundLikeAssignments.symbols b/tests/baselines/reference/literalWideningWithCompoundLikeAssignments.symbols new file mode 100644 index 00000000000..599027e35ff --- /dev/null +++ b/tests/baselines/reference/literalWideningWithCompoundLikeAssignments.symbols @@ -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)) + diff --git a/tests/baselines/reference/literalWideningWithCompoundLikeAssignments.types b/tests/baselines/reference/literalWideningWithCompoundLikeAssignments.types new file mode 100644 index 00000000000..b605622a24b --- /dev/null +++ b/tests/baselines/reference/literalWideningWithCompoundLikeAssignments.types @@ -0,0 +1,169 @@ +//// [tests/cases/compiler/literalWideningWithCompoundLikeAssignments.ts] //// + +=== literalWideningWithCompoundLikeAssignments.ts === +// repro from #13865 + +const empty: "" = ""; +>empty : "" +>"" : "" + +let foo = empty; +>foo : "" +>empty : "" + +foo = foo + "bar" +>foo = foo + "bar" : string +>foo : string +>foo + "bar" : string +>foo : "" +>"bar" : "bar" + +foo // string +>foo : string + +declare const numLiteral: 0; +>numLiteral : 0 + +let t1 = numLiteral; +>t1 : 0 +>numLiteral : 0 + +t1 = t1 + 42 +>t1 = t1 + 42 : number +>t1 : number +>t1 + 42 : number +>t1 : 0 +>42 : 42 + +t1 // number +>t1 : number + +let t2 = numLiteral; +>t2 : 0 +>numLiteral : 0 + +t2 = t2 - 42 +>t2 = t2 - 42 : number +>t2 : number +>t2 - 42 : number +>t2 : 0 +>42 : 42 + +t2 // number +>t2 : number + +let t3 = numLiteral; +>t3 : 0 +>numLiteral : 0 + +t3 = t3 * 42 +>t3 = t3 * 42 : number +>t3 : number +>t3 * 42 : number +>t3 : 0 +>42 : 42 + +t3 // number +>t3 : number + +let t4 = numLiteral; +>t4 : 0 +>numLiteral : 0 + +t4 = t4 ** 42 +>t4 = t4 ** 42 : number +>t4 : number +>t4 ** 42 : number +>t4 : 0 +>42 : 42 + +t4 // number +>t4 : number + +let t5 = numLiteral; +>t5 : 0 +>numLiteral : 0 + +t5 = t5 / 42 +>t5 = t5 / 42 : number +>t5 : number +>t5 / 42 : number +>t5 : 0 +>42 : 42 + +t5 // number +>t5 : number + +let t6 = numLiteral; +>t6 : 0 +>numLiteral : 0 + +t6 = t6 % 42 +>t6 = t6 % 42 : number +>t6 : number +>t6 % 42 : number +>t6 : 0 +>42 : 42 + +t6 // number +>t6 : number + +let t7 = numLiteral; +>t7 : 0 +>numLiteral : 0 + +t7 = t7 >> 0 +>t7 = t7 >> 0 : number +>t7 : number +>t7 >> 0 : number +>t7 : 0 +>0 : 0 + +t7 // number +>t7 : number + +let t8 = numLiteral; +>t8 : 0 +>numLiteral : 0 + +t8 = t8 >>> 0 +>t8 = t8 >>> 0 : number +>t8 : number +>t8 >>> 0 : number +>t8 : 0 +>0 : 0 + +t8 // number +>t8 : number + +let t9 = numLiteral; +>t9 : 0 +>numLiteral : 0 + +t9 = t9 << 0 +>t9 = t9 << 0 : number +>t9 : number +>t9 << 0 : number +>t9 : 0 +>0 : 0 + +t9 // number +>t9 : number + +declare const literalUnion: "a" | 0; +>literalUnion : 0 | "a" + +let t10 = literalUnion; +>t10 : 0 | "a" +>literalUnion : 0 | "a" + +t10 = t10 + 'b' +>t10 = t10 + 'b' : string +>t10 : string | number +>t10 + 'b' : string +>t10 : 0 | "a" +>'b' : "b" + +t10 // string +>t10 : string + diff --git a/tests/cases/compiler/literalWideningWithCompoundLikeAssignments.ts b/tests/cases/compiler/literalWideningWithCompoundLikeAssignments.ts new file mode 100644 index 00000000000..e61b19c2280 --- /dev/null +++ b/tests/cases/compiler/literalWideningWithCompoundLikeAssignments.ts @@ -0,0 +1,52 @@ +// @strict: true +// @noEmit: true + +// repro from #13865 + +const empty: "" = ""; +let foo = empty; +foo = foo + "bar" +foo // string + +declare const numLiteral: 0; + +let t1 = numLiteral; +t1 = t1 + 42 +t1 // number + +let t2 = numLiteral; +t2 = t2 - 42 +t2 // number + +let t3 = numLiteral; +t3 = t3 * 42 +t3 // number + +let t4 = numLiteral; +t4 = t4 ** 42 +t4 // number + +let t5 = numLiteral; +t5 = t5 / 42 +t5 // number + +let t6 = numLiteral; +t6 = t6 % 42 +t6 // number + +let t7 = numLiteral; +t7 = t7 >> 0 +t7 // number + +let t8 = numLiteral; +t8 = t8 >>> 0 +t8 // number + +let t9 = numLiteral; +t9 = t9 << 0 +t9 // number + +declare const literalUnion: "a" | 0; +let t10 = literalUnion; +t10 = t10 + 'b' +t10 // string