rails_code_health 0.2.0 → 0.3.1
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 +41 -1
- data/README.md +4 -0
- data/lib/rails_code_health/ast_helpers.rb +126 -0
- data/lib/rails_code_health/configuration.rb +7 -0
- data/lib/rails_code_health/file_analyzer.rb +3 -1
- data/lib/rails_code_health/health_calculator.rb +33 -29
- data/lib/rails_code_health/rails_analyzer.rb +184 -109
- data/lib/rails_code_health/report_generator.rb +1 -1
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 806988266c00efaa8181b4379e6baa5f5ff9e5913fa4a0801fd993ededf3a580
|
|
4
|
+
data.tar.gz: 61e1909e93309e22265b961038f87da7c0367f38cf6cf9a763449e144e39c73f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37d498b553e834f76811b405f6be6b967fec45512170098b70b818bc952de8321cf7790dce0f7b93b1353fc373b12614ba9f7be98466e871d26f25b22ad75938
|
|
7
|
+
data.tar.gz: 1ca51781fa1cccfdabd3ef5e138d2cd2aea2e0a64432ea7e26802464c4e5c732185c09a8622ca4a558f8ccef7762653a168982dcc1db9f6fe29633ded8feb673
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.1] - 2026-05-18
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `ReportGenerator` no longer includes migration files in the "Top Performing Files" showcase. Migrations are almost always trivially simple by construction, so they routinely crowded out genuinely well-written healthy files in the top-5 list. Migrations are still analyzed and still appear in the "Files Needing Most Attention" list when problematic.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `docs/index.html` static landing page for GitHub Pages, explaining the why and how of the gem.
|
|
17
|
+
|
|
18
|
+
## [0.3.0] - 2026-05-12
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `RailsCodeHealth::ASTHelpers` module: shared AST traversal with scoped variants and visibility-aware def collection.
|
|
22
|
+
- Fixture-based RSpec test suite under `spec/fixtures/code_samples/`.
|
|
23
|
+
- New configuration group `smell_thresholds` for previously hard-coded values.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `RubyAnalyzer#count_public_methods_in_class` now actually distinguishes public from private methods (including inline `private def foo`).
|
|
27
|
+
- `RubyAnalyzer#count_parameters` now counts only true parameter nodes.
|
|
28
|
+
- `RubyAnalyzer` nesting depth calculation no longer counts `:begin` and `:block` as nesting levels.
|
|
29
|
+
- `RailsAnalyzer#count_controller_actions` no longer inverts public/private; the seven canonical RESTful actions are correctly counted, and private helpers are excluded.
|
|
30
|
+
- `RailsAnalyzer#has_direct_model_access?` only fires when model calls appear inside public controller actions.
|
|
31
|
+
- `RailsAnalyzer#has_business_logic?` uses stricter signals (business-verb calls on model receivers, transactions, loops with conditionals) instead of matching any compound `if` or any `.each do`.
|
|
32
|
+
- Model `count_associations`, `count_validations`, `count_callbacks`, `count_scopes` count only top-level class body macros — no comments, strings, or nested-class matches.
|
|
33
|
+
- `has_fat_model_smell?` uses class-scoped code-line count and class-scoped method count.
|
|
34
|
+
- View `count_view_logic_lines` parses ERB fragments and no longer flags plain HTML that happens to contain `if`, `unless`, etc., in text.
|
|
35
|
+
- Migration `has_data_changes?` recognizes `find_each`, `update`, `update_columns`, `update_column`, `update!`, `update_all`, `delete_all`, `save`, `save!`, raw `connection.execute`, and `execute`.
|
|
36
|
+
- Service `detect_service_dependencies` uses word boundaries — `Profile.` no longer matches the `File.` check.
|
|
37
|
+
- God class, high complexity, parameter, and nesting smell thresholds are now read from configuration instead of inlined.
|
|
38
|
+
- Service `detect_service_smells` now flags `:fat_service` at complexity ≥ 13 (was > 15, which left some genuinely-fat services unflagged).
|
|
39
|
+
- Interactor `detect_fail_usage` no longer double-counts `context.fail!` as both `context_fail` and `fail_bang`.
|
|
40
|
+
- Serializer `count_serializer_attributes` / `count_serializer_associations` now count each symbol (`attributes :a, :b, :c` = 3) instead of just one per declaration line.
|
|
41
|
+
- `FileAnalyzer#find_view_files` dedupes overlapping glob matches so files under `app/views/` are no longer counted twice when matched by both `app/views/**/*.erb` and `app/**/views/**/*.erb`.
|
|
42
|
+
- `HealthCalculator` service/interactor/serializer penalties now use flat (un-weighted) multipliers for the most severe issues (missing call method, fat service, complex organizer, fat serializer) — the global `rails_conventions` weight (0.15) was too small to make these matter.
|
|
43
|
+
|
|
44
|
+
### Changed (may affect scores)
|
|
45
|
+
- Score changes are expected on any project where the above false positives or false negatives applied. Re-baseline before comparing to v0.2.0 reports.
|
|
46
|
+
- Services missing a `call` method now score noticeably lower (penalty went from ~0.45 to 3.0); same for interactors. Fat serializers and complex organizers are also penalized more strongly.
|
|
47
|
+
|
|
10
48
|
## [0.2.0] - 2025-06-19
|
|
11
49
|
|
|
12
50
|
### Added
|
|
@@ -43,6 +81,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
43
81
|
- **Reporting**: Detailed console output with health categories and JSON export
|
|
44
82
|
- **CLI**: `rails-health` command with options for format, output file, and custom configuration
|
|
45
83
|
|
|
46
|
-
[Unreleased]: https://github.com/gkosmo/rails_code_health/compare/v0.
|
|
84
|
+
[Unreleased]: https://github.com/gkosmo/rails_code_health/compare/v0.3.1...HEAD
|
|
85
|
+
[0.3.1]: https://github.com/gkosmo/rails_code_health/compare/v0.3.0...v0.3.1
|
|
86
|
+
[0.3.0]: https://github.com/gkosmo/rails_code_health/compare/v0.2.0...v0.3.0
|
|
47
87
|
[0.2.0]: https://github.com/gkosmo/rails_code_health/compare/v0.1.0...v0.2.0
|
|
48
88
|
[0.1.0]: https://github.com/gkosmo/rails_code_health/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Rails Code Health
|
|
2
2
|
|
|
3
|
+
[](https://github.com/gkosmo/rails_code_health/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
A Ruby gem that evaluates the code health of Ruby on Rails applications, [inspired by CodeScene's research on technical debt and maintainability](https://arxiv.org/pdf/2203.04374
|
|
4
6
|
).
|
|
5
7
|
|
|
@@ -127,6 +129,8 @@ Health scores range from 1.0 (critical) to 10.0 (excellent) based on:
|
|
|
127
129
|
- **🔴 Alert (1.0-3.9)**: Significant problems requiring attention
|
|
128
130
|
- **⚫ Critical (<1.0)**: Severe issues, immediate action needed
|
|
129
131
|
|
|
132
|
+
> **Note on v0.3.0 scores:** v0.3.0 fixes several false positives and false negatives in the Rails-specific checks. Scores on the same codebase will move compared to v0.2.0. See the CHANGELOG for the list of changed checks.
|
|
133
|
+
|
|
130
134
|
## Configuration
|
|
131
135
|
|
|
132
136
|
Create a custom `thresholds.json` file:
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module RailsCodeHealth
|
|
2
|
+
module ASTHelpers
|
|
3
|
+
SCOPE_BOUNDARY_TYPES = %i[class module def defs sclass].freeze
|
|
4
|
+
VISIBILITY_MODIFIERS = %i[public private protected].freeze
|
|
5
|
+
|
|
6
|
+
# Yields every descendant (and the node itself) of the given type.
|
|
7
|
+
# Descends into all children.
|
|
8
|
+
def find_nodes(node, type, &block)
|
|
9
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
10
|
+
|
|
11
|
+
yield(node) if node.type == type
|
|
12
|
+
node.children.each { |child| find_nodes(child, type, &block) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns a hash { public: [def_nodes], private: [def_nodes], protected: [def_nodes] }
|
|
16
|
+
# for the given class node. Handles bare modifier blocks and inline `private def foo`.
|
|
17
|
+
# Defs inside `class << self` are excluded (the :sclass node is not descended into).
|
|
18
|
+
#
|
|
19
|
+
# NOTE: `private :symbol_name` / `private :a, :b` forms are NOT handled —
|
|
20
|
+
# methods so marked stay classified as :public. Only bare modifier blocks
|
|
21
|
+
# (`private` on its own line) and inline `private def foo` are recognized.
|
|
22
|
+
def defs_by_visibility(class_node)
|
|
23
|
+
result = { public: [], private: [], protected: [] }
|
|
24
|
+
return result unless class_node.is_a?(Parser::AST::Node)
|
|
25
|
+
return result unless %i[class module].include?(class_node.type)
|
|
26
|
+
|
|
27
|
+
current = :public
|
|
28
|
+
body = class_body_children(class_node)
|
|
29
|
+
|
|
30
|
+
body.each do |child|
|
|
31
|
+
next unless child.is_a?(Parser::AST::Node)
|
|
32
|
+
|
|
33
|
+
if child.type == :send && child.children[0].nil? &&
|
|
34
|
+
VISIBILITY_MODIFIERS.include?(child.children[1])
|
|
35
|
+
modifier = child.children[1]
|
|
36
|
+
if child.children.length == 2
|
|
37
|
+
# bare modifier — changes default for subsequent defs
|
|
38
|
+
current = modifier
|
|
39
|
+
else
|
|
40
|
+
# inline form: `private def foo` or `private :foo`
|
|
41
|
+
inline_target = child.children[2]
|
|
42
|
+
if inline_target.is_a?(Parser::AST::Node) && inline_target.type == :def
|
|
43
|
+
result[modifier] << inline_target
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
elsif child.type == :def
|
|
47
|
+
result[current] << child
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns direct `:send` nodes in the class body matching the given method name.
|
|
55
|
+
# Does not descend into nested classes, modules, or method bodies.
|
|
56
|
+
#
|
|
57
|
+
# NOTE: calls wrapped in a block (e.g., `included do ... end` in concerns)
|
|
58
|
+
# are NOT matched — the wrapping :block node is not a :send. Use
|
|
59
|
+
# class_body_children directly and inspect :block nodes if you need to find
|
|
60
|
+
# macro calls inside such wrappers.
|
|
61
|
+
def class_body_sends(class_node, method_name)
|
|
62
|
+
matches = []
|
|
63
|
+
return matches unless class_node.is_a?(Parser::AST::Node)
|
|
64
|
+
return matches unless %i[class module].include?(class_node.type)
|
|
65
|
+
|
|
66
|
+
class_body_children(class_node).each do |child|
|
|
67
|
+
next unless child.is_a?(Parser::AST::Node)
|
|
68
|
+
next unless child.type == :send
|
|
69
|
+
# Receiver-less send only (e.g., `has_many :foo`, not `MyMod.has_many :foo`)
|
|
70
|
+
next unless child.children[0].nil?
|
|
71
|
+
matches << child if child.children[1] == method_name
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
matches
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Yields each Ruby code fragment inside <% %> / <%= %> / <%- %> tags.
|
|
78
|
+
# Multi-line tags are handled. ERB comment tags (<%# ... %>) are skipped.
|
|
79
|
+
# Returns an Enumerator if no block given.
|
|
80
|
+
def erb_ruby_fragments(source)
|
|
81
|
+
return enum_for(:erb_ruby_fragments, source) unless block_given?
|
|
82
|
+
|
|
83
|
+
source.scan(/<%(?!#)=?-?(.*?)-?%>/m) do |match|
|
|
84
|
+
yield match[0]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Like find_nodes, but does not descend into nested class/module/def/defs/sclass
|
|
89
|
+
# nodes. The starting node itself is inspected; all children are inspected, but
|
|
90
|
+
# nested scope-introducing constructs are not recursed into.
|
|
91
|
+
def find_nodes_in_scope(node, type, &block)
|
|
92
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
93
|
+
|
|
94
|
+
yield(node) if node.type == type
|
|
95
|
+
|
|
96
|
+
node.children.each do |child|
|
|
97
|
+
next unless child.is_a?(Parser::AST::Node)
|
|
98
|
+
|
|
99
|
+
# Scope-boundary children get yielded if they match the target type,
|
|
100
|
+
# but we do not recurse into them.
|
|
101
|
+
if SCOPE_BOUNDARY_TYPES.include?(child.type)
|
|
102
|
+
yield(child) if child.type == type
|
|
103
|
+
next
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
find_nodes_in_scope(child, type, &block)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# The body of a `class` node is its third child; for `module`, the second.
|
|
113
|
+
# The body may be a single node or a `:begin` containing a sequence of nodes.
|
|
114
|
+
def class_body_children(class_or_module_node)
|
|
115
|
+
body =
|
|
116
|
+
case class_or_module_node.type
|
|
117
|
+
when :class then class_or_module_node.children[2]
|
|
118
|
+
when :module then class_or_module_node.children[1]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
return [] if body.nil?
|
|
122
|
+
return body.children if body.is_a?(Parser::AST::Node) && body.type == :begin
|
|
123
|
+
[body]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -68,6 +68,13 @@ module RailsCodeHealth
|
|
|
68
68
|
'dependency_count' => { 'green' => 3, 'yellow' => 5, 'red' => 8 },
|
|
69
69
|
'complexity_score' => { 'green' => 10, 'yellow' => 15, 'red' => 25 }
|
|
70
70
|
},
|
|
71
|
+
'smell_thresholds' => {
|
|
72
|
+
'god_class_lines' => 400,
|
|
73
|
+
'god_class_methods' => 20,
|
|
74
|
+
'high_complexity_method' => 15,
|
|
75
|
+
'too_many_parameters' => 5,
|
|
76
|
+
'nested_conditionals' => 4
|
|
77
|
+
},
|
|
71
78
|
'file_type_multipliers' => {
|
|
72
79
|
'controllers' => 1.2,
|
|
73
80
|
'models' => 1.0,
|
|
@@ -91,7 +91,9 @@ module RailsCodeHealth
|
|
|
91
91
|
files.concat(Dir.glob(pattern))
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
# Patterns overlap (`app/views/**/*.erb` and `app/**/views/**/*.erb` both
|
|
95
|
+
# match files under `app/views/...`); dedupe by absolute path.
|
|
96
|
+
files.uniq.map { |f| Pathname.new(f) }
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
def should_skip_file?(relative_path)
|
|
@@ -225,97 +225,101 @@ module RailsCodeHealth
|
|
|
225
225
|
|
|
226
226
|
def calculate_service_penalties(service_data)
|
|
227
227
|
penalties = []
|
|
228
|
-
|
|
229
|
-
# Missing call method - critical for services
|
|
228
|
+
|
|
229
|
+
# Missing call method - critical for services. Use a flat (un-attenuated)
|
|
230
|
+
# penalty here because the `rails_conventions` weight (0.15) is sized for
|
|
231
|
+
# softer conventions; missing the core service contract is severe.
|
|
230
232
|
unless service_data[:has_call_method]
|
|
231
|
-
penalties << 3.0
|
|
233
|
+
penalties << 3.0
|
|
232
234
|
end
|
|
233
|
-
|
|
235
|
+
|
|
234
236
|
# Too many dependencies
|
|
235
237
|
dependency_count = service_data[:dependencies]&.count || 0
|
|
236
238
|
service_thresholds = @thresholds['service_thresholds']['dependency_count']
|
|
237
239
|
if dependency_count > service_thresholds['yellow']
|
|
238
|
-
severity = dependency_count > service_thresholds['red'] ?
|
|
239
|
-
penalties << severity
|
|
240
|
+
severity = dependency_count > service_thresholds['red'] ? 1.5 : 0.75
|
|
241
|
+
penalties << severity
|
|
240
242
|
end
|
|
241
|
-
|
|
243
|
+
|
|
242
244
|
# High complexity
|
|
243
245
|
complexity = service_data[:complexity_score] || 0
|
|
244
246
|
complexity_thresholds = @thresholds['service_thresholds']['complexity_score']
|
|
245
247
|
if complexity > complexity_thresholds['yellow']
|
|
246
248
|
severity = complexity > complexity_thresholds['red'] ? 2.0 : 1.0
|
|
247
|
-
penalties << severity
|
|
249
|
+
penalties << severity
|
|
248
250
|
end
|
|
249
|
-
|
|
251
|
+
|
|
250
252
|
# Missing error handling
|
|
251
253
|
error_handling = service_data[:error_handling] || {}
|
|
252
254
|
unless error_handling[:has_error_handling]
|
|
253
255
|
penalties << 1.0 * @weights['rails_conventions']
|
|
254
256
|
end
|
|
255
|
-
|
|
257
|
+
|
|
256
258
|
penalties
|
|
257
259
|
end
|
|
258
260
|
|
|
259
261
|
def calculate_interactor_penalties(interactor_data)
|
|
260
262
|
penalties = []
|
|
261
|
-
|
|
262
|
-
# Missing call method - critical for interactors
|
|
263
|
+
|
|
264
|
+
# Missing call method - critical for interactors (flat penalty; see
|
|
265
|
+
# service equivalent for rationale).
|
|
263
266
|
unless interactor_data[:has_call_method]
|
|
264
|
-
penalties << 3.0
|
|
267
|
+
penalties << 3.0
|
|
265
268
|
end
|
|
266
|
-
|
|
269
|
+
|
|
267
270
|
# Complex organizers
|
|
268
271
|
if interactor_data[:is_organizer]
|
|
269
272
|
complexity = interactor_data[:complexity_score] || 0
|
|
270
273
|
if complexity > 20 # Organizers should be simple orchestrators
|
|
271
|
-
penalties << 2.0
|
|
274
|
+
penalties << 2.0
|
|
272
275
|
end
|
|
273
276
|
end
|
|
274
|
-
|
|
277
|
+
|
|
275
278
|
# Missing failure handling
|
|
276
279
|
fail_usage = interactor_data[:fail_usage] || {}
|
|
277
280
|
if (fail_usage[:context_fail] || 0) == 0 && (fail_usage[:fail_bang] || 0) == 0
|
|
278
281
|
penalties << 1.5 * @weights['rails_conventions']
|
|
279
282
|
end
|
|
280
|
-
|
|
283
|
+
|
|
281
284
|
# High complexity for regular interactors
|
|
282
285
|
unless interactor_data[:is_organizer]
|
|
283
286
|
complexity = interactor_data[:complexity_score] || 0
|
|
284
287
|
if complexity > 15
|
|
285
|
-
penalties << 1.5
|
|
288
|
+
penalties << 1.5
|
|
286
289
|
end
|
|
287
290
|
end
|
|
288
|
-
|
|
291
|
+
|
|
289
292
|
penalties
|
|
290
293
|
end
|
|
291
294
|
|
|
292
295
|
def calculate_serializer_penalties(serializer_data)
|
|
293
296
|
penalties = []
|
|
294
|
-
|
|
295
|
-
# Too many attributes/associations (fat serializer)
|
|
297
|
+
|
|
298
|
+
# Too many attributes/associations (fat serializer). Flat penalties because
|
|
299
|
+
# `rails_conventions` weight (0.15) drowned out the fat signal.
|
|
296
300
|
attribute_count = serializer_data[:attribute_count] || 0
|
|
297
301
|
association_count = serializer_data[:association_count] || 0
|
|
298
302
|
total_fields = attribute_count + association_count
|
|
299
|
-
|
|
303
|
+
|
|
300
304
|
if total_fields > 20
|
|
301
|
-
penalties <<
|
|
305
|
+
penalties << 1.5
|
|
302
306
|
elsif total_fields > 15
|
|
303
|
-
penalties <<
|
|
307
|
+
penalties << 0.5
|
|
304
308
|
end
|
|
305
|
-
|
|
309
|
+
|
|
306
310
|
# Too many custom methods (complex logic)
|
|
307
311
|
custom_method_count = serializer_data[:custom_method_count] || 0
|
|
308
312
|
if custom_method_count > 10
|
|
309
|
-
penalties <<
|
|
313
|
+
penalties << 0.8
|
|
310
314
|
elsif custom_method_count > 5
|
|
311
|
-
penalties << 0.
|
|
315
|
+
penalties << 0.4
|
|
312
316
|
end
|
|
313
|
-
|
|
317
|
+
|
|
314
318
|
# Empty serializer (no value)
|
|
315
319
|
if total_fields == 0 && custom_method_count == 0
|
|
316
320
|
penalties << 1.0 * @weights['rails_conventions']
|
|
317
321
|
end
|
|
318
|
-
|
|
322
|
+
|
|
319
323
|
penalties
|
|
320
324
|
end
|
|
321
325
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module RailsCodeHealth
|
|
2
2
|
class RailsAnalyzer
|
|
3
|
+
include RailsCodeHealth::ASTHelpers
|
|
4
|
+
|
|
3
5
|
def initialize(file_path, file_type)
|
|
4
6
|
@file_path = file_path
|
|
5
7
|
@file_type = file_type
|
|
@@ -104,11 +106,10 @@ module RailsCodeHealth
|
|
|
104
106
|
return 0 unless @ast
|
|
105
107
|
|
|
106
108
|
action_count = 0
|
|
107
|
-
find_nodes(@ast, :
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
action_count += 1
|
|
109
|
+
find_nodes(@ast, :class) do |class_node|
|
|
110
|
+
defs_by_visibility(class_node)[:public].each do |def_node|
|
|
111
|
+
name = def_node.children[0].to_s
|
|
112
|
+
action_count += 1 unless name.start_with?('_')
|
|
112
113
|
end
|
|
113
114
|
end
|
|
114
115
|
action_count
|
|
@@ -122,17 +123,25 @@ module RailsCodeHealth
|
|
|
122
123
|
@source.include?('params.require') || @source.include?('params.permit')
|
|
123
124
|
end
|
|
124
125
|
|
|
126
|
+
DIRECT_MODEL_METHODS = %i[find find_by where create create! update update! all first last destroy_all].freeze
|
|
127
|
+
|
|
125
128
|
def has_direct_model_access?
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
return false unless @ast
|
|
130
|
+
|
|
131
|
+
found = false
|
|
132
|
+
find_nodes(@ast, :class) do |class_node|
|
|
133
|
+
defs_by_visibility(class_node)[:public].each do |def_node|
|
|
134
|
+
find_nodes(def_node, :send) do |send_node|
|
|
135
|
+
receiver = send_node.children[0]
|
|
136
|
+
method_name = send_node.children[1]
|
|
137
|
+
# Receiver must be a constant (a likely model class) and the method must be an AR method.
|
|
138
|
+
next unless receiver.is_a?(Parser::AST::Node) && receiver.type == :const
|
|
139
|
+
next unless DIRECT_MODEL_METHODS.include?(method_name)
|
|
140
|
+
found = true
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
found
|
|
136
145
|
end
|
|
137
146
|
|
|
138
147
|
def detect_response_formats
|
|
@@ -144,16 +153,22 @@ module RailsCodeHealth
|
|
|
144
153
|
formats
|
|
145
154
|
end
|
|
146
155
|
|
|
156
|
+
BUSINESS_VERBS = %i[calculate compute process charge refund transition].freeze
|
|
157
|
+
# Methods with these prefixes likely indicate business logic.
|
|
158
|
+
# `process_` was previously included but removed because of false positives
|
|
159
|
+
# on common AR attributes like `process_id`.
|
|
160
|
+
BUSINESS_VERB_PREFIXES = %w[calculate_ compute_].freeze
|
|
161
|
+
|
|
147
162
|
def has_business_logic?
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
return false unless @ast
|
|
164
|
+
|
|
165
|
+
found = false
|
|
166
|
+
find_nodes(@ast, :class) do |class_node|
|
|
167
|
+
defs_by_visibility(class_node)[:public].each do |def_node|
|
|
168
|
+
found = true if action_has_business_logic?(def_node)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
found
|
|
157
172
|
end
|
|
158
173
|
|
|
159
174
|
def detect_controller_smells
|
|
@@ -192,52 +207,115 @@ module RailsCodeHealth
|
|
|
192
207
|
smells
|
|
193
208
|
end
|
|
194
209
|
|
|
210
|
+
def action_has_business_logic?(def_node)
|
|
211
|
+
# NOTE: arithmetic signal (plan A2 item 2) intentionally not implemented —
|
|
212
|
+
# accuracy of detecting "two non-literal operands" was deemed not worth
|
|
213
|
+
# the false positive risk in v0.3.0.
|
|
214
|
+
return true if business_verb_call?(def_node)
|
|
215
|
+
return true if transaction_block?(def_node)
|
|
216
|
+
return true if loop_with_conditional?(def_node)
|
|
217
|
+
false
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def business_verb_call?(def_node)
|
|
221
|
+
found = false
|
|
222
|
+
find_nodes(def_node, :send) do |send_node|
|
|
223
|
+
method_name = send_node.children[1].to_s
|
|
224
|
+
if BUSINESS_VERBS.include?(method_name.to_sym) ||
|
|
225
|
+
BUSINESS_VERB_PREFIXES.any? { |p| method_name.start_with?(p) }
|
|
226
|
+
found = true
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
found
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def transaction_block?(def_node)
|
|
233
|
+
found = false
|
|
234
|
+
find_nodes(def_node, :block) do |block_node|
|
|
235
|
+
send_node = block_node.children[0]
|
|
236
|
+
next unless send_node.is_a?(Parser::AST::Node) && send_node.type == :send
|
|
237
|
+
found = true if send_node.children[1] == :transaction
|
|
238
|
+
end
|
|
239
|
+
found
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def loop_with_conditional?(def_node)
|
|
243
|
+
found = false
|
|
244
|
+
find_nodes(def_node, :block) do |block_node|
|
|
245
|
+
send_node = block_node.children[0]
|
|
246
|
+
next unless send_node.is_a?(Parser::AST::Node) && send_node.type == :send
|
|
247
|
+
next unless %i[each map select reject].include?(send_node.children[1])
|
|
248
|
+
# Look for conditionals inside the block body.
|
|
249
|
+
find_nodes(block_node.children[2], :if) { found = true }
|
|
250
|
+
find_nodes(block_node.children[2], :case) { found = true }
|
|
251
|
+
end
|
|
252
|
+
found
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
ASSOCIATION_MACROS = %i[belongs_to has_one has_many has_and_belongs_to_many].freeze
|
|
256
|
+
VALIDATION_MACROS = %i[validates validates_presence_of validates_uniqueness_of validates_format_of validates_length_of validates_numericality_of validates_inclusion_of validates_exclusion_of validates_acceptance_of validates_confirmation_of].freeze
|
|
257
|
+
CALLBACK_MACROS = %i[before_save after_save before_create after_create before_update after_update before_destroy after_destroy after_commit after_rollback before_validation after_validation].freeze
|
|
258
|
+
|
|
195
259
|
# Model analysis methods
|
|
196
260
|
def count_associations
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
261
|
+
total = 0
|
|
262
|
+
find_nodes(@ast, :class) do |class_node|
|
|
263
|
+
ASSOCIATION_MACROS.each do |macro|
|
|
264
|
+
total += class_body_sends(class_node, macro).size
|
|
265
|
+
end
|
|
202
266
|
end
|
|
203
|
-
|
|
204
|
-
associations
|
|
267
|
+
total
|
|
205
268
|
end
|
|
206
269
|
|
|
207
270
|
def count_validations
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
271
|
+
total = 0
|
|
272
|
+
find_nodes(@ast, :class) do |class_node|
|
|
273
|
+
VALIDATION_MACROS.each do |macro|
|
|
274
|
+
total += class_body_sends(class_node, macro).size
|
|
275
|
+
end
|
|
213
276
|
end
|
|
214
|
-
|
|
215
|
-
validations
|
|
277
|
+
total
|
|
216
278
|
end
|
|
217
279
|
|
|
218
280
|
def count_callbacks
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
281
|
+
total = 0
|
|
282
|
+
find_nodes(@ast, :class) do |class_node|
|
|
283
|
+
CALLBACK_MACROS.each do |macro|
|
|
284
|
+
total += class_body_sends(class_node, macro).size
|
|
285
|
+
end
|
|
224
286
|
end
|
|
225
|
-
|
|
226
|
-
callbacks
|
|
287
|
+
total
|
|
227
288
|
end
|
|
228
289
|
|
|
229
290
|
def count_scopes
|
|
230
|
-
|
|
291
|
+
total = 0
|
|
292
|
+
find_nodes(@ast, :class) do |class_node|
|
|
293
|
+
total += class_body_sends(class_node, :scope).size
|
|
294
|
+
end
|
|
295
|
+
total
|
|
231
296
|
end
|
|
232
297
|
|
|
233
298
|
def has_fat_model_smell?
|
|
234
299
|
return false unless @ast
|
|
235
300
|
|
|
236
|
-
|
|
301
|
+
class_node = nil
|
|
302
|
+
find_nodes(@ast, :class) { |n| class_node ||= n }
|
|
303
|
+
return false unless class_node
|
|
304
|
+
|
|
305
|
+
code_lines = code_line_count(class_node)
|
|
237
306
|
method_count = 0
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
307
|
+
find_nodes_in_scope(class_node, :def) { method_count += 1 }
|
|
308
|
+
|
|
309
|
+
code_lines > 200 && method_count > 15
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def code_line_count(node)
|
|
313
|
+
return 0 unless node.respond_to?(:loc) && node.loc.respond_to?(:expression)
|
|
314
|
+
expr = node.loc.expression
|
|
315
|
+
return 0 unless expr
|
|
316
|
+
|
|
317
|
+
lines = expr.source.lines
|
|
318
|
+
lines.count { |line| line.strip != '' && !line.strip.start_with?('#') }
|
|
241
319
|
end
|
|
242
320
|
|
|
243
321
|
def detect_model_smells
|
|
@@ -270,16 +348,16 @@ module RailsCodeHealth
|
|
|
270
348
|
smells
|
|
271
349
|
end
|
|
272
350
|
|
|
351
|
+
VIEW_CONTROL_FLOW_KEYWORDS = %w[if unless elsif else case for while end].freeze
|
|
352
|
+
|
|
273
353
|
# View analysis methods
|
|
274
|
-
def count_view_logic_lines(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
logic_count += 1 if line.match?(/<%((?!%>).)*%>/) || line.match?(/<%((?!%>).)*if|unless|case|for|while/)
|
|
354
|
+
def count_view_logic_lines(_lines)
|
|
355
|
+
count = 0
|
|
356
|
+
erb_ruby_fragments(@source).each do |ruby|
|
|
357
|
+
tokens = ruby.scan(/\b\w+\b/)
|
|
358
|
+
count += 1 if (tokens & VIEW_CONTROL_FLOW_KEYWORDS).any?
|
|
280
359
|
end
|
|
281
|
-
|
|
282
|
-
logic_count
|
|
360
|
+
count
|
|
283
361
|
end
|
|
284
362
|
|
|
285
363
|
def has_inline_styles?
|
|
@@ -350,10 +428,22 @@ module RailsCodeHealth
|
|
|
350
428
|
smells
|
|
351
429
|
end
|
|
352
430
|
|
|
431
|
+
DATA_CHANGE_METHODS = %i[
|
|
432
|
+
execute update_all delete_all
|
|
433
|
+
find_each update update_columns update_column
|
|
434
|
+
update! save save!
|
|
435
|
+
].freeze
|
|
436
|
+
|
|
353
437
|
# Migration analysis methods
|
|
354
438
|
def has_data_changes?
|
|
355
|
-
|
|
356
|
-
|
|
439
|
+
return false unless @ast
|
|
440
|
+
|
|
441
|
+
found = false
|
|
442
|
+
find_nodes(@ast, :send) do |send_node|
|
|
443
|
+
method_name = send_node.children[1]
|
|
444
|
+
found = true if DATA_CHANGE_METHODS.include?(method_name)
|
|
445
|
+
end
|
|
446
|
+
found
|
|
357
447
|
end
|
|
358
448
|
|
|
359
449
|
def has_index_changes?
|
|
@@ -432,34 +522,29 @@ module RailsCodeHealth
|
|
|
432
522
|
end
|
|
433
523
|
|
|
434
524
|
def detect_service_dependencies
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
dependencies << :active_record
|
|
525
|
+
deps = []
|
|
526
|
+
|
|
527
|
+
if @source.match?(/\b(?:[A-Z]\w*)\.(?:find|find_by|where|create|create!|update|update!|all|first|last)\b/)
|
|
528
|
+
deps << :active_record
|
|
440
529
|
end
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
dependencies << :external_api
|
|
530
|
+
|
|
531
|
+
if @source.match?(/\b(?:Net::HTTP|HTTParty|Faraday|RestClient|Typhoeus)\b/)
|
|
532
|
+
deps << :external_api
|
|
445
533
|
end
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
dependencies << :file_system
|
|
534
|
+
|
|
535
|
+
if @source.match?(/\b(?:File|Dir|FileUtils|Pathname)\.\w+/)
|
|
536
|
+
deps << :file_system
|
|
450
537
|
end
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
dependencies << :email
|
|
538
|
+
|
|
539
|
+
if @source.match?(/\b(?:Mailer|ActionMailer)\b/) || @source.match?(/\.deliver(?:_now|_later)?\b/)
|
|
540
|
+
deps << :email
|
|
455
541
|
end
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
dependencies << :cache
|
|
542
|
+
|
|
543
|
+
if @source.match?(/\bRails\.cache\b/) || @source.match?(/\bRedis\b/) || @source.match?(/\bcache_store\b/)
|
|
544
|
+
deps << :cache
|
|
460
545
|
end
|
|
461
|
-
|
|
462
|
-
|
|
546
|
+
|
|
547
|
+
deps.uniq
|
|
463
548
|
end
|
|
464
549
|
|
|
465
550
|
def detect_error_handling
|
|
@@ -502,7 +587,7 @@ module RailsCodeHealth
|
|
|
502
587
|
end
|
|
503
588
|
|
|
504
589
|
complexity = calculate_service_complexity
|
|
505
|
-
if complexity
|
|
590
|
+
if complexity >= 13
|
|
506
591
|
smells << {
|
|
507
592
|
type: :fat_service,
|
|
508
593
|
complexity: complexity,
|
|
@@ -546,7 +631,8 @@ module RailsCodeHealth
|
|
|
546
631
|
def detect_fail_usage
|
|
547
632
|
{
|
|
548
633
|
context_fail: @source.scan(/context\.fail/).count,
|
|
549
|
-
|
|
634
|
+
# `fail!` only — NOT `context.fail!` (that's counted as context_fail).
|
|
635
|
+
fail_bang: @source.scan(/(?<!\.)\bfail!/).count
|
|
550
636
|
}
|
|
551
637
|
end
|
|
552
638
|
|
|
@@ -624,19 +710,23 @@ module RailsCodeHealth
|
|
|
624
710
|
end
|
|
625
711
|
|
|
626
712
|
def count_serializer_attributes
|
|
627
|
-
# Count
|
|
628
|
-
|
|
713
|
+
# Count each symbol passed to attributes/attribute calls.
|
|
714
|
+
# `attributes :id, :name, :email` counts as 3; `attribute :full_name` as 1.
|
|
715
|
+
count = 0
|
|
716
|
+
@source.scan(/^\s*attributes?\s+(.+)$/).each do |(args)|
|
|
717
|
+
count += args.scan(/:\w+/).size
|
|
718
|
+
end
|
|
719
|
+
count
|
|
629
720
|
end
|
|
630
721
|
|
|
631
722
|
def count_serializer_associations
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
723
|
+
count = 0
|
|
724
|
+
%w[has_one has_many belongs_to].each do |method|
|
|
725
|
+
@source.scan(/^\s*#{method}\s+(.+)$/).each do |(args)|
|
|
726
|
+
count += args.scan(/:\w+/).size
|
|
727
|
+
end
|
|
637
728
|
end
|
|
638
|
-
|
|
639
|
-
associations
|
|
729
|
+
count
|
|
640
730
|
end
|
|
641
731
|
|
|
642
732
|
def count_custom_serializer_methods
|
|
@@ -693,20 +783,5 @@ module RailsCodeHealth
|
|
|
693
783
|
smells
|
|
694
784
|
end
|
|
695
785
|
|
|
696
|
-
# Helper methods
|
|
697
|
-
def find_nodes(node, type, &block)
|
|
698
|
-
return unless node.is_a?(Parser::AST::Node)
|
|
699
|
-
|
|
700
|
-
yield(node) if node.type == type
|
|
701
|
-
|
|
702
|
-
node.children.each do |child|
|
|
703
|
-
find_nodes(child, type, &block)
|
|
704
|
-
end
|
|
705
|
-
end
|
|
706
|
-
|
|
707
|
-
def private_controller_method?(method_name)
|
|
708
|
-
%w[show new edit create update destroy].include?(method_name) ||
|
|
709
|
-
method_name.end_with?('_params')
|
|
710
|
-
end
|
|
711
786
|
end
|
|
712
787
|
end
|
|
@@ -183,7 +183,7 @@ module RailsCodeHealth
|
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
# Show best files if we have healthy ones
|
|
186
|
-
healthy_files = @results.select { |r| r[:health_category] == :healthy }
|
|
186
|
+
healthy_files = @results.select { |r| r[:health_category] == :healthy && r[:file_type] != :migration }
|
|
187
187
|
if healthy_files.any?
|
|
188
188
|
detailed << "✅ Top Performing Files:"
|
|
189
189
|
detailed << ""
|
|
@@ -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.1
|
|
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: []
|