Skip to content

Commit be4ff97

Browse files
committed
Merge pull request #2308 from Microsoft/for-ofES5
Type checking 'for...of' in ES3/5
2 parents 17d2a1b + 6691408 commit be4ff97

File tree

125 files changed

+1204
-600
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+1204
-600
lines changed

src/compiler/checker.ts

+89-27
Original file line numberDiff line numberDiff line change
@@ -1918,7 +1918,11 @@ module ts {
19181918
return anyType;
19191919
}
19201920
if (declaration.parent.parent.kind === SyntaxKind.ForOfStatement) {
1921-
return getTypeForVariableDeclarationInForOfStatement(<ForOfStatement>declaration.parent.parent);
1921+
// checkRightHandSideOfForOf will return undefined if the for-of expression type was
1922+
// missing properties/signatures required to get its iteratedType (like
1923+
// [Symbol.iterator] or next). This may be because we accessed properties from anyType,
1924+
// or it may have led to an error inside getIteratedType.
1925+
return checkRightHandSideOfForOf((<ForOfStatement>declaration.parent.parent).expression) || anyType;
19221926
}
19231927
if (isBindingPattern(declaration.parent)) {
19241928
return getTypeForBindingElement(<BindingElement>declaration);
@@ -4759,17 +4763,22 @@ module ts {
47594763

47604764
// For a union type, remove all constituent types that are of the given type kind (when isOfTypeKind is true)
47614765
// or not of the given type kind (when isOfTypeKind is false)
4762-
function removeTypesFromUnionType(type: Type, typeKind: TypeFlags, isOfTypeKind: boolean): Type {
4766+
function removeTypesFromUnionType(type: Type, typeKind: TypeFlags, isOfTypeKind: boolean, allowEmptyUnionResult: boolean): Type {
47634767
if (type.flags & TypeFlags.Union) {
47644768
var types = (<UnionType>type).types;
47654769
if (forEach(types, t => !!(t.flags & typeKind) === isOfTypeKind)) {
47664770
// Above we checked if we have anything to remove, now use the opposite test to do the removal
47674771
var narrowedType = getUnionType(filter(types, t => !(t.flags & typeKind) === isOfTypeKind));
4768-
if (narrowedType !== emptyObjectType) {
4772+
if (allowEmptyUnionResult || narrowedType !== emptyObjectType) {
47694773
return narrowedType;
47704774
}
47714775
}
47724776
}
4777+
else if (allowEmptyUnionResult && !!(type.flags & typeKind) === isOfTypeKind) {
4778+
// Use getUnionType(emptyArray) instead of emptyObjectType in case the way empty union types
4779+
// are represented ever changes.
4780+
return getUnionType(emptyArray);
4781+
}
47734782
return type;
47744783
}
47754784

@@ -4972,20 +4981,21 @@ module ts {
49724981
if (assumeTrue) {
49734982
// Assumed result is true. If check was not for a primitive type, remove all primitive types
49744983
if (!typeInfo) {
4975-
return removeTypesFromUnionType(type, /*typeKind*/ TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.Boolean | TypeFlags.ESSymbol, /*isOfTypeKind*/ true);
4984+
return removeTypesFromUnionType(type, /*typeKind*/ TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.Boolean | TypeFlags.ESSymbol,
4985+
/*isOfTypeKind*/ true, /*allowEmptyUnionResult*/ false);
49764986
}
49774987
// Check was for a primitive type, return that primitive type if it is a subtype
49784988
if (isTypeSubtypeOf(typeInfo.type, type)) {
49794989
return typeInfo.type;
49804990
}
49814991
// Otherwise, remove all types that aren't of the primitive type kind. This can happen when the type is
49824992
// union of enum types and other types.
4983-
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ false);
4993+
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ false, /*allowEmptyUnionResult*/ false);
49844994
}
49854995
else {
49864996
// Assumed result is false. If check was for a primitive type, remove that primitive type
49874997
if (typeInfo) {
4988-
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ true);
4998+
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ true, /*allowEmptyUnionResult*/ false);
49894999
}
49905000
// Otherwise we don't have enough information to do anything.
49915001
return type;
@@ -8826,25 +8836,19 @@ module ts {
88268836
}
88278837

88288838
function checkForOfStatement(node: ForOfStatement): void {
8829-
if (languageVersion < ScriptTarget.ES6) {
8830-
grammarErrorOnFirstToken(node, Diagnostics.for_of_statements_are_only_available_when_targeting_ECMAScript_6_or_higher);
8831-
return;
8832-
}
8833-
88348839
checkGrammarForInOrForOfStatement(node)
88358840

88368841
// Check the LHS and RHS
88378842
// If the LHS is a declaration, just check it as a variable declaration, which will in turn check the RHS
8838-
// via getTypeForVariableDeclarationInForOfStatement.
8843+
// via checkRightHandSideOfForOf.
88398844
// If the LHS is an expression, check the LHS, as a destructuring assignment or as a reference.
88408845
// Then check that the RHS is assignable to it.
88418846
if (node.initializer.kind === SyntaxKind.VariableDeclarationList) {
88428847
checkForInOrForOfVariableDeclaration(node);
88438848
}
88448849
else {
88458850
var varExpr = <Expression>node.initializer;
8846-
var rightType = checkExpression(node.expression);
8847-
var iteratedType = checkIteratedType(rightType, node.expression);
8851+
var iteratedType = checkRightHandSideOfForOf(node.expression);
88488852

88498853
// There may be a destructuring assignment on the left side
88508854
if (varExpr.kind === SyntaxKind.ArrayLiteralExpression || varExpr.kind === SyntaxKind.ObjectLiteralExpression) {
@@ -8926,18 +8930,11 @@ module ts {
89268930
}
89278931
}
89288932

8929-
function getTypeForVariableDeclarationInForOfStatement(forOfStatement: ForOfStatement): Type {
8930-
// Temporarily return 'any' below ES6
8931-
if (languageVersion < ScriptTarget.ES6) {
8932-
return anyType;
8933-
}
8934-
8935-
// iteratedType will be undefined if the for-of expression type was missing properties/signatures
8936-
// required to get its iteratedType (like [Symbol.iterator] or next). This may be
8937-
// because we accessed properties from anyType, or it may have led to an error inside
8938-
// getIteratedType.
8939-
var expressionType = getTypeOfExpression(forOfStatement.expression);
8940-
return checkIteratedType(expressionType, forOfStatement.expression) || anyType;
8933+
function checkRightHandSideOfForOf(rhsExpression: Expression): Type {
8934+
var expressionType = getTypeOfExpression(rhsExpression);
8935+
return languageVersion >= ScriptTarget.ES6
8936+
? checkIteratedType(expressionType, rhsExpression)
8937+
: checkElementTypeOfArrayOrString(expressionType, rhsExpression);
89418938
}
89428939

89438940
/**
@@ -9036,6 +9033,72 @@ module ts {
90369033
}
90379034
}
90389035

9036+
/**
9037+
* This function does the following steps:
9038+
* 1. Break up arrayOrStringType (possibly a union) into its string constituents and array constituents.
9039+
* 2. Take the element types of the array constituents.
9040+
* 3. Return the union of the element types, and string if there was a string constitutent.
9041+
*
9042+
* For example:
9043+
* string -> string
9044+
* number[] -> number
9045+
* string[] | number[] -> string | number
9046+
* string | number[] -> string | number
9047+
* string | string[] | number[] -> string | number
9048+
*
9049+
* It also errors if:
9050+
* 1. Some constituent is neither a string nor an array.
9051+
* 2. Some constituent is a string and target is less than ES5 (because in ES3 string is not indexable).
9052+
*/
9053+
function checkElementTypeOfArrayOrString(arrayOrStringType: Type, expressionForError: Expression): Type {
9054+
Debug.assert(languageVersion < ScriptTarget.ES6);
9055+
9056+
// After we remove all types that are StringLike, we will know if there was a string constituent
9057+
// based on whether the remaining type is the same as the initial type.
9058+
var arrayType = removeTypesFromUnionType(arrayOrStringType, TypeFlags.StringLike, /*isTypeOfKind*/ true, /*allowEmptyUnionResult*/ true);
9059+
var hasStringConstituent = arrayOrStringType !== arrayType;
9060+
9061+
var reportedError = false;
9062+
if (hasStringConstituent) {
9063+
if (languageVersion < ScriptTarget.ES5) {
9064+
error(expressionForError, Diagnostics.Using_a_string_in_a_for_of_statement_is_only_supported_in_ECMAScript_5_and_higher);
9065+
reportedError = true;
9066+
}
9067+
9068+
// Now that we've removed all the StringLike types, if no constituents remain, then the entire
9069+
// arrayOrStringType was a string.
9070+
if (arrayType === emptyObjectType) {
9071+
return stringType;
9072+
}
9073+
}
9074+
9075+
if (!isArrayLikeType(arrayType)) {
9076+
if (!reportedError) {
9077+
// Which error we report depends on whether there was a string constituent. For example,
9078+
// if the input type is number | string, we want to say that number is not an array type.
9079+
// But if the input was just number, we want to say that number is not an array type
9080+
// or a string type.
9081+
var diagnostic = hasStringConstituent
9082+
? Diagnostics.Type_0_is_not_an_array_type
9083+
: Diagnostics.Type_0_is_not_an_array_type_or_a_string_type;
9084+
error(expressionForError, diagnostic, typeToString(arrayType));
9085+
}
9086+
return hasStringConstituent ? stringType : unknownType;
9087+
}
9088+
9089+
var arrayElementType = getIndexTypeOfType(arrayType, IndexKind.Number) || unknownType;
9090+
if (hasStringConstituent) {
9091+
// This is just an optimization for the case where arrayOrStringType is string | string[]
9092+
if (arrayElementType.flags & TypeFlags.StringLike) {
9093+
return stringType;
9094+
}
9095+
9096+
return getUnionType([arrayElementType, stringType]);
9097+
}
9098+
9099+
return arrayElementType;
9100+
}
9101+
90399102
function checkBreakOrContinueStatement(node: BreakOrContinueStatement) {
90409103
// Grammar checking
90419104
checkGrammarStatementInAmbientContext(node) || checkGrammarBreakOrContinueStatement(node);
@@ -10980,7 +11043,6 @@ module ts {
1098011043
}
1098111044

1098211045
function isUnknownIdentifier(location: Node, name: string): boolean {
10983-
// Do not call resolveName on a synthesized node!
1098411046
Debug.assert(!nodeIsSynthesized(location), "isUnknownIdentifier called with a synthesized location");
1098511047
return !resolveName(location, name, SymbolFlags.Value, /*nodeNotFoundMessage*/ undefined, /*nameArg*/ undefined) &&
1098611048
!hasProperty(getGeneratedNamesForSourceFile(getSourceFile(location)), name);

src/compiler/diagnosticInformationMap.generated.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,6 @@ module ts {
327327
Property_0_does_not_exist_on_const_enum_1: { code: 2479, category: DiagnosticCategory.Error, key: "Property '{0}' does not exist on 'const' enum '{1}'." },
328328
let_is_not_allowed_to_be_used_as_a_name_in_let_or_const_declarations: { code: 2480, category: DiagnosticCategory.Error, key: "'let' is not allowed to be used as a name in 'let' or 'const' declarations." },
329329
Cannot_initialize_outer_scoped_variable_0_in_the_same_scope_as_block_scoped_declaration_1: { code: 2481, category: DiagnosticCategory.Error, key: "Cannot initialize outer scoped variable '{0}' in the same scope as block scoped declaration '{1}'." },
330-
for_of_statements_are_only_available_when_targeting_ECMAScript_6_or_higher: { code: 2482, category: DiagnosticCategory.Error, key: "'for...of' statements are only available when targeting ECMAScript 6 or higher." },
331330
The_left_hand_side_of_a_for_of_statement_cannot_use_a_type_annotation: { code: 2483, category: DiagnosticCategory.Error, key: "The left-hand side of a 'for...of' statement cannot use a type annotation." },
332331
Export_declaration_conflicts_with_exported_declaration_of_0: { code: 2484, category: DiagnosticCategory.Error, key: "Export declaration conflicts with exported declaration of '{0}'" },
333332
The_left_hand_side_of_a_for_of_statement_cannot_be_a_previously_defined_constant: { code: 2485, category: DiagnosticCategory.Error, key: "The left-hand side of a 'for...of' statement cannot be a previously defined constant." },
@@ -339,6 +338,8 @@ module ts {
339338
The_left_hand_side_of_a_for_in_statement_cannot_be_a_destructuring_pattern: { code: 2491, category: DiagnosticCategory.Error, key: "The left-hand side of a 'for...in' statement cannot be a destructuring pattern." },
340339
Cannot_redeclare_identifier_0_in_catch_clause: { code: 2492, category: DiagnosticCategory.Error, key: "Cannot redeclare identifier '{0}' in catch clause" },
341340
Tuple_type_0_with_length_1_cannot_be_assigned_to_tuple_with_length_2: { code: 2493, category: DiagnosticCategory.Error, key: "Tuple type '{0}' with length '{1}' cannot be assigned to tuple with length '{2}'." },
341+
Using_a_string_in_a_for_of_statement_is_only_supported_in_ECMAScript_5_and_higher: { code: 2494, category: DiagnosticCategory.Error, key: "Using a string in a 'for...of' statement is only supported in ECMAScript 5 and higher." },
342+
Type_0_is_not_an_array_type_or_a_string_type: { code: 2461, category: DiagnosticCategory.Error, key: "Type '{0}' is not an array type or a string type." },
342343
Import_declaration_0_is_using_private_name_1: { code: 4000, category: DiagnosticCategory.Error, key: "Import declaration '{0}' is using private name '{1}'." },
343344
Type_parameter_0_of_exported_class_has_or_is_using_private_name_1: { code: 4002, category: DiagnosticCategory.Error, key: "Type parameter '{0}' of exported class has or is using private name '{1}'." },
344345
Type_parameter_0_of_exported_interface_has_or_is_using_private_name_1: { code: 4004, category: DiagnosticCategory.Error, key: "Type parameter '{0}' of exported interface has or is using private name '{1}'." },

src/compiler/diagnosticMessages.json

+8-4
Original file line numberDiff line numberDiff line change
@@ -1299,10 +1299,6 @@
12991299
"category": "Error",
13001300
"code": 2481
13011301
},
1302-
"'for...of' statements are only available when targeting ECMAScript 6 or higher.": {
1303-
"category": "Error",
1304-
"code": 2482
1305-
},
13061302
"The left-hand side of a 'for...of' statement cannot use a type annotation.": {
13071303
"category": "Error",
13081304
"code": 2483
@@ -1347,6 +1343,14 @@
13471343
"category": "Error",
13481344
"code": 2493
13491345
},
1346+
"Using a string in a 'for...of' statement is only supported in ECMAScript 5 and higher.": {
1347+
"category": "Error",
1348+
"code": 2494
1349+
},
1350+
"Type '{0}' is not an array type or a string type.": {
1351+
"category": "Error",
1352+
"code": 2461
1353+
},
13501354

13511355
"Import declaration '{0}' is using private name '{1}'.": {
13521356
"category": "Error",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
tests/cases/conformance/statements/for-ofStatements/ES3For-ofTypeCheck1.ts(1,15): error TS2494: Using a string in a 'for...of' statement is only supported in ECMAScript 5 and higher.
2+
3+
4+
==== tests/cases/conformance/statements/for-ofStatements/ES3For-ofTypeCheck1.ts (1 errors) ====
5+
for (var v of "") { }
6+
~~
7+
!!! error TS2494: Using a string in a 'for...of' statement is only supported in ECMAScript 5 and higher.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
//// [ES3For-ofTypeCheck1.ts]
2+
for (var v of "") { }
3+
4+
//// [ES3For-ofTypeCheck1.js]
5+
for (var _i = 0, _a = ""; _i < _a.length; _i++) {
6+
var v = _a[_i];
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//// [ES3For-ofTypeCheck2.ts]
2+
for (var v of [true]) { }
3+
4+
//// [ES3For-ofTypeCheck2.js]
5+
for (var _i = 0, _a = [
6+
true
7+
]; _i < _a.length; _i++) {
8+
var v = _a[_i];
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
=== tests/cases/conformance/statements/for-ofStatements/ES3For-ofTypeCheck2.ts ===
2+
for (var v of [true]) { }
3+
>v : boolean
4+
>[true] : boolean[]
5+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
tests/cases/conformance/statements/for-ofStatements/ES3For-ofTypeCheck4.ts(2,17): error TS2494: Using a string in a 'for...of' statement is only supported in ECMAScript 5 and higher.
2+
3+
4+
==== tests/cases/conformance/statements/for-ofStatements/ES3For-ofTypeCheck4.ts (1 errors) ====
5+
var union: string | string[];
6+
for (const v of union) { }
7+
~~~~~
8+
!!! error TS2494: Using a string in a 'for...of' statement is only supported in ECMAScript 5 and higher.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//// [ES3For-ofTypeCheck4.ts]
2+
var union: string | string[];
3+
for (const v of union) { }
4+
5+
//// [ES3For-ofTypeCheck4.js]
6+
var union;
7+
for (var _i = 0; _i < union.length; _i++) {
8+
var v = union[_i];
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//// [ES3For-ofTypeCheck6.ts]
2+
var union: string[] | number[];
3+
for (var v of union) { }
4+
5+
//// [ES3For-ofTypeCheck6.js]
6+
var union;
7+
for (var _i = 0; _i < union.length; _i++) {
8+
var v = union[_i];
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== tests/cases/conformance/statements/for-ofStatements/ES3For-ofTypeCheck6.ts ===
2+
var union: string[] | number[];
3+
>union : string[] | number[]
4+
5+
for (var v of union) { }
6+
>v : string | number
7+
>union : string[] | number[]
8+
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
tests/cases/conformance/statements/for-ofStatements/ES5For-of1.ts(1,1): error TS2482: 'for...of' statements are only available when targeting ECMAScript 6 or higher.
1+
tests/cases/conformance/statements/for-ofStatements/ES5For-of1.ts(2,5): error TS2304: Cannot find name 'console'.
22

33

44
==== tests/cases/conformance/statements/for-ofStatements/ES5For-of1.ts (1 errors) ====
55
for (var v of ['a', 'b', 'c']) {
6-
~~~
7-
!!! error TS2482: 'for...of' statements are only available when targeting ECMAScript 6 or higher.
86
console.log(v);
7+
~~~~~~~
8+
!!! error TS2304: Cannot find name 'console'.
99
}

tests/baselines/reference/ES5For-of10.errors.txt

-13
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
=== tests/cases/conformance/statements/for-ofStatements/ES5For-of10.ts ===
2+
function foo() {
3+
>foo : () => { x: number; }
4+
5+
return { x: 0 };
6+
>{ x: 0 } : { x: number; }
7+
>x : number
8+
}
9+
for (foo().x of []) {
10+
>foo().x : number
11+
>foo() : { x: number; }
12+
>foo : () => { x: number; }
13+
>x : number
14+
>[] : undefined[]
15+
16+
for (foo().x of [])
17+
>foo().x : number
18+
>foo() : { x: number; }
19+
>foo : () => { x: number; }
20+
>x : number
21+
>[] : undefined[]
22+
23+
var p = foo().x;
24+
>p : number
25+
>foo().x : number
26+
>foo() : { x: number; }
27+
>foo : () => { x: number; }
28+
>x : number
29+
}

tests/baselines/reference/ES5For-of11.errors.txt

-8
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
=== tests/cases/conformance/statements/for-ofStatements/ES5For-of11.ts ===
2+
var v;
3+
>v : any
4+
5+
for (v of []) { }
6+
>v : any
7+
>[] : undefined[]
8+

0 commit comments

Comments
 (0)