From 4d69be1db84782b717f78b96a264f4e0972fa2d4 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Thu, 1 Dec 2022 23:28:32 +0900 Subject: [PATCH] feat: support for modifier --- package.json | 1 + scripts/update-fixtures.ts | 4 + src/ast.ts | 24 + src/ecma-versions.ts | 1 + src/parser.ts | 85 +++- src/reader.ts | 10 - src/validator.ts | 363 +++++++++++--- src/visitor.ts | 39 ++ .../parser/literal/basic-valid-2015-u.json | 2 + .../parser/literal/basic-valid-2015.json | 4 + test/fixtures/parser/literal/basic-valid.json | 4 + test/fixtures/parser/literal/flags.json | 6 + .../literal/modifiers-invalid-2022.json | 26 + .../literal/modifiers-invalid-2024.json | 44 ++ .../parser/literal/modifiers-valid-2024.json | 448 ++++++++++++++++++ .../named-capturing-group-invalid-2018.json | 4 +- .../named-capturing-group-valid-2018.json | 1 + test/fixtures/visitor/full.json | 76 +++ test/visitor.ts | 4 + tsconfig.json | 2 +- 20 files changed, 1065 insertions(+), 83 deletions(-) create mode 100644 test/fixtures/parser/literal/modifiers-invalid-2022.json create mode 100644 test/fixtures/parser/literal/modifiers-invalid-2024.json create mode 100644 test/fixtures/parser/literal/modifiers-valid-2024.json diff --git a/package.json b/package.json index 6303573..809ba4d 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "clean": "rimraf .temp index.*", "lint": "eslint . --ext .ts", "test": "nyc _mocha \"test/*.ts\" --reporter dot --timeout 10000", + "debug": "mocha --require ts-node/register \"test/*.ts\" --reporter dot --timeout 10000", "update:test": "ts-node scripts/update-fixtures.ts", "update:unicode": "run-s update:unicode:*", "update:unicode:ids": "ts-node scripts/update-unicode-ids.ts", diff --git a/scripts/update-fixtures.ts b/scripts/update-fixtures.ts index cea1a4b..8f54f10 100644 --- a/scripts/update-fixtures.ts +++ b/scripts/update-fixtures.ts @@ -49,6 +49,8 @@ for (const filename of Object.keys(Visitor.fixturesData)) { onCharacterSetEnter: enter, onFlagsEnter: enter, onGroupEnter: enter, + onModifierFlagsEnter: enter, + onModifiersEnter: enter, onPatternEnter: enter, onQuantifierEnter: enter, onRegExpLiteralEnter: enter, @@ -62,6 +64,8 @@ for (const filename of Object.keys(Visitor.fixturesData)) { onCharacterSetLeave: leave, onFlagsLeave: leave, onGroupLeave: leave, + onModifierFlagsLeave: leave, + onModifiersLeave: leave, onPatternLeave: leave, onQuantifierLeave: leave, onRegExpLiteralLeave: leave, diff --git a/src/ast.ts b/src/ast.ts index cabdcf2..514b93a 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -13,6 +13,7 @@ export type BranchNode = | CharacterClassRange | Group | LookaroundAssertion + | Modifiers | Pattern | Quantifier | RegExpLiteral @@ -26,6 +27,7 @@ export type LeafNode = | Character | CharacterSet | Flags + | ModifierFlags /** * The type which includes all atom nodes. @@ -105,6 +107,7 @@ export interface Alternative extends NodeBase { export interface Group extends NodeBase { type: "Group" parent: Alternative | Quantifier + modifiers: Modifiers | null alternatives: Alternative[] } @@ -279,6 +282,27 @@ export interface Backreference extends NodeBase { resolved: CapturingGroup } +/** + * The modifiers. + */ +export interface Modifiers extends NodeBase { + type: "Modifiers" + parent: Group + add: ModifierFlags | null + remove: ModifierFlags | null +} + +/** + * The modifier flags. + */ +export interface ModifierFlags extends NodeBase { + type: "ModifierFlags" + parent: Modifiers + dotAll: boolean + ignoreCase: boolean + multiline: boolean +} + /** * The flags. */ diff --git a/src/ecma-versions.ts b/src/ecma-versions.ts index db55c45..1c12885 100644 --- a/src/ecma-versions.ts +++ b/src/ecma-versions.ts @@ -9,3 +9,4 @@ export type EcmaVersion = | 2021 | 2022 | 2023 + | 2024 diff --git a/src/parser.ts b/src/parser.ts index 6f957c9..97549bc 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -11,6 +11,7 @@ import type { LookaroundAssertion, Pattern, Quantifier, + Modifiers, } from "./ast" import type { EcmaVersion } from "./ecma-versions" import { HYPHEN_MINUS } from "./unicode" @@ -22,6 +23,7 @@ type AppendableNode = | CharacterClass | Group | LookaroundAssertion + | Modifiers | Pattern const DUMMY_PATTERN: Pattern = {} as Pattern @@ -45,7 +47,7 @@ class RegExpParserState { public constructor(options?: RegExpParser.Options) { this.strict = Boolean(options?.strict) - this.ecmaVersion = options?.ecmaVersion ?? 2023 + this.ecmaVersion = options?.ecmaVersion ?? 2024 } public get pattern(): Pattern { @@ -172,6 +174,7 @@ class RegExpParserState { start, end: start, raw: "", + modifiers: null, alternatives: [], } parent.elements.push(this._node) @@ -188,6 +191,85 @@ class RegExpParserState { this._node = node.parent } + public onModifiersEnter(start: number): void { + const parent = this._node + if (parent.type !== "Group") { + throw new Error("UnknownError") + } + + this._node = { + type: "Modifiers", + parent, + start, + end: start, + raw: "", + add: null, + remove: null, + } + parent.modifiers = this._node + } + + public onModifiersLeave(start: number, end: number): void { + const node = this._node + if (node.type !== "Modifiers" || node.parent.type !== "Group") { + throw new Error("UnknownError") + } + + node.end = end + node.raw = this.source.slice(start, end) + this._node = node.parent + } + + public onAddModifiers( + start: number, + end: number, + { + ignoreCase, + multiline, + dotAll, + }: { ignoreCase: boolean; multiline: boolean; dotAll: boolean }, + ): void { + const parent = this._node + if (parent.type !== "Modifiers") { + throw new Error("UnknownError") + } + parent.add = { + type: "ModifierFlags", + parent, + start, + end, + raw: this.source.slice(start, end), + ignoreCase, + multiline, + dotAll, + } + } + + public onRemoveModifiers( + start: number, + end: number, + { + ignoreCase, + multiline, + dotAll, + }: { ignoreCase: boolean; multiline: boolean; dotAll: boolean }, + ): void { + const parent = this._node + if (parent.type !== "Modifiers") { + throw new Error("UnknownError") + } + parent.remove = { + type: "ModifierFlags", + parent, + start, + end, + raw: this.source.slice(start, end), + ignoreCase, + multiline, + dotAll, + } + } + public onCapturingGroupEnter(start: number, name: string | null): void { const parent = this._node if (parent.type !== "Alternative") { @@ -526,6 +608,7 @@ export namespace RegExpParser { * - `2019`, `2020`, and `2021` added more valid Unicode Property Escapes. * - `2022` added `d` flag. * - `2023` added more valid Unicode Property Escapes. + * - `2024` added modifier. */ ecmaVersion?: EcmaVersion } diff --git a/src/reader.ts b/src/reader.ts index e087616..c96da90 100644 --- a/src/reader.ts +++ b/src/reader.ts @@ -124,14 +124,4 @@ export class Reader { } return false } - - public eat3(cp1: number, cp2: number, cp3: number): boolean { - if (this._cp1 === cp1 && this._cp2 === cp2 && this._cp3 === cp3) { - this.advance() - this.advance() - this.advance() - return true - } - return false - } } diff --git a/src/validator.ts b/src/validator.ts index b029969..5f36ae2 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,3 +1,4 @@ +import type { Flags } from "./ast" import type { EcmaVersion } from "./ecma-versions" import { Reader } from "./reader" import { RegExpSyntaxError } from "./regexp-syntax-error" @@ -75,6 +76,23 @@ import { isValidUnicode, } from "./unicode" +const FLAG_PROP_TO_CODEPOINT = { + global: LATIN_SMALL_LETTER_G, + ignoreCase: LATIN_SMALL_LETTER_I, + multiline: LATIN_SMALL_LETTER_M, + unicode: LATIN_SMALL_LETTER_U, + sticky: LATIN_SMALL_LETTER_Y, + dotAll: LATIN_SMALL_LETTER_S, + hasIndices: LATIN_SMALL_LETTER_D, +} as const +const FLAG_CODEPOINT_TO_PROP: Record = + Object.fromEntries( + Object.entries(FLAG_PROP_TO_CODEPOINT).map(([k, v]) => [v, k]), + ) as never +type FlagProp = keyof typeof FLAG_PROP_TO_CODEPOINT +type FlagCodePoint = typeof FLAG_PROP_TO_CODEPOINT[FlagProp] +type FlagsRecord = Omit + function isSyntaxCharacter(cp: number): boolean { return ( cp === CIRCUMFLEX_ACCENT || @@ -134,6 +152,7 @@ export namespace RegExpValidator { * - `2019`, `2020`, and `2021` added more valid Unicode Property Escapes. * - `2022` added `d` flag. * - `2023` added more valid Unicode Property Escapes. + * - `2024` added modifier. */ ecmaVersion?: EcmaVersion @@ -255,6 +274,57 @@ export namespace RegExpValidator { */ onGroupLeave?: (start: number, end: number) => void + /** + * A function that is called when the validator entered a modifiers. + * @param start The 0-based index of the first character. + */ + onModifiersEnter?: (start: number) => void + + /** + * A function that is called when the validator left a modifiers. + * @param start The 0-based index of the first character. + * @param end The next 0-based index of the last character. + */ + onModifiersLeave?: (start: number, end: number) => void + + /** + * A function that is called when the validator found an add modifiers. + * @param start The 0-based index of the first character. + * @param end The next 0-based index of the last character. + * @param flags flags. + * @param flags.ignoreCase `i` flag. + * @param flags.multiline `m` flag. + * @param flags.dotAll `s` flag. + */ + onAddModifiers?: ( + start: number, + end: number, + flags: { + ignoreCase: boolean + multiline: boolean + dotAll: boolean + }, + ) => void + + /** + * A function that is called when the validator found a remove modifiers. + * @param start The 0-based index of the first character. + * @param end The next 0-based index of the last character. + * @param flags flags. + * @param flags.ignoreCase `i` flag. + * @param flags.multiline `m` flag. + * @param flags.dotAll `s` flag. + */ + onRemoveModifiers?: ( + start: number, + end: number, + flags: { + ignoreCase: boolean + multiline: boolean + dotAll: boolean + }, + ) => void + /** * A function that is called when the validator entered a capturing group. * @param start The 0-based index of the first character. @@ -517,61 +587,8 @@ export class RegExpValidator { start = 0, end: number = source.length, ): void { - const existingFlags = new Set() - let global = false - let ignoreCase = false - let multiline = false - let sticky = false - let unicode = false - let dotAll = false - let hasIndices = false - for (let i = start; i < end; ++i) { - const flag = source.charCodeAt(i) - - if (existingFlags.has(flag)) { - this.raise(`Duplicated flag '${source[i]}'`) - } - existingFlags.add(flag) - - if (flag === LATIN_SMALL_LETTER_G) { - global = true - } else if (flag === LATIN_SMALL_LETTER_I) { - ignoreCase = true - } else if (flag === LATIN_SMALL_LETTER_M) { - multiline = true - } else if ( - flag === LATIN_SMALL_LETTER_U && - this.ecmaVersion >= 2015 - ) { - unicode = true - } else if ( - flag === LATIN_SMALL_LETTER_Y && - this.ecmaVersion >= 2015 - ) { - sticky = true - } else if ( - flag === LATIN_SMALL_LETTER_S && - this.ecmaVersion >= 2018 - ) { - dotAll = true - } else if ( - flag === LATIN_SMALL_LETTER_D && - this.ecmaVersion >= 2022 - ) { - hasIndices = true - } else { - this.raise(`Invalid flag '${source[i]}'`) - } - } - this.onRegExpFlags(start, end, { - global, - ignoreCase, - multiline, - unicode, - sticky, - dotAll, - hasIndices, - }) + const flags = this.parseFlags(source, start, end) + this.onRegExpFlags(start, end, flags) } /** @@ -615,7 +632,7 @@ export class RegExpValidator { } private get ecmaVersion() { - return this._options.ecmaVersion ?? 2023 + return this._options.ecmaVersion ?? 2024 } private onLiteralEnter(start: number): void { @@ -714,6 +731,38 @@ export class RegExpValidator { } } + private onModifiersEnter(start: number): void { + if (this._options.onModifiersEnter) { + this._options.onModifiersEnter(start) + } + } + + private onModifiersLeave(start: number, end: number): void { + if (this._options.onModifiersLeave) { + this._options.onModifiersLeave(start, end) + } + } + + private onAddModifiers( + start: number, + end: number, + flags: { ignoreCase: boolean; multiline: boolean; dotAll: boolean }, + ): void { + if (this._options.onAddModifiers) { + this._options.onAddModifiers(start, end, flags) + } + } + + private onRemoveModifiers( + start: number, + end: number, + flags: { ignoreCase: boolean; multiline: boolean; dotAll: boolean }, + ): void { + if (this._options.onRemoveModifiers) { + this._options.onRemoveModifiers(start, end, flags) + } + } + private onCapturingGroupEnter(start: number, name: string | null): void { if (this._options.onCapturingGroupEnter) { this._options.onCapturingGroupEnter(start, name) @@ -912,10 +961,6 @@ export class RegExpValidator { return this._reader.eat2(cp1, cp2) } - private eat3(cp1: number, cp2: number, cp3: number): boolean { - return this._reader.eat3(cp1, cp2, cp3) - } - // #endregion private raise(message: string): never { @@ -1280,8 +1325,9 @@ export class RegExpValidator { * `.` * `\\` AtomEscape[?U, ?N] * CharacterClass[?U] - * `(?:` Disjunction[?U, ?N] ) * `(` GroupSpecifier[?U] Disjunction[?U, ?N] `)` + * `(?` RegularExpressionFlags `:` Disjunction[?U, ?N] `)` + * `(?` RegularExpressionFlags `-` RegularExpressionFlags `:` Disjunction[?U, ?N] `)` * ``` * @returns `true` if it consumed the next characters successfully. */ @@ -1291,8 +1337,8 @@ export class RegExpValidator { this.consumeDot() || this.consumeReverseSolidusAtomEscape() || this.consumeCharacterClass() || - this.consumeUncapturingGroup() || - this.consumeCapturingGroup() + this.consumeCapturingGroup() || + this.consumeUncapturingGroup() ) } @@ -1332,14 +1378,26 @@ export class RegExpValidator { /** * Validate the next characters as the following alternatives if possible. * ``` - * `(?:` Disjunction[?U, ?N] ) + * `(?` RegularExpressionFlags `:` Disjunction[?U, ?N] `)` + * `(?` RegularExpressionFlags `-` RegularExpressionFlags `:` Disjunction[?U, ?N] `)` + * RegularExpressionFlags :: + * [empty] + * RegularExpressionFlags IdentifierPartChar * ``` * @returns `true` if it consumed the next characters successfully. */ private consumeUncapturingGroup(): boolean { const start = this.index - if (this.eat3(LEFT_PARENTHESIS, QUESTION_MARK, COLON)) { + if (this.eat2(LEFT_PARENTHESIS, QUESTION_MARK)) { this.onGroupEnter(start) + if (this.ecmaVersion >= 2024) { + this.consumeModifiers() + } + + if (!this.eat(COLON)) { + this.rewind(start + 1) // Throw an error at the question mark position. + this.raise("Invalid group") + } this.consumeDisjunction() if (!this.eat(RIGHT_PARENTHESIS)) { this.raise("Unterminated group") @@ -1350,6 +1408,55 @@ export class RegExpValidator { return false } + /** + * Validate the next characters as the following alternatives if possible. + * ``` + * RegularExpressionFlags + * RegularExpressionFlags `-` RegularExpressionFlags + * ``` + */ + private consumeModifiers(): boolean { + const start = this.index + + if (this.eatModifiers()) { + this.onModifiersEnter(start) + const addModifiers = this.parseModifiers(start, this.index) + this.onAddModifiers(start, this.index, addModifiers) + if (this.eat(HYPHEN_MINUS)) { + const modifiersStart = this.index + if (this.eatModifiers()) { + const modifiers = this.parseModifiers( + modifiersStart, + this.index, + addModifiers, + ) + this.onRemoveModifiers( + modifiersStart, + this.index, + modifiers, + ) + } + } + this.onModifiersLeave(start, this.index) + return true + } else if (this.eat(HYPHEN_MINUS)) { + this.onModifiersEnter(start) + const modifiersStart = this.index + if (this.eatModifiers()) { + const modifiers = this.parseModifiers( + modifiersStart, + this.index, + ) + this.onRemoveModifiers(modifiersStart, this.index, modifiers) + } else { + this.raise("Invalid empty flags") + } + this.onModifiersLeave(start, this.index) + return true + } + return false + } + /** * Validate the next characters as the following alternatives if possible. * ``` @@ -1364,9 +1471,13 @@ export class RegExpValidator { if (this.ecmaVersion >= 2018) { if (this.consumeGroupSpecifier()) { name = this._lastStrValue + } else if (this.currentCodePoint === QUESTION_MARK) { + this.rewind(start) + return false } } else if (this.currentCodePoint === QUESTION_MARK) { - this.raise("Invalid group") + this.rewind(start) + return false } this.onCapturingGroupEnter(start, name) @@ -1390,8 +1501,9 @@ export class RegExpValidator { * `\` AtomEscape[~U, ?N] * `\` [lookahead = c] * CharacterClass[~U] - * `(?:` Disjunction[~U, ?N] `)` * `(` Disjunction[~U, ?N] `)` + * `(?` RegularExpressionFlags `:` Disjunction[?U, ?N] `)` + * `(?` RegularExpressionFlags `-` RegularExpressionFlags `:` Disjunction[?U, ?N] `)` * InvalidBracedQuantifier * ExtendedPatternCharacter * ``` @@ -1403,8 +1515,8 @@ export class RegExpValidator { this.consumeReverseSolidusAtomEscape() || this.consumeReverseSolidusFollowedByC() || this.consumeCharacterClass() || - this.consumeUncapturingGroup() || this.consumeCapturingGroup() || + this.consumeUncapturingGroup() || this.consumeInvalidBracedQuantifier() || this.consumeExtendedPatternCharacter() ) @@ -1513,6 +1625,7 @@ export class RegExpValidator { * @returns `true` if the group name existed. */ private consumeGroupSpecifier(): boolean { + const start = this.index if (this.eat(QUESTION_MARK)) { if (this.eatGroupName()) { if (!this._groupNames.has(this._lastStrValue)) { @@ -1521,7 +1634,7 @@ export class RegExpValidator { } this.raise("Duplicate capture group name") } - this.raise("Invalid group") + this.rewind(start) } return false } @@ -2550,4 +2663,116 @@ export class RegExpValidator { } return true } + + /** + * Eat the next characters as the following alternatives. + * ``` + * RegularExpressionFlags :: + * [empty] + * RegularExpressionFlags IdentifierPartChar + * ``` + * @returns `true` if it ate the next characters successfully. + */ + private eatModifiers(): boolean { + let ate = false + while (isRegExpIdentifierPart(this.currentCodePoint)) { + this.advance() + ate = true + } + return ate + } + + /** + * Parse a regexp modifiers. E.g. "ims" + * @param start The start index in the source code. + * @param end The end index in the source code. + */ + private parseModifiers( + start: number, + end: number, + alreadyUsedFlags?: { + ignoreCase: boolean + multiline: boolean + dotAll: boolean + }, + ) { + const { ignoreCase, multiline, dotAll } = this.parseFlags( + this.source, + start, + end, + { + modifiers: true, + alreadyUsedFlags, + }, + ) + + return { ignoreCase, multiline, dotAll } + } + + /** + * Parse a regexp flags. E.g. "ims" + * @param start The start index in the source code. + * @param end The end index in the source code. + */ + private parseFlags( + source: string, + start: number, + end: number, + options?: { + modifiers?: boolean + alreadyUsedFlags?: Partial + }, + ): FlagsRecord { + const flags = { + global: false, + ignoreCase: false, + multiline: false, + unicode: false, + sticky: false, + dotAll: false, + hasIndices: false, + } + + const alreadyUsedFlags = new Set() + const validFlags = new Set() + if (options?.modifiers) { + if (options?.alreadyUsedFlags) { + for (const [flagProp] of Object.entries( + options.alreadyUsedFlags, + ).filter(([, enable]) => enable) as [FlagProp, boolean][]) { + alreadyUsedFlags.add(FLAG_PROP_TO_CODEPOINT[flagProp]) + } + } + validFlags.add(LATIN_SMALL_LETTER_I) + validFlags.add(LATIN_SMALL_LETTER_M) + validFlags.add(LATIN_SMALL_LETTER_S) + } else { + validFlags.add(LATIN_SMALL_LETTER_G) + validFlags.add(LATIN_SMALL_LETTER_I) + validFlags.add(LATIN_SMALL_LETTER_M) + if (this.ecmaVersion >= 2015) { + validFlags.add(LATIN_SMALL_LETTER_U) + validFlags.add(LATIN_SMALL_LETTER_Y) + if (this.ecmaVersion >= 2018) { + validFlags.add(LATIN_SMALL_LETTER_S) + if (this.ecmaVersion >= 2021) { + validFlags.add(LATIN_SMALL_LETTER_D) + } + } + } + } + for (let i = start; i < end; ++i) { + const flag = source.charCodeAt(i) as FlagCodePoint + if (validFlags.has(flag)) { + const prop = FLAG_CODEPOINT_TO_PROP[flag] + if (flags[prop] || alreadyUsedFlags.has(flag)) { + this.raise(`Duplicated flag '${source[i]}'`) + } + flags[prop] = true + } else { + this.raise(`Invalid flag '${source[i]}'`) + } + } + return flags + } } diff --git a/src/visitor.ts b/src/visitor.ts index 83d23a6..6a14e32 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -9,6 +9,8 @@ import type { CharacterSet, Flags, Group, + ModifierFlags, + Modifiers, Node, Pattern, Quantifier, @@ -65,6 +67,12 @@ export class RegExpVisitor { case "Group": this.visitGroup(node) break + case "Modifiers": + this.visitModifiers(node) + break + case "ModifierFlags": + this.visitModifierFlags(node) + break case "Pattern": this.visitPattern(node) break @@ -174,12 +182,39 @@ export class RegExpVisitor { if (this._handlers.onGroupEnter) { this._handlers.onGroupEnter(node) } + if (node.modifiers) { + this.visit(node.modifiers) + } node.alternatives.forEach(this.visit, this) if (this._handlers.onGroupLeave) { this._handlers.onGroupLeave(node) } } + private visitModifiers(node: Modifiers): void { + if (this._handlers.onModifiersEnter) { + this._handlers.onModifiersEnter(node) + } + if (node.add) { + this.visit(node.add) + } + if (node.remove) { + this.visit(node.remove) + } + if (this._handlers.onModifiersLeave) { + this._handlers.onModifiersLeave(node) + } + } + + private visitModifierFlags(node: ModifierFlags): void { + if (this._handlers.onModifierFlagsEnter) { + this._handlers.onModifierFlagsEnter(node) + } + if (this._handlers.onModifierFlagsLeave) { + this._handlers.onModifierFlagsLeave(node) + } + } + private visitPattern(node: Pattern): void { if (this._handlers.onPatternEnter) { this._handlers.onPatternEnter(node) @@ -234,6 +269,10 @@ export namespace RegExpVisitor { onFlagsLeave?: (node: Flags) => void onGroupEnter?: (node: Group) => void onGroupLeave?: (node: Group) => void + onModifierFlagsEnter?: (node: ModifierFlags) => void + onModifierFlagsLeave?: (node: ModifierFlags) => void + onModifiersEnter?: (node: Modifiers) => void + onModifiersLeave?: (node: Modifiers) => void onPatternEnter?: (node: Pattern) => void onPatternLeave?: (node: Pattern) => void onQuantifierEnter?: (node: Quantifier) => void diff --git a/test/fixtures/parser/literal/basic-valid-2015-u.json b/test/fixtures/parser/literal/basic-valid-2015-u.json index af92810..9f9a31f 100644 --- a/test/fixtures/parser/literal/basic-valid-2015-u.json +++ b/test/fixtures/parser/literal/basic-valid-2015-u.json @@ -2486,6 +2486,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -11055,6 +11056,7 @@ "start": 48, "end": 67, "raw": "(?:\\.[a-zA-Z0-9-]+)", + "modifiers": null, "alternatives": [ { "type": "Alternative", diff --git a/test/fixtures/parser/literal/basic-valid-2015.json b/test/fixtures/parser/literal/basic-valid-2015.json index 18277a2..3b57f8b 100644 --- a/test/fixtures/parser/literal/basic-valid-2015.json +++ b/test/fixtures/parser/literal/basic-valid-2015.json @@ -3876,6 +3876,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -4028,6 +4029,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -5124,6 +5126,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -16689,6 +16692,7 @@ "start": 48, "end": 67, "raw": "(?:\\.[a-zA-Z0-9-]+)", + "modifiers": null, "alternatives": [ { "type": "Alternative", diff --git a/test/fixtures/parser/literal/basic-valid.json b/test/fixtures/parser/literal/basic-valid.json index d5d0136..817cd15 100644 --- a/test/fixtures/parser/literal/basic-valid.json +++ b/test/fixtures/parser/literal/basic-valid.json @@ -3876,6 +3876,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -4028,6 +4029,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -5124,6 +5126,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -16689,6 +16692,7 @@ "start": 48, "end": 67, "raw": "(?:\\.[a-zA-Z0-9-]+)", + "modifiers": null, "alternatives": [ { "type": "Alternative", diff --git a/test/fixtures/parser/literal/flags.json b/test/fixtures/parser/literal/flags.json index 8e2a7bc..abc3ffe 100644 --- a/test/fixtures/parser/literal/flags.json +++ b/test/fixtures/parser/literal/flags.json @@ -31,6 +31,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -89,6 +90,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -147,6 +149,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -205,6 +208,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -263,6 +267,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -321,6 +326,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", diff --git a/test/fixtures/parser/literal/modifiers-invalid-2022.json b/test/fixtures/parser/literal/modifiers-invalid-2022.json new file mode 100644 index 0000000..4790e56 --- /dev/null +++ b/test/fixtures/parser/literal/modifiers-invalid-2022.json @@ -0,0 +1,26 @@ +{ + "options": { + "strict": false, + "ecmaVersion": 2022 + }, + "patterns": { + "/(?ims-ims:sub_expression)/": { + "error": { + "message": "Invalid regular expression: /(?ims-ims:sub_expression)/: Invalid group", + "index": 2 + } + }, + "/(?ims:sub_expression)/": { + "error": { + "message": "Invalid regular expression: /(?ims:sub_expression)/: Invalid group", + "index": 2 + } + }, + "/(?-ims:sub_expression)/": { + "error": { + "message": "Invalid regular expression: /(?-ims:sub_expression)/: Invalid group", + "index": 2 + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/parser/literal/modifiers-invalid-2024.json b/test/fixtures/parser/literal/modifiers-invalid-2024.json new file mode 100644 index 0000000..3492131 --- /dev/null +++ b/test/fixtures/parser/literal/modifiers-invalid-2024.json @@ -0,0 +1,44 @@ +{ + "options": { + "strict": false, + "ecmaVersion": 2024 + }, + "patterns": { + "/(?-:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?-:sub_expression)?/: Invalid empty flags", + "index": 4 + } + }, + "/(?unknown:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?unknown:sub_expression)?/: Invalid flag 'u'", + "index": 10 + } + }, + "/(?i-unknown:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?i-unknown:sub_expression)?/: Invalid flag 'u'", + "index": 12 + } + }, + "/(?ii:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?ii:sub_expression)?/: Duplicated flag 'i'", + "index": 5 + } + }, + "/(?-ii:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?-ii:sub_expression)?/: Duplicated flag 'i'", + "index": 6 + } + }, + "/(?i-i:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?i-i:sub_expression)?/: Duplicated flag 'i'", + "index": 6 + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/parser/literal/modifiers-valid-2024.json b/test/fixtures/parser/literal/modifiers-valid-2024.json new file mode 100644 index 0000000..d2199d8 --- /dev/null +++ b/test/fixtures/parser/literal/modifiers-valid-2024.json @@ -0,0 +1,448 @@ +{ + "options": { + "strict": false, + "ecmaVersion": 2024 + }, + "patterns": { + "/(?i-m:p)/": { + "ast": { + "type": "RegExpLiteral", + "parent": null, + "start": 0, + "end": 10, + "raw": "/(?i-m:p)/", + "pattern": { + "type": "Pattern", + "parent": "♻️..", + "start": 1, + "end": 9, + "raw": "(?i-m:p)", + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 1, + "end": 9, + "raw": "(?i-m:p)", + "elements": [ + { + "type": "Group", + "parent": "♻️../..", + "start": 1, + "end": 9, + "raw": "(?i-m:p)", + "modifiers": { + "type": "Modifiers", + "parent": "♻️..", + "start": 3, + "end": 6, + "raw": "i-m", + "add": { + "type": "ModifierFlags", + "parent": "♻️..", + "start": 3, + "end": 4, + "raw": "i", + "ignoreCase": true, + "multiline": false, + "dotAll": false + }, + "remove": { + "type": "ModifierFlags", + "parent": "♻️..", + "start": 5, + "end": 6, + "raw": "m", + "ignoreCase": false, + "multiline": true, + "dotAll": false + } + }, + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 7, + "end": 8, + "raw": "p", + "elements": [ + { + "type": "Character", + "parent": "♻️../..", + "start": 7, + "end": 8, + "raw": "p", + "value": 112 + } + ] + } + ] + } + ] + } + ] + }, + "flags": { + "type": "Flags", + "parent": "♻️..", + "start": 10, + "end": 10, + "raw": "", + "global": false, + "ignoreCase": false, + "multiline": false, + "unicode": false, + "sticky": false, + "dotAll": false, + "hasIndices": false + } + } + }, + "/(?ims:p)/u": { + "ast": { + "type": "RegExpLiteral", + "parent": null, + "start": 0, + "end": 11, + "raw": "/(?ims:p)/u", + "pattern": { + "type": "Pattern", + "parent": "♻️..", + "start": 1, + "end": 9, + "raw": "(?ims:p)", + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 1, + "end": 9, + "raw": "(?ims:p)", + "elements": [ + { + "type": "Group", + "parent": "♻️../..", + "start": 1, + "end": 9, + "raw": "(?ims:p)", + "modifiers": { + "type": "Modifiers", + "parent": "♻️..", + "start": 3, + "end": 6, + "raw": "ims", + "add": { + "type": "ModifierFlags", + "parent": "♻️..", + "start": 3, + "end": 6, + "raw": "ims", + "ignoreCase": true, + "multiline": true, + "dotAll": true + }, + "remove": null + }, + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 7, + "end": 8, + "raw": "p", + "elements": [ + { + "type": "Character", + "parent": "♻️../..", + "start": 7, + "end": 8, + "raw": "p", + "value": 112 + } + ] + } + ] + } + ] + } + ] + }, + "flags": { + "type": "Flags", + "parent": "♻️..", + "start": 10, + "end": 11, + "raw": "u", + "global": false, + "ignoreCase": false, + "multiline": false, + "unicode": true, + "sticky": false, + "dotAll": false, + "hasIndices": false + } + } + }, + "/(?-ims:p)?/": { + "ast": { + "type": "RegExpLiteral", + "parent": null, + "start": 0, + "end": 12, + "raw": "/(?-ims:p)?/", + "pattern": { + "type": "Pattern", + "parent": "♻️..", + "start": 1, + "end": 11, + "raw": "(?-ims:p)?", + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 1, + "end": 11, + "raw": "(?-ims:p)?", + "elements": [ + { + "type": "Quantifier", + "parent": "♻️../..", + "start": 1, + "end": 11, + "raw": "(?-ims:p)?", + "min": 0, + "max": 1, + "greedy": true, + "element": { + "type": "Group", + "parent": "♻️..", + "start": 1, + "end": 10, + "raw": "(?-ims:p)", + "modifiers": { + "type": "Modifiers", + "parent": "♻️..", + "start": 3, + "end": 7, + "raw": "-ims", + "add": null, + "remove": { + "type": "ModifierFlags", + "parent": "♻️..", + "start": 4, + "end": 7, + "raw": "ims", + "ignoreCase": true, + "multiline": true, + "dotAll": true + } + }, + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 8, + "end": 9, + "raw": "p", + "elements": [ + { + "type": "Character", + "parent": "♻️../..", + "start": 8, + "end": 9, + "raw": "p", + "value": 112 + } + ] + } + ] + } + } + ] + } + ] + }, + "flags": { + "type": "Flags", + "parent": "♻️..", + "start": 12, + "end": 12, + "raw": "", + "global": false, + "ignoreCase": false, + "multiline": false, + "unicode": false, + "sticky": false, + "dotAll": false, + "hasIndices": false + } + } + }, + "/(?:no-modifiers)?/": { + "ast": { + "type": "RegExpLiteral", + "parent": null, + "start": 0, + "end": 19, + "raw": "/(?:no-modifiers)?/", + "pattern": { + "type": "Pattern", + "parent": "♻️..", + "start": 1, + "end": 18, + "raw": "(?:no-modifiers)?", + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 1, + "end": 18, + "raw": "(?:no-modifiers)?", + "elements": [ + { + "type": "Quantifier", + "parent": "♻️../..", + "start": 1, + "end": 18, + "raw": "(?:no-modifiers)?", + "min": 0, + "max": 1, + "greedy": true, + "element": { + "type": "Group", + "parent": "♻️..", + "start": 1, + "end": 17, + "raw": "(?:no-modifiers)", + "modifiers": null, + "alternatives": [ + { + "type": "Alternative", + "parent": "♻️../..", + "start": 4, + "end": 16, + "raw": "no-modifiers", + "elements": [ + { + "type": "Character", + "parent": "♻️../..", + "start": 4, + "end": 5, + "raw": "n", + "value": 110 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 5, + "end": 6, + "raw": "o", + "value": 111 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 6, + "end": 7, + "raw": "-", + "value": 45 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 7, + "end": 8, + "raw": "m", + "value": 109 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 8, + "end": 9, + "raw": "o", + "value": 111 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 9, + "end": 10, + "raw": "d", + "value": 100 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 10, + "end": 11, + "raw": "i", + "value": 105 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 11, + "end": 12, + "raw": "f", + "value": 102 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 12, + "end": 13, + "raw": "i", + "value": 105 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 13, + "end": 14, + "raw": "e", + "value": 101 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 14, + "end": 15, + "raw": "r", + "value": 114 + }, + { + "type": "Character", + "parent": "♻️../..", + "start": 15, + "end": 16, + "raw": "s", + "value": 115 + } + ] + } + ] + } + } + ] + } + ] + }, + "flags": { + "type": "Flags", + "parent": "♻️..", + "start": 19, + "end": 19, + "raw": "", + "global": false, + "ignoreCase": false, + "multiline": false, + "unicode": false, + "sticky": false, + "dotAll": false, + "hasIndices": false + } + } + } + } +} \ No newline at end of file diff --git a/test/fixtures/parser/literal/named-capturing-group-invalid-2018.json b/test/fixtures/parser/literal/named-capturing-group-invalid-2018.json index ed4d33d..f4ad8a7 100644 --- a/test/fixtures/parser/literal/named-capturing-group-invalid-2018.json +++ b/test/fixtures/parser/literal/named-capturing-group-invalid-2018.json @@ -7,13 +7,13 @@ "/(?a/": { "error": { "message": "Invalid regular expression: /(?a/: Invalid group", - "index": 3 + "index": 2 } }, "/(?a)/": { "error": { "message": "Invalid regular expression: /(?a)/: Invalid group", - "index": 3 + "index": 2 } }, "/(? { onCharacterSetEnter: enter, onFlagsEnter: enter, onGroupEnter: enter, + onModifierFlagsEnter: enter, + onModifiersEnter: enter, onPatternEnter: enter, onQuantifierEnter: enter, onRegExpLiteralEnter: enter, @@ -50,6 +52,8 @@ describe("visitRegExpAST function:", () => { onCharacterSetLeave: leave, onFlagsLeave: leave, onGroupLeave: leave, + onModifierFlagsLeave: leave, + onModifiersLeave: leave, onPatternLeave: leave, onQuantifierLeave: leave, onRegExpLiteralLeave: leave, diff --git a/tsconfig.json b/tsconfig.json index 70ebc9f..689e10d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "lib": [ - "es2015" + "es2019" ], "module": "commonjs", "moduleResolution": "node",