synvert-core 0.1.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/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +38 -0
- data/Rakefile +2 -0
- data/lib/synvert/core.rb +24 -0
- data/lib/synvert/core/cli.rb +147 -0
- data/lib/synvert/core/configuration.rb +25 -0
- data/lib/synvert/core/exceptions.rb +13 -0
- data/lib/synvert/core/node_ext.rb +319 -0
- data/lib/synvert/core/rewriter.rb +200 -0
- data/lib/synvert/core/rewriter/action.rb +224 -0
- data/lib/synvert/core/rewriter/condition.rb +56 -0
- data/lib/synvert/core/rewriter/gem_spec.rb +42 -0
- data/lib/synvert/core/rewriter/instance.rb +185 -0
- data/lib/synvert/core/rewriter/scope.rb +46 -0
- data/lib/synvert/core/version.rb +7 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/parser_helper.rb +5 -0
- data/spec/synvert/core/node_ext_spec.rb +201 -0
- data/spec/synvert/core/rewriter/action_spec.rb +225 -0
- data/spec/synvert/core/rewriter/condition_spec.rb +106 -0
- data/spec/synvert/core/rewriter/gem_spec_spec.rb +52 -0
- data/spec/synvert/core/rewriter/instance_spec.rb +163 -0
- data/spec/synvert/core/rewriter/scope_spec.rb +42 -0
- data/spec/synvert/core/rewriter_spec.rb +153 -0
- data/synvert-core.gemspec +27 -0
- metadata +153 -0
@@ -0,0 +1,200 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Synvert::Core
|
4
|
+
# Rewriter is the top level namespace in a snippet.
|
5
|
+
#
|
6
|
+
# One Rewriter can contain one or many [Synvert::Core::Rewriter::Instance],
|
7
|
+
# which define the behavior what files and what codes to detect and rewrite to what code.
|
8
|
+
#
|
9
|
+
# Synvert::Rewriter.new 'factory_girl_short_syntax', 'use FactoryGirl short syntax' do
|
10
|
+
# if_gem 'factory_girl', {gte: '2.0.0'}
|
11
|
+
#
|
12
|
+
# within_files 'spec/**/*.rb' do
|
13
|
+
# with_node type: 'send', receiver: 'FactoryGirl', message: 'create' do
|
14
|
+
# replace_with "create({{arguments}})"
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
class Rewriter
|
19
|
+
autoload :Action, 'synvert/core/rewriter/action'
|
20
|
+
autoload :AppendAction, 'synvert/core/rewriter/action'
|
21
|
+
autoload :InsertAction, 'synvert/core/rewriter/action'
|
22
|
+
autoload :InsertAfterAction, 'synvert/core/rewriter/action'
|
23
|
+
autoload :ReplaceWithAction, 'synvert/core/rewriter/action'
|
24
|
+
autoload :RemoveAction, 'synvert/core/rewriter/action'
|
25
|
+
|
26
|
+
autoload :Instance, 'synvert/core/rewriter/instance'
|
27
|
+
|
28
|
+
autoload :Scope, 'synvert/core/rewriter/scope'
|
29
|
+
|
30
|
+
autoload :Condition, 'synvert/core/rewriter/condition'
|
31
|
+
autoload :IfExistCondition, 'synvert/core/rewriter/condition'
|
32
|
+
autoload :UnlessExistCondition, 'synvert/core/rewriter/condition'
|
33
|
+
autoload :IfOnlyExistCondition, 'synvert/core/rewriter/condition'
|
34
|
+
|
35
|
+
autoload :GemSpec, 'synvert/core/rewriter/gem_spec'
|
36
|
+
|
37
|
+
class <<self
|
38
|
+
# Register a rewriter with its name.
|
39
|
+
#
|
40
|
+
# @param name [String] the unique rewriter name.
|
41
|
+
# @param rewriter [Synvert::Core::Rewriter] the rewriter to register.
|
42
|
+
def register(name, rewriter)
|
43
|
+
@rewriters ||= {}
|
44
|
+
@rewriters[name] = rewriter
|
45
|
+
end
|
46
|
+
|
47
|
+
# Fetch a rewriter by name.
|
48
|
+
#
|
49
|
+
# @param name [String] rewrtier name.
|
50
|
+
# @return [Synvert::Core::Rewriter] the matching rewriter.
|
51
|
+
def fetch(name)
|
52
|
+
@rewriters[name]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get a registered rewriter by name and process that rewriter.
|
56
|
+
#
|
57
|
+
# @param name [String] the rewriter name.
|
58
|
+
# @return [Synvert::Core::Rewriter] the registered rewriter.
|
59
|
+
# @raise [Synvert::Core::RewriterNotFound] if the registered rewriter is not found.
|
60
|
+
def call(name)
|
61
|
+
if (rewriter = @rewriters[name])
|
62
|
+
rewriter.process
|
63
|
+
rewriter
|
64
|
+
else
|
65
|
+
raise RewriterNotFound.new "Rewriter #{name} not found"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Get all available rewriters
|
70
|
+
#
|
71
|
+
# @return [Array<Synvert::Core::Rewriter>]
|
72
|
+
def availables
|
73
|
+
@rewriters.values
|
74
|
+
end
|
75
|
+
|
76
|
+
# Clear all registered rewriters.
|
77
|
+
def clear
|
78
|
+
@rewriters.clear
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @!attribute [r] name
|
83
|
+
# @return [String] the unique name of rewriter
|
84
|
+
# @!attribute [r] sub_snippets
|
85
|
+
# @return [Array<Synvert::Core::Rewriter>] all rewriters this rewiter calls.
|
86
|
+
attr_reader :name, :sub_snippets
|
87
|
+
|
88
|
+
# Initialize a rewriter.
|
89
|
+
# When a rewriter is initialized, it is also registered.
|
90
|
+
#
|
91
|
+
# @param name [String] name of the rewriter.
|
92
|
+
# @param block [Block] a block defines the behaviors of the rewriter, block code won't be called when initialization.
|
93
|
+
# @return [Synvert::Core::Rewriter]
|
94
|
+
def initialize(name, &block)
|
95
|
+
@name = name.to_s
|
96
|
+
@block = block
|
97
|
+
@helpers = []
|
98
|
+
@sub_snippets = []
|
99
|
+
self.class.register(@name, self)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Process the rewriter.
|
103
|
+
# It will call the block.
|
104
|
+
def process
|
105
|
+
self.instance_eval &@block
|
106
|
+
end
|
107
|
+
|
108
|
+
# Process rewriter with sandbox mode.
|
109
|
+
# It will call the block but doesn't change any file.
|
110
|
+
def process_with_sandbox
|
111
|
+
@sandbox = true
|
112
|
+
self.process
|
113
|
+
@sandbox = false
|
114
|
+
end
|
115
|
+
|
116
|
+
#######
|
117
|
+
# DSL #
|
118
|
+
#######
|
119
|
+
|
120
|
+
# Parse description dsl, it sets description of the rewrite.
|
121
|
+
# Or get description.
|
122
|
+
#
|
123
|
+
# @param description [String] rewriter description.
|
124
|
+
# @return rewriter description.
|
125
|
+
def description(description=nil)
|
126
|
+
if description
|
127
|
+
@description = description
|
128
|
+
else
|
129
|
+
@description
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Parse if_gem dsl, it compares version of the specified gem.
|
134
|
+
#
|
135
|
+
# @param name [String] gem name.
|
136
|
+
# @param comparator [Hash] equal, less than or greater than specified version, e.g. {gte: '2.0.0'},
|
137
|
+
# key can be eq, lt, gt, lte, gte or ne.
|
138
|
+
def if_gem(name, comparator)
|
139
|
+
@gem_spec = Rewriter::GemSpec.new(name, comparator)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Parse within_files dsl, it finds specified files.
|
143
|
+
# It creates a [Synvert::Core::Rewriter::Instance] to rewrite code.
|
144
|
+
#
|
145
|
+
# @param file_pattern [String] pattern to find files, e.g. spec/**/*_spec.rb
|
146
|
+
# @param block [Block] the block to rewrite code in the matching files.
|
147
|
+
def within_files(file_pattern, &block)
|
148
|
+
return if @sandbox
|
149
|
+
|
150
|
+
if !@gem_spec || @gem_spec.match?
|
151
|
+
instance = Rewriter::Instance.new(file_pattern, &block)
|
152
|
+
@helpers.each { |helper| instance.singleton_class.send(:define_method, helper[:name], &helper[:block]) }
|
153
|
+
instance.process
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# Parse within_file dsl, it finds a specifiled file.
|
158
|
+
alias within_file within_files
|
159
|
+
|
160
|
+
# Parses add_file dsl, it adds a new file.
|
161
|
+
#
|
162
|
+
# @param filename [String] file name of newly created file.
|
163
|
+
# @param content [String] file body of newly created file.
|
164
|
+
def add_file(filename, content)
|
165
|
+
return if @sandbox
|
166
|
+
|
167
|
+
File.open filename, 'w' do |file|
|
168
|
+
file.write content
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Parse add_snippet dsl, it calls anther rewriter.
|
173
|
+
#
|
174
|
+
# @param name [String] name of another rewriter.
|
175
|
+
def add_snippet(name)
|
176
|
+
@sub_snippets << self.class.call(name.to_s)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Parse helper_method dsl, it defines helper method for [Synvert::Core::Rewriter::Instance].
|
180
|
+
#
|
181
|
+
# @param name [String] helper method name.
|
182
|
+
# @param block [Block] helper method block.
|
183
|
+
def helper_method(name, &block)
|
184
|
+
@helpers << {name: name, block: block}
|
185
|
+
end
|
186
|
+
|
187
|
+
# Parse todo dsl, it sets todo of the rewriter.
|
188
|
+
# Or get todo.
|
189
|
+
#
|
190
|
+
# @param todo_list [String] rewriter todo.
|
191
|
+
# @return [String] rewriter todo.
|
192
|
+
def todo(todo=nil)
|
193
|
+
if todo
|
194
|
+
@todo = todo
|
195
|
+
else
|
196
|
+
@todo
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Synvert::Core
|
4
|
+
# Action defines rewriter action, add, replace or remove code.
|
5
|
+
class Rewriter::Action
|
6
|
+
# Initialize an action.
|
7
|
+
#
|
8
|
+
# @param instance [Synvert::Core::Rewriter::Instance]
|
9
|
+
# @param code {String] new code to add, replace or remove.
|
10
|
+
def initialize(instance, code)
|
11
|
+
@instance = instance
|
12
|
+
@code = code
|
13
|
+
@node = @instance.current_node
|
14
|
+
end
|
15
|
+
|
16
|
+
# Line number of the node.
|
17
|
+
#
|
18
|
+
# @return [Integer] line number.
|
19
|
+
def line
|
20
|
+
@node.loc.expression.line
|
21
|
+
end
|
22
|
+
|
23
|
+
# The rewritten source code with proper indent.
|
24
|
+
#
|
25
|
+
# @return [String] rewritten code.
|
26
|
+
def rewritten_code
|
27
|
+
if rewritten_source.split("\n").length > 1
|
28
|
+
"\n\n" + rewritten_source.split("\n").map { |line|
|
29
|
+
indent(@node) + line
|
30
|
+
}.join("\n")
|
31
|
+
else
|
32
|
+
"\n" + indent(@node) + rewritten_source
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# The rewritten source code.
|
37
|
+
#
|
38
|
+
# @return [String] rewritten source code.
|
39
|
+
def rewritten_source
|
40
|
+
@rewritten_source ||= @node.rewritten_source(@code)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Compare actions by begin position.
|
44
|
+
#
|
45
|
+
# @param action [Synvert::Core::Rewriter::Action]
|
46
|
+
# @return [Integer] -1, 0 or 1
|
47
|
+
def <=>(action)
|
48
|
+
self.begin_pos <=> action.begin_pos
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# ReplaceWithAction to replace code.
|
53
|
+
class Rewriter::ReplaceWithAction < Rewriter::Action
|
54
|
+
# Begin position of code to replace.
|
55
|
+
#
|
56
|
+
# @return [Integer] begin position.
|
57
|
+
def begin_pos
|
58
|
+
@node.loc.expression.begin_pos
|
59
|
+
end
|
60
|
+
|
61
|
+
# End position of code to replace.
|
62
|
+
#
|
63
|
+
# @return [Integer] end position.
|
64
|
+
def end_pos
|
65
|
+
@node.loc.expression.end_pos
|
66
|
+
end
|
67
|
+
|
68
|
+
# The rewritten source code with proper indent.
|
69
|
+
#
|
70
|
+
# @return [String] rewritten code.
|
71
|
+
def rewritten_code
|
72
|
+
if rewritten_source.split("\n").length > 1
|
73
|
+
"\n\n" + rewritten_source.split("\n").map { |line|
|
74
|
+
indent(@node) + line
|
75
|
+
}.join("\n")
|
76
|
+
else
|
77
|
+
rewritten_source
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# Indent of the node
|
84
|
+
#
|
85
|
+
# @param node [Parser::AST::Node]
|
86
|
+
# @return [String] n times whitesphace
|
87
|
+
def indent(node)
|
88
|
+
' ' * node.indent
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# AppendWithAction to append code to the bottom of node body.
|
93
|
+
class Rewriter::AppendAction < Rewriter::Action
|
94
|
+
# Begin position to append code.
|
95
|
+
#
|
96
|
+
# @return [Integer] begin position.
|
97
|
+
def begin_pos
|
98
|
+
if :begin == @node.type
|
99
|
+
@node.loc.expression.end_pos
|
100
|
+
else
|
101
|
+
@node.loc.expression.end_pos - 4
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# End position, always same to begin position.
|
106
|
+
#
|
107
|
+
# @return [Integer] end position.
|
108
|
+
def end_pos
|
109
|
+
begin_pos
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# Indent of the node.
|
115
|
+
#
|
116
|
+
# @param node [Parser::AST::Node]
|
117
|
+
# @return [String] n times whitesphace
|
118
|
+
def indent(node)
|
119
|
+
if [:block, :class].include? node.type
|
120
|
+
' ' * (node.indent + 2)
|
121
|
+
else
|
122
|
+
' ' * node.indent
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# InsertAction to insert code to the top of node body.
|
128
|
+
class Rewriter::InsertAction < Rewriter::Action
|
129
|
+
# Begin position to insert code.
|
130
|
+
#
|
131
|
+
# @return [Integer] begin position.
|
132
|
+
def begin_pos
|
133
|
+
insert_position(@node)
|
134
|
+
end
|
135
|
+
|
136
|
+
# End position, always same to begin position.
|
137
|
+
#
|
138
|
+
# @return [Integer] end position.
|
139
|
+
def end_pos
|
140
|
+
begin_pos
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# Insert position.
|
146
|
+
#
|
147
|
+
# @return [Integer] insert position.
|
148
|
+
def insert_position(node)
|
149
|
+
case node.type
|
150
|
+
when :block
|
151
|
+
node.children[1].children.empty? ? node.children[0].loc.expression.end_pos + 3 : node.children[1].loc.expression.end_pos
|
152
|
+
when :class
|
153
|
+
node.children[1] ? node.children[1].loc.expression.end_pos : node.children[0].loc.expression.end_pos
|
154
|
+
else
|
155
|
+
node.children.last.loc.expression.end_pos
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Indent of the node.
|
160
|
+
#
|
161
|
+
# @param node [Parser::AST::Node]
|
162
|
+
# @return [String] n times whitesphace
|
163
|
+
def indent(node)
|
164
|
+
if [:block, :class].include? node.type
|
165
|
+
' ' * (node.indent + 2)
|
166
|
+
else
|
167
|
+
' ' * node.indent
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# InsertAfterAction to insert code next to the node.
|
173
|
+
class Rewriter::InsertAfterAction < Rewriter::Action
|
174
|
+
# Begin position to insert code.
|
175
|
+
#
|
176
|
+
# @return [Integer] begin position.
|
177
|
+
def begin_pos
|
178
|
+
@node.loc.expression.end_pos
|
179
|
+
end
|
180
|
+
|
181
|
+
# End position, always same to begin position.
|
182
|
+
#
|
183
|
+
# @return [Integer] end position.
|
184
|
+
def end_pos
|
185
|
+
begin_pos
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
# Indent of the node.
|
191
|
+
#
|
192
|
+
# @param node [Parser::AST::Node]
|
193
|
+
# @return [String] n times whitesphace
|
194
|
+
def indent(node)
|
195
|
+
' ' * node.indent
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# RemoveAction to remove code.
|
200
|
+
class Rewriter::RemoveAction < Rewriter::Action
|
201
|
+
def initialize(instance, code=nil)
|
202
|
+
super
|
203
|
+
end
|
204
|
+
|
205
|
+
# Begin position of code to replace.
|
206
|
+
#
|
207
|
+
# @return [Integer] begin position.
|
208
|
+
def begin_pos
|
209
|
+
@node.loc.expression.begin_pos
|
210
|
+
end
|
211
|
+
|
212
|
+
# End position of code to replace.
|
213
|
+
#
|
214
|
+
# @return [Integer] end position.
|
215
|
+
def end_pos
|
216
|
+
@node.loc.expression.end_pos
|
217
|
+
end
|
218
|
+
|
219
|
+
# The rewritten code, always empty string.
|
220
|
+
def rewritten_code
|
221
|
+
''
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Synvert::Core
|
4
|
+
# Condition checks if rules matches.
|
5
|
+
class Rewriter::Condition
|
6
|
+
# Initialize a condition.
|
7
|
+
#
|
8
|
+
# @param instance [Synvert::Core::Rewriter::Instance]
|
9
|
+
# @param rules [Hash]
|
10
|
+
# @param block [Block]
|
11
|
+
# @return [Synvert::Core::Rewriter::Condition]
|
12
|
+
def initialize(instance, rules, &block)
|
13
|
+
@instance = instance
|
14
|
+
@rules = rules
|
15
|
+
@block = block
|
16
|
+
end
|
17
|
+
|
18
|
+
# If condition matches, run the block code.
|
19
|
+
def process
|
20
|
+
@instance.instance_eval &@block if match?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# IfExistCondition checks if matching node exists in the node children.
|
25
|
+
class Rewriter::IfExistCondition < Rewriter::Condition
|
26
|
+
# check if any child node matches the rules.
|
27
|
+
def match?
|
28
|
+
match = false
|
29
|
+
@instance.current_node.recursive_children do |child_node|
|
30
|
+
match = match || (child_node && child_node.match?(@instance, @rules))
|
31
|
+
end
|
32
|
+
match
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# UnlessExistCondition checks if matching node doesn't exist in the node children.
|
37
|
+
class Rewriter::UnlessExistCondition < Rewriter::Condition
|
38
|
+
# check if none of child node matches the rules.
|
39
|
+
def match?
|
40
|
+
match = false
|
41
|
+
@instance.current_node.recursive_children do |child_node|
|
42
|
+
match = match || (child_node && child_node.match?(@instance, @rules))
|
43
|
+
end
|
44
|
+
!match
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# IfExistCondition checks if node has only one child node and the child node matches rules.
|
49
|
+
class Rewriter::IfOnlyExistCondition < Rewriter::Condition
|
50
|
+
# check if only have one child node and the child node matches rules.
|
51
|
+
def match?
|
52
|
+
@instance.current_node.body.size == 1 &&
|
53
|
+
@instance.current_node.body.first.match?(@instance, @rules)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|