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.
@@ -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