parser 2.3.0.2 → 2.3.0.3
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 +4 -4
- data/CHANGELOG.md +7 -1
- data/doc/AST_FORMAT.md +1 -1
- data/lib/parser/lexer.rl +82 -96
- data/lib/parser/lexer/literal.rb +5 -5
- data/lib/parser/source/buffer.rb +39 -12
- data/lib/parser/source/comment/associator.rb +17 -11
- data/lib/parser/source/range.rb +22 -12
- data/lib/parser/source/rewriter.rb +214 -59
- data/lib/parser/version.rb +1 -1
- data/test/parse_helper.rb +36 -26
- data/test/test_lexer.rb +1181 -1147
- data/test/test_parser.rb +12 -0
- data/test/test_source_comment_associator.rb +17 -0
- data/test/test_source_range.rb +20 -0
- data/test/test_source_rewriter.rb +269 -10
- metadata +2 -2
data/lib/parser/lexer/literal.rb
CHANGED
@@ -78,7 +78,7 @@ module Parser
|
|
78
78
|
!heredoc?)
|
79
79
|
|
80
80
|
# Capture opening delimiter in percent-literals.
|
81
|
-
@str_type << delimiter if @str_type.start_with?('%')
|
81
|
+
@str_type << delimiter if @str_type.start_with?('%'.freeze)
|
82
82
|
|
83
83
|
clear_buffer
|
84
84
|
|
@@ -103,7 +103,7 @@ module Parser
|
|
103
103
|
end
|
104
104
|
|
105
105
|
def backslash_delimited?
|
106
|
-
@end_delim == '\\'
|
106
|
+
@end_delim == '\\'.freeze
|
107
107
|
end
|
108
108
|
|
109
109
|
def type
|
@@ -116,7 +116,7 @@ module Parser
|
|
116
116
|
if words? && character =~ /[ \t\v\r\f\n]/
|
117
117
|
true
|
118
118
|
else
|
119
|
-
['\\', @start_delim, @end_delim].include?(character)
|
119
|
+
['\\'.freeze, @start_delim, @end_delim].include?(character)
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
@@ -135,8 +135,8 @@ module Parser
|
|
135
135
|
extend_space(ts, ts)
|
136
136
|
end
|
137
137
|
|
138
|
-
if lookahead &&
|
139
|
-
|
138
|
+
if lookahead && @label_allowed && lookahead[0] == ?: &&
|
139
|
+
lookahead[1] != ?: && @start_tok == :tSTRING_BEG
|
140
140
|
# This is a quoted label.
|
141
141
|
flush_string
|
142
142
|
emit(:tLABEL_END, @end_delim, ts, te + 1)
|
data/lib/parser/source/buffer.rb
CHANGED
@@ -40,8 +40,6 @@ module Parser
|
|
40
40
|
)
|
41
41
|
/x
|
42
42
|
|
43
|
-
NEW_LINE = "\n".freeze
|
44
|
-
|
45
43
|
##
|
46
44
|
# Try to recognize encoding of `string` as Ruby would, i.e. by looking for
|
47
45
|
# magic encoding comment or UTF-8 BOM. `string` can be in any encoding.
|
@@ -58,7 +56,7 @@ module Parser
|
|
58
56
|
|
59
57
|
if first_line =~ /\A\xef\xbb\xbf/ # BOM
|
60
58
|
return Encoding::UTF_8
|
61
|
-
elsif first_line[0, 2] == '#!'
|
59
|
+
elsif first_line[0, 2] == '#!'.freeze
|
62
60
|
encoding_line = second_line
|
63
61
|
else
|
64
62
|
encoding_line = first_line
|
@@ -108,6 +106,10 @@ module Parser
|
|
108
106
|
|
109
107
|
@lines = nil
|
110
108
|
@line_begins = nil
|
109
|
+
|
110
|
+
# Cache for fast lookup
|
111
|
+
@line_for_position = {}
|
112
|
+
@col_for_position = {}
|
111
113
|
end
|
112
114
|
|
113
115
|
##
|
@@ -175,7 +177,7 @@ module Parser
|
|
175
177
|
raise ArgumentError, 'Source::Buffer is immutable'
|
176
178
|
end
|
177
179
|
|
178
|
-
@source = input.gsub("\r\n",
|
180
|
+
@source = input.gsub("\r\n".freeze, "\n".freeze).freeze
|
179
181
|
end
|
180
182
|
|
181
183
|
##
|
@@ -190,6 +192,34 @@ module Parser
|
|
190
192
|
[ @first_line + line_no, position - line_begin ]
|
191
193
|
end
|
192
194
|
|
195
|
+
##
|
196
|
+
# Convert a character index into the source to a line number.
|
197
|
+
#
|
198
|
+
# @param [Integer] position
|
199
|
+
# @return [Integer] line
|
200
|
+
# @api private
|
201
|
+
#
|
202
|
+
def line_for_position(position)
|
203
|
+
@line_for_position[position] ||= begin
|
204
|
+
line_no, _ = line_for(position)
|
205
|
+
@first_line + line_no
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Convert a character index into the source to a column number.
|
211
|
+
#
|
212
|
+
# @param [Integer] position
|
213
|
+
# @return [Integer] column
|
214
|
+
# @api private
|
215
|
+
#
|
216
|
+
def column_for_position(position)
|
217
|
+
@col_for_position[position] ||= begin
|
218
|
+
_, line_begin = line_for(position)
|
219
|
+
position - line_begin
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
193
223
|
##
|
194
224
|
# Return an `Array` of source code lines.
|
195
225
|
#
|
@@ -198,10 +228,10 @@ module Parser
|
|
198
228
|
def source_lines
|
199
229
|
@lines ||= begin
|
200
230
|
lines = @source.lines.to_a
|
201
|
-
lines << '' if @source.end_with?("\n")
|
231
|
+
lines << '' if @source.end_with?("\n".freeze)
|
202
232
|
|
203
233
|
lines.each do |line|
|
204
|
-
line.chomp!(
|
234
|
+
line.chomp!("\n".freeze)
|
205
235
|
line.freeze
|
206
236
|
end
|
207
237
|
|
@@ -253,14 +283,11 @@ module Parser
|
|
253
283
|
|
254
284
|
def line_begins
|
255
285
|
unless @line_begins
|
256
|
-
@line_begins, index = [ [ 0, 0 ] ],
|
257
|
-
|
258
|
-
@source.each_char do |char|
|
259
|
-
if char == NEW_LINE
|
260
|
-
@line_begins.unshift [ @line_begins.length, index ]
|
261
|
-
end
|
286
|
+
@line_begins, index = [ [ 0, 0 ] ], 0
|
262
287
|
|
288
|
+
while index = @source.index("\n".freeze, index)
|
263
289
|
index += 1
|
290
|
+
@line_begins.unshift [ @line_begins.length, index ]
|
264
291
|
end
|
265
292
|
end
|
266
293
|
|
@@ -120,12 +120,22 @@ module Parser
|
|
120
120
|
def visit(node)
|
121
121
|
process_leading_comments(node)
|
122
122
|
|
123
|
-
|
124
|
-
|
125
|
-
|
123
|
+
return unless @current_comment
|
124
|
+
|
125
|
+
# If the next comment is beyond the last line of this node, we don't
|
126
|
+
# need to iterate over its subnodes
|
127
|
+
# (Unless this node is a heredoc... there could be a comment in its body,
|
128
|
+
# inside an interpolation)
|
129
|
+
node_loc = node.location
|
130
|
+
if @current_comment.location.line <= node_loc.last_line ||
|
131
|
+
node_loc.is_a?(Map::Heredoc)
|
132
|
+
node.children.each do |child|
|
133
|
+
next unless child.is_a?(AST::Node) && child.loc && child.loc.expression
|
134
|
+
visit(child)
|
135
|
+
end
|
136
|
+
|
137
|
+
process_trailing_comments(node)
|
126
138
|
end
|
127
|
-
|
128
|
-
process_trailing_comments(node)
|
129
139
|
end
|
130
140
|
|
131
141
|
def process_leading_comments(node)
|
@@ -152,12 +162,8 @@ module Parser
|
|
152
162
|
def current_comment_before?(node)
|
153
163
|
return false if !@current_comment
|
154
164
|
comment_loc = @current_comment.location.expression
|
155
|
-
|
156
|
-
|
157
|
-
node_loc = node.location.expression
|
158
|
-
return false if comment_loc.end_pos > node_loc.begin_pos
|
159
|
-
end
|
160
|
-
true
|
165
|
+
node_loc = node.location.expression
|
166
|
+
comment_loc.end_pos <= node_loc.begin_pos
|
161
167
|
end
|
162
168
|
|
163
169
|
def current_comment_before_end?(node)
|
data/lib/parser/source/range.rb
CHANGED
@@ -34,6 +34,9 @@ module Parser
|
|
34
34
|
if end_pos < begin_pos
|
35
35
|
raise ArgumentError, 'Parser::Source::Range: end_pos must not be less than begin_pos'
|
36
36
|
end
|
37
|
+
if source_buffer.nil?
|
38
|
+
raise ArgumentError, 'Parser::Source::Range: source_buffer must not be nil'
|
39
|
+
end
|
37
40
|
|
38
41
|
@source_buffer = source_buffer
|
39
42
|
@begin_pos, @end_pos = begin_pos, end_pos
|
@@ -74,9 +77,7 @@ module Parser
|
|
74
77
|
# @return [Integer] line number of the beginning of this range.
|
75
78
|
#
|
76
79
|
def line
|
77
|
-
|
78
|
-
|
79
|
-
line
|
80
|
+
@source_buffer.line_for_position(@begin_pos)
|
80
81
|
end
|
81
82
|
|
82
83
|
alias_method :first_line, :line
|
@@ -85,27 +86,21 @@ module Parser
|
|
85
86
|
# @return [Integer] zero-based column number of the beginning of this range.
|
86
87
|
#
|
87
88
|
def column
|
88
|
-
|
89
|
-
|
90
|
-
column
|
89
|
+
@source_buffer.column_for_position(@begin_pos)
|
91
90
|
end
|
92
91
|
|
93
92
|
##
|
94
93
|
# @return [Integer] line number of the end of this range.
|
95
94
|
#
|
96
95
|
def last_line
|
97
|
-
|
98
|
-
|
99
|
-
line
|
96
|
+
@source_buffer.line_for_position(@end_pos)
|
100
97
|
end
|
101
98
|
|
102
99
|
##
|
103
100
|
# @return [Integer] zero-based column number of the end of this range.
|
104
101
|
#
|
105
102
|
def last_column
|
106
|
-
|
107
|
-
|
108
|
-
column
|
103
|
+
@source_buffer.column_for_position(@end_pos)
|
109
104
|
end
|
110
105
|
|
111
106
|
##
|
@@ -209,6 +204,21 @@ module Parser
|
|
209
204
|
@begin_pos >= other.end_pos || other.begin_pos >= @end_pos
|
210
205
|
end
|
211
206
|
|
207
|
+
##
|
208
|
+
# @param [Range] other
|
209
|
+
# @return [Boolean] `true` if this range and `other` overlap
|
210
|
+
#
|
211
|
+
def overlaps?(other)
|
212
|
+
@begin_pos < other.end_pos && other.begin_pos < @end_pos
|
213
|
+
end
|
214
|
+
|
215
|
+
##
|
216
|
+
# Checks if a range is empty; if it contains no characters
|
217
|
+
# @return [Boolean]
|
218
|
+
def empty?
|
219
|
+
@begin_pos == @end_pos
|
220
|
+
end
|
221
|
+
|
212
222
|
##
|
213
223
|
# Compares ranges.
|
214
224
|
# @return [Boolean]
|
@@ -5,7 +5,8 @@ module Parser
|
|
5
5
|
# {Rewriter} performs the heavy lifting in the source rewriting process.
|
6
6
|
# It schedules code updates to be performed in the correct order and
|
7
7
|
# verifies that no two updates _clobber_ each other, that is, attempt to
|
8
|
-
# modify the same
|
8
|
+
# modify the same section of code. (However, if two updates modify the
|
9
|
+
# same section in exactly the same way, they are merged.)
|
9
10
|
#
|
10
11
|
# If it is detected that one update clobbers another one, an `:error` and
|
11
12
|
# a `:note` diagnostics describing both updates are generated and passed to
|
@@ -37,6 +38,7 @@ module Parser
|
|
37
38
|
@source_buffer = source_buffer
|
38
39
|
@queue = []
|
39
40
|
@clobber = 0
|
41
|
+
@insertions = 0 # clobbered zero-length positions; index 0 is the far left
|
40
42
|
end
|
41
43
|
|
42
44
|
##
|
@@ -47,7 +49,7 @@ module Parser
|
|
47
49
|
# @raise [ClobberingError] when clobbering is detected
|
48
50
|
#
|
49
51
|
def remove(range)
|
50
|
-
append Rewriter::Action.new(range, '')
|
52
|
+
append Rewriter::Action.new(range, ''.freeze)
|
51
53
|
end
|
52
54
|
|
53
55
|
##
|
@@ -100,9 +102,7 @@ module Parser
|
|
100
102
|
adjustment = 0
|
101
103
|
source = @source_buffer.source.dup
|
102
104
|
|
103
|
-
sorted_queue = @queue.sort_by
|
104
|
-
[action.range.begin_pos, index]
|
105
|
-
end
|
105
|
+
sorted_queue = @queue.sort_by { |action| action.range.begin_pos }
|
106
106
|
|
107
107
|
sorted_queue.each do |action|
|
108
108
|
begin_pos = action.range.begin_pos + adjustment
|
@@ -118,7 +118,7 @@ module Parser
|
|
118
118
|
|
119
119
|
##
|
120
120
|
# Provides a protected block where a sequence of multiple rewrite actions
|
121
|
-
# are handled
|
121
|
+
# are handled atomically. If any of the actions failed by clobbering,
|
122
122
|
# all the actions are rolled back.
|
123
123
|
#
|
124
124
|
# @example
|
@@ -144,110 +144,249 @@ module Parser
|
|
144
144
|
|
145
145
|
@pending_queue = @queue.dup
|
146
146
|
@pending_clobber = @clobber
|
147
|
+
@pending_insertions = @insertions
|
147
148
|
|
148
149
|
yield
|
149
150
|
|
150
151
|
@queue = @pending_queue
|
151
152
|
@clobber = @pending_clobber
|
153
|
+
@insertions = @pending_insertions
|
152
154
|
|
153
155
|
self
|
154
156
|
ensure
|
155
157
|
@pending_queue = nil
|
156
158
|
@pending_clobber = nil
|
159
|
+
@pending_insertions = nil
|
157
160
|
end
|
158
161
|
|
159
162
|
private
|
160
163
|
|
164
|
+
# Schedule a code update. If it overlaps with another update, check
|
165
|
+
# whether they conflict, and raise a clobbering error if they do.
|
166
|
+
# (As a special case, zero-length ranges at the same position are
|
167
|
+
# considered to "overlap".) Otherwise, merge them.
|
168
|
+
#
|
169
|
+
# Updates which are adjacent to each other, but do not overlap, are also
|
170
|
+
# merged.
|
171
|
+
#
|
172
|
+
# RULES:
|
173
|
+
#
|
174
|
+
# - Insertion ("replacing" a zero-length range):
|
175
|
+
# - Two insertions at the same point conflict. This is true even
|
176
|
+
# if the earlier insertion has already been merged with an adjacent
|
177
|
+
# update, and even if they are both inserting the same text.
|
178
|
+
# - An insertion never conflicts with a replace or remove operation
|
179
|
+
# on its right or left side, which does not overlap it (in other
|
180
|
+
# words, which does not update BOTH its right and left sides).
|
181
|
+
# - An insertion always conflicts with a remove operation which spans
|
182
|
+
# both its sides.
|
183
|
+
# - An insertion conflicts with a replace operation which spans both its
|
184
|
+
# sides, unless the replacement text is longer than the replaced text
|
185
|
+
# by the size of the insertion (or more), and the portion of
|
186
|
+
# replacement text immediately after the insertion position is
|
187
|
+
# identical to the inserted text.
|
188
|
+
#
|
189
|
+
# - Removal operations never conflict with each other.
|
190
|
+
#
|
191
|
+
# - Replacement operations:
|
192
|
+
# - Take the portion of each replacement text which falls within:
|
193
|
+
# - The other operation's replaced region
|
194
|
+
# - The other operation's replacement text, if it extends past the
|
195
|
+
# end of its own replaced region (in other words, if the replacement
|
196
|
+
# text is longer than the text it replaces)
|
197
|
+
# - If and only if the taken texts are identical for both operations,
|
198
|
+
# they do not conflict.
|
199
|
+
#
|
161
200
|
def append(action)
|
162
|
-
|
163
|
-
|
201
|
+
range = action.range
|
202
|
+
|
203
|
+
# Is this an insertion?
|
204
|
+
if range.empty?
|
205
|
+
# Replacing nothing with... nothing?
|
206
|
+
return self if action.replacement.empty?
|
207
|
+
|
208
|
+
if (conflicting = clobbered_insertion?(range))
|
209
|
+
raise_clobber_error(action, [conflicting])
|
210
|
+
end
|
211
|
+
|
212
|
+
record_insertion(range)
|
213
|
+
|
214
|
+
if (adjacent = adjacent_updates?(range))
|
215
|
+
conflicting = adjacent.find do |a|
|
216
|
+
a.range.overlaps?(range) &&
|
217
|
+
!replace_compatible_with_insertion?(a, action)
|
218
|
+
end
|
219
|
+
raise_clobber_error(action, [conflicting]) if conflicting
|
220
|
+
|
221
|
+
merge_actions!(action, adjacent)
|
222
|
+
else
|
223
|
+
active_queue << action
|
224
|
+
end
|
164
225
|
else
|
165
|
-
|
166
|
-
|
226
|
+
# It's a replace or remove operation.
|
227
|
+
if (insertions = adjacent_insertions?(range))
|
228
|
+
insertions.each do |insertion|
|
229
|
+
if range.overlaps?(insertion.range) &&
|
230
|
+
!replace_compatible_with_insertion?(action, insertion)
|
231
|
+
raise_clobber_error(action, [insertion])
|
232
|
+
else
|
233
|
+
action = merge_actions(action, [insertion])
|
234
|
+
active_queue.delete(insertion)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
if (adjacent = adjacent_updates?(range))
|
240
|
+
if can_merge?(action, adjacent)
|
241
|
+
record_replace(range)
|
242
|
+
merge_actions!(action, adjacent)
|
243
|
+
else
|
244
|
+
raise_clobber_error(action, adjacent)
|
245
|
+
end
|
246
|
+
else
|
247
|
+
record_replace(range)
|
248
|
+
active_queue << action
|
249
|
+
end
|
167
250
|
end
|
168
251
|
|
169
252
|
self
|
170
253
|
end
|
171
254
|
|
172
|
-
def
|
173
|
-
self.
|
255
|
+
def record_insertion(range)
|
256
|
+
self.active_insertions = active_insertions | (1 << range.begin_pos)
|
257
|
+
end
|
258
|
+
|
259
|
+
def record_replace(range)
|
260
|
+
self.active_clobber = active_clobber | clobbered_position_mask(range)
|
261
|
+
end
|
262
|
+
|
263
|
+
def clobbered_position_mask(range)
|
264
|
+
((1 << range.size) - 1) << range.begin_pos
|
265
|
+
end
|
266
|
+
|
267
|
+
def adjacent_position_mask(range)
|
268
|
+
((1 << (range.size + 2)) - 1) << (range.begin_pos - 1)
|
269
|
+
end
|
270
|
+
|
271
|
+
def adjacent_insertion_mask(range)
|
272
|
+
((1 << (range.size + 1)) - 1) << range.begin_pos
|
174
273
|
end
|
175
274
|
|
176
|
-
def
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
275
|
+
def clobbered_insertion?(insertion)
|
276
|
+
insertion_pos = insertion.begin_pos
|
277
|
+
if active_insertions & (1 << insertion_pos) != 0
|
278
|
+
# The clobbered insertion may have already been merged with other
|
279
|
+
# updates, so it won't necessarily have the same begin_pos.
|
280
|
+
active_queue.find do |a|
|
281
|
+
a.range.begin_pos <= insertion_pos && insertion_pos <= a.range.end_pos
|
181
282
|
end
|
182
283
|
end
|
183
284
|
end
|
184
285
|
|
185
|
-
def
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
{ :action => existing[0] },
|
200
|
-
existing[0].range)
|
201
|
-
@diagnostics.process(diagnostic)
|
202
|
-
|
203
|
-
raise ClobberingError, "Parser::Source::Rewriter detected clobbering"
|
286
|
+
def adjacent_insertions?(range)
|
287
|
+
# Just retrieve insertions which have not been merged with an adjacent
|
288
|
+
# remove or replace.
|
289
|
+
if active_insertions & adjacent_insertion_mask(range) != 0
|
290
|
+
result = active_queue.select do |a|
|
291
|
+
a.range.empty? && adjacent?(range, a.range)
|
292
|
+
end
|
293
|
+
result.empty? ? nil : result
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def adjacent_updates?(range)
|
298
|
+
if active_clobber & adjacent_position_mask(range) != 0
|
299
|
+
active_queue.select { |a| adjacent?(range, a.range) }
|
204
300
|
end
|
205
301
|
end
|
206
302
|
|
303
|
+
def replace_compatible_with_insertion?(replace, insertion)
|
304
|
+
(replace.replacement.length - replace.range.size) >= insertion.range.size &&
|
305
|
+
(offset = insertion.range.begin_pos - replace.range.begin_pos) &&
|
306
|
+
replace.replacement[offset, insertion.replacement.length] == insertion.replacement
|
307
|
+
end
|
308
|
+
|
207
309
|
def can_merge?(action, existing)
|
208
|
-
|
209
|
-
|
210
|
-
action_offset = overlap.begin_pos - action.range.begin_pos
|
211
|
-
other_offset = overlap.begin_pos - other.range.begin_pos
|
310
|
+
# Compare 2 replace/remove operations (neither is an insertion)
|
311
|
+
range = action.range
|
212
312
|
|
213
|
-
|
214
|
-
|
313
|
+
existing.all? do |other|
|
314
|
+
overlap = range.intersect(other.range)
|
315
|
+
next true if overlap.nil? # adjacent, not overlapping
|
316
|
+
|
317
|
+
repl1_offset = overlap.begin_pos - range.begin_pos
|
318
|
+
repl2_offset = overlap.begin_pos - other.range.begin_pos
|
319
|
+
repl1_length = [other.range.length - repl2_offset,
|
320
|
+
other.replacement.length - repl2_offset].max
|
321
|
+
repl2_length = [range.length - repl1_offset,
|
322
|
+
action.replacement.length - repl1_offset].max
|
323
|
+
|
324
|
+
replacement1 = action.replacement[repl1_offset, repl1_length] || ''.freeze
|
325
|
+
replacement2 = other.replacement[repl2_offset, repl2_length] || ''.freeze
|
215
326
|
replacement1 == replacement2
|
216
327
|
end
|
217
328
|
end
|
218
329
|
|
219
|
-
def merge_actions
|
220
|
-
actions
|
221
|
-
|
222
|
-
|
223
|
-
range
|
224
|
-
|
225
|
-
|
226
|
-
clobber(range)
|
227
|
-
|
228
|
-
replacement = merge_replacements(actions)
|
229
|
-
replace_actions(actions, Rewriter::Action.new(range, replacement))
|
330
|
+
def merge_actions(action, existing)
|
331
|
+
actions = existing.push(action).sort_by do |a|
|
332
|
+
[a.range.begin_pos, a.range.end_pos]
|
333
|
+
end
|
334
|
+
range = actions.first.range.join(actions.last.range)
|
335
|
+
|
336
|
+
Rewriter::Action.new(range, merge_replacements(actions))
|
230
337
|
end
|
231
338
|
|
232
|
-
def
|
233
|
-
|
234
|
-
active_queue
|
339
|
+
def merge_actions!(action, existing)
|
340
|
+
new_action = merge_actions(action, existing)
|
341
|
+
active_queue.delete(action)
|
342
|
+
replace_actions(existing, new_action)
|
235
343
|
end
|
236
344
|
|
237
345
|
def merge_replacements(actions)
|
238
346
|
# `actions` must be sorted by beginning position
|
239
347
|
begin_pos = actions.first.range.begin_pos
|
240
348
|
result = ''
|
349
|
+
prev_act = nil
|
241
350
|
|
242
351
|
actions.each do |act|
|
243
|
-
|
244
|
-
|
245
|
-
|
352
|
+
if !prev_act || act.range.disjoint?(prev_act.range)
|
353
|
+
result << act.replacement
|
354
|
+
else
|
355
|
+
prev_end = [prev_act.range.begin_pos + prev_act.replacement.length,
|
356
|
+
prev_act.range.end_pos].max
|
357
|
+
offset = prev_end - act.range.begin_pos
|
358
|
+
result << act.replacement[offset..-1] if offset < act.replacement.size
|
359
|
+
end
|
360
|
+
|
361
|
+
prev_act = act
|
246
362
|
end
|
247
363
|
|
248
364
|
result
|
249
365
|
end
|
250
366
|
|
367
|
+
def replace_actions(old, updated)
|
368
|
+
old.each { |act| active_queue.delete(act) }
|
369
|
+
active_queue << updated
|
370
|
+
end
|
371
|
+
|
372
|
+
def raise_clobber_error(action, existing)
|
373
|
+
# cannot replace 3 characters with "foobar"
|
374
|
+
diagnostic = Diagnostic.new(:error,
|
375
|
+
:invalid_action,
|
376
|
+
{ :action => action },
|
377
|
+
action.range)
|
378
|
+
@diagnostics.process(diagnostic)
|
379
|
+
|
380
|
+
# clobbered by: remove 3 characters
|
381
|
+
diagnostic = Diagnostic.new(:note,
|
382
|
+
:clobbered,
|
383
|
+
{ :action => existing[0] },
|
384
|
+
existing[0].range)
|
385
|
+
@diagnostics.process(diagnostic)
|
386
|
+
|
387
|
+
raise ClobberingError, "Parser::Source::Rewriter detected clobbering"
|
388
|
+
end
|
389
|
+
|
251
390
|
def in_transaction?
|
252
391
|
!@pending_queue.nil?
|
253
392
|
end
|
@@ -260,6 +399,10 @@ module Parser
|
|
260
399
|
@pending_clobber || @clobber
|
261
400
|
end
|
262
401
|
|
402
|
+
def active_insertions
|
403
|
+
@pending_insertions || @insertions
|
404
|
+
end
|
405
|
+
|
263
406
|
def active_clobber=(value)
|
264
407
|
if @pending_clobber
|
265
408
|
@pending_clobber = value
|
@@ -267,6 +410,18 @@ module Parser
|
|
267
410
|
@clobber = value
|
268
411
|
end
|
269
412
|
end
|
413
|
+
|
414
|
+
def active_insertions=(value)
|
415
|
+
if @pending_insertions
|
416
|
+
@pending_insertions = value
|
417
|
+
else
|
418
|
+
@insertions = value
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
def adjacent?(range1, range2)
|
423
|
+
range1.begin_pos <= range2.end_pos && range2.begin_pos <= range1.end_pos
|
424
|
+
end
|
270
425
|
end
|
271
426
|
|
272
427
|
end
|