astel 0.1.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
@@ -13,79 +15,38 @@ module Astel
13
15
  end
14
16
  end
15
17
 
18
+ class ValidationError < Error
19
+ attr_reader :result, :parse_errors
20
+
21
+ def initialize(result, parse_errors)
22
+ @result = result
23
+ @parse_errors = parse_errors.freeze
24
+ super("rewritten source contains #{parse_errors.length} syntax error(s)")
25
+ end
26
+ end
27
+
16
28
  Edit = Data.define(:start_offset, :end_offset, :replacement, :sequence) do
17
29
  def insertion?
18
30
  start_offset == end_offset
19
31
  end
20
32
  end
21
33
 
22
- class EditIndex
23
- include Enumerable
24
-
25
- SPLIT_THRESHOLD = 256
26
-
27
- def initialize
28
- @blocks = []
29
- end
30
-
31
- def add(edit)
32
- if @blocks.empty?
33
- @blocks << [edit]
34
- return
35
- end
36
-
37
- block_index = block_index_for(edit)
38
- block = @blocks.fetch(block_index)
39
- edit_index = block.bsearch_index { |existing| ordered_after?(existing, edit) } || block.length
40
- following = block[edit_index] || @blocks[block_index + 1]&.first
41
- return following if following && yield(following)
42
-
43
- preceding = if edit_index.positive?
44
- block[edit_index - 1]
45
- elsif block_index.positive?
46
- @blocks[block_index - 1].last
47
- end
48
- return preceding if preceding && yield(preceding)
49
-
50
- block.insert(edit_index, edit)
51
- split(block_index, block) if block.length > SPLIT_THRESHOLD
52
- nil
53
- end
54
-
55
- def each
56
- return enum_for(__method__) unless block_given?
57
-
58
- @blocks.each do |block|
59
- block.each { |edit| yield edit }
60
- end
61
- end
62
-
63
- private
64
-
65
- def block_index_for(edit)
66
- @blocks.bsearch_index { |block| ordered_after?(block.last, edit) } || @blocks.length - 1
67
- end
68
-
69
- def ordered_after?(existing, edit)
70
- existing.start_offset > edit.start_offset ||
71
- (existing.start_offset == edit.start_offset && existing.end_offset > edit.end_offset) ||
72
- (existing.start_offset == edit.start_offset && existing.end_offset == edit.end_offset &&
73
- existing.sequence > edit.sequence)
74
- end
34
+ attr_reader :source_file
75
35
 
76
- def split(block_index, block)
77
- middle = block.length / 2
78
- @blocks.insert(block_index + 1, block.slice!(middle, block.length - middle))
36
+ def initialize(source_file, duplicate_insertions: :accept)
37
+ unless %i[accept raise].include?(duplicate_insertions)
38
+ raise ArgumentError, 'duplicate_insertions must be :accept or :raise'
79
39
  end
80
- end
81
40
 
82
- def initialize(source_file)
83
41
  @source_file = source_file
42
+ @duplicate_insertions = duplicate_insertions
84
43
  @edits = []
85
44
  @edits_snapshot = nil
86
45
  @edit_index = EditIndex.new
87
46
  @sequence = 0
88
47
  @result_size_delta = 0
48
+ @transaction_staging = false
49
+ @transaction_failed = false
89
50
  end
90
51
 
91
52
  def edits
@@ -111,7 +72,44 @@ module Astel
111
72
  replace(location, '')
112
73
  end
113
74
 
114
- def rewrite
75
+ def wrap(location, before, after)
76
+ transaction(raise_on_conflict: true) do |rewriter|
77
+ rewriter.insert_before(location, before)
78
+ rewriter.insert_after(location, after)
79
+ end
80
+ self
81
+ end
82
+
83
+ def merge!(other)
84
+ validated_edits_from(other).each do |edit|
85
+ add_edit(edit.start_offset, edit.end_offset, edit.replacement)
86
+ end
87
+ self
88
+ end
89
+
90
+ def transaction(raise_on_conflict: false)
91
+ staging = transaction_staging
92
+ yield staging
93
+ return transaction_failed if transaction_failed?(staging)
94
+
95
+ merge!(staging)
96
+ true
97
+ rescue ConflictError
98
+ @transaction_failed = true if @transaction_staging
99
+ raise if raise_on_conflict
100
+
101
+ false
102
+ end
103
+
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
115
113
  source = @source_file.source
116
114
  result = String.new(capacity: source.bytesize + @result_size_delta, encoding: source.encoding)
117
115
  cursor = 0
@@ -126,16 +124,37 @@ module Astel
126
124
  result
127
125
  end
128
126
 
129
- private
130
-
131
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)
132
134
  validate_offsets(start_offset, end_offset)
133
- edit = Edit.new(
135
+ Edit.new(
134
136
  start_offset: start_offset,
135
137
  end_offset: end_offset,
136
- replacement: replacement.to_s.dup.freeze,
138
+ replacement: validated_replacement(replacement),
137
139
  sequence: @sequence
138
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)
139
158
  conflict = @edit_index.add(edit) { |existing| conflict?(edit, existing) }
140
159
  raise ConflictError.new(edit, conflict) if conflict
141
160
 
@@ -143,7 +162,34 @@ module Astel
143
162
  @result_size_delta += edit.replacement.bytesize - (edit.end_offset - edit.start_offset)
144
163
  @edits << edit
145
164
  @edits_snapshot = nil
146
- 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)
147
193
  end
148
194
 
149
195
  def offsets(location)
@@ -155,17 +201,42 @@ module Astel
155
201
  end
156
202
 
157
203
  def validate_offsets(start_offset, end_offset)
158
- valid = start_offset.is_a?(Integer) && end_offset.is_a?(Integer) &&
159
- start_offset >= 0 && start_offset <= end_offset && end_offset <= @source_file.source.bytesize
160
- 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
161
210
  end
162
211
 
163
212
  def conflict?(left, right)
164
- return left.start_offset == right.start_offset if left.insertion? && right.insertion?
165
- return left.start_offset > right.start_offset && left.start_offset < right.end_offset if left.insertion?
166
- 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?
167
216
 
168
217
  left.start_offset < right.end_offset && right.start_offset < left.end_offset
169
218
  end
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
+
229
+ def transaction_failed
230
+ @transaction_failed = true if @transaction_staging
231
+ false
232
+ end
233
+
234
+ def validate_result(result, validate)
235
+ return unless validate
236
+ raise ArgumentError, 'validate must be false or :parse' unless validate == :parse
237
+
238
+ parsed = SourceFile.from_string(result, path: @source_file.path, version: @source_file.version)
239
+ raise ValidationError.new(result, parsed.errors) unless parsed.valid?
240
+ end
170
241
  end
171
242
  end
@@ -4,7 +4,7 @@ require 'prism'
4
4
 
5
5
  module Astel
6
6
  class SourceFile
7
- attr_reader :path, :source, :ast, :comments, :errors, :parse_result
7
+ attr_reader :path, :source, :ast, :comments, :errors, :parse_result, :version
8
8
 
9
9
  def self.parse(path:, version: nil)
10
10
  from_string(File.binread(path), path: path, version: version)
@@ -16,10 +16,8 @@ module Astel
16
16
 
17
17
  def initialize(code, path:, version: nil)
18
18
  @path = path.to_s.freeze
19
- raw_source = code.dup.freeze
20
- options = { filepath: @path }
21
- options[:version] = normalize_version(version) if version
22
- @parse_result = Prism.parse(raw_source, **options)
19
+ @version = normalize_version(version).freeze if version
20
+ @parse_result = parse_source(code, path: @path, version: @version)
23
21
  @source = @parse_result.source.source.freeze
24
22
  @ast = @parse_result.value
25
23
  @comments = @parse_result.comments.freeze
@@ -47,20 +45,123 @@ module Astel
47
45
  start_line: 1,
48
46
  start_column: 0,
49
47
  end_line: 1,
50
- end_column: line.delete_suffix("\n").delete_suffix("\r").length
48
+ end_column: line.delete_suffix("\n").delete_suffix("\r").bytesize
51
49
  )
52
50
  end
53
51
 
54
52
  def magic_comment?(name)
55
- pattern = /\A#\s*#{Regexp.escape(name.to_s)}\s*:\s*true\b/
56
- lines.first(2).any? do |line|
57
- candidate = line.start_with?("\uFEFF") ? line.delete_prefix("\uFEFF") : line
58
- candidate.match?(pattern)
53
+ @parse_result.magic_comments.any? do |comment|
54
+ comment.key == name.to_s && comment.value == 'true' && effective_magic_comment?(comment)
59
55
  end
60
56
  end
61
57
 
58
+ def attach_comments!
59
+ return self if @comments_attached
60
+
61
+ @parse_result.attach_comments!
62
+ @comments_attached = true
63
+ self
64
+ end
65
+
66
+ def line_at(byte_offset)
67
+ validate_offset!(byte_offset)
68
+ @parse_result.source.line(byte_offset)
69
+ end
70
+
71
+ def column_at(byte_offset)
72
+ validate_offset!(byte_offset)
73
+ @parse_result.source.column(byte_offset)
74
+ end
75
+
76
+ def line_start(byte_offset)
77
+ validate_offset!(byte_offset)
78
+ @parse_result.source.line_start(byte_offset)
79
+ end
80
+
81
+ def line_end(byte_offset)
82
+ validate_offset!(byte_offset)
83
+ start = line_start(byte_offset)
84
+ ending = @parse_result.source.line_end(byte_offset)
85
+ ending -= 1 if ending > start && source.getbyte(ending - 1) == 10
86
+ ending -= 1 if ending > start && source.getbyte(ending - 1) == 13
87
+ ending
88
+ end
89
+
90
+ def line_location(byte_offset)
91
+ location(line_start(byte_offset), line_end(byte_offset))
92
+ end
93
+
94
+ def line_location_with_newline(byte_offset)
95
+ validate_offset!(byte_offset)
96
+ location(line_start(byte_offset), @parse_result.source.line_end(byte_offset))
97
+ end
98
+
99
+ def slice(location)
100
+ source.byteslice(location.start_offset, location.end_offset - location.start_offset)
101
+ end
102
+
103
+ def newline
104
+ @newline ||= crlf_dominant? ? "\r\n" : "\n"
105
+ end
106
+
107
+ def indentation_at(byte_offset)
108
+ start_offset = line_start(byte_offset)
109
+ source.byteslice(start_offset, line_end(byte_offset) - start_offset)[/\A[ \t]*/]
110
+ end
111
+
112
+ def indent_unit
113
+ @indent_unit ||= detected_indent_unit
114
+ end
115
+
62
116
  private
63
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
+
147
+ def location(start_offset, end_offset)
148
+ prism_source = @parse_result.source
149
+ Location.new(
150
+ start_offset: start_offset,
151
+ end_offset: end_offset,
152
+ start_line: prism_source.line(start_offset),
153
+ start_column: prism_source.column(start_offset),
154
+ end_line: prism_source.line(end_offset),
155
+ end_column: prism_source.column(end_offset)
156
+ )
157
+ end
158
+
159
+ def validate_offset!(byte_offset)
160
+ unless byte_offset.is_a?(Integer) && byte_offset.between?(0, source.bytesize)
161
+ raise RangeError, 'offset is outside the source'
162
+ end
163
+ end
164
+
64
165
  def normalize_version(version)
65
166
  version.match?(/\A\d+\.\d+\z/) ? "#{version}.0" : version
66
167
  end
@@ -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.1.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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
@@ -43,7 +43,10 @@ files:
43
43
  - benchmark/dispatch_bench.rb
44
44
  - benchmark/node_pattern_bench.rb
45
45
  - benchmark/rewriter_bench.rb
46
+ - docs/codemod-guide.md
47
+ - docs/refactor.md
46
48
  - lib/astel.rb
49
+ - lib/astel/diff.rb
47
50
  - lib/astel/dispatcher.rb
48
51
  - lib/astel/location.rb
49
52
  - lib/astel/node_ext.rb
@@ -52,9 +55,18 @@ files:
52
55
  - lib/astel/node_pattern/matcher_compiler.rb
53
56
  - lib/astel/node_pattern/parser.rb
54
57
  - lib/astel/node_pattern/predicate_compiler.rb
58
+ - lib/astel/node_source_text.rb
55
59
  - lib/astel/node_type.rb
60
+ - lib/astel/refactor.rb
56
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
57
68
  - lib/astel/source_file.rb
69
+ - lib/astel/unified_diff.rb
58
70
  - lib/astel/version.rb
59
71
  homepage: https://rubygems.org/gems/astel
60
72
  licenses:
@@ -75,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
87
  - !ruby/object:Gem::Version
76
88
  version: '0'
77
89
  requirements: []
78
- rubygems_version: 4.0.6
90
+ rubygems_version: 3.6.9
79
91
  specification_version: 4
80
92
  summary: Fast Prism AST traversal, matching, and source rewriting
81
93
  test_files: []