churn_vs_complexity 1.5.2 → 1.6.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/CLAUDE.md +60 -0
  4. data/README.md +60 -14
  5. data/lib/churn_vs_complexity/churn.rb +1 -1
  6. data/lib/churn_vs_complexity/cli/main.rb +8 -0
  7. data/lib/churn_vs_complexity/cli/parser.rb +68 -11
  8. data/lib/churn_vs_complexity/cli.rb +3 -1
  9. data/lib/churn_vs_complexity/complexity/go_calculator.rb +48 -0
  10. data/lib/churn_vs_complexity/complexity/python_calculator.rb +50 -0
  11. data/lib/churn_vs_complexity/complexity.rb +2 -0
  12. data/lib/churn_vs_complexity/complexity_validator.rb +4 -0
  13. data/lib/churn_vs_complexity/delta/config.rb +2 -2
  14. data/lib/churn_vs_complexity/delta.rb +8 -0
  15. data/lib/churn_vs_complexity/diff/checker.rb +46 -0
  16. data/lib/churn_vs_complexity/diff/config.rb +53 -0
  17. data/lib/churn_vs_complexity/diff/serializer.rb +78 -0
  18. data/lib/churn_vs_complexity/diff.rb +10 -0
  19. data/lib/churn_vs_complexity/file_selector.rb +26 -0
  20. data/lib/churn_vs_complexity/focus/checker.rb +59 -0
  21. data/lib/churn_vs_complexity/focus/config.rb +66 -0
  22. data/lib/churn_vs_complexity/focus/serializer.rb +98 -0
  23. data/lib/churn_vs_complexity/focus.rb +10 -0
  24. data/lib/churn_vs_complexity/gamma_score.rb +13 -0
  25. data/lib/churn_vs_complexity/gate/checker.rb +36 -0
  26. data/lib/churn_vs_complexity/gate/config.rb +53 -0
  27. data/lib/churn_vs_complexity/gate/serializer.rb +35 -0
  28. data/lib/churn_vs_complexity/gate.rb +10 -0
  29. data/lib/churn_vs_complexity/git_strategy.rb +13 -5
  30. data/lib/churn_vs_complexity/hotspots/checker.rb +17 -0
  31. data/lib/churn_vs_complexity/hotspots/config.rb +50 -0
  32. data/lib/churn_vs_complexity/hotspots/serializer.rb +49 -0
  33. data/lib/churn_vs_complexity/hotspots.rb +10 -0
  34. data/lib/churn_vs_complexity/language_validator.rb +3 -1
  35. data/lib/churn_vs_complexity/normal/config.rb +18 -0
  36. data/lib/churn_vs_complexity/normal/serializer/json.rb +36 -0
  37. data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +25 -32
  38. data/lib/churn_vs_complexity/normal/serializer.rb +1 -0
  39. data/lib/churn_vs_complexity/normal.rb +1 -1
  40. data/lib/churn_vs_complexity/risk_annotator.rb +26 -0
  41. data/lib/churn_vs_complexity/risk_classifier.rb +35 -0
  42. data/lib/churn_vs_complexity/triage/checker.rb +64 -0
  43. data/lib/churn_vs_complexity/triage/config.rb +52 -0
  44. data/lib/churn_vs_complexity/triage/serializer.rb +16 -0
  45. data/lib/churn_vs_complexity/triage.rb +10 -0
  46. data/lib/churn_vs_complexity/version.rb +1 -1
  47. data/lib/churn_vs_complexity.rb +8 -0
  48. data/tmp/test-support/delta/ruby-summary.txt +10 -5
  49. data/tmp/test-support/delta/ruby.csv +5 -5
  50. data/tmp/test-support/go/main.go +11 -0
  51. data/tmp/test-support/go/utils.go +13 -0
  52. data/tmp/test-support/python/example.py +6 -0
  53. data/tmp/test-support/python/utils.py +9 -0
  54. metadata +41 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 843cae8dad7f09344cbea477eb73ae3d9d57945df8a5955d93d6dfc820b5a8e1
4
- data.tar.gz: 5066b98af73acde0f18b233fd12efe6858bb9977c4a9d553970c6ad859db98a9
3
+ metadata.gz: 6dfa844a5bcd2fe116001652410d39f802c64623be5ec588afcc6ed331b9aac9
4
+ data.tar.gz: 42c5d56c7b4c6640bd4040f7f5d8b35ea6acac0aae9285be4c6cb1aaf860f0fb
5
5
  SHA512:
6
- metadata.gz: 7af6d2c460fd9bb2cb78751d8a7f5cdd3ada48e9026e61ccbd3ef74a22c738bcfea4185984ffea2476a5d4b726f902c881edabf2baefa211884bfb129b59f80c
7
- data.tar.gz: c3cf5e4dfd7bea34298fac9d7e2db1b7ffe58ad16acab25e9217dae0f84b43e2da7d89f147027f1ebb3fd52115538aba81a1f05bca5a0f7e13f0ad5d3a7b9aef
6
+ metadata.gz: 0ad833af0897a784aa0499d277884e011e9be71142d343c441ef0a12b1185c27f18c09b0b7780a3f8493e3ef0399faff5f97b252f51fb4713f3dd98d11b666a7
7
+ data.tar.gz: 19cf09a9777aa06a4772fae21fee5a4043ba034efde9d2af13618461b20a602270879438c4aa626463c3ea14054bf1dc8a7b9a25cde09c9636439d9d567a30f3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## [1.6.1] - 2026-02-19
2
+
3
+ ### Changed
4
+ - Updated gemspec summary and description to reflect all supported languages and modes
5
+ - Updated README with current usage, all languages (Ruby, JS/TS, Java, Python, Go), all modes, and new examples
6
+ - Added external dependency documentation for Python (radon) and Go (gocyclo)
7
+
8
+ ## [1.6.0] - 2026-02-19
9
+
10
+ ### Added
11
+ - Python support for complexity calculation
12
+ - Go support for complexity calculation (via gocognit)
13
+ - New `--triage` mode: per-file risk assessment based on churn and complexity
14
+ - New `--hotspots` mode: ranked list of files by risk score
15
+ - New `--gate` mode: pass/fail quality gate with `--max-gamma` threshold (exit 0/1)
16
+ - New `--focus start|end` mode: capture complexity snapshots before and after coding sessions
17
+ - New `--diff REF` mode: compare codebase health between a reference commit and HEAD
18
+ - New `--json` output format for all modes
19
+ - New `--markdown` output format
20
+ - Risk classifier and risk annotator modules
21
+ - Gamma score module
22
+
23
+ ### Fixed
24
+ - Improved CLI help text with grouped sections (Languages, Modes, Output formats, Modifiers)
25
+
1
26
  ## [1.5.2] - 2024-10-21
2
27
 
3
28
  - Fixed bug where delta mode validations would fail when the commit was a non-sha value.
data/CLAUDE.md ADDED
@@ -0,0 +1,60 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `churn_vs_complexity` is a Ruby gem that analyzes code quality by correlating file churn (how often files change) with complexity scores. It supports Ruby (via Flog), JavaScript/TypeScript (via ESLint), and Java (via PMD). Requires Ruby >= 3.3.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ # Run tests (TLDR framework)
13
+ bundle exec rake # default task runs tldr
14
+ bundle exec tldr # direct
15
+
16
+ # Run a single test file
17
+ bundle exec tldr test/churn_vs_complexity/engine_test.rb
18
+
19
+ # Run a single test by name
20
+ bundle exec tldr --name test_something test/path/to_test.rb
21
+
22
+ # Lint
23
+ bundle exec rubocop
24
+ bundle exec rubocop -a # auto-fix safe offenses
25
+
26
+ # Build/install gem
27
+ bundle exec rake build
28
+ bundle exec rake install
29
+ ```
30
+
31
+ ## Architecture
32
+
33
+ Three operating modes, each with its own Config/Checker/Serializer pipeline:
34
+
35
+ - **Normal** (`Normal::Config` → `Engine` → `ConcurrentCalculator`): Analyzes a folder, computing churn + complexity per file. Outputs CSV, HTML graph, or text summary.
36
+ - **Timetravel** (`Timetravel::Config` → `Traveller`): Samples quality scores across historical commits at N-day intervals. Uses forked processes with git worktrees for isolation.
37
+ - **Delta** (`Delta::Config` → `MultiChecker` → `Checker`): Analyzes complexity of files changed in specific commits. Uses threads with git worktrees.
38
+
39
+ Entry point: `CLI::run!` → `CLI::Parser` → `CLI::Main.run!` → mode-specific Config → checker.
40
+
41
+ ### Key interfaces
42
+
43
+ - **Complexity calculators**: `folder_based?`, `calculate(folder:)` or `calculate(files:)`
44
+ - **Churn calculators**: `calculate(folder:, file:, since:)`, `date_of_latest_commit(folder:)`
45
+ - **Serializers**: `serialize(result)`
46
+ - **File selectors**: `select_files(folder)` → `{ included: [...], explicitly_excluded: [...] }`
47
+
48
+ ### Concurrency model
49
+
50
+ - Normal: threads (nprocessors) for per-file churn calculation
51
+ - Timetravel: forked child processes with IO.pipe for IPC
52
+ - Delta: threads (2x nprocessors) for concurrent commit analysis
53
+
54
+ ## Code Style
55
+
56
+ RuboCop config (`.rubocop.yml`):
57
+ - Max line length: 120
58
+ - Trailing commas enforced on multiline (arrays, hashes, arguments)
59
+ - `NewCops: enable`
60
+ - No class/module documentation required (`Style::Documentation` disabled)
data/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  # ChurnVsComplexity
4
4
 
5
- A tool to visualise code complexity in a project and help direct refactoring efforts.
5
+ Correlates file churn (how often files change) with complexity scores to identify refactoring hotspots and track codebase health over time. Supports Ruby, JavaScript/TypeScript, Java, Python, and Go.
6
6
 
7
- Inspired by [Michael Feathers' article "Getting Empirical about Refactoring"](https://www.agileconnection.com/article/getting-empirical-about-refactoring) and the gem [turbulence](https://rubygems.org/gems/turbulence) by Chad Fowler and others.
7
+ Modes include hotspots ranking, triage assessment, CI quality gate, diff comparison, focus sessions, and timetravel history.
8
8
 
9
- This gem currently supports analysis of Java, Ruby, JavaScript, and TypeScript repositories, but it can easily be extended.
9
+ Inspired by [Michael Feathers' article "Getting Empirical about Refactoring"](https://www.agileconnection.com/article/getting-empirical-about-refactoring).
10
10
 
11
11
  ## Installation
12
12
 
@@ -26,30 +26,52 @@ Or install it yourself as:
26
26
 
27
27
  This gem depends on git for churn analysis.
28
28
 
29
- Complexity analysis for Java relies on [PMD](https://pmd.github.io). In order to use the `--java` flag, you must first install PMD manually, and the gem assumes it is available on the search path as `pmd`. On macOS, for example, you can install it using homebrew with `brew install pmd`.
29
+ External tool dependencies per language:
30
30
 
31
- Complexity analysis for JavaScript and TypeScript relies on [ESLint](https://eslint.org). In order to use the `--js`, `--ts`, `--javascript`, or `--typescript` flag, you must have Node.js installed.
31
+ - **Ruby**: None (uses [Flog](https://rubygems.org/gems/flog), bundled as a gem dependency).
32
+ - **Java**: Requires [PMD](https://pmd.github.io) on the search path as `pmd`. On macOS: `brew install pmd`.
33
+ - **JavaScript/TypeScript**: Requires [Node.js](https://nodejs.org) (uses ESLint internally).
34
+ - **Python**: Requires [Radon](https://radon.readthedocs.io) on the search path as `radon`. Install with `pip install radon`.
35
+ - **Go**: Requires [gocyclo](https://github.com/fzipp/gocyclo) on the search path. Install with `go install github.com/fzipp/gocyclo/cmd/gocyclo@latest`.
32
36
 
33
37
  ## Usage
34
38
 
35
- Execute the `churn_vs_complexity` with the applicable arguments. Output in the requested format will be directed to stdout.
39
+ Execute `churn_vs_complexity` with the applicable arguments. Output in the requested format will be directed to stdout.
36
40
 
37
41
  ```
38
- Usage: churn_vs_complexity [options] folder
42
+ Usage: churn_vs_complexity [options] folder|file...
43
+
44
+ Languages:
39
45
  --java Check complexity of java classes
40
46
  --ruby Check complexity of ruby files
41
47
  --js, --ts, --javascript, --typescript
42
48
  Check complexity of javascript and typescript files
49
+ --python Check complexity of python files
50
+ --go Check complexity of go files
51
+
52
+ Modes (mutually exclusive):
53
+ --timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
54
+ --triage Assess risk of files based on churn and complexity. Accepts file paths or a folder as arguments.
55
+ --hotspots Generate ranked list of files by risk
56
+ --gate Pass/fail quality check against gamma threshold (exits 0 on pass, 1 on fail)
57
+ --focus start|end Capture complexity snapshot before (start) and after (end) a coding session
58
+ --diff REF Compare codebase health between REF and HEAD
59
+ --delta SHA Identify changes between the specified commit (SHA) and the previous commit and annotate changed files with complexity score. SHA can be a full or short commit hash, or the value HEAD. Can be used multiple times to specify multiple commits.
60
+
61
+ Output formats:
43
62
  --csv Format output as CSV
44
63
  --graph Format output as HTML page with Churn vs Complexity graph
45
64
  --summary Output summary statistics (mean and median) for churn and complexity
65
+ --json Format output as JSON
66
+ --markdown Format output as Markdown
67
+
68
+ Modifiers:
46
69
  --excluded PATTERN Exclude file paths including this string. Can be used multiple times.
47
70
  --since YYYY-MM-DD Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date
48
71
  -m, --month Calculate churn for the month leading up to the most recent commit
49
72
  -q, --quarter Calculate churn for the quarter leading up to the most recent commit
50
73
  -y, --year Calculate churn for the year leading up to the most recent commit
51
- --timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
52
- --delta SHA Identify changes between the specified commit (SHA) and the previous commit and annotate changed files with complexity score. SHA can be a full or short commit hash, or the value HEAD. Can be used multiple times to specify multiple commits.
74
+ --max-gamma N Maximum gamma score threshold for gate mode (default: 25)
53
75
  --dry-run Echo the chosen options from the CLI
54
76
  -h, --help Display help
55
77
  --version Display version
@@ -67,15 +89,39 @@ Summaries in normal mode include a gamma score, which is an unnormalised harmoni
67
89
  Summary points in timetravel mode instead include an alpha score, which is the same harmonic mean of churn and complexity, where churn and complexity values are normalised to a 0-1 range to avoid either churn or complexity dominating the score. The summary points also include a beta score, which is the geometric mean of the normalised churn and complexity values.
68
90
  ## Examples
69
91
 
70
- `churn_vs_complexity --ruby --csv my_ruby_project > ~/Desktop/ruby-demo.csv`
92
+ ```bash
93
+ # CSV churn vs complexity report for a Ruby project
94
+ churn_vs_complexity --ruby --csv my_ruby_project > ~/Desktop/ruby-demo.csv
95
+
96
+ # Interactive HTML graph for a Java project (excluding generated code)
97
+ churn_vs_complexity --java --graph --excluded generated-sources --since 2023-01-01 my_java_project > ~/Desktop/java-demo.html
98
+
99
+ # Monthly summary for a Python project
100
+ churn_vs_complexity --python --summary -m my_python_project
101
+
102
+ # Top refactoring hotspots ranked by risk
103
+ churn_vs_complexity --ruby --hotspots -q my_ruby_project
71
104
 
72
- `churn_vs_complexity --java --graph --exclude generated-sources --exclude generated-test-sources --since 2023-01-01 my_java_project > ~/Desktop/java-demo.html`
105
+ # CI quality gate (exits 1 if gamma exceeds threshold)
106
+ churn_vs_complexity --ruby --gate --max-gamma 30 my_ruby_project
73
107
 
74
- `churn_vs_complexity --ruby --summary -m my_ruby_project >> ~/Desktop/monthly-report.txt`
108
+ # Triage specific files before a code review
109
+ churn_vs_complexity --go --triage src/server.go src/handler.go
75
110
 
76
- `churn_vs_complexity --java -m --since 2019-03-01 --timetravel 30 --graph my_java_project > ~/Desktop/timetravel-after-1st-march-2019.html`
111
+ # Compare codebase health between a branch and HEAD
112
+ churn_vs_complexity --ruby --diff origin/main --summary my_ruby_project
77
113
 
78
- `churn_vs_complexity --delta 1496402e81e68e86c5ac240559099fbe581a9a2g --delta 2845296758861773778d70d96328a5f2a1a9e933 --js --summary my_javascript_project > ~/Desktop/interesting-commits.txt`
114
+ # Focus session: snapshot before and after a coding session
115
+ churn_vs_complexity --ruby --focus start my_ruby_project
116
+ # ... do some coding ...
117
+ churn_vs_complexity --ruby --focus end my_ruby_project
118
+
119
+ # Timetravel: track quality over time at 30-day intervals
120
+ churn_vs_complexity --java -m --since 2019-03-01 --timetravel 30 --graph my_java_project > ~/Desktop/timetravel.html
121
+
122
+ # Analyse complexity of specific commits
123
+ churn_vs_complexity --js --delta HEAD --summary my_js_project
124
+ ```
79
125
 
80
126
  ## Development
81
127
 
@@ -17,7 +17,7 @@ module ChurnVsComplexity
17
17
  def calculate(folder:, file:, since:)
18
18
  git_dir = File.join(folder, '.git')
19
19
  earliest_date = [date_of_first_commit(folder:), since].max
20
- formatted_date = earliest_date.strftime('%Y-%m-%d')
20
+ formatted_date = earliest_date.strftime('%Y-%m-%dT00:00:00')
21
21
  cmd = %(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
22
22
  `(#{cmd}) 2>/dev/null`.to_i
23
23
  end
@@ -26,16 +26,24 @@ module ChurnVsComplexity
26
26
  raise ValidationError, 'No options selected. Use --help for usage information.' if options.empty?
27
27
  raise ValidationError, 'No language selected. Use --help for usage information.' if options[:language].nil?
28
28
 
29
+ return if MODES_WITHOUT_SERIALIZER.include?(options[:mode])
29
30
  return unless options[:serializer].nil?
30
31
 
31
32
  raise ValidationError, 'No serializer selected. Use --help for usage information.'
32
33
  end
33
34
 
35
+ MODES_WITHOUT_SERIALIZER = %i[triage hotspots gate focus diff].freeze
36
+
34
37
  def config(options)
35
38
  config_class =
36
39
  case options[:mode]
37
40
  when :timetravel then Timetravel::Config
38
41
  when :delta then Delta::Config
42
+ when :triage then Triage::Config
43
+ when :hotspots then Hotspots::Config
44
+ when :gate then Gate::Config
45
+ when :focus then Focus::Config
46
+ when :diff then Diff::Config
39
47
  else Normal::Config
40
48
  end
41
49
  config_class.new(**options)
@@ -8,7 +8,10 @@ module ChurnVsComplexity
8
8
  def self.create
9
9
  options = { excluded: [] }
10
10
  parser = OptionParser.new do |opts|
11
- opts.banner = 'Usage: churn_vs_complexity [options] folder'
11
+ opts.banner = 'Usage: churn_vs_complexity [options] folder|file...'
12
+
13
+ opts.separator ''
14
+ opts.separator 'Languages:'
12
15
 
13
16
  opts.on('--java', 'Check complexity of java classes') do
14
17
  options[:language] = :java
@@ -23,6 +26,57 @@ module ChurnVsComplexity
23
26
  options[:language] = :javascript
24
27
  end
25
28
 
29
+ opts.on('--python', 'Check complexity of python files') do
30
+ options[:language] = :python
31
+ end
32
+
33
+ opts.on('--go', 'Check complexity of go files') do
34
+ options[:language] = :go
35
+ end
36
+
37
+ opts.separator ''
38
+ opts.separator 'Modes (mutually exclusive):'
39
+
40
+ opts.on('--timetravel N',
41
+ 'Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since',) do |value|
42
+ options[:mode] = :timetravel
43
+ options[:jump_days] = value.to_i
44
+ end
45
+
46
+ opts.on('--triage',
47
+ 'Assess risk of files based on churn and complexity. Accepts file paths or a folder as arguments.',) do
48
+ options[:mode] = :triage
49
+ end
50
+
51
+ opts.on('--hotspots', 'Generate ranked list of files by risk') do
52
+ options[:mode] = :hotspots
53
+ end
54
+
55
+ opts.on('--gate',
56
+ 'Pass/fail quality check against gamma threshold (exits 0 on pass, 1 on fail)',) do
57
+ options[:mode] = :gate
58
+ end
59
+
60
+ opts.on('--focus start|end',
61
+ 'Capture complexity snapshot before (start) and after (end) a coding session',) do |value|
62
+ options[:mode] = :focus
63
+ options[:subcommand] = value.to_sym
64
+ end
65
+
66
+ opts.on('--diff REF', 'Compare codebase health between REF and HEAD') do |value|
67
+ options[:mode] = :diff
68
+ options[:reference] = value
69
+ end
70
+
71
+ opts.on('--delta SHA',
72
+ 'Identify changes between the specified commit (SHA) and the previous commit and annotate changed files with complexity score. SHA can be a full or short commit hash, or the value HEAD. Can be used multiple times to specify multiple commits.',) do |value|
73
+ options[:mode] = :delta
74
+ (options[:commits] ||= []) << value
75
+ end
76
+
77
+ opts.separator ''
78
+ opts.separator 'Output formats:'
79
+
26
80
  opts.on('--csv', 'Format output as CSV') do
27
81
  options[:serializer] = :csv
28
82
  end
@@ -35,6 +89,17 @@ module ChurnVsComplexity
35
89
  options[:serializer] = :summary
36
90
  end
37
91
 
92
+ opts.on('--json', 'Format output as JSON') do
93
+ options[:serializer] = :json
94
+ end
95
+
96
+ opts.on('--markdown', 'Format output as Markdown') do
97
+ options[:serializer] = :markdown
98
+ end
99
+
100
+ opts.separator ''
101
+ opts.separator 'Modifiers:'
102
+
38
103
  opts.on('--excluded PATTERN',
39
104
  'Exclude file paths including this string. Can be used multiple times.',) do |value|
40
105
  options[:excluded] << value
@@ -57,16 +122,8 @@ module ChurnVsComplexity
57
122
  options[:relative_period] = :year
58
123
  end
59
124
 
60
- opts.on('--timetravel N',
61
- 'Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since',) do |value|
62
- options[:mode] = :timetravel
63
- options[:jump_days] = value.to_i
64
- end
65
-
66
- opts.on('--delta SHA',
67
- 'Identify changes between the specified commit (SHA) and the previous commit and annotate changed files with complexity score. SHA can be a full or short commit hash, or the value HEAD. Can be used multiple times to specify multiple commits.',) do |value|
68
- options[:mode] = :delta
69
- (options[:commits] ||= []) << value
125
+ opts.on('--max-gamma N', Float, 'Maximum gamma score threshold for gate mode (default: 25)') do |value|
126
+ options[:max_gamma] = value
70
127
  end
71
128
 
72
129
  opts.on('--dry-run', 'Echo the chosen options from the CLI') do
@@ -12,7 +12,9 @@ module ChurnVsComplexity
12
12
  # First argument that is not an option is the folder
13
13
  folder = ARGV.first
14
14
 
15
- puts Main.run!(options, folder)
15
+ result = Main.run!(options, folder)
16
+ puts result
17
+ exit 1 if result.respond_to?(:passed?) && !result.passed?
16
18
  end
17
19
  end
18
20
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module ChurnVsComplexity
7
+ module Complexity
8
+ module GoCalculator
9
+ class << self
10
+ attr_writer :command_runner
11
+
12
+ def folder_based? = false
13
+
14
+ def calculate(files:)
15
+ json_output = run_gocognit(files)
16
+ parse_gocognit_output(json_output, files:)
17
+ end
18
+
19
+ def parse_gocognit_output(json_output, files:)
20
+ stats = JSON.parse(json_output) || []
21
+ scores = stats.group_by { |s| s.dig('Pos', 'Filename') }
22
+ .transform_values { |funcs| funcs.sum { |f| f['Complexity'] } }
23
+ files.to_h { |file| [file, scores[file] || 0] }
24
+ end
25
+
26
+ def check_dependencies!
27
+ command_runner.call('gocognit --help 2>&1')
28
+ rescue Errno::ENOENT
29
+ raise Error, 'Needs gocognit installed (go install github.com/uudashr/gocognit/cmd/gocognit@latest)'
30
+ end
31
+
32
+ private
33
+
34
+ def command_runner
35
+ @command_runner || Open3.method(:capture2)
36
+ end
37
+
38
+ def run_gocognit(files)
39
+ files_arg = files.map { |f| "'#{f}'" }.join(' ')
40
+ stdout, status = command_runner.call("gocognit -json #{files_arg}")
41
+ raise Error, "gocognit failed (exit #{status.exitstatus}). Is it installed?" unless status.success?
42
+
43
+ stdout
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module ChurnVsComplexity
7
+ module Complexity
8
+ module PythonCalculator
9
+ class << self
10
+ attr_writer :command_runner
11
+
12
+ def folder_based? = false
13
+
14
+ def calculate(files:)
15
+ json_output = run_radon(files)
16
+ parse_radon_output(json_output, files:)
17
+ end
18
+
19
+ def parse_radon_output(json_output, files:)
20
+ data = JSON.parse(json_output)
21
+ files.to_h do |file|
22
+ blocks = data[file] || []
23
+ total = blocks.sum { |b| b['complexity'] }
24
+ [file, total]
25
+ end
26
+ end
27
+
28
+ def check_dependencies!
29
+ command_runner.call('radon --version 2>&1')
30
+ rescue Errno::ENOENT
31
+ raise Error, 'Needs radon installed (pip install radon)'
32
+ end
33
+
34
+ private
35
+
36
+ def command_runner
37
+ @command_runner || Open3.method(:capture2)
38
+ end
39
+
40
+ def run_radon(files)
41
+ files_arg = files.map { |f| "'#{f}'" }.join(' ')
42
+ stdout, status = command_runner.call("radon cc #{files_arg} -j")
43
+ raise Error, "radon failed (exit #{status.exitstatus}). Is it installed?" unless status.success?
44
+
45
+ stdout
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -3,6 +3,8 @@
3
3
  require_relative 'complexity/pmd'
4
4
  require_relative 'complexity/flog_calculator'
5
5
  require_relative 'complexity/eslint_calculator'
6
+ require_relative 'complexity/python_calculator'
7
+ require_relative 'complexity/go_calculator'
6
8
 
7
9
  module ChurnVsComplexity
8
10
  module Complexity
@@ -8,6 +8,10 @@ module ChurnVsComplexity
8
8
  Complexity::PMD.check_dependencies!
9
9
  when :javascript
10
10
  Complexity::ESLintCalculator.check_dependencies!
11
+ when :python
12
+ Complexity::PythonCalculator.check_dependencies!
13
+ when :go
14
+ Complexity::GoCalculator.check_dependencies!
11
15
  end
12
16
  end
13
17
  end
@@ -38,10 +38,10 @@ module ChurnVsComplexity
38
38
  end
39
39
 
40
40
  def validate_commit!(commit)
41
- return if commit == 'HEAD' || commit.match?(/\A[0-9a-f]{40}\z/i) || commit.match?(/\A[0-9a-f]{8}\z/i)
41
+ return if commit == 'HEAD' || commit.match?(/\A[0-9a-f]{40}\z/i) || commit.match?(/\A[0-9a-f]{7,12}\z/i)
42
42
 
43
43
  raise ValidationError,
44
- "Invalid commit: #{commit}. It must be a valid 40-character SHA-1 hash or an 8-character shortened form."
44
+ "Invalid commit: #{commit}. It must be a valid 40-character SHA-1 hash or a 7-12 character shortened form."
45
45
  end
46
46
 
47
47
  def serializer = Serializer.resolve(@serializer)
@@ -34,6 +34,10 @@ module ChurnVsComplexity
34
34
  FileSelector::Ruby.predefined(included:, excluded:)
35
35
  when :javascript
36
36
  FileSelector::JavaScript.predefined(included:, excluded:)
37
+ when :python
38
+ FileSelector::Python.predefined(included:, excluded:)
39
+ when :go
40
+ FileSelector::Go.predefined(included:, excluded:)
37
41
  end
38
42
  end
39
43
 
@@ -45,6 +49,10 @@ module ChurnVsComplexity
45
49
  Complexity::FlogCalculator
46
50
  when :javascript
47
51
  Complexity::ESLintCalculator
52
+ when :python
53
+ Complexity::PythonCalculator
54
+ when :go
55
+ Complexity::GoCalculator
48
56
  end
49
57
  end
50
58
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Diff
5
+ class Checker
6
+ def initialize(engine_builder:, serializer:, reference:)
7
+ @engine_builder = engine_builder
8
+ @serializer = serializer
9
+ @reference = reference
10
+ end
11
+
12
+ def check(folder:)
13
+ worktree = setup_worktree(folder:)
14
+
15
+ begin
16
+ before_result = @engine_builder.call.check(folder: worktree.folder)
17
+ after_result = @engine_builder.call.check(folder:)
18
+ serialize_results(before_result, after_result)
19
+ ensure
20
+ cleanup_worktree(worktree)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def setup_worktree(folder:)
27
+ git_strategy = GitStrategy.new(folder:)
28
+ worktree_number = "diff_#{Thread.current.object_id}"
29
+ worktree = Timetravel::Worktree.new(root_folder: folder, git_strategy:, number: worktree_number)
30
+ worktree.prepare
31
+ worktree.checkout(@reference)
32
+ worktree
33
+ end
34
+
35
+ def cleanup_worktree(worktree)
36
+ worktree.remove
37
+ rescue StandardError
38
+ FileUtils.rm_rf(worktree.folder) if worktree&.folder
39
+ end
40
+
41
+ def serialize_results(before_result, after_result)
42
+ @serializer.serialize(reference: @reference, before: before_result, after: after_result)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Diff
5
+ class Config
6
+ def initialize(
7
+ language:,
8
+ reference:,
9
+ serializer: :json,
10
+ since: nil,
11
+ excluded: [],
12
+ complexity_validator: ComplexityValidator,
13
+ since_validator: Normal::SinceValidator,
14
+ **options
15
+ )
16
+ @language = language
17
+ @reference = reference
18
+ @serializer = serializer
19
+ @since = since
20
+ @excluded = excluded
21
+ @complexity_validator = complexity_validator
22
+ @since_validator = since_validator
23
+ @options = options
24
+ end
25
+
26
+ def validate!
27
+ LanguageValidator.validate!(@language)
28
+ @since_validator.validate!(since: @since, relative_period: nil)
29
+ @complexity_validator.validate!(@language)
30
+ end
31
+
32
+ def checker
33
+ Checker.new(
34
+ engine_builder: method(:build_engine),
35
+ serializer: diff_serializer,
36
+ reference: @reference,
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ def build_engine
43
+ Normal::Config.new(language: @language, serializer: :none, since: @since, excluded: @excluded).checker
44
+ end
45
+
46
+ def diff_serializer
47
+ case @serializer
48
+ when :json then Serializer::Json
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end