astel 0.2.0 → 0.3.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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'rewriter/edit_index'
4
+
3
5
  module Astel
4
6
  class Rewriter
5
7
  class ConflictError < Error
@@ -29,85 +31,6 @@ module Astel
29
31
  end
30
32
  end
31
33
 
32
- class EditIndex
33
- include Enumerable
34
-
35
- SPLIT_THRESHOLD = 256
36
-
37
- def initialize
38
- @blocks = []
39
- end
40
-
41
- def add(edit)
42
- if @blocks.empty?
43
- @blocks << [edit]
44
- return
45
- end
46
-
47
- block_index, block, edit_index = insertion_position(edit)
48
- conflict = conflict_at(block_index, block, edit_index) { |existing| yield existing }
49
- return conflict if conflict
50
-
51
- block.insert(edit_index, edit)
52
- split(block_index, block) if block.length > SPLIT_THRESHOLD
53
- nil
54
- end
55
-
56
- def find_conflict(edit)
57
- return if @blocks.empty?
58
-
59
- block_index, block, edit_index = insertion_position(edit)
60
- conflict_at(block_index, block, edit_index) { |existing| yield existing }
61
- end
62
-
63
- def each
64
- return enum_for(__method__) unless block_given?
65
-
66
- @blocks.each do |block|
67
- block.each { |edit| yield edit }
68
- end
69
- end
70
-
71
- private
72
-
73
- def insertion_position(edit)
74
- block_index = block_index_for(edit)
75
- block = @blocks.fetch(block_index)
76
- edit_index = block.bsearch_index { |existing| ordered_after?(existing, edit) } || block.length
77
- [block_index, block, edit_index]
78
- end
79
-
80
- def conflict_at(block_index, block, edit_index)
81
- following = block[edit_index] || @blocks[block_index + 1]&.first
82
- return following if following && yield(following)
83
-
84
- preceding = if edit_index.positive?
85
- block[edit_index - 1]
86
- elsif block_index.positive?
87
- @blocks[block_index - 1].last
88
- end
89
- return preceding if preceding && yield(preceding)
90
-
91
- nil
92
- end
93
-
94
- def block_index_for(edit)
95
- @blocks.bsearch_index { |block| ordered_after?(block.last, edit) } || @blocks.length - 1
96
- end
97
-
98
- def ordered_after?(existing, edit)
99
- existing.start_offset > edit.start_offset ||
100
- (existing.start_offset == edit.start_offset && existing.end_offset > edit.end_offset) ||
101
- (existing.start_offset == edit.start_offset && existing.end_offset == edit.end_offset &&
102
- existing.sequence > edit.sequence)
103
- end
104
-
105
- def split(block_index, block)
106
- middle = block.length / 2
107
- @blocks.insert(block_index + 1, block.slice!(middle, block.length - middle))
108
- end
109
- end
110
-
111
34
  attr_reader :source_file
112
35
 
113
36
  def initialize(source_file, duplicate_insertions: :accept)
@@ -158,27 +81,16 @@ module Astel
158
81
  end
159
82
 
160
83
  def merge!(other)
161
- unless other.is_a?(Rewriter) && other.source_file.equal?(@source_file)
162
- raise ArgumentError, 'cannot merge edits for a different source'
84
+ validated_edits_from(other).each do |edit|
85
+ add_edit(edit.start_offset, edit.end_offset, edit.replacement)
163
86
  end
164
-
165
- validation_index = EditIndex.new
166
- @edit_index.each { |edit| validation_index.add(edit) { false } }
167
- other.edits.each do |edit|
168
- conflict = validation_index.find_conflict(edit) { |existing| conflict?(edit, existing) }
169
- raise ConflictError.new(edit, conflict) if conflict
170
-
171
- validation_index.add(edit) { false }
172
- end
173
- other.edits.each { |edit| add_edit(edit.start_offset, edit.end_offset, edit.replacement) }
174
87
  self
175
88
  end
176
89
 
177
90
  def transaction(raise_on_conflict: false)
178
- staging = Rewriter.new(@source_file, duplicate_insertions: @duplicate_insertions)
179
- staging.instance_variable_set(:@transaction_staging, true)
91
+ staging = transaction_staging
180
92
  yield staging
181
- return transaction_failed if staging.instance_variable_get(:@transaction_failed)
93
+ return transaction_failed if transaction_failed?(staging)
182
94
 
183
95
  merge!(staging)
184
96
  true
@@ -190,6 +102,14 @@ module Astel
190
102
  end
191
103
 
192
104
  def rewrite(validate: false)
105
+ result = apply_edits
106
+ validate_result(result, validate)
107
+ result
108
+ end
109
+
110
+ private
111
+
112
+ def apply_edits
193
113
  source = @source_file.source
194
114
  result = String.new(capacity: source.bytesize + @result_size_delta, encoding: source.encoding)
195
115
  cursor = 0
@@ -201,25 +121,40 @@ module Astel
201
121
  end
202
122
 
203
123
  result << source.byteslice(cursor, source.bytesize - cursor)
204
- validate_result(result, validate)
205
124
  result
206
125
  end
207
126
 
208
- private
209
-
210
127
  def add_edit(start_offset, end_offset, replacement)
128
+ edit = build_edit(start_offset, end_offset, replacement)
129
+ register_edit(edit)
130
+ self
131
+ end
132
+
133
+ def build_edit(start_offset, end_offset, replacement)
211
134
  validate_offsets(start_offset, end_offset)
212
- replacement = replacement.to_s.dup
213
- unless Encoding.compatible?(@source_file.source, replacement)
214
- raise Encoding::CompatibilityError,
215
- "#{replacement.encoding} replacement is incompatible with #{@source_file.source.encoding} source"
216
- end
217
- edit = Edit.new(
135
+ Edit.new(
218
136
  start_offset: start_offset,
219
137
  end_offset: end_offset,
220
- replacement: replacement.freeze,
138
+ replacement: validated_replacement(replacement),
221
139
  sequence: @sequence
222
140
  )
141
+ end
142
+
143
+ def validated_replacement(replacement)
144
+ replacement = replacement.to_s
145
+ unless replacement.valid_encoding?
146
+ raise Encoding::CompatibilityError, "#{replacement.encoding} replacement contains invalid bytes"
147
+ end
148
+
149
+ begin
150
+ replacement.encode(@source_file.source.encoding).freeze
151
+ rescue EncodingError
152
+ raise Encoding::CompatibilityError,
153
+ "#{replacement.encoding} replacement is incompatible with #{@source_file.source.encoding} source"
154
+ end
155
+ end
156
+
157
+ def register_edit(edit)
223
158
  conflict = @edit_index.add(edit) { |existing| conflict?(edit, existing) }
224
159
  raise ConflictError.new(edit, conflict) if conflict
225
160
 
@@ -227,7 +162,34 @@ module Astel
227
162
  @result_size_delta += edit.replacement.bytesize - (edit.end_offset - edit.start_offset)
228
163
  @edits << edit
229
164
  @edits_snapshot = nil
230
- self
165
+ end
166
+
167
+ def validated_edits_from(other)
168
+ raise ArgumentError, 'cannot merge edits for a different source' unless same_source_rewriter?(other)
169
+
170
+ validation_index = EditIndex.new
171
+ @edit_index.each { |edit| validation_index.add(edit) { false } }
172
+ other.edits.each do |edit|
173
+ conflict = validation_index.find_conflict(edit) { |existing| conflict?(edit, existing) }
174
+ raise ConflictError.new(edit, conflict) if conflict
175
+
176
+ validation_index.add(edit) { false }
177
+ end
178
+ other.edits
179
+ end
180
+
181
+ def same_source_rewriter?(other)
182
+ other.is_a?(Rewriter) && other.source_file.equal?(@source_file)
183
+ end
184
+
185
+ def transaction_staging
186
+ staging = Rewriter.new(@source_file, duplicate_insertions: @duplicate_insertions)
187
+ staging.instance_variable_set(:@transaction_staging, true)
188
+ staging
189
+ end
190
+
191
+ def transaction_failed?(staging)
192
+ staging.instance_variable_get(:@transaction_failed)
231
193
  end
232
194
 
233
195
  def offsets(location)
@@ -239,21 +201,31 @@ module Astel
239
201
  end
240
202
 
241
203
  def validate_offsets(start_offset, end_offset)
242
- valid = start_offset.is_a?(Integer) && end_offset.is_a?(Integer) &&
243
- start_offset >= 0 && start_offset <= end_offset && end_offset <= @source_file.source.bytesize
244
- raise RangeError, 'edit range is outside the source' unless valid
204
+ raise RangeError, 'edit range is outside the source' unless valid_offsets?(start_offset, end_offset)
205
+ end
206
+
207
+ def valid_offsets?(start_offset, end_offset)
208
+ start_offset.is_a?(Integer) && end_offset.is_a?(Integer) &&
209
+ start_offset >= 0 && start_offset <= end_offset && end_offset <= @source_file.source.bytesize
245
210
  end
246
211
 
247
212
  def conflict?(left, right)
248
- if @duplicate_insertions == :raise && left.insertion? && right.insertion?
249
- return left.start_offset == right.start_offset
250
- end
251
- return left.start_offset > right.start_offset && left.start_offset < right.end_offset if left.insertion?
252
- return right.start_offset > left.start_offset && right.start_offset < left.end_offset if right.insertion?
213
+ return true if duplicate_insertion_conflict?(left, right)
214
+ return insertion_inside?(left, right) if left.insertion?
215
+ return insertion_inside?(right, left) if right.insertion?
253
216
 
254
217
  left.start_offset < right.end_offset && right.start_offset < left.end_offset
255
218
  end
256
219
 
220
+ def duplicate_insertion_conflict?(left, right)
221
+ @duplicate_insertions == :raise && left.insertion? && right.insertion? &&
222
+ left.start_offset == right.start_offset
223
+ end
224
+
225
+ def insertion_inside?(insertion, edit)
226
+ insertion.start_offset > edit.start_offset && insertion.start_offset < edit.end_offset
227
+ end
228
+
257
229
  def transaction_failed
258
230
  @transaction_failed = true if @transaction_staging
259
231
  false
@@ -17,10 +17,7 @@ module Astel
17
17
  def initialize(code, path:, version: nil)
18
18
  @path = path.to_s.freeze
19
19
  @version = normalize_version(version).freeze if version
20
- raw_source = code.dup.freeze
21
- options = { filepath: @path }
22
- options[:version] = @version if @version
23
- @parse_result = Prism.parse(raw_source, **options)
20
+ @parse_result = parse_source(code, path: @path, version: @version)
24
21
  @source = @parse_result.source.source.freeze
25
22
  @ast = @parse_result.value
26
23
  @comments = @parse_result.comments.freeze
@@ -53,10 +50,8 @@ module Astel
53
50
  end
54
51
 
55
52
  def magic_comment?(name)
56
- pattern = /\A#\s*#{Regexp.escape(name.to_s)}\s*:\s*true\b/
57
- lines.first(2).any? do |line|
58
- candidate = line.start_with?("\uFEFF") ? line.delete_prefix("\uFEFF") : line
59
- candidate.match?(pattern)
53
+ @parse_result.magic_comments.any? do |comment|
54
+ comment.key == name.to_s && comment.value == 'true' && effective_magic_comment?(comment)
60
55
  end
61
56
  end
62
57
 
@@ -85,9 +80,10 @@ module Astel
85
80
 
86
81
  def line_end(byte_offset)
87
82
  validate_offset!(byte_offset)
83
+ start = line_start(byte_offset)
88
84
  ending = @parse_result.source.line_end(byte_offset)
89
- ending -= 1 if ending.positive? && source.getbyte(ending - 1) == 10
90
- ending -= 1 if ending.positive? && source.getbyte(ending - 1) == 13
85
+ ending -= 1 if ending > start && source.getbyte(ending - 1) == 10
86
+ ending -= 1 if ending > start && source.getbyte(ending - 1) == 13
91
87
  ending
92
88
  end
93
89
 
@@ -105,10 +101,7 @@ module Astel
105
101
  end
106
102
 
107
103
  def newline
108
- @newline ||= begin
109
- crlf = source.b.scan(/\r\n/).length
110
- crlf > source.count("\n") - crlf ? "\r\n" : "\n"
111
- end
104
+ @newline ||= crlf_dominant? ? "\r\n" : "\n"
112
105
  end
113
106
 
114
107
  def indentation_at(byte_offset)
@@ -117,20 +110,40 @@ module Astel
117
110
  end
118
111
 
119
112
  def indent_unit
120
- @indent_unit ||= begin
121
- indentations = lines.filter_map { |line| line[/\A[ \t]+/] }
122
- if indentations.any? { |indentation| indentation.include?("\t") }
123
- "\t"
124
- else
125
- widths = [0, *indentations.map(&:bytesize)].uniq.sort
126
- width = widths.each_cons(2).map { |left, right| right - left }.min
127
- width ? ' ' * width : ' '
128
- end
129
- end
113
+ @indent_unit ||= detected_indent_unit
130
114
  end
131
115
 
132
116
  private
133
117
 
118
+ def effective_magic_comment?(comment)
119
+ line = @parse_result.source.line(comment.key_loc.start_offset)
120
+ line == 1 || (line == 2 && source.b.start_with?('#!'.b))
121
+ end
122
+
123
+ def parse_source(code, path:, version:)
124
+ options = { filepath: path }
125
+ options[:version] = version if version
126
+ Prism.parse(code.dup.freeze, **options)
127
+ end
128
+
129
+ def crlf_dominant?
130
+ crlf_count = source.b.scan(/\r\n/).length
131
+ crlf_count > source.count("\n") - crlf_count
132
+ end
133
+
134
+ def detected_indent_unit
135
+ indentations = lines.filter_map { |line| line[/\A[ \t]+/] }
136
+ return "\t" if indentations.any? { |indentation| indentation.include?("\t") }
137
+
138
+ space_indent_unit(indentations)
139
+ end
140
+
141
+ def space_indent_unit(indentations)
142
+ widths = [0, *indentations.map(&:bytesize)].uniq.sort
143
+ width = widths.each_cons(2).map { |left, right| right - left }.min
144
+ width ? ' ' * width : ' '
145
+ end
146
+
134
147
  def location(start_offset, end_offset)
135
148
  prism_source = @parse_result.source
136
149
  Location.new(
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../astel'
4
+
5
+ module Astel
6
+ module UnifiedDiff
7
+ MYERS_TRACE_DISTANCE_LIMIT = 256
8
+ private_constant :MYERS_TRACE_DISTANCE_LIMIT
9
+
10
+ module_function
11
+
12
+ def unified(before, after, path:, context: 3)
13
+ validate_options(path, context)
14
+ return '' if before == after
15
+
16
+ operations = operations(before.lines(chomp: false), after.lines(chomp: false))
17
+ annotated = annotate(operations)
18
+ hunks = hunk_ranges(annotated, context)
19
+ output = String.new(encoding: Encoding::BINARY)
20
+ output << '--- a/'.b << path.to_s.b << "\n+++ b/".b << path.to_s.b << "\n".b
21
+ hunks.each { |range| append_hunk(output, annotated[range]) }
22
+ output
23
+ end
24
+
25
+ def operations(before, after)
26
+ prefix = common_prefix_length(before, after)
27
+ suffix = common_suffix_length(before, after, prefix)
28
+
29
+ old_middle = before[prefix...(before.length - suffix)]
30
+ new_middle = after[prefix...(after.length - suffix)]
31
+ before.first(prefix).map { |line| [:equal, line] } +
32
+ myers_operations(old_middle, new_middle) +
33
+ (suffix.zero? ? [] : before.last(suffix).map { |line| [:equal, line] })
34
+ end
35
+ private_class_method :operations
36
+
37
+ def common_prefix_length(before, after)
38
+ length = 0
39
+ length += 1 while length < before.length && length < after.length && before[length] == after[length]
40
+ length
41
+ end
42
+ private_class_method :common_prefix_length
43
+
44
+ def common_suffix_length(before, after, prefix_length)
45
+ length = 0
46
+ while length < before.length - prefix_length && length < after.length - prefix_length &&
47
+ before[-length - 1] == after[-length - 1]
48
+ length += 1
49
+ end
50
+ length
51
+ end
52
+ private_class_method :common_suffix_length
53
+
54
+ def myers_operations(before, after)
55
+ frontier = Hash.new(0)
56
+ frontier[1] = 0
57
+ trace = []
58
+
59
+ (0..(before.length + after.length)).each do |distance|
60
+ # ponytail: use linear-space backtracking if minimal large rewrites become necessary.
61
+ return replacement_operations(before, after) if distance > MYERS_TRACE_DISTANCE_LIMIT
62
+
63
+ trace << frontier.dup
64
+ (-distance).step(distance, 2) do |diagonal|
65
+ old_index = next_old_index(frontier, diagonal, distance)
66
+ new_index = old_index - diagonal
67
+ while old_index < before.length && new_index < after.length && before[old_index] == after[new_index]
68
+ old_index += 1
69
+ new_index += 1
70
+ end
71
+ frontier[diagonal] = old_index
72
+ return backtrack_myers(before, after, trace, distance) if
73
+ old_index >= before.length && new_index >= after.length
74
+ end
75
+ end
76
+ end
77
+ private_class_method :myers_operations
78
+
79
+ def replacement_operations(before, after)
80
+ before.map { |line| [:delete, line] } + after.map { |line| [:insert, line] }
81
+ end
82
+ private_class_method :replacement_operations
83
+
84
+ def next_old_index(frontier, diagonal, distance)
85
+ if diagonal == -distance || (diagonal != distance && frontier[diagonal - 1] < frontier[diagonal + 1])
86
+ frontier[diagonal + 1]
87
+ else
88
+ frontier[diagonal - 1] + 1
89
+ end
90
+ end
91
+ private_class_method :next_old_index
92
+
93
+ def backtrack_myers(before, after, trace, distance)
94
+ old_index = before.length
95
+ new_index = after.length
96
+ operations = []
97
+
98
+ distance.downto(0) do |depth|
99
+ previous = trace[depth]
100
+ diagonal = old_index - new_index
101
+ previous_diagonal = if diagonal == -depth ||
102
+ (diagonal != depth && previous[diagonal - 1] < previous[diagonal + 1])
103
+ diagonal + 1
104
+ else
105
+ diagonal - 1
106
+ end
107
+ previous_old_index = previous[previous_diagonal]
108
+ previous_new_index = previous_old_index - previous_diagonal
109
+
110
+ while old_index > previous_old_index && new_index > previous_new_index
111
+ operations << [:equal, before[old_index - 1]]
112
+ old_index -= 1
113
+ new_index -= 1
114
+ end
115
+ next if depth.zero?
116
+
117
+ if old_index == previous_old_index
118
+ operations << [:insert, after[new_index - 1]]
119
+ new_index -= 1
120
+ else
121
+ operations << [:delete, before[old_index - 1]]
122
+ old_index -= 1
123
+ end
124
+ end
125
+
126
+ operations.reverse
127
+ end
128
+ private_class_method :backtrack_myers
129
+
130
+ def annotate(operations)
131
+ old_line = 1
132
+ new_line = 1
133
+ operations.map do |type, line|
134
+ entry = { type: type, line: line, old_line: old_line, new_line: new_line }
135
+ old_line += 1 unless type == :insert
136
+ new_line += 1 unless type == :delete
137
+ entry
138
+ end
139
+ end
140
+ private_class_method :annotate
141
+
142
+ def hunk_ranges(operations, context)
143
+ changed = operations.each_index.reject { |index| operations[index][:type] == :equal }
144
+ ranges = changed.map do |index|
145
+ [index - context, 0].max..[index + context, operations.length - 1].min
146
+ end
147
+ merge_adjacent_ranges(ranges)
148
+ end
149
+ private_class_method :hunk_ranges
150
+
151
+ def merge_adjacent_ranges(ranges)
152
+ ranges.each_with_object([]) do |range, merged|
153
+ if merged.last && range.begin <= merged.last.end + 1
154
+ merged[-1] = merged.last.begin..[merged.last.end, range.end].max
155
+ else
156
+ merged << range
157
+ end
158
+ end
159
+ end
160
+ private_class_method :merge_adjacent_ranges
161
+
162
+ def append_hunk(output, entries)
163
+ output << hunk_header(entries)
164
+ entries.each { |entry| append_hunk_entry(output, entry) }
165
+ end
166
+ private_class_method :append_hunk
167
+
168
+ def append_hunk_entry(output, entry)
169
+ marker = { equal: ' ', delete: '-', insert: '+' }.fetch(entry[:type])
170
+ output << marker << entry[:line].b
171
+ output << "\n\\n" unless entry[:line].end_with?("\n")
172
+ end
173
+ private_class_method :append_hunk_entry
174
+
175
+ def hunk_header(entries)
176
+ old_count = entries.count { |entry| entry[:type] != :insert }
177
+ new_count = entries.count { |entry| entry[:type] != :delete }
178
+ old_start = entries.first[:old_line] - (old_count.zero? ? 1 : 0)
179
+ new_start = entries.first[:new_line] - (new_count.zero? ? 1 : 0)
180
+ "@@ -#{range(old_start, old_count)} +#{range(new_start, new_count)} @@\n"
181
+ end
182
+ private_class_method :hunk_header
183
+
184
+ def range(start_line, count)
185
+ count == 1 ? start_line.to_s : "#{start_line},#{count}"
186
+ end
187
+ private_class_method :range
188
+
189
+ def validate_options(path, context)
190
+ if path.to_s.empty? || path.to_s.match?(/[\t\r\n]/)
191
+ raise ArgumentError, 'path must not be empty or contain control separators'
192
+ end
193
+ raise ArgumentError, 'context must be a non-negative integer' unless context.is_a?(Integer) && context >= 0
194
+ end
195
+ private_class_method :validate_options
196
+ end
197
+
198
+ module Diffable
199
+ def to_diff(context: 3, path: @source_file.path)
200
+ UnifiedDiff.unified(@source_file.source, rewrite, path: path, context: context)
201
+ end
202
+ end
203
+ end
204
+
205
+ Astel::Rewriter.include(Astel::Diffable)
data/lib/astel/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Astel
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/astel.rb CHANGED
@@ -7,6 +7,7 @@ module Astel
7
7
 
8
8
  autoload :Location, File.expand_path('astel/location', __dir__)
9
9
  autoload :SourceFile, File.expand_path('astel/source_file', __dir__)
10
+ autoload :NodeSourceText, File.expand_path('astel/node_source_text', __dir__)
10
11
  autoload :NodeExt, File.expand_path('astel/node_ext', __dir__)
11
12
  autoload :NodeType, File.expand_path('astel/node_type', __dir__)
12
13
  autoload :Dispatcher, File.expand_path('astel/dispatcher', __dir__)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: astel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
@@ -55,12 +55,18 @@ files:
55
55
  - lib/astel/node_pattern/matcher_compiler.rb
56
56
  - lib/astel/node_pattern/parser.rb
57
57
  - lib/astel/node_pattern/predicate_compiler.rb
58
+ - lib/astel/node_source_text.rb
58
59
  - lib/astel/node_type.rb
59
60
  - lib/astel/refactor.rb
60
- - lib/astel/refactor/comments.rb
61
- - lib/astel/refactor/formatting.rb
62
61
  - lib/astel/rewriter.rb
62
+ - lib/astel/rewriter/edit_index.rb
63
+ - lib/astel/rewriter/structured.rb
64
+ - lib/astel/rewriter/structured/collections.rb
65
+ - lib/astel/rewriter/structured/comments.rb
66
+ - lib/astel/rewriter/structured/formatting.rb
67
+ - lib/astel/rewriter/structured/source_indenter.rb
63
68
  - lib/astel/source_file.rb
69
+ - lib/astel/unified_diff.rb
64
70
  - lib/astel/version.rb
65
71
  homepage: https://rubygems.org/gems/astel
66
72
  licenses:
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Astel
4
- module Refactor
5
- def leading_comments(node)
6
- attach_comments
7
- node.location.leading_comments
8
- end
9
-
10
- def trailing_comment(node)
11
- attach_comments
12
- node.location.trailing_comments.first
13
- end
14
-
15
- def remove_with_comments(node)
16
- comments = leading_comments(node)
17
- trailing = trailing_comment(node)
18
- start_offset = comments.empty? ? node.location.start_offset : comments.first.location.start_offset
19
- end_offset = trailing ? trailing.location.end_offset : node.location.end_offset
20
- first = @source_file.line_location(start_offset)
21
- last = @source_file.line_location(end_offset - 1)
22
- before = @source_file.source.byteslice(first.start_offset, start_offset - first.start_offset)
23
- after = @source_file.source.byteslice(end_offset, last.end_offset - end_offset)
24
- return remove(start_offset...end_offset) unless before.strip.empty? && after.strip.empty?
25
-
26
- ending = @source_file.line_location_with_newline(end_offset - 1).end_offset
27
- remove(first.start_offset...ending)
28
- end
29
-
30
- private
31
-
32
- def attach_comments
33
- @source_file.attach_comments!
34
- end
35
- end
36
- end