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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ee3374fb46922e5b590efa36db97d72a9e44abce
4
+ data.tar.gz: 3fe3c251671150387708c7e882078198956443fe
5
+ SHA512:
6
+ metadata.gz: 6422babf1c7b32bf5c5201aa8eb8e56fb4cfe7b7082e5ab0c7e0d3cfd6fec55250475bcbf789c71cc25dcbcf4364e74d7e2c7aadf6e4c967c303ea8e26be9ed7
7
+ data.tar.gz: 71d807430eb04e21a047625d59786be237ef6f4de3465095a5a9524d7a5384eb001ddf6bee14b7a5092dbfd2580385941449aea62e8f7b7a22432da7e8c05fd5
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.1.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # CHANGELOG
2
+
3
+ ## 0.1.0
4
+
5
+ * Abstract from synvert
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in synvert.gemspec
4
+ gemspec
5
+
6
+ gem 'coveralls', require: false
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Richard Huang
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Synvert::Core
2
+
3
+ [![Build Status](https://secure.travis-ci.org/xinminlabs/synvert-core.png)](http://travis-ci.org/xinminlabs/synvert-core)
4
+ [![Coverage Status](https://coveralls.io/repos/xinminlabs/synvert-core/badge.png?branch=master)](https://coveralls.io/r/xinminlabs/synvert-core)
5
+ [![Gem Version](https://badge.fury.io/rb/synvert-core.png)](http://badge.fury.io/rb/synvert-core)
6
+
7
+ synvert-core provides a dsl to convert ruby source code.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'synvert-core'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install synvert-core
22
+
23
+
24
+ ## Documentation
25
+
26
+ [Website][1]
27
+ [RDoc][2]
28
+
29
+ ## Contributing
30
+
31
+ 1. Fork it ( https://github.com/[my-github-username]/synvert-core/fork )
32
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
33
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
34
+ 4. Push to the branch (`git push origin my-new-feature`)
35
+ 5. Create a new Pull Request
36
+
37
+ [1]: http://xinminlabs.github.io/synvert/
38
+ [2]: http://rubydoc.info/github/xinminlabs/synvert-core/master/frames
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,24 @@
1
+ require "synvert/core/version"
2
+
3
+ # coding: utf-8
4
+ require "synvert/core/version"
5
+ require 'bundler'
6
+ require 'parser'
7
+ require 'parser/current'
8
+ require 'ast'
9
+ require 'active_support/inflector'
10
+ require 'synvert/core/node_ext'
11
+
12
+ module Synvert
13
+ module Core
14
+ autoload :Configuration, 'synvert/core/configuration'
15
+ autoload :Rewriter, 'synvert/core/rewriter'
16
+ autoload :RewriterNotFound, 'synvert/core/exceptions'
17
+ autoload :GemfileLockNotFound, 'synvert/core/exceptions'
18
+ autoload :MethodNotSupported, 'synvert/core/exceptions'
19
+ end
20
+ end
21
+
22
+ module Synvert
23
+ Rewriter = Core::Rewriter
24
+ end
@@ -0,0 +1,147 @@
1
+ # coding: utf-8
2
+ require 'optparse'
3
+ require 'open-uri'
4
+
5
+ module Synvert::Core
6
+ # Synvert command line interface.
7
+ class CLI
8
+ # Initialize the cli and run.
9
+ #
10
+ # @param args [Array] arguments, default is ARGV.
11
+ # @return [Boolean] true if command runs successfully.
12
+ def self.run(args = ARGV)
13
+ new.run(args)
14
+ end
15
+
16
+ # Initialize a CLI.
17
+ def initialize
18
+ @options = {command: 'run', snippet_paths: [], snippet_names: []}
19
+ Configuration.instance.set :skip_files, []
20
+ end
21
+
22
+ # Run the CLI.
23
+ # @param args [Array] arguments.
24
+ # @return [Boolean] true if command runs successfully.
25
+ def run(args)
26
+ run_option_parser(args)
27
+ load_rewriters
28
+
29
+ case @options[:command]
30
+ when 'list' then list_available_rewriters
31
+ when 'query' then query_available_rewriters
32
+ when 'show' then show_rewriter
33
+ else
34
+ @options[:snippet_names].each do |snippet_name|
35
+ puts "===== #{snippet_name} started ====="
36
+ rewriter = Rewriter.call snippet_name
37
+ puts rewriter.todo if rewriter.todo
38
+ puts "===== #{snippet_name} done ====="
39
+ end
40
+ end
41
+ true
42
+ rescue SystemExit
43
+ true
44
+ rescue Parser::SyntaxError => e
45
+ puts "Syntax error: #{e.message}"
46
+ puts "file #{e.diagnostic.location.source_buffer.name}"
47
+ puts "line #{e.diagnostic.location.line}"
48
+ false
49
+ rescue Exception => e
50
+ print "Error: "
51
+ p e
52
+ false
53
+ end
54
+
55
+ private
56
+
57
+ # Run OptionParser to parse arguments.
58
+ def run_option_parser(args)
59
+ optparse = OptionParser.new do |opts|
60
+ opts.banner = "Usage: synvert [project_path]"
61
+ opts.on '-d', '--load SNIPPET_PATHS', 'load additional snippets, snippet paths can be local file path or remote http url' do |snippet_paths|
62
+ @options[:snippet_paths] = snippet_paths.split(',').map(&:strip)
63
+ end
64
+ opts.on '-l', '--list', 'list all available snippets' do
65
+ @options[:command] = 'list'
66
+ end
67
+ opts.on '-q', '--query QUERY', 'query specified snippets' do |query|
68
+ @options[:command] = 'query'
69
+ @options[:query] = query
70
+ end
71
+ opts.on '--skip FILE_PATTERNS', 'skip specified files or directories, separated by comma, e.g. app/models/post.rb,vendor/plugins/**/*.rb' do |file_patterns|
72
+ @options[:skip_file_patterns] = file_patterns.split(',')
73
+ end
74
+ opts.on '-s', '--show SNIPPET_NAME', 'show specified snippet description' do |snippet_name|
75
+ @options[:command] = 'show'
76
+ @options[:snippet_name] = snippet_name
77
+ end
78
+ opts.on '-r', '--run SNIPPET_NAMES', 'run specified snippets' do |snippet_names|
79
+ @options[:snippet_names] = snippet_names.split(',').map(&:strip)
80
+ end
81
+ opts.on '-v', '--version', 'show this version' do
82
+ puts Synvert::Core::VERSION
83
+ exit
84
+ end
85
+ end
86
+ paths = optparse.parse(args)
87
+ Configuration.instance.set :path, paths.first || Dir.pwd
88
+ if @options[:skip_file_patterns] && !@options[:skip_file_patterns].empty?
89
+ skip_files = @options[:skip_file_patterns].map { |file_pattern|
90
+ full_file_pattern = File.join(Configuration.instance.get(:path), file_pattern)
91
+ Dir.glob(full_file_pattern)
92
+ }.flatten
93
+ Configuration.instance.set :skip_files, skip_files
94
+ end
95
+ end
96
+
97
+ # Load all rewriters.
98
+ def load_rewriters
99
+ Dir.glob(File.join(File.dirname(__FILE__), 'snippets/**/*.rb')).each { |file| eval(File.read(file)) }
100
+
101
+ @options[:snippet_paths].each do |snippet_path|
102
+ if snippet_path =~ /^http/
103
+ uri = URI.parse snippet_path
104
+ eval(uri.read)
105
+ else
106
+ eval(File.read(snippet_path))
107
+ end
108
+ end
109
+ end
110
+
111
+ # List and print all available rewriters.
112
+ def list_available_rewriters
113
+ Rewriter.availables.each do |rewriter|
114
+ print rewriter.name.to_s + " "
115
+ end
116
+ puts
117
+ end
118
+
119
+ # Query and print available rewriters.
120
+ def query_available_rewriters
121
+ Rewriter.availables.each do |rewriter|
122
+ if rewriter.name.include? @options[:query]
123
+ print rewriter.name + " "
124
+ end
125
+ end
126
+ puts
127
+ end
128
+
129
+ # Show and print one rewriter.
130
+ def show_rewriter
131
+ rewriter = Rewriter.fetch(@options[:snippet_name])
132
+ if rewriter
133
+ rewriter.process_with_sandbox
134
+ puts rewriter.description
135
+ rewriter.sub_snippets.each do |sub_rewriter|
136
+ puts
137
+ puts "=" * 80
138
+ puts "snippet: #{sub_rewriter.name}"
139
+ puts "=" * 80
140
+ puts sub_rewriter.description
141
+ end
142
+ else
143
+ puts "snippet #{@options[:snippet_name]} not found"
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ require 'singleton'
3
+
4
+ module Synvert::Core
5
+ # Synvert global configuration.
6
+ class Configuration < Hash
7
+ include Singleton
8
+
9
+ # Set the configuration.
10
+ #
11
+ # @param key [String] configuration key.
12
+ # @param value [Object] configuration value.
13
+ def set(key, value)
14
+ self[key] = value
15
+ end
16
+
17
+ # Get the configuration.
18
+ #
19
+ # @param key [String] configuration key.
20
+ # @return [Object] configuration value.
21
+ def get(key)
22
+ self[key]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module Synvert::Core
2
+ # Rewriter not found exception.
3
+ class RewriterNotFound < Exception
4
+ end
5
+
6
+ # Gemfile.lock not found exception.
7
+ class GemfileLockNotFound < Exception
8
+ end
9
+
10
+ # Method not supported exception.
11
+ class MethodNotSupported < Exception
12
+ end
13
+ end
@@ -0,0 +1,319 @@
1
+ # Parser::AST::Node monkey patch.
2
+ class Parser::AST::Node
3
+ # Get name node of :class, :module, :def and :defs node.
4
+ #
5
+ # @return [Parser::AST::Node] name node.
6
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
7
+ def name
8
+ case self.type
9
+ when :class, :module, :def
10
+ self.children[0]
11
+ when :defs
12
+ self.children[1]
13
+ else
14
+ raise Synvert::Core::MethodNotSupported.new "name is not handled for #{self.inspect}"
15
+ end
16
+ end
17
+
18
+ # Get receiver node of :send node.
19
+ #
20
+ # @return [Parser::AST::Node] receiver node.
21
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
22
+ def receiver
23
+ if :send == self.type
24
+ self.children[0]
25
+ else
26
+ raise Synvert::Core::MethodNotSupported.new "receiver is not handled for #{self.inspect}"
27
+ end
28
+ end
29
+
30
+ # Get message node of :send node.
31
+ #
32
+ # @return [Parser::AST::Node] mesage node.
33
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
34
+ def message
35
+ if :send == self.type
36
+ self.children[1]
37
+ else
38
+ raise Synvert::Core::MethodNotSupported.new "message is not handled for #{self.inspect}"
39
+ end
40
+ end
41
+
42
+ # Get arguments node of :send, :block or :defined? node.
43
+ #
44
+ # @return [Array<Parser::AST::Node>] arguments node.
45
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
46
+ def arguments
47
+ case self.type
48
+ when :send
49
+ self.children[2..-1]
50
+ when :block
51
+ self.children[1].children
52
+ when :defined?
53
+ self.children
54
+ else
55
+ raise Synvert::Core::MethodNotSupported.new "arguments is not handled for #{self.inspect}"
56
+ end
57
+ end
58
+
59
+ # Get caller node of :block node.
60
+ #
61
+ # @return [Parser::AST::Node] caller node.
62
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
63
+ def caller
64
+ if :block == self.type
65
+ self.children[0]
66
+ else
67
+ raise Synvert::Core::MethodNotSupported.new "caller is not handled for #{self.inspect}"
68
+ end
69
+ end
70
+
71
+ # Get body node of :begin or :block node.
72
+ #
73
+ # @return [Array<Parser::AST::Node>] body node.
74
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
75
+ def body
76
+ case self.type
77
+ when :begin
78
+ self.children
79
+ when :block
80
+ :begin == self.children[2].type ? self.children[2].children : [self.children[2]]
81
+ else
82
+ raise Synvert::Core::MethodNotSupported.new "body is not handled for #{self.inspect}"
83
+ end
84
+ end
85
+
86
+ # Get condition node of :if node.
87
+ #
88
+ # @return [Parser::AST::Node] condition node.
89
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
90
+ def condition
91
+ if :if == self.type
92
+ self.children[0]
93
+ else
94
+ raise Synvert::Core::MethodNotSupported.new "condition is not handled for #{self.inspect}"
95
+ end
96
+ end
97
+
98
+ # Get keys node of :hash node.
99
+ #
100
+ # @return [Array<Parser::AST::Node>] keys node.
101
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
102
+ def keys
103
+ if :hash == self.type
104
+ self.children.map { |child| child.children[0] }
105
+ else
106
+ raise Synvert::Core::MethodNotSupported.new "keys is not handled for #{self.inspect}"
107
+ end
108
+ end
109
+
110
+ # Get values node of :hash node.
111
+ #
112
+ # @return [Array<Parser::AST::Node>] values node.
113
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
114
+ def values
115
+ if :hash == self.type
116
+ self.children.map { |child| child.children[1] }
117
+ else
118
+ raise Synvert::Core::MethodNotSupported.new "keys is not handled for #{self.inspect}"
119
+ end
120
+ end
121
+
122
+ # Get key node of hash :pair node.
123
+ #
124
+ # @return [Parser::AST::Node] key node.
125
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
126
+ def key
127
+ if :pair == self.type
128
+ self.children.first
129
+ else
130
+ raise Synvert::Core::MethodNotSupported.new "key is not handled for #{self.inspect}"
131
+ end
132
+ end
133
+
134
+ # Get value node of hash :pair node.
135
+ #
136
+ # @return [Parser::AST::Node] value node.
137
+ # @raise [Synvert::Core::MethodNotSupported] if calls on other node.
138
+ def value
139
+ if :pair == self.type
140
+ self.children.last
141
+ else
142
+ raise Synvert::Core::MethodNotSupported.new "value is not handled for #{self.inspect}"
143
+ end
144
+ end
145
+
146
+ # Get the source code of current node.
147
+ #
148
+ # @param instance [Synvert::Core::Rewriter::Instance]
149
+ # @return [String] source code.
150
+ def source(instance)
151
+ if self.loc.expression
152
+ instance.current_source[self.loc.expression.begin_pos...self.loc.expression.end_pos]
153
+ end
154
+ end
155
+
156
+ # Get the indent of current node.
157
+ #
158
+ # @return [Integer] indent.
159
+ def indent
160
+ self.loc.expression.column
161
+ end
162
+
163
+ # Recursively iterate all child nodes of current node.
164
+ #
165
+ # @yield [child] Gives a child node.
166
+ # @yieldparam child [Parser::AST::Node] child node
167
+ def recursive_children
168
+ self.children.each do |child|
169
+ if Parser::AST::Node === child
170
+ yield child
171
+ child.recursive_children { |c| yield c }
172
+ end
173
+ end
174
+ end
175
+
176
+ # Match current node with rules.
177
+ #
178
+ # @param instance [Synvert::Core::Rewriter::Instance] used to get crrent source code.
179
+ # @param rules [Hash] rules to match.
180
+ # @return true if matches.
181
+ def match?(instance, rules)
182
+ flat_hash(rules).keys.all? do |multi_keys|
183
+ if multi_keys.last == :any
184
+ actual_values = actual_value(self, instance, multi_keys[0...-1])
185
+ expected = expected_value(rules, multi_keys)
186
+ actual_values.any? { |actual| match_value?(instance, actual, expected) }
187
+ elsif multi_keys.last == :not
188
+ actual = actual_value(self, instance, multi_keys[0...-1])
189
+ expected = expected_value(rules, multi_keys)
190
+ !match_value?(instance, actual, expected)
191
+ else
192
+ actual = actual_value(self, instance, multi_keys)
193
+ expected = expected_value(rules, multi_keys)
194
+ match_value?(instance, actual, expected)
195
+ end
196
+ end
197
+ end
198
+
199
+ # Get rewritten source code.
200
+ # @example
201
+ # node.rewritten_source("create({{arguments}})") #=> "create(:post)"
202
+ #
203
+ # @param code [String] raw code.
204
+ # @return [String] rewritten code, replace string in block {{ }} in raw code.
205
+ # @raise [Synvert::Core::MethodNotSupported] if string in block {{ }} does not support.
206
+ def rewritten_source(code)
207
+ code.gsub(/{{(.*?)}}/m) do
208
+ evaluated = self.instance_eval $1
209
+ case evaluated
210
+ when Parser::AST::Node
211
+ source = evaluated.loc.expression.source_buffer.source
212
+ source[evaluated.loc.expression.begin_pos...evaluated.loc.expression.end_pos]
213
+ when Array
214
+ if evaluated.size > 0
215
+ source = evaluated.first.loc.expression.source_buffer.source
216
+ source[evaluated.first.loc.expression.begin_pos...evaluated.last.loc.expression.end_pos]
217
+ end
218
+ when String
219
+ evaluated
220
+ when NilClass
221
+ 'nil'
222
+ else
223
+ raise Synvert::Core::MethodNotSupported.new "rewritten_source is not handled for #{evaluated.inspect}"
224
+ end
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ # Compare actual value with expected value.
231
+ #
232
+ # @param instance [Synvert::Core::Rewriter::Instance] used to get source code.
233
+ # @param actual [Object] actual value.
234
+ # @param expected [Object] expected value.
235
+ # @return [Integer] -1, 0 or 1.
236
+ # @raise [Synvert::Core::MethodNotSupported] if expected class is not supported.
237
+ def match_value?(instance, actual, expected)
238
+ case expected
239
+ when Symbol
240
+ if Parser::AST::Node === actual
241
+ actual.source(instance) == ":#{expected}"
242
+ else
243
+ actual.to_sym == expected
244
+ end
245
+ when String
246
+ if Parser::AST::Node === actual
247
+ actual.source(instance) == expected || actual.source(instance)[1...-1] == expected
248
+ else
249
+ actual.to_s == expected
250
+ end
251
+ when Regexp
252
+ if Parser::AST::Node === actual
253
+ actual.source(instance) =~ Regexp.new(expected.to_s, Regexp::MULTILINE)
254
+ else
255
+ actual.to_s =~ Regexp.new(expected.to_s, Regexp::MULTILINE)
256
+ end
257
+ when Array
258
+ actual.zip(expected).all? { |a, e| match_value?(instance, a, e) }
259
+ when NilClass
260
+ actual.nil?
261
+ when Numeric
262
+ if Parser::AST::Node === actual
263
+ actual.children[0] == expected
264
+ else
265
+ actual == expected
266
+ end
267
+ when TrueClass
268
+ :true == actual.type
269
+ when FalseClass
270
+ :false == actual.type
271
+ when Parser::AST::Node
272
+ actual == expected
273
+ else
274
+ raise Synvert::Core::MethodNotSupported.new "#{expected.class} is not handled for match_value?"
275
+ end
276
+ end
277
+
278
+ # Convert a hash to flat one.
279
+ #
280
+ # @example
281
+ # flat_hash(type: 'block', caller: {type: 'send', receiver: 'RSpec'})
282
+ # #=> {[:type] => 'block', [:caller, :type] => 'send', [:caller, :receiver] => 'RSpec'}
283
+ # @param h [Hash] original hash.
284
+ # @return flatten hash.
285
+ def flat_hash(h, k = [])
286
+ new_hash = {}
287
+ h.each_pair do |key, val|
288
+ if val.is_a?(Hash)
289
+ new_hash.merge!(flat_hash(val, k + [key]))
290
+ else
291
+ new_hash[k + [key]] = val
292
+ end
293
+ end
294
+ new_hash
295
+ end
296
+
297
+ # Get actual value from the node.
298
+ #
299
+ # @param node [Parser::AST::Node]
300
+ # @param instance [Synvert::Core::Rewriter::Instance]
301
+ # @param multi_keys [Array<Symbol>]
302
+ # @return [Object] actual value.
303
+ def actual_value(node, instance, multi_keys)
304
+ multi_keys.inject(node) { |n, key|
305
+ if n
306
+ key == :source ? n.send(key, instance) : n.send(key)
307
+ end
308
+ }
309
+ end
310
+
311
+ # Get expected value from rules.
312
+ #
313
+ # @param rules [Hash]
314
+ # @param multi_keys [Array<Symbol>]
315
+ # @return [Object] expected value.
316
+ def expected_value(rules, multi_keys)
317
+ multi_keys.inject(rules) { |o, key| o[key] }
318
+ end
319
+ end