Skip to content

[WIP] Update parser and diagnostics for numbers starting with zero #39698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2266,8 +2266,13 @@ namespace ts {
}

function checkStrictModeNumericLiteral(node: NumericLiteral) {
if (inStrictMode && node.numericLiteralFlags & TokenFlags.Octal) {
file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Octal_literals_are_not_allowed_in_strict_mode));
if (inStrictMode) {
if (node.numericLiteralFlags & TokenFlags.Octal) {
file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Octal_literals_are_not_allowed_in_strict_mode));
}
else if (node.numericLiteralFlags & TokenFlags.StartsWithZero) {
file.bindDiagnostics.push(createDiagnosticForNode(node, Diagnostics.Number_literals_starting_with_0_are_not_allowed_in_strict_mode));
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1180,6 +1180,14 @@
"category": "Error",
"code": 1388
},
"Number literals starting with '0' are not allowed in strict mode.": {
"category": "Error",
"code": 1389
},
"A bigint literal cannot start with '0'.": {
"category": "Error",
"code": 1390
},

"The types of '{0}' are incompatible between these types.": {
"category": "Error",
Expand Down Expand Up @@ -4677,6 +4685,10 @@
"category": "Error",
"code": 6504
},
"Numeric separators are not allowed in numbers that start with '0'.": {
"category": "Error",
"code": 6505
},

"Variable '{0}' implicitly has an '{1}' type.": {
"category": "Error",
Expand Down
133 changes: 116 additions & 17 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1094,6 +1094,116 @@ namespace ts {
}
}

function scanPotentiallyOctalNumberFragment(): { fragment: string, isOctalCandidate: boolean } {
let start = pos;
let allowSeparator = false;
let isPreviousTokenSeparator = false;
let shouldShowLeadingZeroError = true;
let isOctalCandidate = pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1));
let result = "";
while (true) {
const ch = text.charCodeAt(pos);
if (ch === CharacterCodes._) {
tokenFlags |= TokenFlags.ContainsSeparator;
isOctalCandidate = false;
if (allowSeparator) {
allowSeparator = false;
isPreviousTokenSeparator = true;
result += text.substring(start, pos);
}
else if (isPreviousTokenSeparator) {
error(Diagnostics.Multiple_consecutive_numeric_separators_are_not_permitted, pos, 1);
}
else {
error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1);
}
if (shouldShowLeadingZeroError) {
error(Diagnostics.Numeric_separators_are_not_allowed_in_numbers_that_start_with_0, pos, 1);
shouldShowLeadingZeroError = false;
}
pos++;
start = pos;
continue;
}
if (isDigit(ch)) {
if (!isOctalDigit(ch)) {
isOctalCandidate = false;
}
allowSeparator = true;
isPreviousTokenSeparator = false;
pos++;
continue;
}
break;
}
if (text.charCodeAt(pos - 1) === CharacterCodes._) {
error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1);
}
return { fragment: result + text.substring(start, pos), isOctalCandidate };
}

function scanZeroLeadingNumber(): { type: SyntaxKind, value: string } {
const start = pos;
const { fragment: mainFragment, isOctalCandidate } = scanPotentiallyOctalNumberFragment();
let decimalFragment: string | undefined;
let scientificFragment: string | undefined;
if (text.charCodeAt(pos) === CharacterCodes.dot && !(isOctalCandidate && isIdentifierStart(codePointAt(text, pos + 1), languageVersion))) {
pos++;
decimalFragment = scanNumberFragment();
}
let end = pos;
if (text.charCodeAt(pos) === CharacterCodes.E || text.charCodeAt(pos) === CharacterCodes.e) {
pos++;
tokenFlags |= TokenFlags.Scientific;
if (text.charCodeAt(pos) === CharacterCodes.plus || text.charCodeAt(pos) === CharacterCodes.minus) pos++;
const preNumericPart = pos;
const finalFragment = scanNumberFragment();
if (!finalFragment) {
error(Diagnostics.Digit_expected);
}
else {
scientificFragment = text.substring(end, preNumericPart) + finalFragment;
end = pos;
}
}
let result: string;
if (tokenFlags & TokenFlags.ContainsSeparator) {
result = mainFragment;
if (decimalFragment) {
result += "." + decimalFragment;
}
if (scientificFragment) {
result += scientificFragment;
}
}
else {
result = text.substring(start, end); // No need to use all the fragments; no _ removal needed
}

if (decimalFragment !== undefined || tokenFlags & TokenFlags.Scientific) {
checkForIdentifierStartAfterNumericLiteral(start, decimalFragment === undefined && !!(tokenFlags & TokenFlags.Scientific));
return {
type: SyntaxKind.NumericLiteral,
value: "" + +result // if value is not an integer, it can be safely coerced to a number
};
}
else {
let type = SyntaxKind.NumericLiteral;
tokenValue = "" + +result;
if (text.charCodeAt(pos) === CharacterCodes.n) {
type = SyntaxKind.BigIntLiteral;
tokenValue = parsePseudoBigInt(tokenValue + "n") + "n";
pos++;
error(Diagnostics.A_bigint_literal_cannot_start_with_0, start, pos - start);
}
else if (isOctalCandidate) {
tokenFlags |= TokenFlags.Octal;
}
checkForIdentifierStartAfterNumericLiteral(start);
return { type, value: tokenValue };
}
}

function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) {
if (!isIdentifierStart(codePointAt(text, pos), languageVersion)) {
return;
Expand All @@ -1116,14 +1226,6 @@ namespace ts {
}
}

function scanOctalDigits(): number {
const start = pos;
while (isOctalDigit(text.charCodeAt(pos))) {
pos++;
}
return +(text.substring(start, pos));
}

/**
* Scans the given number of hexadecimal digits in the text,
* returning -1 if the given number is unavailable.
Expand Down Expand Up @@ -1852,16 +1954,13 @@ namespace ts {
tokenFlags |= TokenFlags.OctalSpecifier;
return token = checkBigIntSuffix();
}
// Try to parse as an octal
if (pos + 1 < end && isOctalDigit(text.charCodeAt(pos + 1))) {
tokenValue = "" + scanOctalDigits();
tokenFlags |= TokenFlags.Octal;
return token = SyntaxKind.NumericLiteral;
else if (pos + 1 < end && (isDigit(text.charCodeAt(pos + 1)) || text.charCodeAt(pos + 1) === CharacterCodes._)) {
tokenFlags |= TokenFlags.StartsWithZero;
({ type: token, value: tokenValue } = scanZeroLeadingNumber());
return token;
}
// This fall-through is a deviation from the EcmaScript grammar. The grammar says that a leading zero
// can only be followed by an octal digit, a dot, or the end of the number literal. However, we are being
// permissive and allowing decimal digits of the form 08* and 09* (which many browsers also do).
// falls through

// Fallthrough only for single digit number '0' or number starting with '0.' which don't need special checks
case CharacterCodes._1:
case CharacterCodes._2:
case CharacterCodes._3:
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2078,9 +2078,11 @@ namespace ts {
/* @internal */
ContainsInvalidEscape = 1 << 11, // e.g. `\uhello`
/* @internal */
StartsWithZero = 1 << 12,
/* @internal */
BinaryOrOctalSpecifier = BinarySpecifier | OctalSpecifier,
/* @internal */
NumericLiteralFlags = Scientific | Octal | HexSpecifier | BinaryOrOctalSpecifier | ContainsSeparator,
NumericLiteralFlags = Scientific | Octal | HexSpecifier | BinaryOrOctalSpecifier | ContainsSeparator | StartsWithZero,
/* @internal */
TemplateLiteralLikeFlags = ContainsInvalidEscape,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ tests/cases/compiler/b.js(3,7): error TS1210: Invalid use of 'eval'. Class defin
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.
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.
tests/cases/compiler/c.js(2,5): error TS1215: Invalid use of 'eval'. Modules are automatically in strict mode.
tests/cases/compiler/d.js(2,9): error TS1121: Octal literals are not allowed in strict mode.
tests/cases/compiler/d.js(2,11): error TS1005: ',' expected.
tests/cases/compiler/d.js(2,9): error TS1389: Number literals starting with '0' are not allowed in strict mode.


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

==== tests/cases/compiler/d.js (2 errors) ====
==== tests/cases/compiler/d.js (1 errors) ====
"use strict";
var x = 009; // error
~~
!!! error TS1121: Octal literals are not allowed in strict mode.
~
!!! error TS1005: ',' expected.
~~~
!!! error TS1389: Number literals starting with '0' are not allowed in strict mode.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,5 @@ var eval = function () {

var x = 009; // error
>x : number
>00 : 0
>9 : 9
>009 : 9

6 changes: 3 additions & 3 deletions tests/baselines/reference/parseBigInt.errors.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
tests/cases/compiler/parseBigInt.ts(51,20): error TS2736: Operator '+' cannot be applied to type 'bigint'.
tests/cases/compiler/parseBigInt.ts(52,23): error TS2736: Operator '+' cannot be applied to type 'bigint'.
tests/cases/compiler/parseBigInt.ts(56,25): error TS1005: ',' expected.
tests/cases/compiler/parseBigInt.ts(56,21): error TS1390: A bigint literal cannot start with '0'.
tests/cases/compiler/parseBigInt.ts(57,22): error TS1352: A bigint literal cannot use exponential notation.
tests/cases/compiler/parseBigInt.ts(58,19): error TS1353: A bigint literal must be an integer.
tests/cases/compiler/parseBigInt.ts(59,26): error TS1353: A bigint literal must be an integer.
Expand Down Expand Up @@ -78,8 +78,8 @@ tests/cases/compiler/parseBigInt.ts(70,72): error TS2345: Argument of type '3' i
// Parsing errors
// In separate blocks because they each declare an "n" variable
{ const legacyOct = 0123n; }
~
!!! error TS1005: ',' expected.
~~~~~
!!! error TS1390: A bigint literal cannot start with '0'.
{ const scientific = 1e2n; }
~~~~
!!! error TS1352: A bigint literal cannot use exponential notation.
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/parseBigInt.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const unaryPlusHex = +0x123n;
// Parsing errors
// In separate blocks because they each declare an "n" variable
{
const legacyOct = 0123, n;
const legacyOct = 123n;
}
{
const scientific = 1e2n;
Expand Down
1 change: 0 additions & 1 deletion tests/baselines/reference/parseBigInt.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ const unaryPlusHex = +0x123n;
// In separate blocks because they each declare an "n" variable
{ const legacyOct = 0123n; }
>legacyOct : Symbol(legacyOct, Decl(parseBigInt.ts, 55, 7))
>n : Symbol(n, Decl(parseBigInt.ts, 55, 24))

{ const scientific = 1e2n; }
>scientific : Symbol(scientific, Decl(parseBigInt.ts, 56, 7))
Expand Down
5 changes: 2 additions & 3 deletions tests/baselines/reference/parseBigInt.types
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,8 @@ const unaryPlusHex = +0x123n;
// Parsing errors
// In separate blocks because they each declare an "n" variable
{ const legacyOct = 0123n; }
>legacyOct : 123
>0123 : 123
>n : any
>legacyOct : 123n
>0123n : 123n

{ const scientific = 1e2n; }
>scientific : 100
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
tests/cases/conformance/parser/ecmascriptnext/numericSeparators/1.ts(1,5): error TS6188: Numeric separators are not allowed here.
tests/cases/conformance/parser/ecmascriptnext/numericSeparators/2.ts(1,3): error TS6188: Numeric separators are not allowed here.
tests/cases/conformance/parser/ecmascriptnext/numericSeparators/3.ts(1,2): error TS6188: Numeric separators are not allowed here.
tests/cases/conformance/parser/ecmascriptnext/numericSeparators/3.ts(1,2): error TS6505: Numeric separators are not allowed in numbers that start with '0'.
tests/cases/conformance/parser/ecmascriptnext/numericSeparators/3.ts(1,3): error TS1351: An identifier or keyword cannot immediately follow a numeric literal.
tests/cases/conformance/parser/ecmascriptnext/numericSeparators/3.ts(1,3): error TS2304: Cannot find name 'B0101'.
tests/cases/conformance/parser/ecmascriptnext/numericSeparators/4.ts(1,6): error TS6189: Multiple consecutive numeric separators are not permitted.
Expand All @@ -23,7 +23,7 @@ tests/cases/conformance/parser/ecmascriptnext/numericSeparators/6.ts(1,5): error
==== tests/cases/conformance/parser/ecmascriptnext/numericSeparators/3.ts (3 errors) ====
0_B0101
~
!!! error TS6188: Numeric separators are not allowed here.
!!! error TS6505: Numeric separators are not allowed in numbers that start with '0'.
~~~~~
!!! error TS1351: An identifier or keyword cannot immediately follow a numeric literal.
~~~~~
Expand Down
Loading