tng 0.2.1
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 +7 -0
- data/Gemfile +15 -0
- data/LICENSE.md +32 -0
- data/README.md +413 -0
- data/Rakefile +124 -0
- data/bin/load_dev +22 -0
- data/bin/tng +888 -0
- data/binaries/tng.bundle +0 -0
- data/binaries/tng.so +0 -0
- data/lib/generators/tng/install_generator.rb +236 -0
- data/lib/tng/analyzers/controller.rb +114 -0
- data/lib/tng/analyzers/model.rb +131 -0
- data/lib/tng/analyzers/other.rb +277 -0
- data/lib/tng/analyzers/service.rb +150 -0
- data/lib/tng/api/http_client.rb +100 -0
- data/lib/tng/railtie.rb +11 -0
- data/lib/tng/services/direct_generation.rb +320 -0
- data/lib/tng/services/extract_methods.rb +39 -0
- data/lib/tng/services/test_generator.rb +287 -0
- data/lib/tng/services/testng.rb +100 -0
- data/lib/tng/services/user_app_config.rb +76 -0
- data/lib/tng/ui/about_display.rb +66 -0
- data/lib/tng/ui/authentication_warning_display.rb +172 -0
- data/lib/tng/ui/configuration_display.rb +52 -0
- data/lib/tng/ui/controller_test_flow_display.rb +79 -0
- data/lib/tng/ui/display_banner.rb +44 -0
- data/lib/tng/ui/goodbye_display.rb +41 -0
- data/lib/tng/ui/model_test_flow_display.rb +80 -0
- data/lib/tng/ui/other_test_flow_display.rb +78 -0
- data/lib/tng/ui/post_install_box.rb +80 -0
- data/lib/tng/ui/service_test_flow_display.rb +78 -0
- data/lib/tng/ui/show_help.rb +78 -0
- data/lib/tng/ui/system_status_display.rb +128 -0
- data/lib/tng/ui/theme.rb +258 -0
- data/lib/tng/ui/user_stats_display.rb +160 -0
- data/lib/tng/utils.rb +325 -0
- data/lib/tng/version.rb +5 -0
- data/lib/tng.rb +308 -0
- data/tng.gemspec +56 -0
- metadata +293 -0
data/binaries/tng.bundle
ADDED
Binary file
|
data/binaries/tng.so
ADDED
Binary file
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "yaml"
|
5
|
+
require "tng"
|
6
|
+
|
7
|
+
module Tng
|
8
|
+
module Generators
|
9
|
+
class InstallGenerator < Rails::Generators::Base
|
10
|
+
desc "Creates a tng.sh Ruby configuration file with comprehensive authentication and authorization support"
|
11
|
+
|
12
|
+
def self.source_root
|
13
|
+
@source_root ||= File.expand_path("templates", __dir__)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_tng_configuration
|
17
|
+
@test_framework = detect_test_framework
|
18
|
+
@authentication_library = detect_authentication_library
|
19
|
+
@authz_library = detect_authorization_library
|
20
|
+
@factory_library = detect_factory_library
|
21
|
+
|
22
|
+
initializer_path = "config/initializers/tng.rb"
|
23
|
+
|
24
|
+
if File.exist?(initializer_path)
|
25
|
+
say "Configuration file already exists at #{initializer_path}", :yellow
|
26
|
+
if yes?("Do you want to overwrite it? (y/n)")
|
27
|
+
remove_file initializer_path
|
28
|
+
create_file initializer_path, ruby_configuration_template
|
29
|
+
say "TNG configuration file updated!", :green
|
30
|
+
else
|
31
|
+
say "Skipping configuration file creation.", :blue
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
else
|
35
|
+
create_file initializer_path, ruby_configuration_template
|
36
|
+
say "tng.sh configuration file created at #{initializer_path}", :green
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def ruby_configuration_template
|
41
|
+
framework_config = generate_framework_config(@test_framework)
|
42
|
+
|
43
|
+
if @authentication_library && @authentication_library != "none"
|
44
|
+
auth_enabled = "config.authentication_enabled = true"
|
45
|
+
auth_comment = " (detected: #{@authentication_library})"
|
46
|
+
auth_lib = "config.authentication_library = \"#{@authentication_library}\""
|
47
|
+
else
|
48
|
+
auth_enabled = "config.authentication_enabled = true"
|
49
|
+
auth_comment = ""
|
50
|
+
auth_lib = "config.authentication_library = nil"
|
51
|
+
end
|
52
|
+
|
53
|
+
if @authz_library && @authz_library != "none"
|
54
|
+
authz_lib = "config.authorization_library = \"#{@authz_library}\""
|
55
|
+
authz_comment = " (detected: #{@authz_library})"
|
56
|
+
else
|
57
|
+
authz_lib = "config.authorization_library = nil"
|
58
|
+
authz_comment = ""
|
59
|
+
end
|
60
|
+
|
61
|
+
factory_lib = "config.factory_library = \"#{@factory_library}\""
|
62
|
+
|
63
|
+
framework_specific = generate_framework_specific_config(@test_framework, framework_config)
|
64
|
+
[
|
65
|
+
"# frozen_string_literal: true",
|
66
|
+
"",
|
67
|
+
"return if Rails.env.production?",
|
68
|
+
"",
|
69
|
+
"Tng.configure do |config|",
|
70
|
+
" config.api_key = nil",
|
71
|
+
" # You dont need to change this url, unless you will instructed by the CLI.",
|
72
|
+
" config.base_url = \"https://app.tng.sh/\"",
|
73
|
+
" config.read_source_code = true # Options: true, false",
|
74
|
+
" config.read_test_code = true # Options: true, false",
|
75
|
+
"",
|
76
|
+
" # Testing Framework",
|
77
|
+
" config.testing_framework = \"#{@test_framework}\" # Options: minitest, rspec",
|
78
|
+
framework_specific,
|
79
|
+
" config.mock_library = \"#{framework_config["mock_library"]}\" # Options: mocha, minitest/mock, rspec-mocks, nil",
|
80
|
+
" config.http_mock_library = \"#{framework_config["http_mock_library"]}\" # Options: webmock, vcr, httparty, nil",
|
81
|
+
" #{factory_lib} # Options: factory_bot, factory_girl, fabrication, fabricator, fixtures, active_record",
|
82
|
+
"",
|
83
|
+
" # Authentication#{auth_comment}",
|
84
|
+
" #{auth_enabled} # Options: true, false",
|
85
|
+
" #{auth_lib} # Options: devise, clearance, sorcery, nil",
|
86
|
+
"",
|
87
|
+
"",
|
88
|
+
" # ⚠️ IMPORTANT: AUTHENTICATION CONFIGURATION REQUIRED ⚠️",
|
89
|
+
" # You MUST configure your authentication methods below for TNG to work properly.",
|
90
|
+
" # Uncomment and modify the authentication_methods configuration:",
|
91
|
+
"",
|
92
|
+
" # Authentication Methods (multiple methods supported)",
|
93
|
+
" # Supported authentication types: session, devise, jwt, token_auth, basic_auth, oauth, headers, custom, nil",
|
94
|
+
" # EXAMPLE: Uncomment and modify these examples to match your app's authentication:",
|
95
|
+
"",
|
96
|
+
" # config.authentication_methods = [",
|
97
|
+
" # {",
|
98
|
+
" # method: \"authenticate_user_via_session!\",",
|
99
|
+
" # file_location: \"app/controllers/application_controller.rb\",",
|
100
|
+
" # auth_type: \"session\"",
|
101
|
+
" # },",
|
102
|
+
" # {",
|
103
|
+
" # method: \"authenticate_user_via_api_key!\",",
|
104
|
+
" # file_location: \"app/controllers/application_controller.rb\",",
|
105
|
+
" # auth_type: \"headers\"",
|
106
|
+
" # }",
|
107
|
+
" # ]",
|
108
|
+
" # ⚠️ Remember to configure your authentication methods above! ⚠️",
|
109
|
+
"",
|
110
|
+
" # Authorization#{authz_comment}",
|
111
|
+
" #{authz_lib} # Options: cancancan, pundit, rolify, nil",
|
112
|
+
"end"
|
113
|
+
].join("\n") + "\n"
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def detect_test_framework
|
119
|
+
rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
|
120
|
+
|
121
|
+
return "rspec" if File.exist?(File.join(rails_root, ".rspec")) ||
|
122
|
+
File.exist?(File.join(rails_root, "spec", "spec_helper.rb")) ||
|
123
|
+
Dir.exist?(File.join(rails_root, "spec"))
|
124
|
+
|
125
|
+
return "minitest" if Dir.exist?(File.join(rails_root, "test")) ||
|
126
|
+
File.exist?(File.join(rails_root, "test", "test_helper.rb"))
|
127
|
+
|
128
|
+
begin
|
129
|
+
if defined?(Bundler)
|
130
|
+
gemfile_specs = Bundler.load.specs
|
131
|
+
return "rspec" if gemfile_specs.any? { |spec| spec.name == "rspec-rails" }
|
132
|
+
return "minitest" if gemfile_specs.any? { |spec| spec.name == "minitest" }
|
133
|
+
end
|
134
|
+
rescue StandardError
|
135
|
+
# Fallback if Bundler isn't available
|
136
|
+
end
|
137
|
+
|
138
|
+
"minitest" # Default fallback
|
139
|
+
end
|
140
|
+
|
141
|
+
def detect_authentication_library
|
142
|
+
return unless defined?(Bundler)
|
143
|
+
|
144
|
+
begin
|
145
|
+
gemfile_specs = Bundler.load.specs
|
146
|
+
return "devise" if gemfile_specs.any? { |spec| spec.name == "devise" }
|
147
|
+
return "clearance" if gemfile_specs.any? { |spec| spec.name == "clearance" }
|
148
|
+
return "sorcery" if gemfile_specs.any? { |spec| spec.name == "sorcery" }
|
149
|
+
rescue StandardError
|
150
|
+
# Fallback if Bundler isn't available
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
|
157
|
+
def detect_authorization_library
|
158
|
+
return unless defined?(Bundler)
|
159
|
+
|
160
|
+
gemfile_specs = Bundler.load.specs
|
161
|
+
return "cancancan" if gemfile_specs.any? { |spec| spec.name == "cancancan" }
|
162
|
+
return "pundit" if gemfile_specs.any? { |spec| spec.name == "pundit" }
|
163
|
+
return "rolify" if gemfile_specs.any? { |spec| spec.name == "rolify" }
|
164
|
+
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
|
168
|
+
def detect_factory_library
|
169
|
+
return "active_record" unless defined?(Bundler)
|
170
|
+
|
171
|
+
rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
|
172
|
+
|
173
|
+
# Check for fixtures in test/fixtures or spec/fixtures
|
174
|
+
test_fixtures_path = File.join(rails_root, "test", "fixtures")
|
175
|
+
spec_fixtures_path = File.join(rails_root, "spec", "fixtures")
|
176
|
+
|
177
|
+
return "fixtures" if Dir.exist?(test_fixtures_path) && !Dir.empty?(test_fixtures_path)
|
178
|
+
|
179
|
+
return "fixtures" if Dir.exist?(spec_fixtures_path) && !Dir.empty?(spec_fixtures_path)
|
180
|
+
|
181
|
+
gemfile_specs = Bundler.load.specs
|
182
|
+
return "factory_bot" if gemfile_specs.any? { |spec| spec.name.match(/factory_bot/) }
|
183
|
+
return "factory_girl" if gemfile_specs.any? { |spec| spec.name.match(/factory_girl/) }
|
184
|
+
return "fabrication" if gemfile_specs.any? { |spec| spec.name.match(/fabrication/) }
|
185
|
+
return "fabricator" if gemfile_specs.any? { |spec| spec.name.match(/fabricator/) }
|
186
|
+
|
187
|
+
"active_record"
|
188
|
+
rescue StandardError
|
189
|
+
# Fallback if Bundler isn't available
|
190
|
+
"active_record"
|
191
|
+
end
|
192
|
+
|
193
|
+
def generate_framework_config(framework)
|
194
|
+
if framework == "minitest"
|
195
|
+
{
|
196
|
+
"test_style" => "test_block", # Options: spec, unit, test_block
|
197
|
+
"setup_style" => true, # Options: true, false
|
198
|
+
"assertion_style" => "assert/refute", # Options: assert/refute, assert/assert_not, must/wont
|
199
|
+
"teardown_style" => false, # Options: true, false
|
200
|
+
"http_mock_library" => "webmock", # Options: webmock, vcr, httparty, none
|
201
|
+
"mock_library" => "minitest/mock" # Options: mocha, minitest/mock, rspec-mocks, none
|
202
|
+
}
|
203
|
+
else # rspec
|
204
|
+
{
|
205
|
+
"describe_style" => true, # Options: true, false
|
206
|
+
"context_style" => "context", # Options: context, describe
|
207
|
+
"it_style" => "it", # Options: it, specify
|
208
|
+
"before_style" => "before", # Options: before, setup
|
209
|
+
"after_style" => "after", # Options: after, teardown
|
210
|
+
"let_style" => true, # Options: true, false
|
211
|
+
"subject_style" => true, # Options: true, false
|
212
|
+
"mock_library" => "rspec-mocks", # Options: rspec-mocks, mocha, flexmock, none
|
213
|
+
"http_mock_library" => "webmock" # Options: webmock, vcr, httparty, none
|
214
|
+
}
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def generate_framework_specific_config(framework, framework_config)
|
219
|
+
if framework == "minitest"
|
220
|
+
" config.test_style = \"#{framework_config["test_style"]}\" # Options: spec, unit, test_block\n" +
|
221
|
+
" config.setup_style = #{framework_config["setup_style"]} # Options: true, false\n" +
|
222
|
+
" config.assertion_style = \"#{framework_config["assertion_style"]}\" # Options: assert/refute, assert/assert_not, must/wont\n" +
|
223
|
+
" config.teardown_style = #{framework_config["teardown_style"]} # Options: true, false"
|
224
|
+
else # rspec
|
225
|
+
" config.describe_style = #{framework_config["describe_style"]} # Options: true, false\n" +
|
226
|
+
" config.context_style = \"#{framework_config["context_style"]}\" # Options: context, describe\n" +
|
227
|
+
" config.it_style = \"#{framework_config["it_style"]}\" # Options: it, specify\n" +
|
228
|
+
" config.before_style = \"#{framework_config["before_style"]}\" # Options: before, setup\n" +
|
229
|
+
" config.after_style = \"#{framework_config["after_style"]}\" # Options: after, teardown\n" +
|
230
|
+
" config.let_style = #{framework_config["let_style"]} # Options: true, false\n" +
|
231
|
+
" config.subject_style = #{framework_config["subject_style"]} # Options: true, false"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Analyzers
|
5
|
+
class Controller
|
6
|
+
def self.files_in_dir(dir)
|
7
|
+
dir = File.join(Dir.pwd, "app/controllers") if dir.nil?
|
8
|
+
Tng::Analyzer::Controller.files_in_dir(dir.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.routes_for_controller(file_path)
|
12
|
+
raise "file_path is required" if file_path.nil?
|
13
|
+
|
14
|
+
Tng::Analyzer::Controller.routes_for_controller(file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.value_for_controller(file_path)
|
18
|
+
raise "file_path is required" if file_path.nil?
|
19
|
+
|
20
|
+
Tng::Analyzer::Controller.parse_controller_file(file_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.parents_for_controller(controller)
|
24
|
+
raise "controller is required" if controller.nil?
|
25
|
+
|
26
|
+
path = controller[:path]
|
27
|
+
relative_path = path.gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
|
28
|
+
clean_controller_name = relative_path.split("/").map(&:camelize).join("::")
|
29
|
+
|
30
|
+
begin
|
31
|
+
Tng::Analyzer::Controller.parents_for_controller(clean_controller_name)
|
32
|
+
rescue NameError => e
|
33
|
+
puts "❌ Error analyzing controller #{clean_controller_name}: #{e.message}"
|
34
|
+
puts " Path: #{path}"
|
35
|
+
puts " This might be due to a syntax error in the controller file."
|
36
|
+
[]
|
37
|
+
rescue StandardError => e
|
38
|
+
puts "❌ Error analyzing controller #{clean_controller_name}: #{e.message}"
|
39
|
+
puts " Path: #{path}"
|
40
|
+
puts " This might be due to a syntax error in the controller file."
|
41
|
+
[]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.model_info_for_controller(file_path)
|
46
|
+
raise "file_path is required" if file_path.nil?
|
47
|
+
|
48
|
+
Tng::Analyzer::Controller.model_info_for_controller(file_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.find_test_file_for_controller(controller_path)
|
52
|
+
controller_name = File.basename(controller_path, ".rb")
|
53
|
+
testing_framework = Tng.testing_framework
|
54
|
+
return [] if testing_framework.nil?
|
55
|
+
|
56
|
+
paths = if testing_framework.downcase == "rspec"
|
57
|
+
[
|
58
|
+
"spec/controllers/#{controller_name}_spec.rb",
|
59
|
+
"spec/requests/#{controller_name}_spec.rb"
|
60
|
+
]
|
61
|
+
else
|
62
|
+
[
|
63
|
+
"test/controllers/#{controller_name}_test.rb",
|
64
|
+
"test/functional/#{controller_name}_test.rb"
|
65
|
+
]
|
66
|
+
end
|
67
|
+
|
68
|
+
paths.select { |path| File.exist?(path) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.read_test_file_for_controller(controller_path)
|
72
|
+
files = find_test_file_for_controller(controller_path)
|
73
|
+
|
74
|
+
files.map do |file_path|
|
75
|
+
{
|
76
|
+
path: file_path,
|
77
|
+
content: File.read(file_path)
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.methods_for_controller(controller_name)
|
83
|
+
raise "controller_name is required" if controller_name.nil?
|
84
|
+
|
85
|
+
begin
|
86
|
+
# Load the controller class
|
87
|
+
controller_class = controller_name.constantize
|
88
|
+
|
89
|
+
# Get public instance methods (actions) excluding inherited ones
|
90
|
+
public_methods = controller_class.public_instance_methods(false)
|
91
|
+
|
92
|
+
# Filter to methods defined in this controller only
|
93
|
+
action_methods = public_methods.select do |method_name|
|
94
|
+
method = controller_class.instance_method(method_name)
|
95
|
+
method.owner == controller_class
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return method info
|
99
|
+
action_methods.map do |method_name|
|
100
|
+
{
|
101
|
+
name: method_name.to_s
|
102
|
+
}
|
103
|
+
end
|
104
|
+
rescue NameError => e
|
105
|
+
puts "❌ Could not load controller class #{controller_name}: #{e.message}"
|
106
|
+
[]
|
107
|
+
rescue StandardError => e
|
108
|
+
puts "❌ Error analyzing controller #{controller_name}: #{e.message}"
|
109
|
+
[]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Tng
|
4
|
+
module Analyzers
|
5
|
+
class Model
|
6
|
+
def self.files_in_dir(dir = nil)
|
7
|
+
dir = File.join(Dir.pwd, "app/models") if dir.nil?
|
8
|
+
Tng::Analyzer::Model.files_in_dir(dir.to_s)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.value_for_model(file_path)
|
12
|
+
raise "file_path is required" if file_path.nil?
|
13
|
+
|
14
|
+
Tng::Analyzer::Model.parse_model_file(file_path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.read_test_file_for_model(model_path)
|
18
|
+
files = find_test_file_for_model(model_path)
|
19
|
+
|
20
|
+
files.map do |file_path|
|
21
|
+
{
|
22
|
+
path: file_path,
|
23
|
+
content: File.read(file_path)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find_test_file_for_model(model_path)
|
29
|
+
model_name = File.basename(model_path, ".rb")
|
30
|
+
testing_framework = Tng.testing_framework
|
31
|
+
return [] if testing_framework.nil?
|
32
|
+
|
33
|
+
paths = if testing_framework.downcase == "rspec"
|
34
|
+
[
|
35
|
+
"spec/models/#{model_name}_spec.rb",
|
36
|
+
"spec/requests/#{model_name}_spec.rb"
|
37
|
+
]
|
38
|
+
else
|
39
|
+
[
|
40
|
+
"test/models/#{model_name}_test.rb",
|
41
|
+
"test/functional/#{model_name}_test.rb"
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
paths.select { |path| File.exist?(path) }
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.model_connections(model)
|
49
|
+
raise "model is required" if model.nil?
|
50
|
+
|
51
|
+
file_path = model.is_a?(Hash) ? model[:path] : model
|
52
|
+
raise "model path is required" if file_path.nil?
|
53
|
+
|
54
|
+
Tng::Analyzer::Model.model_connections(file_path.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.methods_for_model(model_name)
|
58
|
+
raise "model_name is required" if model_name.nil?
|
59
|
+
|
60
|
+
begin
|
61
|
+
# Load the model class
|
62
|
+
model_class = model_name.constantize
|
63
|
+
|
64
|
+
instance_methods = model_class.public_instance_methods(false)
|
65
|
+
class_methods = model_class.public_methods(false) - Class.public_methods
|
66
|
+
|
67
|
+
model_file = model_class.const_source_location(model_class.name.split("::").last)&.first
|
68
|
+
|
69
|
+
model_methods = if model_file && File.exist?(model_file)
|
70
|
+
source_code = File.read(model_file)
|
71
|
+
result = Prism.parse(source_code)
|
72
|
+
|
73
|
+
defined_methods = []
|
74
|
+
extract_method_names(result.value, defined_methods)
|
75
|
+
|
76
|
+
filtered_instance_methods = instance_methods.select do |method_name|
|
77
|
+
method = model_class.instance_method(method_name)
|
78
|
+
next false unless method.owner == model_class
|
79
|
+
|
80
|
+
defined_methods.include?(method_name.to_s)
|
81
|
+
end
|
82
|
+
|
83
|
+
filtered_class_methods = class_methods.select do |method_name|
|
84
|
+
defined_methods.include?(method_name.to_s)
|
85
|
+
end
|
86
|
+
|
87
|
+
filtered_instance_methods + filtered_class_methods
|
88
|
+
else
|
89
|
+
[]
|
90
|
+
end
|
91
|
+
|
92
|
+
model_methods.map { |method_name| { name: method_name.to_s } }
|
93
|
+
rescue NameError => e
|
94
|
+
puts "❌ Could not load model class #{model_name}: #{e.message}"
|
95
|
+
[]
|
96
|
+
rescue StandardError => e
|
97
|
+
puts "❌ Error analyzing model #{model_name}: #{e.message}"
|
98
|
+
[]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.extract_method_names(node, methods)
|
103
|
+
return unless node.is_a?(Prism::Node)
|
104
|
+
|
105
|
+
case node
|
106
|
+
when Prism::DefNode
|
107
|
+
# Both instance and class methods (def self.method_name)
|
108
|
+
methods << node.name.to_s
|
109
|
+
when Prism::CallNode
|
110
|
+
# Handle scope definitions: scope :name, -> { ... }
|
111
|
+
if node.name == :scope && node.arguments&.arguments&.any?
|
112
|
+
first_arg = node.arguments.arguments.first
|
113
|
+
if first_arg.is_a?(Prism::SymbolNode)
|
114
|
+
scope_name = first_arg.value
|
115
|
+
methods << scope_name if scope_name
|
116
|
+
end
|
117
|
+
end
|
118
|
+
when Prism::SingletonClassNode
|
119
|
+
# Methods inside class << self blocks
|
120
|
+
node.body&.child_nodes&.each do |child|
|
121
|
+
extract_method_names(child, methods)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
node.child_nodes.each do |child|
|
126
|
+
extract_method_names(child, methods)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|