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,42 @@
1
+ # encoding: utf-8
2
+
3
+ module Synvert::Core
4
+ # GemSpec checks and compares gem version.
5
+ class Rewriter::GemSpec
6
+ OPERATORS = {eq: '==', lt: '<', gt: '>', lte: '<=', gte: '>=', ne: '!='}
7
+
8
+ # Initialize a gem_spec.
9
+ #
10
+ # @param name [String] gem name
11
+ # @param comparator [Hash] comparator to gem version, e.g. {eg: '2.0.0'},
12
+ # comparator key can be eq, lt, gt, lte, gte or ne.
13
+ def initialize(name, comparator)
14
+ @name = name
15
+ if Hash === comparator
16
+ @operator = comparator.keys.first
17
+ @version = Gem::Version.new comparator.values.first
18
+ else
19
+ @operator = :eq
20
+ @version = Gem::Version.new comparator
21
+ end
22
+ end
23
+
24
+ # Check if the specified gem version in Gemfile.lock matches gem_spec comparator.
25
+ #
26
+ # @return [Boolean] true if matches, otherwise false.
27
+ # @raise [Synvert::Core::GemfileLockNotFound] raise if Gemfile.lock does not exist.
28
+ def match?
29
+ gemfile_lock_path = File.join(Configuration.instance.get(:path), 'Gemfile.lock')
30
+ if File.exists? gemfile_lock_path
31
+ parser = Bundler::LockfileParser.new(File.read(gemfile_lock_path))
32
+ if spec = parser.specs.find { |spec| spec.name == @name }
33
+ Gem::Version.new(spec.version).send(OPERATORS[@operator], @version)
34
+ else
35
+ false
36
+ end
37
+ else
38
+ raise GemfileLockNotFound.new 'Gemfile.lock does not exist'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,185 @@
1
+ # encoding: utf-8
2
+
3
+ module Synvert::Core
4
+ # Instance is an execution unit, it finds specified ast nodes,
5
+ # checks if the nodes match some conditions, then add, replace or remove code.
6
+ #
7
+ # One instance can contains one or many [Synvert::Core::Rewriter::Scope] and [Synvert::Rewriter::Condition].
8
+ class Rewriter::Instance
9
+ # @!attribute [rw] current_node
10
+ # @return current parsing node
11
+ # @!attribute [rw] current_source
12
+ # @return current source code of file
13
+ # @!attribute [rw] current_file
14
+ # @return current filename
15
+ attr_accessor :current_node, :current_source, :current_file
16
+
17
+ # Initialize an instance.
18
+ #
19
+ # @param file_pattern [String] pattern to find files, e.g. spec/**/*_spec.rb
20
+ # @param block [Block] block code to find nodes, match conditions and rewrite code.
21
+ # @return [Synvert::Core::Rewriter::Instance]
22
+ def initialize(file_pattern, &block)
23
+ @actions = []
24
+ @file_pattern = file_pattern
25
+ @block = block
26
+ end
27
+
28
+ # Process the instance.
29
+ # It finds all files, for each file, it executes the block code, gets all rewrite actions,
30
+ # and rewrite source code back to original file.
31
+ def process
32
+ parser = Parser::CurrentRuby.new
33
+ file_pattern = File.join(Configuration.instance.get(:path), @file_pattern)
34
+ Dir.glob(file_pattern).each do |file_path|
35
+ unless Configuration.instance.get(:skip_files).include? file_path
36
+ begin
37
+ source = File.read(file_path)
38
+ buffer = Parser::Source::Buffer.new file_path
39
+ buffer.source = source
40
+
41
+ parser.reset
42
+ ast = parser.parse buffer
43
+
44
+ @current_file = file_path
45
+ @current_source = source
46
+ @current_node = ast
47
+ instance_eval &@block
48
+ @current_node = ast
49
+
50
+ @actions.sort!
51
+ check_conflict_actions
52
+ @actions.reverse.each do |action|
53
+ source[action.begin_pos...action.end_pos] = action.rewritten_code
54
+ source = remove_code_or_whole_line(source, action.line)
55
+ end
56
+ @actions = []
57
+
58
+ File.write file_path, source
59
+ end while !@conflict_actions.empty?
60
+ end
61
+ end
62
+ end
63
+
64
+ # Gets current node, it allows to get current node in block code.
65
+ #
66
+ # @return [Parser::AST::Node]
67
+ def node
68
+ @current_node
69
+ end
70
+
71
+ #######
72
+ # DSL #
73
+ #######
74
+
75
+ # Parse within_node dsl, it creates a [Synvert::Core::Rewriter::Scope] to find matching ast nodes,
76
+ # then continue operating on each matching ast node.
77
+ #
78
+ # @param rules [Hash] rules to find mathing ast nodes.
79
+ # @param block [Block] block code to continue operating on the matching nodes.
80
+ def within_node(rules, &block)
81
+ Rewriter::Scope.new(self, rules, &block).process
82
+ end
83
+
84
+ alias with_node within_node
85
+
86
+ # Parse if_exist_node dsl, it creates a [Synvert::Core::Rewriter::IfExistCondition] to check
87
+ # if matching nodes exist in the child nodes, if so, then continue operating on each matching ast node.
88
+ #
89
+ # @param rules [Hash] rules to check mathing ast nodes.
90
+ # @param block [Block] block code to continue operating on the matching nodes.
91
+ def if_exist_node(rules, &block)
92
+ Rewriter::IfExistCondition.new(self, rules, &block).process
93
+ end
94
+
95
+ # Parse unless_exist_node dsl, it creates a [Synvert::Core::Rewriter::UnlessExistCondition] to check
96
+ # if matching nodes doesn't exist in the child nodes, if so, then continue operating on each matching ast node.
97
+ #
98
+ # @param rules [Hash] rules to check mathing ast nodes.
99
+ # @param block [Block] block code to continue operating on the matching nodes.
100
+ def unless_exist_node(rules, &block)
101
+ Rewriter::UnlessExistCondition.new(self, rules, &block).process
102
+ end
103
+
104
+ # Parse if_only_exist_node dsl, it creates a [Synvert::Core::Rewriter::IfOnlyExistCondition] to check
105
+ # if current node has only one child node and the child node matches rules,
106
+ # if so, then continue operating on each matching ast node.
107
+ #
108
+ # @param rules [Hash] rules to check mathing ast nodes.
109
+ # @param block [Block] block code to continue operating on the matching nodes.
110
+ def if_only_exist_node(rules, &block)
111
+ Rewriter::IfOnlyExistCondition.new(self, rules, &block).process
112
+ end
113
+
114
+ # Parse append dsl, it creates a [Synvert::Core::Rewriter::AppendAction] to
115
+ # append the code to the bottom of current node body.
116
+ #
117
+ # @param code [String] code need to be appended.
118
+ def append(code)
119
+ @actions << Rewriter::AppendAction.new(self, code)
120
+ end
121
+
122
+ # Parse insert dsl, it creates a [Synvert::Core::Rewriter::InsertAction] to
123
+ # insert the code to the top of current node body.
124
+ #
125
+ # @param code [String] code need to be inserted.
126
+ def insert(code)
127
+ @actions << Rewriter::InsertAction.new(self, code)
128
+ end
129
+
130
+ # Parse insert_after dsl, it creates a [Synvert::Core::Rewriter::InsertAfterAction] to
131
+ # insert the code next to the current node.
132
+ #
133
+ # @param code [String] code need to be inserted.
134
+ def insert_after(node)
135
+ @actions << Rewriter::InsertAfterAction.new(self, node)
136
+ end
137
+
138
+ # Parse replace_with dsl, it creates a [Synvert::Core::Rewriter::ReplaceWithAction] to
139
+ # replace current node with code.
140
+ #
141
+ # @param code [String] code need to be replaced with.
142
+ def replace_with(code)
143
+ @actions << Rewriter::ReplaceWithAction.new(self, code)
144
+ end
145
+
146
+ # Parse remove dsl, it creates a [Synvert::Core::Rewriter::RemoveAction] to current node.
147
+ def remove
148
+ @actions << Rewriter::RemoveAction.new(self)
149
+ end
150
+
151
+ private
152
+
153
+ # It changes source code from bottom to top, and it can change source code twice at the same time.
154
+ # So if there is an overlap between two actions, it removes the conflict actions and operate them in the next loop.
155
+ def check_conflict_actions
156
+ i = @actions.length - 1
157
+ @conflict_actions = []
158
+ while i > 0
159
+ if @actions[i].begin_pos <= @actions[i - 1].end_pos
160
+ @conflict_actions << @actions.delete_at(i)
161
+ end
162
+ i -= 1
163
+ end
164
+ @conflict_actions
165
+ end
166
+
167
+ # It checks if code is removed and that line is empty.
168
+ #
169
+ # @param source [String] source code of file
170
+ # @param line [String] the line number
171
+ def remove_code_or_whole_line(source, line)
172
+ newline_at_end_of_line = source[-1] == "\n"
173
+ source_arr = source.split("\n")
174
+ if source_arr[line - 1] && source_arr[line - 1].strip.empty?
175
+ source_arr.delete_at(line - 1)
176
+ if source_arr[line - 2] && source_arr[line - 2].strip.empty? && source_arr[line - 1] && source_arr[line - 1].strip.empty?
177
+ source_arr.delete_at(line - 1)
178
+ end
179
+ source_arr.join("\n") + (newline_at_end_of_line ? "\n" : '')
180
+ else
181
+ source
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ module Synvert::Core
4
+ # Scope finds the child nodes which match rules.
5
+ class Rewriter::Scope
6
+ # Initialize a scope
7
+ #
8
+ # @param instance [Synvert::Core::Rewriter::Instance]
9
+ # @param rules [Hash]
10
+ # @param block [Block]
11
+ def initialize(instance, rules, &block)
12
+ @instance = instance
13
+ @rules = rules
14
+ @block = block
15
+ end
16
+
17
+ # Find the matching nodes. It checks the current node and iterates all child nodes,
18
+ # then run the block code for each matching node.
19
+ def process
20
+ current_node = @instance.current_node
21
+ return unless current_node
22
+ process_with_node current_node do
23
+ matching_nodes = []
24
+ matching_nodes << current_node if current_node.match? @instance, @rules
25
+ current_node.recursive_children do |child_node|
26
+ matching_nodes << child_node if child_node.match? @instance, @rules
27
+ end
28
+ matching_nodes.each do |matching_node|
29
+ process_with_node matching_node do
30
+ @instance.instance_eval &@block
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Set instance current node properly and process.
39
+ # @param node [Parser::AST::Node]
40
+ def process_with_node(node)
41
+ @instance.current_node = node
42
+ yield
43
+ @instance.current_node = node
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,7 @@
1
+ # coding: utf-8
2
+
3
+ module Synvert
4
+ module Core
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'synvert/core'
4
+
5
+ require 'coveralls'
6
+ Coveralls.wear!
7
+
8
+ Dir[File.join(File.dirname(__FILE__), 'support', '*')].each do |path|
9
+ require path
10
+ end
11
+
12
+ RSpec.configure do |config|
13
+ config.include ParserHelper
14
+
15
+ config.treat_symbols_as_metadata_keys_with_true_values = true
16
+ config.run_all_when_everything_filtered = true
17
+ config.filter_run :focus
18
+
19
+ config.order = 'random'
20
+
21
+ config.before do
22
+ Synvert::Core::Configuration.instance.set :skip_files, []
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ module ParserHelper
2
+ def parse(code)
3
+ Parser::CurrentRuby.parse code
4
+ end
5
+ end
@@ -0,0 +1,201 @@
1
+ require 'spec_helper'
2
+
3
+ describe Parser::AST::Node do
4
+ describe '#name' do
5
+ it 'gets for class node' do
6
+ node = parse('class Synvert; end')
7
+ expect(node.name).to eq parse('Synvert')
8
+
9
+ node = parse('class Synvert::Core::Rewriter::Instance; end')
10
+ expect(node.name).to eq parse('Synvert::Core::Rewriter::Instance')
11
+ end
12
+
13
+ it 'gets for module node' do
14
+ node = parse('module Synvert; end')
15
+ expect(node.name).to eq parse('Synvert')
16
+ end
17
+
18
+ it 'gets for def node' do
19
+ node = parse('def current_node; end')
20
+ expect(node.name).to eq :current_node
21
+ end
22
+
23
+ it 'gets for defs node' do
24
+ node = parse('def self.current_node; end')
25
+ expect(node.name).to eq :current_node
26
+ end
27
+ end
28
+
29
+ describe '#receiver' do
30
+ it 'gets for send node' do
31
+ node = parse('FactoryGirl.create :post')
32
+ expect(node.receiver).to eq parse('FactoryGirl')
33
+ end
34
+ end
35
+
36
+ describe '#message' do
37
+ it 'gets for send node' do
38
+ node = parse('FactoryGirl.create :post')
39
+ expect(node.message).to eq :create
40
+ end
41
+ end
42
+
43
+ describe '#arguments' do
44
+ it 'gets for send node' do
45
+ node = parse("FactoryGirl.create :post, title: 'post'")
46
+ expect(node.arguments).to eq parse("[:post, title: 'post']").children
47
+ end
48
+
49
+ it 'gets for block node' do
50
+ source = 'RSpec.configure do |config|; end'
51
+ node = parse(source)
52
+ instance = double(current_source: source)
53
+ expect(node.arguments.map { |argument| argument.source(instance) }).to eq ['config']
54
+ end
55
+
56
+ it 'gets for defined? node' do
57
+ node = parse('defined?(Bundler)')
58
+ expect(node.arguments).to eq [parse('Bundler')]
59
+ end
60
+ end
61
+
62
+ describe '#caller' do
63
+ it 'gets for block node' do
64
+ node = parse('RSpec.configure do |config|; end')
65
+ expect(node.caller).to eq parse('RSpec.configure')
66
+ end
67
+ end
68
+
69
+ describe '#body' do
70
+ it 'gets one line for block node' do
71
+ node = parse('RSpec.configure do |config|; include EmailSpec::Helpers; end')
72
+ expect(node.body).to eq [parse('include EmailSpec::Helpers')]
73
+ end
74
+
75
+ it 'gets multiple lines for block node' do
76
+ node = parse('RSpec.configure do |config|; include EmailSpec::Helpers; include EmailSpec::Matchers; end')
77
+ expect(node.body).to eq [parse('include EmailSpec::Helpers'), parse('include EmailSpec::Matchers')]
78
+ end
79
+
80
+ it 'gets for begin node' do
81
+ node = parse('foo; bar')
82
+ expect(node.body).to eq [parse('foo'), parse('bar')]
83
+ end
84
+ end
85
+
86
+ describe "#keys" do
87
+ it 'gets for hash node' do
88
+ node = parse("{:foo => :bar, 'foo' => 'bar'}")
89
+ expect(node.keys).to eq [Parser::CurrentRuby.parse(':foo'), Parser::CurrentRuby.parse("'foo'")]
90
+ end
91
+ end
92
+
93
+ describe "#values" do
94
+ it 'gets for hash node' do
95
+ node = parse("{:foo => :bar, 'foo' => 'bar'}")
96
+ expect(node.values).to eq [Parser::CurrentRuby.parse(':bar'), Parser::CurrentRuby.parse("'bar'")]
97
+ end
98
+ end
99
+
100
+ describe "#key" do
101
+ it 'gets for pair node' do
102
+ node = parse("{:foo => 'bar'}").children[0]
103
+ expect(node.key).to eq Parser::CurrentRuby.parse(':foo')
104
+ end
105
+ end
106
+
107
+ describe "#value" do
108
+ it 'gets for hash node' do
109
+ node = parse("{:foo => 'bar'}").children[0]
110
+ expect(node.value).to eq Parser::CurrentRuby.parse("'bar'")
111
+ end
112
+ end
113
+
114
+ describe "#condition" do
115
+ it 'gets for if node' do
116
+ node = parse('if defined?(Bundler); end')
117
+ expect(node.condition).to eq parse('defined?(Bundler)')
118
+ end
119
+ end
120
+
121
+ describe '#source' do
122
+ it 'gets for node' do
123
+ source = 'params[:user][:email]'
124
+ instance = double(current_source: source)
125
+ node = parse(source)
126
+ expect(node.source(instance)).to eq 'params[:user][:email]'
127
+ end
128
+ end
129
+
130
+ describe '#indent' do
131
+ it 'gets column number' do
132
+ node = parse(' FactoryGirl.create :post')
133
+ expect(node.indent).to eq 2
134
+ end
135
+ end
136
+
137
+ describe '#recursive_children' do
138
+ it 'iterates all children recursively' do
139
+ node = parse('class Synvert; def current_node; @node; end; end')
140
+ children = []
141
+ node.recursive_children { |child| children << child.type }
142
+ expect(children).to be_include :const
143
+ expect(children).to be_include :def
144
+ expect(children).to be_include :args
145
+ expect(children).to be_include :ivar
146
+ end
147
+ end
148
+
149
+ describe '#match?' do
150
+ let(:instance) { Synvert::Core::Rewriter::Instance.new('file pattern') }
151
+
152
+ it 'matches class name' do
153
+ source = 'class Synvert; end'
154
+ instance.current_source = source
155
+ node = parse(source)
156
+ expect(node).to be_match(instance, type: 'class', name: 'Synvert')
157
+ end
158
+
159
+ it 'matches message with regexp' do
160
+ source = 'User.find_by_login(login)'
161
+ instance.current_source = source
162
+ node = parse(source)
163
+ expect(node).to be_match(instance, type: 'send', message: /^find_by_/)
164
+ end
165
+
166
+ it 'matches arguments with symbol' do
167
+ source = 'params[:user]'
168
+ instance.current_source = source
169
+ node = parse(source)
170
+ expect(node).to be_match(instance, type: 'send', receiver: 'params', message: '[]', arguments: [:user])
171
+ end
172
+
173
+ it 'matches assign number' do
174
+ source = 'at_least(0)'
175
+ instance.current_source = source
176
+ node = parse(source)
177
+ expect(node).to be_match(instance, type: 'send', arguments: [0])
178
+ end
179
+
180
+ it 'matches arguments with string' do
181
+ source = 'params["user"]'
182
+ instance.current_source = source
183
+ node = parse(source)
184
+ expect(node).to be_match(instance, type: 'send', receiver: 'params', message: '[]', arguments: ['user'])
185
+ end
186
+
187
+ it 'matches arguments any' do
188
+ source = 'config.middleware.insert_after ActiveRecord::QueryCache, Lifo::Cache, page_cache: false'
189
+ instance.current_source = source
190
+ node = parse(source)
191
+ expect(node).to be_match(instance, type: 'send', arguments: {any: 'Lifo::Cache'})
192
+ end
193
+
194
+ it 'matches not' do
195
+ source = 'class Synvert; end'
196
+ instance.current_source = source
197
+ node = parse(source)
198
+ expect(node).not_to be_match(instance, type: 'class', name: {not: 'Synvert'})
199
+ end
200
+ end
201
+ end