diff --git a/scripts/update-fixtures.ts b/scripts/update-fixtures.ts index b7ecc12..27715ff 100644 --- a/scripts/update-fixtures.ts +++ b/scripts/update-fixtures.ts @@ -53,6 +53,8 @@ for (const filename of Object.keys(Visitor.fixturesData)) { onExpressionCharacterClassEnter: enter, onFlagsEnter: enter, onGroupEnter: enter, + onModifierFlagsEnter: enter, + onModifiersEnter: enter, onPatternEnter: enter, onQuantifierEnter: enter, onRegExpLiteralEnter: enter, @@ -71,6 +73,8 @@ for (const filename of Object.keys(Visitor.fixturesData)) { onExpressionCharacterClassLeave: 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 361a4fc..d3dae11 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -17,6 +17,7 @@ export type BranchNode = | ExpressionCharacterClass | Group | LookaroundAssertion + | Modifiers | Pattern | Quantifier | RegExpLiteral @@ -31,6 +32,7 @@ export type LeafNode = | Character | CharacterSet | Flags + | ModifierFlags /** * The type which includes all atom nodes. @@ -122,6 +124,7 @@ export interface Alternative extends NodeBase { export interface Group extends NodeBase { type: "Group" parent: Alternative | Quantifier + modifiers: Modifiers | null alternatives: Alternative[] } @@ -432,6 +435,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/parser.ts b/src/parser.ts index fabbb22..8a76f62 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -19,6 +19,7 @@ import type { UnicodeSetsCharacterClass, ExpressionCharacterClass, StringAlternative, + Modifiers, } from "./ast" import type { EcmaVersion } from "./ecma-versions" import { latestEcmaVersion } from "./ecma-versions" @@ -32,6 +33,7 @@ type AppendableNode = | ClassStringDisjunction | Group | LookaroundAssertion + | Modifiers | Pattern | StringAlternative @@ -203,6 +205,7 @@ class RegExpParserState { start, end: start, raw: "", + modifiers: null, alternatives: [], } parent.elements.push(this._node) @@ -219,6 +222,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") { @@ -754,6 +836,7 @@ export namespace RegExpParser { * - `2022` added `d` flag. * - `2023` added more valid Unicode Property Escapes. * - `2024` added `v` flag. + * - `202x` added modifier. */ ecmaVersion?: EcmaVersion } diff --git a/src/validator.ts b/src/validator.ts index 7e19ba8..f3187b1 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 { latestEcmaVersion } from "./ecma-versions" import { Reader } from "./reader" @@ -155,6 +156,24 @@ const CLASS_SET_RESERVED_PUNCTUATOR = new Set([ TILDE, ]) +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, + unicodeSets: LATIN_SMALL_LETTER_V, +} 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 SYNTAX_CHARACTER.has(cp) @@ -239,6 +258,7 @@ export namespace RegExpValidator { * - `2022` added `d` flag. * - `2023` added more valid Unicode Property Escapes. * - `2024` added `v` flag. + * - `202x` added modifier. */ ecmaVersion?: EcmaVersion @@ -362,6 +382,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. @@ -776,68 +847,8 @@ export class RegExpValidator { start: number, end: number, ): 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 - let unicodeSets = false - for (let i = start; i < end; ++i) { - const flag = source.charCodeAt(i) - - if (existingFlags.has(flag)) { - this.raise(`Duplicated flag '${source[i]}'`, { index: start }) - } - 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 if ( - flag === LATIN_SMALL_LETTER_V && - this.ecmaVersion >= 2024 - ) { - unicodeSets = true - } else { - this.raise(`Invalid flag '${source[i]}'`, { index: start }) - } - } - this.onRegExpFlags(start, end, { - global, - ignoreCase, - multiline, - unicode, - sticky, - dotAll, - hasIndices, - unicodeSets, - }) + const flags = this.parseFlags(source, start, end) + this.onRegExpFlags(start, end, flags) } private _parseFlagsOptionToMode( @@ -996,6 +1007,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) @@ -1612,7 +1655,8 @@ export class RegExpValidator { * `\\` AtomEscape[?UnicodeMode, ?UnicodeSetsMode, ?N] * CharacterClass[?UnicodeMode, ?UnicodeSetsMode] * `(` GroupSpecifier[?UnicodeMode] Disjunction[?UnicodeMode, ?UnicodeSetsMode, ?N] `)` - * `(?:` Disjunction[?UnicodeMode, ?UnicodeSetsMode, ?N] `)` + * `(?` RegularExpressionFlags `:` Disjunction[?UnicodeMode, ?UnicodeSetsMode, ?N] `)` + * `(?` RegularExpressionFlags `-` RegularExpressionFlags `:` Disjunction[?UnicodeMode, ?UnicodeSetsMode, ?N] `)` * ``` * @returns `true` if it consumed the next characters successfully. */ @@ -1622,8 +1666,8 @@ export class RegExpValidator { this.consumeDot() || this.consumeReverseSolidusAtomEscape() || Boolean(this.consumeCharacterClass()) || - this.consumeUncapturingGroup() || - this.consumeCapturingGroup() + this.consumeCapturingGroup() || + this.consumeUncapturingGroup() ) } @@ -1663,14 +1707,26 @@ export class RegExpValidator { /** * Validate the next characters as the following alternatives if possible. * ``` - * `(?:` Disjunction[?UnicodeMode, ?UnicodeSetsMode, ?N] ) + * `(?` RegularExpressionFlags `:` Disjunction[?UnicodeMode, ?UnicodeSetsMode, ?N] `)` + * `(?` RegularExpressionFlags `-` RegularExpressionFlags `:` Disjunction[?UnicodeMode, ?UnicodeSetsMode, ?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") @@ -1681,6 +1737,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. * ``` @@ -1695,9 +1800,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) @@ -1721,8 +1830,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 * ``` @@ -1734,8 +1844,8 @@ export class RegExpValidator { this.consumeReverseSolidusAtomEscape() || this.consumeReverseSolidusFollowedByC() || Boolean(this.consumeCharacterClass()) || - this.consumeUncapturingGroup() || this.consumeCapturingGroup() || + this.consumeUncapturingGroup() || this.consumeInvalidBracedQuantifier() || this.consumeExtendedPatternCharacter() ) @@ -1844,6 +1954,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)) { @@ -1852,7 +1963,7 @@ export class RegExpValidator { } this.raise("Duplicate capture group name") } - this.raise("Invalid group") + this.rewind(start) } return false } @@ -3367,4 +3478,122 @@ 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 (isIdentifierPartChar(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._reader.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, + unicodeSets: 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) + if (this.ecmaVersion >= 2024) { + validFlags.add(LATIN_SMALL_LETTER_V) + } + } + } + } + } + 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]}'`, { + index: start, + }) + } + flags[prop] = true + } else { + this.raise(`Invalid flag '${source[i]}'`, { index: start }) + } + } + return flags + } } diff --git a/src/visitor.ts b/src/visitor.ts index f7f8c72..a2cb2a0 100644 --- a/src/visitor.ts +++ b/src/visitor.ts @@ -13,6 +13,8 @@ import type { ExpressionCharacterClass, Flags, Group, + ModifierFlags, + Modifiers, Node, Pattern, Quantifier, @@ -83,6 +85,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 @@ -239,12 +247,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) @@ -321,6 +356,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 5bc7306..9da3859 100644 --- a/test/fixtures/parser/literal/basic-valid-2015-u.json +++ b/test/fixtures/parser/literal/basic-valid-2015-u.json @@ -2515,6 +2515,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -11273,6 +11274,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 fb85c3c..bc489e5 100644 --- a/test/fixtures/parser/literal/basic-valid-2015.json +++ b/test/fixtures/parser/literal/basic-valid-2015.json @@ -3931,6 +3931,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -4085,6 +4086,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -5185,6 +5187,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -17006,6 +17009,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 16341ee..debaeee 100644 --- a/test/fixtures/parser/literal/basic-valid.json +++ b/test/fixtures/parser/literal/basic-valid.json @@ -3931,6 +3931,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -4085,6 +4086,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -5185,6 +5187,7 @@ "start": 1, "end": 6, "raw": "(?:a)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -17006,6 +17009,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 ec87673..52c2834 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", @@ -90,6 +91,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -149,6 +151,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -208,6 +211,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -267,6 +271,7 @@ "start": 1, "end": 5, "raw": "(?:)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -326,6 +331,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..57df5b1 --- /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": 3 + } + }, + "/(?i-unknown:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?i-unknown:sub_expression)?/: Invalid flag 'u'", + "index": 5 + } + }, + "/(?ii:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?ii:sub_expression)?/: Duplicated flag 'i'", + "index": 3 + } + }, + "/(?-ii:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?-ii:sub_expression)?/: Duplicated flag 'i'", + "index": 4 + } + }, + "/(?i-i:sub_expression)?/": { + "error": { + "message": "Invalid regular expression: /(?i-i:sub_expression)?/: Duplicated flag 'i'", + "index": 5 + } + } + } +} \ 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..4d9ee71 --- /dev/null +++ b/test/fixtures/parser/literal/modifiers-valid-2024.json @@ -0,0 +1,452 @@ +{ + "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, + "unicodeSets": 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, + "unicodeSets": 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, + "unicodeSets": 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, + "unicodeSets": 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 } }, "/(?)", + "modifiers": null, "alternatives": [ { "type": "Alternative", diff --git a/test/fixtures/parser/literal/test262/regexp-lookbehind.json b/test/fixtures/parser/literal/test262/regexp-lookbehind.json index a6bc9f9..5bd25a1 100644 --- a/test/fixtures/parser/literal/test262/regexp-lookbehind.json +++ b/test/fixtures/parser/literal/test262/regexp-lookbehind.json @@ -2831,6 +2831,7 @@ "start": 6, "end": 16, "raw": "(?:b\\d{2})", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -3112,6 +3113,7 @@ "start": 5, "end": 12, "raw": "(?:\\1b)", + "modifiers": null, "alternatives": [ { "type": "Alternative", @@ -3255,6 +3257,7 @@ "start": 5, "end": 13, "raw": "(?:\\1|b)", + "modifiers": null, "alternatives": [ { "type": "Alternative", diff --git a/test/fixtures/visitor/full.json b/test/fixtures/visitor/full.json index 6c435c0..bad9fad 100644 --- a/test/fixtures/visitor/full.json +++ b/test/fixtures/visitor/full.json @@ -8138,6 +8138,110 @@ "enter:Flags:v", "leave:Flags:v", "leave:RegExpLiteral:/[\\q{a|bc|def}]/v" + ], + "/(?i-m:p)/": [ + "enter:RegExpLiteral:/(?i-m:p)/", + "enter:Pattern:(?i-m:p)", + "enter:Alternative:(?i-m:p)", + "enter:Group:(?i-m:p)", + "enter:Modifiers:i-m", + "enter:ModifierFlags:i", + "leave:ModifierFlags:i", + "enter:ModifierFlags:m", + "leave:ModifierFlags:m", + "leave:Modifiers:i-m", + "enter:Alternative:p", + "enter:Character:p", + "leave:Character:p", + "leave:Alternative:p", + "leave:Group:(?i-m:p)", + "leave:Alternative:(?i-m:p)", + "leave:Pattern:(?i-m:p)", + "enter:Flags:", + "leave:Flags:", + "leave:RegExpLiteral:/(?i-m:p)/" + ], + "/(?ims:p)/u": [ + "enter:RegExpLiteral:/(?ims:p)/u", + "enter:Pattern:(?ims:p)", + "enter:Alternative:(?ims:p)", + "enter:Group:(?ims:p)", + "enter:Modifiers:ims", + "enter:ModifierFlags:ims", + "leave:ModifierFlags:ims", + "leave:Modifiers:ims", + "enter:Alternative:p", + "enter:Character:p", + "leave:Character:p", + "leave:Alternative:p", + "leave:Group:(?ims:p)", + "leave:Alternative:(?ims:p)", + "leave:Pattern:(?ims:p)", + "enter:Flags:u", + "leave:Flags:u", + "leave:RegExpLiteral:/(?ims:p)/u" + ], + "/(?-ims:p)?/": [ + "enter:RegExpLiteral:/(?-ims:p)?/", + "enter:Pattern:(?-ims:p)?", + "enter:Alternative:(?-ims:p)?", + "enter:Quantifier:(?-ims:p)?", + "enter:Group:(?-ims:p)", + "enter:Modifiers:-ims", + "enter:ModifierFlags:ims", + "leave:ModifierFlags:ims", + "leave:Modifiers:-ims", + "enter:Alternative:p", + "enter:Character:p", + "leave:Character:p", + "leave:Alternative:p", + "leave:Group:(?-ims:p)", + "leave:Quantifier:(?-ims:p)?", + "leave:Alternative:(?-ims:p)?", + "leave:Pattern:(?-ims:p)?", + "enter:Flags:", + "leave:Flags:", + "leave:RegExpLiteral:/(?-ims:p)?/" + ], + "/(?:no-modifiers)?/": [ + "enter:RegExpLiteral:/(?:no-modifiers)?/", + "enter:Pattern:(?:no-modifiers)?", + "enter:Alternative:(?:no-modifiers)?", + "enter:Quantifier:(?:no-modifiers)?", + "enter:Group:(?:no-modifiers)", + "enter:Alternative:no-modifiers", + "enter:Character:n", + "leave:Character:n", + "enter:Character:o", + "leave:Character:o", + "enter:Character:-", + "leave:Character:-", + "enter:Character:m", + "leave:Character:m", + "enter:Character:o", + "leave:Character:o", + "enter:Character:d", + "leave:Character:d", + "enter:Character:i", + "leave:Character:i", + "enter:Character:f", + "leave:Character:f", + "enter:Character:i", + "leave:Character:i", + "enter:Character:e", + "leave:Character:e", + "enter:Character:r", + "leave:Character:r", + "enter:Character:s", + "leave:Character:s", + "leave:Alternative:no-modifiers", + "leave:Group:(?:no-modifiers)", + "leave:Quantifier:(?:no-modifiers)?", + "leave:Alternative:(?:no-modifiers)?", + "leave:Pattern:(?:no-modifiers)?", + "enter:Flags:", + "leave:Flags:", + "leave:RegExpLiteral:/(?:no-modifiers)?/" ] } } \ No newline at end of file diff --git a/test/visitor.ts b/test/visitor.ts index c59f7c4..92fa77e 100644 --- a/test/visitor.ts +++ b/test/visitor.ts @@ -41,6 +41,8 @@ describe("visitRegExpAST function:", () => { onExpressionCharacterClassEnter: enter, onFlagsEnter: enter, onGroupEnter: enter, + onModifierFlagsEnter: enter, + onModifiersEnter: enter, onPatternEnter: enter, onQuantifierEnter: enter, onRegExpLiteralEnter: enter, @@ -59,6 +61,8 @@ describe("visitRegExpAST function:", () => { onExpressionCharacterClassLeave: 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",