linecounter 0.1.2 → 0.1.3
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 +15 -1
- data/README.md +5 -0
- data/lib/linecounter/analyzer.rb +7 -2
- data/lib/linecounter/cli.rb +9 -1
- data/lib/linecounter/report.rb +53 -0
- data/lib/linecounter/structure_analyzer.rb +31 -2
- data/lib/linecounter/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 98e5cacd90c7df107053970b93d250d554f6220d2929569271f391205ed731c7
|
|
4
|
+
data.tar.gz: d2693bf65a29b6af2e8e8c374013f611a4724eed9af679bb1c1a33af8bd8cff5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c3d2a41191872cc1198591b2f668bd59bdfcbc7170e2aab00b9f9ab536f57ddbf6d0b1454cd968637e964ce8b2c347b69c85df0d055f93bac166f33de4f4ec2
|
|
7
|
+
data.tar.gz: 2b4192f5a738fa6ea680823360eb99fc791c3991f04fcff8fb129c7cda2d2e48096acc4cd106eee45edb2268c8a7ec52a66c93738419e6aed1be28a7a7901dc6
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.3] - 2026-07-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `--crud-only-controllers` lists Rails controllers whose public actions are all
|
|
13
|
+
standard RESTful actions (index/show/new/create/edit/update/destroy). Private
|
|
14
|
+
and protected helpers are ignored; a single non-CRUD public action excludes a
|
|
15
|
+
controller. Available in both text and JSON output.
|
|
16
|
+
- `--non-crud-controllers` lists the inverse of `--crud-only-controllers`:
|
|
17
|
+
controllers that expose at least one custom (non-RESTful) public action, with
|
|
18
|
+
those extra actions shown. Available in both text and JSON output.
|
|
19
|
+
- `--no-churn` skips the per-file git churn computation, which dominates runtime
|
|
20
|
+
on large repositories (one `git log` per file). The Churn column reports 0.
|
|
21
|
+
|
|
9
22
|
## [0.1.2] - 2026-05-24
|
|
10
23
|
|
|
11
24
|
### Changed
|
|
@@ -33,7 +46,8 @@ release is the intended patch following 0.1.1.
|
|
|
33
46
|
- AST-based analysis via Prism for accurate structure and branch signals
|
|
34
47
|
(no false positives from keywords in strings, comments, or method bodies).
|
|
35
48
|
|
|
36
|
-
[Unreleased]: https://github.com/roberthopman/linecounter/compare/v0.1.
|
|
49
|
+
[Unreleased]: https://github.com/roberthopman/linecounter/compare/v0.1.3...HEAD
|
|
50
|
+
[0.1.3]: https://github.com/roberthopman/linecounter/compare/v0.1.2...v0.1.3
|
|
37
51
|
[0.1.2]: https://github.com/roberthopman/linecounter/compare/v0.1.1...v0.1.2
|
|
38
52
|
[0.1.1]: https://github.com/roberthopman/linecounter/compare/v0.1.0...v0.1.1
|
|
39
53
|
[0.1.0]: https://github.com/roberthopman/linecounter/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -39,6 +39,9 @@ linecounter [options]
|
|
|
39
39
|
- `--show-structure-overview` Show a summary of class structure counts across all files, including `avg_loc_per_item` (avg statement lines per item).
|
|
40
40
|
- `--show-interaction-overview` Alias for `--show-structure-overview`.
|
|
41
41
|
- `--detailed-structure` Show overall structure averages (avg lines per item) for each regex item across all files.
|
|
42
|
+
- `--crud-only-controllers` List Rails controllers (files matching `app/controllers/**/*_controller.rb`) whose public actions are all standard RESTful actions (`index`, `show`, `new`, `create`, `edit`, `update`, `destroy`). Private and protected methods are ignored; a single non-CRUD public action excludes the controller.
|
|
43
|
+
- `--non-crud-controllers` The inverse: list controllers that expose at least one custom (non-RESTful) public action, showing those extra actions. Useful for spotting controllers that have grown beyond plain resourceful routing.
|
|
44
|
+
- `--no-churn` Skip git churn computation. Churn runs one `git log` per file, so on large repos it dominates runtime; skipping it makes runs near-instant at the cost of a `0` Churn column. Useful with `--crud-only-controllers`, which does not need churn.
|
|
42
45
|
- `-h`, `--help` Show help.
|
|
43
46
|
|
|
44
47
|
## Examples
|
|
@@ -57,6 +60,8 @@ linecounter --repo /path/to/repo --min-loc 50
|
|
|
57
60
|
linecounter --repo /path/to/repo --show-branch-count
|
|
58
61
|
linecounter --repo /path/to/repo --show-structure-overview
|
|
59
62
|
linecounter --repo /path/to/repo --detailed-structure
|
|
63
|
+
linecounter --repo /path/to/repo --crud-only-controllers
|
|
64
|
+
linecounter --repo /path/to/repo --non-crud-controllers --no-churn
|
|
60
65
|
linecounter --repo /path/to/repo --json
|
|
61
66
|
linecounter --repo /path/to/repo --top 30 --since 3.months.ago --show-structure-overview
|
|
62
67
|
```
|
data/lib/linecounter/analyzer.rb
CHANGED
|
@@ -13,9 +13,11 @@ module Linecounter
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
module Analyzer
|
|
16
|
+
CONTROLLER_PATH = %r{app/controllers/.*_controller\.rb\z}
|
|
17
|
+
|
|
16
18
|
module_function
|
|
17
19
|
|
|
18
|
-
def run(repo_path:, min_loc:, since:)
|
|
20
|
+
def run(repo_path:, min_loc:, since:, churn: true)
|
|
19
21
|
rows = []
|
|
20
22
|
structure_overview = Hash.new(0)
|
|
21
23
|
structure_item_loc_overview = Hash.new(0)
|
|
@@ -28,12 +30,14 @@ module Linecounter
|
|
|
28
30
|
|
|
29
31
|
breakdown = BranchAnalyzer.breakdown(content)
|
|
30
32
|
b = breakdown.values.sum
|
|
31
|
-
c = Git.churn(repo_path, file, since)
|
|
33
|
+
c = churn ? Git.churn(repo_path, file, since) : 0
|
|
32
34
|
structures, structure_item_counts, structure_item_loc = StructureAnalyzer.counts(content)
|
|
33
35
|
structures.each { |k, v| structure_overview[k] += v }
|
|
34
36
|
structure_item_loc.each { |k, v| structure_item_loc_overview[k] += v }
|
|
35
37
|
structure_item_counts.each { |k, v| structure_item_counts_overview[k] += v }
|
|
36
38
|
|
|
39
|
+
crud_profile = file.match?(CONTROLLER_PATH) ? StructureAnalyzer.crud_profile(content) : nil
|
|
40
|
+
|
|
37
41
|
rows << {
|
|
38
42
|
file: file,
|
|
39
43
|
loc: size,
|
|
@@ -42,6 +46,7 @@ module Linecounter
|
|
|
42
46
|
structures: structures,
|
|
43
47
|
structure_item_counts: structure_item_counts,
|
|
44
48
|
structure_item_loc: structure_item_loc,
|
|
49
|
+
crud_profile: crud_profile,
|
|
45
50
|
churn: c
|
|
46
51
|
}
|
|
47
52
|
end
|
data/lib/linecounter/cli.rb
CHANGED
|
@@ -13,6 +13,9 @@ module Linecounter
|
|
|
13
13
|
show_branch_count: false,
|
|
14
14
|
show_structure_overview: false,
|
|
15
15
|
show_detailed_structure: false,
|
|
16
|
+
crud_only_controllers: false,
|
|
17
|
+
non_crud_controllers: false,
|
|
18
|
+
no_churn: false,
|
|
16
19
|
repo: nil
|
|
17
20
|
}.freeze
|
|
18
21
|
|
|
@@ -28,7 +31,7 @@ module Linecounter
|
|
|
28
31
|
repo_path = File.expand_path(options[:repo])
|
|
29
32
|
abort "Not inside a git repository: #{repo_path}" unless Git.repo?(repo_path)
|
|
30
33
|
|
|
31
|
-
result = Analyzer.run(repo_path: repo_path, min_loc: options[:min_loc], since: options[:since])
|
|
34
|
+
result = Analyzer.run(repo_path: repo_path, min_loc: options[:min_loc], since: options[:since], churn: !options[:no_churn])
|
|
32
35
|
Report.render(result, options)
|
|
33
36
|
end
|
|
34
37
|
|
|
@@ -46,6 +49,9 @@ module Linecounter
|
|
|
46
49
|
o.on("--show-structure-overview", "Show a summary of class structure counts across all files, including avg_loc_per_item (avg statement lines per item).") { options[:show_structure_overview] = true }
|
|
47
50
|
o.on("--show-interaction-overview", "Alias for --show-structure-overview.") { options[:show_structure_overview] = true }
|
|
48
51
|
o.on("--detailed-structure", "Show overall structure averages (avg lines per item) for each regex item across all files.") { options[:show_detailed_structure] = true }
|
|
52
|
+
o.on("--crud-only-controllers", "List Rails controllers whose public actions are all standard RESTful actions (index/show/new/create/edit/update/destroy).") { options[:crud_only_controllers] = true }
|
|
53
|
+
o.on("--non-crud-controllers", "List Rails controllers that expose custom (non-RESTful) public actions, showing those extra actions.") { options[:non_crud_controllers] = true }
|
|
54
|
+
o.on("--no-churn", "Skip git churn computation (much faster on large repos; the Churn column reports 0).") { options[:no_churn] = true }
|
|
49
55
|
o.on("-h", "--help", "Show this help.") { puts o; exit }
|
|
50
56
|
o.separator ""
|
|
51
57
|
o.separator "Examples:"
|
|
@@ -58,6 +64,8 @@ module Linecounter
|
|
|
58
64
|
o.separator " linecounter --repo /path/to/repo --show-branch-count"
|
|
59
65
|
o.separator " linecounter --repo /path/to/repo --show-structure-overview"
|
|
60
66
|
o.separator " linecounter --repo /path/to/repo --detailed-structure"
|
|
67
|
+
o.separator " linecounter --repo /path/to/repo --crud-only-controllers"
|
|
68
|
+
o.separator " linecounter --repo /path/to/repo --non-crud-controllers"
|
|
61
69
|
o.separator " linecounter --repo /path/to/repo --json"
|
|
62
70
|
o.separator " linecounter --repo /path/to/repo --top 30 --since 3.months.ago"
|
|
63
71
|
end
|
data/lib/linecounter/report.rb
CHANGED
|
@@ -36,6 +36,17 @@ module Linecounter
|
|
|
36
36
|
unless options[:show_detailed_structure]
|
|
37
37
|
payload[:top] = payload[:top].map { |r| r.reject { |k, _| k == :structure_item_loc || k == :structure_item_counts } }
|
|
38
38
|
end
|
|
39
|
+
payload[:top] = payload[:top].map { |r| r.reject { |k, _| k == :crud_profile } }
|
|
40
|
+
if options[:crud_only_controllers]
|
|
41
|
+
payload[:crud_only_controllers] = crud_only_controllers(rows).map do |r|
|
|
42
|
+
{ file: r[:file], actions: r[:crud_profile][:actions] }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
if options[:non_crud_controllers]
|
|
46
|
+
payload[:non_crud_controllers] = non_crud_controllers(rows).map do |r|
|
|
47
|
+
{ file: r[:file], actions: r[:crud_profile][:actions], extra_actions: r[:crud_profile][:extra_actions] }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
39
50
|
puts JSON.pretty_generate(payload)
|
|
40
51
|
end
|
|
41
52
|
|
|
@@ -88,6 +99,48 @@ module Linecounter
|
|
|
88
99
|
lines.each { |line| puts line }
|
|
89
100
|
end
|
|
90
101
|
end
|
|
102
|
+
|
|
103
|
+
if options[:crud_only_controllers]
|
|
104
|
+
controllers = rows.select { |r| r[:crud_profile] }
|
|
105
|
+
crud_only = crud_only_controllers(rows)
|
|
106
|
+
puts
|
|
107
|
+
puts "CRUD-only controllers:"
|
|
108
|
+
puts "Controllers scanned: #{controllers.size}"
|
|
109
|
+
puts "CRUD-only: #{crud_only.size}"
|
|
110
|
+
puts
|
|
111
|
+
crud_only.each do |r|
|
|
112
|
+
puts " %s %s" % [r[:file], r[:crud_profile][:actions].join(", ")]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if options[:non_crud_controllers]
|
|
117
|
+
controllers = rows.select { |r| r[:crud_profile] }
|
|
118
|
+
non_crud = non_crud_controllers(rows)
|
|
119
|
+
puts
|
|
120
|
+
puts "Non-CRUD controllers:"
|
|
121
|
+
puts "Controllers scanned: #{controllers.size}"
|
|
122
|
+
puts "Non-CRUD: #{non_crud.size}"
|
|
123
|
+
puts
|
|
124
|
+
non_crud.each do |r|
|
|
125
|
+
puts " %s %s" % [r[:file], r[:crud_profile][:extra_actions].join(", ")]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Rows for controllers whose public actions are all standard RESTful
|
|
131
|
+
# actions, sorted by path for stable output.
|
|
132
|
+
def crud_only_controllers(rows)
|
|
133
|
+
rows
|
|
134
|
+
.select { |r| r[:crud_profile]&.fetch(:crud_only) }
|
|
135
|
+
.sort_by { |r| r[:file] }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Rows for controllers that expose at least one custom (non-RESTful) public
|
|
139
|
+
# action, sorted by path for stable output.
|
|
140
|
+
def non_crud_controllers(rows)
|
|
141
|
+
rows
|
|
142
|
+
.select { |r| r[:crud_profile] && !r[:crud_profile][:crud_only] }
|
|
143
|
+
.sort_by { |r| r[:file] }
|
|
91
144
|
end
|
|
92
145
|
|
|
93
146
|
# Total statement LOC for a structure type, summed from its items. The
|
|
@@ -82,13 +82,16 @@ module Linecounter
|
|
|
82
82
|
# macro, and constant is attributed accurately at the structure level only
|
|
83
83
|
# (declarations inside method bodies are not miscounted).
|
|
84
84
|
class StructureVisitor < Prism::Visitor
|
|
85
|
-
attr_reader :type_counts, :item_counts, :item_loc_sums
|
|
85
|
+
attr_reader :type_counts, :item_counts, :item_loc_sums,
|
|
86
|
+
:public_instance_method_names, :class_names
|
|
86
87
|
|
|
87
88
|
def initialize
|
|
88
89
|
super
|
|
89
90
|
@type_counts = Hash.new(0)
|
|
90
91
|
@item_counts = Hash.new(0)
|
|
91
92
|
@item_loc_sums = Hash.new(0)
|
|
93
|
+
@public_instance_method_names = []
|
|
94
|
+
@class_names = []
|
|
92
95
|
@visibility = [:public]
|
|
93
96
|
@method_depth = 0
|
|
94
97
|
@singleton_depth = 0
|
|
@@ -96,6 +99,7 @@ module Linecounter
|
|
|
96
99
|
end
|
|
97
100
|
|
|
98
101
|
def visit_class_node(node)
|
|
102
|
+
@class_names << node.constant_path.slice
|
|
99
103
|
with_scope { super }
|
|
100
104
|
end
|
|
101
105
|
|
|
@@ -177,7 +181,9 @@ module Linecounter
|
|
|
177
181
|
elsif node.name == :initialize
|
|
178
182
|
record(:initializer_def, node)
|
|
179
183
|
else
|
|
180
|
-
|
|
184
|
+
key = VISIBILITY_DEF_KEYS.fetch(current_visibility)
|
|
185
|
+
@public_instance_method_names << node.name if key == :public_instance_method_def
|
|
186
|
+
record(key, node)
|
|
181
187
|
end
|
|
182
188
|
end
|
|
183
189
|
|
|
@@ -212,6 +218,9 @@ module Linecounter
|
|
|
212
218
|
end
|
|
213
219
|
end
|
|
214
220
|
|
|
221
|
+
# The seven standard Rails RESTful actions.
|
|
222
|
+
CRUD_ACTIONS = %i[index show new create edit update destroy].freeze
|
|
223
|
+
|
|
215
224
|
module_function
|
|
216
225
|
|
|
217
226
|
def counts(content)
|
|
@@ -219,5 +228,25 @@ module Linecounter
|
|
|
219
228
|
Prism.parse(content).value.accept(visitor)
|
|
220
229
|
[visitor.type_counts, visitor.item_counts, visitor.item_loc_sums]
|
|
221
230
|
end
|
|
231
|
+
|
|
232
|
+
# Classifies a Ruby source as a Rails controller by CRUD coverage. Returns
|
|
233
|
+
# nil unless the source defines a class named *Controller with at least one
|
|
234
|
+
# public action; otherwise a hash describing how its public actions relate
|
|
235
|
+
# to the standard RESTful set.
|
|
236
|
+
def crud_profile(content)
|
|
237
|
+
visitor = StructureVisitor.new
|
|
238
|
+
Prism.parse(content).value.accept(visitor)
|
|
239
|
+
return nil unless visitor.class_names.any? { |name| name.end_with?("Controller") }
|
|
240
|
+
|
|
241
|
+
actions = visitor.public_instance_method_names
|
|
242
|
+
return nil if actions.empty?
|
|
243
|
+
|
|
244
|
+
extra = actions - CRUD_ACTIONS
|
|
245
|
+
{
|
|
246
|
+
actions: actions & CRUD_ACTIONS,
|
|
247
|
+
extra_actions: extra,
|
|
248
|
+
crud_only: extra.empty?
|
|
249
|
+
}
|
|
250
|
+
end
|
|
222
251
|
end
|
|
223
252
|
end
|
data/lib/linecounter/version.rb
CHANGED