From b7e09455c89187f728adc17f0e0a00cc43084a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 26 Jul 2022 19:51:28 +0200 Subject: [PATCH] Fix position of errors in template literals after newlines (#14798) --- .../babel-helper-string-parser/src/index.ts | 85 ++++++++++--- packages/babel-parser/src/tokenizer/index.ts | 120 +++++++----------- .../template/error-after-newline/input.js | 3 + .../template/error-after-newline/output.json | 36 ++++++ 4 files changed, 149 insertions(+), 95 deletions(-) create mode 100644 packages/babel-parser/test/fixtures/es2015/template/error-after-newline/input.js create mode 100644 packages/babel-parser/test/fixtures/es2015/template/error-after-newline/output.json diff --git a/packages/babel-helper-string-parser/src/index.ts b/packages/babel-helper-string-parser/src/index.ts index beb7bccf8381..25ad930f5085 100644 --- a/packages/babel-helper-string-parser/src/index.ts +++ b/packages/babel-helper-string-parser/src/index.ts @@ -142,12 +142,12 @@ function isStringEnd( ); } -export type EscapedCharErrorHandlers = HexCharErrorHandlers & +type EscapedCharErrorHandlers = HexCharErrorHandlers & CodePointErrorHandlers & { - strictNumericEscape(pos: number): void; + strictNumericEscape(pos: number, lineStart: number, curLine: number): void; }; -export function readEscapedChar( +function readEscapedChar( input: string, pos: number, lineStart: number, @@ -171,6 +171,8 @@ export function readEscapedChar( ({ code, pos } = readHexChar( input, pos, + lineStart, + curLine, 2, false, throwOnInvalid, @@ -180,7 +182,14 @@ export function readEscapedChar( } case charCodes.lowercaseU: { let code; - ({ code, pos } = readCodePoint(input, pos, throwOnInvalid, errors)); + ({ code, pos } = readCodePoint( + input, + pos, + lineStart, + curLine, + throwOnInvalid, + errors, + )); return res(code === null ? null : String.fromCodePoint(code)); } case charCodes.lowercaseT: @@ -208,7 +217,7 @@ export function readEscapedChar( if (inTemplate) { return res(null); } else { - errors.strictNumericEscape(pos - 1); + errors.strictNumericEscape(pos - 1, lineStart, curLine); } // fall through default: @@ -233,7 +242,7 @@ export function readEscapedChar( if (inTemplate) { return res(null); } else { - errors.strictNumericEscape(startPos); + errors.strictNumericEscape(startPos, lineStart, curLine); } } @@ -245,13 +254,15 @@ export function readEscapedChar( } type HexCharErrorHandlers = IntErrorHandlers & { - invalidEscapeSequence(pos: number, startPos: number): void; + invalidEscapeSequence(pos: number, lineStart: number, curLine: number): void; }; // Used to read character escape sequences ('\x', '\u'). function readHexChar( input: string, pos: number, + lineStart: number, + curLine: number, len: number, forceLen: boolean, throwOnInvalid: boolean, @@ -259,10 +270,20 @@ function readHexChar( ) { const initialPos = pos; let n; - ({ n, pos } = readInt(input, pos, 16, len, forceLen, false, errors)); + ({ n, pos } = readInt( + input, + pos, + lineStart, + curLine, + 16, + len, + forceLen, + false, + errors, + )); if (n === null) { if (throwOnInvalid) { - errors.invalidEscapeSequence(pos, initialPos); + errors.invalidEscapeSequence(initialPos, lineStart, curLine); } else { pos = initialPos - 1; } @@ -271,16 +292,31 @@ function readHexChar( } export type IntErrorHandlers = { - numericSeparatorInEscapeSequence(pos: number): void; - unexpectedNumericSeparator(pos: number): void; + numericSeparatorInEscapeSequence( + pos: number, + lineStart: number, + curLine: number, + ): void; + unexpectedNumericSeparator( + pos: number, + lineStart: number, + curLine: number, + ): void; // It can return "true" to indicate that the error was handled // and the int parsing should continue. - invalidDigit(pos: number, radix: number): boolean; + invalidDigit( + pos: number, + lineStart: number, + curLine: number, + radix: number, + ): boolean; }; export function readInt( input: string, pos: number, + lineStart: number, + curLine: number, radix: number, len: number | undefined, forceLen: boolean, @@ -313,14 +349,14 @@ export function readInt( const next = input.charCodeAt(pos + 1); if (!allowNumSeparator) { - errors.numericSeparatorInEscapeSequence(pos); + errors.numericSeparatorInEscapeSequence(pos, lineStart, curLine); } else if ( Number.isNaN(next) || !isAllowedSibling(next) || forbiddenSiblings.has(prev) || forbiddenSiblings.has(next) ) { - errors.unexpectedNumericSeparator(pos); + errors.unexpectedNumericSeparator(pos, lineStart, curLine); } // Ignore this _ character @@ -340,7 +376,7 @@ export function readInt( if (val >= radix) { // If we found a digit which is too big, errors.invalidDigit can return true to avoid // breaking the loop (this is used for error recovery). - if (val <= 9 && errors.invalidDigit(pos, radix)) { + if (val <= 9 && errors.invalidDigit(pos, lineStart, curLine, radix)) { val = 0; } else if (forceLen) { val = 0; @@ -360,12 +396,14 @@ export function readInt( } export type CodePointErrorHandlers = HexCharErrorHandlers & { - invalidCodePoint(pos: number): void; + invalidCodePoint(pos: number, lineStart: number, curLine: number): void; }; export function readCodePoint( input: string, pos: number, + lineStart: number, + curLine: number, throwOnInvalid: boolean, errors: CodePointErrorHandlers, ) { @@ -377,6 +415,8 @@ export function readCodePoint( ({ code, pos } = readHexChar( input, pos, + lineStart, + curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, @@ -385,13 +425,22 @@ export function readCodePoint( ++pos; if (code !== null && code > 0x10ffff) { if (throwOnInvalid) { - errors.invalidCodePoint(pos); + errors.invalidCodePoint(pos, lineStart, curLine); } else { return { code: null, pos }; } } } else { - ({ code, pos } = readHexChar(input, pos, 4, false, throwOnInvalid, errors)); + ({ code, pos } = readHexChar( + input, + pos, + lineStart, + curLine, + 4, + false, + throwOnInvalid, + errors, + )); } return { code, pos }; } diff --git a/packages/babel-parser/src/tokenizer/index.ts b/packages/babel-parser/src/tokenizer/index.ts index 5cbda4602559..22a0ab5246d0 100644 --- a/packages/babel-parser/src/tokenizer/index.ts +++ b/packages/babel-parser/src/tokenizer/index.ts @@ -36,16 +36,18 @@ import type { LookaheadState, DeferredStrictError } from "./state"; import { readInt, readCodePoint, - readEscapedChar, readStringContents, type IntErrorHandlers, type CodePointErrorHandlers, - type EscapedCharErrorHandlers, type StringContentsErrorHandlers, } from "@babel/helper-string-parser"; import type { Plugin } from "../typings"; +function buildPosition(pos: number, lineStart: number, curLine: number) { + return new Position(curLine, pos - lineStart, pos); +} + const VALID_REGEX_FLAGS = new Set([ charCodes.lowercaseG, charCodes.lowercaseM, @@ -1129,6 +1131,8 @@ export default class Tokenizer extends CommentsParser { const { n, pos } = readInt( this.input, this.state.pos, + this.state.lineStart, + this.state.curLine, radix, len, forceLen, @@ -1284,6 +1288,8 @@ export default class Tokenizer extends CommentsParser { const { code, pos } = readCodePoint( this.input, this.state.pos, + this.state.lineStart, + this.state.curLine, throwOnInvalid, this.errorHandlers_readCodePoint, ); @@ -1348,11 +1354,7 @@ export default class Tokenizer extends CommentsParser { recordStrictModeErrors( toParseError: DeferredStrictError, - { - at, - }: { - at: Position; - }, + { at }: { at: Position }, ) { const index = at.index; @@ -1363,22 +1365,6 @@ export default class Tokenizer extends CommentsParser { } } - // Used to read escaped characters - readEscapedChar(inTemplate: boolean): string | null { - const { ch, pos, lineStart, curLine } = readEscapedChar( - this.input, - this.state.pos, - this.state.lineStart, - this.state.curLine, - inTemplate, - this.errorHandlers_readEscapedChar, - ); - this.state.pos = pos; - this.state.lineStart = lineStart; - this.state.curLine = curLine; - return ch; - } - // Read an identifier, and return it as a string. Sets `this.state.containsEsc` // to whether the word contained a '\u' escape. // @@ -1552,80 +1538,60 @@ export default class Tokenizer extends CommentsParser { } } - errorHandlers_readInt: IntErrorHandlers = { - invalidDigit: (pos, radix) => { - if (this.options.errorRecovery) { - this.state.pos = pos; - this.raise(Errors.InvalidDigit, { - at: this.state.curPosition(), - radix, - }); - // Continue parsing the number as if there was no invalid digit. - return true; - } - return false; - }, - numericSeparatorInEscapeSequence: pos => { - this.state.pos = pos; - this.raise(Errors.NumericSeparatorInEscapeSequence, { - at: this.state.curPosition(), + errorBuilder(error: ParseErrorConstructor<{}>) { + return (pos: number, lineStart: number, curLine: number) => { + this.raise(error, { + at: buildPosition(pos, lineStart, curLine), }); - }, - unexpectedNumericSeparator: pos => { - this.state.pos = pos; - this.raise(Errors.UnexpectedNumericSeparator, { - at: this.state.curPosition(), + }; + } + + errorHandlers_readInt: IntErrorHandlers = { + invalidDigit: (pos, lineStart, curLine, radix) => { + if (!this.options.errorRecovery) return false; + + this.raise(Errors.InvalidDigit, { + at: buildPosition(pos, lineStart, curLine), + radix, }); + // Continue parsing the number as if there was no invalid digit. + return true; }, + numericSeparatorInEscapeSequence: this.errorBuilder( + Errors.NumericSeparatorInEscapeSequence, + ), + unexpectedNumericSeparator: this.errorBuilder( + Errors.UnexpectedNumericSeparator, + ), }; errorHandlers_readCodePoint: CodePointErrorHandlers = { ...this.errorHandlers_readInt, - invalidEscapeSequence: (pos, startPos) => { - this.state.pos = pos; - this.raise(Errors.InvalidEscapeSequence, { - at: createPositionWithColumnOffset( - this.state.curPosition(), - startPos - pos, - ), - }); - }, - invalidCodePoint: pos => { - this.state.pos = pos; - this.raise(Errors.InvalidCodePoint, { at: this.state.curPosition() }); - }, + invalidEscapeSequence: this.errorBuilder(Errors.InvalidEscapeSequence), + invalidCodePoint: this.errorBuilder(Errors.InvalidCodePoint), }; - errorHandlers_readEscapedChar: EscapedCharErrorHandlers = { + errorHandlers_readStringContents_string: StringContentsErrorHandlers = { ...this.errorHandlers_readCodePoint, - strictNumericEscape: pos => { - this.state.pos = pos; + strictNumericEscape: (pos, lineStart, curLine) => { this.recordStrictModeErrors(Errors.StrictNumericEscape, { - at: this.state.curPosition(), + at: buildPosition(pos, lineStart, curLine), }); }, - }; - - errorHandlers_readStringContents_string: StringContentsErrorHandlers = { - ...this.errorHandlers_readEscapedChar, - unterminated: (initialPos, initialLineStart, initialCurLine) => { - this.state.pos = initialPos - 1; // Report the error at the string quote - this.state.lineStart = initialLineStart; - this.state.curLine = initialCurLine; + unterminated: (pos, lineStart, curLine) => { throw this.raise(Errors.UnterminatedString, { - at: this.state.curPosition(), + // Report the error at the string quote + at: buildPosition(pos - 1, lineStart, curLine), }); }, }; errorHandlers_readStringContents_template: StringContentsErrorHandlers = { - ...this.errorHandlers_readEscapedChar, - unterminated: (initialPos, initialLineStart, initialCurLine) => { - this.state.pos = initialPos; // TODO: For strings, we subtract 1 - this.state.lineStart = initialLineStart; - this.state.curLine = initialCurLine; + ...this.errorHandlers_readCodePoint, + strictNumericEscape: this.errorBuilder(Errors.StrictNumericEscape), + unterminated: (pos, lineStart, curLine) => { throw this.raise(Errors.UnterminatedTemplate, { - at: this.state.curPosition(), + at: buildPosition(pos, lineStart, curLine), }); }, }; diff --git a/packages/babel-parser/test/fixtures/es2015/template/error-after-newline/input.js b/packages/babel-parser/test/fixtures/es2015/template/error-after-newline/input.js new file mode 100644 index 000000000000..cb5d7265e799 --- /dev/null +++ b/packages/babel-parser/test/fixtures/es2015/template/error-after-newline/input.js @@ -0,0 +1,3 @@ +` +\u{12_34} +`; \ No newline at end of file diff --git a/packages/babel-parser/test/fixtures/es2015/template/error-after-newline/output.json b/packages/babel-parser/test/fixtures/es2015/template/error-after-newline/output.json new file mode 100644 index 000000000000..993296bc62c2 --- /dev/null +++ b/packages/babel-parser/test/fixtures/es2015/template/error-after-newline/output.json @@ -0,0 +1,36 @@ +{ + "type": "File", + "start":0,"end":14,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":3,"column":2,"index":14}}, + "errors": [ + "SyntaxError: Numeric separators are not allowed inside unicode escape sequences or hex escape sequences. (2:5)" + ], + "program": { + "type": "Program", + "start":0,"end":14,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":3,"column":2,"index":14}}, + "sourceType": "script", + "interpreter": null, + "body": [ + { + "type": "ExpressionStatement", + "start":0,"end":14,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":3,"column":2,"index":14}}, + "expression": { + "type": "TemplateLiteral", + "start":0,"end":13,"loc":{"start":{"line":1,"column":0,"index":0},"end":{"line":3,"column":1,"index":13}}, + "expressions": [], + "quasis": [ + { + "type": "TemplateElement", + "start":1,"end":12,"loc":{"start":{"line":1,"column":1,"index":1},"end":{"line":3,"column":0,"index":12}}, + "value": { + "raw": "\n\\u{12_34}\n", + "cooked": "\nሴ\n" + }, + "tail": true + } + ] + } + } + ], + "directives": [] + } +}