node_mutation 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +96 -0
- data/Guardfile +7 -0
- data/README.md +78 -0
- data/Rakefile +8 -0
- data/code.rb +4 -0
- data/lib/node_mutation/action/append_action.rb +22 -0
- data/lib/node_mutation/action/delete_action.rb +32 -0
- data/lib/node_mutation/action/insert_action.rb +44 -0
- data/lib/node_mutation/action/insert_after_action.rb +20 -0
- data/lib/node_mutation/action/prepend_action.rb +25 -0
- data/lib/node_mutation/action/remove_action.rb +54 -0
- data/lib/node_mutation/action/replace_action.rb +29 -0
- data/lib/node_mutation/action/replace_with_action.rb +34 -0
- data/lib/node_mutation/action/wrap_action.rb +34 -0
- data/lib/node_mutation/action.rb +98 -0
- data/lib/node_mutation/adapter.rb +70 -0
- data/lib/node_mutation/parser_adapter.rb +174 -0
- data/lib/node_mutation/version.rb +5 -0
- data/lib/node_mutation.rb +261 -0
- data/node_mutation.gemspec +36 -0
- data/sig/node_mutation/adapter.rbs +19 -0
- data/sig/node_mutation.rbs +43 -0
- metadata +84 -0
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
data/CHANGELOG.md
ADDED
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
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
data/code.rb
ADDED
@@ -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,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: []
|