node_mutation 1.0.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.
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: []