parser 2.7.0.5 → 2.7.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +21 -32
- data/CHANGELOG.md +59 -1
- data/README.md +2 -2
- data/Rakefile +2 -1
- data/doc/AST_FORMAT.md +106 -3
- data/lib/parser.rb +1 -0
- data/lib/parser/all.rb +1 -0
- data/lib/parser/ast/processor.rb +9 -0
- data/lib/parser/builders/default.rb +103 -12
- data/lib/parser/context.rb +1 -0
- data/lib/parser/current.rb +13 -4
- data/lib/parser/diagnostic.rb +1 -1
- data/lib/parser/diagnostic/engine.rb +1 -2
- data/lib/parser/lexer.rl +15 -1
- data/lib/parser/macruby.y +14 -4
- data/lib/parser/messages.rb +15 -0
- data/lib/parser/meta.rb +4 -4
- data/lib/parser/ruby18.y +2 -0
- data/lib/parser/ruby19.y +14 -4
- data/lib/parser/ruby20.y +14 -4
- data/lib/parser/ruby21.y +9 -2
- data/lib/parser/ruby22.y +9 -2
- data/lib/parser/ruby23.y +9 -2
- data/lib/parser/ruby24.y +9 -2
- data/lib/parser/ruby25.y +9 -2
- data/lib/parser/ruby26.y +9 -2
- data/lib/parser/ruby27.y +28 -8
- data/lib/parser/ruby28.y +3043 -0
- data/lib/parser/rubymotion.y +14 -4
- data/lib/parser/runner.rb +26 -2
- data/lib/parser/runner/ruby_rewrite.rb +2 -2
- data/lib/parser/source/buffer.rb +3 -1
- data/lib/parser/source/comment/associator.rb +14 -4
- data/lib/parser/source/map/endless_definition.rb +23 -0
- data/lib/parser/source/range.rb +17 -1
- data/lib/parser/source/tree_rewriter.rb +115 -12
- data/lib/parser/source/tree_rewriter/action.rb +135 -26
- data/lib/parser/tree_rewriter.rb +1 -2
- data/lib/parser/version.rb +1 -1
- data/parser.gemspec +3 -2
- data/test/helper.rb +49 -6
- data/test/parse_helper.rb +27 -23
- data/test/test_ast_processor.rb +32 -0
- data/test/test_base.rb +1 -1
- data/test/test_current.rb +2 -0
- data/test/test_diagnostic.rb +6 -7
- data/test/test_diagnostic_engine.rb +5 -8
- data/test/test_lexer.rb +17 -8
- data/test/test_meta.rb +12 -0
- data/test/test_parser.rb +477 -54
- data/test/test_runner_parse.rb +22 -1
- data/test/test_runner_rewrite.rb +1 -1
- data/test/test_source_buffer.rb +4 -1
- data/test/test_source_comment.rb +2 -2
- data/test/test_source_comment_associator.rb +47 -15
- data/test/test_source_map.rb +1 -2
- data/test/test_source_range.rb +29 -9
- data/test/test_source_rewriter.rb +4 -4
- data/test/test_source_rewriter_action.rb +2 -2
- data/test/test_source_tree_rewriter.rb +201 -13
- metadata +19 -12
data/lib/parser/rubymotion.y
CHANGED
@@ -1018,11 +1018,15 @@ rule
|
|
1018
1018
|
result = @builder.block(val[0],
|
1019
1019
|
begin_t, args, body, end_t)
|
1020
1020
|
}
|
1021
|
-
| tLAMBDA
|
1021
|
+
| tLAMBDA
|
1022
|
+
{
|
1023
|
+
@context.push(:lambda)
|
1024
|
+
}
|
1025
|
+
lambda
|
1022
1026
|
{
|
1023
1027
|
lambda_call = @builder.call_lambda(val[0])
|
1024
1028
|
|
1025
|
-
args, (begin_t, body, end_t) = val[
|
1029
|
+
args, (begin_t, body, end_t) = val[2]
|
1026
1030
|
result = @builder.block(lambda_call,
|
1027
1031
|
begin_t, args, body, end_t)
|
1028
1032
|
}
|
@@ -1136,6 +1140,7 @@ rule
|
|
1136
1140
|
{
|
1137
1141
|
@static_env.extend_static
|
1138
1142
|
@lexer.push_cmdarg
|
1143
|
+
@context.push(:module)
|
1139
1144
|
}
|
1140
1145
|
bodystmt kEND
|
1141
1146
|
{
|
@@ -1148,6 +1153,7 @@ rule
|
|
1148
1153
|
|
1149
1154
|
@lexer.pop_cmdarg
|
1150
1155
|
@static_env.unextend
|
1156
|
+
@context.pop
|
1151
1157
|
}
|
1152
1158
|
| kDEF fname
|
1153
1159
|
{
|
@@ -1429,9 +1435,13 @@ rule
|
|
1429
1435
|
lambda: {
|
1430
1436
|
@static_env.extend_dynamic
|
1431
1437
|
}
|
1432
|
-
f_larglist
|
1438
|
+
f_larglist
|
1439
|
+
{
|
1440
|
+
@context.pop
|
1441
|
+
}
|
1442
|
+
lambda_body
|
1433
1443
|
{
|
1434
|
-
result = [ val[1], val[
|
1444
|
+
result = [ val[1], val[3] ]
|
1435
1445
|
|
1436
1446
|
@static_env.unextend
|
1437
1447
|
}
|
data/lib/parser/runner.rb
CHANGED
@@ -14,9 +14,8 @@ module Parser
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def initialize
|
17
|
-
Parser::Builders::Default.modernize
|
18
|
-
|
19
17
|
@option_parser = OptionParser.new { |opts| setup_option_parsing(opts) }
|
18
|
+
@legacy = {}
|
20
19
|
@parser_class = nil
|
21
20
|
@parser = nil
|
22
21
|
@files = []
|
@@ -30,6 +29,7 @@ module Parser
|
|
30
29
|
|
31
30
|
def execute(options)
|
32
31
|
parse_options(options)
|
32
|
+
setup_builder_default
|
33
33
|
prepare_parser
|
34
34
|
|
35
35
|
process_all_input
|
@@ -37,6 +37,8 @@ module Parser
|
|
37
37
|
|
38
38
|
private
|
39
39
|
|
40
|
+
LEGACY_MODES = %i[lambda procarg0 encoding index arg_inside_procarg0 forward_arg].freeze
|
41
|
+
|
40
42
|
def runner_name
|
41
43
|
raise NotImplementedError, "implement #{self.class}##{__callee__}"
|
42
44
|
end
|
@@ -111,6 +113,11 @@ module Parser
|
|
111
113
|
@parser_class = Parser::Ruby27
|
112
114
|
end
|
113
115
|
|
116
|
+
opts.on '--28', 'Parse as Ruby 2.8 would' do
|
117
|
+
require 'parser/ruby28'
|
118
|
+
@parser_class = Parser::Ruby28
|
119
|
+
end
|
120
|
+
|
114
121
|
opts.on '--mac', 'Parse as MacRuby 0.12 would' do
|
115
122
|
require 'parser/macruby'
|
116
123
|
@parser_class = Parser::MacRuby
|
@@ -121,6 +128,17 @@ module Parser
|
|
121
128
|
@parser_class = Parser::RubyMotion
|
122
129
|
end
|
123
130
|
|
131
|
+
opts.on '--legacy', "Parse with all legacy modes" do
|
132
|
+
@legacy = Hash.new(true)
|
133
|
+
end
|
134
|
+
|
135
|
+
LEGACY_MODES.each do |mode|
|
136
|
+
opt_name = "--legacy-#{mode.to_s.gsub('_', '-')}"
|
137
|
+
opts.on opt_name, "Parse with legacy mode for emit_#{mode}" do
|
138
|
+
@legacy[mode] = true
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
124
142
|
opts.on '-w', '--warnings', 'Enable warnings' do |w|
|
125
143
|
@warnings = w
|
126
144
|
end
|
@@ -159,6 +177,12 @@ module Parser
|
|
159
177
|
end
|
160
178
|
end
|
161
179
|
|
180
|
+
def setup_builder_default
|
181
|
+
LEGACY_MODES.each do |mode|
|
182
|
+
Parser::Builders::Default.send(:"emit_#{mode}=", !@legacy[mode])
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
162
186
|
def prepare_parser
|
163
187
|
@parser = @parser_class.new
|
164
188
|
|
@@ -55,8 +55,8 @@ module Parser
|
|
55
55
|
new_source = rewriter.rewrite(buffer, ast)
|
56
56
|
|
57
57
|
new_buffer = Source::Buffer.new(initial_buffer.name +
|
58
|
-
'|after ' + rewriter_class.name
|
59
|
-
|
58
|
+
'|after ' + rewriter_class.name,
|
59
|
+
source: new_source)
|
60
60
|
|
61
61
|
@parser.reset
|
62
62
|
new_ast = @parser.parse(new_buffer)
|
data/lib/parser/source/buffer.rb
CHANGED
@@ -102,7 +102,7 @@ module Parser
|
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
-
def initialize(name, first_line = 1)
|
105
|
+
def initialize(name, first_line = 1, source: nil)
|
106
106
|
@name = name.to_s
|
107
107
|
@source = nil
|
108
108
|
@first_line = first_line
|
@@ -116,6 +116,8 @@ module Parser
|
|
116
116
|
# Cache for fast lookup
|
117
117
|
@line_for_position = {}
|
118
118
|
@column_for_position = {}
|
119
|
+
|
120
|
+
self.source = source if source
|
119
121
|
end
|
120
122
|
|
121
123
|
##
|
@@ -107,6 +107,19 @@ module Parser
|
|
107
107
|
|
108
108
|
private
|
109
109
|
|
110
|
+
POSTFIX_TYPES = Set[:if, :while, :while_post, :until, :until_post].freeze
|
111
|
+
def children_in_source_order(node)
|
112
|
+
if POSTFIX_TYPES.include?(node.type)
|
113
|
+
# All these types have either nodes with expressions, or `nil`
|
114
|
+
# so a compact will do, but they need to be sorted.
|
115
|
+
node.children.compact.sort_by { |child| child.loc.expression.begin_pos }
|
116
|
+
else
|
117
|
+
node.children.select do |child|
|
118
|
+
child.is_a?(AST::Node) && child.loc && child.loc.expression
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
110
123
|
def do_associate
|
111
124
|
@mapping = Hash.new { |h, k| h[k] = [] }
|
112
125
|
@comment_num = -1
|
@@ -131,10 +144,7 @@ module Parser
|
|
131
144
|
node_loc = node.location
|
132
145
|
if @current_comment.location.line <= node_loc.last_line ||
|
133
146
|
node_loc.is_a?(Map::Heredoc)
|
134
|
-
node.
|
135
|
-
next unless child.is_a?(AST::Node) && child.loc && child.loc.expression
|
136
|
-
visit(child)
|
137
|
-
end
|
147
|
+
children_in_source_order(node).each { |child| visit(child) }
|
138
148
|
|
139
149
|
process_trailing_comments(node)
|
140
150
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Parser
|
4
|
+
module Source
|
5
|
+
|
6
|
+
class Map::EndlessDefinition < Map
|
7
|
+
attr_reader :keyword
|
8
|
+
attr_reader :operator
|
9
|
+
attr_reader :name
|
10
|
+
attr_reader :assignment
|
11
|
+
|
12
|
+
def initialize(keyword_l, operator_l, name_l, assignment_l, body_l)
|
13
|
+
@keyword = keyword_l
|
14
|
+
@operator = operator_l
|
15
|
+
@name = name_l
|
16
|
+
@assignment = assignment_l
|
17
|
+
|
18
|
+
super(@keyword.join(body_l))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
data/lib/parser/source/range.rb
CHANGED
@@ -13,7 +13,7 @@ module Parser
|
|
13
13
|
# ^^
|
14
14
|
#
|
15
15
|
# @!attribute [r] source_buffer
|
16
|
-
# @return [Parser::
|
16
|
+
# @return [Parser::Source::Buffer]
|
17
17
|
#
|
18
18
|
# @!attribute [r] begin_pos
|
19
19
|
# @return [Integer] index of the first character in the range
|
@@ -149,6 +149,13 @@ module Parser
|
|
149
149
|
(@begin_pos...@end_pos).to_a
|
150
150
|
end
|
151
151
|
|
152
|
+
##
|
153
|
+
# @return [Range] a Ruby range with the same `begin_pos` and `end_pos`
|
154
|
+
#
|
155
|
+
def to_range
|
156
|
+
self.begin_pos...self.end_pos
|
157
|
+
end
|
158
|
+
|
152
159
|
##
|
153
160
|
# Composes a GNU/Clang-style string representation of the beginning of this
|
154
161
|
# range.
|
@@ -298,6 +305,15 @@ module Parser
|
|
298
305
|
(@end_pos <=> other.end_pos)
|
299
306
|
end
|
300
307
|
|
308
|
+
alias_method :eql?, :==
|
309
|
+
|
310
|
+
##
|
311
|
+
# Support for Ranges be used in as Hash indices and in Sets.
|
312
|
+
#
|
313
|
+
def hash
|
314
|
+
[@source_buffer, @begin_pos, @end_pos].hash
|
315
|
+
end
|
316
|
+
|
301
317
|
##
|
302
318
|
# @return [String] a human-readable representation of this range.
|
303
319
|
#
|
@@ -117,6 +117,71 @@ module Parser
|
|
117
117
|
@action_root = TreeRewriter::Action.new(all_encompassing_range, @enforcer)
|
118
118
|
end
|
119
119
|
|
120
|
+
##
|
121
|
+
# Returns true iff no (non trivial) update has been recorded
|
122
|
+
#
|
123
|
+
# @return [Boolean]
|
124
|
+
#
|
125
|
+
def empty?
|
126
|
+
@action_root.empty?
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Merges the updates of argument with the receiver.
|
131
|
+
# Policies of the receiver are used.
|
132
|
+
# This action is atomic in that it won't change the receiver
|
133
|
+
# unless it succeeds.
|
134
|
+
#
|
135
|
+
# @param [Rewriter] with
|
136
|
+
# @return [Rewriter] self
|
137
|
+
# @raise [ClobberingError] when clobbering is detected
|
138
|
+
#
|
139
|
+
def merge!(with)
|
140
|
+
raise 'TreeRewriter are not for the same source_buffer' unless
|
141
|
+
source_buffer == with.source_buffer
|
142
|
+
|
143
|
+
@action_root = @action_root.combine(with.action_root)
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Returns a new rewriter that consists of the updates of the received
|
149
|
+
# and the given argument. Policies of the receiver are used.
|
150
|
+
#
|
151
|
+
# @param [Rewriter] with
|
152
|
+
# @return [Rewriter] merge of receiver and argument
|
153
|
+
# @raise [ClobberingError] when clobbering is detected
|
154
|
+
#
|
155
|
+
def merge(with)
|
156
|
+
dup.merge!(with)
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# For special cases where one needs to merge a rewriter attached to a different source_buffer
|
161
|
+
# or that needs to be offset. Policies of the receiver are used.
|
162
|
+
#
|
163
|
+
# @param [TreeRewriter] rewriter from different source_buffer
|
164
|
+
# @param [Integer] offset
|
165
|
+
# @return [Rewriter] self
|
166
|
+
# @raise [IndexError] if action ranges (once offset) don't fit the current buffer
|
167
|
+
#
|
168
|
+
def import!(foreign_rewriter, offset: 0)
|
169
|
+
return self if foreign_rewriter.empty?
|
170
|
+
|
171
|
+
contracted = foreign_rewriter.action_root.contract
|
172
|
+
merge_effective_range = ::Parser::Source::Range.new(
|
173
|
+
@source_buffer,
|
174
|
+
contracted.range.begin_pos + offset,
|
175
|
+
contracted.range.end_pos + offset,
|
176
|
+
)
|
177
|
+
check_range_validity(merge_effective_range)
|
178
|
+
|
179
|
+
merge_with = contracted.moved(@source_buffer, offset)
|
180
|
+
|
181
|
+
@action_root = @action_root.combine(merge_with)
|
182
|
+
self
|
183
|
+
end
|
184
|
+
|
120
185
|
##
|
121
186
|
# Replaces the code of the source range `range` with `content`.
|
122
187
|
#
|
@@ -185,28 +250,62 @@ module Parser
|
|
185
250
|
# @return [String]
|
186
251
|
#
|
187
252
|
def process
|
188
|
-
source = @source_buffer.source
|
189
|
-
adjustment = 0
|
253
|
+
source = @source_buffer.source
|
190
254
|
|
255
|
+
chunks = []
|
256
|
+
last_end = 0
|
191
257
|
@action_root.ordered_replacements.each do |range, replacement|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
source[begin_pos...end_pos] = replacement
|
196
|
-
|
197
|
-
adjustment += replacement.length - range.length
|
258
|
+
chunks << source[last_end...range.begin_pos] << replacement
|
259
|
+
last_end = range.end_pos
|
198
260
|
end
|
261
|
+
chunks << source[last_end...source.length]
|
262
|
+
chunks.join
|
263
|
+
end
|
199
264
|
|
200
|
-
|
265
|
+
##
|
266
|
+
# Returns a representation of the rewriter as an ordered list of replacements.
|
267
|
+
#
|
268
|
+
# rewriter.as_replacements # => [ [1...1, '('],
|
269
|
+
# [2...4, 'foo'],
|
270
|
+
# [5...6, ''],
|
271
|
+
# [6...6, '!'],
|
272
|
+
# [10...10, ')'],
|
273
|
+
# ]
|
274
|
+
#
|
275
|
+
# This representation is sufficient to recreate the result of `process` but it is
|
276
|
+
# not sufficient to recreate completely the rewriter for further merging/actions.
|
277
|
+
# See `as_nested_actions`
|
278
|
+
#
|
279
|
+
# @return [Array<Range, String>] an ordered list of pairs of range & replacement
|
280
|
+
#
|
281
|
+
def as_replacements
|
282
|
+
@action_root.ordered_replacements
|
283
|
+
end
|
284
|
+
|
285
|
+
##
|
286
|
+
# Returns a representation of the rewriter as nested insertions (:wrap) and replacements.
|
287
|
+
#
|
288
|
+
# rewriter.as_actions # =>[ [:wrap, 1...10, '(', ')'],
|
289
|
+
# [:wrap, 2...6, '', '!'], # aka "insert_after"
|
290
|
+
# [:replace, 2...4, 'foo'],
|
291
|
+
# [:replace, 5...6, ''], # aka "removal"
|
292
|
+
# ],
|
293
|
+
#
|
294
|
+
# Contrary to `as_replacements`, this representation is sufficient to recreate exactly
|
295
|
+
# the rewriter.
|
296
|
+
#
|
297
|
+
# @return [Array<(Symbol, Range, String{, String})>]
|
298
|
+
#
|
299
|
+
def as_nested_actions
|
300
|
+
@action_root.nested_actions
|
201
301
|
end
|
202
302
|
|
203
303
|
##
|
204
304
|
# Provides a protected block where a sequence of multiple rewrite actions
|
205
305
|
# are handled atomically. If any of the actions failed by clobbering,
|
206
|
-
# all the actions are rolled back.
|
306
|
+
# all the actions are rolled back. Transactions can be nested.
|
207
307
|
#
|
208
308
|
# @raise [RuntimeError] when no block is passed
|
209
|
-
# @raise [RuntimeError] when already in a transaction
|
210
309
|
#
|
211
310
|
def transaction
|
212
311
|
unless block_given?
|
@@ -256,6 +355,10 @@ module Parser
|
|
256
355
|
|
257
356
|
extend Deprecation
|
258
357
|
|
358
|
+
protected
|
359
|
+
|
360
|
+
attr_reader :action_root
|
361
|
+
|
259
362
|
private
|
260
363
|
|
261
364
|
ACTIONS = %i[accept warn raise].freeze
|
@@ -273,7 +376,7 @@ module Parser
|
|
273
376
|
|
274
377
|
def check_range_validity(range)
|
275
378
|
if range.begin_pos < 0 || range.end_pos > @source_buffer.source.size
|
276
|
-
raise IndexError, "The range #{range} is outside the bounds of the source"
|
379
|
+
raise IndexError, "The range #{range.to_range} is outside the bounds of the source"
|
277
380
|
end
|
278
381
|
range
|
279
382
|
end
|
@@ -7,7 +7,7 @@ module Parser
|
|
7
7
|
#
|
8
8
|
# Actions are arranged in a tree and get combined so that:
|
9
9
|
# children are strictly contained by their parent
|
10
|
-
# sibblings all disjoint from one another
|
10
|
+
# sibblings all disjoint from one another and ordered
|
11
11
|
# only actions with replacement==nil may have children
|
12
12
|
#
|
13
13
|
class TreeRewriter::Action
|
@@ -25,30 +25,77 @@ module Parser
|
|
25
25
|
freeze
|
26
26
|
end
|
27
27
|
|
28
|
-
# Assumes action.children.empty?
|
29
28
|
def combine(action)
|
30
|
-
return self
|
29
|
+
return self if action.empty? # Ignore empty action
|
31
30
|
do_combine(action)
|
32
31
|
end
|
33
32
|
|
33
|
+
def empty?
|
34
|
+
@insert_before.empty? &&
|
35
|
+
@insert_after.empty? &&
|
36
|
+
@children.empty? &&
|
37
|
+
(@replacement == nil || (@replacement.empty? && @range.empty?))
|
38
|
+
end
|
39
|
+
|
34
40
|
def ordered_replacements
|
35
41
|
reps = []
|
36
42
|
reps << [@range.begin, @insert_before] unless @insert_before.empty?
|
37
43
|
reps << [@range, @replacement] if @replacement
|
38
|
-
reps.concat(@children.
|
44
|
+
reps.concat(@children.flat_map(&:ordered_replacements))
|
39
45
|
reps << [@range.end, @insert_after] unless @insert_after.empty?
|
40
46
|
reps
|
41
47
|
end
|
42
48
|
|
49
|
+
def nested_actions
|
50
|
+
actions = []
|
51
|
+
actions << [:wrap, @range, @insert_before, @insert_after] if !@insert_before.empty? ||
|
52
|
+
!@insert_after.empty?
|
53
|
+
actions << [:replace, @range, @replacement] if @replacement
|
54
|
+
actions.concat(@children.flat_map(&:nested_actions))
|
55
|
+
end
|
56
|
+
|
43
57
|
def insertion?
|
44
58
|
!insert_before.empty? || !insert_after.empty? || (replacement && !replacement.empty?)
|
45
59
|
end
|
46
60
|
|
61
|
+
##
|
62
|
+
# A root action has its range set to the whole source range, even
|
63
|
+
# though it typically do not act on that range.
|
64
|
+
# This method returns the action as if it was a child action with
|
65
|
+
# its range contracted.
|
66
|
+
# @return [Action]
|
67
|
+
def contract
|
68
|
+
raise 'Empty actions can not be contracted' if empty?
|
69
|
+
return self if insertion?
|
70
|
+
range = @range.with(
|
71
|
+
begin_pos: children.first.range.begin_pos,
|
72
|
+
end_pos: children.last.range.end_pos,
|
73
|
+
)
|
74
|
+
with(range: range)
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# @return [Action] that has been moved to the given source_buffer and with the given offset
|
79
|
+
# No check is done on validity of resulting range.
|
80
|
+
def moved(source_buffer, offset)
|
81
|
+
moved_range = ::Parser::Source::Range.new(
|
82
|
+
source_buffer,
|
83
|
+
@range.begin_pos + offset,
|
84
|
+
@range.end_pos + offset
|
85
|
+
)
|
86
|
+
with(
|
87
|
+
range: moved_range,
|
88
|
+
children: children.map { |child| child.moved(source_buffer, offset) }
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
47
92
|
protected
|
48
93
|
|
49
|
-
|
94
|
+
attr_reader :children
|
95
|
+
|
96
|
+
def with(range: @range, enforcer: @enforcer, children: @children, insert_before: @insert_before, replacement: @replacement, insert_after: @insert_after)
|
50
97
|
children = swallow(children) if replacement
|
51
|
-
self.class.new(range,
|
98
|
+
self.class.new(range, enforcer, children: children, insert_before: insert_before, replacement: replacement, insert_after: insert_after)
|
52
99
|
end
|
53
100
|
|
54
101
|
# Assumes range.contains?(action.range) && action.children.empty?
|
@@ -61,19 +108,27 @@ module Parser
|
|
61
108
|
end
|
62
109
|
|
63
110
|
def place_in_hierarchy(action)
|
64
|
-
family =
|
111
|
+
family = analyse_hierarchy(action)
|
65
112
|
|
66
113
|
if family[:fusible]
|
67
|
-
fuse_deletions(action, family[:fusible], [*family[:
|
114
|
+
fuse_deletions(action, family[:fusible], [*family[:sibbling_left], *family[:child], *family[:sibbling_right]])
|
68
115
|
else
|
69
116
|
extra_sibbling = if family[:parent] # action should be a descendant of one of the children
|
70
|
-
family[:parent]
|
117
|
+
family[:parent].do_combine(action)
|
71
118
|
elsif family[:child] # or it should become the parent of some of the children,
|
72
|
-
action.with(children: family[:child])
|
119
|
+
action.with(children: family[:child], enforcer: @enforcer)
|
120
|
+
.combine_children(action.children)
|
73
121
|
else # or else it should become an additional child
|
74
122
|
action
|
75
123
|
end
|
76
|
-
with(children: [*family[:
|
124
|
+
with(children: [*family[:sibbling_left], extra_sibbling, *family[:sibbling_right]])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Assumes `more_children` all contained within `@range`
|
129
|
+
def combine_children(more_children)
|
130
|
+
more_children.inject(self) do |parent, new_child|
|
131
|
+
parent.place_in_hierarchy(new_child)
|
77
132
|
end
|
78
133
|
end
|
79
134
|
|
@@ -84,22 +139,76 @@ module Parser
|
|
84
139
|
without_fusible.do_combine(fused_deletion)
|
85
140
|
end
|
86
141
|
|
87
|
-
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
142
|
+
# Similar to @children.bsearch_index || size
|
143
|
+
# except allows for a starting point
|
144
|
+
# and `bsearch_index` is only Ruby 2.3+
|
145
|
+
def bsearch_child_index(from = 0)
|
146
|
+
size = @children.size
|
147
|
+
(from...size).bsearch { |i| yield @children[i] } || size
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns the children in a hierarchy with respect to `action`:
|
151
|
+
# :sibbling_left, sibbling_right (for those that are disjoint from `action`)
|
152
|
+
# :parent (in case one of our children contains `action`)
|
153
|
+
# :child (in case `action` strictly contains some of our children)
|
154
|
+
# :fusible (in case `action` overlaps some children but they can be fused in one deletion)
|
155
|
+
# or raises a `CloberingError`
|
156
|
+
# In case a child has equal range to `action`, it is returned as `:parent`
|
157
|
+
# Reminder: an empty range 1...1 is considered disjoint from 1...10
|
158
|
+
def analyse_hierarchy(action)
|
159
|
+
r = action.range
|
160
|
+
# left_index is the index of the first child that isn't completely to the left of action
|
161
|
+
left_index = bsearch_child_index { |child| child.range.end_pos > r.begin_pos }
|
162
|
+
# right_index is the index of the first child that is completely on the right of action
|
163
|
+
start = left_index == 0 ? 0 : left_index - 1 # See "corner case" below for reason of -1
|
164
|
+
right_index = bsearch_child_index(start) { |child| child.range.begin_pos >= r.end_pos }
|
165
|
+
center = right_index - left_index
|
166
|
+
case center
|
167
|
+
when 0
|
168
|
+
# All children are disjoint from action, nothing else to do
|
169
|
+
when -1
|
170
|
+
# Corner case: if a child has empty range == action's range
|
171
|
+
# then it will appear to be both disjoint and to the left of action,
|
172
|
+
# as well as disjoint and to the right of action.
|
173
|
+
# Since ranges are equal, we return it as parent
|
174
|
+
left_index -= 1 # Fix indices, as otherwise this child would be
|
175
|
+
right_index += 1 # considered as a sibbling (both left and right!)
|
176
|
+
parent = @children[left_index]
|
100
177
|
else
|
101
|
-
@
|
178
|
+
overlap_left = @children[left_index].range.begin_pos <=> r.begin_pos
|
179
|
+
overlap_right = @children[right_index-1].range.end_pos <=> r.end_pos
|
180
|
+
|
181
|
+
# For one child to be the parent of action, we must have:
|
182
|
+
if center == 1 && overlap_left <= 0 && overlap_right >= 0
|
183
|
+
parent = @children[left_index]
|
184
|
+
else
|
185
|
+
# Otherwise consider all non disjoint elements (center) to be contained...
|
186
|
+
contained = @children[left_index...right_index]
|
187
|
+
fusible = check_fusible(action,
|
188
|
+
(contained.shift if overlap_left < 0), # ... but check first and last one
|
189
|
+
(contained.pop if overlap_right > 0) # ... for overlaps
|
190
|
+
)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
{
|
195
|
+
parent: parent,
|
196
|
+
sibbling_left: @children[0...left_index],
|
197
|
+
sibbling_right: @children[right_index...@children.size],
|
198
|
+
fusible: fusible,
|
199
|
+
child: contained,
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
# @param [Array(Action | nil)] fusible
|
204
|
+
def check_fusible(action, *fusible)
|
205
|
+
fusible.compact!
|
206
|
+
return if fusible.empty?
|
207
|
+
fusible.each do |child|
|
208
|
+
kind = action.insertion? || child.insertion? ? :crossing_insertions : :crossing_deletions
|
209
|
+
@enforcer.call(kind) { {range: action.range, conflict: child.range} }
|
102
210
|
end
|
211
|
+
fusible
|
103
212
|
end
|
104
213
|
|
105
214
|
# Assumes action.range == range && action.children.empty?
|
@@ -109,7 +218,7 @@ module Parser
|
|
109
218
|
insert_before: "#{action.insert_before}#{insert_before}",
|
110
219
|
replacement: action.replacement || @replacement,
|
111
220
|
insert_after: "#{insert_after}#{action.insert_after}",
|
112
|
-
)
|
221
|
+
).combine_children(action.children)
|
113
222
|
end
|
114
223
|
|
115
224
|
def call_enforcer_for_merge(action)
|