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,391 @@
1
+ module RailsCodeHealth
2
+ class RailsAnalyzer
3
+ def initialize(file_path, file_type)
4
+ @file_path = file_path
5
+ @file_type = file_type
6
+ @source = File.read(file_path)
7
+ @ast = Parser::CurrentRuby.parse(@source) if file_path.extname == '.rb'
8
+ rescue Parser::SyntaxError
9
+ @ast = nil
10
+ end
11
+
12
+ def analyze
13
+ case @file_type
14
+ when :controller
15
+ analyze_controller
16
+ when :model
17
+ analyze_model
18
+ when :view
19
+ analyze_view
20
+ when :helper
21
+ analyze_helper
22
+ when :migration
23
+ analyze_migration
24
+ else
25
+ {}
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def analyze_controller
32
+ return {} unless @ast
33
+
34
+ {
35
+ rails_type: :controller,
36
+ action_count: count_controller_actions,
37
+ has_before_actions: has_before_actions?,
38
+ uses_strong_parameters: uses_strong_parameters?,
39
+ has_direct_model_access: has_direct_model_access?,
40
+ response_formats: detect_response_formats,
41
+ rails_smells: detect_controller_smells
42
+ }
43
+ end
44
+
45
+ def analyze_model
46
+ return {} unless @ast
47
+
48
+ {
49
+ rails_type: :model,
50
+ association_count: count_associations,
51
+ validation_count: count_validations,
52
+ callback_count: count_callbacks,
53
+ scope_count: count_scopes,
54
+ has_fat_model_smell: has_fat_model_smell?,
55
+ rails_smells: detect_model_smells
56
+ }
57
+ end
58
+
59
+ def analyze_view
60
+ lines = @source.lines
61
+ logic_lines = count_view_logic_lines(lines)
62
+
63
+ {
64
+ rails_type: :view,
65
+ total_lines: lines.count,
66
+ logic_lines: logic_lines,
67
+ has_inline_styles: has_inline_styles?,
68
+ has_inline_javascript: has_inline_javascript?,
69
+ rails_smells: detect_view_smells(lines, logic_lines)
70
+ }
71
+ end
72
+
73
+ def analyze_helper
74
+ return {} unless @ast
75
+
76
+ {
77
+ rails_type: :helper,
78
+ method_count: count_helper_methods,
79
+ rails_smells: detect_helper_smells
80
+ }
81
+ end
82
+
83
+ def analyze_migration
84
+ return {} unless @ast
85
+
86
+ {
87
+ rails_type: :migration,
88
+ has_data_changes: has_data_changes?,
89
+ has_index_changes: has_index_changes?,
90
+ complexity_score: calculate_migration_complexity,
91
+ rails_smells: detect_migration_smells
92
+ }
93
+ end
94
+
95
+ # Controller analysis methods
96
+ def count_controller_actions
97
+ return 0 unless @ast
98
+
99
+ action_count = 0
100
+ find_nodes(@ast, :def) do |node|
101
+ method_name = node.children[0].to_s
102
+ # Skip private methods and Rails internal methods
103
+ unless method_name.start_with?('_') || private_controller_method?(method_name)
104
+ action_count += 1
105
+ end
106
+ end
107
+ action_count
108
+ end
109
+
110
+ def has_before_actions?
111
+ @source.include?('before_action') || @source.include?('before_filter')
112
+ end
113
+
114
+ def uses_strong_parameters?
115
+ @source.include?('params.require') || @source.include?('params.permit')
116
+ end
117
+
118
+ def has_direct_model_access?
119
+ # Look for direct ActiveRecord calls in controller actions
120
+ model_patterns = [
121
+ /\w+\.find\(/,
122
+ /\w+\.where\(/,
123
+ /\w+\.create\(/,
124
+ /\w+\.update\(/,
125
+ /\w+\.all/
126
+ ]
127
+
128
+ model_patterns.any? { |pattern| @source.match?(pattern) }
129
+ end
130
+
131
+ def detect_response_formats
132
+ formats = []
133
+ formats << :html if @source.include?('format.html')
134
+ formats << :json if @source.include?('format.json')
135
+ formats << :xml if @source.include?('format.xml')
136
+ formats << :js if @source.include?('format.js')
137
+ formats
138
+ end
139
+
140
+ def detect_controller_smells
141
+ smells = []
142
+
143
+ action_count = count_controller_actions
144
+ if action_count > 10
145
+ smells << {
146
+ type: :too_many_actions,
147
+ count: action_count,
148
+ severity: :high
149
+ }
150
+ end
151
+
152
+ unless uses_strong_parameters?
153
+ smells << {
154
+ type: :missing_strong_parameters,
155
+ severity: :medium
156
+ }
157
+ end
158
+
159
+ if has_direct_model_access?
160
+ smells << {
161
+ type: :direct_model_access,
162
+ severity: :medium
163
+ }
164
+ end
165
+
166
+ smells
167
+ end
168
+
169
+ # Model analysis methods
170
+ def count_associations
171
+ associations = 0
172
+ association_methods = %w[belongs_to has_one has_many has_and_belongs_to_many]
173
+
174
+ association_methods.each do |method|
175
+ associations += @source.scan(/#{method}\s+:/).count
176
+ end
177
+
178
+ associations
179
+ end
180
+
181
+ def count_validations
182
+ validations = 0
183
+ validation_methods = %w[validates validates_presence_of validates_uniqueness_of validates_format_of]
184
+
185
+ validation_methods.each do |method|
186
+ validations += @source.scan(/#{method}\s+/).count
187
+ end
188
+
189
+ validations
190
+ end
191
+
192
+ def count_callbacks
193
+ callbacks = 0
194
+ callback_methods = %w[before_save after_save before_create after_create before_update after_update before_destroy after_destroy]
195
+
196
+ callback_methods.each do |method|
197
+ callbacks += @source.scan(/#{method}\s+/).count
198
+ end
199
+
200
+ callbacks
201
+ end
202
+
203
+ def count_scopes
204
+ @source.scan(/scope\s+:/).count
205
+ end
206
+
207
+ def has_fat_model_smell?
208
+ return false unless @ast
209
+
210
+ line_count = @source.lines.count
211
+ method_count = 0
212
+ find_nodes(@ast, :def) { method_count += 1 }
213
+
214
+ line_count > 200 && method_count > 15
215
+ end
216
+
217
+ def detect_model_smells
218
+ smells = []
219
+
220
+ if has_fat_model_smell?
221
+ smells << {
222
+ type: :fat_model,
223
+ severity: :high
224
+ }
225
+ end
226
+
227
+ callback_count = count_callbacks
228
+ if callback_count > 5
229
+ smells << {
230
+ type: :callback_hell,
231
+ count: callback_count,
232
+ severity: :medium
233
+ }
234
+ end
235
+
236
+ validation_count = count_validations
237
+ if validation_count == 0
238
+ smells << {
239
+ type: :missing_validations,
240
+ severity: :low
241
+ }
242
+ end
243
+
244
+ smells
245
+ end
246
+
247
+ # View analysis methods
248
+ def count_view_logic_lines(lines)
249
+ logic_count = 0
250
+
251
+ lines.each do |line|
252
+ # Count Ruby code blocks in ERB
253
+ logic_count += 1 if line.match?(/<%((?!%>).)*%>/) || line.match?(/<%((?!%>).)*if|unless|case|for|while/)
254
+ end
255
+
256
+ logic_count
257
+ end
258
+
259
+ def has_inline_styles?
260
+ @source.include?('style=') || @source.include?('<style>')
261
+ end
262
+
263
+ def has_inline_javascript?
264
+ @source.include?('<script>') || @source.include?('onclick=') || @source.include?('onload=')
265
+ end
266
+
267
+ def detect_view_smells(lines, logic_lines)
268
+ smells = []
269
+
270
+ if lines.count > 50
271
+ smells << {
272
+ type: :long_view,
273
+ line_count: lines.count,
274
+ severity: :medium
275
+ }
276
+ end
277
+
278
+ if logic_lines > 10
279
+ smells << {
280
+ type: :logic_in_view,
281
+ logic_lines: logic_lines,
282
+ severity: :high
283
+ }
284
+ end
285
+
286
+ if has_inline_styles?
287
+ smells << {
288
+ type: :inline_styles,
289
+ severity: :low
290
+ }
291
+ end
292
+
293
+ if has_inline_javascript?
294
+ smells << {
295
+ type: :inline_javascript,
296
+ severity: :medium
297
+ }
298
+ end
299
+
300
+ smells
301
+ end
302
+
303
+ # Helper analysis methods
304
+ def count_helper_methods
305
+ return 0 unless @ast
306
+
307
+ method_count = 0
308
+ find_nodes(@ast, :def) { method_count += 1 }
309
+ method_count
310
+ end
311
+
312
+ def detect_helper_smells
313
+ smells = []
314
+
315
+ method_count = count_helper_methods
316
+ if method_count > 15
317
+ smells << {
318
+ type: :fat_helper,
319
+ method_count: method_count,
320
+ severity: :medium
321
+ }
322
+ end
323
+
324
+ smells
325
+ end
326
+
327
+ # Migration analysis methods
328
+ def has_data_changes?
329
+ data_methods = %w[execute update_all delete_all]
330
+ data_methods.any? { |method| @source.include?(method) }
331
+ end
332
+
333
+ def has_index_changes?
334
+ @source.include?('add_index') || @source.include?('remove_index')
335
+ end
336
+
337
+ def calculate_migration_complexity
338
+ complexity = 0
339
+
340
+ # Count different types of operations
341
+ complexity += @source.scan(/create_table/).count * 2
342
+ complexity += @source.scan(/drop_table/).count * 2
343
+ complexity += @source.scan(/add_column/).count * 1
344
+ complexity += @source.scan(/remove_column/).count * 1
345
+ complexity += @source.scan(/change_column/).count * 2
346
+ complexity += @source.scan(/add_index/).count * 1
347
+ complexity += @source.scan(/remove_index/).count * 1
348
+ complexity += @source.scan(/execute/).count * 3
349
+
350
+ complexity
351
+ end
352
+
353
+ def detect_migration_smells
354
+ smells = []
355
+
356
+ if has_data_changes?
357
+ smells << {
358
+ type: :data_changes_in_migration,
359
+ severity: :high
360
+ }
361
+ end
362
+
363
+ complexity = calculate_migration_complexity
364
+ if complexity > 20
365
+ smells << {
366
+ type: :complex_migration,
367
+ complexity: complexity,
368
+ severity: :medium
369
+ }
370
+ end
371
+
372
+ smells
373
+ end
374
+
375
+ # Helper methods
376
+ def find_nodes(node, type, &block)
377
+ return unless node.is_a?(Parser::AST::Node)
378
+
379
+ yield(node) if node.type == type
380
+
381
+ node.children.each do |child|
382
+ find_nodes(child, type, &block)
383
+ end
384
+ end
385
+
386
+ def private_controller_method?(method_name)
387
+ %w[show new edit create update destroy].include?(method_name) ||
388
+ method_name.end_with?('_params')
389
+ end
390
+ end
391
+ end