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,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