rails_code_health 0.1.0 โ†’ 0.3.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.
@@ -58,18 +58,110 @@ module RailsCodeHealth
58
58
 
59
59
  file_types = @results.group_by { |r| r[:file_type] }
60
60
 
61
- file_types.each do |type, files|
61
+ # Define the order for file types
62
+ type_order = [
63
+ :controller, :model, :view, :helper, :migration,
64
+ :service, :interactor, :serializer, :form, :decorator,
65
+ :presenter, :policy, :job, :worker, :mailer, :channel,
66
+ :lib, :config, :test, :ruby
67
+ ]
68
+
69
+ # Sort file types by the defined order, with unknown types at the end
70
+ sorted_types = file_types.keys.sort_by do |type|
71
+ index = type_order.index(type)
72
+ index || type_order.length
73
+ end
74
+
75
+ sorted_types.each do |type, _|
76
+ files = file_types[type]
62
77
  next if files.empty?
63
78
 
64
79
  avg_score = (files.sum { |f| f[:health_score] || 0 } / files.count.to_f).round(1)
65
80
  healthy_count = files.count { |f| f[:health_category] == :healthy }
66
81
 
67
- breakdown << " #{type.to_s.capitalize}: #{files.count} files, avg score: #{avg_score}, #{healthy_count} healthy"
82
+ # Get file type emoji
83
+ emoji = get_file_type_emoji(type)
84
+
85
+ # Show context breakdown for new file types
86
+ context_info = ""
87
+ if [:service, :interactor, :serializer, :form, :decorator, :presenter, :policy, :job, :worker].include?(type)
88
+ context_info = generate_context_breakdown(files)
89
+ end
90
+
91
+ breakdown << " #{emoji} #{type.to_s.capitalize}: #{files.count} files, avg score: #{avg_score}, #{healthy_count} healthy"
92
+ breakdown << context_info if !context_info.empty?
68
93
  end
69
94
 
70
95
  breakdown.join("\n")
71
96
  end
72
97
 
98
+ def get_file_type_emoji(type)
99
+ emoji_map = {
100
+ controller: "๐ŸŽฎ",
101
+ model: "๐Ÿ“Š",
102
+ view: "๐Ÿ–ผ๏ธ",
103
+ helper: "๐Ÿ”ง",
104
+ migration: "๐Ÿ“ˆ",
105
+ service: "โš™๏ธ",
106
+ interactor: "๐Ÿ”„",
107
+ serializer: "๐Ÿ“ฆ",
108
+ form: "๐Ÿ“",
109
+ decorator: "๐ŸŽจ",
110
+ presenter: "๐ŸŽช",
111
+ policy: "๐Ÿ›ก๏ธ",
112
+ job: "โšก",
113
+ worker: "๐Ÿ‘ท",
114
+ mailer: "๐Ÿ“ง",
115
+ channel: "๐Ÿ“ก",
116
+ lib: "๐Ÿ“š",
117
+ config: "โš™๏ธ",
118
+ test: "๐Ÿงช",
119
+ ruby: "๐Ÿ’Ž"
120
+ }
121
+ emoji_map[type] || "๐Ÿ“„"
122
+ end
123
+
124
+ def generate_context_breakdown(files)
125
+ # Check if any files have context information
126
+ files_with_context = files.select { |f| f[:context] && !f[:context].empty? }
127
+ return "" if files_with_context.empty?
128
+
129
+ breakdown_lines = []
130
+
131
+ # Organization patterns
132
+ organizations = files_with_context.group_by { |f| f[:context][:organization] }
133
+ if organizations.keys.any? { |org| org != :traditional }
134
+ org_breakdown = organizations.map do |org, org_files|
135
+ next if org == :traditional
136
+ "#{org.to_s.gsub('_', ' ').capitalize}: #{org_files.count}"
137
+ end.compact
138
+ breakdown_lines << " ๐Ÿ“ Organization: #{org_breakdown.join(', ')}" if org_breakdown.any?
139
+ end
140
+
141
+ # Domains
142
+ domains = files_with_context.group_by { |f| f[:context][:domain] }.reject { |domain, _| domain.nil? }
143
+ if domains.any?
144
+ domain_breakdown = domains.map { |domain, domain_files| "#{domain}: #{domain_files.count}" }
145
+ breakdown_lines << " ๐Ÿข Domains: #{domain_breakdown.join(', ')}"
146
+ end
147
+
148
+ # Areas
149
+ areas = files_with_context.group_by { |f| f[:context][:area] }.reject { |area, _| area.nil? }
150
+ if areas.any?
151
+ area_breakdown = areas.map { |area, area_files| "#{area}: #{area_files.count}" }
152
+ breakdown_lines << " ๐Ÿ  Areas: #{area_breakdown.join(', ')}"
153
+ end
154
+
155
+ # API versions
156
+ api_versions = files_with_context.group_by { |f| f[:context][:api_version] }.reject { |version, _| version.nil? }
157
+ if api_versions.any?
158
+ version_breakdown = api_versions.map { |version, version_files| "#{version}: #{version_files.count}" }
159
+ breakdown_lines << " ๐Ÿ”ข API Versions: #{version_breakdown.join(', ')}"
160
+ end
161
+
162
+ breakdown_lines.join("\n")
163
+ end
164
+
73
165
  def generate_detailed_report
74
166
  detailed = []
75
167
  detailed << "๐Ÿ“‹ Detailed File Analysis"
@@ -174,7 +266,21 @@ module RailsCodeHealth
174
266
  end
175
267
 
176
268
  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])}"
269
+
270
+ # Build context string
271
+ context_parts = ["Type: #{result[:file_type]}", "Size: #{format_file_size(result[:file_size])}"]
272
+
273
+ if result[:context] && !result[:context].empty?
274
+ context_info = []
275
+ context_info << "#{result[:context][:domain]}" if result[:context][:domain]
276
+ context_info << "#{result[:context][:area]}" if result[:context][:area]
277
+ context_info << "#{result[:context][:api_version]}" if result[:context][:api_version]
278
+ context_info << "#{result[:context][:organization].to_s.gsub('_', ' ')}" if result[:context][:organization] && result[:context][:organization] != :traditional
279
+
280
+ context_parts << "Context: #{context_info.join(', ')}" if context_info.any?
281
+ end
282
+
283
+ summary << " Score: #{result[:health_score]}/10.0 | #{context_parts.join(' | ')}"
178
284
 
179
285
  # Add key metrics if available
180
286
  if result[:ruby_analysis]
@@ -200,11 +306,24 @@ module RailsCodeHealth
200
306
  case result[:rails_analysis][:rails_type]
201
307
  when :controller
202
308
  rails_info << "#{result[:rails_analysis][:action_count]} actions"
309
+ rails_info << "business logic" if result[:rails_analysis][:has_business_logic]
203
310
  when :model
204
311
  rails_info << "#{result[:rails_analysis][:association_count]} associations" if result[:rails_analysis][:association_count]
205
312
  rails_info << "#{result[:rails_analysis][:validation_count]} validations" if result[:rails_analysis][:validation_count]
206
313
  when :view
207
314
  rails_info << "#{result[:rails_analysis][:logic_lines]} logic lines" if result[:rails_analysis][:logic_lines]
315
+ when :service
316
+ rails_info << "call method" if result[:rails_analysis][:has_call_method]
317
+ rails_info << "deps: #{result[:rails_analysis][:dependencies].join(', ')}" if result[:rails_analysis][:dependencies]&.any?
318
+ rails_info << "complexity: #{result[:rails_analysis][:complexity_score]}" if result[:rails_analysis][:complexity_score]
319
+ when :interactor
320
+ rails_info << "call method" if result[:rails_analysis][:has_call_method]
321
+ rails_info << "organizer" if result[:rails_analysis][:is_organizer]
322
+ rails_info << "complexity: #{result[:rails_analysis][:complexity_score]}" if result[:rails_analysis][:complexity_score]
323
+ when :serializer
324
+ rails_info << "#{result[:rails_analysis][:attribute_count]} attributes" if result[:rails_analysis][:attribute_count]
325
+ rails_info << "#{result[:rails_analysis][:association_count]} associations" if result[:rails_analysis][:association_count]
326
+ rails_info << "#{result[:rails_analysis][:custom_method_count]} custom methods" if result[:rails_analysis][:custom_method_count]
208
327
  end
209
328
 
210
329
  summary << " Rails: #{rails_info.join(', ')}" if rails_info.any?
@@ -296,6 +415,7 @@ module RailsCodeHealth
296
415
  when :controller
297
416
  metrics[:controller_actions] = rails_metrics[:action_count]
298
417
  metrics[:uses_strong_parameters] = rails_metrics[:uses_strong_parameters]
418
+ metrics[:has_business_logic] = rails_metrics[:has_business_logic]
299
419
  when :model
300
420
  metrics[:associations] = rails_metrics[:association_count]
301
421
  metrics[:validations] = rails_metrics[:validation_count]
@@ -303,6 +423,21 @@ module RailsCodeHealth
303
423
  when :view
304
424
  metrics[:view_logic_lines] = rails_metrics[:logic_lines]
305
425
  metrics[:has_inline_styles] = rails_metrics[:has_inline_styles]
426
+ when :service
427
+ metrics[:has_call_method] = rails_metrics[:has_call_method]
428
+ metrics[:dependencies] = rails_metrics[:dependencies]
429
+ metrics[:complexity_score] = rails_metrics[:complexity_score]
430
+ metrics[:error_handling] = rails_metrics[:error_handling]
431
+ when :interactor
432
+ metrics[:has_call_method] = rails_metrics[:has_call_method]
433
+ metrics[:is_organizer] = rails_metrics[:is_organizer]
434
+ metrics[:context_usage] = rails_metrics[:context_usage]
435
+ metrics[:complexity_score] = rails_metrics[:complexity_score]
436
+ when :serializer
437
+ metrics[:attribute_count] = rails_metrics[:attribute_count]
438
+ metrics[:association_count] = rails_metrics[:association_count]
439
+ metrics[:custom_method_count] = rails_metrics[:custom_method_count]
440
+ metrics[:has_conditional_attributes] = rails_metrics[:has_conditional_attributes]
306
441
  end
307
442
  end
308
443
 
@@ -1,5 +1,6 @@
1
1
  module RailsCodeHealth
2
2
  class RubyAnalyzer
3
+ include RailsCodeHealth::ASTHelpers
3
4
  def initialize(file_path)
4
5
  @file_path = file_path
5
6
  @source = File.read(file_path)
@@ -89,17 +90,6 @@ module RailsCodeHealth
89
90
  smells
90
91
  end
91
92
 
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
93
  # Complexity calculations
104
94
  def calculate_cyclomatic_complexity(node)
105
95
  complexity = 1 # Base complexity
@@ -166,11 +156,12 @@ module RailsCodeHealth
166
156
 
167
157
  def detect_god_classes
168
158
  classes = []
159
+ t = RailsCodeHealth.configuration.thresholds['smell_thresholds']
169
160
  find_nodes(@ast, :class) do |node|
170
161
  line_count = count_lines_in_node(node)
171
162
  method_count = count_methods_in_class(node)
172
-
173
- if line_count > 400 && method_count > 20
163
+
164
+ if line_count > t['god_class_lines'] && method_count > t['god_class_methods']
174
165
  classes << {
175
166
  type: :god_class,
176
167
  class_name: extract_class_name(node),
@@ -185,9 +176,10 @@ module RailsCodeHealth
185
176
 
186
177
  def detect_high_complexity_methods
187
178
  methods = []
179
+ threshold = RailsCodeHealth.configuration.thresholds['smell_thresholds']['high_complexity_method']
188
180
  find_nodes(@ast, :def) do |node|
189
181
  complexity = calculate_cyclomatic_complexity(node)
190
- if complexity > 15
182
+ if complexity > threshold
191
183
  methods << {
192
184
  type: :high_complexity,
193
185
  method_name: node.children[0],
@@ -201,9 +193,10 @@ module RailsCodeHealth
201
193
 
202
194
  def detect_too_many_parameters
203
195
  methods = []
196
+ threshold = RailsCodeHealth.configuration.thresholds['smell_thresholds']['too_many_parameters']
204
197
  find_nodes(@ast, :def) do |node|
205
198
  param_count = count_parameters(node)
206
- if param_count > 5
199
+ if param_count > threshold
207
200
  methods << {
208
201
  type: :too_many_parameters,
209
202
  method_name: node.children[0],
@@ -217,9 +210,10 @@ module RailsCodeHealth
217
210
 
218
211
  def detect_nested_conditionals
219
212
  methods = []
213
+ threshold = RailsCodeHealth.configuration.thresholds['smell_thresholds']['nested_conditionals']
220
214
  find_nodes(@ast, :def) do |node|
221
215
  max_depth = calculate_max_nesting_depth(node)
222
- if max_depth > 4
216
+ if max_depth > threshold
223
217
  methods << {
224
218
  type: :nested_conditionals,
225
219
  method_name: node.children[0],
@@ -245,15 +239,16 @@ module RailsCodeHealth
245
239
  end
246
240
 
247
241
  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)
242
+ defs_by_visibility(class_node)[:public].size
250
243
  end
251
244
 
245
+ PARAM_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg].freeze
246
+
252
247
  def count_parameters(method_node)
253
248
  args_node = method_node.children[1]
254
- return 0 unless args_node
255
-
256
- args_node.children.count
249
+ return 0 unless args_node.is_a?(Parser::AST::Node)
250
+
251
+ args_node.children.count { |c| c.is_a?(Parser::AST::Node) && PARAM_TYPES.include?(c.type) }
257
252
  end
258
253
 
259
254
  def extract_class_name(class_node)
@@ -306,8 +301,10 @@ module RailsCodeHealth
306
301
  count
307
302
  end
308
303
 
304
+ NESTING_TYPES = %i[if case while until for].freeze
305
+
309
306
  def nesting_node?(node)
310
- [:if, :case, :while, :until, :for, :begin, :block].include?(node.type)
307
+ NESTING_TYPES.include?(node.type)
311
308
  end
312
309
 
313
310
  def has_rescue_block?(node)
@@ -1,3 +1,3 @@
1
1
  module RailsCodeHealth
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -6,6 +6,7 @@ require 'json'
6
6
  require 'pathname'
7
7
 
8
8
  require_relative 'rails_code_health/version'
9
+ require_relative 'rails_code_health/ast_helpers'
9
10
  require_relative 'rails_code_health/configuration'
10
11
  require_relative 'rails_code_health/project_detector'
11
12
  require_relative 'rails_code_health/file_analyzer'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_code_health
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - George Kosmopoulos
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-06-17 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: parser
@@ -151,6 +150,7 @@ files:
151
150
  - bin/rails-health
152
151
  - config/tresholds.json
153
152
  - lib/rails_code_health.rb
153
+ - lib/rails_code_health/ast_helpers.rb
154
154
  - lib/rails_code_health/cli.rb
155
155
  - lib/rails_code_health/configuration.rb
156
156
  - lib/rails_code_health/file_analyzer.rb
@@ -164,7 +164,6 @@ homepage: https://github.com/gkosmo/rails_code_health
164
164
  licenses:
165
165
  - MIT
166
166
  metadata: {}
167
- post_install_message:
168
167
  rdoc_options: []
169
168
  require_paths:
170
169
  - lib
@@ -179,8 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
179
178
  - !ruby/object:Gem::Version
180
179
  version: '0'
181
180
  requirements: []
182
- rubygems_version: 3.3.26
183
- signing_key:
181
+ rubygems_version: 3.6.9
184
182
  specification_version: 4
185
183
  summary: Code health analyzer for Ruby on Rails applications
186
184
  test_files: []