rails_code_health 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,335 @@
1
+ module RailsCodeHealth
2
+ class ReportGenerator
3
+ def initialize(scored_results)
4
+ @results = scored_results
5
+ end
6
+
7
+ def generate
8
+ puts generate_summary_report
9
+ puts "\n" + "="*80 + "\n"
10
+ puts generate_detailed_report
11
+ puts "\n" + "="*80 + "\n"
12
+ puts generate_recommendations_report
13
+ end
14
+
15
+ def generate_json_report
16
+ {
17
+ summary: generate_summary_data,
18
+ files: @results.map { |result| format_file_result(result) },
19
+ recommendations: collect_all_recommendations,
20
+ generated_at: Time.now.iso8601
21
+ }.to_json
22
+ end
23
+
24
+ private
25
+
26
+ def generate_summary_report
27
+ summary = []
28
+ summary << "Rails Code Health Report"
29
+ summary << "=" * 50
30
+ summary << ""
31
+
32
+ total_files = @results.count
33
+ healthy_files = @results.count { |r| r[:health_category] == :healthy }
34
+ warning_files = @results.count { |r| r[:health_category] == :warning }
35
+ alert_files = @results.count { |r| r[:health_category] == :alert }
36
+ critical_files = @results.count { |r| r[:health_category] == :critical }
37
+
38
+ summary << "📊 Overall Health Summary:"
39
+ summary << " Total files analyzed: #{total_files}"
40
+ summary << " 🟢 Healthy files (8.0-10.0): #{healthy_files} (#{percentage(healthy_files, total_files)}%)"
41
+ summary << " 🟡 Warning files (4.0-7.9): #{warning_files} (#{percentage(warning_files, total_files)}%)"
42
+ summary << " 🔴 Alert files (1.0-3.9): #{alert_files} (#{percentage(alert_files, total_files)}%)"
43
+ summary << " ⚫ Critical files (<1.0): #{critical_files} (#{percentage(critical_files, total_files)}%)" if critical_files > 0
44
+ summary << ""
45
+
46
+ average_score = (@results.sum { |r| r[:health_score] || 0 } / total_files.to_f).round(1)
47
+ summary << "📈 Average Health Score: #{average_score}/10.0"
48
+
49
+ summary << ""
50
+ summary << generate_file_type_breakdown
51
+
52
+ summary.join("\n")
53
+ end
54
+
55
+ def generate_file_type_breakdown
56
+ breakdown = []
57
+ breakdown << "📂 Breakdown by File Type:"
58
+
59
+ file_types = @results.group_by { |r| r[:file_type] }
60
+
61
+ file_types.each do |type, files|
62
+ next if files.empty?
63
+
64
+ avg_score = (files.sum { |f| f[:health_score] || 0 } / files.count.to_f).round(1)
65
+ healthy_count = files.count { |f| f[:health_category] == :healthy }
66
+
67
+ breakdown << " #{type.to_s.capitalize}: #{files.count} files, avg score: #{avg_score}, #{healthy_count} healthy"
68
+ end
69
+
70
+ breakdown.join("\n")
71
+ end
72
+
73
+ def generate_detailed_report
74
+ detailed = []
75
+ detailed << "📋 Detailed File Analysis"
76
+ detailed << "=" * 50
77
+ detailed << ""
78
+
79
+ # Sort by health score (worst first)
80
+ sorted_results = @results.sort_by { |r| r[:health_score] || 0 }
81
+
82
+ # Show worst 10 files
83
+ worst_files = sorted_results.first(10)
84
+
85
+ detailed << "🚨 Files Needing Most Attention (Bottom 10):"
86
+ detailed << ""
87
+
88
+ worst_files.each_with_index do |result, index|
89
+ detailed << format_file_summary(result, index + 1)
90
+ detailed << ""
91
+ end
92
+
93
+ # Show best files if we have healthy ones
94
+ healthy_files = @results.select { |r| r[:health_category] == :healthy }
95
+ if healthy_files.any?
96
+ detailed << "✅ Top Performing Files:"
97
+ detailed << ""
98
+
99
+ best_files = healthy_files.sort_by { |r| -(r[:health_score] || 0) }.first(5)
100
+ best_files.each_with_index do |result, index|
101
+ detailed << format_file_summary(result, index + 1, prefix: "👍")
102
+ detailed << ""
103
+ end
104
+ end
105
+
106
+ detailed.join("\n")
107
+ end
108
+
109
+ def generate_recommendations_report
110
+ recommendations = []
111
+ recommendations << "💡 Key Recommendations"
112
+ recommendations << "=" * 50
113
+ recommendations << ""
114
+
115
+ # Collect and categorize all recommendations
116
+ all_recommendations = collect_all_recommendations
117
+
118
+ if all_recommendations.empty?
119
+ recommendations << "🎉 Great job! No major issues found."
120
+ return recommendations.join("\n")
121
+ end
122
+
123
+ # Group recommendations by frequency
124
+ recommendation_counts = Hash.new(0)
125
+ all_recommendations.each { |rec| recommendation_counts[rec] += 1 }
126
+
127
+ # Sort by frequency (most common first)
128
+ sorted_recommendations = recommendation_counts.sort_by { |_, count| -count }
129
+
130
+ recommendations << "📈 Most Common Issues (by frequency):"
131
+ recommendations << ""
132
+
133
+ sorted_recommendations.first(10).each_with_index do |(rec, count), index|
134
+ recommendations << "#{index + 1}. #{rec} (#{count} occurrence#{'s' if count > 1})"
135
+ end
136
+
137
+ recommendations << ""
138
+ recommendations << "🎯 Priority Actions:"
139
+ recommendations << ""
140
+
141
+ # Find files with lowest scores and their recommendations
142
+ critical_files = @results.select { |r| r[:health_score] && r[:health_score] < 4.0 }
143
+ if critical_files.any?
144
+ recommendations << "1. 🚨 Address critical files immediately:"
145
+ critical_files.first(3).each do |file|
146
+ recommendations << " - #{file[:relative_path]} (score: #{file[:health_score]})"
147
+ if file[:recommendations] && file[:recommendations].any?
148
+ file[:recommendations].first(2).each do |rec|
149
+ recommendations << " • #{rec}"
150
+ end
151
+ end
152
+ end
153
+ recommendations << ""
154
+ end
155
+
156
+ recommendations << "2. 🔧 Focus on these improvement areas:"
157
+ recommendations << " - Reduce method and class lengths"
158
+ recommendations << " - Lower cyclomatic complexity"
159
+ recommendations << " - Follow Rails conventions"
160
+ recommendations << " - Extract business logic from controllers and views"
161
+
162
+ recommendations.join("\n")
163
+ end
164
+
165
+ def format_file_summary(result, rank, prefix: "🔍")
166
+ summary = []
167
+
168
+ health_emoji = case result[:health_category]
169
+ when :healthy then "🟢"
170
+ when :warning then "🟡"
171
+ when :alert then "🔴"
172
+ when :critical then "⚫"
173
+ else "❓"
174
+ end
175
+
176
+ summary << "#{rank}. #{prefix} #{health_emoji} #{result[:relative_path]}"
177
+ summary << " Score: #{result[:health_score]}/10.0 | Type: #{result[:file_type]} | Size: #{format_file_size(result[:file_size])}"
178
+
179
+ # Add key metrics if available
180
+ if result[:ruby_analysis]
181
+ metrics = []
182
+ if result[:ruby_analysis][:method_metrics]
183
+ method_count = result[:ruby_analysis][:method_metrics].count
184
+ avg_complexity = result[:ruby_analysis][:method_metrics].map { |m| m[:cyclomatic_complexity] }.sum.to_f / method_count
185
+ metrics << "#{method_count} methods"
186
+ metrics << "avg complexity: #{avg_complexity.round(1)}"
187
+ end
188
+
189
+ if result[:ruby_analysis][:class_metrics]
190
+ class_count = result[:ruby_analysis][:class_metrics].count
191
+ metrics << "#{class_count} class#{'es' if class_count != 1}"
192
+ end
193
+
194
+ summary << " Metrics: #{metrics.join(', ')}" if metrics.any?
195
+ end
196
+
197
+ # Add Rails-specific info
198
+ if result[:rails_analysis]
199
+ rails_info = []
200
+ case result[:rails_analysis][:rails_type]
201
+ when :controller
202
+ rails_info << "#{result[:rails_analysis][:action_count]} actions"
203
+ when :model
204
+ rails_info << "#{result[:rails_analysis][:association_count]} associations" if result[:rails_analysis][:association_count]
205
+ rails_info << "#{result[:rails_analysis][:validation_count]} validations" if result[:rails_analysis][:validation_count]
206
+ when :view
207
+ rails_info << "#{result[:rails_analysis][:logic_lines]} logic lines" if result[:rails_analysis][:logic_lines]
208
+ end
209
+
210
+ summary << " Rails: #{rails_info.join(', ')}" if rails_info.any?
211
+ end
212
+
213
+ # Add top recommendations
214
+ if result[:recommendations] && result[:recommendations].any?
215
+ summary << " Top issues:"
216
+ result[:recommendations].first(2).each do |rec|
217
+ summary << " • #{rec}"
218
+ end
219
+ end
220
+
221
+ summary.join("\n")
222
+ end
223
+
224
+ def collect_all_recommendations
225
+ @results.flat_map { |r| r[:recommendations] || [] }.uniq
226
+ end
227
+
228
+ def generate_summary_data
229
+ total_files = @results.count
230
+ {
231
+ total_files: total_files,
232
+ healthy_files: @results.count { |r| r[:health_category] == :healthy },
233
+ warning_files: @results.count { |r| r[:health_category] == :warning },
234
+ alert_files: @results.count { |r| r[:health_category] == :alert },
235
+ critical_files: @results.count { |r| r[:health_category] == :critical },
236
+ average_score: (@results.sum { |r| r[:health_score] || 0 } / total_files.to_f).round(1),
237
+ file_types: generate_file_type_summary
238
+ }
239
+ end
240
+
241
+ def generate_file_type_summary
242
+ file_types = @results.group_by { |r| r[:file_type] }
243
+
244
+ file_types.transform_values do |files|
245
+ {
246
+ count: files.count,
247
+ average_score: (files.sum { |f| f[:health_score] || 0 } / files.count.to_f).round(1),
248
+ healthy_count: files.count { |f| f[:health_category] == :healthy }
249
+ }
250
+ end
251
+ end
252
+
253
+ def format_file_result(result)
254
+ {
255
+ file_path: result[:relative_path],
256
+ file_type: result[:file_type],
257
+ health_score: result[:health_score],
258
+ health_category: result[:health_category],
259
+ file_size: result[:file_size],
260
+ last_modified: result[:last_modified],
261
+ recommendations: result[:recommendations] || [],
262
+ metrics: extract_key_metrics(result)
263
+ }
264
+ end
265
+
266
+ def extract_key_metrics(result)
267
+ metrics = {}
268
+
269
+ if result[:ruby_analysis]
270
+ ruby_metrics = result[:ruby_analysis]
271
+
272
+ if ruby_metrics[:file_metrics]
273
+ metrics[:lines_of_code] = ruby_metrics[:file_metrics][:code_lines]
274
+ metrics[:total_lines] = ruby_metrics[:file_metrics][:total_lines]
275
+ end
276
+
277
+ if ruby_metrics[:method_metrics]
278
+ methods = ruby_metrics[:method_metrics]
279
+ metrics[:method_count] = methods.count
280
+ metrics[:average_method_length] = methods.map { |m| m[:line_count] }.sum.to_f / methods.count if methods.any?
281
+ metrics[:average_complexity] = methods.map { |m| m[:cyclomatic_complexity] }.sum.to_f / methods.count if methods.any?
282
+ metrics[:max_complexity] = methods.map { |m| m[:cyclomatic_complexity] }.max
283
+ end
284
+
285
+ if ruby_metrics[:class_metrics]
286
+ classes = ruby_metrics[:class_metrics]
287
+ metrics[:class_count] = classes.count
288
+ metrics[:average_class_length] = classes.map { |c| c[:line_count] }.sum.to_f / classes.count if classes.any?
289
+ end
290
+ end
291
+
292
+ if result[:rails_analysis]
293
+ rails_metrics = result[:rails_analysis]
294
+
295
+ case rails_metrics[:rails_type]
296
+ when :controller
297
+ metrics[:controller_actions] = rails_metrics[:action_count]
298
+ metrics[:uses_strong_parameters] = rails_metrics[:uses_strong_parameters]
299
+ when :model
300
+ metrics[:associations] = rails_metrics[:association_count]
301
+ metrics[:validations] = rails_metrics[:validation_count]
302
+ metrics[:callbacks] = rails_metrics[:callback_count]
303
+ when :view
304
+ metrics[:view_logic_lines] = rails_metrics[:logic_lines]
305
+ metrics[:has_inline_styles] = rails_metrics[:has_inline_styles]
306
+ end
307
+ end
308
+
309
+ # Round float values
310
+ metrics.transform_values do |value|
311
+ value.is_a?(Float) ? value.round(2) : value
312
+ end
313
+ end
314
+
315
+ def percentage(part, total)
316
+ return 0 if total.zero?
317
+ ((part.to_f / total) * 100).round(1)
318
+ end
319
+
320
+ def format_file_size(size_in_bytes)
321
+ return "0 B" if size_in_bytes.nil? || size_in_bytes.zero?
322
+
323
+ units = %w[B KB MB GB]
324
+ size = size_in_bytes.to_f
325
+ unit_index = 0
326
+
327
+ while size >= 1024 && unit_index < units.length - 1
328
+ size /= 1024
329
+ unit_index += 1
330
+ end
331
+
332
+ "#{size.round(1)} #{units[unit_index]}"
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,319 @@
1
+ module RailsCodeHealth
2
+ class RubyAnalyzer
3
+ def initialize(file_path)
4
+ @file_path = file_path
5
+ @source = File.read(file_path)
6
+ @ast = Parser::CurrentRuby.parse(@source)
7
+ rescue Parser::SyntaxError => e
8
+ @parse_error = e
9
+ @ast = nil
10
+ end
11
+
12
+ def analyze
13
+ return { parse_error: @parse_error.message } if @parse_error
14
+
15
+ {
16
+ file_metrics: analyze_file,
17
+ class_metrics: analyze_classes,
18
+ method_metrics: analyze_methods,
19
+ complexity_metrics: analyze_complexity,
20
+ code_smells: detect_code_smells
21
+ }
22
+ end
23
+
24
+ private
25
+
26
+ def analyze_file
27
+ lines = @source.lines
28
+ {
29
+ total_lines: lines.count,
30
+ code_lines: lines.reject { |line| line.strip.empty? || line.strip.start_with?('#') }.count,
31
+ comment_lines: lines.count { |line| line.strip.start_with?('#') },
32
+ blank_lines: lines.count { |line| line.strip.empty? }
33
+ }
34
+ end
35
+
36
+ def analyze_classes
37
+ return [] unless @ast
38
+
39
+ classes = []
40
+ find_nodes(@ast, :class) do |node|
41
+ classes << {
42
+ name: extract_class_name(node),
43
+ line_count: count_lines_in_node(node),
44
+ method_count: count_methods_in_class(node),
45
+ public_method_count: count_public_methods_in_class(node),
46
+ inheritance: extract_inheritance(node),
47
+ complexity_score: calculate_class_complexity(node)
48
+ }
49
+ end
50
+ classes
51
+ end
52
+
53
+ def analyze_methods
54
+ return [] unless @ast
55
+
56
+ methods = []
57
+ find_nodes(@ast, :def) do |node|
58
+ methods << {
59
+ name: node.children[0],
60
+ line_count: count_lines_in_node(node),
61
+ parameter_count: count_parameters(node),
62
+ cyclomatic_complexity: calculate_cyclomatic_complexity(node),
63
+ nesting_depth: calculate_max_nesting_depth(node),
64
+ abc_score: calculate_abc_score(node),
65
+ has_rescue: has_rescue_block?(node)
66
+ }
67
+ end
68
+ methods
69
+ end
70
+
71
+ def analyze_complexity
72
+ return {} unless @ast
73
+
74
+ {
75
+ overall_complexity: calculate_overall_complexity,
76
+ max_nesting_depth: find_max_nesting_depth(@ast),
77
+ conditional_complexity: count_conditionals(@ast),
78
+ loop_complexity: count_loops(@ast)
79
+ }
80
+ end
81
+
82
+ def detect_code_smells
83
+ smells = []
84
+ smells.concat(detect_long_methods)
85
+ smells.concat(detect_god_classes)
86
+ smells.concat(detect_high_complexity_methods)
87
+ smells.concat(detect_too_many_parameters)
88
+ smells.concat(detect_nested_conditionals)
89
+ smells
90
+ end
91
+
92
+ # AST traversal helper
93
+ def find_nodes(node, type, &block)
94
+ return unless node.is_a?(Parser::AST::Node)
95
+
96
+ yield(node) if node.type == type
97
+
98
+ node.children.each do |child|
99
+ find_nodes(child, type, &block)
100
+ end
101
+ end
102
+
103
+ # Complexity calculations
104
+ def calculate_cyclomatic_complexity(node)
105
+ complexity = 1 # Base complexity
106
+
107
+ find_nodes(node, :if) { complexity += 1 }
108
+ find_nodes(node, :case) { complexity += 1 }
109
+ find_nodes(node, :while) { complexity += 1 }
110
+ find_nodes(node, :until) { complexity += 1 }
111
+ find_nodes(node, :for) { complexity += 1 }
112
+ find_nodes(node, :rescue) { complexity += 1 }
113
+ find_nodes(node, :when) { complexity += 1 }
114
+
115
+ complexity
116
+ end
117
+
118
+ def calculate_max_nesting_depth(node, depth = 0)
119
+ return depth unless node.is_a?(Parser::AST::Node)
120
+
121
+ max_depth = depth
122
+
123
+ if nesting_node?(node)
124
+ depth += 1
125
+ max_depth = depth
126
+ end
127
+
128
+ node.children.each do |child|
129
+ child_depth = calculate_max_nesting_depth(child, depth)
130
+ max_depth = [max_depth, child_depth].max
131
+ end
132
+
133
+ max_depth
134
+ end
135
+
136
+ def calculate_abc_score(node)
137
+ assignments = 0
138
+ branches = 0
139
+ conditions = 0
140
+
141
+ find_nodes(node, :lvasgn) { assignments += 1 }
142
+ find_nodes(node, :ivasgn) { assignments += 1 }
143
+ find_nodes(node, :send) { branches += 1 }
144
+ find_nodes(node, :if) { conditions += 1 }
145
+ find_nodes(node, :case) { conditions += 1 }
146
+
147
+ Math.sqrt(assignments**2 + branches**2 + conditions**2).round(2)
148
+ end
149
+
150
+ # Code smell detection
151
+ def detect_long_methods
152
+ methods = []
153
+ find_nodes(@ast, :def) do |node|
154
+ line_count = count_lines_in_node(node)
155
+ if line_count > RailsCodeHealth.configuration.thresholds['ruby_thresholds']['method_length']['red']
156
+ methods << {
157
+ type: :long_method,
158
+ method_name: node.children[0],
159
+ line_count: line_count,
160
+ severity: :high
161
+ }
162
+ end
163
+ end
164
+ methods
165
+ end
166
+
167
+ def detect_god_classes
168
+ classes = []
169
+ find_nodes(@ast, :class) do |node|
170
+ line_count = count_lines_in_node(node)
171
+ method_count = count_methods_in_class(node)
172
+
173
+ if line_count > 400 && method_count > 20
174
+ classes << {
175
+ type: :god_class,
176
+ class_name: extract_class_name(node),
177
+ line_count: line_count,
178
+ method_count: method_count,
179
+ severity: :high
180
+ }
181
+ end
182
+ end
183
+ classes
184
+ end
185
+
186
+ def detect_high_complexity_methods
187
+ methods = []
188
+ find_nodes(@ast, :def) do |node|
189
+ complexity = calculate_cyclomatic_complexity(node)
190
+ if complexity > 15
191
+ methods << {
192
+ type: :high_complexity,
193
+ method_name: node.children[0],
194
+ complexity: complexity,
195
+ severity: :high
196
+ }
197
+ end
198
+ end
199
+ methods
200
+ end
201
+
202
+ def detect_too_many_parameters
203
+ methods = []
204
+ find_nodes(@ast, :def) do |node|
205
+ param_count = count_parameters(node)
206
+ if param_count > 5
207
+ methods << {
208
+ type: :too_many_parameters,
209
+ method_name: node.children[0],
210
+ parameter_count: param_count,
211
+ severity: :medium
212
+ }
213
+ end
214
+ end
215
+ methods
216
+ end
217
+
218
+ def detect_nested_conditionals
219
+ methods = []
220
+ find_nodes(@ast, :def) do |node|
221
+ max_depth = calculate_max_nesting_depth(node)
222
+ if max_depth > 4
223
+ methods << {
224
+ type: :nested_conditionals,
225
+ method_name: node.children[0],
226
+ nesting_depth: max_depth,
227
+ severity: :medium
228
+ }
229
+ end
230
+ end
231
+ methods
232
+ end
233
+
234
+ # Helper methods
235
+ def count_lines_in_node(node)
236
+ return 0 unless node.respond_to?(:loc) && node.loc.respond_to?(:last_line)
237
+
238
+ node.loc.last_line - node.loc.first_line + 1
239
+ end
240
+
241
+ def count_methods_in_class(class_node)
242
+ method_count = 0
243
+ find_nodes(class_node, :def) { method_count += 1 }
244
+ method_count
245
+ end
246
+
247
+ def count_public_methods_in_class(class_node)
248
+ # This is a simplified version - in reality, you'd need to track visibility modifiers
249
+ count_methods_in_class(class_node)
250
+ end
251
+
252
+ def count_parameters(method_node)
253
+ args_node = method_node.children[1]
254
+ return 0 unless args_node
255
+
256
+ args_node.children.count
257
+ end
258
+
259
+ def extract_class_name(class_node)
260
+ class_node.children[0]&.children&.last || 'Unknown'
261
+ end
262
+
263
+ def extract_inheritance(class_node)
264
+ superclass_node = class_node.children[1]
265
+ return nil unless superclass_node
266
+
267
+ if superclass_node.type == :const
268
+ superclass_node.children.last
269
+ else
270
+ 'Unknown'
271
+ end
272
+ end
273
+
274
+ def calculate_class_complexity(class_node)
275
+ total_complexity = 0
276
+ find_nodes(class_node, :def) do |method_node|
277
+ total_complexity += calculate_cyclomatic_complexity(method_node)
278
+ end
279
+ total_complexity
280
+ end
281
+
282
+ def calculate_overall_complexity
283
+ total = 0
284
+ find_nodes(@ast, :def) do |node|
285
+ total += calculate_cyclomatic_complexity(node)
286
+ end
287
+ total
288
+ end
289
+
290
+ def find_max_nesting_depth(node)
291
+ calculate_max_nesting_depth(node)
292
+ end
293
+
294
+ def count_conditionals(node)
295
+ count = 0
296
+ find_nodes(node, :if) { count += 1 }
297
+ find_nodes(node, :case) { count += 1 }
298
+ count
299
+ end
300
+
301
+ def count_loops(node)
302
+ count = 0
303
+ find_nodes(node, :while) { count += 1 }
304
+ find_nodes(node, :until) { count += 1 }
305
+ find_nodes(node, :for) { count += 1 }
306
+ count
307
+ end
308
+
309
+ def nesting_node?(node)
310
+ [:if, :case, :while, :until, :for, :begin, :block].include?(node.type)
311
+ end
312
+
313
+ def has_rescue_block?(node)
314
+ has_rescue = false
315
+ find_nodes(node, :rescue) { has_rescue = true }
316
+ has_rescue
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,3 @@
1
+ module RailsCodeHealth
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,46 @@
1
+ require 'parser/current'
2
+ require 'ast'
3
+ require 'rubocop/ast'
4
+ require 'active_support'
5
+ require 'json'
6
+ require 'pathname'
7
+
8
+ require_relative 'rails_code_health/version'
9
+ require_relative 'rails_code_health/configuration'
10
+ require_relative 'rails_code_health/project_detector'
11
+ require_relative 'rails_code_health/file_analyzer'
12
+ require_relative 'rails_code_health/ruby_analyzer'
13
+ require_relative 'rails_code_health/rails_analyzer'
14
+ require_relative 'rails_code_health/health_calculator'
15
+ require_relative 'rails_code_health/report_generator'
16
+ require_relative 'rails_code_health/cli'
17
+
18
+ module RailsCodeHealth
19
+ class Error < StandardError; end
20
+
21
+ class << self
22
+ def analyze(path = '.')
23
+ project_path = Pathname.new(path).expand_path
24
+
25
+ unless ProjectDetector.rails_project?(project_path)
26
+ raise Error, "Not a Rails project directory: #{project_path}"
27
+ end
28
+
29
+ analyzer = FileAnalyzer.new(project_path)
30
+ results = analyzer.analyze_all
31
+
32
+ health_calculator = HealthCalculator.new
33
+ scored_results = health_calculator.calculate_scores(results)
34
+
35
+ ReportGenerator.new(scored_results).generate
36
+ end
37
+
38
+ def configuration
39
+ @configuration ||= Configuration.new
40
+ end
41
+
42
+ def configure
43
+ yield(configuration) if block_given?
44
+ end
45
+ end
46
+ end