parser_tree_rewriter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 775027865d822a480a3c13327d75ac655936ac2a
4
+ data.tar.gz: 38862b4c8b1e9c2a3394e7a01876581054724d28
5
+ SHA512:
6
+ metadata.gz: 5f0cdc7a1c97b5854f1db0504dca706cfe248e915e5f38beb280c49aa06172d467e1ae2f9529c0dad810e34a8d690e198ae59f8bf4490f2ed2c0c0c7df483c4e
7
+ data.tar.gz: 5e9e55a9840c0406f4ef054239a52a6fc436c70aa63ef91987863e4b2977bbd779d5d0e1421d03f469cf270c83077876018c524b412e4fe6b32013f6aec8fca9
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.5
5
+ before_install: gem install bundler -v 1.16.1
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in parser_tree_rewriter.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Marc-Andre Lafortune
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # ParserTreeRewriter
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/parser_tree_rewriter`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'parser_tree_rewriter'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install parser_tree_rewriter
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/marcandre/parser_tree_rewriter.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/test_*.rb"]
8
+ t.warning = false
9
+ end
10
+
11
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "parser_tree_rewriter"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,11 @@
1
+ require "parser_tree_rewriter/version"
2
+
3
+ require 'parser'
4
+
5
+ if Parser::VERSION <= '2.4.0.2'
6
+ require_relative 'parser_tree_rewriter/messages'
7
+ require_relative 'parser_tree_rewriter/source/buffer'
8
+ require_relative 'parser_tree_rewriter/source/range'
9
+ require_relative 'parser_tree_rewriter/source/tree_rewriter'
10
+ require_relative 'parser_tree_rewriter/source/tree_rewriter/action'
11
+ end
@@ -0,0 +1,21 @@
1
+ module Parser
2
+ ##
3
+ # Diagnostic messages (errors, warnings and notices) that can be generated.
4
+ #
5
+ # @see Diagnostic
6
+ #
7
+ # @api public
8
+ #
9
+ new_messages = MESSAGES.merge({
10
+ # Rewriter diagnostics
11
+ :different_replacements => 'different replacements: %{replacement} vs %{other_replacement}',
12
+ :swallowed_insertions => 'this replacement:',
13
+ :swallowed_insertions_conflict => 'swallows some inner rewriting actions:',
14
+ :crossing_deletions => 'the deletion of:',
15
+ :crossing_deletions_conflict => 'is crossing:',
16
+ :crossing_insertions => 'the rewriting action on:',
17
+ :crossing_insertions_conflict => 'is crossing that on:',
18
+ }).freeze
19
+ remove_const(:MESSAGES)
20
+ const_set(:MESSAGES, new_messages)
21
+ end
@@ -0,0 +1,9 @@
1
+ module Parser
2
+ module Source
3
+ class Buffer
4
+ def source_range
5
+ @source_range ||= Range.new(self, 0, source.size)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,98 @@
1
+ module Parser
2
+ module Source
3
+ class Range
4
+ include Comparable
5
+
6
+ ##
7
+ # @param [Hash] Endpoint(s) to change, any combination of :begin_pos or :end_pos
8
+ # @return [Range] the same range as this range but with the given end point(s) changed
9
+ # to the given value(s).
10
+ #
11
+ def with(begin_pos: @begin_pos, end_pos: @end_pos)
12
+ Range.new(@source_buffer, begin_pos, end_pos)
13
+ end
14
+
15
+ ##
16
+ # @param [Hash] Endpoint(s) to change, any combination of :begin_pos or :end_pos
17
+ # @return [Range] the same range as this range but with the given end point(s) adjusted
18
+ # by the given amount(s)
19
+ #
20
+ def adjust(begin_pos: 0, end_pos: 0)
21
+ Range.new(@source_buffer, @begin_pos + begin_pos, @end_pos + end_pos)
22
+ end
23
+
24
+ ##
25
+ # Return `true` iff this range and `other` are disjoint.
26
+ #
27
+ # Two ranges must be one and only one of ==, disjoint?, contains?, contained? or crossing?
28
+ #
29
+ # @param [Range] other
30
+ # @return [Boolean]
31
+ #
32
+ def disjoint?(other)
33
+ if empty? && other.empty?
34
+ @begin_pos != other.begin_pos
35
+ else
36
+ @begin_pos >= other.end_pos || other.begin_pos >= @end_pos
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Return `true` iff this range is not disjoint from `other`.
42
+ #
43
+ # @param [Range] other
44
+ # @return [Boolean] `true` if this range and `other` overlap
45
+ #
46
+ def overlaps?(other)
47
+ !disjoint?(other)
48
+ end
49
+
50
+ ##
51
+ # Returns true iff this range contains (strictly) `other`.
52
+ #
53
+ # Two ranges must be one and only one of ==, disjoint?, contains?, contained? or crossing?
54
+ #
55
+ # @param [Range] other
56
+ # @return [Boolean]
57
+ #
58
+ def contains?(other)
59
+ (other.begin_pos <=> @begin_pos) + (@end_pos <=> other.end_pos) >= (other.empty? ? 2 : 1)
60
+ end
61
+
62
+ ##
63
+ # Return `other.contains?(self)`
64
+ #
65
+ # Two ranges must be one and only one of ==, disjoint?, contains?, contained? or crossing?
66
+ #
67
+ # @param [Range] other
68
+ # @return [Boolean]
69
+ #
70
+ def contained?(other)
71
+ other.contains?(self)
72
+ end
73
+
74
+ ##
75
+ # Returns true iff both ranges intersect and also have different elements from one another.
76
+ #
77
+ # Two ranges must be one and only one of ==, disjoint?, contains?, contained? or crossing?
78
+ #
79
+ # @param [Range] other
80
+ # @return [Boolean]
81
+ #
82
+ def crossing?(other)
83
+ return false unless overlaps?(other)
84
+ (@begin_pos <=> other.begin_pos) * (@end_pos <=> other.end_pos) == 1
85
+ end
86
+
87
+ ##
88
+ # Compare ranges, first by begin_pos, then by end_pos.
89
+ #
90
+ def <=>(other)
91
+ return nil unless other.is_a?(::Parser::Source::Range) &&
92
+ @source_buffer == other.source_buffer
93
+ (@begin_pos <=> other.begin_pos).nonzero? ||
94
+ (@end_pos <=> other.end_pos)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,274 @@
1
+ module Parser
2
+ module Source
3
+
4
+ ##
5
+ # {TreeRewriter} performs the heavy lifting in the source rewriting process.
6
+ # It schedules code updates to be performed in the correct order.
7
+ #
8
+ # For simple cases, the resulting source will be obvious.
9
+ #
10
+ # Examples for more complex cases follow. Assume these examples are acting on
11
+ # the source `'puts(:hello, :world)`. The methods #wrap, #remove, etc.
12
+ # receive a Range as first argument; for clarity, examples below use english
13
+ # sentences and a string of raw code instead.
14
+ #
15
+ # ## Overlapping ranges:
16
+ #
17
+ # Any two rewriting actions on overlapping ranges will fail and raise
18
+ # a `ClobberingError`, unless they are both deletions (covered next).
19
+ #
20
+ # * wrap ':hello, ' with '(' and ')'
21
+ # * wrap ', :world' with '(' and ')'
22
+ # => CloberringError
23
+ #
24
+ # ## Overlapping deletions:
25
+ #
26
+ # * remove ':hello, '
27
+ # * remove ', :world'
28
+ #
29
+ # The overlapping ranges are merged and `':hello, :world'` will be removed.
30
+ # This policy can be changed. `:crossing_deletions` defaults to `:accept`
31
+ # but can be set to `:warn` or `:raise`.
32
+ #
33
+ # ## Multiple actions at the same end points:
34
+ #
35
+ # Results will always be independent on the order they were given.
36
+ # Exception: rewriting actions done on exactly the same range (covered next).
37
+ #
38
+ # Example:
39
+ # * replace ', ' by ' => '
40
+ # * wrap ':hello, :world' with '{' and '}'
41
+ # * replace ':world' with ':everybody'
42
+ # * wrap ':world' with '[', ']'
43
+ #
44
+ # The resulting string will be `'puts({:hello => [:everybody]})'`
45
+ # and this result is independent on the order the instructions were given in.
46
+ #
47
+ # Note that if the two "replace" were given as a single replacement of ', :world'
48
+ # for ' => :everybody', the result would be a `ClobberingError` because of the wrap
49
+ # in square brackets.
50
+ #
51
+ # ## Multiple wraps on same range:
52
+ # * wrap ':hello' with '(' and ')'
53
+ # * wrap ':hello' with '[' and ']'
54
+ #
55
+ # The wraps are combined in order given and results would be `'puts([(:hello)], :world)'`.
56
+ #
57
+ # ## Multiple replacements on same range:
58
+ # * replace ':hello' by ':hi', then
59
+ # * replace ':hello' by ':hey'
60
+ #
61
+ # The replacements are made in the order given, so the latter replacement
62
+ # supersedes the former and ':hello' will be replaced by ':hey'.
63
+ #
64
+ # This policy can be changed. `:different_replacements` defaults to `:accept`
65
+ # but can be set to `:warn` or `:raise`.
66
+ #
67
+ # ## Swallowed insertions:
68
+ # wrap 'world' by '__', '__'
69
+ # replace ':hello, :world' with ':hi'
70
+ #
71
+ # A containing replacement will swallow the contained rewriting actions
72
+ # and `':hello, :world'` will be replaced by `':hi'`.
73
+ #
74
+ # This policy can be changed for swallowed insertions. `:swallowed_insertions`
75
+ # defaults to `:accept` but can be set to `:warn` or `:raise`
76
+ #
77
+ # ## Implementation
78
+ # The updates are organized in a tree, according to the ranges they act on
79
+ # (where children are strictly contained by their parent), hence the name.
80
+ #
81
+ # @!attribute [r] source_buffer
82
+ # @return [Source::Buffer]
83
+ #
84
+ # @!attribute [r] diagnostics
85
+ # @return [Diagnostic::Engine]
86
+ #
87
+ # @api public
88
+ #
89
+ class TreeRewriter
90
+ attr_reader :source_buffer
91
+ attr_reader :diagnostics
92
+
93
+ ##
94
+ # @param [Source::Buffer] source_buffer
95
+ #
96
+ def initialize(source_buffer,
97
+ crossing_deletions: :accept,
98
+ different_replacements: :accept,
99
+ swallowed_insertions: :accept)
100
+ @diagnostics = Diagnostic::Engine.new
101
+ @diagnostics.consumer = -> diag { $stderr.puts diag.render }
102
+
103
+ @source_buffer = source_buffer
104
+ @in_transaction = false
105
+
106
+ @policy = {crossing_deletions: crossing_deletions,
107
+ different_replacements: different_replacements,
108
+ swallowed_insertions: swallowed_insertions}.freeze
109
+ check_policy_validity
110
+
111
+ @enforcer = method(:enforce_policy)
112
+ # We need a range that would be jugded as containing all other ranges,
113
+ # including 0...0 and size...size:
114
+ all_encompassing_range = @source_buffer.source_range.adjust(begin_pos: -1, end_pos: +1)
115
+ @action_root = TreeRewriter::Action.new(all_encompassing_range, @enforcer)
116
+ end
117
+
118
+ ##
119
+ # Replaces the code of the source range `range` with `content`.
120
+ #
121
+ # @param [Range] range
122
+ # @param [String] content
123
+ # @return [Rewriter] self
124
+ # @raise [ClobberingError] when clobbering is detected
125
+ #
126
+ def replace(range, content)
127
+ combine(range, replacement: content)
128
+ end
129
+
130
+ ##
131
+ # Inserts the given strings before and after the given range.
132
+ #
133
+ # @param [Range] range
134
+ # @param [String or nil] insert_before
135
+ # @param [String or nil] insert_after
136
+ # @return [Rewriter] self
137
+ # @raise [ClobberingError] when clobbering is detected
138
+ #
139
+ def wrap(range, insert_before, insert_after)
140
+ combine(range, insert_before: insert_before.to_s, insert_after: insert_after.to_s)
141
+ end
142
+
143
+ ##
144
+ # Shortcut for `replace(range, '')`
145
+ #
146
+ # @param [Range] range
147
+ # @return [Rewriter] self
148
+ # @raise [ClobberingError] when clobbering is detected
149
+ #
150
+ def remove(range)
151
+ replace(range, ''.freeze)
152
+ end
153
+
154
+
155
+ ##
156
+ # Shortcut for `wrap(range, content, nil)`
157
+ #
158
+ # @param [Range] range
159
+ # @param [String] content
160
+ # @return [Rewriter] self
161
+ # @raise [ClobberingError] when clobbering is detected
162
+ #
163
+ def insert_before(range, content)
164
+ wrap(range, content, nil)
165
+ end
166
+
167
+ ##
168
+ # Shortcut for `wrap(range, nil, content)`
169
+ #
170
+ # @param [Range] range
171
+ # @param [String] content
172
+ # @return [Rewriter] self
173
+ # @raise [ClobberingError] when clobbering is detected
174
+ #
175
+ def insert_after(range, content)
176
+ wrap(range, nil, content)
177
+ end
178
+
179
+ ##
180
+ # Applies all scheduled changes to the `source_buffer` and returns
181
+ # modified source as a new string.
182
+ #
183
+ # @return [String]
184
+ #
185
+ def process
186
+ source = @source_buffer.source.dup
187
+ adjustment = 0
188
+
189
+ @action_root.ordered_replacements.each do |range, replacement|
190
+ begin_pos = range.begin_pos + adjustment
191
+ end_pos = begin_pos + range.length
192
+
193
+ source[begin_pos...end_pos] = replacement
194
+
195
+ adjustment += replacement.length - range.length
196
+ end
197
+
198
+ source
199
+ end
200
+
201
+ ##
202
+ # Provides a protected block where a sequence of multiple rewrite actions
203
+ # are handled atomically. If any of the actions failed by clobbering,
204
+ # all the actions are rolled back.
205
+ #
206
+ # @raise [RuntimeError] when no block is passed
207
+ # @raise [RuntimeError] when already in a transaction
208
+ #
209
+ def transaction
210
+ unless block_given?
211
+ raise "#{self.class}##{__method__} requires block"
212
+ end
213
+
214
+ previous = @in_transaction
215
+ @in_transaction = true
216
+ restore_root = @action_root
217
+
218
+ yield
219
+
220
+ restore_root = nil
221
+
222
+ self
223
+ ensure
224
+ @action_root = restore_root if restore_root
225
+ @in_transaction = previous
226
+ end
227
+
228
+ def in_transaction?
229
+ @in_transaction
230
+ end
231
+
232
+ private
233
+
234
+ ACTIONS = %i[accept warn raise].freeze
235
+ def check_policy_validity
236
+ invalid = @policy.values - ACTIONS
237
+ raise ArgumentError, "Invalid policy: #{invalid.join(', ')}" unless invalid.empty?
238
+ end
239
+
240
+ def combine(range, attributes)
241
+ range = check_range_validity(range)
242
+ action = TreeRewriter::Action.new(range, @enforcer, attributes)
243
+ @action_root = @action_root.combine(action)
244
+ self
245
+ end
246
+
247
+ def check_range_validity(range)
248
+ if range.begin_pos < 0 || range.end_pos > @source_buffer.source.size
249
+ raise IndexError, "The range #{action.range} is outside the bounds of the source"
250
+ end
251
+ range
252
+ end
253
+
254
+ def enforce_policy(event)
255
+ return if @policy[event] == :accept
256
+ return unless (values = yield)
257
+ trigger_policy(event, values)
258
+ end
259
+
260
+ POLICY_TO_LEVEL = {warn: :warning, raise: :error}.freeze
261
+ def trigger_policy(event, range: raise, conflict: nil, **arguments)
262
+ action = @policy[event] || :raise
263
+ diag = Parser::Diagnostic.new(POLICY_TO_LEVEL[action], event, arguments, range)
264
+ @diagnostics.process(diag)
265
+ if conflict
266
+ range, *highlights = conflict
267
+ diag = Parser::Diagnostic.new(POLICY_TO_LEVEL[action], :"#{event}_conflict", arguments, range, highlights)
268
+ @diagnostics.process(diag)
269
+ end
270
+ raise Parser::ClobberingError, "Parser::Source::TreeRewriter detected clobbering" if action == :raise
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,131 @@
1
+ module Parser
2
+ module Source
3
+ ##
4
+ # @api private
5
+ #
6
+ # Actions are arranged in a tree and get combined so that:
7
+ # children are strictly contained by their parent
8
+ # sibblings all disjoint from one another
9
+ # only actions with replacement==nil may have children
10
+ #
11
+ class TreeRewriter::Action
12
+ attr_reader :range, :replacement, :insert_before, :insert_after
13
+
14
+ def initialize(range, enforcer,
15
+ insert_before: '',
16
+ replacement: nil,
17
+ insert_after: '',
18
+ children: []
19
+ )
20
+ @range, @enforcer, @children, @insert_before, @replacement, @insert_after =
21
+ range, enforcer, children.freeze, insert_before.freeze, replacement, insert_after.freeze
22
+
23
+ freeze
24
+ end
25
+
26
+ # Assumes action.children.empty?
27
+ def combine(action)
28
+ return self unless action.insertion? || action.replacement # Ignore empty action
29
+ do_combine(action)
30
+ end
31
+
32
+ def ordered_replacements
33
+ reps = []
34
+ reps << [@range.begin, @insert_before] unless @insert_before.empty?
35
+ reps << [@range, @replacement] if @replacement
36
+ reps.concat(@children.sort_by(&:range).flat_map(&:ordered_replacements))
37
+ reps << [@range.end, @insert_after] unless @insert_after.empty?
38
+ reps
39
+ end
40
+
41
+ def insertion?
42
+ !insert_before.empty? || !insert_after.empty? || (replacement && !replacement.empty?)
43
+ end
44
+
45
+ protected
46
+
47
+ def with(range: @range, children: @children, insert_before: @insert_before, replacement: @replacement, insert_after: @insert_after)
48
+ children = swallow(children) if replacement
49
+ self.class.new(range, @enforcer, children: children, insert_before: insert_before, replacement: replacement, insert_after: insert_after)
50
+ end
51
+
52
+ # Assumes range.contains?(action.range) && action.children.empty?
53
+ def do_combine(action)
54
+ if action.range == @range
55
+ merge(action)
56
+ else
57
+ place_in_hierachy(action)
58
+ end
59
+ end
60
+
61
+ def place_in_hierachy(action)
62
+ family = @children.group_by { |child| child.relationship_with(action) }
63
+
64
+ if family[:fusible]
65
+ fuse_deletions(action, family[:fusible], [*family[:sibbling], *family[:child]])
66
+ else
67
+ extra_sibbling = if family[:parent] # action should be a descendant of one of the children
68
+ family[:parent][0].do_combine(action)
69
+ elsif family[:child] # or it should become the parent of some of the children,
70
+ action.with(children: family[:child])
71
+ else # or else it should become an additional child
72
+ action
73
+ end
74
+ with(children: [*family[:sibbling], extra_sibbling])
75
+ end
76
+ end
77
+
78
+ def fuse_deletions(action, fusible, other_sibblings)
79
+ without_fusible = with(children: other_sibblings)
80
+ fused_range = [action, *fusible].map(&:range).inject(:join)
81
+ fused_deletion = action.with(range: fused_range)
82
+ without_fusible.do_combine(fused_deletion)
83
+ end
84
+
85
+ # Returns what relationship self should have with `action`; either of
86
+ # :sibbling, :parent, :child, :fusible or raises a CloberingError
87
+ # In case of equal range, returns :parent
88
+ def relationship_with(action)
89
+ if action.range == @range || @range.contains?(action.range)
90
+ :parent
91
+ elsif @range.contained?(action.range)
92
+ :child
93
+ elsif @range.disjoint?(action.range)
94
+ :sibbling
95
+ elsif !action.insertion? && !insertion?
96
+ @enforcer.call(:crossing_deletions) { {range: action.range, conflict: @range} }
97
+ :fusible
98
+ else
99
+ @enforcer.call(:crossing_insertions) { {range: action.range, conflict: @range} }
100
+ end
101
+ end
102
+
103
+ # Assumes action.range == range && action.children.empty?
104
+ def merge(action)
105
+ call_enforcer_for_merge(action)
106
+ with(
107
+ insert_before: "#{action.insert_before}#{insert_before}",
108
+ replacement: action.replacement || @replacement,
109
+ insert_after: "#{insert_after}#{action.insert_after}",
110
+ )
111
+ end
112
+
113
+ def call_enforcer_for_merge(action)
114
+ @enforcer.call(:different_replacements) do
115
+ if @replacement && action.replacement && @replacement != action.replacement
116
+ {range: @range, replacement: action.replacement, other_replacement: @replacement}
117
+ end
118
+ end
119
+ end
120
+
121
+ def swallow(children)
122
+ @enforcer.call(:swallowed_insertions) do
123
+ insertions = children.select(&:insertion?)
124
+
125
+ {range: @range, conflict: insertions.map(&:range)} unless insertions.empty?
126
+ end
127
+ []
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ module ParserTreeRewriter
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,28 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "parser_tree_rewriter/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "parser_tree_rewriter"
8
+ spec.version = ParserTreeRewriter::VERSION
9
+ spec.authors = ["Marc-Andre Lafortune"]
10
+ spec.email = ["github@marc-andre.ca"]
11
+
12
+ spec.summary = %q{Adds Parser::Source::TreeRewriter to parser, until next release}
13
+ spec.description = %q{Adds Parser::Source::TreeRewriter to parser, until next release}
14
+ spec.homepage = "https://github.com/marcandre/parser_tree_rewriter"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = "exe"
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ["lib"]
23
+ spec.add_dependency "parser", ">= 2.4.0.2"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.16"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "minitest", "~> 5.0"
28
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parser_tree_rewriter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Marc-Andre Lafortune
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-02-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parser
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.4.0.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.4.0.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '5.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '5.0'
69
+ description: Adds Parser::Source::TreeRewriter to parser, until next release
70
+ email:
71
+ - github@marc-andre.ca
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".travis.yml"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/console
83
+ - bin/setup
84
+ - lib/parser_tree_rewriter.rb
85
+ - lib/parser_tree_rewriter/messages.rb
86
+ - lib/parser_tree_rewriter/source/buffer.rb
87
+ - lib/parser_tree_rewriter/source/range.rb
88
+ - lib/parser_tree_rewriter/source/tree_rewriter.rb
89
+ - lib/parser_tree_rewriter/source/tree_rewriter/action.rb
90
+ - lib/parser_tree_rewriter/version.rb
91
+ - parser_tree_rewriter.gemspec
92
+ homepage: https://github.com/marcandre/parser_tree_rewriter
93
+ licenses:
94
+ - MIT
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 2.6.14
113
+ signing_key:
114
+ specification_version: 4
115
+ summary: Adds Parser::Source::TreeRewriter to parser, until next release
116
+ test_files: []