Improved error messages for invalid assignments to identifiers

This commit is contained in:
Anders Hejlsberg
2016-10-30 16:01:15 -07:00
parent 8d87971fee
commit 5515dcea34
5 changed files with 94 additions and 59 deletions

View File

@@ -1192,9 +1192,9 @@ namespace ts {
}
else {
forEachChild(node, bind);
if (operator === SyntaxKind.EqualsToken && !isAssignmentTarget(node)) {
if (isAssignmentOperator(operator) && !isAssignmentTarget(node)) {
bindAssignmentTargetFlow(node.left);
if (node.left.kind === SyntaxKind.ElementAccessExpression) {
if (operator === SyntaxKind.EqualsToken && node.left.kind === SyntaxKind.ElementAccessExpression) {
const elementAccess = <ElementAccessExpression>node.left;
if (isNarrowableOperand(elementAccess.expression)) {
currentFlow = createFlowArrayMutation(currentFlow, node);

View File

@@ -8851,7 +8851,7 @@ namespace ts {
// Assignments only narrow the computed type if the declared type is a union type. Thus, we
// only need to evaluate the assigned type if the declared type is a union type.
if (isMatchingReference(reference, node)) {
if (node.parent.kind === SyntaxKind.PrefixUnaryExpression || node.parent.kind === SyntaxKind.PostfixUnaryExpression) {
if (getAssignmentTargetKind(node) === AssignmentKind.Compound) {
const flowType = getTypeAtFlowNode(flow.antecedent);
return createFlowType(getBaseTypeOfLiteralType(getTypeFromFlowType(flowType)), isIncomplete(flowType));
}
@@ -9298,10 +9298,10 @@ namespace ts {
}
}
else {
const invokedExpression = skipParenthesizedNodes(callExpression.expression);
const invokedExpression = skipParentheses(callExpression.expression);
if (invokedExpression.kind === SyntaxKind.ElementAccessExpression || invokedExpression.kind === SyntaxKind.PropertyAccessExpression) {
const accessExpression = invokedExpression as ElementAccessExpression | PropertyAccessExpression;
const possibleReference = skipParenthesizedNodes(accessExpression.expression);
const possibleReference = skipParentheses(accessExpression.expression);
if (isMatchingReference(reference, possibleReference)) {
return getNarrowedType(type, predicate.type, assumeTrue);
}
@@ -9361,13 +9361,6 @@ namespace ts {
return getTypeOfSymbol(symbol);
}
function skipParenthesizedNodes(expression: Expression): Expression {
while (expression.kind === SyntaxKind.ParenthesizedExpression) {
expression = (expression as ParenthesizedExpression).expression;
}
return expression;
}
function getControlFlowContainer(node: Node): Node {
while (true) {
node = node.parent;
@@ -9425,6 +9418,9 @@ namespace ts {
function checkIdentifier(node: Identifier): Type {
const symbol = getResolvedSymbol(node);
if (symbol === unknownSymbol) {
return unknownType;
}
// As noted in ECMAScript 6 language spec, arrow functions never have an arguments objects.
// Although in down-level emit of arrow function, we emit it using function expression which means that
@@ -9446,6 +9442,7 @@ namespace ts {
if (node.flags & NodeFlags.AwaitContext) {
getNodeLinks(container).flags |= NodeCheckFlags.CaptureArguments;
}
return getTypeOfSymbol(symbol);
}
if (symbol.flags & SymbolFlags.Alias && !isInTypeQuery(node) && !isConstEnumOrConstEnumOnlyModule(resolveAlias(symbol))) {
@@ -9498,9 +9495,22 @@ namespace ts {
const type = getTypeOfSymbol(localOrExportSymbol);
const declaration = localOrExportSymbol.valueDeclaration;
const assignmentKind = getAssignmentTargetKind(node);
if (assignmentKind) {
if (!(localOrExportSymbol.flags & SymbolFlags.Variable)) {
error(node, Diagnostics.Cannot_assign_to_0_because_it_is_not_a_variable, symbolToString(symbol));
return unknownType;
}
if (isReadonlySymbol(localOrExportSymbol)) {
error(node, Diagnostics.Cannot_assign_to_0_because_it_is_a_constant_or_a_read_only_property, symbolToString(symbol));
return unknownType;
}
}
// We only narrow variables and parameters occurring in a non-assignment position. For all other
// entities we simply return the declared type.
if (!(localOrExportSymbol.flags & SymbolFlags.Variable) || isAssignmentTarget(node) || !declaration) {
if (!(localOrExportSymbol.flags & SymbolFlags.Variable) || assignmentKind === AssignmentKind.Definite || !declaration) {
return type;
}
// The declaration container is the innermost function that encloses the declaration of the variable
@@ -9542,7 +9552,7 @@ namespace ts {
// Return the declared type to reduce follow-on errors
return type;
}
return flowType;
return assignmentKind ? getBaseTypeOfLiteralType(flowType) : flowType;
}
function isInsideFunction(node: Node, threshold: Node): boolean {
@@ -11397,6 +11407,20 @@ namespace ts {
return checkPropertyAccessExpressionOrQualifiedName(node, node.left, node.right);
}
function reportNonexistentProperty(propNode: Identifier, containingType: Type) {
let errorInfo: DiagnosticMessageChain;
if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) {
for (const subtype of (containingType as UnionType).types) {
if (!getPropertyOfType(subtype, propNode.text)) {
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype));
break;
}
}
}
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType));
diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo));
}
function checkPropertyAccessExpressionOrQualifiedName(node: PropertyAccessExpression | QualifiedName, left: Expression | QualifiedName, right: Identifier) {
const type = checkNonNullExpression(left);
if (isTypeAny(type) || type === silentNeverType) {
@@ -11445,20 +11469,6 @@ namespace ts {
return propType;
}
return getFlowTypeOfReference(node, propType, /*assumeInitialized*/ true, /*flowContainer*/ undefined);
function reportNonexistentProperty(propNode: Identifier, containingType: Type) {
let errorInfo: DiagnosticMessageChain;
if (containingType.flags & TypeFlags.Union && !(containingType.flags & TypeFlags.Primitive)) {
for (const subtype of (containingType as UnionType).types) {
if (!getPropertyOfType(subtype, propNode.text)) {
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(subtype));
break;
}
}
}
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Property_0_does_not_exist_on_type_1, declarationNameToString(propNode), typeToString(containingType));
diagnostics.add(createDiagnosticForNodeFromMessageChain(propNode, errorInfo));
}
}
function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean {
@@ -11505,7 +11515,7 @@ namespace ts {
* that references a for-in variable for an object with numeric property names.
*/
function isForInVariableForNumericPropertyNames(expr: Expression) {
const e = skipParenthesizedNodes(expr);
const e = skipParentheses(expr);
if (e.kind === SyntaxKind.Identifier) {
const symbol = getResolvedSymbol(<Identifier>e);
if (symbol.flags & SymbolFlags.Variable) {
@@ -13486,7 +13496,7 @@ namespace ts {
function isReferenceThroughNamespaceImport(expr: Expression): boolean {
if (expr.kind === SyntaxKind.PropertyAccessExpression || expr.kind === SyntaxKind.ElementAccessExpression) {
const node = skipParenthesizedNodes((expr as PropertyAccessExpression | ElementAccessExpression).expression);
const node = skipParentheses((expr as PropertyAccessExpression | ElementAccessExpression).expression);
if (node.kind === SyntaxKind.Identifier) {
const symbol = getNodeLinks(node).resolvedSymbol;
if (symbol.flags & SymbolFlags.Alias) {
@@ -13500,11 +13510,14 @@ namespace ts {
function checkReferenceExpression(expr: Expression, invalidReferenceMessage: DiagnosticMessage, constantVariableMessage: DiagnosticMessage): boolean {
// References are combinations of identifiers, parentheses, and property accesses.
const node = skipParenthesizedNodes(expr);
const node = skipParentheses(expr);
if (node.kind !== SyntaxKind.Identifier && node.kind !== SyntaxKind.PropertyAccessExpression && node.kind !== SyntaxKind.ElementAccessExpression) {
error(expr, invalidReferenceMessage);
return false;
}
if (node.kind === SyntaxKind.Identifier) {
return true;
}
// Because we get the symbol from the resolvedSymbol property, it might be of kind
// SymbolFlags.ExportValue. In this case it is necessary to get the actual export
// symbol, which will have the correct flags set on it.
@@ -13514,10 +13527,10 @@ namespace ts {
if (symbol !== unknownSymbol && symbol !== argumentsSymbol) {
// Only variables (and not functions, classes, namespaces, enum objects, or enum members)
// are considered references when referenced using a simple identifier.
if (node.kind === SyntaxKind.Identifier && !(symbol.flags & SymbolFlags.Variable)) {
error(expr, invalidReferenceMessage);
return false;
}
// if (node.kind === SyntaxKind.Identifier && !(symbol.flags & SymbolFlags.Variable)) {
// error(expr, invalidReferenceMessage);
// return false;
// }
if (isReferenceToReadonlyEntity(node, symbol) || isReferenceThroughNamespaceImport(node)) {
error(expr, constantVariableMessage);
return false;
@@ -14255,7 +14268,7 @@ namespace ts {
}
function isTypeAssertion(node: Expression) {
node = skipParenthesizedNodes(node);
node = skipParentheses(node);
return node.kind === SyntaxKind.TypeAssertionExpression || node.kind === SyntaxKind.AsExpression;
}

View File

@@ -1763,6 +1763,14 @@
"category": "Error",
"code": 2538
},
"Cannot assign to '{0}' because it is not a variable.": {
"category": "Error",
"code": 2539
},
"Cannot assign to '{0}' because it is a constant or a read-only property.": {
"category": "Error",
"code": 2540
},
"JSX element attributes type '{0}' may not be a union type.": {
"category": "Error",
"code": 2600

View File

@@ -956,10 +956,6 @@ namespace ts {
operator: PostfixUnaryOperator;
}
export interface PostfixExpression extends UnaryExpression {
_postfixExpressionBrand: any;
}
export interface LeftHandSideExpression extends IncrementExpression {
_leftHandSideExpressionBrand: any;
}

View File

@@ -1624,29 +1624,47 @@ namespace ts {
return node && node.dotDotDotToken !== undefined;
}
export const enum AssignmentKind {
None, Definite, Compound
}
export function getAssignmentTargetKind(node: Node): AssignmentKind {
let parent = node.parent;
while (true) {
switch (parent.kind) {
case SyntaxKind.BinaryExpression:
const binaryOperator = (<BinaryExpression>parent).operatorToken.kind;
return isAssignmentOperator(binaryOperator) && (<BinaryExpression>parent).left === node ?
binaryOperator === SyntaxKind.EqualsToken ? AssignmentKind.Definite : AssignmentKind.Compound :
AssignmentKind.None;
case SyntaxKind.PrefixUnaryExpression:
case SyntaxKind.PostfixUnaryExpression:
const unaryOperator = (<PrefixUnaryExpression | PostfixUnaryExpression>parent).operator;
return unaryOperator === SyntaxKind.PlusPlusToken || unaryOperator === SyntaxKind.MinusMinusToken ? AssignmentKind.Compound : AssignmentKind.None;
case SyntaxKind.ForInStatement:
case SyntaxKind.ForOfStatement:
return (<ForInStatement | ForOfStatement>parent).initializer === node ? AssignmentKind.Definite : AssignmentKind.None;
case SyntaxKind.ParenthesizedExpression:
case SyntaxKind.ArrayLiteralExpression:
case SyntaxKind.SpreadElementExpression:
node = parent;
break;
case SyntaxKind.PropertyAssignment:
case SyntaxKind.ShorthandPropertyAssignment:
node = parent.parent;
break;
default:
return AssignmentKind.None;
}
parent = node.parent;
}
}
// 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', '[{ p: a}] = xxx'.
export function isAssignmentTarget(node: Node): boolean {
while (node.parent.kind === SyntaxKind.ParenthesizedExpression) {
node = node.parent;
}
while (true) {
const parent = node.parent;
if (parent.kind === SyntaxKind.ArrayLiteralExpression || parent.kind === SyntaxKind.SpreadElementExpression) {
node = parent;
continue;
}
if (parent.kind === SyntaxKind.PropertyAssignment || parent.kind === SyntaxKind.ShorthandPropertyAssignment) {
node = parent.parent;
continue;
}
return parent.kind === SyntaxKind.BinaryExpression &&
isAssignmentOperator((<BinaryExpression>parent).operatorToken.kind) &&
(<BinaryExpression>parent).left === node ||
(parent.kind === SyntaxKind.ForInStatement || parent.kind === SyntaxKind.ForOfStatement) &&
(<ForInStatement | ForOfStatement>parent).initializer === node;
}
return getAssignmentTargetKind(node) !== AssignmentKind.None;
}
export function isNodeDescendantOf(node: Node, ancestor: Node): boolean {