Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: [3.1, 3.2, 3.3, 3.4]
ruby: [3.3, 3.4, '4.0']
name: Ruby ${{ matrix.ruby }}
steps:
- uses: actions/checkout@v4
Expand Down
14 changes: 3 additions & 11 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
AllCops:
NewCops: disable
SuggestExtensions: false
Exclude:
- spec/fixtures/**/*.rb

Expand All @@ -20,18 +22,11 @@ Metrics/BlockLength:
Exclude:
- spec/**/*.rb

Layout/HeredocIndentation:
Exclude:
- spec/code_keeper/formatter_spec.rb
- lib/code_keeper/formatter.rb

Naming/HeredocDelimiterNaming:
Enabled: false

Metrics/MethodLength:
Max: 20
Exclude:
- lib/code_keeper/formatter.rb # because uses here doc
Max: 40

# Because TargetFileNotFoundError needs to be passed an argument to initializer.
Style/RaiseArgs:
Expand All @@ -56,6 +51,3 @@ Metrics/PerceivedComplexity:
Exclude:
# It's hard to control.
- lib/code_keeper/metrics/class_length.rb

Metrics/MethodLength:
Max: 40
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,3 @@ gemspec
gem "rake", "~> 13.0"

gem "rspec", "~> 3.0"

gem "csv" if RUBY_VERSION >= '3.4'
42 changes: 21 additions & 21 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
PATH
remote: .
specs:
code_keeper (0.6.1)
parallel (>= 1.20.1, < 2)
rubocop (>= 1.13.0)
rubocop-ast (>= 1.4.1)
code_keeper (1.0.0)
rubocop (>= 1.88.0)

GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
csv (3.3.2)
ast (2.4.3)
diff-lcs (1.5.1)
json (2.9.0)
language_server-protocol (3.17.0.3)
parallel (1.26.3)
parser (3.3.6.0)
json (2.20.0)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
parallel (1.28.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
prism (1.9.0)
racc (1.8.1)
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.9.3)
regexp_parser (2.12.0)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
Expand All @@ -35,29 +34,30 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.2)
rubocop (1.69.2)
rubocop (1.88.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (>= 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
rubocop-ast (>= 1.49.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.36.2)
parser (>= 3.3.1.0)
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
ruby-progressbar (1.13.0)
unicode-display_width (3.1.2)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.2.0)

PLATFORMS
x86_64-linux

DEPENDENCIES
code_keeper!
csv
rake (~> 13.0)
rspec (~> 3.0)

Expand Down
90 changes: 82 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
# CodeKeeper
The CodeKeeper measures metrics especially about complexity and size of Ruby files, aiming to be a Ruby version of [gmetrics](https://github.com/dx42/gmetrics)
CodeKeeper emits Ruby code metric reports for periodic code quality reviews.

Mesuring metrics leads to keep codebase simple and clean, and I name the gem CodeKeeper.
It is not a RuboCop replacement. RuboCop is excellent at reporting style and metric offenses, but offense reporting is intentionally configurable: projects can disable cops, exclude files, allow specific methods, or relax thresholds. CodeKeeper is built for a different job. It keeps measuring metric values even when RuboCop offense reporting has been silenced or loosened.

Now CodeKeeper supports the cyclomatic complexity of a file, the ABC software metric of a file, and class length. The scores are output to stdout of a json or csv format.
The intended use is recurring code quality review. Run CodeKeeper, hand the structured metric report to a human or AI agent, and use the metric values as review signals.

## Design

CodeKeeper uses RuboCop as a Ruby analysis foundation, not as an offense policy engine.

- Ruby parsing is based on `RuboCop::AST::ProcessedSource`.
- Standard metric values are calculated through RuboCop metric calculators or RuboCop metric cop behavior behind CodeKeeper adapters.
- RuboCop project configuration is not applied to measurement.

RuboCop's output is threshold-gated offense reporting, not a metric inventory. Values below configured limits are not emitted as metric facts. Even aggressive RuboCop metric settings still remain policy-filtered by inline disable comments, excludes, disabled cops, allowed methods, and relaxed thresholds.

CodeKeeper does not read `.rubocop.yml`, `rubocop:disable`, `Max`, `AllowedMethods`, `AllowedPatterns`, or `Exclude`. Those settings control RuboCop offense reporting, but CodeKeeper's purpose is to keep the underlying measurements visible.

Each metric is measured at its natural scope:

- `abc_metric`: method, singleton method, and `define_method` block.
- `cyclomatic_complexity`: method, singleton method, and `define_method` block.
- `class_length`: class, module, singleton class, and class-like constant assignment.

File paths are included as location data, but files are not the primary measurement scope. If you need file-level grouping, build it from the `measurements` array or ask an AI agent to group hotspots by file, directory, domain, or owner.

## Installation

Expand All @@ -22,14 +42,46 @@ Or install it yourself as:
$ gem install code_keeper

## Usage
Run CodeKeeper and you get scores of metrics from stdout like
Run CodeKeeper and you get a metric report from stdout.

```rb
$ bundle exec code_keeper app/models/user.rb app/models/admin.rb > metrics.json
$ cat metrics.json
{"cyclomatic_complexity":{"app/models/admin.rb":9,"app/models/user.rb":23},"class_length":{"Admin":86,"User":1475},"abc_metric":{"app/models/admin.rb":76.909,"app/models/user.rb":1546.4155}}
{
"summary": {
"metrics": {
"abc_metric": {
"count": 2,
"max": 18.2,
"top_hotspots": [
{
"metric": "abc_metric",
"scope_type": "method",
"scope_name": "User#save!",
"path": "app/models/user.rb",
"start_line": 42,
"end_line": 80,
"value": 18.2
}
]
}
}
},
"measurements": [
{
"metric": "abc_metric",
"scope_type": "method",
"scope_name": "User#save!",
"path": "app/models/user.rb",
"start_line": 42,
"end_line": 80,
"value": 18.2
}
]
}
```
If you need a csv format, change the configuration as explained later.

The `summary` section is intended for quick review. `top_hotspots` contains up to five measurements per metric, ordered by descending value. The `measurements` section contains the metric-native values that support deeper analysis. If you need a tabular view such as CSV, derive it from the `measurements` array.

### Run CodeKeeper
To measure metrics of all the ruby files recursively in the current directory, run
Expand All @@ -51,11 +103,33 @@ CodeKeeper.configure do |config|
config.metrics = %i(cyclomatic_complexity abc_metric class_length)
# The number of threads. The default is 2. Executed sequentially if you set 1.
config.number_of_threads = 4
# The default is json
config.format = :json
end
```

## Using CodeKeeper with AI review workflows

CodeKeeper is designed to provide structured metric data for recurring AI-assisted reviews. A typical workflow is:

1. Run CodeKeeper on the target codebase.
2. Save the JSON metric report.
3. Ask an AI agent to analyze hotspots, group measurements by file, directory, domain, or owner, and suggest focused refactoring candidates.
4. Use the metrics as review signals, not as automatic pass/fail thresholds.

Example prompt:

```text
Analyze this CodeKeeper JSON metric report.

Focus on:
- the highest ABC and cyclomatic complexity measurements
- classes or modules with unusually large length
- files or domains that contain multiple hotspots
- refactoring candidates that reduce risk without broad rewrites

Do not treat these metrics as hard pass/fail thresholds.
Use them as signals for code quality review.
```

## Development

After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
Expand Down
6 changes: 2 additions & 4 deletions code_keeper.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
spec.description = "The CodeKeeper measures metrics especially about complexity and size of Ruby files, aiming to be a Ruby version of gmetrics."
spec.homepage = "https://github.com/ebihara99999/code_keeper"
spec.license = "MIT"
spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")

spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/ebihara99999/code_keeper/"
Expand All @@ -28,9 +28,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

# Uncomment to register a new dependency of your gem
spec.add_dependency "parallel", '>= 1.20.1', '< 2'
spec.add_dependency "rubocop", '>= 1.13.0'
spec.add_dependency "rubocop-ast", '>= 1.4.1'
spec.add_dependency "rubocop", '>= 1.88.0'

# For more information and examples about making a new gem, checkout our
# guide at: https://bundler.io/guides/creating_gem.html
Expand Down
7 changes: 5 additions & 2 deletions lib/code_keeper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

require_relative "code_keeper/version"
require "rubocop"
require 'code_keeper/parser'
require 'code_keeper/source_file'
require 'code_keeper/measurement'
require 'code_keeper/metric_report'
require 'code_keeper/finder'
require 'code_keeper/cli'
require 'code_keeper/formatter'
require 'code_keeper/config'
require 'code_keeper/scorer'
require 'code_keeper/result'
require 'code_keeper/metrics'
require 'code_keeper/metrics/scope_name'
require 'code_keeper/metrics/rubocop_metric_calculator'
require 'code_keeper/metrics/abc_metric'
require 'code_keeper/metrics/cyclomatic_complexity'
require 'code_keeper/metrics/class_length'
Expand Down
4 changes: 2 additions & 2 deletions lib/code_keeper/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ def self.run(paths)
return ERROR_CODE
end

result = CodeKeeper::Scorer.keep(paths)
metric_report = CodeKeeper::Scorer.keep(paths)

puts ::CodeKeeper::Formatter.format(result)
puts ::CodeKeeper::Formatter.format(metric_report)
SUCCESS_CODE
rescue Interrupt
puts 'Exiting...'
Expand Down
3 changes: 1 addition & 2 deletions lib/code_keeper/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
module CodeKeeper
# Provide configuration
class Config
attr_accessor :metrics, :number_of_threads, :format
attr_accessor :metrics, :number_of_threads

def initialize
@metrics = %i[cyclomatic_complexity class_length abc_metric]
@number_of_threads = 2
@format = :json # json and csv are supported.
end
end
end
20 changes: 4 additions & 16 deletions lib/code_keeper/formatter.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
# frozen_string_literal: true

require 'csv'
require 'json'

module CodeKeeper
# Format a result and make it human-readable.
# Formats a metric report for output.
class Formatter
class << self
def format(result)
return result.scores.to_json if CodeKeeper.config.format == :json

# csv is supported besides json
csv_array = []
result.scores.each_key do |metric|
result.scores[metric].each { |k, v| csv_array << [metric, k, v] }
end

headers = %w[metric file score]
CSV.generate(headers: true) do |csv|
csv << headers
csv_array.each { |array| csv << array }
end
def format(metric_report)
metric_report.to_h.to_json
end
end
end
Expand Down
30 changes: 30 additions & 0 deletions lib/code_keeper/measurement.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module CodeKeeper
# A metric value measured at the metric's natural source scope.
class Measurement
attr_reader :metric, :scope_type, :scope_name, :path, :start_line, :end_line, :value

def initialize(attributes)
@metric = attributes.fetch(:metric).to_sym
@scope_type = attributes.fetch(:scope_type).to_sym
@scope_name = attributes.fetch(:scope_name).to_s
@path = attributes.fetch(:path)
@start_line = attributes.fetch(:start_line)
@end_line = attributes.fetch(:end_line)
@value = attributes.fetch(:value)
end

def to_h
{
metric:,
scope_type:,
scope_name:,
path:,
start_line:,
end_line:,
value:
}
end
end
end
Loading
Loading