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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +51 -2
- data/README.md +6 -1
- data/lib/rails_code_health/ast_helpers.rb +126 -0
- data/lib/rails_code_health/configuration.rb +21 -1
- data/lib/rails_code_health/file_analyzer.rb +7 -2
- data/lib/rails_code_health/health_calculator.rb +129 -0
- data/lib/rails_code_health/project_detector.rb +76 -1
- data/lib/rails_code_health/rails_analyzer.rb +456 -60
- data/lib/rails_code_health/report_generator.rb +138 -3
- data/lib/rails_code_health/ruby_analyzer.rb +19 -22
- data/lib/rails_code_health/version.rb +1 -1
- data/lib/rails_code_health.rb +1 -0
- metadata +4 -6
|
@@ -58,18 +58,110 @@ module RailsCodeHealth
|
|
|
58
58
|
|
|
59
59
|
file_types = @results.group_by { |r| r[:file_type] }
|
|
60
60
|
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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 >
|
|
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 >
|
|
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 >
|
|
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
|
-
|
|
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
|
-
|
|
307
|
+
NESTING_TYPES.include?(node.type)
|
|
311
308
|
end
|
|
312
309
|
|
|
313
310
|
def has_rescue_block?(node)
|
data/lib/rails_code_health.rb
CHANGED
|
@@ -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.
|
|
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:
|
|
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.
|
|
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: []
|