From 0d1b98314f93e14098c9848311cc6b50f4c78487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Ribaudo?= Date: Tue, 26 Jul 2022 13:31:03 +0200 Subject: [PATCH] Fix position of errors in template literals after newlines --- .../babel-helper-string-parser/src/index.ts | 101 ++++++++++--- packages/babel-parser/src/tokenizer/index.ts | 135 +++++++----------- .../template/error-after-newline/output.json | 2 +- 3 files changed, 130 insertions(+), 108 deletions(-) diff --git a/packages/babel-helper-string-parser/src/index.ts b/packages/babel-helper-string-parser/src/index.ts index beb7bccf8381..94ad071fbd8a 100644 --- a/packages/babel-helper-string-parser/src/index.ts +++ b/packages/babel-helper-string-parser/src/index.ts @@ -43,6 +43,7 @@ export type StringContentsErrorHandlers = EscapedCharErrorHandlers & { initialPos: number, initialLineStart: number, initialCurLine: number, + isTemplate: boolean, ): void; }; @@ -54,6 +55,7 @@ export function readStringContents( curLine: number, errors: StringContentsErrorHandlers, ) { + const isTemplate = type === "template"; const initialPos = pos; const initialLineStart = lineStart; const initialCurLine = curLine; @@ -64,7 +66,12 @@ export function readStringContents( const { length } = input; for (;;) { if (pos >= length) { - errors.unterminated(initialPos, initialLineStart, initialCurLine); + errors.unterminated( + initialPos, + initialLineStart, + initialCurLine, + isTemplate, + ); out += input.slice(chunkStart, pos); break; } @@ -115,7 +122,12 @@ export function readStringContents( ++curLine; chunkStart = lineStart = pos; } else { - errors.unterminated(initialPos, initialLineStart, initialCurLine); + errors.unterminated( + initialPos, + initialLineStart, + initialCurLine, + isTemplate, + ); } } else { ++pos; @@ -142,12 +154,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 +183,8 @@ export function readEscapedChar( ({ code, pos } = readHexChar( input, pos, + lineStart, + curLine, 2, false, throwOnInvalid, @@ -180,7 +194,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 +229,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 +254,7 @@ export function readEscapedChar( if (inTemplate) { return res(null); } else { - errors.strictNumericEscape(startPos); + errors.strictNumericEscape(startPos, lineStart, curLine); } } @@ -245,13 +266,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 +282,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 +304,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 +361,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 +388,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 +408,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 +427,8 @@ export function readCodePoint( ({ code, pos } = readHexChar( input, pos, + lineStart, + curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, @@ -385,13 +437,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..39613d47de3a 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, ); @@ -1298,7 +1304,7 @@ export default class Tokenizer extends CommentsParser { this.state.pos + 1, // skip the quote this.state.lineStart, this.state.curLine, - this.errorHandlers_readStringContents_string, + this.errorHandlers_readStringContents, ); this.state.pos = pos + 1; // skip the quote this.state.lineStart = lineStart; @@ -1326,7 +1332,7 @@ export default class Tokenizer extends CommentsParser { this.state.pos + 1, // skip '`' or `}` this.state.lineStart, this.state.curLine, - this.errorHandlers_readStringContents_template, + this.errorHandlers_readStringContents, ); this.state.pos = pos + 1; // skip '`' or `$` this.state.lineStart = lineStart; @@ -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,81 +1538,56 @@ 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: StringContentsErrorHandlers = { ...this.errorHandlers_readCodePoint, - strictNumericEscape: pos => { - this.state.pos = pos; + strictNumericEscape: (pos, lineStart, curLine) => { this.recordStrictModeErrors(Errors.StrictNumericEscape, { - at: this.state.curPosition(), - }); - }, - }; - - 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; - throw this.raise(Errors.UnterminatedString, { - at: this.state.curPosition(), + at: buildPosition(pos, 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; - throw this.raise(Errors.UnterminatedTemplate, { - at: this.state.curPosition(), - }); + unterminated: (pos, lineStart, curLine, isTemplate) => { + const at = buildPosition( + isTemplate ? pos : pos - 1, // Report the error at the string quote + lineStart, + curLine, + ); + throw this.raise( + isTemplate ? Errors.UnterminatedTemplate : Errors.UnterminatedString, + { at }, + ); }, }; } 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 index 94e624380fed..993296bc62c2 100644 --- 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 @@ -2,7 +2,7 @@ "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. (1:7)" + "SyntaxError: Numeric separators are not allowed inside unicode escape sequences or hex escape sequences. (2:5)" ], "program": { "type": "Program",