churn_vs_complexity 1.7.0 → 1.8.0
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 +6 -0
- data/CLAUDE.md +1 -1
- data/README.md +11 -1
- data/lib/churn_vs_complexity/cli/parser.rb +8 -0
- data/lib/churn_vs_complexity/complexity/rust_calculator.rb +54 -0
- data/lib/churn_vs_complexity/complexity/swift_calculator.rb +54 -0
- data/lib/churn_vs_complexity/complexity.rb +2 -0
- data/lib/churn_vs_complexity/complexity_validator.rb +4 -0
- data/lib/churn_vs_complexity/delta.rb +8 -0
- data/lib/churn_vs_complexity/file_selector.rb +24 -0
- data/lib/churn_vs_complexity/language_validator.rb +1 -1
- data/lib/churn_vs_complexity/normal/config.rb +16 -0
- data/lib/churn_vs_complexity/risk_classifier.rb +2 -0
- data/lib/churn_vs_complexity/version.rb +1 -1
- data/tmp/test-support/rust/main.rs +7 -0
- data/tmp/test-support/rust/utils.rs +20 -0
- data/tmp/test-support/swift/main.swift +7 -0
- data/tmp/test-support/swift/utils.swift +24 -0
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5a149d026d33ac7319cc78d8c57af7e03113edf42d69f35612bbd72be708c77
|
|
4
|
+
data.tar.gz: c87fb3252fc699c681fd8da590fe02fe2d06e47b85fd5b7667fe314bd8ce3393
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '09d8eddfd30cd8c56abbd200d5f4e31529ac5fc1171837e3420ccfe2536bf203e3d003a8fbebe7c4c6b0fb54be9a84d71071ca447b9526538edd814953c8b436'
|
|
7
|
+
data.tar.gz: ad11518d39553ff5b03fb6c8ea0bc44989ba47d034cbd3b82582279055cad86806612cf0f71c1888b6ecf2aa312a20afce3ad80c8d36b5ab85480152e213e570
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [1.8.0] - 2026-04-05
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Rust support for complexity calculation (via [lizard](https://github.com/terryyin/lizard), install with `pip install lizard`)
|
|
5
|
+
- Swift support for complexity calculation (via [lizard](https://github.com/terryyin/lizard), install with `pip install lizard`)
|
|
6
|
+
|
|
1
7
|
## [1.7.0] - 2026-04-05
|
|
2
8
|
|
|
3
9
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
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), Java (via PMD), Python (via Radon), Go (via gocognit), and
|
|
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), Java (via PMD), Python (via Radon), Go (via gocognit), Kotlin (via lizard), Rust (via lizard), and Swift (via lizard). Requires Ruby >= 3.3.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# ChurnVsComplexity
|
|
4
4
|
|
|
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, Go, and
|
|
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, Go, Kotlin, Rust, and Swift.
|
|
6
6
|
|
|
7
7
|
Modes include hotspots ranking, triage assessment, CI quality gate, diff comparison, focus sessions, and timetravel history.
|
|
8
8
|
|
|
@@ -34,6 +34,8 @@ External tool dependencies per language:
|
|
|
34
34
|
- **Python**: Requires [Radon](https://radon.readthedocs.io) on the search path as `radon`. Install with `pip install radon`.
|
|
35
35
|
- **Go**: Requires [gocyclo](https://github.com/fzipp/gocyclo) on the search path. Install with `go install github.com/fzipp/gocyclo/cmd/gocyclo@latest`.
|
|
36
36
|
- **Kotlin**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
|
|
37
|
+
- **Rust**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
|
|
38
|
+
- **Swift**: Requires [lizard](https://github.com/terryyin/lizard) on the search path. Install with `pip install lizard`.
|
|
37
39
|
|
|
38
40
|
## Usage
|
|
39
41
|
|
|
@@ -50,6 +52,8 @@ Languages:
|
|
|
50
52
|
--python Check complexity of python files
|
|
51
53
|
--go Check complexity of go files
|
|
52
54
|
--kotlin Check complexity of kotlin files
|
|
55
|
+
--rust Check complexity of rust files
|
|
56
|
+
--swift Check complexity of swift files
|
|
53
57
|
|
|
54
58
|
Modes (mutually exclusive):
|
|
55
59
|
--timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
|
|
@@ -126,6 +130,12 @@ churn_vs_complexity --js --delta HEAD --summary my_js_project
|
|
|
126
130
|
|
|
127
131
|
# Kotlin project hotspots
|
|
128
132
|
churn_vs_complexity --kotlin --hotspots my_kotlin_project
|
|
133
|
+
|
|
134
|
+
# Rust project CSV report
|
|
135
|
+
churn_vs_complexity --rust --csv my_rust_project > ~/Desktop/rust-demo.csv
|
|
136
|
+
|
|
137
|
+
# Swift project summary
|
|
138
|
+
churn_vs_complexity --swift --summary -q my_swift_project
|
|
129
139
|
```
|
|
130
140
|
|
|
131
141
|
## Development
|
|
@@ -38,6 +38,14 @@ module ChurnVsComplexity
|
|
|
38
38
|
options[:language] = :kotlin
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
opts.on('--rust', 'Check complexity of rust files') do
|
|
42
|
+
options[:language] = :rust
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
opts.on('--swift', 'Check complexity of swift files') do
|
|
46
|
+
options[:language] = :swift
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
opts.separator ''
|
|
42
50
|
opts.separator 'Modes (mutually exclusive):'
|
|
43
51
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module ChurnVsComplexity
|
|
6
|
+
module Complexity
|
|
7
|
+
module RustCalculator
|
|
8
|
+
class << self
|
|
9
|
+
attr_writer :command_runner
|
|
10
|
+
|
|
11
|
+
def folder_based? = false
|
|
12
|
+
|
|
13
|
+
def calculate(files:)
|
|
14
|
+
csv_output = run_lizard(files)
|
|
15
|
+
parse_lizard_output(csv_output, files:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Lizard CSV format: NLOC,CCN,tokens,params,length,"loc","file","func","long_func",start,end
|
|
19
|
+
LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"/
|
|
20
|
+
|
|
21
|
+
def parse_lizard_output(csv_output, files:)
|
|
22
|
+
scores = Hash.new(0)
|
|
23
|
+
csv_output.each_line do |line|
|
|
24
|
+
match = line.match(LIZARD_LINE_PATTERN)
|
|
25
|
+
next unless match
|
|
26
|
+
|
|
27
|
+
scores[match[2]] += match[1].to_i
|
|
28
|
+
end
|
|
29
|
+
files.to_h { |file| [file, scores[file] || 0] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check_dependencies!
|
|
33
|
+
command_runner.call('lizard --version 2>&1')
|
|
34
|
+
rescue Errno::ENOENT
|
|
35
|
+
raise Error, 'Needs lizard installed (pip install lizard)'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def command_runner
|
|
41
|
+
@command_runner || Open3.method(:capture2)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run_lizard(files)
|
|
45
|
+
files_arg = files.map { |f| "'#{f}'" }.join(' ')
|
|
46
|
+
stdout, status = command_runner.call("lizard --csv #{files_arg}")
|
|
47
|
+
raise Error, "lizard failed (exit #{status.exitstatus}). Is it installed?" unless status.success?
|
|
48
|
+
|
|
49
|
+
stdout
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module ChurnVsComplexity
|
|
6
|
+
module Complexity
|
|
7
|
+
module SwiftCalculator
|
|
8
|
+
class << self
|
|
9
|
+
attr_writer :command_runner
|
|
10
|
+
|
|
11
|
+
def folder_based? = false
|
|
12
|
+
|
|
13
|
+
def calculate(files:)
|
|
14
|
+
csv_output = run_lizard(files)
|
|
15
|
+
parse_lizard_output(csv_output, files:)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Lizard CSV format: NLOC,CCN,tokens,params,length,"loc","file","func","long_func",start,end
|
|
19
|
+
LIZARD_LINE_PATTERN = /^\d+,(\d+),\d+,\d+,\d+,"[^"]*","([^"]*)"/
|
|
20
|
+
|
|
21
|
+
def parse_lizard_output(csv_output, files:)
|
|
22
|
+
scores = Hash.new(0)
|
|
23
|
+
csv_output.each_line do |line|
|
|
24
|
+
match = line.match(LIZARD_LINE_PATTERN)
|
|
25
|
+
next unless match
|
|
26
|
+
|
|
27
|
+
scores[match[2]] += match[1].to_i
|
|
28
|
+
end
|
|
29
|
+
files.to_h { |file| [file, scores[file] || 0] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check_dependencies!
|
|
33
|
+
command_runner.call('lizard --version 2>&1')
|
|
34
|
+
rescue Errno::ENOENT
|
|
35
|
+
raise Error, 'Needs lizard installed (pip install lizard)'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def command_runner
|
|
41
|
+
@command_runner || Open3.method(:capture2)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run_lizard(files)
|
|
45
|
+
files_arg = files.map { |f| "'#{f}'" }.join(' ')
|
|
46
|
+
stdout, status = command_runner.call("lizard --csv #{files_arg}")
|
|
47
|
+
raise Error, "lizard failed (exit #{status.exitstatus}). Is it installed?" unless status.success?
|
|
48
|
+
|
|
49
|
+
stdout
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -6,6 +6,8 @@ require_relative 'complexity/eslint_calculator'
|
|
|
6
6
|
require_relative 'complexity/python_calculator'
|
|
7
7
|
require_relative 'complexity/go_calculator'
|
|
8
8
|
require_relative 'complexity/kotlin_calculator'
|
|
9
|
+
require_relative 'complexity/rust_calculator'
|
|
10
|
+
require_relative 'complexity/swift_calculator'
|
|
9
11
|
|
|
10
12
|
module ChurnVsComplexity
|
|
11
13
|
module Complexity
|
|
@@ -14,6 +14,10 @@ module ChurnVsComplexity
|
|
|
14
14
|
Complexity::GoCalculator.check_dependencies!
|
|
15
15
|
when :kotlin
|
|
16
16
|
Complexity::KotlinCalculator.check_dependencies!
|
|
17
|
+
when :rust
|
|
18
|
+
Complexity::RustCalculator.check_dependencies!
|
|
19
|
+
when :swift
|
|
20
|
+
Complexity::SwiftCalculator.check_dependencies!
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
end
|
|
@@ -40,6 +40,10 @@ module ChurnVsComplexity
|
|
|
40
40
|
FileSelector::Go.predefined(included:, excluded:)
|
|
41
41
|
when :kotlin
|
|
42
42
|
FileSelector::Kotlin.predefined(included:, excluded:)
|
|
43
|
+
when :rust
|
|
44
|
+
FileSelector::Rust.predefined(included:, excluded:)
|
|
45
|
+
when :swift
|
|
46
|
+
FileSelector::Swift.predefined(included:, excluded:)
|
|
43
47
|
end
|
|
44
48
|
end
|
|
45
49
|
|
|
@@ -57,6 +61,10 @@ module ChurnVsComplexity
|
|
|
57
61
|
Complexity::GoCalculator
|
|
58
62
|
when :kotlin
|
|
59
63
|
Complexity::KotlinCalculator
|
|
64
|
+
when :rust
|
|
65
|
+
Complexity::RustCalculator
|
|
66
|
+
when :swift
|
|
67
|
+
Complexity::SwiftCalculator
|
|
60
68
|
end
|
|
61
69
|
end
|
|
62
70
|
end
|
|
@@ -16,6 +16,10 @@ module ChurnVsComplexity
|
|
|
16
16
|
['.go']
|
|
17
17
|
when :kotlin
|
|
18
18
|
['.kt', '.kts']
|
|
19
|
+
when :rust
|
|
20
|
+
['.rs']
|
|
21
|
+
when :swift
|
|
22
|
+
['.swift']
|
|
19
23
|
else
|
|
20
24
|
raise Error, "Unsupported language: #{language}"
|
|
21
25
|
end
|
|
@@ -142,5 +146,25 @@ module ChurnVsComplexity
|
|
|
142
146
|
Predefined.new(included:, extensions: FileSelector.extensions(:kotlin), excluded:)
|
|
143
147
|
end
|
|
144
148
|
end
|
|
149
|
+
|
|
150
|
+
module Rust
|
|
151
|
+
def self.excluding(excluded)
|
|
152
|
+
Excluding.new(FileSelector.extensions(:rust), excluded)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.predefined(included:, excluded:)
|
|
156
|
+
Predefined.new(included:, extensions: FileSelector.extensions(:rust), excluded:)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
module Swift
|
|
161
|
+
def self.excluding(excluded)
|
|
162
|
+
Excluding.new(FileSelector.extensions(:swift), excluded)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def self.predefined(included:, excluded:)
|
|
166
|
+
Predefined.new(included:, extensions: FileSelector.extensions(:swift), excluded:)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
145
169
|
end
|
|
146
170
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module ChurnVsComplexity
|
|
4
4
|
module LanguageValidator
|
|
5
|
-
SUPPORTED = %i[java ruby javascript python go kotlin].freeze
|
|
5
|
+
SUPPORTED = %i[java ruby javascript python go kotlin rust swift].freeze
|
|
6
6
|
|
|
7
7
|
def self.validate!(language)
|
|
8
8
|
raise ValidationError, "Unsupported language: #{language}" unless SUPPORTED.include?(language)
|
|
@@ -83,6 +83,22 @@ module ChurnVsComplexity
|
|
|
83
83
|
serializer:,
|
|
84
84
|
since: @since || @relative_period,
|
|
85
85
|
)
|
|
86
|
+
when :rust
|
|
87
|
+
Engine.concurrent(
|
|
88
|
+
complexity: Complexity::RustCalculator,
|
|
89
|
+
churn:,
|
|
90
|
+
file_selector: FileSelector::Rust.excluding(@excluded),
|
|
91
|
+
serializer:,
|
|
92
|
+
since: @since || @relative_period,
|
|
93
|
+
)
|
|
94
|
+
when :swift
|
|
95
|
+
Engine.concurrent(
|
|
96
|
+
complexity: Complexity::SwiftCalculator,
|
|
97
|
+
churn:,
|
|
98
|
+
file_selector: FileSelector::Swift.excluding(@excluded),
|
|
99
|
+
serializer:,
|
|
100
|
+
since: @since || @relative_period,
|
|
101
|
+
)
|
|
86
102
|
end
|
|
87
103
|
end
|
|
88
104
|
|
|
@@ -15,6 +15,8 @@ module ChurnVsComplexity
|
|
|
15
15
|
# Java PMD Cyclomatic complexity 1 ~39 1-40
|
|
16
16
|
# Go gocognit Cognitive complexity 0 ~87 0-50
|
|
17
17
|
# Kotlin lizard Cyclomatic complexity 1 ~40 1-40
|
|
18
|
+
# Rust lizard Cyclomatic complexity 1 ~40 1-40
|
|
19
|
+
# Swift lizard Cyclomatic complexity 1 ~40 1-40
|
|
18
20
|
#
|
|
19
21
|
# The DEFAULT_LOW and DEFAULT_HIGH thresholds below are rough midpoints
|
|
20
22
|
# suitable for Java/Python/JS. They are too aggressive for Ruby (Flog
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
fn calculate_sum(numbers: &[i32]) -> i32 {
|
|
2
|
+
let mut total = 0;
|
|
3
|
+
for n in numbers {
|
|
4
|
+
total += n;
|
|
5
|
+
}
|
|
6
|
+
total
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
fn classify(value: i32) -> &'static str {
|
|
10
|
+
match value {
|
|
11
|
+
0 => "zero",
|
|
12
|
+
1..=10 => "low",
|
|
13
|
+
11..=100 => "medium",
|
|
14
|
+
_ => "high",
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
fn is_even(n: i32) -> bool {
|
|
19
|
+
n % 2 == 0
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
func calculateSum(_ numbers: [Int]) -> Int {
|
|
2
|
+
var total = 0
|
|
3
|
+
for n in numbers {
|
|
4
|
+
total += n
|
|
5
|
+
}
|
|
6
|
+
return total
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
func classify(_ value: Int) -> String {
|
|
10
|
+
switch value {
|
|
11
|
+
case 0:
|
|
12
|
+
return "zero"
|
|
13
|
+
case 1...10:
|
|
14
|
+
return "low"
|
|
15
|
+
case 11...100:
|
|
16
|
+
return "medium"
|
|
17
|
+
default:
|
|
18
|
+
return "high"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func isEven(_ n: Int) -> Bool {
|
|
23
|
+
return n % 2 == 0
|
|
24
|
+
}
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: churn_vs_complexity
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Erik T. Madsen
|
|
@@ -39,9 +39,9 @@ dependencies:
|
|
|
39
39
|
version: '2.1'
|
|
40
40
|
description: Correlates file churn (how often files change) with complexity scores
|
|
41
41
|
to identify refactoring hotspots. Supports Ruby, JavaScript/TypeScript, Java, Python,
|
|
42
|
-
Go, and
|
|
43
|
-
diff comparison, focus sessions, and timetravel history. Inspired
|
|
44
|
-
article "Getting Empirical about Refactoring".
|
|
42
|
+
Go, Kotlin, Rust, and Swift. Modes include hotspots ranking, triage assessment,
|
|
43
|
+
CI quality gate, diff comparison, focus sessions, and timetravel history. Inspired
|
|
44
|
+
by Michael Feathers' article "Getting Empirical about Refactoring".
|
|
45
45
|
email:
|
|
46
46
|
- beatmadsen@gmail.com
|
|
47
47
|
executables:
|
|
@@ -74,6 +74,8 @@ files:
|
|
|
74
74
|
- lib/churn_vs_complexity/complexity/pmd/files_calculator.rb
|
|
75
75
|
- lib/churn_vs_complexity/complexity/pmd/folder_calculator.rb
|
|
76
76
|
- lib/churn_vs_complexity/complexity/python_calculator.rb
|
|
77
|
+
- lib/churn_vs_complexity/complexity/rust_calculator.rb
|
|
78
|
+
- lib/churn_vs_complexity/complexity/swift_calculator.rb
|
|
77
79
|
- lib/churn_vs_complexity/complexity_validator.rb
|
|
78
80
|
- lib/churn_vs_complexity/concurrent_calculator.rb
|
|
79
81
|
- lib/churn_vs_complexity/delta.rb
|
|
@@ -150,6 +152,10 @@ files:
|
|
|
150
152
|
- tmp/test-support/kotlin/Utils.kt
|
|
151
153
|
- tmp/test-support/python/example.py
|
|
152
154
|
- tmp/test-support/python/utils.py
|
|
155
|
+
- tmp/test-support/rust/main.rs
|
|
156
|
+
- tmp/test-support/rust/utils.rs
|
|
157
|
+
- tmp/test-support/swift/main.swift
|
|
158
|
+
- tmp/test-support/swift/utils.swift
|
|
153
159
|
- tmp/test-support/txt/abc.txt
|
|
154
160
|
- tmp/test-support/txt/d.txt
|
|
155
161
|
- tmp/test-support/txt/ef.txt
|