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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9826e951ee479ac0be77163811c7bcd4e6b37489ce40c2ab4090348760e51c1b
4
- data.tar.gz: 3a0ffeab5b6d22b4c768e8bbfd69387b161df1774ae6b4e4bb59a1c62bf3cfc1
3
+ metadata.gz: 98e5cacd90c7df107053970b93d250d554f6220d2929569271f391205ed731c7
4
+ data.tar.gz: d2693bf65a29b6af2e8e8c374013f611a4724eed9af679bb1c1a33af8bd8cff5
5
5
  SHA512:
6
- metadata.gz: d8ef5a38346f6def7c34c43bd6d32fbb6c79ea89da0c793323bfe9084917e2f9c370fb3ca3a86252ef72fde8cbaadf48776ec86f82ebc2ac04965615c9627232
7
- data.tar.gz: dc631e326efb57ad2bc33ef730c2593558c46af66f2371c3a5d11d6e7a54f36c7feb044a1331c779bff47ed78bcfe643ff468309f8bbcc6e009ab93ae323931a
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.2...HEAD
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
  ```
@@ -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
@@ -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
@@ -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
- record(VISIBILITY_DEF_KEYS.fetch(current_visibility), node)
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
@@ -1,3 +1,3 @@
1
1
  module Linecounter
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linecounter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Hopman