@ttsc/lint 0.18.4 → 0.19.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.
Files changed (188) hide show
  1. package/README.md +125 -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 +159 -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 +231 -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 +206 -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,1685 @@
1
+ // regex_tree_optimizer.go is a behavioral port of regexp-tree's optimizer —
2
+ // the engine behind the upstream unicorn/better-regex rule. Each transform
3
+ // below mirrors one module under regexp-tree/src/optimizer/transforms, and
4
+ // the pipeline reproduces the upstream driver: run every transform once per
5
+ // round, accept a transform's whole-tree mutation only when the regenerated
6
+ // literal is not longer (measured in UTF-16 units, the way JavaScript's
7
+ // String#length measures it), and repeat rounds until a full round changes
8
+ // nothing.
9
+ //
10
+ // Fidelity notes:
11
+ // - The accept/rollback bookkeeping intentionally replicates the upstream
12
+ // aliasing behavior (a rejected transform that runs after an accepted
13
+ // one in the same round leaves its mutation on the working tree while
14
+ // the accepted snapshot string stays authoritative); see regexOptimize.
15
+ // - Node equality goes through regexEqualityKey, which reproduces the
16
+ // JSON-encoding distinctions upstream equality checks rely on (field
17
+ // presence and construction-site key order), so the same duplicates
18
+ // collapse and the same near-duplicates survive.
19
+ // - Deviations are safety fixes marked "SAFETY:"; each prevents a rewrite
20
+ // that would change what the regex matches.
21
+ package linthost
22
+
23
+ import (
24
+ "fmt"
25
+ "math"
26
+ "sort"
27
+ "strings"
28
+ "unicode"
29
+ )
30
+
31
+ // regexOptimizeLiteral runs the full optimizer over a `/pattern/flags`
32
+ // literal string and returns the optimized literal.
33
+ func regexOptimizeLiteral(literal string, blacklist map[string]bool) (string, error) {
34
+ ast, err := regexParseLiteral(literal)
35
+ if err != nil {
36
+ return "", err
37
+ }
38
+ return regexOptimize(ast, blacklist), nil
39
+ }
40
+
41
+ type regexTransform struct {
42
+ name string
43
+ shouldRun func(*regexRegExpNode) bool
44
+ run func(*regexRegExpNode)
45
+ }
46
+
47
+ var regexTransformList = []regexTransform{
48
+ {
49
+ name: "charSurrogatePairToSingleUnicode",
50
+ shouldRun: func(re *regexRegExpNode) bool { return strings.ContainsRune(re.Flags, 'u') },
51
+ run: regexTransformSurrogatePairs,
52
+ },
53
+ {name: "charCodeToSimpleChar", run: regexTransformCharCodeToSimple},
54
+ {
55
+ name: "charCaseInsensitiveLowerCaseTransform",
56
+ shouldRun: func(re *regexRegExpNode) bool { return strings.ContainsRune(re.Flags, 'i') },
57
+ run: regexTransformCaseInsensitiveLower,
58
+ },
59
+ {name: "charClassRemoveDuplicates", run: regexTransformClassRemoveDuplicates},
60
+ {name: "quantifiersMerge", run: regexTransformQuantifiersMerge},
61
+ {name: "quantifierRangeToSymbol", run: regexTransformQuantifierRangeToSymbol},
62
+ {name: "charClassClassrangesToChars", run: regexTransformClassRangesToChars},
63
+ {name: "charClassToMeta", run: regexTransformClassToMeta},
64
+ {name: "charClassToSingleChar", run: regexTransformClassToSingleChar},
65
+ {name: "charEscapeUnescape", run: regexTransformEscapeUnescape},
66
+ {name: "charClassClassrangesMerge", run: regexTransformClassRangesMerge},
67
+ {name: "disjunctionRemoveDuplicates", run: regexTransformDisjunctionRemoveDuplicates},
68
+ {name: "groupSingleCharsToCharClass", run: regexTransformGroupSingleCharsToClass},
69
+ {name: "removeEmptyGroup", run: regexTransformRemoveEmptyGroup},
70
+ {name: "ungroup", run: regexTransformUngroup},
71
+ {name: "combineRepeatingPatterns", run: regexTransformCombineRepeating},
72
+ }
73
+
74
+ // regexOptimize is the upstream optimizer driver. `result` is the last
75
+ // accepted tree and resultStr its snapshot string; `ast` is the working
76
+ // tree. After an acceptance both names alias one tree, exactly like the
77
+ // upstream TransformResult/ast aliasing, so a later rejection in the same
78
+ // round clones the (already mutated) working tree — replicating upstream's
79
+ // observable accept/reject sequence rather than an idealized one.
80
+ func regexOptimize(parsed *regexRegExpNode, blacklist map[string]bool) string {
81
+ result := parsed
82
+ resultStr := regexGenerate(result)
83
+ for {
84
+ prev := resultStr
85
+ ast := regexCloneRegExp(result)
86
+ for _, t := range regexTransformList {
87
+ if blacklist[t.name] {
88
+ continue
89
+ }
90
+ if t.shouldRun != nil && !t.shouldRun(ast) {
91
+ continue
92
+ }
93
+ t.run(ast)
94
+ newStr := regexGenerate(ast)
95
+ if newStr != resultStr {
96
+ if regexUTF16Length(newStr) <= regexUTF16Length(resultStr) {
97
+ result = ast
98
+ resultStr = newStr
99
+ } else {
100
+ ast = regexCloneRegExp(result)
101
+ }
102
+ }
103
+ }
104
+ if resultStr == prev {
105
+ break
106
+ }
107
+ }
108
+ return resultStr
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Generic pre-order walker
113
+ // ---------------------------------------------------------------------------
114
+
115
+ // regexIterState carries a live list-iteration index that removal/insert
116
+ // helpers adjust, mirroring regexp-tree's traversingIndex bookkeeping.
117
+ type regexIterState struct {
118
+ i int
119
+ }
120
+
121
+ // regexSlot describes where the currently visited node lives so a visitor
122
+ // can replace or remove it, inspect its parent, and mutate siblings.
123
+ type regexSlot struct {
124
+ parent regexNode // enclosing node; the *regexRegExpNode for the body
125
+ list *[]regexNode // non-nil when the node is a list element
126
+ index int // element index when list != nil, else -1
127
+ iter *regexIterState // iteration state for the enclosing list walk
128
+ single *regexNode // non-nil when the node sits in a single-child field
129
+ // rangeFrom/rangeTo point at ClassRange char fields.
130
+ rangeFrom **regexCharNode
131
+ rangeTo **regexCharNode
132
+ }
133
+
134
+ func (s *regexSlot) parentType() string {
135
+ switch s.parent.(type) {
136
+ case *regexRegExpNode:
137
+ return "RegExp"
138
+ case *regexAlternativeNode:
139
+ return "Alternative"
140
+ case *regexDisjunctionNode:
141
+ return "Disjunction"
142
+ case *regexGroupNode:
143
+ return "Group"
144
+ case *regexRepetitionNode:
145
+ return "Repetition"
146
+ case *regexAssertionNode:
147
+ return "Assertion"
148
+ case *regexClassNode:
149
+ return "CharacterClass"
150
+ case *regexClassRangeNode:
151
+ return "ClassRange"
152
+ }
153
+ return ""
154
+ }
155
+
156
+ // get re-reads the node currently in the slot (replacement-aware).
157
+ func (s *regexSlot) get() regexNode {
158
+ switch {
159
+ case s.list != nil:
160
+ if s.index < 0 || s.index >= len(*s.list) {
161
+ return nil
162
+ }
163
+ return (*s.list)[s.index]
164
+ case s.single != nil:
165
+ return *s.single
166
+ case s.rangeFrom != nil:
167
+ return *s.rangeFrom
168
+ case s.rangeTo != nil:
169
+ return *s.rangeTo
170
+ }
171
+ return nil
172
+ }
173
+
174
+ // set replaces the node in the slot.
175
+ func (s *regexSlot) set(node regexNode) {
176
+ switch {
177
+ case s.list != nil:
178
+ (*s.list)[s.index] = node
179
+ case s.single != nil:
180
+ *s.single = node
181
+ case s.rangeFrom != nil:
182
+ if ch, ok := node.(*regexCharNode); ok {
183
+ *s.rangeFrom = ch
184
+ }
185
+ case s.rangeTo != nil:
186
+ if ch, ok := node.(*regexCharNode); ok {
187
+ *s.rangeTo = ch
188
+ }
189
+ }
190
+ }
191
+
192
+ // remove deletes the node from its slot: list elements are spliced out
193
+ // (adjusting the live iteration index), single-child fields become nil.
194
+ func (s *regexSlot) remove() {
195
+ if s.list != nil {
196
+ regexListRemove(s.list, s.iter, s.index)
197
+ return
198
+ }
199
+ if s.single != nil {
200
+ *s.single = nil
201
+ }
202
+ }
203
+
204
+ func regexListRemove(list *[]regexNode, iter *regexIterState, idx int) {
205
+ if idx < 0 || idx >= len(*list) {
206
+ return
207
+ }
208
+ *list = append((*list)[:idx], (*list)[idx+1:]...)
209
+ if iter != nil && idx <= iter.i {
210
+ iter.i--
211
+ }
212
+ }
213
+
214
+ func regexListInsert(list *[]regexNode, iter *regexIterState, idx int, node regexNode) {
215
+ *list = append(*list, nil)
216
+ copy((*list)[idx+1:], (*list)[idx:])
217
+ (*list)[idx] = node
218
+ if iter != nil && idx <= iter.i {
219
+ iter.i++
220
+ }
221
+ }
222
+
223
+ // regexWalk visits every node pre-order; children are visited in the same
224
+ // property order as regexp-tree's traversal. After the visitor runs, the
225
+ // slot is re-read so a replacement's children are the ones descended into.
226
+ func regexWalk(re *regexRegExpNode, visit func(node regexNode, slot *regexSlot)) {
227
+ slot := &regexSlot{parent: re, single: &re.Body, index: -1}
228
+ regexWalkSlot(slot, visit)
229
+ }
230
+
231
+ func regexWalkSlot(slot *regexSlot, visit func(node regexNode, slot *regexSlot)) {
232
+ node := slot.get()
233
+ if node == nil {
234
+ return
235
+ }
236
+ visit(node, slot)
237
+ node = slot.get() // replacement-aware re-read
238
+ switch n := node.(type) {
239
+ case *regexAlternativeNode:
240
+ regexWalkList(n, &n.Expressions, visit)
241
+ case *regexClassNode:
242
+ regexWalkList(n, &n.Expressions, visit)
243
+ case *regexDisjunctionNode:
244
+ regexWalkSlot(&regexSlot{parent: n, single: &n.Left, index: -1}, visit)
245
+ regexWalkSlot(&regexSlot{parent: n, single: &n.Right, index: -1}, visit)
246
+ case *regexGroupNode:
247
+ regexWalkSlot(&regexSlot{parent: n, single: &n.Expression, index: -1}, visit)
248
+ case *regexRepetitionNode:
249
+ regexWalkSlot(&regexSlot{parent: n, single: &n.Expression, index: -1}, visit)
250
+ if n.Quantifier != nil {
251
+ visit(n.Quantifier, &regexSlot{parent: n, index: -1})
252
+ }
253
+ case *regexAssertionNode:
254
+ regexWalkSlot(&regexSlot{parent: n, single: &n.Assertion, index: -1}, visit)
255
+ case *regexClassRangeNode:
256
+ regexWalkSlot(&regexSlot{parent: n, rangeFrom: &n.From, index: -1}, visit)
257
+ regexWalkSlot(&regexSlot{parent: n, rangeTo: &n.To, index: -1}, visit)
258
+ }
259
+ }
260
+
261
+ func regexWalkList(parent regexNode, list *[]regexNode, visit func(node regexNode, slot *regexSlot)) {
262
+ iter := &regexIterState{}
263
+ for iter.i = 0; iter.i < len(*list); iter.i++ {
264
+ slot := &regexSlot{parent: parent, list: list, index: iter.i, iter: iter}
265
+ regexWalkSlot(slot, visit)
266
+ }
267
+ }
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // charSurrogatePairToSingleUnicode
271
+ // ---------------------------------------------------------------------------
272
+
273
+ // 🚀 -> \u{1f680} (u flag only; the rule skips u-flag literals, so
274
+ // this stays for constructor-path completeness and pipeline parity).
275
+ func regexTransformSurrogatePairs(re *regexRegExpNode) {
276
+ regexWalk(re, func(node regexNode, _ *regexSlot) {
277
+ ch, ok := node.(*regexCharNode)
278
+ if !ok || ch.Kind != "unicode" || !ch.SurrogatePair || ch.codePointIsNaN() {
279
+ return
280
+ }
281
+ ch.Value = fmt.Sprintf("\\u{%x}", ch.CodePoint)
282
+ ch.SurrogatePair = false
283
+ })
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // charCodeToSimpleChar
288
+ // ---------------------------------------------------------------------------
289
+
290
+ // a -> a
291
+ func regexTransformCharCodeToSimple(re *regexRegExpNode) {
292
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
293
+ ch, ok := node.(*regexCharNode)
294
+ if !ok || ch.codePointIsNaN() || ch.Kind == "simple" {
295
+ return
296
+ }
297
+ parentType := slot.parentType()
298
+ if cr, isRange := slot.parent.(*regexClassRangeNode); isRange {
299
+ if !regexIsSimpleRange(cr) {
300
+ return
301
+ }
302
+ }
303
+ if ch.CodePoint < 0x20 || ch.CodePoint > 0x7e {
304
+ return
305
+ }
306
+ symbol := string(rune(ch.CodePoint))
307
+ newChar := &regexCharNode{
308
+ Value: symbol, Kind: "simple",
309
+ Symbol: symbol, SymbolState: regexFieldValue,
310
+ CodePoint: ch.CodePoint, CodePointState: regexFieldValue,
311
+ AltKeyOrder: true,
312
+ }
313
+ if regexNeedsEscape(symbol, parentType) {
314
+ newChar.Escaped = true
315
+ newChar.EscapedState = regexFieldValue
316
+ }
317
+ slot.set(newChar)
318
+ })
319
+ }
320
+
321
+ // regexIsSimpleRange reports whether a range lies within 0-9, a-z, or A-Z.
322
+ func regexIsSimpleRange(cr *regexClassRangeNode) bool {
323
+ if cr.From.codePointIsNaN() || cr.To.codePointIsNaN() {
324
+ return false
325
+ }
326
+ from, to := cr.From.CodePoint, cr.To.CodePoint
327
+ within := func(lo, hi int) bool { return from >= lo && from <= hi && to >= lo && to <= hi }
328
+ return within('0', '9') || within('A', 'Z') || within('a', 'z')
329
+ }
330
+
331
+ func regexNeedsEscape(symbol, parentType string) bool {
332
+ if parentType == "ClassRange" || parentType == "CharacterClass" {
333
+ return strings.ContainsAny(symbol, "]\\^-")
334
+ }
335
+ return strings.ContainsAny(symbol, "*[()+?^$./\\|{}")
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // charCaseInsensitiveLowerCaseTransform
340
+ // ---------------------------------------------------------------------------
341
+
342
+ // /AaBb/i -> /aabb/i
343
+ func regexTransformCaseInsensitiveLower(re *regexRegExpNode) {
344
+ hasUFlag := strings.ContainsRune(re.Flags, 'u')
345
+ azRanges := map[*regexClassRangeNode]bool{}
346
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
347
+ ch, ok := node.(*regexCharNode)
348
+ if !ok || ch.codePointIsNaN() {
349
+ return
350
+ }
351
+ if !hasUFlag && ch.CodePoint >= 0x1000 {
352
+ // Case-insensitive matching without the u flag is unreliable
353
+ // above က in engines; upstream skips those too.
354
+ return
355
+ }
356
+ if cr, isRange := slot.parent.(*regexClassRangeNode); isRange {
357
+ if !azRanges[cr] && !regexIsAZClassRange(cr) {
358
+ return
359
+ }
360
+ azRanges[cr] = true
361
+ }
362
+ r := rune(ch.CodePoint)
363
+ // SAFETY: U+0130 (İ) is the one uppercase letter whose full lowercase
364
+ // mapping is multi-character ("i" + combining dot). Upstream lowercases
365
+ // it through String#toLowerCase and then truncates to the first code
366
+ // point, silently dropping the combining mark; skip it instead.
367
+ if r == 0x130 {
368
+ return
369
+ }
370
+ lower := unicode.ToLower(r)
371
+ if lower == r {
372
+ return
373
+ }
374
+ ch.Value = regexDisplaySymbolAsValue(lower, ch)
375
+ ch.Symbol = string(lower)
376
+ ch.SymbolState = regexFieldValue
377
+ ch.CodePoint = int(lower)
378
+ ch.CodePointState = regexFieldValue
379
+ })
380
+ }
381
+
382
+ func regexIsAZClassRange(cr *regexClassRangeNode) bool {
383
+ if cr.From.codePointIsNaN() || cr.To.codePointIsNaN() {
384
+ return false
385
+ }
386
+ return cr.From.CodePoint >= 'A' && cr.From.CodePoint <= 'Z' &&
387
+ cr.To.CodePoint >= 'A' && cr.To.CodePoint <= 'Z'
388
+ }
389
+
390
+ // regexDisplaySymbolAsValue re-spells a code point in the same escape
391
+ // family the original char used.
392
+ func regexDisplaySymbolAsValue(r rune, node *regexCharNode) string {
393
+ cp := int(r)
394
+ switch node.Kind {
395
+ case "decimal":
396
+ return fmt.Sprintf("\\%d", cp)
397
+ case "oct":
398
+ // SAFETY: upstream prints "\0" + octal digits, which misparses for
399
+ // values above 0o77 (the leading 0 caps the legacy octal at three
400
+ // digits). Print the plain legacy octal spelling instead.
401
+ return fmt.Sprintf("\\%o", cp)
402
+ case "hex":
403
+ return fmt.Sprintf("\\x%x", cp)
404
+ case "unicode":
405
+ if node.SurrogatePair {
406
+ lead := 0xd800 + (cp-0x10000)/0x400
407
+ trail := 0xdc00 + (cp-0x10000)%0x400
408
+ return fmt.Sprintf("\\u%04x\\u%04x", lead, trail)
409
+ }
410
+ if strings.Contains(node.Value, "{") {
411
+ return fmt.Sprintf("\\u{%x}", cp)
412
+ }
413
+ return fmt.Sprintf("\\u%04x", cp)
414
+ }
415
+ return string(r)
416
+ }
417
+
418
+ // ---------------------------------------------------------------------------
419
+ // charClassRemoveDuplicates
420
+ // ---------------------------------------------------------------------------
421
+
422
+ // [\d\d] -> [\d]
423
+ func regexTransformClassRemoveDuplicates(re *regexRegExpNode) {
424
+ regexWalk(re, func(node regexNode, _ *regexSlot) {
425
+ class, ok := node.(*regexClassNode)
426
+ if !ok {
427
+ return
428
+ }
429
+ seen := map[string]bool{}
430
+ for i := 0; i < len(class.Expressions); i++ {
431
+ key := regexEqualityKey(class.Expressions[i])
432
+ if seen[key] {
433
+ class.Expressions = append(class.Expressions[:i], class.Expressions[i+1:]...)
434
+ i--
435
+ continue
436
+ }
437
+ seen[key] = true
438
+ }
439
+ })
440
+ }
441
+
442
+ // ---------------------------------------------------------------------------
443
+ // quantifiersMerge
444
+ // ---------------------------------------------------------------------------
445
+
446
+ // a{1,2}a{2,3} -> a{3,5}
447
+ func regexTransformQuantifiersMerge(re *regexRegExpNode) {
448
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
449
+ rep, ok := node.(*regexRepetitionNode)
450
+ if !ok {
451
+ return
452
+ }
453
+ if _, inAlt := slot.parent.(*regexAlternativeNode); !inAlt || slot.list == nil || slot.index == 0 {
454
+ return
455
+ }
456
+ prev := (*slot.list)[slot.index-1]
457
+ if prevRep, isRep := prev.(*regexRepetitionNode); isRep {
458
+ if regexEqualityKey(prevRep.Expression) != regexEqualityKey(rep.Expression) {
459
+ return
460
+ }
461
+ prevFrom, prevTo, prevHasTo := regexExtractFromTo(prevRep.Quantifier)
462
+ nodeFrom, nodeTo, nodeHasTo := regexExtractFromTo(rep.Quantifier)
463
+ if prevRep.Quantifier.Greedy != rep.Quantifier.Greedy &&
464
+ !regexIsGreedyOpenRange(prevRep.Quantifier) &&
465
+ !regexIsGreedyOpenRange(rep.Quantifier) {
466
+ return
467
+ }
468
+ q := rep.Quantifier
469
+ q.Kind = "Range"
470
+ q.setFrom(prevFrom + nodeFrom)
471
+ if prevHasTo && nodeHasTo {
472
+ q.setTo(prevTo + nodeTo)
473
+ } else {
474
+ q.deleteTo()
475
+ }
476
+ if regexIsGreedyOpenRange(prevRep.Quantifier) || regexIsGreedyOpenRange(q) {
477
+ q.Greedy = true
478
+ }
479
+ regexListRemove(slot.list, slot.iter, slot.index-1)
480
+ return
481
+ }
482
+ if regexEqualityKey(prev) != regexEqualityKey(rep.Expression) {
483
+ return
484
+ }
485
+ regexIncreaseQuantifierByOne(rep.Quantifier)
486
+ regexListRemove(slot.list, slot.iter, slot.index-1)
487
+ })
488
+ }
489
+
490
+ // setFrom / setTo / deleteTo mirror JavaScript object-key mechanics: a
491
+ // (re)assigned key that is absent appends to the key order, deletion
492
+ // removes it. FieldOrder feeds the equality encoding only.
493
+ func (q *regexQuantifierNode) setFrom(v int) {
494
+ q.From = v
495
+ if !strings.ContainsRune(q.FieldOrder, 'f') {
496
+ q.FieldOrder += "f"
497
+ }
498
+ }
499
+
500
+ func (q *regexQuantifierNode) setTo(v int) {
501
+ q.To = v
502
+ q.HasTo = true
503
+ if !strings.ContainsRune(q.FieldOrder, 't') {
504
+ q.FieldOrder += "t"
505
+ }
506
+ }
507
+
508
+ func (q *regexQuantifierNode) deleteTo() {
509
+ q.HasTo = false
510
+ q.FieldOrder = strings.ReplaceAll(q.FieldOrder, "t", "")
511
+ }
512
+
513
+ func (q *regexQuantifierNode) deleteFrom() {
514
+ q.FieldOrder = strings.ReplaceAll(q.FieldOrder, "f", "")
515
+ }
516
+
517
+ func (q *regexQuantifierNode) hasFrom() bool {
518
+ return strings.ContainsRune(q.FieldOrder, 'f')
519
+ }
520
+
521
+ // regexExtractFromTo mirrors the upstream extractFromTo, including the
522
+ // JavaScript falsiness of a zero `to` (treated as absent).
523
+ func regexExtractFromTo(q *regexQuantifierNode) (int, int, bool) {
524
+ switch q.Kind {
525
+ case "*":
526
+ return 0, 0, false
527
+ case "+":
528
+ return 1, 0, false
529
+ case "?":
530
+ return 0, 1, true
531
+ }
532
+ if q.HasTo && q.To != 0 {
533
+ return q.From, q.To, true
534
+ }
535
+ return q.From, 0, false
536
+ }
537
+
538
+ func regexIsGreedyOpenRange(q *regexQuantifierNode) bool {
539
+ return q.Greedy &&
540
+ (q.Kind == "+" || q.Kind == "*" ||
541
+ (q.Kind == "Range" && (!q.HasTo || q.To == 0)))
542
+ }
543
+
544
+ // regexIncreaseQuantifierByOne mirrors transform/utils.js.
545
+ func regexIncreaseQuantifierByOne(q *regexQuantifierNode) {
546
+ switch q.Kind {
547
+ case "*":
548
+ q.Kind = "+"
549
+ case "+":
550
+ q.Kind = "Range"
551
+ q.setFrom(2)
552
+ q.deleteTo()
553
+ case "?":
554
+ q.Kind = "Range"
555
+ q.setFrom(1)
556
+ q.setTo(2)
557
+ case "Range":
558
+ q.From++
559
+ if q.HasTo && q.To != 0 {
560
+ q.To++
561
+ }
562
+ }
563
+ }
564
+
565
+ // ---------------------------------------------------------------------------
566
+ // quantifierRangeToSymbol
567
+ // ---------------------------------------------------------------------------
568
+
569
+ // a{0,} -> a*, a{1,} -> a+, a{1} -> a
570
+ //
571
+ // Note: regexp-tree 0.1.27 (the release the upstream rule depended on) has
572
+ // no `a{0,1} -> a?` rewrite; that arrived on master only. `{0,1}` stays.
573
+ func regexTransformQuantifierRangeToSymbol(re *regexRegExpNode) {
574
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
575
+ rep, ok := node.(*regexRepetitionNode)
576
+ if !ok || rep.Quantifier == nil || rep.Quantifier.Kind != "Range" {
577
+ return
578
+ }
579
+ q := rep.Quantifier
580
+ truthyTo := q.HasTo && q.To != 0
581
+ // a{0,} -> a*
582
+ if q.hasFrom() && q.From == 0 && !truthyTo {
583
+ q.Kind = "*"
584
+ q.deleteFrom()
585
+ return
586
+ }
587
+ // a{1,} -> a+
588
+ if q.hasFrom() && q.From == 1 && !truthyTo {
589
+ q.Kind = "+"
590
+ q.deleteFrom()
591
+ return
592
+ }
593
+ // a{1} -> a
594
+ if q.hasFrom() && q.From == 1 && q.HasTo && q.To == 1 {
595
+ slot.set(rep.Expression)
596
+ }
597
+ })
598
+ }
599
+
600
+ // ---------------------------------------------------------------------------
601
+ // charClassClassrangesToChars
602
+ // ---------------------------------------------------------------------------
603
+
604
+ // [a-a] -> [a], [a-b] -> [ab]
605
+ func regexTransformClassRangesToChars(re *regexRegExpNode) {
606
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
607
+ cr, ok := node.(*regexClassRangeNode)
608
+ if !ok || slot.list == nil {
609
+ return
610
+ }
611
+ if cr.From.codePointIsNaN() || cr.To.codePointIsNaN() {
612
+ return
613
+ }
614
+ if cr.From.CodePoint == cr.To.CodePoint {
615
+ slot.set(cr.From)
616
+ return
617
+ }
618
+ if cr.From.CodePoint == cr.To.CodePoint-1 {
619
+ regexListInsert(slot.list, slot.iter, slot.index+1, cr.To)
620
+ slot.set(cr.From)
621
+ }
622
+ })
623
+ }
624
+
625
+ // ---------------------------------------------------------------------------
626
+ // charClassToMeta
627
+ // ---------------------------------------------------------------------------
628
+
629
+ // [0-9] -> [\d], [a-zA-Z_0-9] -> [\w], whitespace batch -> [\s]
630
+ func regexTransformClassToMeta(re *regexRegExpNode) {
631
+ hasIFlag := strings.ContainsRune(re.Flags, 'i')
632
+ hasUFlag := strings.ContainsRune(re.Flags, 'u')
633
+ regexWalk(re, func(node regexNode, _ *regexSlot) {
634
+ class, ok := node.(*regexClassNode)
635
+ if !ok {
636
+ return
637
+ }
638
+ regexRewriteNumberRanges(class)
639
+ regexRewriteWordRanges(class, hasIFlag, hasUFlag)
640
+ regexRewriteWhitespaceRanges(class)
641
+ })
642
+ }
643
+
644
+ func regexRewriteNumberRanges(class *regexClassNode) {
645
+ for i, expr := range class.Expressions {
646
+ if cr, ok := expr.(*regexClassRangeNode); ok &&
647
+ cr.From.Value == "0" && cr.To.Value == "9" {
648
+ class.Expressions[i] = &regexCharNode{Value: "\\d", Kind: "meta"}
649
+ }
650
+ }
651
+ }
652
+
653
+ func regexRewriteWordRanges(class *regexClassNode, hasIFlag, hasUFlag bool) {
654
+ numberIdx, lowerIdx, upperIdx, underscoreIdx, u017fIdx, u212aIdx := -1, -1, -1, -1, -1, -1
655
+ for i, expr := range class.Expressions {
656
+ switch {
657
+ case regexIsMetaCharValue(expr, "\\d"):
658
+ numberIdx = i
659
+ case regexIsValueClassRange(expr, "a", "z"):
660
+ lowerIdx = i
661
+ case regexIsValueClassRange(expr, "A", "Z"):
662
+ upperIdx = i
663
+ case regexIsSimpleCharValue(expr, "_"):
664
+ underscoreIdx = i
665
+ case hasIFlag && hasUFlag && regexIsUnicodeCodePoint(expr, 0x017f):
666
+ u017fIdx = i
667
+ case hasIFlag && hasUFlag && regexIsUnicodeCodePoint(expr, 0x212a):
668
+ u212aIdx = i
669
+ }
670
+ }
671
+ if numberIdx < 0 || underscoreIdx < 0 {
672
+ return
673
+ }
674
+ if !((lowerIdx >= 0 && upperIdx >= 0) || (hasIFlag && (lowerIdx >= 0 || upperIdx >= 0))) {
675
+ return
676
+ }
677
+ if hasUFlag && hasIFlag && (u017fIdx < 0 || u212aIdx < 0) {
678
+ return
679
+ }
680
+ removed := map[int]bool{
681
+ lowerIdx: true, upperIdx: true, underscoreIdx: true, u017fIdx: true, u212aIdx: true,
682
+ }
683
+ delete(removed, -1)
684
+ class.Expressions[numberIdx] = &regexCharNode{Value: "\\w", Kind: "meta"}
685
+ kept := class.Expressions[:0]
686
+ for i, expr := range class.Expressions {
687
+ if !removed[i] {
688
+ kept = append(kept, expr)
689
+ }
690
+ }
691
+ class.Expressions = kept
692
+ }
693
+
694
+ // regexWhitespaceClassTests mirrors the upstream whitespaceRangeTests list.
695
+ var regexWhitespaceClassTests = []func(regexNode) bool{
696
+ func(n regexNode) bool { return regexIsSimpleCharValue(n, " ") },
697
+ func(n regexNode) bool { return regexIsMetaCharValue(n, "\\f") },
698
+ func(n regexNode) bool { return regexIsMetaCharValue(n, "\\n") },
699
+ func(n regexNode) bool { return regexIsMetaCharValue(n, "\\r") },
700
+ func(n regexNode) bool { return regexIsMetaCharValue(n, "\\t") },
701
+ func(n regexNode) bool { return regexIsMetaCharValue(n, "\\v") },
702
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0x00a0) },
703
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0x1680) },
704
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0x2028) },
705
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0x2029) },
706
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0x202f) },
707
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0x205f) },
708
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0x3000) },
709
+ func(n regexNode) bool { return regexIsUnicodeCodePoint(n, 0xfeff) },
710
+ func(n regexNode) bool {
711
+ cr, ok := n.(*regexClassRangeNode)
712
+ return ok && regexIsUnicodeCodePoint(cr.From, 0x2000) && regexIsUnicodeCodePoint(cr.To, 0x200a)
713
+ },
714
+ }
715
+
716
+ func regexRewriteWhitespaceRanges(class *regexClassNode) {
717
+ if len(class.Expressions) < len(regexWhitespaceClassTests) {
718
+ return
719
+ }
720
+ for _, test := range regexWhitespaceClassTests {
721
+ matched := false
722
+ for _, expr := range class.Expressions {
723
+ if test(expr) {
724
+ matched = true
725
+ break
726
+ }
727
+ }
728
+ if !matched {
729
+ return
730
+ }
731
+ }
732
+ // Put \s in place of \n.
733
+ for _, expr := range class.Expressions {
734
+ if regexIsMetaCharValue(expr, "\\n") {
735
+ ch := expr.(*regexCharNode)
736
+ ch.Value = "\\s"
737
+ ch.SymbolState = regexFieldAbsent
738
+ ch.CodePointState = regexFieldNaN
739
+ break
740
+ }
741
+ }
742
+ kept := class.Expressions[:0]
743
+ for _, expr := range class.Expressions {
744
+ remove := false
745
+ for _, test := range regexWhitespaceClassTests {
746
+ if test(expr) {
747
+ remove = true
748
+ break
749
+ }
750
+ }
751
+ if !remove {
752
+ kept = append(kept, expr)
753
+ }
754
+ }
755
+ class.Expressions = kept
756
+ }
757
+
758
+ func regexIsSimpleCharValue(n regexNode, value string) bool {
759
+ ch, ok := n.(*regexCharNode)
760
+ return ok && ch.Kind == "simple" && ch.Value == value
761
+ }
762
+
763
+ func regexIsMetaCharValue(n regexNode, value string) bool {
764
+ ch, ok := n.(*regexCharNode)
765
+ return ok && ch.Kind == "meta" && ch.Value == value
766
+ }
767
+
768
+ func regexIsValueClassRange(n regexNode, from, to string) bool {
769
+ cr, ok := n.(*regexClassRangeNode)
770
+ return ok && cr.From.Value == from && cr.To.Value == to
771
+ }
772
+
773
+ func regexIsUnicodeCodePoint(n regexNode, cp int) bool {
774
+ ch, ok := n.(*regexCharNode)
775
+ return ok && ch.Kind == "unicode" && ch.CodePointState == regexFieldValue && ch.CodePoint == cp
776
+ }
777
+
778
+ // ---------------------------------------------------------------------------
779
+ // charClassToSingleChar
780
+ // ---------------------------------------------------------------------------
781
+
782
+ // [\d] -> \d, [^\w] -> \W
783
+ func regexTransformClassToSingleChar(re *regexRegExpNode) {
784
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
785
+ class, ok := node.(*regexClassNode)
786
+ if !ok || len(class.Expressions) != 1 {
787
+ return
788
+ }
789
+ if !regexHasSafeExtractionSiblings(slot) {
790
+ return
791
+ }
792
+ member, isChar := class.Expressions[0].(*regexCharNode)
793
+ if !isChar || member.Value == "\\b" {
794
+ return
795
+ }
796
+ value := member.Value
797
+ if class.Negative {
798
+ if !regexIsInvertibleMeta(value) {
799
+ return
800
+ }
801
+ value = regexInverseMeta(value)
802
+ }
803
+ escaped := member.EscapedState == regexFieldValue && member.Escaped
804
+ slot.set(&regexCharNode{
805
+ Value: value, Kind: member.Kind,
806
+ Escaped: escaped || regexSingleCharShouldEscape(value),
807
+ EscapedState: regexFieldValue, // upstream always writes the key
808
+ })
809
+ })
810
+ }
811
+
812
+ // regexHasSafeExtractionSiblings blocks extraction (and ungrouping) when
813
+ // the previous Alternative sibling ends in a decimal spelling that a bare
814
+ // digit would extend: \1[0] must not become \10.
815
+ func regexHasSafeExtractionSiblings(slot *regexSlot) bool {
816
+ if _, inAlt := slot.parent.(*regexAlternativeNode); !inAlt || slot.list == nil || slot.index == 0 {
817
+ return true
818
+ }
819
+ switch prev := (*slot.list)[slot.index-1].(type) {
820
+ case *regexBackreferenceNode:
821
+ if prev.Kind == "number" {
822
+ return false
823
+ }
824
+ case *regexCharNode:
825
+ // SAFETY: upstream only guards kind "decimal" because regexp-tree
826
+ // labels all legacy \NNN escapes decimal; this parser labels them
827
+ // "oct", so both kinds guard.
828
+ if prev.Kind == "decimal" || prev.Kind == "oct" {
829
+ return false
830
+ }
831
+ }
832
+ return true
833
+ }
834
+
835
+ func regexIsInvertibleMeta(value string) bool {
836
+ if len(value) != 2 || value[0] != '\\' {
837
+ return false
838
+ }
839
+ return strings.ContainsRune("dwsDWS", rune(value[1]))
840
+ }
841
+
842
+ func regexInverseMeta(value string) string {
843
+ r := rune(value[1])
844
+ if strings.ContainsRune("dws", r) {
845
+ return "\\" + strings.ToUpper(string(r))
846
+ }
847
+ return "\\" + strings.ToLower(string(r))
848
+ }
849
+
850
+ // Note: \{ and \} stay escaped so a[{]2[}] does not turn into a{2}.
851
+ func regexSingleCharShouldEscape(value string) bool {
852
+ return len(value) == 1 && strings.ContainsAny(value, "*[()+?$./{}|")
853
+ }
854
+
855
+ // ---------------------------------------------------------------------------
856
+ // charEscapeUnescape
857
+ // ---------------------------------------------------------------------------
858
+
859
+ // \e -> e, [\(] -> [(]
860
+ func regexTransformEscapeUnescape(re *regexRegExpNode) {
861
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
862
+ ch, ok := node.(*regexCharNode)
863
+ if !ok || ch.EscapedState != regexFieldValue || !ch.Escaped {
864
+ return
865
+ }
866
+ if regexShouldUnescape(ch, slot) {
867
+ ch.Escaped = false
868
+ ch.EscapedState = regexFieldAbsent
869
+ }
870
+ })
871
+ }
872
+
873
+ func regexShouldUnescape(ch *regexCharNode, slot *regexSlot) bool {
874
+ parentType := slot.parentType()
875
+ if parentType != "CharacterClass" && parentType != "ClassRange" {
876
+ return !regexPreservesEscape(ch, slot)
877
+ }
878
+ return !regexPreservesInCharClass(ch, slot)
879
+ }
880
+
881
+ // \], \\, \^ (leading, in a positive class), \- keep their escapes inside
882
+ // classes.
883
+ func regexPreservesInCharClass(ch *regexCharNode, slot *regexSlot) bool {
884
+ switch ch.Value {
885
+ case "^":
886
+ class, ok := slot.parent.(*regexClassNode)
887
+ return ok && slot.index == 0 && !class.Negative
888
+ case "-":
889
+ return true
890
+ case "]", "\\":
891
+ return true
892
+ }
893
+ return false
894
+ }
895
+
896
+ func regexPreservesEscape(ch *regexCharNode, slot *regexSlot) bool {
897
+ value := ch.Value
898
+ if value == "{" {
899
+ return regexPreservesOpeningCurlyBraceEscape(slot)
900
+ }
901
+ if value == "}" {
902
+ return regexPreservesClosingCurlyBraceEscape(slot)
903
+ }
904
+ return len(value) == 1 && strings.ContainsAny(value, "*[()+?^$./\\|")
905
+ }
906
+
907
+ // regexConsumeNumbers counts adjacent plain digit chars starting at
908
+ // startIndex, walking right (or left when rtl).
909
+ func regexConsumeNumbers(list []regexNode, startIndex int, rtl bool) int {
910
+ i := startIndex
911
+ count := 0
912
+ for {
913
+ inBounds := i >= 0 && i < len(list)
914
+ if !inBounds {
915
+ break
916
+ }
917
+ ch, ok := list[i].(*regexCharNode)
918
+ if !ok || ch.Kind != "simple" ||
919
+ (ch.EscapedState == regexFieldValue && ch.Escaped) ||
920
+ len(ch.Value) != 1 || ch.Value[0] < '0' || ch.Value[0] > '9' {
921
+ break
922
+ }
923
+ count++
924
+ if rtl {
925
+ i--
926
+ } else {
927
+ i++
928
+ }
929
+ }
930
+ return count
931
+ }
932
+
933
+ func regexIsPlainSimpleChar(n regexNode, value string) bool {
934
+ ch, ok := n.(*regexCharNode)
935
+ return ok && ch.Kind == "simple" &&
936
+ !(ch.EscapedState == regexFieldValue && ch.Escaped) &&
937
+ ch.Value == value
938
+ }
939
+
940
+ // Avoid \{3} or \{3,} or \{3,4} turning into a quantifier.
941
+ func regexPreservesOpeningCurlyBraceEscape(slot *regexSlot) bool {
942
+ if slot.list == nil || slot.index < 0 {
943
+ return false
944
+ }
945
+ list := *slot.list
946
+ index := slot.index
947
+ nbFollowing := regexConsumeNumbers(list, index+1, false)
948
+ i := index + nbFollowing + 1
949
+ if nbFollowing == 0 {
950
+ return false
951
+ }
952
+ if i < len(list) && regexIsPlainSimpleChar(list[i], "}") {
953
+ return true
954
+ }
955
+ if i < len(list) && regexIsPlainSimpleChar(list[i], ",") {
956
+ nbFollowing = regexConsumeNumbers(list, i+1, false)
957
+ i = i + nbFollowing + 1
958
+ return i < len(list) && regexIsPlainSimpleChar(list[i], "}")
959
+ }
960
+ return false
961
+ }
962
+
963
+ // Avoid {3\} or {3,\} turning into a quantifier.
964
+ func regexPreservesClosingCurlyBraceEscape(slot *regexSlot) bool {
965
+ if slot.list == nil || slot.index < 0 {
966
+ return false
967
+ }
968
+ list := *slot.list
969
+ index := slot.index
970
+ nbPreceding := regexConsumeNumbers(list, index-1, true)
971
+ i := index - nbPreceding - 1
972
+ if nbPreceding > 0 && i >= 0 && i < len(list) && regexIsPlainSimpleChar(list[i], "{") {
973
+ return true
974
+ }
975
+ if i >= 0 && i < len(list) && regexIsPlainSimpleChar(list[i], ",") {
976
+ nbPreceding = regexConsumeNumbers(list, i-1, true)
977
+ i = i - nbPreceding - 1
978
+ return nbPreceding > 0 && i >= 0 && i < len(list) && regexIsPlainSimpleChar(list[i], "{")
979
+ }
980
+ return false
981
+ }
982
+
983
+ // ---------------------------------------------------------------------------
984
+ // charClassClassrangesMerge
985
+ // ---------------------------------------------------------------------------
986
+
987
+ // [a-ec] -> [a-e], [\w\da-f] -> [\w], [abcdef] -> [a-f]
988
+ func regexTransformClassRangesMerge(re *regexRegExpNode) {
989
+ hasIUFlags := strings.ContainsRune(re.Flags, 'i') && strings.ContainsRune(re.Flags, 'u')
990
+ regexWalk(re, func(node regexNode, _ *regexSlot) {
991
+ class, ok := node.(*regexClassNode)
992
+ if !ok {
993
+ return
994
+ }
995
+ var metas []string
996
+ for _, expr := range class.Expressions {
997
+ if regexIsMergeMeta(expr, "") {
998
+ metas = append(metas, expr.(*regexCharNode).Value)
999
+ }
1000
+ }
1001
+ sort.SliceStable(class.Expressions, func(i, j int) bool {
1002
+ return regexSortCharClassCompare(class.Expressions[i], class.Expressions[j]) < 0
1003
+ })
1004
+ exprs := &class.Expressions
1005
+ for i := 0; i < len(*exprs); i++ {
1006
+ expr := (*exprs)[i]
1007
+ var prevExpr, nextExpr regexNode
1008
+ if i > 0 {
1009
+ prevExpr = (*exprs)[i-1]
1010
+ }
1011
+ if i+1 < len(*exprs) {
1012
+ nextExpr = (*exprs)[i+1]
1013
+ }
1014
+ if regexFitsInMetas(expr, metas, hasIUFlags) ||
1015
+ regexCombinesWithPrecedingClassRange(expr, prevExpr) ||
1016
+ regexCombinesWithFollowingClassRange(expr, nextExpr) {
1017
+ *exprs = append((*exprs)[:i], (*exprs)[i+1:]...)
1018
+ i--
1019
+ continue
1020
+ }
1021
+ merged := regexCharCombinesWithPrecedingChars(expr, i, exprs)
1022
+ if merged > 0 {
1023
+ *exprs = append((*exprs)[:i-merged+1], (*exprs)[i+1:]...)
1024
+ i -= merged
1025
+ }
1026
+ }
1027
+ })
1028
+ }
1029
+
1030
+ // regexSortCharClassCompare ports sortCharClass; a NaN comparison result is
1031
+ // mapped to 0 (JavaScript's engines treat a NaN comparator result as "keep
1032
+ // order" in their stable sorts).
1033
+ func regexSortCharClassCompare(a, b regexNode) float64 {
1034
+ av := regexClassSortValue(a)
1035
+ bv := regexClassSortValue(b)
1036
+ if av == bv {
1037
+ _, aIsRange := a.(*regexClassRangeNode)
1038
+ _, bIsRange := b.(*regexClassRangeNode)
1039
+ if aIsRange && !bIsRange {
1040
+ return -1
1041
+ }
1042
+ if bIsRange && !aIsRange {
1043
+ return 1
1044
+ }
1045
+ if aIsRange && bIsRange {
1046
+ diff := regexClassSortValue(a.(*regexClassRangeNode).To) - regexClassSortValue(b.(*regexClassRangeNode).To)
1047
+ if math.IsNaN(diff) {
1048
+ return 0
1049
+ }
1050
+ return diff
1051
+ }
1052
+ if (regexIsMergeMeta(a, "") && regexIsMergeMeta(b, "")) ||
1053
+ (regexIsControlChar(a) && regexIsControlChar(b)) {
1054
+ if a.(*regexCharNode).Value < b.(*regexCharNode).Value {
1055
+ return -1
1056
+ }
1057
+ return 1
1058
+ }
1059
+ }
1060
+ diff := av - bv
1061
+ if math.IsNaN(diff) {
1062
+ return 0
1063
+ }
1064
+ return diff
1065
+ }
1066
+
1067
+ func regexClassSortValue(n regexNode) float64 {
1068
+ switch expr := n.(type) {
1069
+ case *regexCharNode:
1070
+ if expr.Value == "-" {
1071
+ return math.Inf(1)
1072
+ }
1073
+ if expr.Kind == "control" {
1074
+ return math.Inf(1)
1075
+ }
1076
+ if expr.Kind == "meta" && expr.codePointIsNaN() {
1077
+ return -1
1078
+ }
1079
+ if expr.codePointIsNaN() {
1080
+ return math.NaN()
1081
+ }
1082
+ return float64(expr.CodePoint)
1083
+ case *regexUnicodePropertyNode:
1084
+ return -1
1085
+ case *regexClassRangeNode:
1086
+ // Upstream reads the raw from.codePoint here (no meta special case),
1087
+ // so a range with a meta endpoint sorts as NaN — order preserved.
1088
+ if expr.From.codePointIsNaN() {
1089
+ return math.NaN()
1090
+ }
1091
+ return float64(expr.From.CodePoint)
1092
+ }
1093
+ return math.NaN()
1094
+ }
1095
+
1096
+ // regexIsMergeMeta reports a Char of kind meta whose value is one of
1097
+ // \d \w \s \D \W \S (or the specific value when given).
1098
+ func regexIsMergeMeta(n regexNode, value string) bool {
1099
+ ch, ok := n.(*regexCharNode)
1100
+ if !ok || ch.Kind != "meta" {
1101
+ return false
1102
+ }
1103
+ if value != "" {
1104
+ return ch.Value == value
1105
+ }
1106
+ return len(ch.Value) == 2 && ch.Value[0] == '\\' &&
1107
+ strings.ContainsRune("dwsDWS", rune(ch.Value[1]))
1108
+ }
1109
+
1110
+ func regexIsControlChar(n regexNode) bool {
1111
+ ch, ok := n.(*regexCharNode)
1112
+ return ok && ch.Kind == "control"
1113
+ }
1114
+
1115
+ func regexFitsInMetas(n regexNode, metas []string, hasIUFlags bool) bool {
1116
+ for _, meta := range metas {
1117
+ if regexFitsInMeta(n, meta, hasIUFlags) {
1118
+ return true
1119
+ }
1120
+ }
1121
+ return false
1122
+ }
1123
+
1124
+ func regexFitsInMeta(n regexNode, meta string, hasIUFlags bool) bool {
1125
+ if cr, ok := n.(*regexClassRangeNode); ok {
1126
+ return regexFitsInMeta(cr.From, meta, hasIUFlags) && regexFitsInMeta(cr.To, meta, hasIUFlags)
1127
+ }
1128
+ // Special containments between meta chars.
1129
+ if meta == "\\S" && (regexIsMergeMeta(n, "\\w") || regexIsMergeMeta(n, "\\d")) {
1130
+ return true
1131
+ }
1132
+ if meta == "\\D" && (regexIsMergeMeta(n, "\\W") || regexIsMergeMeta(n, "\\s")) {
1133
+ return true
1134
+ }
1135
+ if meta == "\\w" && regexIsMergeMeta(n, "\\d") {
1136
+ return true
1137
+ }
1138
+ if meta == "\\W" && regexIsMergeMeta(n, "\\s") {
1139
+ return true
1140
+ }
1141
+ ch, ok := n.(*regexCharNode)
1142
+ if !ok || ch.codePointIsNaN() {
1143
+ return false
1144
+ }
1145
+ switch meta {
1146
+ case "\\s":
1147
+ return regexFitsInMetaS(ch)
1148
+ case "\\S":
1149
+ return !regexFitsInMetaS(ch)
1150
+ case "\\d":
1151
+ return regexFitsInMetaD(ch)
1152
+ case "\\D":
1153
+ return !regexFitsInMetaD(ch)
1154
+ case "\\w":
1155
+ return regexFitsInMetaW(ch, hasIUFlags)
1156
+ case "\\W":
1157
+ return !regexFitsInMetaW(ch, hasIUFlags)
1158
+ }
1159
+ return false
1160
+ }
1161
+
1162
+ func regexFitsInMetaS(ch *regexCharNode) bool {
1163
+ cp := ch.CodePoint
1164
+ return cp == 0x0009 || cp == 0x000a || cp == 0x000b || cp == 0x000c ||
1165
+ cp == 0x000d || cp == 0x0020 || cp == 0x00a0 || cp == 0x1680 ||
1166
+ (cp >= 0x2000 && cp <= 0x200a) ||
1167
+ cp == 0x2028 || cp == 0x2029 || cp == 0x202f || cp == 0x205f ||
1168
+ cp == 0x3000 || cp == 0xfeff
1169
+ }
1170
+
1171
+ func regexFitsInMetaD(ch *regexCharNode) bool {
1172
+ return ch.CodePoint >= 0x30 && ch.CodePoint <= 0x39
1173
+ }
1174
+
1175
+ func regexFitsInMetaW(ch *regexCharNode, hasIUFlags bool) bool {
1176
+ cp := ch.CodePoint
1177
+ return regexFitsInMetaD(ch) ||
1178
+ (cp >= 0x41 && cp <= 0x5a) || (cp >= 0x61 && cp <= 0x7a) ||
1179
+ ch.Value == "_" ||
1180
+ (hasIUFlags && (cp == 0x017f || cp == 0x212a))
1181
+ }
1182
+
1183
+ func regexCombinesWithPrecedingClassRange(expr, preceding regexNode) bool {
1184
+ cr, ok := preceding.(*regexClassRangeNode)
1185
+ if !ok {
1186
+ return false
1187
+ }
1188
+ if regexFitsInClassRange(expr, cr) {
1189
+ // [a-gc] -> [a-g]
1190
+ return true
1191
+ }
1192
+ if ch, isChar := expr.(*regexCharNode); isChar && regexIsMetaWCharOrCode(ch) &&
1193
+ !cr.To.codePointIsNaN() && cr.To.CodePoint == ch.CodePoint-1 {
1194
+ // [a-de] -> [a-e]
1195
+ cr.To = ch
1196
+ return true
1197
+ }
1198
+ if exprRange, isRange := expr.(*regexClassRangeNode); isRange &&
1199
+ !exprRange.From.codePointIsNaN() && !exprRange.To.codePointIsNaN() &&
1200
+ !cr.From.codePointIsNaN() && !cr.To.codePointIsNaN() &&
1201
+ exprRange.From.CodePoint <= cr.To.CodePoint+1 &&
1202
+ exprRange.To.CodePoint >= cr.From.CodePoint-1 {
1203
+ // [a-db-f] -> [a-f]
1204
+ if exprRange.From.CodePoint < cr.From.CodePoint {
1205
+ cr.From = exprRange.From
1206
+ }
1207
+ if exprRange.To.CodePoint > cr.To.CodePoint {
1208
+ cr.To = exprRange.To
1209
+ }
1210
+ return true
1211
+ }
1212
+ return false
1213
+ }
1214
+
1215
+ func regexCombinesWithFollowingClassRange(expr, following regexNode) bool {
1216
+ cr, ok := following.(*regexClassRangeNode)
1217
+ if !ok {
1218
+ return false
1219
+ }
1220
+ // [ab-e] -> [a-e]
1221
+ if ch, isChar := expr.(*regexCharNode); isChar && regexIsMetaWCharOrCode(ch) &&
1222
+ !cr.From.codePointIsNaN() && cr.From.CodePoint == ch.CodePoint+1 {
1223
+ cr.From = ch
1224
+ return true
1225
+ }
1226
+ return false
1227
+ }
1228
+
1229
+ func regexFitsInClassRange(expr regexNode, cr *regexClassRangeNode) bool {
1230
+ if cr.From.codePointIsNaN() || cr.To.codePointIsNaN() {
1231
+ return false
1232
+ }
1233
+ switch n := expr.(type) {
1234
+ case *regexCharNode:
1235
+ if n.codePointIsNaN() {
1236
+ return false
1237
+ }
1238
+ return n.CodePoint >= cr.From.CodePoint && n.CodePoint <= cr.To.CodePoint
1239
+ case *regexClassRangeNode:
1240
+ return regexFitsInClassRange(n.From, cr) && regexFitsInClassRange(n.To, cr)
1241
+ }
1242
+ return false
1243
+ }
1244
+
1245
+ // regexCharCombinesWithPrecedingChars collapses runs of consecutive chars
1246
+ // into a range: [abcdef] -> [a-f]. Returns the number of chars merged.
1247
+ func regexCharCombinesWithPrecedingChars(expr regexNode, index int, exprs *[]regexNode) int {
1248
+ ch, ok := expr.(*regexCharNode)
1249
+ if !ok || !regexIsMetaWCharOrCode(ch) {
1250
+ return 0
1251
+ }
1252
+ merged := 0
1253
+ i := index
1254
+ for i > 0 {
1255
+ current, curOK := (*exprs)[i].(*regexCharNode)
1256
+ preceding, preOK := (*exprs)[i-1].(*regexCharNode)
1257
+ if curOK && preOK && regexIsMetaWCharOrCode(preceding) &&
1258
+ preceding.CodePoint == current.CodePoint-1 {
1259
+ merged++
1260
+ i--
1261
+ } else {
1262
+ break
1263
+ }
1264
+ }
1265
+ if merged > 1 {
1266
+ (*exprs)[i] = &regexClassRangeNode{From: (*exprs)[i].(*regexCharNode), To: ch}
1267
+ return merged
1268
+ }
1269
+ return 0
1270
+ }
1271
+
1272
+ func regexIsMetaWCharOrCode(n regexNode) bool {
1273
+ ch, ok := n.(*regexCharNode)
1274
+ if !ok || ch.codePointIsNaN() {
1275
+ return false
1276
+ }
1277
+ return regexFitsInMetaW(ch, false) ||
1278
+ ch.Kind == "unicode" || ch.Kind == "hex" || ch.Kind == "oct" || ch.Kind == "decimal"
1279
+ }
1280
+
1281
+ // ---------------------------------------------------------------------------
1282
+ // disjunctionRemoveDuplicates
1283
+ // ---------------------------------------------------------------------------
1284
+
1285
+ // (ab|bc|ab) -> (ab|bc)
1286
+ func regexTransformDisjunctionRemoveDuplicates(re *regexRegExpNode) {
1287
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
1288
+ disjunction, ok := node.(*regexDisjunctionNode)
1289
+ if !ok {
1290
+ return
1291
+ }
1292
+ parts := regexDisjunctionToList(disjunction)
1293
+ seen := map[string]bool{}
1294
+ unique := parts[:0]
1295
+ for _, part := range parts {
1296
+ key := "null"
1297
+ if part != nil {
1298
+ key = regexEqualityKey(part)
1299
+ }
1300
+ if seen[key] {
1301
+ continue
1302
+ }
1303
+ seen[key] = true
1304
+ unique = append(unique, part)
1305
+ }
1306
+ slot.set(regexListToDisjunction(unique))
1307
+ })
1308
+ }
1309
+
1310
+ func regexDisjunctionToList(node *regexDisjunctionNode) []regexNode {
1311
+ var list []regexNode
1312
+ if left, ok := node.Left.(*regexDisjunctionNode); ok {
1313
+ list = append(regexDisjunctionToList(left), node.Right)
1314
+ } else {
1315
+ list = []regexNode{node.Left, node.Right}
1316
+ }
1317
+ return list
1318
+ }
1319
+
1320
+ func regexListToDisjunction(list []regexNode) regexNode {
1321
+ if len(list) == 0 {
1322
+ return nil
1323
+ }
1324
+ node := list[0]
1325
+ for _, right := range list[1:] {
1326
+ node = &regexDisjunctionNode{Left: node, Right: right}
1327
+ }
1328
+ return node
1329
+ }
1330
+
1331
+ // ---------------------------------------------------------------------------
1332
+ // groupSingleCharsToCharClass
1333
+ // ---------------------------------------------------------------------------
1334
+
1335
+ // (a|b|c) -> ([abc]), (?:a|b|c) -> [abc], top-level a|b|c -> [abc]
1336
+ //
1337
+ // Upstream fires on the Disjunction and replaces through the parent path:
1338
+ // a capturing group keeps the group and swaps its expression, while a
1339
+ // non-capturing group is itself replaced by the class. This port fires at
1340
+ // the slots it can reach (the RegExp body and each Group), which visits the
1341
+ // same parent set.
1342
+ func regexTransformGroupSingleCharsToClass(re *regexRegExpNode) {
1343
+ if disjunction, ok := re.Body.(*regexDisjunctionNode); ok {
1344
+ if class := regexSingleCharsClass(disjunction); class != nil {
1345
+ re.Body = class
1346
+ }
1347
+ }
1348
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
1349
+ group, ok := node.(*regexGroupNode)
1350
+ if !ok {
1351
+ return
1352
+ }
1353
+ disjunction, isDisjunction := group.Expression.(*regexDisjunctionNode)
1354
+ if !isDisjunction {
1355
+ return
1356
+ }
1357
+ class := regexSingleCharsClass(disjunction)
1358
+ if class == nil {
1359
+ return
1360
+ }
1361
+ if group.Capturing {
1362
+ group.Expression = class
1363
+ return
1364
+ }
1365
+ slot.set(class)
1366
+ })
1367
+ }
1368
+
1369
+ // regexSingleCharsClass builds the replacement CharacterClass for a
1370
+ // single-char disjunction, or nil when the disjunction does not qualify.
1371
+ func regexSingleCharsClass(disjunction *regexDisjunctionNode) *regexClassNode {
1372
+ charset := map[string]*regexCharNode{}
1373
+ var order []string
1374
+ if !regexCollectSingleChars(disjunction, charset, &order) || len(charset) == 0 {
1375
+ return nil
1376
+ }
1377
+ sort.SliceStable(order, func(i, j int) bool { return regexUTF16Less(order[i], order[j]) })
1378
+ members := make([]regexNode, 0, len(order))
1379
+ for _, key := range order {
1380
+ members = append(members, charset[key])
1381
+ }
1382
+ return &regexClassNode{Expressions: members}
1383
+ }
1384
+
1385
+ // regexCollectSingleChars ports shouldProcess: walks a disjunction whose
1386
+ // parts are single chars or positive flat classes of chars.
1387
+ func regexCollectSingleChars(expr regexNode, charset map[string]*regexCharNode, order *[]string) bool {
1388
+ switch n := expr.(type) {
1389
+ case nil:
1390
+ return false
1391
+ case *regexDisjunctionNode:
1392
+ return regexCollectSingleChars(n.Left, charset, order) &&
1393
+ regexCollectSingleChars(n.Right, charset, order)
1394
+ case *regexCharNode:
1395
+ if n.Kind == "meta" && n.SymbolState == regexFieldValue && n.Symbol == "." {
1396
+ return false
1397
+ }
1398
+ if _, exists := charset[n.Value]; !exists {
1399
+ *order = append(*order, n.Value)
1400
+ }
1401
+ charset[n.Value] = n
1402
+ return true
1403
+ case *regexClassNode:
1404
+ if n.Negative {
1405
+ return false
1406
+ }
1407
+ for _, member := range n.Expressions {
1408
+ if !regexCollectSingleChars(member, charset, order) {
1409
+ return false
1410
+ }
1411
+ }
1412
+ return true
1413
+ }
1414
+ return false
1415
+ }
1416
+
1417
+ // regexUTF16Less compares strings by UTF-16 code units, matching the
1418
+ // JavaScript default Array#sort string ordering upstream relies on.
1419
+ func regexUTF16Less(a, b string) bool {
1420
+ ar := []rune(a)
1421
+ br := []rune(b)
1422
+ for i := 0; i < len(ar) && i < len(br); i++ {
1423
+ au := regexUTF16Units(ar[i])
1424
+ bu := regexUTF16Units(br[i])
1425
+ for j := 0; j < len(au) && j < len(bu); j++ {
1426
+ if au[j] != bu[j] {
1427
+ return au[j] < bu[j]
1428
+ }
1429
+ }
1430
+ if len(au) != len(bu) {
1431
+ return len(au) < len(bu)
1432
+ }
1433
+ }
1434
+ return len(ar) < len(br)
1435
+ }
1436
+
1437
+ func regexUTF16Units(r rune) []uint16 {
1438
+ if r < 0x10000 {
1439
+ return []uint16{uint16(r)}
1440
+ }
1441
+ r -= 0x10000
1442
+ return []uint16{uint16(0xd800 + r/0x400), uint16(0xdc00 + r%0x400)}
1443
+ }
1444
+
1445
+ // ---------------------------------------------------------------------------
1446
+ // removeEmptyGroup
1447
+ // ---------------------------------------------------------------------------
1448
+
1449
+ // (?:)a -> a, (?:)+ -> (?:)
1450
+ func regexTransformRemoveEmptyGroup(re *regexRegExpNode) {
1451
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
1452
+ group, ok := node.(*regexGroupNode)
1453
+ if !ok || group.Capturing || group.Expression != nil {
1454
+ return
1455
+ }
1456
+ switch slot.parent.(type) {
1457
+ case *regexRepetitionNode:
1458
+ // The group's slot is the repetition's expression; the repetition
1459
+ // itself must be replaced by the group in ITS parent slot. The
1460
+ // walker cannot reach that slot from here, so this case is handled
1461
+ // by the dedicated pass below.
1462
+ case *regexRegExpNode:
1463
+ // Keep a lone (?:) as the whole pattern.
1464
+ default:
1465
+ slot.remove()
1466
+ }
1467
+ })
1468
+ // Second, structural pass: (?:)+ -> (?:). Upstream reaches the parent
1469
+ // repetition through path.getParent().replace(); this walker replaces
1470
+ // Repetition nodes whose expression is an empty non-capturing group.
1471
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
1472
+ rep, ok := node.(*regexRepetitionNode)
1473
+ if !ok {
1474
+ return
1475
+ }
1476
+ if group, isGroup := rep.Expression.(*regexGroupNode); isGroup &&
1477
+ !group.Capturing && group.Expression == nil {
1478
+ slot.set(group)
1479
+ }
1480
+ })
1481
+ }
1482
+
1483
+ // ---------------------------------------------------------------------------
1484
+ // ungroup
1485
+ // ---------------------------------------------------------------------------
1486
+
1487
+ // (?:a) -> a
1488
+ //
1489
+ // The Alternative-merge case reproduces an upstream traversal subtlety:
1490
+ // regexp-tree's merge replaces the parent Alternative with a rebuilt node
1491
+ // and deletes the old node's registry path, so every later ungroup whose
1492
+ // parent is that same Alternative silently no-ops for the rest of the pass
1493
+ // (the next optimizer round picks it up). This port splices in place,
1494
+ // marks the Alternative dirty, skips further ungroups under it this pass,
1495
+ // and resumes iteration after the spliced-in segment — reaching the same
1496
+ // per-pass strings the upstream accept/rollback guard sees.
1497
+ func regexTransformUngroup(re *regexRegExpNode) {
1498
+ dirty := map[*regexAlternativeNode]bool{}
1499
+ regexWalk(re, func(node regexNode, slot *regexSlot) {
1500
+ group, ok := node.(*regexGroupNode)
1501
+ if !ok || group.Capturing || group.Expression == nil {
1502
+ return
1503
+ }
1504
+ parentAlt, parentIsAlt := slot.parent.(*regexAlternativeNode)
1505
+ if parentIsAlt && dirty[parentAlt] {
1506
+ return
1507
+ }
1508
+ if !regexHasSafeExtractionSiblings(slot) {
1509
+ return
1510
+ }
1511
+ child := group.Expression
1512
+ // Don't optimize /a(?:b|c)/ to /ab|c/; /(?:b|c)/ -> /b|c/ is ok.
1513
+ if _, isDisjunction := child.(*regexDisjunctionNode); isDisjunction {
1514
+ if _, parentIsRegExp := slot.parent.(*regexRegExpNode); !parentIsRegExp {
1515
+ return
1516
+ }
1517
+ }
1518
+ // Don't optimize /(?:ab)+/ to /ab+/; /(?:a)+/ and /(?:[a-d])+/ are ok.
1519
+ if _, parentIsRepetition := slot.parent.(*regexRepetitionNode); parentIsRepetition {
1520
+ switch child.(type) {
1521
+ case *regexCharNode, *regexClassNode:
1522
+ default:
1523
+ return
1524
+ }
1525
+ }
1526
+ if alt, childIsAlt := child.(*regexAlternativeNode); childIsAlt {
1527
+ // A multi-term group body merges only into a surrounding
1528
+ // Alternative; under any other parent it stays grouped.
1529
+ if parentIsAlt && slot.list != nil {
1530
+ list := slot.list
1531
+ idx := slot.index
1532
+ *list = append((*list)[:idx], append(append([]regexNode{}, alt.Expressions...), (*list)[idx+1:]...)...)
1533
+ if slot.iter != nil {
1534
+ slot.iter.i += len(alt.Expressions) - 1
1535
+ }
1536
+ dirty[parentAlt] = true
1537
+ }
1538
+ return
1539
+ }
1540
+ slot.set(child)
1541
+ })
1542
+ }
1543
+
1544
+ // ---------------------------------------------------------------------------
1545
+ // combineRepeatingPatterns
1546
+ // ---------------------------------------------------------------------------
1547
+
1548
+ // abcabcabc -> (?:abc){3}
1549
+ func regexTransformCombineRepeating(re *regexRegExpNode) {
1550
+ regexWalk(re, func(node regexNode, _ *regexSlot) {
1551
+ alt, ok := node.(*regexAlternativeNode)
1552
+ if !ok {
1553
+ return
1554
+ }
1555
+ index := 1
1556
+ for index < len(alt.Expressions) {
1557
+ index = maxInt(1, regexCombineRepeatingPatternLeft(alt, index))
1558
+ if index >= len(alt.Expressions) {
1559
+ break
1560
+ }
1561
+ index = maxInt(1, regexCombineWithPreviousRepetition(alt, index))
1562
+ if index >= len(alt.Expressions) {
1563
+ break
1564
+ }
1565
+ index = maxInt(1, regexCombineRepetitionWithPrevious(alt, index))
1566
+ index++
1567
+ }
1568
+ })
1569
+ }
1570
+
1571
+ // abcabc -> (?:abc){2}
1572
+ func regexCombineRepeatingPatternLeft(alt *regexAlternativeNode, index int) int {
1573
+ exprs := alt.Expressions
1574
+ child := exprs[index]
1575
+ nbPossibleLengths := (index + 1) / 2
1576
+ for i := 0; i < nbPossibleLengths; i++ {
1577
+ startIndex := index - 2*i - 1
1578
+ var leftKey, rightKey string
1579
+ var rightNode regexNode
1580
+ if i == 0 {
1581
+ rightNode = child
1582
+ leftKey = regexEqualityKey(exprs[startIndex])
1583
+ rightKey = regexEqualityKey(child)
1584
+ } else {
1585
+ right := &regexAlternativeNode{
1586
+ Expressions: append(append([]regexNode{}, exprs[index-i:index]...), child),
1587
+ }
1588
+ left := &regexAlternativeNode{
1589
+ Expressions: append([]regexNode{}, exprs[startIndex:index-i]...),
1590
+ }
1591
+ rightNode = right
1592
+ leftKey = regexEqualityKey(left)
1593
+ rightKey = regexEqualityKey(right)
1594
+ }
1595
+ if leftKey == rightKey {
1596
+ var expression regexNode
1597
+ if _, isRep := rightNode.(*regexRepetitionNode); i == 0 && !isRep {
1598
+ expression = rightNode
1599
+ } else {
1600
+ expression = &regexGroupNode{Capturing: false, Expression: rightNode}
1601
+ }
1602
+ replacement := &regexRepetitionNode{
1603
+ Expression: expression,
1604
+ Quantifier: &regexQuantifierNode{
1605
+ Kind: "Range", From: 2, To: 2, HasTo: true, Greedy: true,
1606
+ FieldOrder: "ftg",
1607
+ },
1608
+ }
1609
+ // Remove the 2i+1 nodes before child, then replace child.
1610
+ alt.Expressions = append(exprs[:startIndex], exprs[index:]...)
1611
+ alt.Expressions[startIndex] = replacement
1612
+ return startIndex
1613
+ }
1614
+ }
1615
+ return index
1616
+ }
1617
+
1618
+ // (?:abc){2}abc -> (?:abc){3}
1619
+ func regexCombineWithPreviousRepetition(alt *regexAlternativeNode, index int) int {
1620
+ exprs := alt.Expressions
1621
+ child := exprs[index]
1622
+ for i := 0; i < index; i++ {
1623
+ prevRep, isRep := exprs[i].(*regexRepetitionNode)
1624
+ if !isRep || !prevRep.Quantifier.Greedy {
1625
+ continue
1626
+ }
1627
+ left := prevRep.Expression
1628
+ if group, isGroup := left.(*regexGroupNode); isGroup && !group.Capturing {
1629
+ left = group.Expression
1630
+ }
1631
+ var right regexNode
1632
+ if i+1 == index {
1633
+ right = child
1634
+ if group, isGroup := right.(*regexGroupNode); isGroup && !group.Capturing {
1635
+ right = group.Expression
1636
+ }
1637
+ } else {
1638
+ right = &regexAlternativeNode{
1639
+ Expressions: append([]regexNode{}, exprs[i+1:index+1]...),
1640
+ }
1641
+ }
1642
+ if left != nil && regexEqualityKey(left) == regexEqualityKey(right) {
1643
+ alt.Expressions = append(exprs[:i+1], exprs[index+1:]...)
1644
+ regexIncreaseQuantifierByOne(prevRep.Quantifier)
1645
+ return i
1646
+ }
1647
+ }
1648
+ return index
1649
+ }
1650
+
1651
+ // abc(?:abc){2} -> (?:abc){3}
1652
+ func regexCombineRepetitionWithPrevious(alt *regexAlternativeNode, index int) int {
1653
+ exprs := alt.Expressions
1654
+ childRep, isRep := exprs[index].(*regexRepetitionNode)
1655
+ if !isRep || !childRep.Quantifier.Greedy {
1656
+ return index
1657
+ }
1658
+ right := childRep.Expression
1659
+ if group, isGroup := right.(*regexGroupNode); isGroup && !group.Capturing {
1660
+ right = group.Expression
1661
+ }
1662
+ var left regexNode
1663
+ var rightLength int
1664
+ if rightAlt, isAlt := right.(*regexAlternativeNode); isAlt {
1665
+ rightLength = len(rightAlt.Expressions)
1666
+ if index-rightLength < 0 {
1667
+ return index
1668
+ }
1669
+ left = &regexAlternativeNode{
1670
+ Expressions: append([]regexNode{}, exprs[index-rightLength:index]...),
1671
+ }
1672
+ } else {
1673
+ rightLength = 1
1674
+ left = exprs[index-1]
1675
+ if group, isGroup := left.(*regexGroupNode); isGroup && !group.Capturing {
1676
+ left = group.Expression
1677
+ }
1678
+ }
1679
+ if left != nil && right != nil && regexEqualityKey(left) == regexEqualityKey(right) {
1680
+ alt.Expressions = append(exprs[:index-rightLength], exprs[index:]...)
1681
+ regexIncreaseQuantifierByOne(childRep.Quantifier)
1682
+ return index - rightLength
1683
+ }
1684
+ return index
1685
+ }