synvert-core 0.1.0

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