regexp_parser 2.12.0 → 2.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa5734a20a0705226c021d9a0efef48a6ca24b5b18f15f93b34c12ffe5021d1e
4
- data.tar.gz: fa77ac98b3bd17d6bdba43d387ebbbd84a6a0a5fa86123c7eee68646e90ce37c
3
+ metadata.gz: 3064d283e8ab829bdaffd9312b9e975ff3c47ace80c2add1dd9336c4cefb955b
4
+ data.tar.gz: 78c30fac8b319e64ba6cadf096b03e88f65fd87171f706bb4ad6f1d2abbcd9c5
5
5
  SHA512:
6
- metadata.gz: 4bb62063aaa64e3828c5abf551fbf5ce83f726b5ebeb56af3e9210fd7e8c110c7f8f9ba1fcafd0dd0f66b8e8f36f04c1d72e5a739c4ace8fa19978dfc1a794b1
7
- data.tar.gz: a82384912534ec3ca98d7665434c655468b15e4d75d6c1f13405f0b777d818746c6686d36049a158f0df9b829c129026487e5c3e6cf8a8f7c7ca65155a937db3
6
+ metadata.gz: d3d1a370ef4671e0dfb50e30612af9364e26f82b0facdc3e526dcf403c06e9fefa52e31f68f7d1949ca28d213603a46af2385ff8831822494b29cc9d945b2f90
7
+ data.tar.gz: b81338ab447b5f03156770c6ba7f165dcc18f4d0ff0c9c3e00a64baedae62da927d508e1fb68c7893eb19738bf6db9edec204bc248b89a92af0f81a60d31df16
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010, 2012-2025, Ammar Ali
1
+ Copyright (c) 2010-2026, Ammar Ali
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person
4
4
  obtaining a copy of this software and associated documentation
@@ -26,24 +26,17 @@ module Regexp::Expression
26
26
 
27
27
  # Deprecated. Prefer `#repetitions` which has a more uniform interface.
28
28
  def quantity
29
- return [nil,nil] unless quantified?
29
+ return [nil, nil] unless quantified?
30
+
30
31
  [quantifier.min, quantifier.max]
31
32
  end
32
33
 
33
34
  def repetitions
34
- @repetitions ||=
35
- if quantified?
36
- min = quantifier.min
37
- max = quantifier.max < 0 ? Float::INFINITY : quantifier.max
38
- range = min..max
39
- # fix Range#minmax on old Rubies - https://bugs.ruby-lang.org/issues/15807
40
- if RUBY_VERSION.to_f < 2.7
41
- range.define_singleton_method(:minmax) { [min, max] }
42
- end
43
- range
44
- else
45
- 1..1
46
- end
35
+ return 1..1 unless quantified?
36
+
37
+ min = quantifier.min
38
+ max = quantifier.max < 0 ? Float::INFINITY : quantifier.max
39
+ min..max
47
40
  end
48
41
 
49
42
  def greedy?
@@ -74,5 +67,48 @@ module Regexp::Expression
74
67
  }
75
68
  end
76
69
  alias :attributes :to_h
70
+
71
+ # Recalculates the nesting_level for this node and children, which reflects
72
+ # how deep a node is in the tree. Do this at the end of parsing to account
73
+ # for tree rewrites.
74
+ # This must be safe against overflows and performant - check the benchmarks.
75
+ def recursively_update_nesting_levels(base = nesting_level)
76
+ queue = [base, self]
77
+
78
+ until queue.empty?
79
+ exp = queue.pop
80
+ exp_lvl = queue.pop
81
+ exp.nesting_level = exp_lvl
82
+ exp.quantifier.nesting_level = exp_lvl if exp.quantifier
83
+ exp.terminal? || exp.expressions.each do |subexp|
84
+ queue.push(exp_lvl + 1, subexp)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Assigns referenced expressions to referring expressions, e.g. if there is
90
+ # an instance of Backreference::Number, its #referenced_expression is set to
91
+ # the instance of Group::Capture that it refers to via its number.
92
+ def assign_referenced_expressions
93
+ # find all referenceable and referring expressions
94
+ targets = {}
95
+ targets[0] = [self] if instance_of?(Root)
96
+ referrers = []
97
+ each_expression do |exp|
98
+ if exp.referential?
99
+ referrers << exp
100
+ elsif exp.is_a?(Group::Capture)
101
+ (targets[exp.identifier] ||= []) << exp
102
+ end
103
+ end
104
+ # assign referenced expressions to referring expressions
105
+ # (in a second iteration because there might be forward references)
106
+ referrers.each do |exp|
107
+ exp.referenced_expressions = targets[exp.reference] || raise(
108
+ Regexp::Parser::ParserError,
109
+ "Invalid reference #{exp.reference} at pos #{exp.ts}"
110
+ )
111
+ end
112
+ end
77
113
  end
78
114
  end
@@ -14,7 +14,7 @@ module Regexp::Expression
14
14
  end
15
15
 
16
16
  def complete?
17
- count == 2
17
+ length == 2
18
18
  end
19
19
  end
20
20
  end
@@ -22,4 +22,4 @@ module Regexp::Expression
22
22
 
23
23
  # alias for symmetry between token symbol and Expression class name
24
24
  Set = CharacterSet
25
- end # module Regexp::Expression
25
+ end
@@ -118,4 +118,4 @@ module Regexp::Expression
118
118
  # alias for symmetry between token symbol and Expression class name
119
119
  Property = UnicodeProperty
120
120
  Nonproperty = UnicodeProperty
121
- end # module Regexp::Expression
121
+ end
@@ -112,9 +112,9 @@ module Regexp::Expression
112
112
  class Subexpression
113
113
  def match_length
114
114
  MatchLength.new(self,
115
- base_min: map { |exp| exp.match_length.min }.inject(0, :+),
116
- base_max: map { |exp| exp.match_length.max }.inject(0, :+),
117
- reify: ->{ map { |exp| exp.match_length.to_re }.join })
115
+ base_min: map { |exp| exp.match_length.min }.inject(0, :+),
116
+ base_max: map { |exp| exp.match_length.max }.inject(0, :+),
117
+ reify: ->{ map { |exp| exp.match_length.to_re }.join })
118
118
  end
119
119
 
120
120
  def inner_match_length
@@ -132,9 +132,9 @@ module Regexp::Expression
132
132
  klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
133
133
  def match_length
134
134
  MatchLength.new(self,
135
- base_min: map { |exp| exp.match_length.min }.min,
136
- base_max: map { |exp| exp.match_length.max }.max,
137
- reify: ->{ map { |exp| exp.match_length.to_re }.join('|') })
135
+ base_min: map { |exp| exp.match_length.min }.min,
136
+ base_max: map { |exp| exp.match_length.max }.max,
137
+ reify: ->{ map { |exp| exp.match_length.to_re }.join('|') })
138
138
  end
139
139
  RUBY
140
140
  end
@@ -158,7 +158,15 @@ module Regexp::Expression
158
158
  if referenced_expression.nil?
159
159
  raise ArgumentError, 'Missing referenced_expression - not parsed?'
160
160
  end
161
- referenced_expression.unquantified_clone.match_length
161
+ # The targets' own quantifiers do not apply: a backref/subexp match what
162
+ # the target captures in one repetition. The backref/call's own quantifier
163
+ # does apply, and they can be multiplexed, i.e. refer to several targets.
164
+ targets = referenced_expressions
165
+ .map { |exp| exp.unquantified_clone.match_length }
166
+ MatchLength.new(self,
167
+ base_min: targets.map(&:min).min,
168
+ base_max: targets.map(&:max).max,
169
+ reify: ->{ targets.map(&:to_re).join('|') })
162
170
  end
163
171
  end
164
172
 
@@ -101,14 +101,30 @@ module Regexp::Expression
101
101
  # When changing the conditions, please make sure to update
102
102
  # #pretty_print_instance_variables so that it includes all relevant values.
103
103
  def ==(other)
104
+ return shallow_equal?(other) if terminal?
105
+
106
+ stack = [[self, other]]
107
+ until stack.empty?
108
+ exp1, exp2 = stack.pop
109
+ return false unless exp1.shallow_equal?(exp2)
110
+
111
+ next if exp1.terminal?
112
+ return false unless exp1.length == exp2.length
113
+
114
+ stack.concat(exp1.zip(exp2))
115
+ end
116
+ true
117
+ end
118
+ alias :=== :==
119
+ alias :eql? :==
120
+
121
+ # Compares two expressions without considering their subexpressions.
122
+ def shallow_equal?(other)
104
123
  self.class == other.class &&
105
124
  text == other.text &&
106
125
  quantifier == other.quantifier &&
107
- options == other.options &&
108
- (terminal? || expressions == other.expressions)
126
+ options == other.options
109
127
  end
110
- alias :=== :==
111
- alias :eql? :==
112
128
 
113
129
  def optional?
114
130
  quantified? && quantifier.min == 0
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
-
2
+ #
3
+ # This file implements several tree traversal methods.
4
+ #
5
+ # Note: The methods are optimized for performance and avoid stack overflows on
6
+ # large trees. Check tasks/benchmark/traversal.rb when working in here.
3
7
  module Regexp::Expression
4
8
  class Subexpression < Regexp::Expression::Base
5
9
 
@@ -36,14 +40,30 @@ module Regexp::Expression
36
40
 
37
41
  block.call(:enter, self, 0) if include_self
38
42
 
39
- each_with_index do |exp, index|
40
- if exp.terminal?
41
- block.call(:visit, exp, index)
42
- else
43
- block.call(:enter, exp, index)
44
- exp.traverse(&block)
45
- block.call(:exit, exp, index)
43
+ stack = []
44
+ parent, children, index = self, expressions, 0
45
+
46
+ loop do
47
+ while (exp = children[index])
48
+ if exp.terminal?
49
+ block.call(:visit, exp, index)
50
+ index += 1
51
+ else
52
+ block.call(:enter, exp, index)
53
+ # Resume at this child to emit its exit event after descending.
54
+ stack.push(parent, index)
55
+ parent, children, index = exp, exp.expressions, 0
56
+ end
46
57
  end
58
+
59
+ break if stack.empty?
60
+
61
+ exp = parent
62
+ index = stack.pop
63
+ parent = stack.pop
64
+ children = parent.expressions
65
+ block.call(:exit, exp, index)
66
+ index += 1
47
67
  end
48
68
 
49
69
  block.call(:exit, self, 0) if include_self
@@ -66,16 +86,34 @@ module Regexp::Expression
66
86
  protected
67
87
 
68
88
  def each_expression_with_index(&block)
69
- each_with_index do |exp, index|
70
- block.call(exp, index)
71
- exp.each_expression_with_index(&block) unless exp.terminal?
89
+ stack = []
90
+ children, index = expressions, 0
91
+
92
+ loop do
93
+ while (exp = children[index])
94
+ block.call(exp, index)
95
+ index += 1
96
+ unless exp.terminal?
97
+ # Resume at the next sibling after visiting this child's subtree.
98
+ stack.push(children, index)
99
+ children, index = exp.expressions, 0
100
+ end
101
+ end
102
+
103
+ break if stack.empty?
104
+
105
+ index = stack.pop
106
+ children = stack.pop
72
107
  end
73
108
  end
74
109
 
75
110
  def each_expression_without_index(&block)
76
- each do |exp|
111
+ queue = expressions.reverse
112
+
113
+ until queue.empty?
114
+ exp = queue.pop
77
115
  block.call(exp)
78
- exp.each_expression_without_index(&block) unless exp.terminal?
116
+ queue.concat(exp.expressions.reverse) unless exp.terminal?
79
117
  end
80
118
  end
81
119
  end
@@ -11,7 +11,7 @@ module Regexp::Expression
11
11
  MODES = %i[greedy possessive reluctant].freeze
12
12
 
13
13
  def initialize(*args)
14
- deprecated_old_init(*args) and return if args.count == 4 || args.count == 5
14
+ deprecated_old_init(*args) and return if args.length == 4 || args.length == 5
15
15
 
16
16
  init_from_token_and_options(*args)
17
17
  # TODO: remove in v3.0.0, stop removing parts of #token (?)
@@ -9,11 +9,9 @@ module Regexp::Expression
9
9
  extend Shared::ClassMethods
10
10
 
11
11
  attr_accessor :type, :token, :text, :ts, :te,
12
- :level, :set_level, :conditional_level,
13
- :options, :parent,
12
+ :level, :set_level, :conditional_level, :nesting_level,
13
+ :options, :parent, :quantifier,
14
14
  :custom_to_s_handling, :pre_quantifier_decorations
15
-
16
- attr_reader :nesting_level, :quantifier
17
15
  end
18
16
  end
19
17
 
@@ -72,15 +70,30 @@ module Regexp::Expression
72
70
  # lit.to_s(:original) # => 'a +' # with quantifier AND intermittent decorations
73
71
  #
74
72
  def to_s(format = :full)
75
- base = ''.dup
76
- parts.each do |part|
77
- if part.instance_of?(String)
78
- base << part
79
- elsif !part.custom_to_s_handling
80
- base << part.to_s(:original)
73
+ result = ''.dup
74
+ stack = []
75
+ exp, current_parts, index = self, parts, 0
76
+
77
+ loop do
78
+ while index < current_parts.length
79
+ part = current_parts[index]
80
+ index += 1
81
+ if part.instance_of?(String)
82
+ result << part
83
+ elsif !part.custom_to_s_handling
84
+ stack.push([exp, format, current_parts, index])
85
+ exp, format, current_parts, index = part, :original, part.parts, 0
86
+ end
81
87
  end
88
+
89
+ result << exp.pre_quantifier_decoration(format).to_s
90
+ result << exp.quantifier_affix(format).to_s
91
+ break if stack.empty?
92
+
93
+ exp, format, current_parts, index = stack.pop
82
94
  end
83
- "#{base}#{pre_quantifier_decoration(format)}#{quantifier_affix(format)}"
95
+
96
+ result
84
97
  end
85
98
  alias :to_str :to_s
86
99
 
@@ -99,16 +112,5 @@ module Regexp::Expression
99
112
  def coded_offset
100
113
  '@%d+%d' % offset
101
114
  end
102
-
103
- def nesting_level=(lvl)
104
- @nesting_level = lvl
105
- quantifier && quantifier.nesting_level = lvl
106
- terminal? || each { |subexp| subexp.nesting_level = lvl + 1 }
107
- end
108
-
109
- def quantifier=(qtf)
110
- @quantifier = qtf
111
- @repetitions = nil # clear memoized value
112
- end
113
115
  end
114
116
  end
@@ -24,7 +24,7 @@ module Regexp::Expression
24
24
  expressions << exp
25
25
  end
26
26
 
27
- %w[[] at each empty? fetch index join last length values_at].each do |method|
27
+ %w[[] at each empty? fetch index join last length values_at zip].each do |method|
28
28
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
29
29
  def #{method}(*args, &block)
30
30
  expressions.#{method}(*args, &block)
@@ -51,7 +51,7 @@ module Regexp::Expression
51
51
 
52
52
  def extract_quantifier_target(quantifier_description)
53
53
  pre_quantifier_decorations = []
54
- target = expressions.reverse.find do |exp|
54
+ target = expressions.reverse_each.find do |exp|
55
55
  if exp.decorative?
56
56
  exp.custom_to_s_handling = true
57
57
  pre_quantifier_decorations << exp.text
@@ -5,19 +5,17 @@
5
5
  # normalizes tokens for the parser, and checks if they are implemented by the
6
6
  # given syntax flavor.
7
7
  class Regexp::Lexer
8
-
9
8
  OPENING_TOKENS = %i[
10
9
  capture passive lookahead nlookahead lookbehind nlookbehind
11
10
  atomic options options_switch named absence open
12
11
  ].freeze
13
12
 
14
- CLOSING_TOKENS = %i[close].freeze
15
-
16
13
  CONDITION_TOKENS = %i[condition condition_close].freeze
17
14
 
18
15
  def self.lex(input, syntax = nil, options: nil, collect_tokens: true, &block)
19
16
  new.lex(input, syntax, options: options, collect_tokens: collect_tokens, &block)
20
17
  end
18
+ class << self; alias :scan :lex end
21
19
 
22
20
  def lex(input, syntax = nil, options: nil, collect_tokens: true, &block)
23
21
  syntax = syntax ? Regexp::Syntax.for(syntax) : Regexp::Syntax::CURRENT
@@ -33,19 +31,21 @@ class Regexp::Lexer
33
31
  self.shift = 0
34
32
 
35
33
  Regexp::Scanner.scan(input, options: options, collect_tokens: false) do |type, token, text, ts, te|
36
- type, token = *syntax.normalize(type, token)
34
+ type, token = syntax.normalize(type, token)
37
35
  syntax.check! type, token
38
36
 
39
37
  ascend(type, token)
40
38
 
41
- if (last = prev_token) &&
42
- type == :quantifier &&
43
- (
44
- (last.type == :literal && (parts = break_literal(last))) ||
45
- (last.token == :codepoint_list && (parts = break_codepoint_list(last)))
46
- )
47
- emit(parts[0])
48
- last = parts[1]
39
+ if (last = prev_token) && type == :quantifier
40
+ shortened_run, quantified_node =
41
+ if last.type == :literal then break_literal_run(last)
42
+ elsif last.token == :codepoint_list then break_codepoint_list(last)
43
+ end
44
+
45
+ if shortened_run
46
+ emit(shortened_run)
47
+ last = quantified_node
48
+ end
49
49
  end
50
50
 
51
51
  current = Regexp::Token.new(type, token, text, ts + shift, te + shift,
@@ -80,10 +80,6 @@ class Regexp::Lexer
80
80
  end
81
81
  end
82
82
 
83
- class << self
84
- alias :scan :lex
85
- end
86
-
87
83
  private
88
84
 
89
85
  attr_accessor :block,
@@ -91,7 +87,7 @@ class Regexp::Lexer
91
87
  :nesting, :set_nesting, :conditional_nesting, :shift
92
88
 
93
89
  def ascend(type, token)
94
- return unless CLOSING_TOKENS.include?(token)
90
+ return unless token == :close
95
91
 
96
92
  case type
97
93
  when :group, :assertion
@@ -122,7 +118,7 @@ class Regexp::Lexer
122
118
 
123
119
  # called by scan to break a literal run that is longer than one character
124
120
  # into two separate tokens when it is followed by a quantifier
125
- def break_literal(token)
121
+ def break_literal_run(token)
126
122
  lead, last, _ = token.text.partition(/.\z/mu)
127
123
  return if lead.empty?
128
124
 
@@ -141,7 +137,7 @@ class Regexp::Lexer
141
137
 
142
138
  # if a codepoint list is followed by a quantifier, that quantifier applies
143
139
  # to the last codepoint, e.g. /\u{61 62 63}{3}/ =~ 'abccc'
144
- # c.f. #break_literal.
140
+ # c.f. #break_literal_run.
145
141
  def break_codepoint_list(token)
146
142
  lead, _, tail = token.text.rpartition(' ')
147
143
  return if lead.empty?
@@ -153,7 +149,11 @@ class Regexp::Lexer
153
149
  (token.ts + lead.length + 1), (token.te + 3),
154
150
  nesting, set_nesting, conditional_nesting)
155
151
 
156
- self.shift = shift + 3 # one space less, but extra \, u, {, and }
152
+ # Splitting the codepoint list into two parts changes the correct offsets
153
+ # of any following tokens because it requires extra chars to represent the
154
+ # 'virtual' token, e.g. `\u{61 62 63}{3}` is lexed as `\u{61 62}\u{63}{3}`,
155
+ # which is 3 chars longer.
156
+ self.shift = shift + 3
157
157
 
158
158
  token_1.previous = preprev_token
159
159
  token_1.next = token_2
@@ -167,5 +167,4 @@ class Regexp::Lexer
167
167
  token.previous = preprev_token # .next will be set by #lex
168
168
  token
169
169
  end
170
-
171
- end # module Regexp::Lexer
170
+ end
@@ -36,15 +36,14 @@ class Regexp::Parser
36
36
  self.conditional_nesting = []
37
37
 
38
38
  self.captured_group_counts = Hash.new(0)
39
+ self.total_captured_group_count = 0
39
40
 
40
- Regexp::Lexer.scan(input, syntax, options: options, collect_tokens: false) do |token|
41
+ Regexp::Lexer.new.lex(input, syntax, options: options, collect_tokens: false) do |token|
41
42
  parse_token(token)
42
43
  end
43
44
 
44
- # Trigger recursive setting of #nesting_level, which reflects how deep
45
- # a node is in the tree. Do this at the end to account for tree rewrites.
46
- root.nesting_level = 0
47
- assign_referenced_expressions
45
+ root.assign_referenced_expressions
46
+ root.recursively_update_nesting_levels
48
47
 
49
48
  if block_given?
50
49
  block.call(root)
@@ -57,7 +56,7 @@ class Regexp::Parser
57
56
 
58
57
  attr_accessor :root, :node, :nesting,
59
58
  :options_stack, :switching_options, :conditional_nesting,
60
- :captured_group_counts
59
+ :captured_group_counts, :total_captured_group_count
61
60
 
62
61
  def extract_options(input, options)
63
62
  if options && !input.is_a?(String)
@@ -197,16 +196,13 @@ class Regexp::Parser
197
196
  nest(group)
198
197
  end
199
198
 
200
- def total_captured_group_count
201
- captured_group_counts.values.reduce(0, :+)
202
- end
203
-
204
199
  def captured_group_count_at_level
205
200
  captured_group_counts[node]
206
201
  end
207
202
 
208
203
  def count_captured_group
209
204
  captured_group_counts[node] += 1
205
+ self.total_captured_group_count = total_captured_group_count + 1
210
206
  end
211
207
 
212
208
  def close_group
@@ -496,7 +492,10 @@ class Regexp::Parser
496
492
  )
497
493
  new_group.implicit = true
498
494
  new_group << target_node
499
- increase_group_level(target_node)
495
+ increment_group_level(target_node)
496
+ unless target_node.terminal?
497
+ target_node.each_expression { |exp| increment_group_level(exp) }
498
+ end
500
499
  node.expressions[node.expressions.index(target_node)] = new_group
501
500
  target_node = new_group
502
501
  end
@@ -509,10 +508,9 @@ class Regexp::Parser
509
508
  target_node.quantify(token, active_opts)
510
509
  end
511
510
 
512
- def increase_group_level(exp)
511
+ def increment_group_level(exp)
513
512
  exp.level += 1
514
513
  exp.quantifier.level += 1 if exp.quantifier
515
- exp.terminal? || exp.each { |subexp| increase_group_level(subexp) }
516
514
  end
517
515
 
518
516
  def set(token)
@@ -577,26 +575,4 @@ class Regexp::Parser
577
575
  def active_opts
578
576
  options_stack.last
579
577
  end
580
-
581
- # Assigns referenced expressions to referring expressions, e.g. if there is
582
- # an instance of Backreference::Number, its #referenced_expression is set to
583
- # the instance of Group::Capture that it refers to via its number.
584
- def assign_referenced_expressions
585
- # find all referenceable and referring expressions
586
- targets = { 0 => [root] }
587
- referrers = []
588
- root.each_expression do |exp|
589
- if exp.referential?
590
- referrers << exp
591
- elsif exp.is_a?(Group::Capture)
592
- (targets[exp.identifier] ||= []) << exp
593
- end
594
- end
595
- # assign referenced expressions to referring expressions
596
- # (in a second iteration because there might be forward references)
597
- referrers.each do |exp|
598
- exp.referenced_expressions = targets[exp.reference] ||
599
- raise(ParserError, "Invalid reference #{exp.reference} at pos #{exp.ts}")
600
- end
601
- end
602
- end # module Regexp::Parser
578
+ end