@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.
- package/README.md +95 -43
- package/go.mod +5 -4
- package/internal/rulecode/rulecode.go +88 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +45 -17
- package/lib/index.js.map +1 -1
- package/lib/structures/ITtscLintConfig.d.ts +3 -0
- package/lib/structures/TtscLintRuleSetting.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormat.d.ts +3 -2
- package/lib/structures/format/ITtscLintFormatSortImports.d.ts +20 -4
- package/lib/structures/rules/ITtscLintBoundariesRuleOptions.d.ts +80 -8
- package/lib/structures/rules/ITtscLintBoundariesRules.d.ts +6 -8
- package/lib/structures/rules/ITtscLintContributorRules.d.ts +18 -2
- package/lib/structures/rules/ITtscLintCoreRuleOptions.d.ts +347 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintCoreRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintCoreRules.d.ts +101 -67
- package/lib/structures/rules/ITtscLintRegexpRules.d.ts +7 -2
- package/lib/structures/rules/ITtscLintRuleOptionsMap.d.ts +53 -7
- package/lib/structures/rules/ITtscLintRules.d.ts +6 -2
- package/lib/structures/rules/ITtscLintSolidRules.d.ts +0 -8
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.d.ts +206 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js +8 -0
- package/lib/structures/rules/ITtscLintTypeScriptRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintTypeScriptRules.d.ts +55 -42
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.d.ts +276 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js +3 -0
- package/lib/structures/rules/ITtscLintUnicornRuleOptions.js.map +1 -0
- package/lib/structures/rules/ITtscLintUnicornRules.d.ts +44 -36
- package/lib/structures/rules/index.d.ts +3 -0
- package/lib/structures/rules/index.js +3 -0
- package/lib/structures/rules/index.js.map +1 -1
- package/linthost/ast_helpers.go +521 -23
- package/linthost/ast_selector_matcher.go +1628 -0
- package/linthost/ast_selector_parser.go +628 -0
- package/linthost/comment_scan.go +154 -0
- package/linthost/compile.go +129 -79
- package/linthost/config.go +352 -88
- package/linthost/config_format.go +15 -1
- package/linthost/contrib_adapter.go +47 -8
- package/linthost/declaration_rules.go +12 -11
- package/linthost/directives.go +9 -67
- package/linthost/dispatch.go +37 -3
- package/linthost/engine.go +368 -53
- package/linthost/escape_string.go +77 -0
- package/linthost/fix.go +133 -23
- package/linthost/flags_gen.go +18 -17
- package/linthost/format.go +73 -167
- package/linthost/format_editor_settings.go +134 -26
- package/linthost/grapheme_tables_gen.go +1920 -0
- package/linthost/host.go +160 -61
- package/linthost/literal_escape_scan.go +117 -0
- package/linthost/lsp.go +545 -98
- package/linthost/numeric_literal_precision.go +228 -0
- package/linthost/polyfill_browserslist.go +1439 -0
- package/linthost/polyfill_browserslist_config.go +603 -0
- package/linthost/polyfill_corejs.go +750 -0
- package/linthost/polyfill_data.go +113 -0
- package/linthost/polyfill_data_gen.json +39868 -0
- package/linthost/print_nodes_array.go +7 -1
- package/linthost/print_nodes_object.go +7 -1
- package/linthost/project_engine.go +151 -0
- package/linthost/project_rules.go +78 -0
- package/linthost/regex_clean.go +191 -0
- package/linthost/regex_tree.go +1544 -0
- package/linthost/regex_tree_optimizer.go +1685 -0
- package/linthost/regexp_cache.go +44 -0
- package/linthost/rule_codes.go +105 -0
- package/linthost/rule_codes.json +745 -0
- package/linthost/rules_ban_ts_comment.go +312 -0
- package/linthost/rules_boundaries.go +128 -43
- package/linthost/rules_boundaries_dependencies.go +1013 -0
- package/linthost/rules_core_extra.go +62 -92
- package/linthost/rules_cypress.go +1 -1
- package/linthost/rules_default_case.go +87 -11
- package/linthost/rules_dupes.go +38 -13
- package/linthost/rules_empty.go +188 -20
- package/linthost/rules_escape.go +62 -48
- package/linthost/rules_format_arrow_parens.go +19 -5
- package/linthost/rules_format_bracket_spacing.go +1 -1
- package/linthost/rules_format_clause_join.go +1 -1
- package/linthost/rules_format_declaration_header.go +31 -19
- package/linthost/rules_format_indent.go +1 -1
- package/linthost/rules_format_jsdoc.go +1 -1
- package/linthost/rules_format_orphan_semi.go +1 -1
- package/linthost/rules_format_parameter_properties.go +17 -7
- package/linthost/rules_format_print_width.go +1 -1
- package/linthost/rules_format_quote_props.go +1 -1
- package/linthost/rules_format_quotes.go +1 -1
- package/linthost/rules_format_semi.go +106 -79
- package/linthost/rules_format_sort_imports.go +79 -45
- package/linthost/rules_format_statement_split.go +1 -1
- package/linthost/rules_format_ternary_nullish_parens.go +1 -1
- package/linthost/rules_format_trailing_comma.go +64 -1
- package/linthost/rules_format_whitespace.go +1 -1
- package/linthost/rules_functional.go +21 -22
- package/linthost/rules_gap.go +80 -3
- package/linthost/rules_grouped_accessor_pairs.go +84 -25
- package/linthost/rules_guard_for_in.go +64 -147
- package/linthost/rules_jsx_a11y.go +232 -22
- package/linthost/rules_logic.go +84 -38
- package/linthost/rules_nextjs.go +105 -16
- package/linthost/rules_no_class_assign.go +70 -0
- package/linthost/rules_no_duplicate_imports.go +266 -0
- package/linthost/rules_no_else_return.go +156 -38
- package/linthost/rules_no_extend_native.go +113 -17
- package/linthost/rules_no_fallthrough.go +1013 -0
- package/linthost/rules_no_func_assign.go +70 -0
- package/linthost/rules_no_import_assign.go +278 -86
- package/linthost/rules_no_inner_declarations.go +186 -0
- package/linthost/rules_no_loop_func.go +438 -53
- package/linthost/rules_no_mixed_operators.go +208 -52
- package/linthost/rules_no_param_reassign.go +376 -61
- package/linthost/rules_no_promise_executor_return.go +156 -0
- package/linthost/rules_no_restricted_imports.go +1013 -57
- package/linthost/rules_no_restricted_syntax.go +190 -26
- package/linthost/rules_no_useless_constructor.go +6 -36
- package/linthost/rules_problems.go +154 -311
- package/linthost/rules_promise.go +2578 -109
- package/linthost/rules_react_extras.go +71 -45
- package/linthost/rules_react_hooks.go +18 -2
- package/linthost/rules_react_perf.go +1 -0
- package/linthost/rules_react_refresh.go +1 -1
- package/linthost/rules_regexp.go +230 -14
- package/linthost/rules_security.go +45 -19
- package/linthost/rules_self.go +21 -1
- package/linthost/rules_solid.go +13 -7
- package/linthost/rules_storybook.go +1 -1
- package/linthost/rules_suggestions.go +490 -84
- package/linthost/rules_testing_library.go +12 -1
- package/linthost/rules_ts.go +227 -54
- package/linthost/rules_ts_async.go +0 -131
- package/linthost/rules_ts_no_base_to_string.go +3 -4
- package/linthost/rules_ts_no_misused_promises.go +817 -0
- package/linthost/rules_ts_no_restricted_types.go +320 -34
- package/linthost/rules_ts_no_unnecessary_qualifier.go +132 -55
- package/linthost/rules_ts_no_unsafe_assignment.go +608 -60
- package/linthost/rules_ts_require_array_sort_compare.go +5 -5
- package/linthost/rules_ts_switch_exhaustiveness_check.go +556 -86
- package/linthost/rules_unicorn_better_regex.go +224 -16
- package/linthost/rules_unicorn_consistent_destructuring.go +1002 -7
- package/linthost/rules_unicorn_consistent_existence_index_check.go +208 -29
- package/linthost/rules_unicorn_consistent_function_scoping.go +1041 -8
- package/linthost/rules_unicorn_consistent_template_literal_escape.go +178 -7
- package/linthost/rules_unicorn_escape_case.go +30 -19
- package/linthost/rules_unicorn_filename_case.go +763 -16
- package/linthost/rules_unicorn_import_style.go +783 -17
- package/linthost/rules_unicorn_isolated_functions.go +934 -14
- package/linthost/rules_unicorn_no_hex_escape.go +26 -11
- package/linthost/rules_unicorn_no_typeof_undefined.go +234 -25
- package/linthost/rules_unicorn_no_unnecessary_polyfills.go +376 -21
- package/linthost/rules_unicorn_no_unused_properties.go +756 -19
- package/linthost/rules_unicorn_number_literal_case.go +58 -39
- package/linthost/rules_unicorn_prefer_at.go +7 -3
- package/linthost/rules_unicorn_prefer_number_properties.go +381 -48
- package/linthost/rules_unicorn_prefer_optional_catch_binding.go +24 -24
- package/linthost/rules_unicorn_prefer_simple_condition_first.go +597 -19
- package/linthost/rules_unicorn_prefer_string_raw.go +147 -16
- package/linthost/rules_unicorn_prevent_abbreviations.go +1963 -95
- package/linthost/rules_unicorn_string_content.go +611 -15
- package/linthost/rules_unicorn_switch_case_break_position.go +172 -15
- package/linthost/rules_unicorn_template_indent.go +923 -18
- package/linthost/rules_unicorn_text_encoding_identifier_case.go +202 -42
- package/linthost/rules_unicorn_throw_new_error.go +218 -26
- package/linthost/rules_var.go +829 -142
- package/linthost/string_length.go +222 -0
- package/package.json +4 -3
- package/rule/project.go +145 -0
- package/rule/rule.go +58 -1
- package/src/index.ts +58 -17
- package/src/structures/ITtscLintConfig.ts +3 -0
- package/src/structures/TtscLintRuleSetting.ts +3 -2
- package/src/structures/format/ITtscLintFormat.ts +3 -2
- package/src/structures/format/ITtscLintFormatSortImports.ts +21 -4
- package/src/structures/rules/ITtscLintBoundariesRuleOptions.ts +122 -8
- package/src/structures/rules/ITtscLintBoundariesRules.ts +6 -8
- package/src/structures/rules/ITtscLintContributorRules.ts +18 -2
- package/src/structures/rules/ITtscLintCoreRuleOptions.ts +465 -0
- package/src/structures/rules/ITtscLintCoreRules.ts +122 -67
- package/src/structures/rules/ITtscLintRegexpRules.ts +7 -2
- package/src/structures/rules/ITtscLintRuleOptionsMap.ts +88 -7
- package/src/structures/rules/ITtscLintRules.ts +7 -2
- package/src/structures/rules/ITtscLintSolidRules.ts +0 -9
- package/src/structures/rules/ITtscLintTypeScriptRuleOptions.ts +248 -0
- package/src/structures/rules/ITtscLintTypeScriptRules.ts +64 -42
- package/src/structures/rules/ITtscLintUnicornRuleOptions.ts +350 -0
- package/src/structures/rules/ITtscLintUnicornRules.ts +60 -36
- 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 ®exRegExpNode{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 ®exClassRangeNode{
|
|
504
|
+
From: regexCloneNode(n.From).(*regexCharNode),
|
|
505
|
+
To: regexCloneNode(n.To).(*regexCharNode),
|
|
506
|
+
}
|
|
507
|
+
case *regexAlternativeNode:
|
|
508
|
+
return ®exAlternativeNode{Expressions: cloneRegexList(n.Expressions)}
|
|
509
|
+
case *regexDisjunctionNode:
|
|
510
|
+
return ®exDisjunctionNode{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 ®exRepetitionNode{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, ®exParseError{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, ®exParseError{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 := ®exParser{
|
|
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 ®exRegExpNode{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 "", ®exParseError{Message: fmt.Sprintf("invalid regular expression flag %q", string(r))}
|
|
651
|
+
}
|
|
652
|
+
if seen[r] {
|
|
653
|
+
return "", ®exParseError{Message: fmt.Sprintf("duplicate regular expression flag %q", string(r))}
|
|
654
|
+
}
|
|
655
|
+
seen[r] = true
|
|
656
|
+
}
|
|
657
|
+
if seen['u'] && seen['v'] {
|
|
658
|
+
return "", ®exParseError{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 ®exParseError{Message: fmt.Sprintf("%s at index %d", message, pos)}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
func (p *regexParser) errorEOF() error {
|
|
669
|
+
return ®exParseError{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 ®exParseError{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 "", "", ®exParseError{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 "", "", ®exParseError{Message: "invalid capture group name"}
|
|
740
|
+
}
|
|
741
|
+
decoded, consumed, ok := decodeUnicodeEscapeBody(raw[i+2:], true)
|
|
742
|
+
if !ok {
|
|
743
|
+
return "", "", ®exParseError{Message: "invalid capture group name"}
|
|
744
|
+
}
|
|
745
|
+
r = decoded
|
|
746
|
+
i += 1 + consumed
|
|
747
|
+
}
|
|
748
|
+
if !isRegexIdentifierRune(r, first) {
|
|
749
|
+
return "", "", ®exParseError{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 = ®exDisjunctionNode{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 ®exAlternativeNode{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 ®exRepetitionNode{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 = ®exQuantifierNode{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 = ®exQuantifierNode{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 ®exAssertionNode{Kind: string(r)}, false, nil
|
|
957
|
+
case '.':
|
|
958
|
+
p.pos++
|
|
959
|
+
return ®exCharNode{
|
|
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 ®exCharNode{
|
|
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 ®exGroupNode{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 := ®exAssertionNode{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 := ®exAssertionNode{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 ®exGroupNode{
|
|
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 ®exGroupNode{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 ®exCharNode{
|
|
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 ®exCharNode{
|
|
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 ®exCharNode{
|
|
1143
|
+
Value: "\\b", Kind: "meta",
|
|
1144
|
+
Symbol: ".", SymbolState: regexFieldValue,
|
|
1145
|
+
CodePointState: regexFieldNaN,
|
|
1146
|
+
}, true, nil
|
|
1147
|
+
}
|
|
1148
|
+
return ®exAssertionNode{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 ®exAssertionNode{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 ®exCharNode{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 ®exCharNode{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 ®exCharNode{
|
|
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 ®exCharNode{
|
|
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 ®exBackreferenceNode{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 ®exCharNode{
|
|
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 ®exCharNode{
|
|
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 ®exCharNode{
|
|
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 ®exCharNode{
|
|
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 := ®exUnicodePropertyNode{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 ®exBackreferenceNode{
|
|
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 := ®exClassNode{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, ®exParseError{
|
|
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, ®exClassRangeNode{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 := ®exClassSetNode{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
|
+
}
|