railsforge 1.0.2 → 2.0.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.
- checksums.yaml +4 -4
- data/README.md +105 -444
- data/lib/railsforge/analyzers/controller_analyzer.rb +29 -55
- data/lib/railsforge/analyzers/database_analyzer.rb +16 -30
- data/lib/railsforge/analyzers/metrics_analyzer.rb +8 -22
- data/lib/railsforge/analyzers/model_analyzer.rb +29 -46
- data/lib/railsforge/analyzers/performance_analyzer.rb +34 -94
- data/lib/railsforge/analyzers/refactor_analyzer.rb +77 -57
- data/lib/railsforge/analyzers/security_analyzer.rb +34 -91
- data/lib/railsforge/analyzers/spec_analyzer.rb +17 -31
- data/lib/railsforge/cli.rb +14 -650
- data/lib/railsforge/cli_minimal.rb +8 -55
- data/lib/railsforge/doctor.rb +52 -225
- data/lib/railsforge/formatter.rb +102 -0
- data/lib/railsforge/issue.rb +23 -0
- data/lib/railsforge/loader.rb +4 -64
- data/lib/railsforge/version.rb +1 -1
- metadata +14 -82
- data/lib/railsforge/api_generator.rb +0 -397
- data/lib/railsforge/audit.rb +0 -289
- data/lib/railsforge/config.rb +0 -181
- data/lib/railsforge/database_analyzer.rb +0 -300
- data/lib/railsforge/feature_generator.rb +0 -560
- data/lib/railsforge/generator.rb +0 -313
- data/lib/railsforge/generators/api_generator.rb +0 -392
- data/lib/railsforge/generators/base_generator.rb +0 -75
- data/lib/railsforge/generators/demo_generator.rb +0 -307
- data/lib/railsforge/generators/devops_generator.rb +0 -287
- data/lib/railsforge/generators/form_generator.rb +0 -180
- data/lib/railsforge/generators/job_generator.rb +0 -176
- data/lib/railsforge/generators/monitoring_generator.rb +0 -134
- data/lib/railsforge/generators/policy_generator.rb +0 -220
- data/lib/railsforge/generators/presenter_generator.rb +0 -173
- data/lib/railsforge/generators/query_generator.rb +0 -174
- data/lib/railsforge/generators/serializer_generator.rb +0 -166
- data/lib/railsforge/generators/service_generator.rb +0 -122
- data/lib/railsforge/generators/stimulus_controller_generator.rb +0 -129
- data/lib/railsforge/generators/test_generator.rb +0 -289
- data/lib/railsforge/generators/view_component_generator.rb +0 -169
- data/lib/railsforge/graph.rb +0 -270
- data/lib/railsforge/mailer_generator.rb +0 -191
- data/lib/railsforge/plugins/plugin_loader.rb +0 -60
- data/lib/railsforge/plugins.rb +0 -30
- data/lib/railsforge/profiles.rb +0 -99
- data/lib/railsforge/refactor_analyzer.rb +0 -401
- data/lib/railsforge/refactor_controller.rb +0 -277
- data/lib/railsforge/refactors/refactor_controller.rb +0 -117
- data/lib/railsforge/template_loader.rb +0 -105
- data/lib/railsforge/templates/v1/form/spec_template.rb +0 -18
- data/lib/railsforge/templates/v1/form/template.rb +0 -28
- data/lib/railsforge/templates/v1/job/spec_template.rb +0 -17
- data/lib/railsforge/templates/v1/job/template.rb +0 -13
- data/lib/railsforge/templates/v1/policy/spec_template.rb +0 -41
- data/lib/railsforge/templates/v1/policy/template.rb +0 -57
- data/lib/railsforge/templates/v1/presenter/spec_template.rb +0 -12
- data/lib/railsforge/templates/v1/presenter/template.rb +0 -13
- data/lib/railsforge/templates/v1/query/spec_template.rb +0 -12
- data/lib/railsforge/templates/v1/query/template.rb +0 -16
- data/lib/railsforge/templates/v1/serializer/spec_template.rb +0 -13
- data/lib/railsforge/templates/v1/serializer/template.rb +0 -11
- data/lib/railsforge/templates/v1/service/spec_template.rb +0 -12
- data/lib/railsforge/templates/v1/service/template.rb +0 -25
- data/lib/railsforge/templates/v1/stimulus_controller/template.rb +0 -35
- data/lib/railsforge/templates/v1/view_component/template.rb +0 -24
- data/lib/railsforge/templates/v2/job/template.rb +0 -49
- data/lib/railsforge/templates/v2/query/template.rb +0 -66
- data/lib/railsforge/templates/v2/service/spec_template.rb +0 -33
- data/lib/railsforge/templates/v2/service/template.rb +0 -71
- data/lib/railsforge/templates/v3/job/template.rb +0 -72
- data/lib/railsforge/templates/v3/query/spec_template.rb +0 -54
- data/lib/railsforge/templates/v3/query/template.rb +0 -115
- data/lib/railsforge/templates/v3/service/spec_template.rb +0 -51
- data/lib/railsforge/templates/v3/service/template.rb +0 -93
- data/lib/railsforge/wizard.rb +0 -265
- data/lib/railsforge/wizard_tui.rb +0 -286
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
module RailsForge
|
|
2
|
-
# RefactorAnalyzer module handles refactoring suggestions and code extraction
|
|
3
|
-
module RefactorAnalyzer
|
|
4
|
-
# Error class for refactoring issues
|
|
5
|
-
class RefactorError < StandardError; end
|
|
6
|
-
|
|
7
|
-
# Configuration thresholds
|
|
8
|
-
CONTROLLER_MAX_LINES = 150
|
|
9
|
-
CONTROLLER_MAX_METHODS = 10
|
|
10
|
-
MODEL_MAX_LINES = 200
|
|
11
|
-
MODEL_MAX_METHOD_LINES = 15
|
|
12
|
-
|
|
13
|
-
# Analyzes controllers for refactoring opportunities
|
|
14
|
-
# @param base_path [String] Rails app root path
|
|
15
|
-
# @return [Array<Hash>] Array of refactoring suggestions
|
|
16
|
-
def self.analyze_controllers(base_path = nil)
|
|
17
|
-
base_path ||= find_rails_app_path
|
|
18
|
-
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
19
|
-
|
|
20
|
-
controllers_dir = File.join(base_path, "app", "controllers")
|
|
21
|
-
return [] unless Dir.exist?(controllers_dir)
|
|
22
|
-
|
|
23
|
-
results = []
|
|
24
|
-
Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
|
|
25
|
-
result = analyze_controller_file(file)
|
|
26
|
-
results << result if result[:needs_refactoring]
|
|
27
|
-
end
|
|
28
|
-
results
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Analyzes models for refactoring opportunities
|
|
32
|
-
# @param base_path [String] Rails app root path
|
|
33
|
-
# @return [Array<Hash>] Array of refactoring suggestions
|
|
34
|
-
def self.analyze_models(base_path = nil)
|
|
35
|
-
base_path ||= find_rails_app_path
|
|
36
|
-
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
37
|
-
|
|
38
|
-
models_dir = File.join(base_path, "app", "models")
|
|
39
|
-
return [] unless Dir.exist?(models_dir)
|
|
40
|
-
|
|
41
|
-
results = []
|
|
42
|
-
Dir.glob(File.join(models_dir, "**", "*.rb")).each do |file|
|
|
43
|
-
next if file.end_with?("_application.rb")
|
|
44
|
-
result = analyze_model_file(file)
|
|
45
|
-
results << result if result[:needs_refactoring]
|
|
46
|
-
end
|
|
47
|
-
results
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Analyzes a controller file
|
|
51
|
-
# @param file_path [String] Path to controller file
|
|
52
|
-
# @return [Hash] Analysis results
|
|
53
|
-
def self.analyze_controller_file(file_path)
|
|
54
|
-
content = File.read(file_path)
|
|
55
|
-
lines = content.lines.count
|
|
56
|
-
methods = extract_methods(content)
|
|
57
|
-
|
|
58
|
-
issues = []
|
|
59
|
-
suggestions = []
|
|
60
|
-
|
|
61
|
-
if lines > CONTROLLER_MAX_LINES
|
|
62
|
-
issues << "Controller exceeds #{CONTROLLER_MAX_LINES} lines (currently #{lines})"
|
|
63
|
-
suggestions << "Consider moving business logic into a Service object"
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
if methods.count > CONTROLLER_MAX_METHODS
|
|
67
|
-
issues << "Controller has #{methods.count} methods (recommended: #{CONTROLLER_MAX_METHODS} or less)"
|
|
68
|
-
suggestions << "Consider extracting some actions into separate controllers or using a Service"
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Find long methods that could be extracted
|
|
72
|
-
methods.each do |method|
|
|
73
|
-
if method[:lines] > MODEL_MAX_METHOD_LINES
|
|
74
|
-
suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting to Service"
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
type: :controller,
|
|
80
|
-
file: File.basename(file_path),
|
|
81
|
-
path: file_path,
|
|
82
|
-
lines: lines,
|
|
83
|
-
methods: methods,
|
|
84
|
-
issues: issues,
|
|
85
|
-
suggestions: suggestions,
|
|
86
|
-
needs_refactoring: issues.any? || suggestions.any?
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Analyzes a model file
|
|
91
|
-
# @param file_path [String] Path to model file
|
|
92
|
-
# @return [Hash] Analysis results
|
|
93
|
-
def self.analyze_model_file(file_path)
|
|
94
|
-
content = File.read(file_path)
|
|
95
|
-
lines = content.lines.count
|
|
96
|
-
methods = extract_methods(content)
|
|
97
|
-
|
|
98
|
-
issues = []
|
|
99
|
-
suggestions = []
|
|
100
|
-
|
|
101
|
-
if lines > MODEL_MAX_LINES
|
|
102
|
-
issues << "Model exceeds #{MODEL_MAX_LINES} lines (currently #{lines})"
|
|
103
|
-
suggestions << "Consider extracting scopes into Query objects or validations to a Form"
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Find long methods
|
|
107
|
-
methods.each do |method|
|
|
108
|
-
if method[:lines] > MODEL_MAX_METHOD_LINES
|
|
109
|
-
suggestions << "Method `#{method[:name]}` has #{method[:lines]} lines - consider extracting to a Service"
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
type: :model,
|
|
115
|
-
file: File.basename(file_path),
|
|
116
|
-
path: file_path,
|
|
117
|
-
lines: lines,
|
|
118
|
-
methods: methods,
|
|
119
|
-
issues: issues,
|
|
120
|
-
suggestions: suggestions,
|
|
121
|
-
needs_refactoring: issues.any? || suggestions.any?
|
|
122
|
-
}
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Extracts method names and line counts from content
|
|
126
|
-
# @param content [String] Ruby code content
|
|
127
|
-
# @return [Array<Hash>] Array of method info
|
|
128
|
-
def self.extract_methods(content)
|
|
129
|
-
methods = []
|
|
130
|
-
|
|
131
|
-
# Match def method_name or def self.method_name
|
|
132
|
-
content.scan(/def\s+(self\.)?([a-z_][a-zA-Z_]*)/) do |prefix, name|
|
|
133
|
-
methods << {
|
|
134
|
-
name: name,
|
|
135
|
-
is_class_method: prefix == "self.",
|
|
136
|
-
lines: 1 # Simplified - just mark as present
|
|
137
|
-
}
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
methods
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Counts lines in a method
|
|
144
|
-
# @param content [String] Ruby code content
|
|
145
|
-
# @param method_name [String] Method name
|
|
146
|
-
# @param start_line [Integer] Starting line number
|
|
147
|
-
# @return [Integer] Number of lines
|
|
148
|
-
def self.count_method_lines(content, method_name, start_line)
|
|
149
|
-
# Find the end of the method
|
|
150
|
-
lines = content.lines
|
|
151
|
-
end_pos = content.length
|
|
152
|
-
|
|
153
|
-
# Look for next def or class or end
|
|
154
|
-
rest = content.lines[start_line..-1].join
|
|
155
|
-
if rest =~ /\n\s*def\s+(self\.)?[a-z_]/i
|
|
156
|
-
end_pos = $~.begin(0)
|
|
157
|
-
elsif rest =~ /\n\s*(class|module)\s+/
|
|
158
|
-
end_pos = $~.begin(0)
|
|
159
|
-
elsif rest =~ /\n\s*end\s*$/
|
|
160
|
-
end_pos = $~.begin(0)
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
content[start_line..end_pos].lines.count
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
# Generates a service file for extracted logic
|
|
167
|
-
# @param name [String] Service name
|
|
168
|
-
# @param logic [String] Logic to extract
|
|
169
|
-
# @param base_path [String] Rails app root
|
|
170
|
-
# @return [String] Path to created file
|
|
171
|
-
def self.generate_service(name, logic, base_path = nil)
|
|
172
|
-
base_path ||= find_rails_app_path
|
|
173
|
-
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
174
|
-
|
|
175
|
-
service_dir = File.join(base_path, "app", "services")
|
|
176
|
-
FileUtils.mkdir_p(service_dir)
|
|
177
|
-
|
|
178
|
-
file_name = "#{name.underscore}_service.rb"
|
|
179
|
-
file_path = File.join(service_dir, file_name)
|
|
180
|
-
|
|
181
|
-
if File.exist?(file_path)
|
|
182
|
-
puts " Skipping service (already exists)"
|
|
183
|
-
return file_path
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
content = <<~RUBY
|
|
187
|
-
# Service class for #{name}
|
|
188
|
-
# Extracted from controller/model logic
|
|
189
|
-
#
|
|
190
|
-
# Usage:
|
|
191
|
-
# #{name}Service.call(params)
|
|
192
|
-
class #{name}Service
|
|
193
|
-
def initialize(**args)
|
|
194
|
-
@args = args
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def call
|
|
198
|
-
# Extracted logic:
|
|
199
|
-
# #{logic.gsub("\n", "\n # ")}
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
|
-
RUBY
|
|
203
|
-
|
|
204
|
-
File.write(file_path, content)
|
|
205
|
-
puts " Created app/services/#{file_name}"
|
|
206
|
-
file_path
|
|
207
|
-
end
|
|
208
|
-
|
|
209
|
-
# Generates a query file for extracted logic
|
|
210
|
-
# @param name [String] Query name
|
|
211
|
-
# @param scope [String] Scope logic
|
|
212
|
-
# @param base_path [String] Rails app root
|
|
213
|
-
# @return [String] Path to created file
|
|
214
|
-
def self.generate_query(name, scope, base_path = nil)
|
|
215
|
-
base_path ||= find_rails_app_path
|
|
216
|
-
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
217
|
-
|
|
218
|
-
query_dir = File.join(base_path, "app", "queries")
|
|
219
|
-
FileUtils.mkdir_p(query_dir)
|
|
220
|
-
|
|
221
|
-
file_name = "find_#{name.underscore}.rb"
|
|
222
|
-
file_path = File.join(query_dir, file_name)
|
|
223
|
-
|
|
224
|
-
if File.exist?(file_path)
|
|
225
|
-
puts " Skipping query (already exists)"
|
|
226
|
-
return file_path
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
content = <<~RUBY
|
|
230
|
-
# Query class for #{name}
|
|
231
|
-
# Extracted scope/query logic
|
|
232
|
-
#
|
|
233
|
-
# Usage:
|
|
234
|
-
# Find#{name}.call
|
|
235
|
-
class Find#{name}
|
|
236
|
-
def initialize(scope: nil)
|
|
237
|
-
@scope = scope || #{name}.all
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
def call
|
|
241
|
-
# Extracted scope:
|
|
242
|
-
# #{scope.gsub("\n", "\n # ")}
|
|
243
|
-
@scope
|
|
244
|
-
end
|
|
245
|
-
end
|
|
246
|
-
RUBY
|
|
247
|
-
|
|
248
|
-
File.write(file_path, content)
|
|
249
|
-
puts " Created app/queries/#{file_name}"
|
|
250
|
-
file_path
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
# Generates an RSpec test for a service
|
|
254
|
-
# @param name [String] Service name
|
|
255
|
-
# @param base_path [String] Rails app root
|
|
256
|
-
# @return [String] Path to created file
|
|
257
|
-
def self.generate_service_spec(name, base_path = nil)
|
|
258
|
-
base_path ||= find_rails_app_path
|
|
259
|
-
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
260
|
-
|
|
261
|
-
spec_dir = File.join(base_path, "spec", "services")
|
|
262
|
-
FileUtils.mkdir_p(spec_dir)
|
|
263
|
-
|
|
264
|
-
file_name = "#{name.underscore}_service_spec.rb"
|
|
265
|
-
file_path = File.join(spec_dir, file_name)
|
|
266
|
-
|
|
267
|
-
if File.exist?(file_path)
|
|
268
|
-
puts " Skipping spec (already exists)"
|
|
269
|
-
return file_path
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
content = <<~RUBY
|
|
273
|
-
require 'rails_helper'
|
|
274
|
-
|
|
275
|
-
RSpec.describe #{name}Service do
|
|
276
|
-
let(:params) { {} }
|
|
277
|
-
subject { described_class.new(params) }
|
|
278
|
-
|
|
279
|
-
describe '#call' do
|
|
280
|
-
it 'returns successful result' do
|
|
281
|
-
expect(subject.call).to be_truthy
|
|
282
|
-
end
|
|
283
|
-
end
|
|
284
|
-
end
|
|
285
|
-
RUBY
|
|
286
|
-
|
|
287
|
-
File.write(file_path, content)
|
|
288
|
-
puts " Created spec/services/#{file_name}"
|
|
289
|
-
file_path
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# Generates an RSpec test for a query
|
|
293
|
-
# @param name [String] Query name
|
|
294
|
-
# @param base_path [String] Rails app root
|
|
295
|
-
# @return [String] Path to created file
|
|
296
|
-
def self.generate_query_spec(name, base_path = nil)
|
|
297
|
-
base_path ||= find_rails_app_path
|
|
298
|
-
raise RefactorError, "Not in a Rails application directory" unless base_path
|
|
299
|
-
|
|
300
|
-
spec_dir = File.join(base_path, "spec", "queries")
|
|
301
|
-
FileUtils.mkdir_p(spec_dir)
|
|
302
|
-
|
|
303
|
-
file_name = "find_#{name.underscore}_spec.rb"
|
|
304
|
-
file_path = File.join(spec_dir, file_name)
|
|
305
|
-
|
|
306
|
-
if File.exist?(file_path)
|
|
307
|
-
puts " Skipping spec (already exists)"
|
|
308
|
-
return file_path
|
|
309
|
-
end
|
|
310
|
-
|
|
311
|
-
content = <<~RUBY
|
|
312
|
-
require 'rails_helper'
|
|
313
|
-
|
|
314
|
-
RSpec.describe Find#{name} do
|
|
315
|
-
let(:scope) { #{name}.all }
|
|
316
|
-
subject { described_class.new(scope: scope) }
|
|
317
|
-
|
|
318
|
-
describe '#call' do
|
|
319
|
-
it 'returns scope' do
|
|
320
|
-
expect(subject.call).to eq(scope)
|
|
321
|
-
end
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
RUBY
|
|
325
|
-
|
|
326
|
-
File.write(file_path, content)
|
|
327
|
-
puts " Created spec/queries/#{file_name}"
|
|
328
|
-
file_path
|
|
329
|
-
end
|
|
330
|
-
|
|
331
|
-
# Extracts code from a controller/model and creates a service
|
|
332
|
-
# @param file_path [String] Source file path
|
|
333
|
-
# @param method_names [Array<String>] Methods to extract
|
|
334
|
-
# @param service_name [String] Name for new service
|
|
335
|
-
# @return [Hash] Results
|
|
336
|
-
def self.extract_to_service(file_path, method_names, service_name)
|
|
337
|
-
content = File.read(file_path)
|
|
338
|
-
|
|
339
|
-
extracted_logic = []
|
|
340
|
-
method_names.each do |method_name|
|
|
341
|
-
# Find method in content
|
|
342
|
-
if content.include?("def #{method_name}")
|
|
343
|
-
# Extract method and its body
|
|
344
|
-
method_match = content.match(/def #{method_name}.*?(\n\s*end\n)/m)
|
|
345
|
-
extracted_logic << method_match[0] if method_match
|
|
346
|
-
end
|
|
347
|
-
end
|
|
348
|
-
|
|
349
|
-
base_path = find_rails_app_path
|
|
350
|
-
generate_service(service_name, extracted_logic.join("\n"), base_path)
|
|
351
|
-
generate_service_spec(service_name, base_path)
|
|
352
|
-
|
|
353
|
-
{
|
|
354
|
-
service: "app/services/#{service_name.underscore}_service.rb",
|
|
355
|
-
spec: "spec/services/#{service_name.underscore}_service_spec.rb"
|
|
356
|
-
}
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# Prints refactoring report
|
|
360
|
-
# @param results [Array<Hash>] Analysis results
|
|
361
|
-
def self.print_report(results)
|
|
362
|
-
return puts "\n✓ No refactoring needed!" if results.empty?
|
|
363
|
-
|
|
364
|
-
puts "\n" + "=" * 60
|
|
365
|
-
puts "REFACTORING REPORT"
|
|
366
|
-
puts "=" * 60
|
|
367
|
-
|
|
368
|
-
results.each do |result|
|
|
369
|
-
puts "\n📁 #{result[:file]} (#{result[:type]})"
|
|
370
|
-
puts " Lines: #{result[:lines]}"
|
|
371
|
-
|
|
372
|
-
if result[:issues].any?
|
|
373
|
-
puts "\n ⚠️ Issues:"
|
|
374
|
-
result[:issues].each { |issue| puts " • #{issue}" }
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
if result[:suggestions].any?
|
|
378
|
-
puts "\n 💡 Suggestions:"
|
|
379
|
-
result[:suggestions].each { |sug| puts " • #{sug}" }
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
puts "\n" + "=" * 60
|
|
384
|
-
puts "Total files needing refactoring: #{results.count}"
|
|
385
|
-
puts "=" * 60
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
# Finds Rails app root path
|
|
389
|
-
# @return [String, nil] Rails app path
|
|
390
|
-
def self.find_rails_app_path
|
|
391
|
-
path = Dir.pwd
|
|
392
|
-
10.times do
|
|
393
|
-
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
394
|
-
parent = File.dirname(path)
|
|
395
|
-
break if parent == path
|
|
396
|
-
path = parent
|
|
397
|
-
end
|
|
398
|
-
nil
|
|
399
|
-
end
|
|
400
|
-
end
|
|
401
|
-
end
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
# RefactorController module for RailsForge
|
|
2
|
-
# Automatically refactors fat controllers by extracting service objects
|
|
3
|
-
|
|
4
|
-
require 'fileutils'
|
|
5
|
-
|
|
6
|
-
module RailsForge
|
|
7
|
-
# RefactorController class extracts services from fat controller methods
|
|
8
|
-
class RefactorController
|
|
9
|
-
# Minimum lines to consider a method for extraction
|
|
10
|
-
MIN_METHOD_LINES = 15
|
|
11
|
-
|
|
12
|
-
# Initialize the refactor
|
|
13
|
-
def initialize(base_path = nil)
|
|
14
|
-
@base_path = base_path || find_rails_app_path
|
|
15
|
-
raise RefactorControllerError, "Not in a Rails application directory" unless @base_path
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Refactor a specific controller
|
|
19
|
-
# @param controller_name [String] Name of controller to refactor
|
|
20
|
-
# @return [Hash] Refactoring results
|
|
21
|
-
def refactor(controller_name)
|
|
22
|
-
controller_file = find_controller_file(controller_name)
|
|
23
|
-
|
|
24
|
-
unless controller_file
|
|
25
|
-
raise RefactorControllerError, "Controller '#{controller_name}' not found"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
puts "Analyzing controller: #{controller_name}"
|
|
29
|
-
puts ""
|
|
30
|
-
|
|
31
|
-
# Read controller content
|
|
32
|
-
content = File.read(controller_file)
|
|
33
|
-
|
|
34
|
-
# Find methods that can be extracted
|
|
35
|
-
long_methods = find_long_methods(content)
|
|
36
|
-
|
|
37
|
-
if long_methods.empty?
|
|
38
|
-
puts "No methods found that need extraction (min #{MIN_METHOD_LINES} lines)"
|
|
39
|
-
return { extracted: [], controller: controller_name }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
puts "Found #{long_methods.count} method(s) to extract:"
|
|
43
|
-
long_methods.each do |method|
|
|
44
|
-
puts " - #{method[:name]} (#{method[:lines]} lines)"
|
|
45
|
-
end
|
|
46
|
-
puts ""
|
|
47
|
-
|
|
48
|
-
# Extract each method to a service
|
|
49
|
-
extracted = []
|
|
50
|
-
long_methods.each do |method|
|
|
51
|
-
service = extract_method_to_service(controller_name, method)
|
|
52
|
-
if service
|
|
53
|
-
update_controller_method(controller_file, method, service)
|
|
54
|
-
extracted << service
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
puts "Extraction complete!"
|
|
59
|
-
puts " Created #{extracted.count} service(s)"
|
|
60
|
-
puts ""
|
|
61
|
-
|
|
62
|
-
{ extracted: extracted, controller: controller_name, methods: long_methods }
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Preview what would be extracted without making changes
|
|
66
|
-
# @param controller_name [String] Name of controller to analyze
|
|
67
|
-
# @return [Array] Methods that would be extracted
|
|
68
|
-
def preview(controller_name)
|
|
69
|
-
controller_file = find_controller_file(controller_name)
|
|
70
|
-
|
|
71
|
-
unless controller_file
|
|
72
|
-
raise RefactorControllerError, "Controller '#{controller_name}' not found"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
content = File.read(controller_file)
|
|
76
|
-
find_long_methods(content)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
private
|
|
80
|
-
|
|
81
|
-
# Find the controller file path
|
|
82
|
-
def find_rails_app_path
|
|
83
|
-
path = Dir.pwd
|
|
84
|
-
max_depth = 10
|
|
85
|
-
|
|
86
|
-
max_depth.times do
|
|
87
|
-
return path if File.exist?(File.join(path, "config", "application.rb"))
|
|
88
|
-
parent = File.dirname(path)
|
|
89
|
-
break if parent == path
|
|
90
|
-
path = parent
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
nil
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# Find controller file by name
|
|
97
|
-
def find_controller_file(controller_name)
|
|
98
|
-
controllers_dir = File.join(@base_path, "app", "controllers")
|
|
99
|
-
|
|
100
|
-
# Try different variations
|
|
101
|
-
[
|
|
102
|
-
"#{controller_name.underscore}_controller.rb",
|
|
103
|
-
"#{controller_name.underscore}_controller.rb"
|
|
104
|
-
].each do |filename|
|
|
105
|
-
path = File.join(controllers_dir, filename)
|
|
106
|
-
return path if File.exist?(path)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
# Search recursively
|
|
110
|
-
Dir.glob(File.join(controllers_dir, "**", "*_controller.rb")).each do |file|
|
|
111
|
-
return file if File.basename(file).include?(controller_name.underscore)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
nil
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Find methods that exceed the line threshold
|
|
118
|
-
def find_long_methods(content)
|
|
119
|
-
methods = []
|
|
120
|
-
lines = content.lines
|
|
121
|
-
|
|
122
|
-
# Simple regex to find method definitions
|
|
123
|
-
in_method = false
|
|
124
|
-
method_start = 0
|
|
125
|
-
method_name = nil
|
|
126
|
-
method_lines = []
|
|
127
|
-
|
|
128
|
-
lines.each_with_index do |line, index|
|
|
129
|
-
# Match def method_name or def self.method_name
|
|
130
|
-
if line =~ /\bdef\s+(self\.)?(\w+)/
|
|
131
|
-
if in_method
|
|
132
|
-
# Close previous method
|
|
133
|
-
methods << {
|
|
134
|
-
name: method_name,
|
|
135
|
-
start: method_start,
|
|
136
|
-
lines: method_lines.count,
|
|
137
|
-
content: method_lines.join
|
|
138
|
-
}
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
in_method = true
|
|
142
|
-
method_start = index
|
|
143
|
-
method_name = $2
|
|
144
|
-
method_lines = [line]
|
|
145
|
-
elsif in_method
|
|
146
|
-
if line.strip.start_with?("end") && method_lines.last =~ /\b(end|else|elsif|when)\b/
|
|
147
|
-
# This might be the end of the method
|
|
148
|
-
method_lines << line
|
|
149
|
-
|
|
150
|
-
# Check if this is the closing end
|
|
151
|
-
depth = method_lines.join.scan(/\b(begin|class|module|def|do|if|case)\b/).count
|
|
152
|
-
end_depth = method_lines.join.scan(/\bend\b/).count
|
|
153
|
-
|
|
154
|
-
if depth == end_depth && depth > 0
|
|
155
|
-
methods << {
|
|
156
|
-
name: method_name,
|
|
157
|
-
start: method_start,
|
|
158
|
-
lines: method_lines.count,
|
|
159
|
-
content: method_lines.join
|
|
160
|
-
}
|
|
161
|
-
in_method = false
|
|
162
|
-
method_lines = []
|
|
163
|
-
end
|
|
164
|
-
else
|
|
165
|
-
method_lines << line
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# Close last method if still open
|
|
171
|
-
if in_method && method_lines.any?
|
|
172
|
-
methods << {
|
|
173
|
-
name: method_name,
|
|
174
|
-
start: method_start,
|
|
175
|
-
lines: method_lines.count,
|
|
176
|
-
content: method_lines.join
|
|
177
|
-
}
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Filter to only long methods
|
|
181
|
-
methods.select { |m| m[:lines] >= MIN_METHOD_LINES }
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# Extract a method to a service object
|
|
185
|
-
def extract_method_to_service(controller_name, method_info)
|
|
186
|
-
service_dir = File.join(@base_path, "app", "services", controller_name.underscore)
|
|
187
|
-
FileUtils.mkdir_p(service_dir)
|
|
188
|
-
|
|
189
|
-
service_name = "#{method_info[:name]}_service"
|
|
190
|
-
service_class_name = "#{controller_name}#{method_info[:name].camelize}Service"
|
|
191
|
-
|
|
192
|
-
service_path = File.join(service_dir, "#{service_name}.rb")
|
|
193
|
-
|
|
194
|
-
if File.exist?(service_path)
|
|
195
|
-
puts " Skipping #{service_name} (already exists)"
|
|
196
|
-
return nil
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Generate service content
|
|
200
|
-
service_content = generate_service_content(service_class_name, method_info)
|
|
201
|
-
|
|
202
|
-
File.write(service_path, service_content)
|
|
203
|
-
puts " Created app/services/#{controller_name.underscore}/#{service_name}.rb"
|
|
204
|
-
|
|
205
|
-
{ name: service_name, class: service_class_name, path: service_path }
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Generate service object content
|
|
209
|
-
def generate_service_content(class_name, method_info)
|
|
210
|
-
<<~RUBY
|
|
211
|
-
# Service extracted from controller method: #{method_info[:name]}
|
|
212
|
-
# Original method had #{method_info[:lines]} lines
|
|
213
|
-
class #{class_name}
|
|
214
|
-
def initialize(params = {})
|
|
215
|
-
@params = params
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
def call
|
|
219
|
-
# TODO: Refactor extracted logic here
|
|
220
|
-
# Original method content:
|
|
221
|
-
# #{method_info[:content].lines.first(5).join('# ')}
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def self.call(params = {})
|
|
225
|
-
new(params).call
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
RUBY
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Update the controller method to use the service
|
|
232
|
-
def update_controller_method(controller_file, method_info, service)
|
|
233
|
-
content = File.read(controller_file)
|
|
234
|
-
|
|
235
|
-
# Replace method body with service call
|
|
236
|
-
new_method = <<~RUBY
|
|
237
|
-
def #{method_info[:name]}
|
|
238
|
-
result = #{service[:class]}.call(params)
|
|
239
|
-
end
|
|
240
|
-
RUBY
|
|
241
|
-
|
|
242
|
-
# Simple replacement - in production would use proper parsing
|
|
243
|
-
lines = content.lines
|
|
244
|
-
|
|
245
|
-
# Find and replace the method
|
|
246
|
-
modified_lines = []
|
|
247
|
-
in_target_method = false
|
|
248
|
-
method_depth = 0
|
|
249
|
-
|
|
250
|
-
lines.each_with_index do |line, index|
|
|
251
|
-
if line =~ /\bdef\s+(self\.)?#{method_info[:name]}\b/
|
|
252
|
-
in_target_method = true
|
|
253
|
-
modified_lines << new_method
|
|
254
|
-
next
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
if in_target_method
|
|
258
|
-
method_depth += 1 if line =~ /\b(def|class|module|if|case|do)\b/
|
|
259
|
-
method_depth -= 1 if line =~ /\bend\b/ && method_depth > 0
|
|
260
|
-
|
|
261
|
-
if method_depth == 0
|
|
262
|
-
in_target_method = false
|
|
263
|
-
end
|
|
264
|
-
next # Skip original method lines
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
modified_lines << line
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
File.write(controller_file, modified_lines.join)
|
|
271
|
-
puts " Updated controller method to use service"
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Error class
|
|
275
|
-
class RefactorControllerError < StandardError; end
|
|
276
|
-
end
|
|
277
|
-
end
|