@ttsc/lint 0.18.4 → 0.19.0

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.
Files changed (188) hide show
  1. package/README.md +95 -43
  2. package/go.mod +5 -4
  3. package/internal/rulecode/rulecode.go +88 -0
  4. package/lib/index.d.ts +10 -1
  5. package/lib/index.js +45 -17
  6. package/lib/index.js.map +1 -1
  7. package/lib/structures/ITtscLintConfig.d.ts +3 -0
  8. package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
  9. package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
  10. package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
  11. package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
  12. package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
  13. package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
  14. package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
  15. package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
  16. package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
  17. package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
  18. package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
  19. package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
  20. package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
  21. package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
  22. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
  23. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
  24. package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
  25. package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
  26. package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
  27. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
  28. package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
  29. package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
  30. package/lib/structures/rules/index.d.ts +3 -0
  31. package/lib/structures/rules/index.js +3 -0
  32. package/lib/structures/rules/index.js.map +1 -1
  33. package/linthost/ast_helpers.go +521 -23
  34. package/linthost/ast_selector_matcher.go +1628 -0
  35. package/linthost/ast_selector_parser.go +628 -0
  36. package/linthost/comment_scan.go +154 -0
  37. package/linthost/compile.go +129 -79
  38. package/linthost/config.go +352 -88
  39. package/linthost/config_format.go +15 -1
  40. package/linthost/contrib_adapter.go +47 -8
  41. package/linthost/declaration_rules.go +12 -11
  42. package/linthost/directives.go +9 -67
  43. package/linthost/dispatch.go +37 -3
  44. package/linthost/engine.go +368 -53
  45. package/linthost/escape_string.go +77 -0
  46. package/linthost/fix.go +133 -23
  47. package/linthost/flags_gen.go +18 -17
  48. package/linthost/format.go +73 -167
  49. package/linthost/format_editor_settings.go +134 -26
  50. package/linthost/grapheme_tables_gen.go +1920 -0
  51. package/linthost/host.go +160 -61
  52. package/linthost/literal_escape_scan.go +117 -0
  53. package/linthost/lsp.go +545 -98
  54. package/linthost/numeric_literal_precision.go +228 -0
  55. package/linthost/polyfill_browserslist.go +1439 -0
  56. package/linthost/polyfill_browserslist_config.go +603 -0
  57. package/linthost/polyfill_corejs.go +750 -0
  58. package/linthost/polyfill_data.go +113 -0
  59. package/linthost/polyfill_data_gen.json +39868 -0
  60. package/linthost/print_nodes_array.go +7 -1
  61. package/linthost/print_nodes_object.go +7 -1
  62. package/linthost/project_engine.go +151 -0
  63. package/linthost/project_rules.go +78 -0
  64. package/linthost/regex_clean.go +191 -0
  65. package/linthost/regex_tree.go +1544 -0
  66. package/linthost/regex_tree_optimizer.go +1685 -0
  67. package/linthost/regexp_cache.go +44 -0
  68. package/linthost/rule_codes.go +105 -0
  69. package/linthost/rule_codes.json +745 -0
  70. package/linthost/rules_ban_ts_comment.go +312 -0
  71. package/linthost/rules_boundaries.go +128 -43
  72. package/linthost/rules_boundaries_dependencies.go +1013 -0
  73. package/linthost/rules_core_extra.go +62 -92
  74. package/linthost/rules_cypress.go +1 -1
  75. package/linthost/rules_default_case.go +87 -11
  76. package/linthost/rules_dupes.go +38 -13
  77. package/linthost/rules_empty.go +188 -20
  78. package/linthost/rules_escape.go +62 -48
  79. package/linthost/rules_format_arrow_parens.go +19 -5
  80. package/linthost/rules_format_bracket_spacing.go +1 -1
  81. package/linthost/rules_format_clause_join.go +1 -1
  82. package/linthost/rules_format_declaration_header.go +31 -19
  83. package/linthost/rules_format_indent.go +1 -1
  84. package/linthost/rules_format_jsdoc.go +1 -1
  85. package/linthost/rules_format_orphan_semi.go +1 -1
  86. package/linthost/rules_format_parameter_properties.go +17 -7
  87. package/linthost/rules_format_print_width.go +1 -1
  88. package/linthost/rules_format_quote_props.go +1 -1
  89. package/linthost/rules_format_quotes.go +1 -1
  90. package/linthost/rules_format_semi.go +106 -79
  91. package/linthost/rules_format_sort_imports.go +79 -45
  92. package/linthost/rules_format_statement_split.go +1 -1
  93. package/linthost/rules_format_ternary_nullish_parens.go +1 -1
  94. package/linthost/rules_format_trailing_comma.go +64 -1
  95. package/linthost/rules_format_whitespace.go +1 -1
  96. package/linthost/rules_functional.go +21 -22
  97. package/linthost/rules_gap.go +80 -3
  98. package/linthost/rules_grouped_accessor_pairs.go +84 -25
  99. package/linthost/rules_guard_for_in.go +64 -147
  100. package/linthost/rules_jsx_a11y.go +232 -22
  101. package/linthost/rules_logic.go +84 -38
  102. package/linthost/rules_nextjs.go +105 -16
  103. package/linthost/rules_no_class_assign.go +70 -0
  104. package/linthost/rules_no_duplicate_imports.go +266 -0
  105. package/linthost/rules_no_else_return.go +156 -38
  106. package/linthost/rules_no_extend_native.go +113 -17
  107. package/linthost/rules_no_fallthrough.go +1013 -0
  108. package/linthost/rules_no_func_assign.go +70 -0
  109. package/linthost/rules_no_import_assign.go +278 -86
  110. package/linthost/rules_no_inner_declarations.go +186 -0
  111. package/linthost/rules_no_loop_func.go +438 -53
  112. package/linthost/rules_no_mixed_operators.go +208 -52
  113. package/linthost/rules_no_param_reassign.go +376 -61
  114. package/linthost/rules_no_promise_executor_return.go +156 -0
  115. package/linthost/rules_no_restricted_imports.go +1013 -57
  116. package/linthost/rules_no_restricted_syntax.go +190 -26
  117. package/linthost/rules_no_useless_constructor.go +6 -36
  118. package/linthost/rules_problems.go +154 -311
  119. package/linthost/rules_promise.go +2578 -109
  120. package/linthost/rules_react_extras.go +71 -45
  121. package/linthost/rules_react_hooks.go +18 -2
  122. package/linthost/rules_react_perf.go +1 -0
  123. package/linthost/rules_react_refresh.go +1 -1
  124. package/linthost/rules_regexp.go +230 -14
  125. package/linthost/rules_security.go +45 -19
  126. package/linthost/rules_self.go +21 -1
  127. package/linthost/rules_solid.go +13 -7
  128. package/linthost/rules_storybook.go +1 -1
  129. package/linthost/rules_suggestions.go +490 -84
  130. package/linthost/rules_testing_library.go +12 -1
  131. package/linthost/rules_ts.go +227 -54
  132. package/linthost/rules_ts_async.go +0 -131
  133. package/linthost/rules_ts_no_base_to_string.go +3 -4
  134. package/linthost/rules_ts_no_misused_promises.go +817 -0
  135. package/linthost/rules_ts_no_restricted_types.go +320 -34
  136. package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
  137. package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
  138. package/linthost/rules_ts_require_array_sort_compare.go +5 -5
  139. package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
  140. package/linthost/rules_unicorn_better_regex.go +224 -16
  141. package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
  142. package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
  143. package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
  144. package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
  145. package/linthost/rules_unicorn_escape_case.go +30 -19
  146. package/linthost/rules_unicorn_filename_case.go +763 -16
  147. package/linthost/rules_unicorn_import_style.go +783 -17
  148. package/linthost/rules_unicorn_isolated_functions.go +934 -14
  149. package/linthost/rules_unicorn_no_hex_escape.go +26 -11
  150. package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
  151. package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
  152. package/linthost/rules_unicorn_no_unused_properties.go +756 -19
  153. package/linthost/rules_unicorn_number_literal_case.go +58 -39
  154. package/linthost/rules_unicorn_prefer_at.go +7 -3
  155. package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
  156. package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
  157. package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
  158. package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
  159. package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
  160. package/linthost/rules_unicorn_string_content.go +611 -15
  161. package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
  162. package/linthost/rules_unicorn_template_indent.go +923 -18
  163. package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
  164. package/linthost/rules_unicorn_throw_new_error.go +218 -26
  165. package/linthost/rules_var.go +829 -142
  166. package/linthost/string_length.go +222 -0
  167. package/package.json +4 -3
  168. package/rule/project.go +145 -0
  169. package/rule/rule.go +58 -1
  170. package/src/index.ts +58 -17
  171. package/src/structures/ITtscLintConfig.ts +3 -0
  172. package/src/structures/TtscLintRuleSetting.ts +3 -2
  173. package/src/structures/format/ITtscLintFormat.ts +3 -2
  174. package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
  175. package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
  176. package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
  177. package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
  178. package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
  179. package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
  180. package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
  181. package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
  182. package/src/structures/rules/ITtscLintRules.ts +7 -2
  183. package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
  184. package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
  185. package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
  186. package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
  187. package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
  188. package/src/structures/rules/index.ts +3 -0
@@ -0,0 +1,1544 @@
1
+ // regex_tree.go hosts the ECMAScript regular-expression AST used by
2
+ // unicorn/better-regex: node types, a source-faithful parser, a generator,
3
+ // and the structural-equality encoding the optimizer transforms rely on.
4
+ //
5
+ // The AST deliberately mirrors regexp-tree (the library the upstream ESLint
6
+ // rule delegated to) because the optimizer in regex_tree_optimizer.go is a
7
+ // behavioral port of regexp-tree's optimizer: node shapes, char "kinds", and
8
+ // even which optional fields a construction site sets all feed the
9
+ // duplicate-elimination checks, so they are modeled explicitly (see
10
+ // regexEqualityKey). Deviations from regexp-tree are deliberate safety fixes
11
+ // and are called out inline with "SAFETY:" comments; each one turns a fix
12
+ // that would corrupt regex semantics into a semantics-preserving one.
13
+ //
14
+ // The parser follows the ECMAScript RegExp grammar including the Annex B
15
+ // web-compat extensions the TypeScript scanner accepts (legacy octal
16
+ // escapes, identity escapes, literal `{` / `}` / `]`), so valid source
17
+ // regexes parse instead of raising false "Problem parsing" reports. Strict
18
+ // syntax violations regexp-tree also rejects (quantified assertions, bare
19
+ // quantifiers, out-of-order ranges) stay parse errors because the upstream
20
+ // rule reports those as parse-error diagnostics.
21
+ package linthost
22
+
23
+ import (
24
+ "fmt"
25
+ "sort"
26
+ "strconv"
27
+ "strings"
28
+ "unicode/utf16"
29
+ )
30
+
31
+ // regexNode is the closed union of regex AST node types.
32
+ type regexNode interface {
33
+ isRegexNode()
34
+ }
35
+
36
+ // Tri-state presence markers for optional Char fields. regexp-tree encodes
37
+ // nodes to JSON for equality checks; a field that is absent, `null`/`NaN`,
38
+ // or a value produces three distinct encodings, and different construction
39
+ // sites (parser vs. individual transforms) set different field subsets, so
40
+ // presence must be modeled to reproduce which duplicates collapse.
41
+ const (
42
+ regexFieldAbsent uint8 = iota
43
+ regexFieldNaN
44
+ regexFieldValue
45
+ )
46
+
47
+ // regexRegExpNode is the root: `/Body/Flags`. Body is nil for an empty
48
+ // pattern.
49
+ type regexRegExpNode struct {
50
+ Body regexNode
51
+ Flags string
52
+ }
53
+
54
+ // regexCharNode is a single character or character escape.
55
+ //
56
+ // Kind mirrors regexp-tree: "simple" (a, \e), "meta" (., \d, \n, [\b]),
57
+ // "hex" (\x41), "unicode" (A, \u{1f680}), "oct" (\052), "decimal"
58
+ // (\0), "control" (\cA and, as a SAFETY deviation, the dangling `\c`).
59
+ type regexCharNode struct {
60
+ Value string
61
+ Kind string
62
+ Symbol string
63
+ SymbolState uint8
64
+ CodePoint int
65
+ CodePointState uint8
66
+ Escaped bool
67
+ EscapedState uint8
68
+ SurrogatePair bool // JSON key present (always true when present)
69
+ // AltKeyOrder marks Chars built by the char-code-to-simple-char
70
+ // transform, whose JSON key order (kind before value) differs from
71
+ // parser-built Chars. regexp-tree's equality is a JSON string compare,
72
+ // so those nodes never equal parser-built ones; see regexEqualityKey.
73
+ AltKeyOrder bool
74
+ }
75
+
76
+ // regexClassNode is `[...]` / `[^...]`.
77
+ type regexClassNode struct {
78
+ Negative bool
79
+ Expressions []regexNode
80
+ // Loc is the half-open rune span of the class in the parsed pattern
81
+ // body. Only the constructor-path surgical rewrite reads it; transforms
82
+ // and equality ignore it (regexp-tree equality also skips loc).
83
+ LocStart int
84
+ LocEnd int
85
+ }
86
+
87
+ // regexClassRangeNode is `a-z` inside a character class.
88
+ type regexClassRangeNode struct {
89
+ From *regexCharNode
90
+ To *regexCharNode
91
+ }
92
+
93
+ // regexAlternativeNode is a concatenation of two or more terms.
94
+ type regexAlternativeNode struct {
95
+ Expressions []regexNode
96
+ }
97
+
98
+ // regexDisjunctionNode is `left|right`; either side may be nil (empty
99
+ // alternative). Chains nest through Left, matching regexp-tree.
100
+ type regexDisjunctionNode struct {
101
+ Left regexNode
102
+ Right regexNode
103
+ }
104
+
105
+ // regexGroupNode is `(...)`, `(?:...)`, or `(?<name>...)`. Expression is
106
+ // nil for an empty group. HasNumber distinguishes parser-built capturing
107
+ // groups (number always present) from transform-built non-capturing groups
108
+ // (no number key), which matters for equality.
109
+ type regexGroupNode struct {
110
+ Capturing bool
111
+ Name string
112
+ NameRaw string
113
+ Number int
114
+ HasNumber bool
115
+ Expression regexNode
116
+ }
117
+
118
+ // regexBackreferenceNode is `\1` or `\k<name>`.
119
+ type regexBackreferenceNode struct {
120
+ Kind string // "number" | "name"
121
+ Number int
122
+ Reference string // name form only
123
+ ReferenceRaw string
124
+ }
125
+
126
+ // regexRepetitionNode is an atom with a quantifier.
127
+ type regexRepetitionNode struct {
128
+ Expression regexNode
129
+ Quantifier *regexQuantifierNode
130
+ }
131
+
132
+ // regexQuantifierNode is `*`, `+`, `?`, or `{n}`/`{n,}`/`{n,m}` (kind
133
+ // "Range"). HasTo is false for open ranges. FieldOrder records the
134
+ // JavaScript object-key order of the from/to/greedy fields ("f", "t", "g"
135
+ // letters): regexp-tree mutates quantifiers in place, and a field assigned
136
+ // after deletion re-appends to the key order, which its JSON-based equality
137
+ // observes. Parser-built symbol quantifiers use "g", open ranges "fg",
138
+ // closed ranges "ftg".
139
+ type regexQuantifierNode struct {
140
+ Kind string
141
+ From int
142
+ To int
143
+ HasTo bool
144
+ Greedy bool
145
+ FieldOrder string
146
+ }
147
+
148
+ // regexAssertionNode is `^`, `$`, `\b`, `\B`, or a lookaround. Negative is
149
+ // tri-state via HasNegative: parser sets the key only on negative
150
+ // lookarounds, mirroring regexp-tree's JSON.
151
+ type regexAssertionNode struct {
152
+ Kind string // "^" | "$" | "\\b" | "\\B" | "Lookahead" | "Lookbehind"
153
+ Negative bool
154
+ HasNegative bool
155
+ Assertion regexNode
156
+ }
157
+
158
+ // regexUnicodePropertyNode is `\p{...}` / `\P{...}` (u/v mode only).
159
+ type regexUnicodePropertyNode struct {
160
+ Name string
161
+ Value string
162
+ Negative bool
163
+ Shorthand bool
164
+ Binary bool
165
+ }
166
+
167
+ // regexClassSetNode is an opaque `v`-mode class that uses set notation
168
+ // (nested classes, `--`/`&&` operators, or `\q{...}` strings). The rule
169
+ // never rewrites these; the node exists so a unicode-sets pattern parses
170
+ // instead of being misread, and regenerates verbatim.
171
+ type regexClassSetNode struct {
172
+ Raw string
173
+ LocStart int
174
+ LocEnd int
175
+ }
176
+
177
+ func (*regexRegExpNode) isRegexNode() {}
178
+ func (*regexCharNode) isRegexNode() {}
179
+ func (*regexClassNode) isRegexNode() {}
180
+ func (*regexClassRangeNode) isRegexNode() {}
181
+ func (*regexAlternativeNode) isRegexNode() {}
182
+ func (*regexDisjunctionNode) isRegexNode() {}
183
+ func (*regexGroupNode) isRegexNode() {}
184
+ func (*regexBackreferenceNode) isRegexNode() {}
185
+ func (*regexRepetitionNode) isRegexNode() {}
186
+ func (*regexQuantifierNode) isRegexNode() {}
187
+ func (*regexAssertionNode) isRegexNode() {}
188
+ func (*regexUnicodePropertyNode) isRegexNode() {}
189
+ func (*regexClassSetNode) isRegexNode() {}
190
+
191
+ // codePointIsNaN mirrors JavaScript's isNaN(node.codePoint): true when the
192
+ // field is absent (undefined) or NaN.
193
+ func (c *regexCharNode) codePointIsNaN() bool {
194
+ return c.CodePointState != regexFieldValue
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Generator
199
+ // ---------------------------------------------------------------------------
200
+
201
+ // regexGenerate renders a full `/body/flags` literal string from the AST.
202
+ func regexGenerate(re *regexRegExpNode) string {
203
+ var sb strings.Builder
204
+ sb.WriteByte('/')
205
+ regexGenerateNode(&sb, re.Body)
206
+ sb.WriteByte('/')
207
+ sb.WriteString(re.Flags)
208
+ return sb.String()
209
+ }
210
+
211
+ // regexGeneratePattern renders only the pattern body.
212
+ func regexGeneratePattern(node regexNode) string {
213
+ var sb strings.Builder
214
+ regexGenerateNode(&sb, node)
215
+ return sb.String()
216
+ }
217
+
218
+ func regexGenerateNode(sb *strings.Builder, node regexNode) {
219
+ switch n := node.(type) {
220
+ case nil:
221
+ case *regexAlternativeNode:
222
+ for _, e := range n.Expressions {
223
+ regexGenerateNode(sb, e)
224
+ }
225
+ case *regexDisjunctionNode:
226
+ regexGenerateNode(sb, n.Left)
227
+ sb.WriteByte('|')
228
+ regexGenerateNode(sb, n.Right)
229
+ case *regexGroupNode:
230
+ if n.Capturing {
231
+ if n.Name != "" {
232
+ sb.WriteString("(?<")
233
+ if n.NameRaw != "" {
234
+ sb.WriteString(n.NameRaw)
235
+ } else {
236
+ sb.WriteString(n.Name)
237
+ }
238
+ sb.WriteByte('>')
239
+ regexGenerateNode(sb, n.Expression)
240
+ sb.WriteByte(')')
241
+ } else {
242
+ sb.WriteByte('(')
243
+ regexGenerateNode(sb, n.Expression)
244
+ sb.WriteByte(')')
245
+ }
246
+ } else {
247
+ sb.WriteString("(?:")
248
+ regexGenerateNode(sb, n.Expression)
249
+ sb.WriteByte(')')
250
+ }
251
+ case *regexBackreferenceNode:
252
+ if n.Kind == "number" {
253
+ sb.WriteByte('\\')
254
+ sb.WriteString(strconv.Itoa(n.Number))
255
+ } else {
256
+ sb.WriteString("\\k<")
257
+ if n.ReferenceRaw != "" {
258
+ sb.WriteString(n.ReferenceRaw)
259
+ } else {
260
+ sb.WriteString(n.Reference)
261
+ }
262
+ sb.WriteByte('>')
263
+ }
264
+ case *regexAssertionNode:
265
+ switch n.Kind {
266
+ case "^", "$", "\\b", "\\B":
267
+ sb.WriteString(n.Kind)
268
+ case "Lookahead":
269
+ if n.Negative {
270
+ sb.WriteString("(?!")
271
+ } else {
272
+ sb.WriteString("(?=")
273
+ }
274
+ regexGenerateNode(sb, n.Assertion)
275
+ sb.WriteByte(')')
276
+ case "Lookbehind":
277
+ if n.Negative {
278
+ sb.WriteString("(?<!")
279
+ } else {
280
+ sb.WriteString("(?<=")
281
+ }
282
+ regexGenerateNode(sb, n.Assertion)
283
+ sb.WriteByte(')')
284
+ }
285
+ case *regexClassNode:
286
+ if n.Negative {
287
+ sb.WriteString("[^")
288
+ } else {
289
+ sb.WriteByte('[')
290
+ }
291
+ for i, e := range n.Expressions {
292
+ // SAFETY: regexp-tree prints an unescaped literal `^` even when
293
+ // class sorting moved it to the front, silently negating the
294
+ // class ([a\^] -> [^a]). Re-escape it in that one position.
295
+ if i == 0 && !n.Negative {
296
+ if ch, ok := e.(*regexCharNode); ok && ch.Kind == "simple" &&
297
+ ch.Value == "^" && !(ch.EscapedState == regexFieldValue && ch.Escaped) {
298
+ sb.WriteString("\\^")
299
+ continue
300
+ }
301
+ }
302
+ regexGenerateNode(sb, e)
303
+ }
304
+ sb.WriteByte(']')
305
+ case *regexClassRangeNode:
306
+ regexGenerateChar(sb, n.From)
307
+ sb.WriteByte('-')
308
+ regexGenerateChar(sb, n.To)
309
+ case *regexRepetitionNode:
310
+ regexGenerateNode(sb, n.Expression)
311
+ regexGenerateNode(sb, n.Quantifier)
312
+ case *regexQuantifierNode:
313
+ switch n.Kind {
314
+ case "+", "?", "*":
315
+ sb.WriteString(n.Kind)
316
+ case "Range":
317
+ if n.HasTo && n.From == n.To {
318
+ fmt.Fprintf(sb, "{%d}", n.From)
319
+ } else if !n.HasTo {
320
+ fmt.Fprintf(sb, "{%d,}", n.From)
321
+ } else {
322
+ fmt.Fprintf(sb, "{%d,%d}", n.From, n.To)
323
+ }
324
+ }
325
+ if !n.Greedy {
326
+ sb.WriteByte('?')
327
+ }
328
+ case *regexCharNode:
329
+ regexGenerateChar(sb, n)
330
+ case *regexUnicodePropertyNode:
331
+ if n.Negative {
332
+ sb.WriteString("\\P{")
333
+ } else {
334
+ sb.WriteString("\\p{")
335
+ }
336
+ if !n.Shorthand && !n.Binary {
337
+ sb.WriteString(n.Name)
338
+ sb.WriteByte('=')
339
+ }
340
+ sb.WriteString(n.Value)
341
+ sb.WriteByte('}')
342
+ case *regexClassSetNode:
343
+ sb.WriteString(n.Raw)
344
+ }
345
+ }
346
+
347
+ func regexGenerateChar(sb *strings.Builder, n *regexCharNode) {
348
+ if n == nil {
349
+ return
350
+ }
351
+ if n.Kind == "simple" {
352
+ if n.EscapedState == regexFieldValue && n.Escaped {
353
+ sb.WriteByte('\\')
354
+ }
355
+ sb.WriteString(n.Value)
356
+ return
357
+ }
358
+ // hex, unicode, oct, decimal, control, meta: the value carries its own
359
+ // spelling (including the backslash).
360
+ sb.WriteString(n.Value)
361
+ }
362
+
363
+ // ---------------------------------------------------------------------------
364
+ // Equality
365
+ // ---------------------------------------------------------------------------
366
+
367
+ // regexEqualityKey renders a canonical encoding of a node that separates
368
+ // exactly the same node pairs regexp-tree's JSON.stringify-based equality
369
+ // separates: field presence, NaN-vs-absent, and construction-site key order
370
+ // all participate. It is only ever compared against other keys produced by
371
+ // this function.
372
+ func regexEqualityKey(node regexNode) string {
373
+ var sb strings.Builder
374
+ regexEqualityKeyRec(&sb, node)
375
+ return sb.String()
376
+ }
377
+
378
+ func regexEqualityKeyRec(sb *strings.Builder, node regexNode) {
379
+ switch n := node.(type) {
380
+ case nil:
381
+ sb.WriteString("null")
382
+ case *regexCharNode:
383
+ sb.WriteString("Char{")
384
+ if n.AltKeyOrder {
385
+ fmt.Fprintf(sb, "kind:%s,value:%q,", n.Kind, n.Value)
386
+ } else {
387
+ fmt.Fprintf(sb, "value:%q,kind:%s,", n.Value, n.Kind)
388
+ }
389
+ switch n.SymbolState {
390
+ case regexFieldValue:
391
+ fmt.Fprintf(sb, "symbol:%q,", n.Symbol)
392
+ }
393
+ switch n.CodePointState {
394
+ case regexFieldNaN:
395
+ sb.WriteString("codePoint:null,")
396
+ case regexFieldValue:
397
+ fmt.Fprintf(sb, "codePoint:%d,", n.CodePoint)
398
+ }
399
+ if n.EscapedState == regexFieldValue {
400
+ fmt.Fprintf(sb, "escaped:%t,", n.Escaped)
401
+ }
402
+ if n.SurrogatePair {
403
+ sb.WriteString("isSurrogatePair:true,")
404
+ }
405
+ sb.WriteByte('}')
406
+ case *regexClassNode:
407
+ fmt.Fprintf(sb, "CharacterClass{negative:%t,[", n.Negative)
408
+ for _, e := range n.Expressions {
409
+ regexEqualityKeyRec(sb, e)
410
+ sb.WriteByte(',')
411
+ }
412
+ sb.WriteString("]}")
413
+ case *regexClassRangeNode:
414
+ sb.WriteString("ClassRange{")
415
+ regexEqualityKeyRec(sb, n.From)
416
+ sb.WriteByte(',')
417
+ regexEqualityKeyRec(sb, n.To)
418
+ sb.WriteByte('}')
419
+ case *regexAlternativeNode:
420
+ sb.WriteString("Alternative[")
421
+ for _, e := range n.Expressions {
422
+ regexEqualityKeyRec(sb, e)
423
+ sb.WriteByte(',')
424
+ }
425
+ sb.WriteByte(']')
426
+ case *regexDisjunctionNode:
427
+ sb.WriteString("Disjunction{")
428
+ regexEqualityKeyRec(sb, n.Left)
429
+ sb.WriteByte(',')
430
+ regexEqualityKeyRec(sb, n.Right)
431
+ sb.WriteByte('}')
432
+ case *regexGroupNode:
433
+ fmt.Fprintf(sb, "Group{capturing:%t,", n.Capturing)
434
+ if n.Name != "" {
435
+ fmt.Fprintf(sb, "name:%q,nameRaw:%q,", n.Name, n.NameRaw)
436
+ }
437
+ if n.HasNumber {
438
+ fmt.Fprintf(sb, "number:%d,", n.Number)
439
+ }
440
+ regexEqualityKeyRec(sb, n.Expression)
441
+ sb.WriteByte('}')
442
+ case *regexBackreferenceNode:
443
+ if n.Kind == "number" {
444
+ fmt.Fprintf(sb, "Backreference{number,%d}", n.Number)
445
+ } else {
446
+ fmt.Fprintf(sb, "Backreference{name,%d,%q,%q}", n.Number, n.Reference, n.ReferenceRaw)
447
+ }
448
+ case *regexRepetitionNode:
449
+ sb.WriteString("Repetition{")
450
+ regexEqualityKeyRec(sb, n.Expression)
451
+ sb.WriteByte(',')
452
+ regexEqualityKeyRec(sb, n.Quantifier)
453
+ sb.WriteByte('}')
454
+ case *regexQuantifierNode:
455
+ fmt.Fprintf(sb, "Quantifier{kind:%s,", n.Kind)
456
+ for _, field := range n.FieldOrder {
457
+ switch field {
458
+ case 'f':
459
+ fmt.Fprintf(sb, "from:%d,", n.From)
460
+ case 't':
461
+ fmt.Fprintf(sb, "to:%d,", n.To)
462
+ case 'g':
463
+ fmt.Fprintf(sb, "greedy:%t,", n.Greedy)
464
+ }
465
+ }
466
+ sb.WriteByte('}')
467
+ case *regexAssertionNode:
468
+ fmt.Fprintf(sb, "Assertion{kind:%s,", n.Kind)
469
+ if n.HasNegative {
470
+ fmt.Fprintf(sb, "negative:%t,", n.Negative)
471
+ }
472
+ regexEqualityKeyRec(sb, n.Assertion)
473
+ sb.WriteByte('}')
474
+ case *regexUnicodePropertyNode:
475
+ fmt.Fprintf(sb, "UnicodeProperty{%q,%q,%t,%t,%t}", n.Name, n.Value, n.Negative, n.Shorthand, n.Binary)
476
+ case *regexClassSetNode:
477
+ fmt.Fprintf(sb, "ClassSet{%q}", n.Raw)
478
+ }
479
+ }
480
+
481
+ // ---------------------------------------------------------------------------
482
+ // Clone
483
+ // ---------------------------------------------------------------------------
484
+
485
+ // regexCloneRegExp deep-copies the tree, mirroring regexp-tree's clone()
486
+ // (which preserves NaN and field presence).
487
+ func regexCloneRegExp(re *regexRegExpNode) *regexRegExpNode {
488
+ return &regexRegExpNode{Body: regexCloneNode(re.Body), Flags: re.Flags}
489
+ }
490
+
491
+ func regexCloneNode(node regexNode) regexNode {
492
+ switch n := node.(type) {
493
+ case nil:
494
+ return nil
495
+ case *regexCharNode:
496
+ c := *n
497
+ return &c
498
+ case *regexClassNode:
499
+ c := *n
500
+ c.Expressions = cloneRegexList(n.Expressions)
501
+ return &c
502
+ case *regexClassRangeNode:
503
+ return &regexClassRangeNode{
504
+ From: regexCloneNode(n.From).(*regexCharNode),
505
+ To: regexCloneNode(n.To).(*regexCharNode),
506
+ }
507
+ case *regexAlternativeNode:
508
+ return &regexAlternativeNode{Expressions: cloneRegexList(n.Expressions)}
509
+ case *regexDisjunctionNode:
510
+ return &regexDisjunctionNode{Left: regexCloneNode(n.Left), Right: regexCloneNode(n.Right)}
511
+ case *regexGroupNode:
512
+ c := *n
513
+ c.Expression = regexCloneNode(n.Expression)
514
+ return &c
515
+ case *regexBackreferenceNode:
516
+ c := *n
517
+ return &c
518
+ case *regexRepetitionNode:
519
+ q := *n.Quantifier
520
+ return &regexRepetitionNode{Expression: regexCloneNode(n.Expression), Quantifier: &q}
521
+ case *regexQuantifierNode:
522
+ c := *n
523
+ return &c
524
+ case *regexAssertionNode:
525
+ c := *n
526
+ c.Assertion = regexCloneNode(n.Assertion)
527
+ return &c
528
+ case *regexUnicodePropertyNode:
529
+ c := *n
530
+ return &c
531
+ case *regexClassSetNode:
532
+ c := *n
533
+ return &c
534
+ }
535
+ return nil
536
+ }
537
+
538
+ func cloneRegexList(list []regexNode) []regexNode {
539
+ out := make([]regexNode, len(list))
540
+ for i, e := range list {
541
+ out[i] = regexCloneNode(e)
542
+ }
543
+ return out
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // Parser
548
+ // ---------------------------------------------------------------------------
549
+
550
+ // regexParseError is a positioned syntax error inside a pattern.
551
+ type regexParseError struct {
552
+ Message string
553
+ }
554
+
555
+ func (e *regexParseError) Error() string { return e.Message }
556
+
557
+ type regexParser struct {
558
+ src []rune
559
+ pos int
560
+ flags string
561
+ // grammar mode
562
+ uMode bool
563
+ vMode bool
564
+ // prescan results
565
+ groupCount int
566
+ groupNames map[string]bool
567
+ // running capture-group numbering
568
+ nextGroupNumber int
569
+ }
570
+
571
+ // regexParseLiteral parses a full `/pattern/flags` literal string.
572
+ func regexParseLiteral(literal string) (*regexRegExpNode, error) {
573
+ runes := []rune(literal)
574
+ if len(runes) < 2 || runes[0] != '/' {
575
+ return nil, &regexParseError{Message: "not a regular expression literal"}
576
+ }
577
+ // Find the closing, unescaped `/` outside a character class.
578
+ body := -1
579
+ inClass := false
580
+ for i := 1; i < len(runes); i++ {
581
+ switch runes[i] {
582
+ case '\\':
583
+ i++
584
+ case '[':
585
+ inClass = true
586
+ case ']':
587
+ inClass = false
588
+ case '/':
589
+ if !inClass {
590
+ body = i
591
+ }
592
+ }
593
+ if body >= 0 {
594
+ break
595
+ }
596
+ }
597
+ if body < 0 {
598
+ return nil, &regexParseError{Message: "unterminated regular expression literal"}
599
+ }
600
+ pattern := string(runes[1:body])
601
+ flags := string(runes[body+1:])
602
+ return regexParsePattern(pattern, flags, true)
603
+ }
604
+
605
+ // regexParsePattern parses a pattern body with the given flags.
606
+ // validateFlags additionally rejects malformed flag strings (used for the
607
+ // literal path; the constructor path reads flags leniently the way the
608
+ // upstream rule's clean-regexp step did).
609
+ func regexParsePattern(pattern string, flags string, validateFlags bool) (*regexRegExpNode, error) {
610
+ normalizedFlags := flags
611
+ if validateFlags {
612
+ sorted, err := regexNormalizeFlags(flags)
613
+ if err != nil {
614
+ return nil, err
615
+ }
616
+ normalizedFlags = sorted
617
+ } else {
618
+ runes := []rune(flags)
619
+ sort.Slice(runes, func(i, j int) bool { return runes[i] < runes[j] })
620
+ normalizedFlags = string(runes)
621
+ }
622
+ p := &regexParser{
623
+ src: []rune(pattern),
624
+ flags: normalizedFlags,
625
+ uMode: strings.ContainsRune(flags, 'u'),
626
+ vMode: strings.ContainsRune(flags, 'v'),
627
+ groupNames: map[string]bool{},
628
+ nextGroupNumber: 1,
629
+ }
630
+ if err := p.prescanGroups(); err != nil {
631
+ return nil, err
632
+ }
633
+ body, err := p.parseDisjunction(true)
634
+ if err != nil {
635
+ return nil, err
636
+ }
637
+ if p.pos < len(p.src) {
638
+ return nil, p.errorAt(p.pos, fmt.Sprintf("unexpected token %q", string(p.src[p.pos])))
639
+ }
640
+ return &regexRegExpNode{Body: body, Flags: normalizedFlags}, nil
641
+ }
642
+
643
+ // regexNormalizeFlags validates and alphabetically sorts a flag string, the
644
+ // way regexp-tree's parser normalizes flags before generation.
645
+ func regexNormalizeFlags(flags string) (string, error) {
646
+ seen := map[rune]bool{}
647
+ runes := []rune(flags)
648
+ for _, r := range runes {
649
+ if !strings.ContainsRune("dgimsuvy", r) {
650
+ return "", &regexParseError{Message: fmt.Sprintf("invalid regular expression flag %q", string(r))}
651
+ }
652
+ if seen[r] {
653
+ return "", &regexParseError{Message: fmt.Sprintf("duplicate regular expression flag %q", string(r))}
654
+ }
655
+ seen[r] = true
656
+ }
657
+ if seen['u'] && seen['v'] {
658
+ return "", &regexParseError{Message: "regular expression flags \"u\" and \"v\" cannot be combined"}
659
+ }
660
+ sort.Slice(runes, func(i, j int) bool { return runes[i] < runes[j] })
661
+ return string(runes), nil
662
+ }
663
+
664
+ func (p *regexParser) errorAt(pos int, message string) error {
665
+ return &regexParseError{Message: fmt.Sprintf("%s at index %d", message, pos)}
666
+ }
667
+
668
+ func (p *regexParser) errorEOF() error {
669
+ return &regexParseError{Message: "unexpected end of input"}
670
+ }
671
+
672
+ // prescanGroups counts capturing groups and collects named-group names so
673
+ // that `\1` and `\k<name>` resolve against the whole pattern (forward
674
+ // references included), matching ECMAScript's two-phase resolution.
675
+ func (p *regexParser) prescanGroups() error {
676
+ src := p.src
677
+ for i := 0; i < len(src); i++ {
678
+ switch src[i] {
679
+ case '\\':
680
+ i++
681
+ case '[':
682
+ // Skip class contents; `\]` does not close, and v-mode classes
683
+ // nest.
684
+ depth := 1
685
+ for i++; i < len(src) && depth > 0; i++ {
686
+ if src[i] == '\\' {
687
+ i++
688
+ } else if p.vMode && src[i] == '[' {
689
+ depth++
690
+ } else if src[i] == ']' {
691
+ depth--
692
+ }
693
+ }
694
+ i--
695
+ case '(':
696
+ if i+1 < len(src) && src[i+1] == '?' {
697
+ if i+2 < len(src) && src[i+2] == '<' &&
698
+ i+3 < len(src) && src[i+3] != '=' && src[i+3] != '!' {
699
+ // Named capturing group.
700
+ end := i + 3
701
+ for end < len(src) && src[end] != '>' {
702
+ end++
703
+ }
704
+ if end >= len(src) {
705
+ return p.errorEOF()
706
+ }
707
+ name, _, err := p.decodeGroupName(src[i+3 : end])
708
+ if err != nil {
709
+ return err
710
+ }
711
+ if p.groupNames[name] {
712
+ return &regexParseError{Message: fmt.Sprintf("duplicate capture group name %q", name)}
713
+ }
714
+ p.groupNames[name] = true
715
+ p.groupCount++
716
+ i = end
717
+ }
718
+ // (?: (?= (?! (?<= (?<! are non-capturing.
719
+ } else {
720
+ p.groupCount++
721
+ }
722
+ }
723
+ }
724
+ return nil
725
+ }
726
+
727
+ // decodeGroupName validates a group-name rune slice and decodes \uXXXX /
728
+ // \u{...} escapes. Returns the decoded name and the raw spelling.
729
+ func (p *regexParser) decodeGroupName(raw []rune) (string, string, error) {
730
+ if len(raw) == 0 {
731
+ return "", "", &regexParseError{Message: "empty capture group name"}
732
+ }
733
+ var name strings.Builder
734
+ first := true
735
+ for i := 0; i < len(raw); i++ {
736
+ r := raw[i]
737
+ if r == '\\' {
738
+ if i+1 >= len(raw) || raw[i+1] != 'u' {
739
+ return "", "", &regexParseError{Message: "invalid capture group name"}
740
+ }
741
+ decoded, consumed, ok := decodeUnicodeEscapeBody(raw[i+2:], true)
742
+ if !ok {
743
+ return "", "", &regexParseError{Message: "invalid capture group name"}
744
+ }
745
+ r = decoded
746
+ i += 1 + consumed
747
+ }
748
+ if !isRegexIdentifierRune(r, first) {
749
+ return "", "", &regexParseError{Message: fmt.Sprintf("invalid capture group name character %q", string(r))}
750
+ }
751
+ name.WriteRune(r)
752
+ first = false
753
+ }
754
+ return name.String(), string(raw), nil
755
+ }
756
+
757
+ func isRegexIdentifierRune(r rune, first bool) bool {
758
+ if r == '$' || r == '_' {
759
+ return true
760
+ }
761
+ if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' {
762
+ return true
763
+ }
764
+ if !first && r >= '0' && r <= '9' {
765
+ return true
766
+ }
767
+ // Permit non-ASCII identifier characters leniently; the TypeScript
768
+ // scanner has already validated the source literal.
769
+ return r > 0x7f
770
+ }
771
+
772
+ // decodeUnicodeEscapeBody decodes the body after `\u`: either XXXX or
773
+ // {X...} (brace form only when braces are allowed). Returns the rune, the
774
+ // number of runes consumed after `\u`, and success.
775
+ func decodeUnicodeEscapeBody(src []rune, allowBraces bool) (rune, int, bool) {
776
+ if len(src) >= 1 && src[0] == '{' && allowBraces {
777
+ end := 1
778
+ for end < len(src) && src[end] != '}' {
779
+ end++
780
+ }
781
+ if end >= len(src) || end == 1 {
782
+ return 0, 0, false
783
+ }
784
+ v, err := strconv.ParseInt(string(src[1:end]), 16, 64)
785
+ if err != nil || v > 0x10ffff {
786
+ return 0, 0, false
787
+ }
788
+ return rune(v), end + 1, true
789
+ }
790
+ if len(src) < 4 {
791
+ return 0, 0, false
792
+ }
793
+ v, err := strconv.ParseInt(string(src[:4]), 16, 32)
794
+ if err != nil {
795
+ return 0, 0, false
796
+ }
797
+ return rune(v), 4, true
798
+ }
799
+
800
+ // parseDisjunction parses `alt (| alt)*`. When top is true the disjunction
801
+ // ends at end-of-input, otherwise at `)`.
802
+ func (p *regexParser) parseDisjunction(top bool) (regexNode, error) {
803
+ first, err := p.parseAlternative(top)
804
+ if err != nil {
805
+ return nil, err
806
+ }
807
+ node := first
808
+ for p.pos < len(p.src) && p.src[p.pos] == '|' {
809
+ p.pos++
810
+ next, err := p.parseAlternative(top)
811
+ if err != nil {
812
+ return nil, err
813
+ }
814
+ node = &regexDisjunctionNode{Left: node, Right: next}
815
+ }
816
+ return node, nil
817
+ }
818
+
819
+ // parseAlternative parses a run of terms up to `|`, `)`, or end.
820
+ func (p *regexParser) parseAlternative(top bool) (regexNode, error) {
821
+ var terms []regexNode
822
+ for p.pos < len(p.src) {
823
+ r := p.src[p.pos]
824
+ if r == '|' {
825
+ break
826
+ }
827
+ if r == ')' {
828
+ if top {
829
+ return nil, p.errorAt(p.pos, "unexpected token \")\"")
830
+ }
831
+ break
832
+ }
833
+ term, err := p.parseTerm()
834
+ if err != nil {
835
+ return nil, err
836
+ }
837
+ terms = append(terms, term)
838
+ }
839
+ switch len(terms) {
840
+ case 0:
841
+ return nil, nil
842
+ case 1:
843
+ return terms[0], nil
844
+ }
845
+ return &regexAlternativeNode{Expressions: terms}, nil
846
+ }
847
+
848
+ // parseTerm parses one assertion or one atom plus an optional quantifier.
849
+ func (p *regexParser) parseTerm() (regexNode, error) {
850
+ atom, quantifiable, err := p.parseAtom()
851
+ if err != nil {
852
+ return nil, err
853
+ }
854
+ quantifier, err := p.parseQuantifier()
855
+ if err != nil {
856
+ return nil, err
857
+ }
858
+ if quantifier == nil {
859
+ return atom, nil
860
+ }
861
+ if !quantifiable {
862
+ // Strict-grammar rejection of quantified assertions, matching
863
+ // regexp-tree (`/(?!a)+/` is a parse error there too).
864
+ return nil, p.errorAt(p.pos-1, fmt.Sprintf("unexpected quantifier after %q", regexGeneratePattern(atom)))
865
+ }
866
+ return &regexRepetitionNode{Expression: atom, Quantifier: quantifier}, nil
867
+ }
868
+
869
+ // parseQuantifier parses `*`, `+`, `?`, or a `{...}` range quantifier
870
+ // (returning nil when the `{` run is not quantifier-shaped, per Annex B).
871
+ func (p *regexParser) parseQuantifier() (*regexQuantifierNode, error) {
872
+ if p.pos >= len(p.src) {
873
+ return nil, nil
874
+ }
875
+ var q *regexQuantifierNode
876
+ switch p.src[p.pos] {
877
+ case '*', '+', '?':
878
+ q = &regexQuantifierNode{Kind: string(p.src[p.pos]), Greedy: true, FieldOrder: "g"}
879
+ p.pos++
880
+ case '{':
881
+ from, to, hasTo, exact, length := scanRangeQuantifier(p.src[p.pos:])
882
+ if length == 0 {
883
+ return nil, nil
884
+ }
885
+ if hasTo && !exact && to < from {
886
+ return nil, p.errorAt(p.pos, "quantifier range out of order")
887
+ }
888
+ q = &regexQuantifierNode{Kind: "Range", From: from, Greedy: true, FieldOrder: "fg"}
889
+ if exact {
890
+ q.To = from
891
+ q.HasTo = true
892
+ q.FieldOrder = "ftg"
893
+ } else if hasTo {
894
+ q.To = to
895
+ q.HasTo = true
896
+ q.FieldOrder = "ftg"
897
+ }
898
+ p.pos += length
899
+ default:
900
+ return nil, nil
901
+ }
902
+ if p.pos < len(p.src) && p.src[p.pos] == '?' {
903
+ q.Greedy = false
904
+ p.pos++
905
+ }
906
+ return q, nil
907
+ }
908
+
909
+ // scanRangeQuantifier recognizes `{n}`, `{n,}`, `{n,m}` at the start of
910
+ // src. Returns (from, to, hasTo, exact, consumedRunes); consumedRunes is 0
911
+ // when the text is not a range quantifier.
912
+ func scanRangeQuantifier(src []rune) (int, int, bool, bool, int) {
913
+ i := 1
914
+ start := i
915
+ for i < len(src) && src[i] >= '0' && src[i] <= '9' {
916
+ i++
917
+ }
918
+ if i == start {
919
+ return 0, 0, false, false, 0
920
+ }
921
+ from, err := strconv.Atoi(string(src[start:i]))
922
+ if err != nil {
923
+ return 0, 0, false, false, 0
924
+ }
925
+ if i < len(src) && src[i] == '}' {
926
+ return from, 0, false, true, i + 1
927
+ }
928
+ if i >= len(src) || src[i] != ',' {
929
+ return 0, 0, false, false, 0
930
+ }
931
+ i++
932
+ if i < len(src) && src[i] == '}' {
933
+ return from, 0, false, false, i + 1
934
+ }
935
+ start = i
936
+ for i < len(src) && src[i] >= '0' && src[i] <= '9' {
937
+ i++
938
+ }
939
+ if i == start || i >= len(src) || src[i] != '}' {
940
+ return 0, 0, false, false, 0
941
+ }
942
+ to, err := strconv.Atoi(string(src[start:i]))
943
+ if err != nil {
944
+ return 0, 0, false, false, 0
945
+ }
946
+ return from, to, true, false, i + 1
947
+ }
948
+
949
+ // parseAtom parses one atom. The second result reports whether the atom is
950
+ // quantifiable (assertions are not).
951
+ func (p *regexParser) parseAtom() (regexNode, bool, error) {
952
+ r := p.src[p.pos]
953
+ switch r {
954
+ case '^', '$':
955
+ p.pos++
956
+ return &regexAssertionNode{Kind: string(r)}, false, nil
957
+ case '.':
958
+ p.pos++
959
+ return &regexCharNode{
960
+ Value: ".", Kind: "meta",
961
+ Symbol: ".", SymbolState: regexFieldValue,
962
+ CodePointState: regexFieldNaN,
963
+ }, true, nil
964
+ case '\\':
965
+ return p.parseEscape(false)
966
+ case '[':
967
+ node, err := p.parseCharacterClass()
968
+ return node, true, err
969
+ case '(':
970
+ return p.parseGroup()
971
+ case '*', '+', '?':
972
+ return nil, false, p.errorAt(p.pos, fmt.Sprintf("unexpected token %q", string(r)))
973
+ case '{':
974
+ if _, _, _, _, length := scanRangeQuantifier(p.src[p.pos:]); length > 0 {
975
+ return nil, false, p.errorAt(p.pos, fmt.Sprintf("unexpected token %q", string(p.src[p.pos:p.pos+length])))
976
+ }
977
+ p.pos++
978
+ return simpleChar(r), true, nil
979
+ default:
980
+ // `]` and `}` are literal pattern characters per Annex B.
981
+ p.pos++
982
+ return regexPatternChar(r), true, nil
983
+ }
984
+ }
985
+
986
+ // regexPatternChar builds a plain unescaped Char for a literal rune.
987
+ func regexPatternChar(r rune) *regexCharNode {
988
+ return simpleChar(r)
989
+ }
990
+
991
+ func simpleChar(r rune) *regexCharNode {
992
+ return &regexCharNode{
993
+ Value: string(r), Kind: "simple",
994
+ Symbol: string(r), SymbolState: regexFieldValue,
995
+ CodePoint: int(r), CodePointState: regexFieldValue,
996
+ }
997
+ }
998
+
999
+ func escapedSimpleChar(r rune) *regexCharNode {
1000
+ c := simpleChar(r)
1001
+ c.Escaped = true
1002
+ c.EscapedState = regexFieldValue
1003
+ return c
1004
+ }
1005
+
1006
+ // parseGroup parses `(...)`, `(?:...)`, `(?<name>...)`, and lookarounds.
1007
+ // Returns (node, quantifiable, err).
1008
+ func (p *regexParser) parseGroup() (regexNode, bool, error) {
1009
+ start := p.pos
1010
+ p.pos++ // consume '('
1011
+ if p.pos < len(p.src) && p.src[p.pos] == '?' {
1012
+ p.pos++
1013
+ if p.pos >= len(p.src) {
1014
+ return nil, false, p.errorEOF()
1015
+ }
1016
+ switch p.src[p.pos] {
1017
+ case ':':
1018
+ p.pos++
1019
+ expr, err := p.parseGroupBody()
1020
+ if err != nil {
1021
+ return nil, false, err
1022
+ }
1023
+ return &regexGroupNode{Capturing: false, Expression: expr}, true, nil
1024
+ case '=', '!':
1025
+ negative := p.src[p.pos] == '!'
1026
+ p.pos++
1027
+ expr, err := p.parseGroupBody()
1028
+ if err != nil {
1029
+ return nil, false, err
1030
+ }
1031
+ node := &regexAssertionNode{Kind: "Lookahead", Assertion: expr}
1032
+ if negative {
1033
+ node.Negative = true
1034
+ node.HasNegative = true
1035
+ }
1036
+ return node, false, nil
1037
+ case '<':
1038
+ if p.pos+1 < len(p.src) && (p.src[p.pos+1] == '=' || p.src[p.pos+1] == '!') {
1039
+ negative := p.src[p.pos+1] == '!'
1040
+ p.pos += 2
1041
+ expr, err := p.parseGroupBody()
1042
+ if err != nil {
1043
+ return nil, false, err
1044
+ }
1045
+ node := &regexAssertionNode{Kind: "Lookbehind", Assertion: expr}
1046
+ if negative {
1047
+ node.Negative = true
1048
+ node.HasNegative = true
1049
+ }
1050
+ return node, false, nil
1051
+ }
1052
+ // Named capturing group.
1053
+ p.pos++
1054
+ nameStart := p.pos
1055
+ for p.pos < len(p.src) && p.src[p.pos] != '>' {
1056
+ p.pos++
1057
+ }
1058
+ if p.pos >= len(p.src) {
1059
+ return nil, false, p.errorEOF()
1060
+ }
1061
+ name, raw, err := p.decodeGroupName(p.src[nameStart:p.pos])
1062
+ if err != nil {
1063
+ return nil, false, err
1064
+ }
1065
+ p.pos++ // consume '>'
1066
+ number := p.nextGroupNumber
1067
+ p.nextGroupNumber++
1068
+ expr, err := p.parseGroupBody()
1069
+ if err != nil {
1070
+ return nil, false, err
1071
+ }
1072
+ return &regexGroupNode{
1073
+ Capturing: true, Name: name, NameRaw: raw,
1074
+ Number: number, HasNumber: true, Expression: expr,
1075
+ }, true, nil
1076
+ default:
1077
+ return nil, false, p.errorAt(start+1, "unexpected token \"?\"")
1078
+ }
1079
+ }
1080
+ number := p.nextGroupNumber
1081
+ p.nextGroupNumber++
1082
+ expr, err := p.parseGroupBody()
1083
+ if err != nil {
1084
+ return nil, false, err
1085
+ }
1086
+ return &regexGroupNode{Capturing: true, Number: number, HasNumber: true, Expression: expr}, true, nil
1087
+ }
1088
+
1089
+ func (p *regexParser) parseGroupBody() (regexNode, error) {
1090
+ expr, err := p.parseDisjunction(false)
1091
+ if err != nil {
1092
+ return nil, err
1093
+ }
1094
+ if p.pos >= len(p.src) || p.src[p.pos] != ')' {
1095
+ return nil, p.errorEOF()
1096
+ }
1097
+ p.pos++
1098
+ return expr, nil
1099
+ }
1100
+
1101
+ // parseEscape parses one `\`-escape. inClass selects the ClassEscape
1102
+ // grammar. Returns (node, quantifiable, err).
1103
+ func (p *regexParser) parseEscape(inClass bool) (regexNode, bool, error) {
1104
+ p.pos++ // consume '\'
1105
+ if p.pos >= len(p.src) {
1106
+ return nil, false, p.errorEOF()
1107
+ }
1108
+ r := p.src[p.pos]
1109
+ switch r {
1110
+ case 'd', 'D', 's', 'S', 'w', 'W':
1111
+ p.pos++
1112
+ return &regexCharNode{
1113
+ Value: "\\" + string(r), Kind: "meta",
1114
+ CodePointState: regexFieldNaN,
1115
+ }, true, nil
1116
+ case 'n', 'r', 't', 'v', 'f':
1117
+ p.pos++
1118
+ var symbol rune
1119
+ switch r {
1120
+ case 'n':
1121
+ symbol = '\n'
1122
+ case 'r':
1123
+ symbol = '\r'
1124
+ case 't':
1125
+ symbol = '\t'
1126
+ case 'v':
1127
+ symbol = '\v'
1128
+ case 'f':
1129
+ symbol = '\f'
1130
+ }
1131
+ return &regexCharNode{
1132
+ Value: "\\" + string(r), Kind: "meta",
1133
+ Symbol: string(symbol), SymbolState: regexFieldValue,
1134
+ CodePoint: int(symbol), CodePointState: regexFieldValue,
1135
+ }, true, nil
1136
+ case 'b':
1137
+ p.pos++
1138
+ if inClass {
1139
+ // [\b] is backspace. regexp-tree models it as a meta Char with a
1140
+ // placeholder symbol and NaN code point, which keeps every
1141
+ // transform away from it; mirror that.
1142
+ return &regexCharNode{
1143
+ Value: "\\b", Kind: "meta",
1144
+ Symbol: ".", SymbolState: regexFieldValue,
1145
+ CodePointState: regexFieldNaN,
1146
+ }, true, nil
1147
+ }
1148
+ return &regexAssertionNode{Kind: "\\b"}, false, nil
1149
+ case 'B':
1150
+ p.pos++
1151
+ if inClass {
1152
+ // Annex B: `[\B]` is an identity escape.
1153
+ return escapedSimpleChar('B'), true, nil
1154
+ }
1155
+ return &regexAssertionNode{Kind: "\\B"}, false, nil
1156
+ case 'c':
1157
+ if p.pos+1 < len(p.src) && isASCIILetter(p.src[p.pos+1]) {
1158
+ value := "\\c" + string(p.src[p.pos+1])
1159
+ p.pos += 2
1160
+ return &regexCharNode{Value: value, Kind: "control"}, true, nil
1161
+ }
1162
+ // SAFETY: a dangling `\c` matches the two characters `\` and `c` in
1163
+ // JavaScript (Annex B). regexp-tree unescapes it to a bare `c`,
1164
+ // corrupting the match; keep it as an inert control-kind Char that
1165
+ // regenerates verbatim instead.
1166
+ p.pos++
1167
+ return &regexCharNode{Value: "\\c", Kind: "control"}, true, nil
1168
+ case 'x':
1169
+ if p.pos+2 < len(p.src) && isHexDigit(p.src[p.pos+1]) && isHexDigit(p.src[p.pos+2]) {
1170
+ hex := string(p.src[p.pos+1 : p.pos+3])
1171
+ cp, _ := strconv.ParseInt(hex, 16, 32)
1172
+ value := "\\x" + hex
1173
+ p.pos += 3
1174
+ return &regexCharNode{
1175
+ Value: value, Kind: "hex",
1176
+ Symbol: string(rune(cp)), SymbolState: regexFieldValue,
1177
+ CodePoint: int(cp), CodePointState: regexFieldValue,
1178
+ }, true, nil
1179
+ }
1180
+ if p.uMode || p.vMode {
1181
+ return nil, false, p.errorAt(p.pos-1, "invalid hexadecimal escape")
1182
+ }
1183
+ p.pos++
1184
+ return escapedSimpleChar('x'), true, nil
1185
+ case 'u':
1186
+ node, ok, err := p.parseUnicodeEscape()
1187
+ if err != nil {
1188
+ return nil, false, err
1189
+ }
1190
+ if ok {
1191
+ return node, true, nil
1192
+ }
1193
+ if p.uMode || p.vMode {
1194
+ return nil, false, p.errorAt(p.pos-1, "invalid unicode escape")
1195
+ }
1196
+ p.pos++
1197
+ return escapedSimpleChar('u'), true, nil
1198
+ case 'p', 'P':
1199
+ if p.uMode || p.vMode {
1200
+ return p.parseUnicodeProperty(r == 'P')
1201
+ }
1202
+ p.pos++
1203
+ return escapedSimpleChar(r), true, nil
1204
+ case 'k':
1205
+ if !inClass && len(p.groupNames) > 0 {
1206
+ if node, ok := p.tryParseNamedBackreference(); ok {
1207
+ return node, true, nil
1208
+ }
1209
+ }
1210
+ p.pos++
1211
+ return escapedSimpleChar('k'), true, nil
1212
+ case '0':
1213
+ // `\0` (not followed by another digit) is NUL, kind "decimal" in
1214
+ // regexp-tree. With a following octal digit it is a legacy octal
1215
+ // escape.
1216
+ if p.pos+1 >= len(p.src) || !isOctalDigit(p.src[p.pos+1]) {
1217
+ p.pos++
1218
+ return &regexCharNode{
1219
+ Value: "\\0", Kind: "decimal",
1220
+ Symbol: "\x00", SymbolState: regexFieldValue,
1221
+ CodePoint: 0, CodePointState: regexFieldValue,
1222
+ }, true, nil
1223
+ }
1224
+ return p.parseLegacyOctal()
1225
+ case '1', '2', '3', '4', '5', '6', '7', '8', '9':
1226
+ if !inClass {
1227
+ // DecimalEscape: a backreference when the whole decimal run does
1228
+ // not exceed the pattern's capture-group count.
1229
+ end := p.pos
1230
+ for end < len(p.src) && p.src[end] >= '0' && p.src[end] <= '9' {
1231
+ end++
1232
+ }
1233
+ number, err := strconv.Atoi(string(p.src[p.pos:end]))
1234
+ if err == nil && number <= p.groupCount {
1235
+ p.pos = end
1236
+ return &regexBackreferenceNode{Kind: "number", Number: number}, true, nil
1237
+ }
1238
+ }
1239
+ if r == '8' || r == '9' {
1240
+ // Annex B: `\8` and `\9` match the plain digits. regexp-tree gives
1241
+ // them an inert "decimal" kind so no transform touches them; mirror
1242
+ // that with a NaN code point (regexp-tree's bogus non-digit code
1243
+ // point could merge them into unrelated ranges).
1244
+ p.pos++
1245
+ return &regexCharNode{
1246
+ Value: "\\" + string(r), Kind: "decimal",
1247
+ CodePointState: regexFieldNaN,
1248
+ }, true, nil
1249
+ }
1250
+ return p.parseLegacyOctal()
1251
+ default:
1252
+ if p.uMode || p.vMode {
1253
+ // In unicode modes only syntax characters may be identity-escaped.
1254
+ if !strings.ContainsRune("^$\\.*+?()[]{}|/-", r) {
1255
+ return nil, false, p.errorAt(p.pos-1, fmt.Sprintf("invalid identity escape %q", "\\"+string(r)))
1256
+ }
1257
+ }
1258
+ p.pos++
1259
+ return escapedSimpleChar(r), true, nil
1260
+ }
1261
+ }
1262
+
1263
+ // parseLegacyOctal consumes an Annex B LegacyOctalEscapeSequence starting
1264
+ // at the current digit. SAFETY: regexp-tree parses multi-digit `\NNN`
1265
+ // escapes as *decimal* character codes, so its optimizer rewrites /\101/
1266
+ // (octal for "A") into /e/ (decimal 101); this port applies the real
1267
+ // ECMAScript octal semantics so the rewrite stays meaning-preserving.
1268
+ func (p *regexParser) parseLegacyOctal() (regexNode, bool, error) {
1269
+ start := p.pos
1270
+ first := p.src[p.pos]
1271
+ length := 1
1272
+ if first <= '3' {
1273
+ for length < 3 && p.pos+length < len(p.src) && isOctalDigit(p.src[p.pos+length]) {
1274
+ length++
1275
+ }
1276
+ } else {
1277
+ if p.pos+1 < len(p.src) && isOctalDigit(p.src[p.pos+1]) {
1278
+ length = 2
1279
+ }
1280
+ }
1281
+ digits := string(p.src[start : start+length])
1282
+ cp, _ := strconv.ParseInt(digits, 8, 32)
1283
+ p.pos += length
1284
+ return &regexCharNode{
1285
+ Value: "\\" + digits, Kind: "oct",
1286
+ Symbol: string(rune(cp)), SymbolState: regexFieldValue,
1287
+ CodePoint: int(cp), CodePointState: regexFieldValue,
1288
+ }, true, nil
1289
+ }
1290
+
1291
+ // parseUnicodeEscape handles `\uXXXX` and, in u/v mode, `\u{...}` plus
1292
+ // surrogate-pair combining. Positioned on the `u`. Returns ok=false when
1293
+ // the escape body is malformed (Annex B identity fallback).
1294
+ func (p *regexParser) parseUnicodeEscape() (regexNode, bool, error) {
1295
+ braces := p.uMode || p.vMode
1296
+ body := p.src[p.pos+1:]
1297
+ r, consumed, ok := decodeUnicodeEscapeBody(body, braces)
1298
+ if !ok {
1299
+ return nil, false, nil
1300
+ }
1301
+ raw := "\\u" + string(body[:consumed])
1302
+ if braces && consumed >= 1 && body[0] != '{' && isHighSurrogate(int(r)) {
1303
+ // Try to combine a trailing low surrogate escape into one code point,
1304
+ // the way regexp-tree and the unicode-mode grammar do.
1305
+ rest := body[consumed:]
1306
+ if len(rest) >= 2 && rest[0] == '\\' && rest[1] == 'u' {
1307
+ low, lowConsumed, lowOK := decodeUnicodeEscapeBody(rest[2:], false)
1308
+ if lowOK && isLowSurrogate(int(low)) {
1309
+ combined := 0x10000 + (int(r)-0xd800)*0x400 + (int(low) - 0xdc00)
1310
+ value := raw + "\\u" + string(rest[2:2+lowConsumed])
1311
+ p.pos += 1 + consumed + 2 + lowConsumed
1312
+ return &regexCharNode{
1313
+ Value: value, Kind: "unicode",
1314
+ Symbol: string(rune(combined)), SymbolState: regexFieldValue,
1315
+ CodePoint: combined, CodePointState: regexFieldValue,
1316
+ SurrogatePair: true,
1317
+ }, true, nil
1318
+ }
1319
+ }
1320
+ }
1321
+ p.pos += 1 + consumed
1322
+ return &regexCharNode{
1323
+ Value: raw, Kind: "unicode",
1324
+ Symbol: string(r), SymbolState: regexFieldValue,
1325
+ CodePoint: int(r), CodePointState: regexFieldValue,
1326
+ }, true, nil
1327
+ }
1328
+
1329
+ // parseUnicodeProperty parses `\p{...}` / `\P{...}` bodies. Positioned on
1330
+ // the `p`/`P`.
1331
+ func (p *regexParser) parseUnicodeProperty(negative bool) (regexNode, bool, error) {
1332
+ if p.pos+1 >= len(p.src) || p.src[p.pos+1] != '{' {
1333
+ return nil, false, p.errorAt(p.pos-1, "invalid unicode property escape")
1334
+ }
1335
+ end := p.pos + 2
1336
+ for end < len(p.src) && p.src[end] != '}' {
1337
+ end++
1338
+ }
1339
+ if end >= len(p.src) {
1340
+ return nil, false, p.errorEOF()
1341
+ }
1342
+ bodyRunes := p.src[p.pos+2 : end]
1343
+ body := string(bodyRunes)
1344
+ if body == "" || !isUnicodePropertyBody(bodyRunes) {
1345
+ return nil, false, p.errorAt(p.pos-1, "invalid unicode property escape")
1346
+ }
1347
+ p.pos = end + 1
1348
+ node := &regexUnicodePropertyNode{Negative: negative}
1349
+ if eq := strings.IndexByte(body, '='); eq >= 0 {
1350
+ node.Name = body[:eq]
1351
+ node.Value = body[eq+1:]
1352
+ } else {
1353
+ // Bare form: either a General_Category shorthand or a binary
1354
+ // property. The distinction only affects regeneration, and both
1355
+ // regenerate without a name part.
1356
+ node.Name = body
1357
+ node.Value = body
1358
+ node.Binary = true
1359
+ }
1360
+ return node, true, nil
1361
+ }
1362
+
1363
+ func isUnicodePropertyBody(runes []rune) bool {
1364
+ seenEq := false
1365
+ for _, r := range runes {
1366
+ if r == '=' {
1367
+ if seenEq {
1368
+ return false
1369
+ }
1370
+ seenEq = true
1371
+ continue
1372
+ }
1373
+ if !(r == '_' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9') {
1374
+ return false
1375
+ }
1376
+ }
1377
+ return true
1378
+ }
1379
+
1380
+ // tryParseNamedBackreference parses `\k<name>` where at least one named
1381
+ // group exists. Positioned on the `k`. Falls back (returns false) when the
1382
+ // name is missing or unknown, matching regexp-tree's literal fallback.
1383
+ func (p *regexParser) tryParseNamedBackreference() (regexNode, bool) {
1384
+ if p.pos+1 >= len(p.src) || p.src[p.pos+1] != '<' {
1385
+ return nil, false
1386
+ }
1387
+ end := p.pos + 2
1388
+ for end < len(p.src) && p.src[end] != '>' {
1389
+ end++
1390
+ }
1391
+ if end >= len(p.src) {
1392
+ return nil, false
1393
+ }
1394
+ name, raw, err := p.decodeGroupName(p.src[p.pos+2 : end])
1395
+ if err != nil || !p.groupNames[name] {
1396
+ return nil, false
1397
+ }
1398
+ p.pos = end + 1
1399
+ return &regexBackreferenceNode{
1400
+ Kind: "name", Reference: name, ReferenceRaw: raw,
1401
+ }, true
1402
+ }
1403
+
1404
+ // parseCharacterClass parses `[...]`. In v mode, classes that use set
1405
+ // notation are captured as opaque nodes.
1406
+ func (p *regexParser) parseCharacterClass() (regexNode, error) {
1407
+ start := p.pos
1408
+ if p.vMode {
1409
+ if node, handled, err := p.tryParseOpaqueClassSet(); handled {
1410
+ return node, err
1411
+ }
1412
+ }
1413
+ p.pos++ // consume '['
1414
+ node := &regexClassNode{LocStart: start}
1415
+ if p.pos < len(p.src) && p.src[p.pos] == '^' {
1416
+ node.Negative = true
1417
+ p.pos++
1418
+ }
1419
+ for {
1420
+ if p.pos >= len(p.src) {
1421
+ return nil, p.errorEOF()
1422
+ }
1423
+ if p.src[p.pos] == ']' {
1424
+ p.pos++
1425
+ node.LocEnd = p.pos
1426
+ return node, nil
1427
+ }
1428
+ atom, err := p.parseClassAtom()
1429
+ if err != nil {
1430
+ return nil, err
1431
+ }
1432
+ // Try to form a ClassRange: `atom - atom` where the dash is not the
1433
+ // final character before `]`.
1434
+ if p.pos+1 < len(p.src) && p.src[p.pos] == '-' && p.src[p.pos+1] != ']' {
1435
+ fromChar, fromIsChar := atom.(*regexCharNode)
1436
+ if fromIsChar {
1437
+ p.pos++ // consume '-'
1438
+ toAtom, err := p.parseClassAtom()
1439
+ if err != nil {
1440
+ return nil, err
1441
+ }
1442
+ toChar, toIsChar := toAtom.(*regexCharNode)
1443
+ if !toIsChar {
1444
+ return nil, p.errorAt(p.pos, "invalid character class range")
1445
+ }
1446
+ if !fromChar.codePointIsNaN() && !toChar.codePointIsNaN() &&
1447
+ fromChar.CodePoint > toChar.CodePoint {
1448
+ return nil, &regexParseError{
1449
+ Message: fmt.Sprintf("range %s-%s out of order in character class",
1450
+ fromChar.Value, toChar.Value),
1451
+ }
1452
+ }
1453
+ node.Expressions = append(node.Expressions, &regexClassRangeNode{From: fromChar, To: toChar})
1454
+ continue
1455
+ }
1456
+ }
1457
+ node.Expressions = append(node.Expressions, atom)
1458
+ }
1459
+ }
1460
+
1461
+ // tryParseOpaqueClassSet detects v-mode set notation (nested classes,
1462
+ // `--`/`&&` operators, `\q{...}`) and consumes the whole class verbatim.
1463
+ // Returns handled=false when the class body is flat and the normal parser
1464
+ // should read it.
1465
+ func (p *regexParser) tryParseOpaqueClassSet() (regexNode, bool, error) {
1466
+ depth := 0
1467
+ exotic := false
1468
+ i := p.pos
1469
+ for i < len(p.src) {
1470
+ switch p.src[i] {
1471
+ case '\\':
1472
+ if i+1 < len(p.src) && p.src[i+1] == 'q' {
1473
+ exotic = true
1474
+ }
1475
+ i++
1476
+ case '[':
1477
+ depth++
1478
+ if depth > 1 {
1479
+ exotic = true
1480
+ }
1481
+ case ']':
1482
+ depth--
1483
+ if depth == 0 {
1484
+ if !exotic {
1485
+ return nil, false, nil
1486
+ }
1487
+ raw := string(p.src[p.pos : i+1])
1488
+ node := &regexClassSetNode{Raw: raw, LocStart: p.pos, LocEnd: i + 1}
1489
+ p.pos = i + 1
1490
+ return node, true, nil
1491
+ }
1492
+ case '-':
1493
+ if i+1 < len(p.src) && p.src[i+1] == '-' {
1494
+ exotic = true
1495
+ }
1496
+ case '&':
1497
+ if i+1 < len(p.src) && p.src[i+1] == '&' {
1498
+ exotic = true
1499
+ }
1500
+ }
1501
+ i++
1502
+ }
1503
+ return nil, true, p.errorEOF()
1504
+ }
1505
+
1506
+ // parseClassAtom parses one class member (a char or escape).
1507
+ func (p *regexParser) parseClassAtom() (regexNode, error) {
1508
+ r := p.src[p.pos]
1509
+ if r == '\\' {
1510
+ node, _, err := p.parseEscape(true)
1511
+ if err != nil {
1512
+ return nil, err
1513
+ }
1514
+ return node, nil
1515
+ }
1516
+ p.pos++
1517
+ return simpleChar(r), nil
1518
+ }
1519
+
1520
+ func isASCIILetter(r rune) bool {
1521
+ return r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z'
1522
+ }
1523
+
1524
+ func isHexDigit(r rune) bool {
1525
+ return r >= '0' && r <= '9' || r >= 'a' && r <= 'f' || r >= 'A' && r <= 'F'
1526
+ }
1527
+
1528
+ func isOctalDigit(r rune) bool {
1529
+ return r >= '0' && r <= '7'
1530
+ }
1531
+
1532
+ func isHighSurrogate(cp int) bool { return cp >= 0xd800 && cp <= 0xdbff }
1533
+ func isLowSurrogate(cp int) bool { return cp >= 0xdc00 && cp <= 0xdfff }
1534
+
1535
+ // regexUTF16Length measures a string in UTF-16 code units, matching the
1536
+ // JavaScript String#length the upstream optimizer's shorter-or-equal
1537
+ // rollback guard compares.
1538
+ func regexUTF16Length(s string) int {
1539
+ n := 0
1540
+ for _, r := range s {
1541
+ n += len(utf16.Encode([]rune{r}))
1542
+ }
1543
+ return n
1544
+ }