Skip to content

Commit 6886416

Browse files
committed
add parse_ruby helper
1 parent 2a63106 commit 6886416

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

lib/helpers/parse_ruby.rb

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# frozen_string_literal: true
2+
3+
Synvert::Helper.new 'ruby/parse' do |options|
4+
configure(parser: Synvert::PRISM_PARSER)
5+
6+
# Set number_of_workers to 1 to skip parallel.
7+
with_configurations(number_of_workers: 1) do
8+
definitions = { modules: [], classes: [] }
9+
current_context = definitions
10+
context_stack = []
11+
12+
within_file Synvert::ALL_RUBY_FILES do
13+
add_callback :module_node, at: 'start' do |node|
14+
name = node.constant_path.to_source
15+
existing_module = current_context[:modules].find { |mod| mod[:name] == name }
16+
if existing_module
17+
new_context = existing_module
18+
else
19+
new_context = { name: name, modules: [], classes: [], methods: [], static_methods: [], constants: [] }
20+
current_context[:modules] << new_context
21+
end
22+
23+
context_stack.push(current_context)
24+
current_context = new_context
25+
end
26+
27+
add_callback :module_node, at: 'end' do |node|
28+
current_context = context_stack.pop
29+
end
30+
31+
add_callback :class_node, at: 'start' do |node|
32+
name = node.constant_path.to_source
33+
superclass = node.superclass&.to_source
34+
existing_class = current_context[:classes].find { |klass| klass[:name] == name }
35+
if existing_class
36+
new_context = existing_class
37+
else
38+
new_context = { name: name, superclass: superclass, modules: [], classes: [], methods: [], static_methods: [], singleton: {}, constants: [], included_modules: [] }
39+
current_context[:classes] << new_context
40+
end
41+
42+
context_stack.push(current_context)
43+
current_context = new_context
44+
end
45+
46+
add_callback :class_node, at: 'end' do |node|
47+
current_context = context_stack.pop
48+
end
49+
50+
add_callback :singleton_class_node, at: 'start' do |node|
51+
existing_singleton = current_context[:singleton]
52+
if !existing_singleton.empty?
53+
new_context = existing_singleton
54+
else
55+
new_context = { methods: [] }
56+
current_context[:singleton] = new_context
57+
end
58+
59+
context_stack.push(current_context)
60+
current_context = new_context
61+
end
62+
63+
add_callback :singleton_class_node, at: 'end' do |node|
64+
current_context = context_stack.pop
65+
end
66+
67+
add_callback :constant_write_node do |node|
68+
current_context[:constants] << { name: node.name.to_s }
69+
end
70+
71+
add_callback :call_node, at: 'start' do |node|
72+
if node.receiver.nil? && node.name == :include
73+
current_context[:included_modules] << node.arguments.arguments.first.to_source
74+
end
75+
end
76+
77+
add_callback :def_node, at: 'start' do |node|
78+
name = node.name.to_s
79+
80+
new_context = { name: name }
81+
if node.receiver.nil?
82+
current_context[:methods] << new_context
83+
else
84+
current_context[:static_methods] << new_context
85+
end
86+
87+
context_stack.push(current_context)
88+
current_context = new_context
89+
end
90+
91+
add_callback :def_node, at: 'end' do |node|
92+
current_context = context_stack.pop
93+
end
94+
end
95+
96+
def find_class(name, definitions)
97+
definitions[:classes].each do |klass|
98+
return klass if klass[:name] == name
99+
found = find_class(name, klass)
100+
return found if found
101+
end
102+
103+
definitions[:modules].each do |mod|
104+
found = find_class(name, mod)
105+
return found if found
106+
end
107+
108+
nil
109+
end
110+
111+
def add_ancestors(definitions)
112+
definitions[:classes].each do |klass|
113+
ancestors = []
114+
superclass = klass[:superclass]
115+
while superclass
116+
ancestors << superclass
117+
superclass_class = find_class(superclass, definitions)
118+
if superclass_class
119+
ancestors.concat(superclass_class[:included_modules]) if superclass_class[:included_modules]
120+
superclass = superclass_class[:superclass]
121+
else
122+
superclass = nil
123+
end
124+
end
125+
ancestors.concat(klass[:included_modules]) if klass[:included_modules]
126+
klass[:ancestors] = ancestors
127+
128+
add_ancestors(klass)
129+
end
130+
131+
definitions[:modules].each do |mod|
132+
add_ancestors(mod)
133+
end
134+
end
135+
136+
add_ancestors(definitions)
137+
138+
save_data :ruby_definitions, definitions
139+
end
140+
end

spec/helpers/parse_ruby_spec.rb

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'helpers/parse_ruby'
5+
6+
RSpec.describe 'ruby/parse helper', fakefs: true do
7+
it 'saves definitions data' do
8+
rewriter =
9+
Synvert::Rewriter.new 'test', 'ruby_parse_helper' do
10+
call_helper 'ruby/parse'
11+
end
12+
13+
FileUtils.mkdir_p('app/models')
14+
File.write('app/models/user.rb', <<~EOF)
15+
module Synvert
16+
class User
17+
include Trackable
18+
19+
ROLES = %w[user admin].freeze
20+
21+
class << self
22+
def system
23+
end
24+
25+
def bot
26+
end
27+
end
28+
29+
def self.authenticate?(email, password)
30+
end
31+
32+
def user_type
33+
"user"
34+
end
35+
end
36+
end
37+
EOF
38+
File.write('app/models/admin.rb', <<~EOF)
39+
module Synvert
40+
class Admin < User
41+
def user_type
42+
"admin"
43+
end
44+
end
45+
end
46+
EOF
47+
48+
rewriter.process
49+
50+
expect(rewriter.load_data(:ruby_definitions)).to eq({
51+
classes: [],
52+
modules: [
53+
{
54+
name: "Synvert",
55+
classes: [
56+
{
57+
name: "Admin",
58+
superclass: "User",
59+
singleton: {},
60+
classes: [],
61+
modules: [],
62+
methods: [{ name: "user_type" }],
63+
static_methods: [],
64+
constants: [],
65+
included_modules: [],
66+
ancestors: ["User", "Trackable"]
67+
},
68+
{
69+
name: "User",
70+
superclass: nil,
71+
singleton: {
72+
methods: [
73+
{ name: 'system' },
74+
{ name: 'bot' }
75+
]
76+
},
77+
classes: [],
78+
modules: [],
79+
methods: [{ name: "user_type" }],
80+
static_methods: [{ name: 'authenticate?' }],
81+
constants: [{ name: "ROLES" }],
82+
included_modules: ["Trackable"],
83+
ancestors: ["Trackable"]
84+
}
85+
],
86+
modules: [],
87+
methods: [],
88+
static_methods: [],
89+
constants: []
90+
}
91+
]
92+
})
93+
end
94+
end

0 commit comments

Comments
 (0)