node_mutation 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
+ SHA256:
3
+ metadata.gz: f708082625b70a33be17a520d51fab328979f7640f6125f7cec8a4f8c0fa348c
4
+ data.tar.gz: 71219747c6be37cb7282658c5dc6087f33ac63ed195de48ab29f2aa79347ea85
5
+ SHA512:
6
+ metadata.gz: 74f6fdb99bec0cd69ae0310ebc728661981ab7e7c41624e2409238426095cfec7576f1905d49f7e9d25393723b9c43dbe6ebf9c72730df38a19308e68682fc49
7
+ data.tar.gz: d2323af8c25e3a69779848be92594075970eb3c22b6c26fc411bc95710f51c9d37881a32c4196b3330eb28544d05dcbf72ef45521bce2e61025ea1ab5681eaae
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # NodeMutation
2
+
3
+ ## 1.0.0 (2022-07-01)
4
+
5
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in node_mutation.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "parser"
13
+ gem "parser_node_ext"
14
+ gem "guard"
15
+ gem "guard-rspec"
16
+ gem "fakefs", require: 'fakefs/safe'
17
+ gem "pp"
data/Gemfile.lock ADDED
@@ -0,0 +1,96 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ node_mutation (1.0.0)
5
+ activesupport
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (7.0.3)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 1.6, < 2)
13
+ minitest (>= 5.1)
14
+ tzinfo (~> 2.0)
15
+ ast (2.4.2)
16
+ coderay (1.1.3)
17
+ concurrent-ruby (1.1.10)
18
+ diff-lcs (1.5.0)
19
+ fakefs (1.8.0)
20
+ ffi (1.15.5)
21
+ formatador (1.1.0)
22
+ guard (2.18.0)
23
+ formatador (>= 0.2.4)
24
+ listen (>= 2.7, < 4.0)
25
+ lumberjack (>= 1.0.12, < 2.0)
26
+ nenv (~> 0.1)
27
+ notiffany (~> 0.0)
28
+ pry (>= 0.13.0)
29
+ shellany (~> 0.0)
30
+ thor (>= 0.18.1)
31
+ guard-compat (1.2.1)
32
+ guard-rspec (4.7.3)
33
+ guard (~> 2.1)
34
+ guard-compat (~> 1.1)
35
+ rspec (>= 2.99.0, < 4.0)
36
+ i18n (1.10.0)
37
+ concurrent-ruby (~> 1.0)
38
+ listen (3.7.1)
39
+ rb-fsevent (~> 0.10, >= 0.10.3)
40
+ rb-inotify (~> 0.9, >= 0.9.10)
41
+ lumberjack (1.2.8)
42
+ method_source (1.0.0)
43
+ minitest (5.16.1)
44
+ nenv (0.3.0)
45
+ notiffany (0.1.3)
46
+ nenv (~> 0.1)
47
+ shellany (~> 0.0)
48
+ parser (3.1.2.0)
49
+ ast (~> 2.4.1)
50
+ parser_node_ext (0.2.0)
51
+ parser
52
+ pp (0.3.0)
53
+ prettyprint
54
+ prettyprint (0.1.1)
55
+ pry (0.14.1)
56
+ coderay (~> 1.1)
57
+ method_source (~> 1.0)
58
+ rake (13.0.6)
59
+ rb-fsevent (0.11.1)
60
+ rb-inotify (0.10.1)
61
+ ffi (~> 1.0)
62
+ rspec (3.11.0)
63
+ rspec-core (~> 3.11.0)
64
+ rspec-expectations (~> 3.11.0)
65
+ rspec-mocks (~> 3.11.0)
66
+ rspec-core (3.11.0)
67
+ rspec-support (~> 3.11.0)
68
+ rspec-expectations (3.11.0)
69
+ diff-lcs (>= 1.2.0, < 2.0)
70
+ rspec-support (~> 3.11.0)
71
+ rspec-mocks (3.11.1)
72
+ diff-lcs (>= 1.2.0, < 2.0)
73
+ rspec-support (~> 3.11.0)
74
+ rspec-support (3.11.0)
75
+ shellany (0.0.1)
76
+ thor (1.2.1)
77
+ tzinfo (2.0.4)
78
+ concurrent-ruby (~> 1.0)
79
+
80
+ PLATFORMS
81
+ x86_64-darwin-21
82
+ x86_64-linux
83
+
84
+ DEPENDENCIES
85
+ fakefs
86
+ guard
87
+ guard-rspec
88
+ node_mutation!
89
+ parser
90
+ parser_node_ext
91
+ pp
92
+ rake (~> 13.0)
93
+ rspec (~> 3.0)
94
+
95
+ BUNDLED WITH
96
+ 2.3.7
data/Guardfile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
6
+ watch('spec/spec_helper.rb') { "spec" }
7
+ end
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # NodeMutation
2
+
3
+ NodeMutation provides a set of APIs to rewrite node source code.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'node_mutation'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install node_mutation
20
+
21
+ ## Usage
22
+
23
+ 1. initialize the NodeMutation instance:
24
+
25
+ ```ruby
26
+ require 'node_mutation'
27
+
28
+ mutation = NodeMutation.new(file_path, source)
29
+ ```
30
+
31
+ 2. call the rewrite apis:
32
+
33
+ ```ruby
34
+ # append the code to the current node.
35
+ mutation.append node, 'include FactoryGirl::Syntax::Methods'
36
+ # delete source code of the child ast node
37
+ mutation.delete node, :dot, :message, and_comma: true
38
+ # insert code to the ast node.
39
+ mutation.insert node, 'URI.', at: 'beginning'
40
+ # insert code next to the ast node.
41
+ mutation.insert_after node, '{{arguments.first}}.include FactoryGirl::Syntax::Methods'
42
+ # prepend code to the ast node.
43
+ mutation.prepend node, '{{arguments.first}}.include FactoryGirl::Syntax::Methods'
44
+ # remove source code of the ast node
45
+ mutation.remove(node: Node)
46
+ # replace child node of the ast node with new code
47
+ mutation.replace node, :message, with: 'test'
48
+ # replace the ast node with new code
49
+ mutation.replace_with node, 'create {{arguments}}'
50
+ # wrap node within a block, class or module
51
+ mutation.wrap node, with: 'module Foo'
52
+ ```
53
+
54
+ 3. process actions and write the new source code to file:
55
+
56
+ ```ruby
57
+ mutation.process
58
+ ```
59
+
60
+ ## Write Adapter
61
+
62
+ Different parsers, like parse and ripper, will generate different AST nodes, to make NodeMutation work for them all,
63
+ we define an [Adapter](https://github.com/xinminlabs/node-mutation-ruby/blob/main/lib/node_mutation/adapter.rb) interface,
64
+ if you implement the Adapter interface, you can set it as NodeMutation's adapter.
65
+
66
+ ```typescript
67
+ NodeMutation.configure({ adapter: ParserAdapter.new })
68
+ ```
69
+
70
+ ## Development
71
+
72
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
73
+
74
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
75
+
76
+ ## Contributing
77
+
78
+ Bug reports and pull requests are welcome on GitHub at https://github.com/xinminlabs/node_mutation.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/code.rb ADDED
@@ -0,0 +1,4 @@
1
+ class Foobar
2
+ def foo; end
3
+ def bar; end
4
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # AppendAction appends code to the bottom of node body.
4
+ class NodeMutation::AppendAction < NodeMutation::Action
5
+ private
6
+
7
+ END_LENGTH = "\nend".length
8
+
9
+ # Calculate the begin the end positions.
10
+ def calculate_position
11
+ @start = NodeMutation.adapter.get_end(@node) - NodeMutation.adapter.get_start_loc(@node).column - END_LENGTH
12
+ @end = @start
13
+ end
14
+
15
+ # Indent of the node.
16
+ #
17
+ # @param node [Parser::AST::Node]
18
+ # @return [String] n times whitesphace
19
+ def indent(node)
20
+ ' ' * (NodeMutation.adapter.get_start_loc(node).column + DEFAULT_INDENT)
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DeleteAction deletes child nodes.
4
+ class NodeMutation::DeleteAction < NodeMutation::Action
5
+ # Initialize a DeleteAction.
6
+ #
7
+ # @param node [Node]
8
+ # @param selectors [Array<Symbol, String>] used to select child nodes
9
+ # @option and_comma [Boolean] delete extra comma.
10
+ def initialize(node, *selectors, and_comma: false)
11
+ super(node, nil)
12
+ @selectors = selectors
13
+ @and_comma = and_comma
14
+ end
15
+
16
+ # The rewritten code, always empty string.
17
+ def new_code
18
+ ''
19
+ end
20
+
21
+ private
22
+
23
+ # Calculate the begin and end positions.
24
+ def calculate_position
25
+ @start = @selectors.map { |selector| NodeMutation.adapter.child_node_range(@node, selector) }
26
+ .compact.map(&:start).min
27
+ @end = @selectors.map { |selector| NodeMutation.adapter.child_node_range(@node, selector) }
28
+ .compact.map(&:end).max
29
+ squeeze_spaces
30
+ remove_comma if @and_comma
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # InsertAction to add code to the node.
4
+ class NodeMutation::InsertAction < NodeMutation::Action
5
+ # Initialize an InsertAction.
6
+ #
7
+ # @param ndoe [Node]
8
+ # @param code [String] to be inserted
9
+ # @param at [String] position to insert, beginning or end
10
+ # @param to [<nil|String>] name of child node
11
+ def initialize(node, code, at: 'end', to: nil)
12
+ super(node, code)
13
+ @at = at
14
+ @to = to
15
+ end
16
+
17
+ # The rewritten source code.
18
+ #
19
+ # @return [String] rewritten code.
20
+ def new_code
21
+ rewritten_source
22
+ end
23
+
24
+ private
25
+
26
+ # Calculate the begin and end positions.
27
+ def calculate_position
28
+ @start =
29
+ if @at == 'end'
30
+ if @to
31
+ NodeMutation.adapter.child_node_range(@node, @to).end
32
+ else
33
+ NodeMutation.adapter.get_end(@node)
34
+ end
35
+ else
36
+ if @to
37
+ NodeMutation.adapter.child_node_range(@node, @to).start
38
+ else
39
+ NodeMutation.adapter.get_start(@node)
40
+ end
41
+ end
42
+ @end = @start
43
+ end
44
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # InsertAfterAction to insert code next to the node.
4
+ class NodeMutation::InsertAfterAction < NodeMutation::Action
5
+ private
6
+
7
+ # Calculate the begin and end positions.
8
+ def calculate_position
9
+ @start = NodeMutation.adapter.get_end(@node)
10
+ @end = @start
11
+ end
12
+
13
+ # Indent of the node.
14
+ #
15
+ # @param node [Parser::AST::Node]
16
+ # @return [Integer] indent size
17
+ def indent(node)
18
+ ' ' * NodeMutation.adapter.get_start_loc(node).column
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PrependAction to prepend code to the top of node body.
4
+ class NodeMutation::PrependAction < NodeMutation::Action
5
+ private
6
+
7
+ DO_LENGTH = ' do'.length
8
+
9
+ # Calculate the begin and end positions.
10
+ def calculate_position
11
+ node_start = NodeMutation.adapter.get_start(@node)
12
+ node_source = NodeMutation.adapter.get_source(@node)
13
+ first_line = node_source.split("\n").first
14
+ @start = first_line.end_with?("do") ? node_start + first_line.index("do") + "do".length : node_start + first_line.length
15
+ @end = @start
16
+ end
17
+
18
+ # Indent of the node.
19
+ #
20
+ # @param node [Parser::AST::Node]
21
+ # @return [String] n times whitesphace
22
+ def indent(node)
23
+ ' ' * (NodeMutation.adapter.get_start_loc(node).column + DEFAULT_INDENT)
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RemoveAction to remove current node.
4
+ class NodeMutation::RemoveAction < NodeMutation::Action
5
+ # Initialize a RemoveAction.
6
+ #
7
+ # @param node [Node]
8
+ # @param options [Hash] options.
9
+ # @option and_comma [Boolean] delete extra comma.
10
+ def initialize(node, and_comma: false)
11
+ super(node, nil)
12
+ @and_comma = and_comma
13
+ end
14
+
15
+ # The rewritten code, always empty string.
16
+ def new_code
17
+ ''
18
+ end
19
+
20
+ private
21
+
22
+ # Calculate the begin the end positions.
23
+ def calculate_position
24
+ if take_whole_line?
25
+ @start = start_index
26
+ @end = end_index
27
+ squeeze_lines
28
+ else
29
+ @start = NodeMutation.adapter.get_start(@node)
30
+ @end = NodeMutation.adapter.get_end(@node)
31
+ squeeze_spaces
32
+ remove_comma if @and_command
33
+ end
34
+ end
35
+
36
+ # Check if the source code of current node takes the whole line.
37
+ #
38
+ # @return [Boolean]
39
+ def take_whole_line?
40
+ NodeMutation.adapter.get_source(@node) == file_source[start_index...end_index].strip
41
+ end
42
+
43
+ # Get the start position of the line
44
+ def start_index
45
+ index = file_source[0..NodeMutation.adapter.get_start(@node)].rindex("\n")
46
+ index ? index + "\n".length : NodeMutation.adapter.get_start(@node)
47
+ end
48
+
49
+ # Get the end position of the line
50
+ def end_index
51
+ index = file_source[NodeMutation.adapter.get_end(@node)..-1].index("\n")
52
+ index ? NodeMutation.adapter.get_end(@node) + index + "\n".length : NodeMutation.adapter.get_end(@node)
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ReplaceAction to replace child node with code.
4
+ class NodeMutation::ReplaceAction < NodeMutation::Action
5
+ # Initailize a ReplaceAction.
6
+ #
7
+ # @param node [Node]
8
+ # @param selectors [Array<Symbol|String>] used to select child nodes
9
+ # @param with [String] the new code
10
+ def initialize(node, *selectors, with:)
11
+ super(node, with)
12
+ @selectors = selectors
13
+ end
14
+
15
+ # The rewritten source code.
16
+ #
17
+ # @return [String] rewritten code.
18
+ def new_code
19
+ rewritten_source
20
+ end
21
+
22
+ private
23
+
24
+ # Calculate the begin the end positions.
25
+ def calculate_position
26
+ @start = @selectors.map { |selector| NodeMutation.adapter.child_node_range(@node, selector).start }.min
27
+ @end = @selectors.map { |selector| NodeMutation.adapter.child_node_range(@node, selector).end }.max
28
+ end
29
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ReplaceWithAction to replace code.
4
+ class NodeMutation::ReplaceWithAction < NodeMutation::Action
5
+ # The rewritten source code with proper indent.
6
+ #
7
+ # @return [String] rewritten code.
8
+ def new_code
9
+ if rewritten_source.include?("\n")
10
+ new_code = []
11
+ rewritten_source.split("\n").each_with_index do |line, index|
12
+ new_code << (index == 0 ? line : indent + line)
13
+ end
14
+ new_code.join("\n")
15
+ else
16
+ rewritten_source
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # Calculate the begin the end positions.
23
+ def calculate_position
24
+ @start = NodeMutation.adapter.get_start(@node)
25
+ @end = NodeMutation.adapter.get_end(@node)
26
+ end
27
+
28
+ # Indent of the node
29
+ #
30
+ # @return [String] n times whitesphace
31
+ def indent
32
+ ' ' * NodeMutation.adapter.get_start_loc(@node).column
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WrapAction to wrap node within a block, class or module.
4
+ #
5
+ # Note: if WrapAction is conflicted with another action (begin_pos and end_pos are overlapped),
6
+ # we have to put those 2 actions into 2 within_file scopes.
7
+ class NodeMutation::WrapAction < NodeMutation::Action
8
+ # Initialize a WrapAction.
9
+ #
10
+ # @param node [Node]
11
+ # @param with [String] new code to wrap
12
+ def initialize(node, with:)
13
+ super(node, with)
14
+ @indent = NodeMutation.adapter.get_start_loc(@node).column
15
+ end
16
+
17
+ # The rewritten source code.
18
+ #
19
+ # @return [String] rewritten code.
20
+ def new_code
21
+ "#{@code}\n#{' ' * @indent}" +
22
+ NodeMutation.adapter.get_source(@node).split("\n").map { |line| " #{line}" }
23
+ .join("\n") +
24
+ "\n#{' ' * @indent}end"
25
+ end
26
+
27
+ private
28
+
29
+ # Calculate the begin the end positions.
30
+ def calculate_position
31
+ @start = NodeMutation.adapter.get_start(@node)
32
+ @end = NodeMutation.adapter.get_end(@node)
33
+ end
34
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Action defines rewriter action, insert, replace or delete code.
4
+ class NodeMutation::Action
5
+ DEFAULT_INDENT = 2
6
+
7
+ # @!attribute [r] start
8
+ # @return [Integer] start position
9
+ # @!attribute [r] end
10
+ # @return [Integer] end position
11
+ attr_reader :start, :end
12
+
13
+ # Initialize an action.
14
+ #
15
+ # @param node [Node]
16
+ # @param code [String] new code to insert, replace or delete.
17
+ def initialize(node, code)
18
+ @node = node
19
+ @code = code
20
+ end
21
+
22
+ # Calculate begin and end positions, and return self.
23
+ #
24
+ # @return [NodeMutation::Action] self
25
+ def process
26
+ calculate_position
27
+ self
28
+ end
29
+
30
+ # The rewritten source code with proper indent.
31
+ #
32
+ # @return [String] rewritten code.
33
+ def new_code
34
+ if rewritten_source.split("\n").length > 1
35
+ "\n\n" + rewritten_source.split("\n").map { |line| indent(@node) + line }.join("\n")
36
+ else
37
+ "\n" + indent(@node) + rewritten_source
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ # Calculate the begin the end positions.
44
+ #
45
+ # @abstract
46
+ def calculate_position
47
+ raise NotImplementedError, 'must be implemented by subclasses'
48
+ end
49
+
50
+ # The rewritten source code.
51
+ #
52
+ # @return [String] rewritten source code.
53
+ def rewritten_source
54
+ @rewritten_source ||= NodeMutation.adapter.rewritten_source(@node, @code)
55
+ end
56
+
57
+ # Squeeze spaces from source code.
58
+ def squeeze_spaces
59
+ if file_source[@start - 1] == ' ' && file_source[@end] == ' '
60
+ @start -= 1
61
+ end
62
+ end
63
+
64
+ # Squeeze empty lines from source code.
65
+ def squeeze_lines
66
+ lines = file_source.split("\n")
67
+ begin_line = NodeMutation.adapter.get_start_loc(@node).line
68
+ end_line = NodeMutation.adapter.get_end_loc(@node).line
69
+ before_line_is_blank = begin_line == 1 || lines[begin_line - 2] == ''
70
+ after_line_is_blank = lines[end_line] == ''
71
+
72
+ if lines.length > 1 && before_line_is_blank && after_line_is_blank
73
+ @end_pos += "\n".length
74
+ end
75
+ end
76
+
77
+ # Remove unused comma.
78
+ # e.g. `foobar(foo, bar)`, if we remove `foo`, the comma should also be removed,
79
+ # the code should be changed to `foobar(bar)`.
80
+ def remove_comma
81
+ if ',' == file_source[@start - 1]
82
+ @start -= 1
83
+ elsif ', ' == file_source[@start - 2, 2]
84
+ @start -= 2
85
+ elsif ', ' == file_source[@end, 2]
86
+ @end += 2
87
+ elsif ',' == file_source[@end]
88
+ @end += 1
89
+ end
90
+ end
91
+
92
+ # Return file source.
93
+ #
94
+ # @return [String]
95
+ def file_source
96
+ @file_source ||= NodeMutation.adapter.file_content(@node)
97
+ end
98
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NodeMutation::Adapter
4
+ # Get source code of the ast node
5
+ # @param node [Node] ast node
6
+ # @return [String] source code
7
+ def get_source(node)
8
+ raise NotImplementedError, "get_source is not implemented"
9
+ end
10
+
11
+ # Replace the child node selector with child node source code
12
+ # @param node [Node] ast node
13
+ # @param code [String] code with child node selector, e.g. `Boolean({{expression.operand.operand}})`
14
+ # @return [String] code with source code of child node selector,
15
+ # @example # source code of ast node is `!!foobar`, code is `Boolean({{expression.operand.operand}})`,
16
+ # it will return `Boolean(foobar)`
17
+ def rewritten_source(node, code)
18
+ raise NotImplementedError, "rewritten_source is not implemented"
19
+ end
20
+
21
+ # The file content of the ast node file
22
+ # @param node [Node] ast node
23
+ # @return file content
24
+ def file_content(node)
25
+ raise NotImplementedError, "file_content is not implemented"
26
+ end
27
+
28
+ # Get the start/end range of the child node
29
+ # @param node [Node] ast node
30
+ # @param child_name [String] child name selector
31
+ # @return [{ start: Number, end: Number }] child node range
32
+ def child_node_range(node, child_name)
33
+ raise NotImplementedError, "child_node_range is not implemented"
34
+ end
35
+
36
+ # Get start position of ast node
37
+ # @param node [Node] ast node
38
+ # @return [Number] start position
39
+ def get_start(node)
40
+ raise NotImplementedError, "get_start is not implemented"
41
+ end
42
+
43
+ # Get end position of ast node
44
+ # @param node [Node] ast node
45
+ # @return [Number] end position
46
+ def get_end(node)
47
+ raise NotImplementedError, "get_end is not implemented"
48
+ end
49
+
50
+ # Get start location of ast node
51
+ # @param node [Node] ast node
52
+ # @return [{ line: Number, column: Number }] start location
53
+ def get_start_loc(node)
54
+ raise NotImplementedError, "get_start_loc is not implemented"
55
+ end
56
+
57
+ # Get end location of ast node
58
+ # @param node [Node] ast node
59
+ # @return [{ line: Number, column: Number }] end location
60
+ def get_end_loc(node)
61
+ raise NotImplementedError, "get_end_loc is not implemented"
62
+ end
63
+
64
+ # Get indent of ast node
65
+ # @param node [Node] ast node
66
+ # @return [Number] indent
67
+ def get_indent(node)
68
+ raise NotImplementedError, "get_indent is not implemented"
69
+ end
70
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NodeMutation::ParserAdapter < NodeMutation::Adapter
4
+ def get_source(node)
5
+ node.loc.expression.source
6
+ end
7
+
8
+ def rewritten_source(node, code)
9
+ code.gsub(/{{(.*?)}}/m) do
10
+ old_code = Regexp.last_match(1)
11
+ if node.respond_to?(old_code.split('.').first)
12
+ evaluated = child_node_by_name(node, old_code)
13
+ case evaluated
14
+ when Parser::AST::Node
15
+ if evaluated.type == :args
16
+ evaluated.loc.expression.source[1...-1]
17
+ else
18
+ evaluated.loc.expression.source
19
+ end
20
+ when Array
21
+ if evaluated.size > 0
22
+ file_source = file_content(evaluated.first)
23
+ source = file_source[evaluated.first.loc.expression.begin_pos...evaluated.last.loc.expression.end_pos]
24
+ lines = source.split "\n"
25
+ lines_count = lines.length
26
+ if lines_count > 1 && lines_count == evaluated.size
27
+ new_code = []
28
+ lines.each_with_index { |line, index|
29
+ new_code << (index == 0 ? line : line[evaluated.first.indent - 2..-1])
30
+ }
31
+ new_code.join("\n")
32
+ else
33
+ source
34
+ end
35
+ end
36
+ when String, Symbol, Integer, Float
37
+ evaluated
38
+ when NilClass
39
+ 'nil'
40
+ else
41
+ raise Synvert::Core::MethodNotSupported, "rewritten_source is not handled for #{evaluated.inspect}"
42
+ end
43
+ else
44
+ "{{#{old_code}}}"
45
+ end
46
+ end
47
+ end
48
+
49
+ def file_content(node)
50
+ node.loc.expression.source_buffer.source
51
+ end
52
+
53
+ def child_node_range(node, child_name)
54
+ if node.is_a?(Array)
55
+ direct_child_name, nested_child_name = child_name.split('.', 2)
56
+ child_node = direct_child_name =~ /\A\d+\z/ ? node[direct_child_name.to_i - 1] : node.send(direct_child_name)
57
+ if nested_child_name
58
+ return child_node_range(child_node, nested_child_name)
59
+ elsif child_node
60
+ return OpenStruct.new(
61
+ start: child_node.loc.expression.begin_pos,
62
+ end: child_node.loc.expression.end_pos
63
+ )
64
+ else
65
+ raise MethodNotSupported, "child_node_range is not handled for #{get_source(node)}, child_name: #{child_name}"
66
+ end
67
+ end
68
+
69
+ case [node.type, child_name.to_sym]
70
+ when %i[block pipes], %i[def parentheses], %i[defs parentheses]
71
+ OpenStruct.new(
72
+ start: node.arguments.first.loc.expression.begin_pos - 1,
73
+ end: node.arguments.last.loc.expression.end_pos + 1
74
+ )
75
+ when %i[block arguments], %i[def arguments], %i[defs arguments]
76
+ OpenStruct.new(
77
+ start: node.arguments.first.loc.expression.begin_pos,
78
+ end: node.arguments.last.loc.expression.end_pos
79
+ )
80
+ when %i[class name], %i[def name], %i[defs name]
81
+ OpenStruct.new(start: node.loc.name.begin_pos, end: node.loc.name.end_pos)
82
+ when %i[defs dot]
83
+ OpenStruct.new(start: node.loc.operator.begin_pos, end: node.loc.operator.end_pos) if node.loc.operator
84
+ when %i[defs self]
85
+ OpenStruct.new(start: node.loc.operator.begin_pos - 'self'.length, end: node.loc.operator.begin_pos)
86
+ when %i[send dot], %i[csend dot]
87
+ OpenStruct.new(start: node.loc.dot.begin_pos, end: node.loc.dot.end_pos) if node.loc.dot
88
+ when %i[send message], %i[csend message]
89
+ if node.loc.operator
90
+ OpenStruct.new(start: node.loc.selector.begin_pos, end: node.loc.operator.end_pos)
91
+ else
92
+ OpenStruct.new(start: node.loc.selector.begin_pos, end: node.loc.selector.end_pos)
93
+ end
94
+ when %i[send parentheses], %i[csend parentheses]
95
+ if node.loc.begin && node.loc.end
96
+ OpenStruct.new(start: node.loc.begin.begin_pos, end: node.loc.end.end_pos)
97
+ end
98
+ else
99
+ direct_child_name, nested_child_name = child_name.to_s.split('.', 2)
100
+ if node.respond_to?(direct_child_name)
101
+ child_node = node.send(direct_child_name)
102
+
103
+ return child_node_range(child_node, nested_child_name) if nested_child_name
104
+
105
+ return nil if child_node.nil?
106
+
107
+ if child_node.is_a?(Parser::AST::Node)
108
+ return(
109
+ OpenStruct.new(
110
+ start: child_node.loc.expression.begin_pos,
111
+ end: child_node.loc.expression.end_pos
112
+ )
113
+ )
114
+ end
115
+
116
+ # arguments
117
+ return nil if child_node.empty?
118
+
119
+ return(
120
+ OpenStruct.new(
121
+ start: child_node.first.loc.expression.begin_pos,
122
+ end: child_node.last.loc.expression.end_pos
123
+ )
124
+ )
125
+ end
126
+ end
127
+ end
128
+
129
+ def get_start(node)
130
+ node.loc.expression.begin_pos
131
+ end
132
+
133
+ def get_end(node)
134
+ node.loc.expression.end_pos
135
+ end
136
+
137
+ def get_start_loc(node)
138
+ begin_loc = node.loc.expression.begin
139
+ OpenStruct.new(line: begin_loc.line, column: begin_loc.column)
140
+ end
141
+
142
+ def get_end_loc(node)
143
+ end_loc = node.loc.expression.end
144
+ OpenStruct.new(line: end_loc.line, column: end_loc.column)
145
+ end
146
+
147
+ def get_indent(node)
148
+ file_content(node).split("\n")[get_start_loc(node).line - 1][/\A */].size
149
+ end
150
+
151
+ private
152
+
153
+ def child_node_by_name(node, child_name)
154
+ direct_child_name, nested_child_name = child_name.to_s.split('.', 2)
155
+
156
+ if node.is_a?(Array)
157
+ child_direct_child_node = direct_child_name =~ /\A\d+\z/ ? node[direct_child_name.to_i - 1] : node.send(direct_child_name)
158
+ return child_node_by_name(child_direct_child_node, nested_child_name) if nested_child_name
159
+ return child_direct_child_node if child_direct_child_node
160
+ end
161
+
162
+ if node.respond_to?(direct_child_name)
163
+ child_node = node.send(direct_child_name)
164
+
165
+ return child_node_by_name(child_node, nested_child_name) if nested_child_name
166
+
167
+ return nil if child_node.nil?
168
+
169
+ return child_node if child_node.is_a?(Parser::AST::Node)
170
+
171
+ return child_node
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NodeMutation
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+ require 'active_support/core_ext/array'
5
+
6
+ require_relative "node_mutation/version"
7
+
8
+ class NodeMutation
9
+ class MethodNotSupported < StandardError; end
10
+ class ConflictActionError < StandardError; end
11
+
12
+ KEEPING_RUNNING = 1
13
+ THROW_ERROR = 2
14
+
15
+ autoload :Adapter, "node_mutation/adapter"
16
+ autoload :ParserAdapter, "node_mutation/parser_adapter"
17
+ autoload :Action, 'node_mutation/action'
18
+ autoload :AppendAction, 'node_mutation/action/append_action'
19
+ autoload :DeleteAction, 'node_mutation/action/delete_action'
20
+ autoload :InsertAction, 'node_mutation/action/insert_action'
21
+ autoload :InsertAfterAction, 'node_mutation/action/insert_after_action'
22
+ autoload :RemoveAction, 'node_mutation/action/remove_action'
23
+ autoload :PrependAction, 'node_mutation/action/prepend_action'
24
+ autoload :ReplaceAction, 'node_mutation/action/replace_action'
25
+ autoload :ReplaceErbStmtWithExprAction, 'node_mutation/action/replace_erb_stmt_with_expr_action'
26
+ autoload :ReplaceWithAction, 'node_mutation/action/replace_with_action'
27
+ autoload :WrapAction, 'node_mutation/action/wrap_action'
28
+
29
+ attr_reader :actions
30
+
31
+ # Configure NodeMutation
32
+ # @param [Hash] options options to configure
33
+ # @option options [NodeMutation::Adapter] :adapter the adpater
34
+ def self.configure(options)
35
+ if options[:adapter]
36
+ @adapter = options[:adapter]
37
+ end
38
+ if options[:strategy]
39
+ @strategy = options[:strategy]
40
+ end
41
+ end
42
+
43
+ # Get the adapter
44
+ # @return [NodeMutation::Adapter] current adapter, by default is {NodeMutation::ParserAdapter}
45
+ def self.adapter
46
+ @adapter ||= ParserAdapter.new
47
+ end
48
+
49
+ # Get the strategy
50
+ # @return [Integer] current strategy, could be {NodeMutation::KEEPING_RUNNING} or {NodeMutation::THROW_ERROR},
51
+ # by default is {NodeMutation::KEEPING_RUNNING}
52
+ def self.strategy
53
+ @strategy ||= KEEP_RUNNING
54
+ end
55
+
56
+ # Initialize a NodeMutation.
57
+ # @param file_path [String] file path
58
+ # @param source [String] source of the file
59
+ def initialize(file_path, source)
60
+ @file_path = file_path
61
+ @source = +source
62
+ @actions = []
63
+ end
64
+
65
+ # Append code to the ast node.
66
+ # @param node [Node] ast node
67
+ # @param code [String] new code to append
68
+ # @example
69
+ # source code of the ast node is
70
+ # def teardown
71
+ # clean_something
72
+ # end
73
+ # then we call
74
+ # mutation.append(node, 'super')
75
+ # the source code will be rewritten to
76
+ # def teardown
77
+ # clean_something
78
+ # super
79
+ # end
80
+ def append(node, code)
81
+ @actions << AppendAction.new(node, code).process
82
+ end
83
+
84
+ # Delete source code of the child ast node.
85
+ # @param node [Node] ast node
86
+ # @param selectors [Array<Symbol>] selector names of child node.
87
+ # @param options [Hash]
88
+ # @option and_comma [Boolean] delete extra comma.
89
+ # @example
90
+ # source code of the ast node is
91
+ # FactoryBot.create(...)
92
+ # then we call
93
+ # mutation.delete(node, :receiver, :dot)
94
+ # the source code will be rewritten to
95
+ # create(...)
96
+ def delete(node, *selectors, **options)
97
+ @actions << DeleteAction.new(node, *selectors, **options).process
98
+ end
99
+
100
+ # Insert code to the ast node.
101
+ # @param node [Node] ast node
102
+ # @param code [String] code need to be inserted.
103
+ # @param at [String] insert position, beginning or end
104
+ # @param to [String] where to insert, if it is nil, will insert to current node.
105
+ # @example
106
+ # source code of the ast node is
107
+ # open('http://test.com')
108
+ # then we call
109
+ # mutation.insert(node, 'URI.', at: 'beginning')
110
+ # the source code will be rewritten to
111
+ # URI.open('http://test.com')
112
+ def insert(node, code, at: 'end', to: nil)
113
+ @actions << InsertAction.new(node, code, at: at, to: to).process
114
+ end
115
+
116
+ # Insert code next to the ast node.
117
+ # @param node [Node] ast node
118
+ # @param code [String] new code to insert.
119
+ # @example
120
+ # source code of the ast node is
121
+ # Synvert::Application.config.secret_token = "0447aa931d42918bfb934750bb78257088fb671186b5d1b6f9fddf126fc8a14d34f1d045cefab3900751c3da121a8dd929aec9bafe975f1cabb48232b4002e4e"
122
+ # then we call
123
+ # mutation.insert_after(node, "{{receiver}}.secret_key_base = \"#{SecureRandom.hex(64)}\"")
124
+ # the source code will be rewritten to
125
+ # Synvert::Application.config.secret_token = "0447aa931d42918bfb934750bb78257088fb671186b5d1b6f9fddf126fc8a14d34f1d045cefab3900751c3da121a8dd929aec9bafe975f1cabb48232b4002e4e"
126
+ # Synvert::Application.config.secret_key_base = "bf4f3f46924ecd9adcb6515681c78144545bba454420973a274d7021ff946b8ef043a95ca1a15a9d1b75f9fbdf85d1a3afaf22f4e3c2f3f78e24a0a188b581df"
127
+ def insert_after(node, code)
128
+ @actions << InsertAfterAction.new(node, code).process
129
+ end
130
+
131
+ # Prepend code to the ast node.
132
+ # @param node [Node] ast node
133
+ # @param code [String] new code to prepend.
134
+ # @example
135
+ # source code of the ast node is
136
+ # def setup
137
+ # do_something
138
+ # end
139
+ # then we call
140
+ # mutation.prepend(node, 'super')
141
+ # the source code will be rewritten to
142
+ # def setup
143
+ # super
144
+ # do_something
145
+ # end
146
+ def prepend(node, code)
147
+ @actions << PrependAction.new(node, code).process
148
+ end
149
+
150
+ # Remove source code of the ast node.
151
+ # @param node [Node] ast node
152
+ # @param options [Hash] options.
153
+ # @option and_comma [Boolean] delete extra comma.
154
+ # @example
155
+ # source code of the ast node is
156
+ # puts "test"
157
+ # then we call
158
+ # mutation.remove(node)
159
+ # the source code will be removed
160
+ def remove(node, **options)
161
+ @actions << RemoveAction.new(node, **options).process
162
+ end
163
+
164
+ # Replace child node of the ast node with new code.
165
+ # @param node [Node] ast node
166
+ # @param selectors [Array<Symbol>] selector names of child node.
167
+ # @param with [String] code need to be replaced with.
168
+ # @example
169
+ # source code of the ast node is
170
+ # assert(object.empty?)
171
+ # then we call
172
+ # replace :message, with: 'assert_empty'
173
+ # replace :arguments, with: '{{arguments.first.receiver}}'
174
+ # the source code will be rewritten to
175
+ # assert_empty(object)
176
+ def replace(node, *selectors, with:)
177
+ @actions << ReplaceAction.new(node, *selectors, with: with).process
178
+ end
179
+
180
+ # Replace source code of the ast node with new code.
181
+ # @param node [Node] ast node
182
+ # @param code [String] code need to be replaced with.
183
+ # @example
184
+ # source code of the ast node is
185
+ # obj.stub(:foo => 1, :bar => 2)
186
+ # then we call
187
+ # replace_with 'allow({{receiver}}).to receive_messages({{arguments}})'
188
+ # the source code will be rewritten to
189
+ # allow(obj).to receive_messages(:foo => 1, :bar => 2)
190
+ def replace_with(node, code)
191
+ @actions << ReplaceWithAction.new(node, code).process
192
+ end
193
+
194
+ # Wrap source code of the ast node with new code.
195
+ # @param node [Node] ast node
196
+ # @param with [String] code need to be wrapped with.
197
+ # @example
198
+ # source code of the ast node is
199
+ # class Foobar
200
+ # end
201
+ # then we call
202
+ # wrap(node, with: 'module Synvert')
203
+ # the source code will be rewritten to
204
+ # module Synvert
205
+ # class Foobar
206
+ # end
207
+ # end
208
+ def wrap(node, with:)
209
+ @actions << WrapAction.new(node, with: with).process
210
+ end
211
+
212
+ # Read the source code from file path,
213
+ # rewrite the source code based on all actions,
214
+ # then write the new source code back to the file.
215
+ #
216
+ # If there's an action range conflict,
217
+ # it will raise a ConflictActionError if strategy is set to THROW_ERROR,
218
+ # it will process all non conflicted actions and return `{ conflict: true }`
219
+ # if strategy is set to KEEP_RUNNING.
220
+ # @return {{conflict: Boolean}} if actions are conflicted
221
+ def process
222
+ conflict_actions = []
223
+ if @actions.length > 0
224
+ @actions.sort_by! { |action| [action.start, action.end] }
225
+ conflict_actions = get_conflict_actions
226
+ if conflict_actions.size > 0 && NodeMutation.strategy == THROW_ERROR
227
+ raise ConflictActionError, "mutation actions are conflicted"
228
+ end
229
+ @actions.reverse_each do |action|
230
+ @source[action.start...action.end] = action.new_code
231
+ end
232
+ @actions = []
233
+
234
+ File.write(@file_path, @source)
235
+ end
236
+ OpenStruct.new(conflict: !conflict_actions.empty?)
237
+ end
238
+
239
+ private
240
+
241
+ # It changes source code from bottom to top, and it can change source code twice at the same time,
242
+ # So if there is an overlap between two actions, it removes the conflict actions and operate them in the next loop.
243
+ def get_conflict_actions
244
+ i = @actions.length - 1
245
+ j = i - 1
246
+ conflict_actions = []
247
+ return [] if i < 0
248
+
249
+ start = @actions[i].start
250
+ while j > -1
251
+ if start < @actions[j].end
252
+ conflict_actions << @actions.delete_at(j)
253
+ else
254
+ i = j
255
+ start = @actions[i].start
256
+ end
257
+ j -= 1
258
+ end
259
+ conflict_actions
260
+ end
261
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/node_mutation/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "node_mutation"
7
+ spec.version = NodeMutation::VERSION
8
+ spec.authors = ["Richard Huang"]
9
+ spec.email = ["flyerhzm@gmail.com"]
10
+
11
+ spec.summary = "ast node mutation apis"
12
+ spec.description = "ast node mutation apis"
13
+ spec.homepage = "https://github.com/xinminlabs/node-mutation-ruby"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/xinminlabs/node-mutation-ruby"
18
+ spec.metadata["changelog_uri"] = "https://github.com/xinminlabs/node-mutation-ruby/blob/master/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject do |f|
24
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
25
+ end
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency "activesupport"
33
+
34
+ # For more information and examples about making a new gem, check out our
35
+ # guide at: https://bundler.io/guides/creating_gem.html
36
+ end
@@ -0,0 +1,19 @@
1
+ class NodeMutation::Adapter[T]
2
+ def get_source: (node: T) -> String
3
+
4
+ def rewritten_source: (node: T, code: String) -> String
5
+
6
+ def file_content: (node: T) -> String
7
+
8
+ def child_node_range: (node: T, child_name: String) -> OpenStruct
9
+
10
+ def get_start: (node: T) -> Integer
11
+
12
+ def get_end: (node: T) -> Integer
13
+
14
+ def get_start_loc: (node: T) -> OpenStruct
15
+
16
+ def get_end_loc: (node: T) -> OpenStruct
17
+
18
+ def get_indent: (node: T) -> Integer
19
+ end
@@ -0,0 +1,43 @@
1
+ module NodeMutation[T]
2
+ VERSION: Stringo
3
+
4
+ class MethodNotSupported < StandardError
5
+ end
6
+
7
+ class ConflictActionError < StandardError
8
+ end
9
+
10
+ KEEPING_RUNNING: Integer
11
+
12
+ THROW_ERROR: Integer
13
+
14
+ attr_reader actions: Array[NodeMutation::Action]
15
+
16
+ def self.configure: (options: { adapter: NodeMutation::Adapter, strategry: Integer }) -> void
17
+
18
+ def self.adapter: () -> NodeMutation::Adapter
19
+
20
+ def self.strategry: () -> Integer
21
+
22
+ def initialize: (file_path: String, source: String) -> NodeMutation
23
+
24
+ def append: (node: T, code: String) -> void
25
+
26
+ def delete: (node: T, *selectors: Array[String], **options: { and_comma: bool }) -> void
27
+
28
+ def insert: (node: T, code: String, ?at: "beginning" | "end", ?to: nil | String) -> void
29
+
30
+ def insert_after: (node: T, code: String) -> void
31
+
32
+ def prepend: (node: T, code: String) -> void
33
+
34
+ def remove: (node: T, **options: { and_comma: bool }) -> void
35
+
36
+ def replace: (node: T, *selectors: Array[String], with: String) -> void
37
+
38
+ def replace_with: (node: T, code: String) -> void
39
+
40
+ def wrap: (node: T, with: String) -> void
41
+
42
+ def process: () -> void
43
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: node_mutation
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Richard Huang
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-07-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: ast node mutation apis
28
+ email:
29
+ - flyerhzm@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".rspec"
35
+ - CHANGELOG.md
36
+ - Gemfile
37
+ - Gemfile.lock
38
+ - Guardfile
39
+ - README.md
40
+ - Rakefile
41
+ - code.rb
42
+ - lib/node_mutation.rb
43
+ - lib/node_mutation/action.rb
44
+ - lib/node_mutation/action/append_action.rb
45
+ - lib/node_mutation/action/delete_action.rb
46
+ - lib/node_mutation/action/insert_action.rb
47
+ - lib/node_mutation/action/insert_after_action.rb
48
+ - lib/node_mutation/action/prepend_action.rb
49
+ - lib/node_mutation/action/remove_action.rb
50
+ - lib/node_mutation/action/replace_action.rb
51
+ - lib/node_mutation/action/replace_with_action.rb
52
+ - lib/node_mutation/action/wrap_action.rb
53
+ - lib/node_mutation/adapter.rb
54
+ - lib/node_mutation/parser_adapter.rb
55
+ - lib/node_mutation/version.rb
56
+ - node_mutation.gemspec
57
+ - sig/node_mutation.rbs
58
+ - sig/node_mutation/adapter.rbs
59
+ homepage: https://github.com/xinminlabs/node-mutation-ruby
60
+ licenses: []
61
+ metadata:
62
+ homepage_uri: https://github.com/xinminlabs/node-mutation-ruby
63
+ source_code_uri: https://github.com/xinminlabs/node-mutation-ruby
64
+ changelog_uri: https://github.com/xinminlabs/node-mutation-ruby/blob/master/CHANGELOG.md
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 2.6.0
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.3.7
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: ast node mutation apis
84
+ test_files: []