synvert-core 0.1.0

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