Skip to content

Commit 0d37497

Browse files
committed
Update parser and diagnostics for numbers starting with zero
1 parent 34adaaf commit 0d37497

27 files changed

Lines changed: 229 additions & 93 deletions

src/compiler/binder.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2266,8 +2266,13 @@ namespace ts {
22662266
}
22672267

22682268
function checkStrictModeNumericLiteral(node: NumericLiteral) {
2269-
if (inStrictMode && node.numericLiteralFlags & TokenFlags.Octal) {
2270-
file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Octal_literals_are_not_allowed_in_strict_mode));
2269+
if (inStrictMode) {
2270+
if (node.numericLiteralFlags & TokenFlags.Octal) {
2271+
file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Octal_literals_are_not_allowed_in_strict_mode));
2272+
}
2273+
else if (node.numericLiteralFlags & TokenFlags.StartsWithZero) {
2274+
file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Number_literals_starting_with_0_are_not_allowed_in_strict_mode));
2275+
}
22712276
}
22722277
}
22732278

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,14 @@
11801180
"category": "Error",
11811181
"code": 1388
11821182
},
1183+
"Number literals starting with '0' are not allowed in strict mode.": {
1184+
"category": "Error",
1185+
"code": 1389
1186+
},
1187+
"A bigint literal cannot start with '0'.": {
1188+
"category": "Error",
1189+
"code": 1390
1190+
},
11831191

11841192
"The types of '{0}' are incompatible between these types.": {
11851193
"category": "Error",
@@ -4677,6 +4685,10 @@
46774685
"category": "Error",
46784686
"code": 6504
46794687
},
4688+
"Numeric separators are not allowed in numbers that start with '0'.": {
4689+
"category": "Error",
4690+
"code": 6505
4691+
},
46804692

46814693
"Variable '{0}' implicitly has an '{1}' type.": {
46824694
"category": "Error",

src/compiler/scanner.ts

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,116 @@ namespace ts {
10941094
}
10951095
}
10961096

1097+
function scanPotentiallyOctalNumberFragment(): { fragment: string, isOctalCandidate: boolean } {
1098+
let start = pos;
1099+
let allowSeparator = false;
1100+
let isPreviousTokenSeparator = false;
1101+
let shouldShowLeadingZeroError = true;
1102+
let isOctalCandidate = pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1));
1103+
let result = "";
1104+
while (true) {
1105+
const ch = text.charCodeAt(pos);
1106+
if (ch === CharacterCodes._) {
1107+
tokenFlags |= TokenFlags.ContainsSeparator;
1108+
isOctalCandidate = false;
1109+
if (allowSeparator) {
1110+
allowSeparator = false;
1111+
isPreviousTokenSeparator = true;
1112+
result += text.substring(start, pos);
1113+
}
1114+
else if (isPreviousTokenSeparator) {
1115+
error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1);
1116+
}
1117+
else {
1118+
error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1);
1119+
}
1120+
if (shouldShowLeadingZeroError) {
1121+
error(Diagnostics.Numeric_separators_are_not_allowed_in_numbers_that_start_with_0, pos, 1);
1122+
shouldShowLeadingZeroError = false;
1123+
}
1124+
pos++;
1125+
start = pos;
1126+
continue;
1127+
}
1128+
if (isDigit(ch)) {
1129+
if (!isOctalDigit(ch)) {
1130+
isOctalCandidate = false;
1131+
}
1132+
allowSeparator = true;
1133+
isPreviousTokenSeparator = false;
1134+
pos++;
1135+
continue;
1136+
}
1137+
break;
1138+
}
1139+
if (text.charCodeAt(pos - 1) === CharacterCodes._) {
1140+
error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1);
1141+
}
1142+
return { fragment: result + text.substring(start, pos), isOctalCandidate };
1143+
}
1144+
1145+
function scanZeroLeadingNumber(): { type: SyntaxKind, value: string } {
1146+
const start = pos;
1147+
const { fragment: mainFragment, isOctalCandidate } = scanPotentiallyOctalNumberFragment();
1148+
let decimalFragment: string | undefined;
1149+
let scientificFragment: string | undefined;
1150+
if (text.charCodeAt(pos) === CharacterCodes.dot && !(isOctalCandidate && isIdentifierStart(codePointAt(text, pos + 1), languageVersion))) {
1151+
pos++;
1152+
decimalFragment = scanNumberFragment();
1153+
}
1154+
let end = pos;
1155+
if (text.charCodeAt(pos) === CharacterCodes.E || text.charCodeAt(pos) === CharacterCodes.e) {
1156+
pos++;
1157+
tokenFlags |= TokenFlags.Scientific;
1158+
if (text.charCodeAt(pos) === CharacterCodes.plus || text.charCodeAt(pos) === CharacterCodes.minus) pos++;
1159+
const preNumericPart = pos;
1160+
const finalFragment = scanNumberFragment();
1161+
if (!finalFragment) {
1162+
error(Diagnostics.Digit_expected);
1163+
}
1164+
else {
1165+
scientificFragment = text.substring(end, preNumericPart) + finalFragment;
1166+
end = pos;
1167+
}
1168+
}
1169+
let result: string;
1170+
if (tokenFlags & TokenFlags.ContainsSeparator) {
1171+
result = mainFragment;
1172+
if (decimalFragment) {
1173+
result += "." + decimalFragment;
1174+
}
1175+
if (scientificFragment) {
1176+
result += scientificFragment;
1177+
}
1178+
}
1179+
else {
1180+
result = text.substring(start, end); // No need to use all the fragments; no _ removal needed
1181+
}
1182+
1183+
if (decimalFragment !== undefined || tokenFlags & TokenFlags.Scientific) {
1184+
checkForIdentifierStartAfterNumericLiteral(start, decimalFragment === undefined && !!(tokenFlags & TokenFlags.Scientific));
1185+
return {
1186+
type: SyntaxKind.NumericLiteral,
1187+
value: "" + +result // if value is not an integer, it can be safely coerced to a number
1188+
};
1189+
}
1190+
else {
1191+
let type = SyntaxKind.NumericLiteral;
1192+
tokenValue = "" + +result;
1193+
if (text.charCodeAt(pos) === CharacterCodes.n) {
1194+
type = SyntaxKind.BigIntLiteral;
1195+
tokenValue = parsePseudoBigInt(tokenValue + "n") + "n";
1196+
pos++;
1197+
error(Diagnostics.A_bigint_literal_cannot_start_with_0, start, pos - start);
1198+
}
1199+
else if (isOctalCandidate) {
1200+
tokenFlags |= TokenFlags.Octal;
1201+
}
1202+
checkForIdentifierStartAfterNumericLiteral(start);
1203+
return { type, value: tokenValue };
1204+
}
1205+
}
1206+
10971207
function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) {
10981208
if (!isIdentifierStart(codePointAt(text, pos), languageVersion)) {
10991209
return;
@@ -1116,14 +1226,6 @@ namespace ts {
11161226
}
11171227
}
11181228

1119-
function scanOctalDigits(): number {
1120-
const start = pos;
1121-
while (isOctalDigit(text.charCodeAt(pos))) {
1122-
pos++;
1123-
}
1124-
return +(text.substring(start, pos));
1125-
}
1126-
11271229
/**
11281230
* Scans the given number of hexadecimal digits in the text,
11291231
* returning -1 if the given number is unavailable.
@@ -1852,16 +1954,13 @@ namespace ts {
18521954
tokenFlags |= TokenFlags.OctalSpecifier;
18531955
return token = checkBigIntSuffix();
18541956
}
1855-
// Try to parse as an octal
1856-
if (pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1))) {
1857-
tokenValue = "" + scanOctalDigits();
1858-
tokenFlags |= TokenFlags.Octal;
1859-
return token = SyntaxKind.NumericLiteral;
1957+
else if (pos + 1 < end && (isDigit(text.charCodeAt(pos + 1)) || text.charCodeAt(pos + 1) === CharacterCodes._)) {
1958+
tokenFlags |= TokenFlags.StartsWithZero;
1959+
({ type: token, value: tokenValue } = scanZeroLeadingNumber());
1960+
return token;
18601961
}
1861-
// This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero
1862-
// can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being
1863-
// permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do).
1864-
// falls through
1962+
1963+
// Fallthrough only for single digit number '0' or number starting with '0.' which don't need special checks
18651964
case CharacterCodes._1:
18661965
case CharacterCodes._2:
18671966
case CharacterCodes._3:

src/compiler/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2078,9 +2078,11 @@ namespace ts {
20782078
/* @internal */
20792079
ContainsInvalidEscape = 1 << 11, // e.g. `\uhello`
20802080
/* @internal */
2081+
StartsWithZero = 1 << 12,
2082+
/* @internal */
20812083
BinaryOrOctalSpecifier = BinarySpecifier | OctalSpecifier,
20822084
/* @internal */
2083-
NumericLiteralFlags = Scientific | Octal | HexSpecifier | BinaryOrOctalSpecifier | ContainsSeparator,
2085+
NumericLiteralFlags = Scientific | Octal | HexSpecifier | BinaryOrOctalSpecifier | ContainsSeparator | StartsWithZero,
20842086
/* @internal */
20852087
TemplateLiteralLikeFlags = ContainsInvalidEscape,
20862088
}

tests/baselines/reference/jsFileCompilationBindStrictModeErrors.errors.txt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ tests/cases/compiler/b.js(3,7): error TS1210: Invalid use of 'eval'. Class defin
1111
tests/cases/compiler/b.js(6,13): error TS1213: Identifier expected. 'let' is a reserved word in strict mode. Class definitions are automatically in strict mode.
1212
tests/cases/compiler/c.js(1,12): error TS1214: Identifier expected. 'let' is a reserved word in strict mode. Modules are automatically in strict mode.
1313
tests/cases/compiler/c.js(2,5): error TS1215: Invalid use of 'eval'. Modules are automatically in strict mode.
14-
tests/cases/compiler/d.js(2,9): error TS1121: Octal literals are not allowed in strict mode.
15-
tests/cases/compiler/d.js(2,11): error TS1005: ',' expected.
14+
tests/cases/compiler/d.js(2,9): error TS1389: Number literals starting with '0' are not allowed in strict mode.
1615

1716

1817
==== tests/cases/compiler/a.js (9 errors) ====
@@ -75,10 +74,8 @@ tests/cases/compiler/d.js(2,11): error TS1005: ',' expected.
7574
!!! error TS1215: Invalid use of 'eval'. Modules are automatically in strict mode.
7675
};
7776

78-
==== tests/cases/compiler/d.js (2 errors) ====
77+
==== tests/cases/compiler/d.js (1 errors) ====
7978
"use strict";
8079
var x = 009; // error
81-
~~
82-
!!! error TS1121: Octal literals are not allowed in strict mode.
83-
~
84-
!!! error TS1005: ',' expected.
80+
~~~
81+
!!! error TS1389: Number literals starting with '0' are not allowed in strict mode.

tests/baselines/reference/jsFileCompilationBindStrictModeErrors.types

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,5 @@ var eval = function () {
7979

8080
var x = 009; // error
8181
>x : number
82-
>00 : 0
83-
>9 : 9
82+
>009 : 9
8483

tests/baselines/reference/parseBigInt.errors.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
tests/cases/compiler/parseBigInt.ts(51,20): error TS2736: Operator '+' cannot be applied to type 'bigint'.
22
tests/cases/compiler/parseBigInt.ts(52,23): error TS2736: Operator '+' cannot be applied to type 'bigint'.
3-
tests/cases/compiler/parseBigInt.ts(56,25): error TS1005: ',' expected.
3+
tests/cases/compiler/parseBigInt.ts(56,21): error TS1390: A bigint literal cannot start with '0'.
44
tests/cases/compiler/parseBigInt.ts(57,22): error TS1352: A bigint literal cannot use exponential notation.
55
tests/cases/compiler/parseBigInt.ts(58,19): error TS1353: A bigint literal must be an integer.
66
tests/cases/compiler/parseBigInt.ts(59,26): error TS1353: A bigint literal must be an integer.
@@ -78,8 +78,8 @@ tests/cases/compiler/parseBigInt.ts(70,72): error TS2345: Argument of type '3' i
7878
// Parsing errors
7979
// In separate blocks because they each declare an "n" variable
8080
{ const legacyOct = 0123n; }
81-
~
82-
!!! error TS1005: ',' expected.
81+
~~~~~
82+
!!! error TS1390: A bigint literal cannot start with '0'.
8383
{ const scientific = 1e2n; }
8484
~~~~
8585
!!! error TS1352: A bigint literal cannot use exponential notation.

tests/baselines/reference/parseBigInt.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ const unaryPlusHex = +0x123n;
120120
// Parsing errors
121121
// In separate blocks because they each declare an "n" variable
122122
{
123-
const legacyOct = 0123, n;
123+
const legacyOct = 123n;
124124
}
125125
{
126126
const scientific = 1e2n;

tests/baselines/reference/parseBigInt.symbols

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ const unaryPlusHex = +0x123n;
127127
// In separate blocks because they each declare an "n" variable
128128
{ const legacyOct = 0123n; }
129129
>legacyOct : Symbol(legacyOct, Decl(parseBigInt.ts, 55, 7))
130-
>n : Symbol(n, Decl(parseBigInt.ts, 55, 24))
131130

132131
{ const scientific = 1e2n; }
133132
>scientific : Symbol(scientific, Decl(parseBigInt.ts, 56, 7))

tests/baselines/reference/parseBigInt.types

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,8 @@ const unaryPlusHex = +0x123n;
174174
// Parsing errors
175175
// In separate blocks because they each declare an "n" variable
176176
{ const legacyOct = 0123n; }
177-
>legacyOct : 123
178-
>0123 : 123
179-
>n : any
177+
>legacyOct : 123n
178+
>0123n : 123n
180179

181180
{ const scientific = 1e2n; }
182181
>scientific : 100

0 commit comments

Comments
 (0)