flagnetic 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +202 -0
- data/README.md +276 -0
- data/lib/flagnetic/backoff.rb +29 -0
- data/lib/flagnetic/changes.rb +88 -0
- data/lib/flagnetic/client.rb +609 -0
- data/lib/flagnetic/controller_helpers.rb +97 -0
- data/lib/flagnetic/detail.rb +73 -0
- data/lib/flagnetic/eval/evaluation.rb +382 -0
- data/lib/flagnetic/eval/instant.rb +71 -0
- data/lib/flagnetic/eval/murmur3.rb +70 -0
- data/lib/flagnetic/eval/operators.rb +225 -0
- data/lib/flagnetic/eval/regex.rb +647 -0
- data/lib/flagnetic/eval/ruleset.rb +267 -0
- data/lib/flagnetic/eval/semver.rb +80 -0
- data/lib/flagnetic/eval/types.rb +148 -0
- data/lib/flagnetic/eval/unicode_tables.rb +180 -0
- data/lib/flagnetic/eval.rb +38 -0
- data/lib/flagnetic/event_batch.rb +119 -0
- data/lib/flagnetic/exposures.rb +342 -0
- data/lib/flagnetic/fallback_chain.rb +230 -0
- data/lib/flagnetic/generations.rb +50 -0
- data/lib/flagnetic/global.rb +59 -0
- data/lib/flagnetic/local_overrides.rb +65 -0
- data/lib/flagnetic/logging.rb +62 -0
- data/lib/flagnetic/railtie.rb +146 -0
- data/lib/flagnetic/recorder.rb +135 -0
- data/lib/flagnetic/reporter.rb +94 -0
- data/lib/flagnetic/sse_parser.rb +94 -0
- data/lib/flagnetic/store.rb +34 -0
- data/lib/flagnetic/stream.rb +65 -0
- data/lib/flagnetic/summaries.rb +257 -0
- data/lib/flagnetic/testing.rb +160 -0
- data/lib/flagnetic/transport.rb +106 -0
- data/lib/flagnetic/uuid.rb +25 -0
- data/lib/flagnetic/version.rb +16 -0
- data/lib/flagnetic/worker.rb +270 -0
- data/lib/flagnetic.rb +34 -0
- metadata +79 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "unicode_tables"
|
|
4
|
+
|
|
5
|
+
module Flagnetic
|
|
6
|
+
# Eval is the evaluator; see flagnetic/eval.rb.
|
|
7
|
+
module Eval
|
|
8
|
+
# Regex compiles the pattern of a matches condition. Patterns are RE2
|
|
9
|
+
# syntax, as the reference evaluator (Go's regexp) reads them. Ruby's
|
|
10
|
+
# engine reads a different dialect: ^ and $ are line anchors, \s includes
|
|
11
|
+
# \v, (?m) means what RE2 calls (?s), (?i) folds ß to ss, lookarounds and
|
|
12
|
+
# backreferences exist. So a pattern is never handed to Regexp as is: it
|
|
13
|
+
# is parsed as RE2 and translated into an Onigmo expression that matches
|
|
14
|
+
# exactly the same strings.
|
|
15
|
+
#
|
|
16
|
+
# The policy:
|
|
17
|
+
#
|
|
18
|
+
# - Every RE2 construct is supported with RE2 semantics: literals and
|
|
19
|
+
# escapes, classes with ranges, POSIX and Perl classes (ASCII, as in
|
|
20
|
+
# RE2), \p and \P with Go's category and script names, groups (plain,
|
|
21
|
+
# non-capturing, named), flags i, m, s and U, alternation, greedy and
|
|
22
|
+
# lazy repetition up to 1000, \A, \z, \b, \B and \Q...\E. Case folding
|
|
23
|
+
# uses Go's simple folding orbits, not Ruby's.
|
|
24
|
+
# - What RE2 rejects is rejected: lookarounds, backreferences, atomic
|
|
25
|
+
# groups, possessive or nested repetition, \Z, \G, \h, \K, \R, \C,
|
|
26
|
+
# comments, unknown escapes and flags. The condition is then
|
|
27
|
+
# inconsistent_ruleset, as in every other evaluator.
|
|
28
|
+
# - \p and \P under (?i) match the property's code points and every
|
|
29
|
+
# code point sharing a folding orbit with one, as Go does; whether a
|
|
30
|
+
# code point has the property is Ruby's Unicode data, the orbits are
|
|
31
|
+
# Go's.
|
|
32
|
+
# - A match searches anywhere in the string unless the pattern is
|
|
33
|
+
# anchored. Every byte of invalid UTF-8 in the matched string reads as
|
|
34
|
+
# one U+FFFD, as Go decodes it. On
|
|
35
|
+
# Ruby 3.2 and later a match is cut off after MATCH_TIMEOUT seconds and
|
|
36
|
+
# the condition is false; RE2 is linear time and never needs to.
|
|
37
|
+
module Regex
|
|
38
|
+
# Error is a pattern outside RE2 syntax or outside RE2.
|
|
39
|
+
class Error < StandardError; end
|
|
40
|
+
|
|
41
|
+
MATCH_TIMEOUT = 0.1
|
|
42
|
+
|
|
43
|
+
REPLACEMENT = [0xFFFD].pack("U")
|
|
44
|
+
private_constant :REPLACEMENT
|
|
45
|
+
|
|
46
|
+
TIMEOUT_ERROR = defined?(Regexp::TimeoutError) ? Regexp::TimeoutError : Class.new(StandardError)
|
|
47
|
+
private_constant :TIMEOUT_ERROR
|
|
48
|
+
|
|
49
|
+
# Pattern is a compiled matches pattern.
|
|
50
|
+
class Pattern
|
|
51
|
+
def initialize(regexp)
|
|
52
|
+
@regexp = regexp
|
|
53
|
+
freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# match? reports whether s contains a match.
|
|
57
|
+
def match?(str)
|
|
58
|
+
@regexp.match?(str.valid_encoding? ? str : str.scrub { |bad| REPLACEMENT * bad.bytesize })
|
|
59
|
+
rescue TIMEOUT_ERROR
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# compile translates an RE2 pattern into a Pattern, raising Error when
|
|
65
|
+
# it is not one this evaluator can match with RE2 semantics.
|
|
66
|
+
def self.compile(pattern)
|
|
67
|
+
source = Translator.new(pattern).translate
|
|
68
|
+
options = Regexp.respond_to?(:timeout) ? { timeout: MATCH_TIMEOUT } : {}
|
|
69
|
+
Pattern.new(Regexp.new(source, **options))
|
|
70
|
+
rescue RegexpError => e
|
|
71
|
+
raise Error, e.message
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
MAX_RUNE = 0x10FFFF
|
|
75
|
+
SURROGATES = (0xD800..0xDFFF)
|
|
76
|
+
WORD = [[0x30, 0x39], [0x41, 0x5A], [0x5F, 0x5F], [0x61, 0x7A]].freeze
|
|
77
|
+
PERL_CLASSES = {
|
|
78
|
+
"d" => [[0x30, 0x39]],
|
|
79
|
+
"s" => [[0x09, 0x0A], [0x0C, 0x0D], [0x20, 0x20]],
|
|
80
|
+
"w" => WORD
|
|
81
|
+
}.freeze
|
|
82
|
+
POSIX_CLASSES = {
|
|
83
|
+
"alnum" => [[0x30, 0x39], [0x41, 0x5A], [0x61, 0x7A]],
|
|
84
|
+
"alpha" => [[0x41, 0x5A], [0x61, 0x7A]],
|
|
85
|
+
"ascii" => [[0x00, 0x7F]],
|
|
86
|
+
"blank" => [[0x09, 0x09], [0x20, 0x20]],
|
|
87
|
+
"cntrl" => [[0x00, 0x1F], [0x7F, 0x7F]],
|
|
88
|
+
"digit" => [[0x30, 0x39]],
|
|
89
|
+
"graph" => [[0x21, 0x7E]],
|
|
90
|
+
"lower" => [[0x61, 0x7A]],
|
|
91
|
+
"print" => [[0x20, 0x7E]],
|
|
92
|
+
"punct" => [[0x21, 0x2F], [0x3A, 0x40], [0x5B, 0x60], [0x7B, 0x7E]],
|
|
93
|
+
"space" => [[0x09, 0x0D], [0x20, 0x20]],
|
|
94
|
+
"upper" => [[0x41, 0x5A]],
|
|
95
|
+
"word" => WORD,
|
|
96
|
+
"xdigit" => [[0x30, 0x39], [0x41, 0x46], [0x61, 0x66]]
|
|
97
|
+
}.freeze
|
|
98
|
+
SIMPLE_ESCAPES = { "a" => 0x07, "f" => 0x0C, "n" => 0x0A, "r" => 0x0D, "t" => 0x09, "v" => 0x0B }.freeze
|
|
99
|
+
PROPERTY_NAMES = (["Any"] + UnicodeTables::PROPERTY_NAMES).to_h { |n| [n, true] }.freeze
|
|
100
|
+
MAX_REPEAT = 1000
|
|
101
|
+
|
|
102
|
+
W = "[0-9A-Za-z_]"
|
|
103
|
+
WORD_BOUNDARY = "(?:(?<=#{W})(?!#{W})|(?<!#{W})(?=#{W}))".freeze
|
|
104
|
+
NOT_WORD_BOUNDARY = "(?:(?<=#{W})(?=#{W})|(?<!#{W})(?!#{W}))".freeze
|
|
105
|
+
private_constant :W
|
|
106
|
+
|
|
107
|
+
FOLD = 1
|
|
108
|
+
MULTILINE = 2
|
|
109
|
+
DOT_NL = 4
|
|
110
|
+
|
|
111
|
+
ORBIT_OF = UnicodeTables::ORBITS.each_with_object({}) do |orbit, h|
|
|
112
|
+
orbit.each { |c| h[c] = orbit }
|
|
113
|
+
end.freeze
|
|
114
|
+
|
|
115
|
+
FOLD_EXTRA = {} # rubocop:disable Style/MutableConstant
|
|
116
|
+
FOLD_EXTRA_LOCK = Mutex.new
|
|
117
|
+
private_constant :FOLD_EXTRA, :FOLD_EXTRA_LOCK
|
|
118
|
+
|
|
119
|
+
# Sets are lists of inclusive [lo, hi] code point ranges.
|
|
120
|
+
module Sets
|
|
121
|
+
module_function
|
|
122
|
+
|
|
123
|
+
def normalize(ranges)
|
|
124
|
+
merged = []
|
|
125
|
+
ranges.sort.each do |lo, hi|
|
|
126
|
+
if merged.empty? || lo > merged.last[1] + 1
|
|
127
|
+
merged << [lo, hi]
|
|
128
|
+
elsif hi > merged.last[1]
|
|
129
|
+
merged.last[1] = hi
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
merged
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def complement(ranges)
|
|
136
|
+
out = []
|
|
137
|
+
next_lo = 0
|
|
138
|
+
normalize(ranges).each do |lo, hi|
|
|
139
|
+
out << [next_lo, lo - 1] if lo > next_lo
|
|
140
|
+
next_lo = hi + 1
|
|
141
|
+
end
|
|
142
|
+
out << [next_lo, MAX_RUNE] if next_lo <= MAX_RUNE
|
|
143
|
+
out
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# fold adds every code point sharing a simple case folding orbit with
|
|
147
|
+
# one already in ranges.
|
|
148
|
+
def fold(ranges)
|
|
149
|
+
if ranges.size == 1 && ranges[0][0] == ranges[0][1]
|
|
150
|
+
same = ORBIT_OF[ranges[0][0]]
|
|
151
|
+
return same ? normalize(same.map { |c| [c, c] }) : ranges
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
out = ranges.dup
|
|
155
|
+
UnicodeTables::ORBITS.each do |orbit|
|
|
156
|
+
next unless orbit.any? { |c| ranges.any? { |lo, hi| c.between?(lo, hi) } }
|
|
157
|
+
|
|
158
|
+
orbit.each { |c| out << [c, c] }
|
|
159
|
+
end
|
|
160
|
+
normalize(out)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# folded_property is a Onigmo class matching the code points with
|
|
164
|
+
# property name, or without it when negated, and, under (?i), those
|
|
165
|
+
# an orbit of simple case folding ties to one that has it. Go adds
|
|
166
|
+
# the same code points (its FoldCategory and FoldScript tables).
|
|
167
|
+
def folded_property(name, negated)
|
|
168
|
+
extra = fold_extra(name)
|
|
169
|
+
return "\\p{#{"^" if negated}#{name}}" if extra.empty?
|
|
170
|
+
|
|
171
|
+
items = extra.map { |c| format("\\u{%x}", c) }.join
|
|
172
|
+
negated ? "[\\p{^#{name}}&&[^#{items}]]" : "[\\p{#{name}}#{items}]"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# fold_extra lists the code points without property name that share
|
|
176
|
+
# a folding orbit with one that has it.
|
|
177
|
+
def fold_extra(name)
|
|
178
|
+
FOLD_EXTRA_LOCK.synchronize do
|
|
179
|
+
FOLD_EXTRA[name] ||= begin
|
|
180
|
+
has = Regexp.new("\\A\\p{#{name}}\\z")
|
|
181
|
+
UnicodeTables::ORBITS.flat_map do |orbit|
|
|
182
|
+
members = orbit.map { |c| [c, has.match?([c].pack("U"))] }
|
|
183
|
+
members.any?(&:last) ? members.reject(&:last).map(&:first) : []
|
|
184
|
+
end.sort.freeze
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# without_surrogates drops code points no Ruby string can hold; no Go
|
|
190
|
+
# string decodes to them either, so they never match.
|
|
191
|
+
def without_surrogates(ranges)
|
|
192
|
+
normalize(ranges).flat_map do |lo, hi|
|
|
193
|
+
next [[lo, hi]] if hi < SURROGATES.begin || lo > SURROGATES.end
|
|
194
|
+
|
|
195
|
+
[([lo, SURROGATES.begin - 1] if lo < SURROGATES.begin),
|
|
196
|
+
([SURROGATES.end + 1, hi] if hi > SURROGATES.end)].compact
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Translator is a recursive-descent parser of RE2 syntax that emits the
|
|
202
|
+
# equivalent Onigmo source as it goes. Every group is emitted
|
|
203
|
+
# non-capturing: a condition only asks whether the pattern matches.
|
|
204
|
+
class Translator
|
|
205
|
+
def initialize(pattern)
|
|
206
|
+
pattern = pattern.dup.force_encoding(Encoding::UTF_8)
|
|
207
|
+
raise Error, "invalid UTF-8" unless pattern.valid_encoding?
|
|
208
|
+
|
|
209
|
+
@chars = pattern.chars
|
|
210
|
+
@pos = 0
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def translate
|
|
214
|
+
out = body(0, top: true)
|
|
215
|
+
raise Error, "unexpected )" unless eof?
|
|
216
|
+
|
|
217
|
+
out
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
def eof? = @pos >= @chars.size
|
|
223
|
+
|
|
224
|
+
def peek(offset = 0) = @chars[@pos + offset]
|
|
225
|
+
|
|
226
|
+
def advance
|
|
227
|
+
c = @chars[@pos]
|
|
228
|
+
@pos += 1
|
|
229
|
+
c
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def expect(char, message)
|
|
233
|
+
raise Error, message unless peek == char
|
|
234
|
+
|
|
235
|
+
advance
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Sequence is the Onigmo source of one group body as it is emitted,
|
|
239
|
+
# with where its last repeatable operand starts.
|
|
240
|
+
class Sequence
|
|
241
|
+
attr_reader :source
|
|
242
|
+
|
|
243
|
+
def initialize
|
|
244
|
+
@source = +""
|
|
245
|
+
@operand = nil
|
|
246
|
+
@repeated = false
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def operand(src)
|
|
250
|
+
@operand = @source.size
|
|
251
|
+
@repeated = false
|
|
252
|
+
@source << src
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# separate ends the current operand: after | or a flag group a
|
|
256
|
+
# repetition has no argument.
|
|
257
|
+
def separate(src = "")
|
|
258
|
+
@operand = nil
|
|
259
|
+
@source << src
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def repeat(quantifier)
|
|
263
|
+
raise Error, "missing argument to repetition operator" if @operand.nil?
|
|
264
|
+
raise Error, "invalid nested repetition operator" if @repeated
|
|
265
|
+
|
|
266
|
+
@source.insert(@operand, "(?:")
|
|
267
|
+
@source << ")" << quantifier
|
|
268
|
+
@repeated = true
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# body parses alternatives up to an unmatched ) or the end. A flag
|
|
273
|
+
# group without a body changes flags up to the end of the enclosing
|
|
274
|
+
# group, across alternatives, as in RE2.
|
|
275
|
+
def body(flags, top: false)
|
|
276
|
+
seq = Sequence.new
|
|
277
|
+
until eof? || (peek == ")" && !top)
|
|
278
|
+
case peek
|
|
279
|
+
when ")" then raise Error, "unexpected )"
|
|
280
|
+
when "|"
|
|
281
|
+
advance
|
|
282
|
+
seq.separate("|")
|
|
283
|
+
when "*", "+", "?", "{" then repeat_or_literal(seq, flags)
|
|
284
|
+
when "(" then flags = group(seq, flags)
|
|
285
|
+
when "\\" then escape(seq, flags)
|
|
286
|
+
else seq.operand(plain_atom(flags))
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
seq.source
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def repeat_or_literal(seq, flags)
|
|
293
|
+
quantifier = repetition
|
|
294
|
+
return seq.repeat(quantifier) if quantifier
|
|
295
|
+
|
|
296
|
+
advance
|
|
297
|
+
seq.operand(literal("{".ord, flags))
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# group reads a group or a flag group and returns the flags in force
|
|
301
|
+
# after it.
|
|
302
|
+
def group(seq, flags)
|
|
303
|
+
advance
|
|
304
|
+
inner = group_flags(flags)
|
|
305
|
+
if inner.is_a?(Array)
|
|
306
|
+
seq.separate
|
|
307
|
+
return inner.first
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
seq.operand("(?:#{body(inner)})")
|
|
311
|
+
expect(")", "missing )")
|
|
312
|
+
flags
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def plain_atom(flags)
|
|
316
|
+
c = advance
|
|
317
|
+
case c
|
|
318
|
+
when "[" then char_class(flags)
|
|
319
|
+
when "." then flags.anybits?(DOT_NL) ? "(?m:.)" : "[^\\n]"
|
|
320
|
+
when "^" then flags.anybits?(MULTILINE) ? "(?:\\A|(?<=\\n))" : "\\A"
|
|
321
|
+
when "$" then flags.anybits?(MULTILINE) ? "(?:\\z|(?=\\n))" : "\\z"
|
|
322
|
+
else literal(c.ord, flags)
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# repetition consumes *, +, ? or a well-formed {n}, {n,} or {n,m},
|
|
327
|
+
# and a lazy ?, returning the Onigmo quantifier. A { that does not
|
|
328
|
+
# start a well-formed repetition is a literal: it returns nil and
|
|
329
|
+
# consumes nothing. Laziness never changes whether a string matches.
|
|
330
|
+
def repetition
|
|
331
|
+
c = peek
|
|
332
|
+
quantifier =
|
|
333
|
+
if c == "{"
|
|
334
|
+
counts = repeat_counts
|
|
335
|
+
return nil unless counts
|
|
336
|
+
|
|
337
|
+
min, max = counts
|
|
338
|
+
invalid = min.negative? || min > MAX_REPEAT || max > MAX_REPEAT || (max >= 0 && min > max)
|
|
339
|
+
raise Error, "invalid repeat count" if invalid
|
|
340
|
+
|
|
341
|
+
if max == min
|
|
342
|
+
"{#{min}}"
|
|
343
|
+
else
|
|
344
|
+
"{#{min},#{max.negative? ? "" : max}}"
|
|
345
|
+
end
|
|
346
|
+
else
|
|
347
|
+
advance
|
|
348
|
+
c
|
|
349
|
+
end
|
|
350
|
+
advance if peek == "?"
|
|
351
|
+
quantifier
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# repeat_counts reads {n}, {n,} or {n,m} as [min, max] (max -1 when
|
|
355
|
+
# unbounded) and consumes it, or returns nil. A count of more than
|
|
356
|
+
# eight digits is -1 so that it is invalid, as in RE2.
|
|
357
|
+
def repeat_counts
|
|
358
|
+
saved = @pos
|
|
359
|
+
advance
|
|
360
|
+
min = count
|
|
361
|
+
max = min
|
|
362
|
+
if min && peek == ","
|
|
363
|
+
advance
|
|
364
|
+
max = peek == "}" ? -1 : count
|
|
365
|
+
end
|
|
366
|
+
return [min, max] if min && max && peek == "}" && advance
|
|
367
|
+
|
|
368
|
+
@pos = saved
|
|
369
|
+
nil
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def count
|
|
373
|
+
digits = +""
|
|
374
|
+
digits << advance while peek&.match?(/\A[0-9]\z/)
|
|
375
|
+
return nil if digits.empty? || (digits.size > 1 && digits.start_with?("0"))
|
|
376
|
+
|
|
377
|
+
digits.size > 8 ? -1 : digits.to_i
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# group_flags reads what follows (. It returns [flags] for a flag
|
|
381
|
+
# group without a body, or the flags of a group's body.
|
|
382
|
+
def group_flags(flags)
|
|
383
|
+
return flags unless peek == "?"
|
|
384
|
+
|
|
385
|
+
advance
|
|
386
|
+
if peek == "P" || (peek == "<" && ![nil, "=", "!"].include?(peek(1)))
|
|
387
|
+
advance if peek == "P"
|
|
388
|
+
expect("<", "invalid or unsupported Perl syntax")
|
|
389
|
+
group_name
|
|
390
|
+
return flags
|
|
391
|
+
end
|
|
392
|
+
flag_list(flags)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def group_name
|
|
396
|
+
name = +""
|
|
397
|
+
name << advance until eof? || peek == ">"
|
|
398
|
+
raise Error, "invalid named capture" if eof? || name.empty? || !name.match?(/\A[0-9A-Za-z_]+\z/)
|
|
399
|
+
|
|
400
|
+
advance
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def flag_list(flags)
|
|
404
|
+
negated = false
|
|
405
|
+
seen = false
|
|
406
|
+
until eof?
|
|
407
|
+
c = advance
|
|
408
|
+
case c
|
|
409
|
+
when "i", "m", "s", "U"
|
|
410
|
+
bit = { "i" => FOLD, "m" => MULTILINE, "s" => DOT_NL, "U" => 0 }.fetch(c)
|
|
411
|
+
flags = negated ? flags & ~bit : flags | bit
|
|
412
|
+
seen = true
|
|
413
|
+
when "-"
|
|
414
|
+
raise Error, "invalid or unsupported Perl syntax" if negated
|
|
415
|
+
|
|
416
|
+
negated = true
|
|
417
|
+
seen = false
|
|
418
|
+
when ":", ")"
|
|
419
|
+
raise Error, "missing flags" if negated && !seen
|
|
420
|
+
|
|
421
|
+
return c == ")" ? [flags] : flags
|
|
422
|
+
else
|
|
423
|
+
raise Error, "invalid or unsupported Perl syntax"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
raise Error, "missing )"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# escape emits what follows a backslash; every character of
|
|
430
|
+
# \Q...\E is an operand of its own.
|
|
431
|
+
def escape(seq, flags)
|
|
432
|
+
atom = escape_atom(flags)
|
|
433
|
+
atom.is_a?(Array) ? atom.each { |a| seq.operand(a) } : seq.operand(atom)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# escape_atom translates what follows a backslash outside a class.
|
|
437
|
+
# \Q...\E yields the list of its literals.
|
|
438
|
+
def escape_atom(flags)
|
|
439
|
+
advance
|
|
440
|
+
raise Error, "trailing backslash" if eof?
|
|
441
|
+
|
|
442
|
+
case peek
|
|
443
|
+
when "A" then advance && "\\A"
|
|
444
|
+
when "z" then advance && "\\z"
|
|
445
|
+
when "b" then advance && WORD_BOUNDARY
|
|
446
|
+
when "B" then advance && NOT_WORD_BOUNDARY
|
|
447
|
+
when "Q" then advance && quoted_literals(flags)
|
|
448
|
+
when "d", "s", "w", "D", "S", "W"
|
|
449
|
+
ranges, props = perl_class(flags)
|
|
450
|
+
set(ranges, props, negated: false)
|
|
451
|
+
when "p", "P"
|
|
452
|
+
ranges, props = unicode_class(flags)
|
|
453
|
+
set(ranges, props, negated: false)
|
|
454
|
+
else literal(escaped_rune, flags)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def quoted_literals(flags)
|
|
459
|
+
out = []
|
|
460
|
+
until eof?
|
|
461
|
+
if peek == "\\" && peek(1) == "E"
|
|
462
|
+
@pos += 2
|
|
463
|
+
break
|
|
464
|
+
end
|
|
465
|
+
out << literal(advance.ord, flags)
|
|
466
|
+
end
|
|
467
|
+
out
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# escaped_rune reads an escape denoting one code point, the
|
|
471
|
+
# backslash already consumed.
|
|
472
|
+
def escaped_rune
|
|
473
|
+
c = advance
|
|
474
|
+
case c
|
|
475
|
+
when "1".."7"
|
|
476
|
+
raise Error, "invalid escape sequence" unless peek&.match?(/\A[0-7]\z/)
|
|
477
|
+
|
|
478
|
+
octal(c)
|
|
479
|
+
when "0" then octal(c)
|
|
480
|
+
when "x" then hex
|
|
481
|
+
else
|
|
482
|
+
return SIMPLE_ESCAPES.fetch(c) if SIMPLE_ESCAPES.key?(c)
|
|
483
|
+
return c.ord if c.ord < 0x80 && !c.match?(/\A[0-9A-Za-z]\z/)
|
|
484
|
+
|
|
485
|
+
raise Error, "invalid escape sequence"
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def octal(first)
|
|
490
|
+
digits = first.dup
|
|
491
|
+
digits << advance while digits.size < 3 && peek&.match?(/\A[0-7]\z/)
|
|
492
|
+
digits.to_i(8)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def hex
|
|
496
|
+
raise Error, "invalid escape sequence" if eof?
|
|
497
|
+
|
|
498
|
+
if peek == "{"
|
|
499
|
+
advance
|
|
500
|
+
digits = +""
|
|
501
|
+
digits << advance while peek&.match?(/\A\h\z/)
|
|
502
|
+
raise Error, "invalid escape sequence" if digits.empty? || peek != "}" || digits.to_i(16) > MAX_RUNE
|
|
503
|
+
|
|
504
|
+
advance
|
|
505
|
+
return digits.to_i(16)
|
|
506
|
+
end
|
|
507
|
+
digits = [advance, advance].join
|
|
508
|
+
raise Error, "invalid escape sequence" unless digits.match?(/\A\h\h\z/)
|
|
509
|
+
|
|
510
|
+
digits.to_i(16)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# perl_class reads \d, \s, \w or a negation, the backslash consumed,
|
|
514
|
+
# as [ranges, props].
|
|
515
|
+
def perl_class(flags)
|
|
516
|
+
c = advance
|
|
517
|
+
ranges = PERL_CLASSES.fetch(c.downcase)
|
|
518
|
+
ranges = Sets.fold(ranges) if flags.anybits?(FOLD)
|
|
519
|
+
[c == c.upcase ? Sets.complement(ranges) : ranges, []]
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# unicode_class reads \pN, \p{Name}, \p{^Name} or a \P negation,
|
|
523
|
+
# the backslash consumed, as [ranges, props].
|
|
524
|
+
def unicode_class(flags)
|
|
525
|
+
negated = advance == "P"
|
|
526
|
+
raise Error, "invalid character class range" if eof?
|
|
527
|
+
|
|
528
|
+
name = advance
|
|
529
|
+
if name == "{"
|
|
530
|
+
name = +""
|
|
531
|
+
name << advance until eof? || peek == "}"
|
|
532
|
+
expect("}", "invalid character class range")
|
|
533
|
+
end
|
|
534
|
+
if name.start_with?("^")
|
|
535
|
+
negated = !negated
|
|
536
|
+
name = name[1..]
|
|
537
|
+
end
|
|
538
|
+
raise Error, "invalid character class range" unless PROPERTY_NAMES.key?(name)
|
|
539
|
+
return [[], [Sets.folded_property(name, negated)]] if flags.anybits?(FOLD) && name != "Any"
|
|
540
|
+
|
|
541
|
+
[[], ["\\p{#{"^" if negated}#{name}}"]]
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# char_class reads a class after its [.
|
|
545
|
+
def char_class(flags)
|
|
546
|
+
negated = peek == "^"
|
|
547
|
+
advance if negated
|
|
548
|
+
ranges = []
|
|
549
|
+
props = []
|
|
550
|
+
first = true
|
|
551
|
+
while eof? || peek != "]" || first
|
|
552
|
+
first = false
|
|
553
|
+
r, p = class_item(flags)
|
|
554
|
+
ranges.concat(r)
|
|
555
|
+
props.concat(p)
|
|
556
|
+
end
|
|
557
|
+
advance
|
|
558
|
+
set(ranges, props, negated: negated)
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def class_item(flags)
|
|
562
|
+
if peek == "[" && peek(1) == ":"
|
|
563
|
+
posix = posix_class(flags)
|
|
564
|
+
return posix if posix
|
|
565
|
+
end
|
|
566
|
+
if peek == "\\" && %w[p P].include?(peek(1))
|
|
567
|
+
advance
|
|
568
|
+
return unicode_class(flags)
|
|
569
|
+
end
|
|
570
|
+
if peek == "\\" && %w[d s w D S W].include?(peek(1))
|
|
571
|
+
advance
|
|
572
|
+
return perl_class(flags)
|
|
573
|
+
end
|
|
574
|
+
lo = class_char
|
|
575
|
+
hi = lo
|
|
576
|
+
if peek == "-" && ![nil, "]"].include?(peek(1))
|
|
577
|
+
advance
|
|
578
|
+
hi = class_char
|
|
579
|
+
raise Error, "invalid character class range" if hi < lo
|
|
580
|
+
end
|
|
581
|
+
ranges = [[lo, hi]]
|
|
582
|
+
[flags.anybits?(FOLD) ? Sets.fold(ranges) : ranges, []]
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# posix_class reads [:name:] or [:^name:], or returns nil when no :]
|
|
586
|
+
# follows, making the [ a literal.
|
|
587
|
+
def posix_class(flags)
|
|
588
|
+
close = @chars[(@pos + 2)..].each_cons(2).find_index { |a, b| a == ":" && b == "]" }
|
|
589
|
+
return nil unless close
|
|
590
|
+
|
|
591
|
+
name = @chars[@pos + 2, close].join
|
|
592
|
+
negated = name.start_with?("^")
|
|
593
|
+
ranges = POSIX_CLASSES[negated ? name[1..] : name]
|
|
594
|
+
raise Error, "invalid character class range" unless ranges
|
|
595
|
+
|
|
596
|
+
@pos += close + 4
|
|
597
|
+
ranges = Sets.fold(ranges) if flags.anybits?(FOLD)
|
|
598
|
+
[negated ? Sets.complement(ranges) : ranges, []]
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def class_char
|
|
602
|
+
raise Error, "missing closing ]" if eof?
|
|
603
|
+
|
|
604
|
+
c = advance
|
|
605
|
+
return c.ord unless c == "\\"
|
|
606
|
+
raise Error, "trailing backslash" if eof?
|
|
607
|
+
|
|
608
|
+
escaped_rune
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def literal(code_point, flags)
|
|
612
|
+
ranges = [[code_point, code_point]]
|
|
613
|
+
ranges = Sets.fold(ranges) if flags.anybits?(FOLD)
|
|
614
|
+
if ranges.size == 1 && ranges[0][0] == ranges[0][1] && !SURROGATES.cover?(code_point)
|
|
615
|
+
rune(code_point)
|
|
616
|
+
else
|
|
617
|
+
set(
|
|
618
|
+
ranges, [], negated: false
|
|
619
|
+
)
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# set emits a class. Ruby cannot hold surrogates, so they are dropped
|
|
624
|
+
# from both a set and its negation; neither matches them in RE2.
|
|
625
|
+
def set(ranges, props, negated:)
|
|
626
|
+
ranges = Sets.without_surrogates(ranges)
|
|
627
|
+
if ranges.empty? && props.empty?
|
|
628
|
+
negated ? "(?m:.)" : "(?!)"
|
|
629
|
+
else
|
|
630
|
+
items = ranges.map { |lo, hi| lo == hi ? rune(lo) : "#{rune(lo)}-#{rune(hi)}" }
|
|
631
|
+
"[#{"^" if negated}#{items.join}#{props.join}]"
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def rune(code_point)
|
|
636
|
+
if code_point < 0x80 && code_point.chr.match?(/\A[0-9A-Za-z]\z/)
|
|
637
|
+
code_point.chr
|
|
638
|
+
else
|
|
639
|
+
format("\\u{%x}", code_point)
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
private_constant :Translator, :Sets
|
|
644
|
+
end
|
|
645
|
+
private_constant :Regex, :UnicodeTables
|
|
646
|
+
end
|
|
647
|
+
end
|