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 +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: []
|