synvert-core 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|