@eslint-community/regexpp 4.11.2 → 4.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -51,6 +51,7 @@ declare module "@eslint-community/regexpp/ast" {
51
51
  | ExpressionCharacterClass
52
52
  | Group
53
53
  | LookaroundAssertion
54
+ | Modifiers
54
55
  | Pattern
55
56
  | Quantifier
56
57
  | RegExpLiteral
@@ -63,7 +64,8 @@ declare module "@eslint-community/regexpp/ast" {
63
64
  | BoundaryAssertion
64
65
  | Character
65
66
  | CharacterSet
66
- | Flags;
67
+ | Flags
68
+ | ModifierFlags;
67
69
  /**
68
70
  * The type which includes all atom nodes.
69
71
  */
@@ -147,6 +149,7 @@ declare module "@eslint-community/regexpp/ast" {
147
149
  export interface Group extends NodeBase {
148
150
  type: "Group";
149
151
  parent: Alternative | Quantifier;
152
+ modifiers: Modifiers | null;
150
153
  alternatives: Alternative[];
151
154
  }
152
155
  /**
@@ -447,6 +450,34 @@ declare module "@eslint-community/regexpp/ast" {
447
450
  ambiguous: false;
448
451
  resolved: CapturingGroup;
449
452
  }
453
+ /**
454
+ * The modifiers.
455
+ */
456
+ export interface Modifiers extends NodeBase {
457
+ type: "Modifiers";
458
+ parent: Group;
459
+ /**
460
+ * The add modifier flags.
461
+ */
462
+ add: ModifierFlags;
463
+ /**
464
+ * The remove modifier flags.
465
+ *
466
+ * `null` means no remove modifier flags. e.g. `(?ims:x)`
467
+ * The reason for `null` is that there is no position where the remove modifier flags appears. Must be behind the minus mark.
468
+ */
469
+ remove: ModifierFlags | null;
470
+ }
471
+ /**
472
+ * The modifier flags.
473
+ */
474
+ export interface ModifierFlags extends NodeBase {
475
+ type: "ModifierFlags";
476
+ parent: Modifiers;
477
+ dotAll: boolean;
478
+ ignoreCase: boolean;
479
+ multiline: boolean;
480
+ }
450
481
  /**
451
482
  * The flags.
452
483
  */
@@ -490,7 +521,7 @@ declare module "@eslint-community/regexpp/parser" {
490
521
  * - `2022` added `d` flag.
491
522
  * - `2023` added more valid Unicode Property Escapes.
492
523
  * - `2024` added `v` flag.
493
- * - `2025` added duplicate named capturing groups.
524
+ * - `2025` added duplicate named capturing groups, modifiers.
494
525
  */
495
526
  ecmaVersion?: EcmaVersion;
496
527
  }
@@ -579,7 +610,7 @@ declare module "@eslint-community/regexpp/validator" {
579
610
  * - `2022` added `d` flag.
580
611
  * - `2023` added more valid Unicode Property Escapes.
581
612
  * - `2024` added `v` flag.
582
- * - `2025` added duplicate named capturing groups.
613
+ * - `2025` added duplicate named capturing groups, modifiers.
583
614
  */
584
615
  ecmaVersion?: EcmaVersion;
585
616
  /**
@@ -691,6 +722,53 @@ declare module "@eslint-community/regexpp/validator" {
691
722
  * @param end The next 0-based index of the last character.
692
723
  */
693
724
  onGroupLeave?: (start: number, end: number) => void;
725
+ /**
726
+ * A function that is called when the validator entered a modifiers.
727
+ * @param start The 0-based index of the first character.
728
+ */
729
+ onModifiersEnter?: (start: number) => void;
730
+ /**
731
+ * A function that is called when the validator left a modifiers.
732
+ * @param start The 0-based index of the first character.
733
+ * @param end The next 0-based index of the last character.
734
+ */
735
+ onModifiersLeave?: (start: number, end: number) => void;
736
+ /**
737
+ * A function that is called when the validator found an add modifiers.
738
+ * @param start The 0-based index of the first character.
739
+ * @param end The next 0-based index of the last character.
740
+ * @param flags flags.
741
+ * @param flags.ignoreCase `i` flag.
742
+ * @param flags.multiline `m` flag.
743
+ * @param flags.dotAll `s` flag.
744
+ */
745
+ onAddModifiers?: (
746
+ start: number,
747
+ end: number,
748
+ flags: {
749
+ ignoreCase: boolean;
750
+ multiline: boolean;
751
+ dotAll: boolean;
752
+ }
753
+ ) => void;
754
+ /**
755
+ * A function that is called when the validator found a remove modifiers.
756
+ * @param start The 0-based index of the first character.
757
+ * @param end The next 0-based index of the last character.
758
+ * @param flags flags.
759
+ * @param flags.ignoreCase `i` flag.
760
+ * @param flags.multiline `m` flag.
761
+ * @param flags.dotAll `s` flag.
762
+ */
763
+ onRemoveModifiers?: (
764
+ start: number,
765
+ end: number,
766
+ flags: {
767
+ ignoreCase: boolean;
768
+ multiline: boolean;
769
+ dotAll: boolean;
770
+ }
771
+ ) => void;
694
772
  /**
695
773
  * A function that is called when the validator entered a capturing group.
696
774
  * @param start The 0-based index of the first character.
@@ -977,6 +1055,8 @@ declare module "@eslint-community/regexpp/visitor" {
977
1055
  ExpressionCharacterClass,
978
1056
  Flags,
979
1057
  Group,
1058
+ ModifierFlags,
1059
+ Modifiers,
980
1060
  Node,
981
1061
  Pattern,
982
1062
  Quantifier,
@@ -1032,6 +1112,10 @@ declare module "@eslint-community/regexpp/visitor" {
1032
1112
  onFlagsLeave?: (node: Flags) => void;
1033
1113
  onGroupEnter?: (node: Group) => void;
1034
1114
  onGroupLeave?: (node: Group) => void;
1115
+ onModifierFlagsEnter?: (node: ModifierFlags) => void;
1116
+ onModifierFlagsLeave?: (node: ModifierFlags) => void;
1117
+ onModifiersEnter?: (node: Modifiers) => void;
1118
+ onModifiersLeave?: (node: Modifiers) => void;
1035
1119
  onPatternEnter?: (node: Pattern) => void;
1036
1120
  onPatternLeave?: (node: Pattern) => void;
1037
1121
  onQuantifierEnter?: (node: Quantifier) => void;
package/index.js CHANGED
@@ -552,6 +552,17 @@ const CLASS_SET_RESERVED_PUNCTUATOR = new Set([
552
552
  GRAVE_ACCENT,
553
553
  TILDE,
554
554
  ]);
555
+ const FLAG_PROP_TO_CODEPOINT = {
556
+ global: LATIN_SMALL_LETTER_G,
557
+ ignoreCase: LATIN_SMALL_LETTER_I,
558
+ multiline: LATIN_SMALL_LETTER_M,
559
+ unicode: LATIN_SMALL_LETTER_U,
560
+ sticky: LATIN_SMALL_LETTER_Y,
561
+ dotAll: LATIN_SMALL_LETTER_S,
562
+ hasIndices: LATIN_SMALL_LETTER_D,
563
+ unicodeSets: LATIN_SMALL_LETTER_V,
564
+ };
565
+ const FLAG_CODEPOINT_TO_PROP = Object.fromEntries(Object.entries(FLAG_PROP_TO_CODEPOINT).map(([k, v]) => [v, k]));
555
566
  function isSyntaxCharacter(cp) {
556
567
  return SYNTAX_CHARACTER.has(cp);
557
568
  }
@@ -579,6 +590,11 @@ function isUnicodePropertyNameCharacter(cp) {
579
590
  function isUnicodePropertyValueCharacter(cp) {
580
591
  return isUnicodePropertyNameCharacter(cp) || isDecimalDigit(cp);
581
592
  }
593
+ function isRegularExpressionModifier(ch) {
594
+ return (ch === LATIN_SMALL_LETTER_I ||
595
+ ch === LATIN_SMALL_LETTER_M ||
596
+ ch === LATIN_SMALL_LETTER_S);
597
+ }
582
598
  class RegExpValidator {
583
599
  constructor(options) {
584
600
  this._reader = new Reader();
@@ -649,64 +665,8 @@ class RegExpValidator {
649
665
  }
650
666
  }
651
667
  validateFlagsInternal(source, start, end) {
652
- const existingFlags = new Set();
653
- let global = false;
654
- let ignoreCase = false;
655
- let multiline = false;
656
- let sticky = false;
657
- let unicode = false;
658
- let dotAll = false;
659
- let hasIndices = false;
660
- let unicodeSets = false;
661
- for (let i = start; i < end; ++i) {
662
- const flag = source.charCodeAt(i);
663
- if (existingFlags.has(flag)) {
664
- this.raise(`Duplicated flag '${source[i]}'`, { index: start });
665
- }
666
- existingFlags.add(flag);
667
- if (flag === LATIN_SMALL_LETTER_G) {
668
- global = true;
669
- }
670
- else if (flag === LATIN_SMALL_LETTER_I) {
671
- ignoreCase = true;
672
- }
673
- else if (flag === LATIN_SMALL_LETTER_M) {
674
- multiline = true;
675
- }
676
- else if (flag === LATIN_SMALL_LETTER_U &&
677
- this.ecmaVersion >= 2015) {
678
- unicode = true;
679
- }
680
- else if (flag === LATIN_SMALL_LETTER_Y &&
681
- this.ecmaVersion >= 2015) {
682
- sticky = true;
683
- }
684
- else if (flag === LATIN_SMALL_LETTER_S &&
685
- this.ecmaVersion >= 2018) {
686
- dotAll = true;
687
- }
688
- else if (flag === LATIN_SMALL_LETTER_D &&
689
- this.ecmaVersion >= 2022) {
690
- hasIndices = true;
691
- }
692
- else if (flag === LATIN_SMALL_LETTER_V &&
693
- this.ecmaVersion >= 2024) {
694
- unicodeSets = true;
695
- }
696
- else {
697
- this.raise(`Invalid flag '${source[i]}'`, { index: start });
698
- }
699
- }
700
- this.onRegExpFlags(start, end, {
701
- global,
702
- ignoreCase,
703
- multiline,
704
- unicode,
705
- sticky,
706
- dotAll,
707
- hasIndices,
708
- unicodeSets,
709
- });
668
+ const flags = this.parseFlags(source, start, end);
669
+ this.onRegExpFlags(start, end, flags);
710
670
  }
711
671
  _parseFlagsOptionToMode(uFlagOrFlags, sourceEnd) {
712
672
  let unicode = false;
@@ -801,6 +761,26 @@ class RegExpValidator {
801
761
  this._options.onGroupLeave(start, end);
802
762
  }
803
763
  }
764
+ onModifiersEnter(start) {
765
+ if (this._options.onModifiersEnter) {
766
+ this._options.onModifiersEnter(start);
767
+ }
768
+ }
769
+ onModifiersLeave(start, end) {
770
+ if (this._options.onModifiersLeave) {
771
+ this._options.onModifiersLeave(start, end);
772
+ }
773
+ }
774
+ onAddModifiers(start, end, flags) {
775
+ if (this._options.onAddModifiers) {
776
+ this._options.onAddModifiers(start, end, flags);
777
+ }
778
+ }
779
+ onRemoveModifiers(start, end, flags) {
780
+ if (this._options.onRemoveModifiers) {
781
+ this._options.onRemoveModifiers(start, end, flags);
782
+ }
783
+ }
804
784
  onCapturingGroupEnter(start, name) {
805
785
  if (this._options.onCapturingGroupEnter) {
806
786
  this._options.onCapturingGroupEnter(start, name);
@@ -1173,8 +1153,8 @@ class RegExpValidator {
1173
1153
  this.consumeDot() ||
1174
1154
  this.consumeReverseSolidusAtomEscape() ||
1175
1155
  Boolean(this.consumeCharacterClass()) ||
1176
- this.consumeUncapturingGroup() ||
1177
- this.consumeCapturingGroup());
1156
+ this.consumeCapturingGroup() ||
1157
+ this.consumeUncapturingGroup());
1178
1158
  }
1179
1159
  consumeDot() {
1180
1160
  if (this.eat(FULL_STOP)) {
@@ -1195,8 +1175,15 @@ class RegExpValidator {
1195
1175
  }
1196
1176
  consumeUncapturingGroup() {
1197
1177
  const start = this.index;
1198
- if (this.eat3(LEFT_PARENTHESIS, QUESTION_MARK, COLON)) {
1178
+ if (this.eat2(LEFT_PARENTHESIS, QUESTION_MARK)) {
1199
1179
  this.onGroupEnter(start);
1180
+ if (this.ecmaVersion >= 2025) {
1181
+ this.consumeModifiers();
1182
+ }
1183
+ if (!this.eat(COLON)) {
1184
+ this.rewind(start + 1);
1185
+ this.raise("Invalid group");
1186
+ }
1200
1187
  this.consumeDisjunction();
1201
1188
  if (!this.eat(RIGHT_PARENTHESIS)) {
1202
1189
  this.raise("Unterminated group");
@@ -1206,6 +1193,35 @@ class RegExpValidator {
1206
1193
  }
1207
1194
  return false;
1208
1195
  }
1196
+ consumeModifiers() {
1197
+ const start = this.index;
1198
+ const hasAddModifiers = this.eatModifiers();
1199
+ const addModifiersEnd = this.index;
1200
+ const hasHyphen = this.eat(HYPHEN_MINUS);
1201
+ if (!hasAddModifiers && !hasHyphen) {
1202
+ return false;
1203
+ }
1204
+ this.onModifiersEnter(start);
1205
+ const addModifiers = this.parseModifiers(start, addModifiersEnd);
1206
+ this.onAddModifiers(start, addModifiersEnd, addModifiers);
1207
+ if (hasHyphen) {
1208
+ const modifiersStart = this.index;
1209
+ if (!this.eatModifiers() &&
1210
+ !hasAddModifiers &&
1211
+ this.currentCodePoint === COLON) {
1212
+ this.raise("Invalid empty flags");
1213
+ }
1214
+ const modifiers = this.parseModifiers(modifiersStart, this.index);
1215
+ for (const [flagName] of Object.entries(modifiers).filter(([, enable]) => enable)) {
1216
+ if (addModifiers[flagName]) {
1217
+ this.raise(`Duplicated flag '${String.fromCodePoint(FLAG_PROP_TO_CODEPOINT[flagName])}'`);
1218
+ }
1219
+ }
1220
+ this.onRemoveModifiers(modifiersStart, this.index, modifiers);
1221
+ }
1222
+ this.onModifiersLeave(start, this.index);
1223
+ return true;
1224
+ }
1209
1225
  consumeCapturingGroup() {
1210
1226
  const start = this.index;
1211
1227
  if (this.eat(LEFT_PARENTHESIS)) {
@@ -1214,9 +1230,14 @@ class RegExpValidator {
1214
1230
  if (this.consumeGroupSpecifier()) {
1215
1231
  name = this._lastStrValue;
1216
1232
  }
1233
+ else if (this.currentCodePoint === QUESTION_MARK) {
1234
+ this.rewind(start);
1235
+ return false;
1236
+ }
1217
1237
  }
1218
1238
  else if (this.currentCodePoint === QUESTION_MARK) {
1219
- this.raise("Invalid group");
1239
+ this.rewind(start);
1240
+ return false;
1220
1241
  }
1221
1242
  this.onCapturingGroupEnter(start, name);
1222
1243
  this.consumeDisjunction();
@@ -1233,8 +1254,8 @@ class RegExpValidator {
1233
1254
  this.consumeReverseSolidusAtomEscape() ||
1234
1255
  this.consumeReverseSolidusFollowedByC() ||
1235
1256
  Boolean(this.consumeCharacterClass()) ||
1236
- this.consumeUncapturingGroup() ||
1237
1257
  this.consumeCapturingGroup() ||
1258
+ this.consumeUncapturingGroup() ||
1238
1259
  this.consumeInvalidBracedQuantifier() ||
1239
1260
  this.consumeExtendedPatternCharacter());
1240
1261
  }
@@ -1287,6 +1308,7 @@ class RegExpValidator {
1287
1308
  return false;
1288
1309
  }
1289
1310
  consumeGroupSpecifier() {
1311
+ const start = this.index;
1290
1312
  if (this.eat(QUESTION_MARK)) {
1291
1313
  if (this.eatGroupName()) {
1292
1314
  if (!this._groupSpecifiers.hasInScope(this._lastStrValue)) {
@@ -1295,7 +1317,7 @@ class RegExpValidator {
1295
1317
  }
1296
1318
  this.raise("Duplicate capture group name");
1297
1319
  }
1298
- this.raise("Invalid group");
1320
+ this.rewind(start);
1299
1321
  }
1300
1322
  return false;
1301
1323
  }
@@ -2043,6 +2065,63 @@ class RegExpValidator {
2043
2065
  }
2044
2066
  return true;
2045
2067
  }
2068
+ eatModifiers() {
2069
+ let ate = false;
2070
+ while (isRegularExpressionModifier(this.currentCodePoint)) {
2071
+ this.advance();
2072
+ ate = true;
2073
+ }
2074
+ return ate;
2075
+ }
2076
+ parseModifiers(start, end) {
2077
+ const { ignoreCase, multiline, dotAll } = this.parseFlags(this._reader.source, start, end);
2078
+ return { ignoreCase, multiline, dotAll };
2079
+ }
2080
+ parseFlags(source, start, end) {
2081
+ const flags = {
2082
+ global: false,
2083
+ ignoreCase: false,
2084
+ multiline: false,
2085
+ unicode: false,
2086
+ sticky: false,
2087
+ dotAll: false,
2088
+ hasIndices: false,
2089
+ unicodeSets: false,
2090
+ };
2091
+ const validFlags = new Set();
2092
+ validFlags.add(LATIN_SMALL_LETTER_G);
2093
+ validFlags.add(LATIN_SMALL_LETTER_I);
2094
+ validFlags.add(LATIN_SMALL_LETTER_M);
2095
+ if (this.ecmaVersion >= 2015) {
2096
+ validFlags.add(LATIN_SMALL_LETTER_U);
2097
+ validFlags.add(LATIN_SMALL_LETTER_Y);
2098
+ if (this.ecmaVersion >= 2018) {
2099
+ validFlags.add(LATIN_SMALL_LETTER_S);
2100
+ if (this.ecmaVersion >= 2022) {
2101
+ validFlags.add(LATIN_SMALL_LETTER_D);
2102
+ if (this.ecmaVersion >= 2024) {
2103
+ validFlags.add(LATIN_SMALL_LETTER_V);
2104
+ }
2105
+ }
2106
+ }
2107
+ }
2108
+ for (let i = start; i < end; ++i) {
2109
+ const flag = source.charCodeAt(i);
2110
+ if (validFlags.has(flag)) {
2111
+ const prop = FLAG_CODEPOINT_TO_PROP[flag];
2112
+ if (flags[prop]) {
2113
+ this.raise(`Duplicated flag '${source[i]}'`, {
2114
+ index: start,
2115
+ });
2116
+ }
2117
+ flags[prop] = true;
2118
+ }
2119
+ else {
2120
+ this.raise(`Invalid flag '${source[i]}'`, { index: start });
2121
+ }
2122
+ }
2123
+ return flags;
2124
+ }
2046
2125
  }
2047
2126
 
2048
2127
  const DUMMY_PATTERN = {};
@@ -2162,14 +2241,16 @@ class RegExpParserState {
2162
2241
  if (parent.type !== "Alternative") {
2163
2242
  throw new Error("UnknownError");
2164
2243
  }
2165
- this._node = {
2244
+ const group = {
2166
2245
  type: "Group",
2167
2246
  parent,
2168
2247
  start,
2169
2248
  end: start,
2170
2249
  raw: "",
2250
+ modifiers: null,
2171
2251
  alternatives: [],
2172
2252
  };
2253
+ this._node = group;
2173
2254
  parent.elements.push(this._node);
2174
2255
  }
2175
2256
  onGroupLeave(start, end) {
@@ -2181,6 +2262,63 @@ class RegExpParserState {
2181
2262
  node.raw = this.source.slice(start, end);
2182
2263
  this._node = node.parent;
2183
2264
  }
2265
+ onModifiersEnter(start) {
2266
+ const parent = this._node;
2267
+ if (parent.type !== "Group") {
2268
+ throw new Error("UnknownError");
2269
+ }
2270
+ this._node = {
2271
+ type: "Modifiers",
2272
+ parent,
2273
+ start,
2274
+ end: start,
2275
+ raw: "",
2276
+ add: null,
2277
+ remove: null,
2278
+ };
2279
+ parent.modifiers = this._node;
2280
+ }
2281
+ onModifiersLeave(start, end) {
2282
+ const node = this._node;
2283
+ if (node.type !== "Modifiers" || node.parent.type !== "Group") {
2284
+ throw new Error("UnknownError");
2285
+ }
2286
+ node.end = end;
2287
+ node.raw = this.source.slice(start, end);
2288
+ this._node = node.parent;
2289
+ }
2290
+ onAddModifiers(start, end, { ignoreCase, multiline, dotAll, }) {
2291
+ const parent = this._node;
2292
+ if (parent.type !== "Modifiers") {
2293
+ throw new Error("UnknownError");
2294
+ }
2295
+ parent.add = {
2296
+ type: "ModifierFlags",
2297
+ parent,
2298
+ start,
2299
+ end,
2300
+ raw: this.source.slice(start, end),
2301
+ ignoreCase,
2302
+ multiline,
2303
+ dotAll,
2304
+ };
2305
+ }
2306
+ onRemoveModifiers(start, end, { ignoreCase, multiline, dotAll, }) {
2307
+ const parent = this._node;
2308
+ if (parent.type !== "Modifiers") {
2309
+ throw new Error("UnknownError");
2310
+ }
2311
+ parent.remove = {
2312
+ type: "ModifierFlags",
2313
+ parent,
2314
+ start,
2315
+ end,
2316
+ raw: this.source.slice(start, end),
2317
+ ignoreCase,
2318
+ multiline,
2319
+ dotAll,
2320
+ };
2321
+ }
2184
2322
  onCapturingGroupEnter(start, name) {
2185
2323
  const parent = this._node;
2186
2324
  if (parent.type !== "Alternative") {
@@ -2666,6 +2804,12 @@ class RegExpVisitor {
2666
2804
  case "Group":
2667
2805
  this.visitGroup(node);
2668
2806
  break;
2807
+ case "Modifiers":
2808
+ this.visitModifiers(node);
2809
+ break;
2810
+ case "ModifierFlags":
2811
+ this.visitModifierFlags(node);
2812
+ break;
2669
2813
  case "Pattern":
2670
2814
  this.visitPattern(node);
2671
2815
  break;
@@ -2804,11 +2948,36 @@ class RegExpVisitor {
2804
2948
  if (this._handlers.onGroupEnter) {
2805
2949
  this._handlers.onGroupEnter(node);
2806
2950
  }
2951
+ if (node.modifiers) {
2952
+ this.visit(node.modifiers);
2953
+ }
2807
2954
  node.alternatives.forEach(this.visit, this);
2808
2955
  if (this._handlers.onGroupLeave) {
2809
2956
  this._handlers.onGroupLeave(node);
2810
2957
  }
2811
2958
  }
2959
+ visitModifiers(node) {
2960
+ if (this._handlers.onModifiersEnter) {
2961
+ this._handlers.onModifiersEnter(node);
2962
+ }
2963
+ if (node.add) {
2964
+ this.visit(node.add);
2965
+ }
2966
+ if (node.remove) {
2967
+ this.visit(node.remove);
2968
+ }
2969
+ if (this._handlers.onModifiersLeave) {
2970
+ this._handlers.onModifiersLeave(node);
2971
+ }
2972
+ }
2973
+ visitModifierFlags(node) {
2974
+ if (this._handlers.onModifierFlagsEnter) {
2975
+ this._handlers.onModifierFlagsEnter(node);
2976
+ }
2977
+ if (this._handlers.onModifierFlagsLeave) {
2978
+ this._handlers.onModifierFlagsLeave(node);
2979
+ }
2980
+ }
2812
2981
  visitPattern(node) {
2813
2982
  if (this._handlers.onPatternEnter) {
2814
2983
  this._handlers.onPatternEnter(node);