Skip to content

Commit

Permalink
feat: add support for ES2025 duplicate named capturing groups
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Apr 13, 2024
1 parent a98196e commit d1b30e0
Show file tree
Hide file tree
Showing 9 changed files with 2,615 additions and 82 deletions.
3 changes: 2 additions & 1 deletion src/ecma-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export type EcmaVersion =
| 2022
| 2023
| 2024
export const latestEcmaVersion = 2024
| 2025
export const latestEcmaVersion = 2025
104 changes: 104 additions & 0 deletions src/group-specifiers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* Holds information for all GroupSpecifiers included in the pattern.
*/
export interface GroupSpecifiers {
/**
* @returns true if there are no GroupSpecifiers included in the pattern.
*/
isEmpty: () => boolean
clear: () => void
/**
* Called when visiting the Alternative.
* For ES2025, manage nesting with new Alternative scopes.
*/
enterAlternative: () => void
/**
* Called when leaving the Alternative.
*/
leaveAlternative: () => void
/**
* Checks whether the given group name is within the pattern.
*/
hasInPattern: (name: string) => boolean
/**
* Checks whether the given group name is within the current scope.
*/
hasInScope: (name: string) => boolean
/**
* Adds the given group name to the current scope.
*/
addToScope: (name: string) => void
}

export class GroupSpecifiersAsES2018 implements GroupSpecifiers {
private groupName = new Set<string>()

public clear(): void {
this.groupName.clear()
}

public isEmpty(): boolean {
return !this.groupName.size
}

public hasInPattern(name: string): boolean {
return this.groupName.has(name)
}

public hasInScope(name: string): boolean {
return this.hasInPattern(name)
}

public addToScope(name: string): void {
this.groupName.add(name)
}

// eslint-disable-next-line class-methods-use-this
public enterAlternative(): void {
// Prior to ES2025, it does not manage alternative scopes.
}

// eslint-disable-next-line class-methods-use-this
public leaveAlternative(): void {
// Prior to ES2025, it does not manage alternative scopes.
}
}

export class GroupSpecifiersAsES2025 implements GroupSpecifiers {
private groupNamesInAlternative = new Set<string>()
private upperGroupNamesStack: Set<string>[] = []

private groupNamesInPattern = new Set<string>()

public clear(): void {
this.groupNamesInAlternative.clear()
this.upperGroupNamesStack.length = 0
this.groupNamesInPattern.clear()
}

public isEmpty(): boolean {
return !this.groupNamesInPattern.size
}

public enterAlternative(): void {
this.upperGroupNamesStack.push(this.groupNamesInAlternative)
this.groupNamesInAlternative = new Set(this.groupNamesInAlternative)
}

public leaveAlternative(): void {
this.groupNamesInAlternative = this.upperGroupNamesStack.pop()!
}

public hasInPattern(name: string): boolean {
return this.groupNamesInPattern.has(name)
}

public hasInScope(name: string): boolean {
return this.groupNamesInAlternative.has(name)
}

public addToScope(name: string): void {
this.groupNamesInAlternative.add(name)
this.groupNamesInPattern.add(name)
}
}
3 changes: 2 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,14 +747,15 @@ export namespace RegExpParser {
strict?: boolean

/**
* ECMAScript version. Default is `2024`.
* ECMAScript version. Default is `2025`.
* - `2015` added `u` and `y` flags.
* - `2018` added `s` flag, Named Capturing Group, Lookbehind Assertion,
* and Unicode Property Escape.
* - `2019`, `2020`, and `2021` added more valid Unicode Property Escapes.
* - `2022` added `d` flag.
* - `2023` added more valid Unicode Property Escapes.
* - `2024` added `v` flag.
* - `2025` added duplicate named capturing groups.
*/
ecmaVersion?: EcmaVersion
}
Expand Down
26 changes: 19 additions & 7 deletions src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { EcmaVersion } from "./ecma-versions"
import { latestEcmaVersion } from "./ecma-versions"
import type { GroupSpecifiers } from "./group-specifiers"
import {
GroupSpecifiersAsES2018,
GroupSpecifiersAsES2025,
} from "./group-specifiers"
import { Reader } from "./reader"
import { newRegExpSyntaxError } from "./regexp-syntax-error"
import {
Expand Down Expand Up @@ -231,14 +236,15 @@ export namespace RegExpValidator {
strict?: boolean

/**
* ECMAScript version. Default is `2024`.
* ECMAScript version. Default is `2025`.
* - `2015` added `u` and `y` flags.
* - `2018` added `s` flag, Named Capturing Group, Lookbehind Assertion,
* and Unicode Property Escape.
* - `2019`, `2020`, and `2021` added more valid Unicode Property Escapes.
* - `2022` added `d` flag.
* - `2023` added more valid Unicode Property Escapes.
* - `2024` added `v` flag.
* - `2025` added duplicate named capturing groups.
*/
ecmaVersion?: EcmaVersion

Expand Down Expand Up @@ -631,7 +637,7 @@ export class RegExpValidator {

private _numCapturingParens = 0

private _groupNames = new Set<string>()
private _groupSpecifiers: GroupSpecifiers

private _backreferenceNames = new Set<string>()

Expand All @@ -643,6 +649,10 @@ export class RegExpValidator {
*/
public constructor(options?: RegExpValidator.Options) {
this._options = options ?? {}
this._groupSpecifiers =
this.ecmaVersion >= 2025
? new GroupSpecifiersAsES2025()
: new GroupSpecifiersAsES2018()
}

/**
Expand Down Expand Up @@ -763,7 +773,7 @@ export class RegExpValidator {
if (
!this._nFlag &&
this.ecmaVersion >= 2018 &&
this._groupNames.size > 0
!this._groupSpecifiers.isEmpty()
) {
this._nFlag = true
this.rewind(start)
Expand Down Expand Up @@ -1301,7 +1311,7 @@ export class RegExpValidator {
private consumePattern(): void {
const start = this.index
this._numCapturingParens = this.countCapturingParens()
this._groupNames.clear()
this._groupSpecifiers.clear()
this._backreferenceNames.clear()

this.onPatternEnter(start)
Expand All @@ -1322,7 +1332,7 @@ export class RegExpValidator {
this.raise(`Unexpected character '${c}'`)
}
for (const name of this._backreferenceNames) {
if (!this._groupNames.has(name)) {
if (!this._groupSpecifiers.hasInPattern(name)) {
this.raise("Invalid named capture referenced")
}
}
Expand Down Expand Up @@ -1380,7 +1390,9 @@ export class RegExpValidator {

this.onDisjunctionEnter(start)
do {
this._groupSpecifiers.enterAlternative()
this.consumeAlternative(i++)
this._groupSpecifiers.leaveAlternative()
} while (this.eat(VERTICAL_LINE))

if (this.consumeQuantifier(true)) {
Expand Down Expand Up @@ -1846,8 +1858,8 @@ export class RegExpValidator {
private consumeGroupSpecifier(): boolean {
if (this.eat(QUESTION_MARK)) {
if (this.eatGroupName()) {
if (!this._groupNames.has(this._lastStrValue)) {
this._groupNames.add(this._lastStrValue)
if (!this._groupSpecifiers.hasInScope(this._lastStrValue)) {
this._groupSpecifiers.addToScope(this._lastStrValue)
return true
}
this.raise("Duplicate capture group name")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"options": {
"strict": false,
"ecmaVersion": 2024
},
"patterns": {
"/(?<year>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year>[0-9]{4})/": {
"error": {
"message": "Invalid regular expression: /(?<year>[0-9]{4})-[0-9]{2}|[0-9]{2}-(?<year>[0-9]{4})/: Duplicate capture group name",
"index": 45
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"options": {
"strict": false,
"ecmaVersion": 2025
},
"patterns": {
"/(?<year>[0-9]{4})-(?<year>[0-9]{2})/": {
"error": {
"message": "Invalid regular expression: /(?<year>[0-9]{4})-(?<year>[0-9]{2})/: Duplicate capture group name",
"index": 27
}
}
}
}
Loading

0 comments on commit d1b30e0

Please sign in to comment.