Skip to content

Commit 5515dce

Browse files
committed
Improved error messages for invalid assignments to identifiers
1 parent 8d87971 commit 5515dce

File tree

5 files changed

+94
-59
lines changed

5 files changed

+94
-59
lines changed

src/compiler/binder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,9 +1192,9 @@ namespace ts {
11921192
}
11931193
else {
11941194
forEachChild(node, bind);
1195-
if (operator === SyntaxKind.EqualsToken && !isAssignmentTarget(node)) {
1195+
if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) {
11961196
bindAssignmentTargetFlow(node.left);
1197-
if (node.left.kind === SyntaxKind.ElementAccessExpression) {
1197+
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
11981198
const elementAccess = <ElementAccessExpression>node.left;
11991199
if (isNarrowableOperand(elementAccess.expression)) {
12001200
currentFlow = createFlowArrayMutation(currentFlow, node);

src/compiler/checker.ts

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8851,7 +8851,7 @@ namespace ts {
88518851
// Assignments only narrow the computed type if the declared type is a union type. Thus, we
88528852
// only need to evaluate the assigned type if the declared type is a union type.
88538853
if (isMatchingReference(reference, node)) {
8854-
if (node.parent.kind === SyntaxKind.PrefixUnaryExpression || node.parent.kind === SyntaxKind.PostfixUnaryExpression) {
8854+
if (getAssignmentTargetKind(node) === AssignmentKind.Compound) {
88558855
const flowType = getTypeAtFlowNode(flow.antecedent);
88568856
return createFlowType(getBaseTypeOfLiteralType(getTypeFromFlowType(flowType)), isIncomplete(flowType));
88578857
}
@@ -9298,10 +9298,10 @@ namespace ts {
92989298
}
92999299
}
93009300
else {
9301-
const invokedExpression = skipParenthesizedNodes(callExpression.expression);
9301+
const invokedExpression = skipParentheses(callExpression.expression);
93029302
if (invokedExpression.kind === SyntaxKind.ElementAccessExpression || invokedExpression.kind === SyntaxKind.PropertyAccessExpression) {
93039303
const accessExpression = invokedExpression as ElementAccessExpression | PropertyAccessExpression;
9304-
const possibleReference = skipParenthesizedNodes(accessExpression.expression);
9304+
const possibleReference = skipParentheses(accessExpression.expression);
93059305
if (isMatchingReference(reference, possibleReference)) {
93069306
return getNarrowedType(type, predicate.type, assumeTrue);
93079307
}
@@ -9361,13 +9361,6 @@ namespace ts {
93619361
return getTypeOfSymbol(symbol);
93629362
}
93639363

9364-
function skipParenthesizedNodes(expression: Expression): Expression {
9365-
while (expression.kind === SyntaxKind.ParenthesizedExpression) {
9366-
expression = (expression as ParenthesizedExpression).expression;
9367-
}
9368-
return expression;
9369-
}
9370-
93719364
function getControlFlowContainer(node: Node): Node {
93729365
while (true) {
93739366
node = node.parent;
@@ -9425,6 +9418,9 @@ namespace ts {
94259418

94269419
function checkIdentifier(node: Identifier): Type {
94279420
const symbol = getResolvedSymbol(node);
9421+
if (symbol === unknownSymbol) {
9422+
return unknownType;
9423+
}
94289424

94299425
// As noted in ECMAScript 6 language spec, arrow functions never have an arguments objects.
94309426
// Although in down-level emit of arrow function, we emit it using function expression which means that
@@ -9446,6 +9442,7 @@ namespace ts {
94469442
if (node.flags & NodeFlags.AwaitContext) {
94479443
getNodeLinks(container).flags |= NodeCheckFlags.CaptureArguments;
94489444
}
9445+
return getTypeOfSymbol(symbol);
94499446
}
94509447

94519448
if (symbol.flags & SymbolFlags.Alias && !isInTypeQuery(node) && !isConstEnumOrConstEnumOnlyModule(resolveAlias(symbol))) {
@@ -9498,9 +9495,22 @@ namespace ts {
94989495

94999496
const type = getTypeOfSymbol(localOrExportSymbol);
95009497
const declaration = localOrExportSymbol.valueDeclaration;
9498+
const assignmentKind = getAssignmentTargetKind(node);
9499+
9500+
if (assignmentKind) {
9501+
if (!(localOrExportSymbol.flags & SymbolFlags.Variable)) {
9502+
error(node, Diagnostics.Cannot_assign_to_0_because_it_is_not_a_variable, symbolToString(symbol));
9503+
return unknownType;
9504+
}
9505+
if (isReadonlySymbol(localOrExportSymbol)) {
9506+
error(node, Diagnostics.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, symbolToString(symbol));
9507+
return unknownType;
9508+
}
9509+
}
9510+
95019511
// We only narrow variables and parameters occurring in a non-assignment position. For all other
95029512
// entities we simply return the declared type.
9503-
if (!(localOrExportSymbol.flags & SymbolFlags.Variable) || isAssignmentTarget(node) || !declaration) {
9513+
if (!(localOrExportSymbol.flags & SymbolFlags.Variable) || assignmentKind === AssignmentKind.Definite || !declaration) {
95049514
return type;
95059515
}
95069516
// The declaration container is the innermost function that encloses the declaration of the variable
@@ -9542,7 +9552,7 @@ namespace ts {
95429552
// Return the declared type to reduce follow-on errors
95439553
return type;
95449554
}
9545-
return flowType;
9555+
return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType;
95469556
}
95479557

95489558
function isInsideFunction(node: Node, threshold: Node): boolean {
@@ -11397,6 +11407,20 @@ namespace ts {
1139711407
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
1139811408
}
1139911409

11410+
function reportNonexistentProperty(propNode: Identifier, containingType: Type) {
11411+
let errorInfo: DiagnosticMessageChain;
11412+
if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) {
11413+
for (const subtype of (containingType as UnionType).types) {
11414+
if (!getPropertyOfType(subtype, propNode.text)) {
11415+
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype));
11416+
break;
11417+
}
11418+
}
11419+
}
11420+
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType));
11421+
diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo));
11422+
}
11423+
1140011424
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) {
1140111425
const type = checkNonNullExpression(left);
1140211426
if (isTypeAny(type) || type === silentNeverType) {
@@ -11445,20 +11469,6 @@ namespace ts {
1144511469
return propType;
1144611470
}
1144711471
return getFlowTypeOfReference(node, propType, /*assumeInitialized*/ true, /*flowContainer*/ undefined);
11448-
11449-
function reportNonexistentProperty(propNode: Identifier, containingType: Type) {
11450-
let errorInfo: DiagnosticMessageChain;
11451-
if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) {
11452-
for (const subtype of (containingType as UnionType).types) {
11453-
if (!getPropertyOfType(subtype, propNode.text)) {
11454-
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype));
11455-
break;
11456-
}
11457-
}
11458-
}
11459-
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType));
11460-
diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo));
11461-
}
1146211472
}
1146311473

1146411474
function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean {
@@ -11505,7 +11515,7 @@ namespace ts {
1150511515
* that references a for-in variable for an object with numeric property names.
1150611516
*/
1150711517
function isForInVariableForNumericPropertyNames(expr: Expression) {
11508-
const e = skipParenthesizedNodes(expr);
11518+
const e = skipParentheses(expr);
1150911519
if (e.kind === SyntaxKind.Identifier) {
1151011520
const symbol = getResolvedSymbol(<Identifier>e);
1151111521
if (symbol.flags & SymbolFlags.Variable) {
@@ -13486,7 +13496,7 @@ namespace ts {
1348613496

1348713497
function isReferenceThroughNamespaceImport(expr: Expression): boolean {
1348813498
if (expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) {
13489-
const node = skipParenthesizedNodes((expr as PropertyAccessExpression | ElementAccessExpression).expression);
13499+
const node = skipParentheses((expr as PropertyAccessExpression | ElementAccessExpression).expression);
1349013500
if (node.kind === SyntaxKind.Identifier) {
1349113501
const symbol = getNodeLinks(node).resolvedSymbol;
1349213502
if (symbol.flags & SymbolFlags.Alias) {
@@ -13500,11 +13510,14 @@ namespace ts {
1350013510

1350113511
function checkReferenceExpression(expr: Expression, invalidReferenceMessage: DiagnosticMessage, constantVariableMessage: DiagnosticMessage): boolean {
1350213512
// References are combinations of identifiers, parentheses, and property accesses.
13503-
const node = skipParenthesizedNodes(expr);
13513+
const node = skipParentheses(expr);
1350413514
if (node.kind !== SyntaxKind.Identifier && node.kind !== SyntaxKind.PropertyAccessExpression && node.kind !== SyntaxKind.ElementAccessExpression) {
1350513515
error(expr, invalidReferenceMessage);
1350613516
return false;
1350713517
}
13518+
if (node.kind === SyntaxKind.Identifier) {
13519+
return true;
13520+
}
1350813521
// Because we get the symbol from the resolvedSymbol property, it might be of kind
1350913522
// SymbolFlags.ExportValue. In this case it is necessary to get the actual export
1351013523
// symbol, which will have the correct flags set on it.
@@ -13514,10 +13527,10 @@ namespace ts {
1351413527
if (symbol !== unknownSymbol && symbol !== argumentsSymbol) {
1351513528
// Only variables (and not functions, classes, namespaces, enum objects, or enum members)
1351613529
// are considered references when referenced using a simple identifier.
13517-
if (node.kind === SyntaxKind.Identifier && !(symbol.flags & SymbolFlags.Variable)) {
13518-
error(expr, invalidReferenceMessage);
13519-
return false;
13520-
}
13530+
// if (node.kind === SyntaxKind.Identifier && !(symbol.flags & SymbolFlags.Variable)) {
13531+
// error(expr, invalidReferenceMessage);
13532+
// return false;
13533+
// }
1352113534
if (isReferenceToReadonlyEntity(node, symbol) || isReferenceThroughNamespaceImport(node)) {
1352213535
error(expr, constantVariableMessage);
1352313536
return false;
@@ -14255,7 +14268,7 @@ namespace ts {
1425514268
}
1425614269

1425714270
function isTypeAssertion(node: Expression) {
14258-
node = skipParenthesizedNodes(node);
14271+
node = skipParentheses(node);
1425914272
return node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression;
1426014273
}
1426114274

src/compiler/diagnosticMessages.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,6 +1763,14 @@
17631763
"category": "Error",
17641764
"code": 2538
17651765
},
1766+
"Cannot assign to '{0}' because it is not a variable.": {
1767+
"category": "Error",
1768+
"code": 2539
1769+
},
1770+
"Cannot assign to '{0}' because it is a constant or a read-only property.": {
1771+
"category": "Error",
1772+
"code": 2540
1773+
},
17661774
"JSX element attributes type '{0}' may not be a union type.": {
17671775
"category": "Error",
17681776
"code": 2600

src/compiler/types.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -956,10 +956,6 @@ namespace ts {
956956
operator: PostfixUnaryOperator;
957957
}
958958

959-
export interface PostfixExpression extends UnaryExpression {
960-
_postfixExpressionBrand: any;
961-
}
962-
963959
export interface LeftHandSideExpression extends IncrementExpression {
964960
_leftHandSideExpressionBrand: any;
965961
}

src/compiler/utilities.ts

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,29 +1624,47 @@ namespace ts {
16241624
return node && node.dotDotDotToken !== undefined;
16251625
}
16261626

1627+
export const enum AssignmentKind {
1628+
None, Definite, Compound
1629+
}
1630+
1631+
export function getAssignmentTargetKind(node: Node): AssignmentKind {
1632+
let parent = node.parent;
1633+
while (true) {
1634+
switch (parent.kind) {
1635+
case SyntaxKind.BinaryExpression:
1636+
const binaryOperator = (<BinaryExpression>parent).operatorToken.kind;
1637+
return isAssignmentOperator(binaryOperator) && (<BinaryExpression>parent).left === node ?
1638+
binaryOperator === SyntaxKind.EqualsToken ? AssignmentKind.Definite : AssignmentKind.Compound :
1639+
AssignmentKind.None;
1640+
case SyntaxKind.PrefixUnaryExpression:
1641+
case SyntaxKind.PostfixUnaryExpression:
1642+
const unaryOperator = (<PrefixUnaryExpression | PostfixUnaryExpression>parent).operator;
1643+
return unaryOperator === SyntaxKind.PlusPlusToken || unaryOperator === SyntaxKind.MinusMinusToken ? AssignmentKind.Compound : AssignmentKind.None;
1644+
case SyntaxKind.ForInStatement:
1645+
case SyntaxKind.ForOfStatement:
1646+
return (<ForInStatement | ForOfStatement>parent).initializer === node ? AssignmentKind.Definite : AssignmentKind.None;
1647+
case SyntaxKind.ParenthesizedExpression:
1648+
case SyntaxKind.ArrayLiteralExpression:
1649+
case SyntaxKind.SpreadElementExpression:
1650+
node = parent;
1651+
break;
1652+
case SyntaxKind.PropertyAssignment:
1653+
case SyntaxKind.ShorthandPropertyAssignment:
1654+
node = parent.parent;
1655+
break;
1656+
default:
1657+
return AssignmentKind.None;
1658+
}
1659+
parent = node.parent;
1660+
}
1661+
}
1662+
16271663
// A node is an assignment target if it is on the left hand side of an '=' token, if it is parented by a property
16281664
// assignment in an object literal that is an assignment target, or if it is parented by an array literal that is
16291665
// an assignment target. Examples include 'a = xxx', '{ p: a } = xxx', '[{ p: a}] = xxx'.
16301666
export function isAssignmentTarget(node: Node): boolean {
1631-
while (node.parent.kind === SyntaxKind.ParenthesizedExpression) {
1632-
node = node.parent;
1633-
}
1634-
while (true) {
1635-
const parent = node.parent;
1636-
if (parent.kind === SyntaxKind.ArrayLiteralExpression || parent.kind === SyntaxKind.SpreadElementExpression) {
1637-
node = parent;
1638-
continue;
1639-
}
1640-
if (parent.kind === SyntaxKind.PropertyAssignment || parent.kind === SyntaxKind.ShorthandPropertyAssignment) {
1641-
node = parent.parent;
1642-
continue;
1643-
}
1644-
return parent.kind === SyntaxKind.BinaryExpression &&
1645-
isAssignmentOperator((<BinaryExpression>parent).operatorToken.kind) &&
1646-
(<BinaryExpression>parent).left === node ||
1647-
(parent.kind === SyntaxKind.ForInStatement || parent.kind === SyntaxKind.ForOfStatement) &&
1648-
(<ForInStatement | ForOfStatement>parent).initializer === node;
1649-
}
1667+
return getAssignmentTargetKind(node) !== AssignmentKind.None;
16501668
}
16511669

16521670
export function isNodeDescendantOf(node: Node, ancestor: Node): boolean {

0 commit comments

Comments
 (0)