churn_vs_complexity 1.0.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/README.md +9 -1
- data/lib/churn_vs_complexity/churn.rb +11 -8
- data/lib/churn_vs_complexity/cli.rb +27 -16
- data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +36 -0
- data/lib/churn_vs_complexity/complexity/flog_calculator.rb +6 -5
- data/lib/churn_vs_complexity/complexity.rb +1 -0
- data/lib/churn_vs_complexity/concurrent_calculator.rb +16 -8
- data/lib/churn_vs_complexity/config.rb +40 -7
- data/lib/churn_vs_complexity/engine.rb +6 -5
- data/lib/churn_vs_complexity/file_selector.rb +23 -4
- data/lib/churn_vs_complexity/git_date.rb +59 -0
- data/lib/churn_vs_complexity/serializer.rb +50 -7
- data/lib/churn_vs_complexity/version.rb +1 -1
- data/lib/churn_vs_complexity.rb +1 -0
- data/package-lock.json +6 -0
- data/tmp/eslint-support/complexity-calculator.js +51 -0
- data/tmp/eslint-support/package.json +11 -0
- data/tmp/test-support/javascript/complex.js +43 -0
- data/tmp/test-support/javascript/moderate.js +12 -0
- data/tmp/test-support/javascript/simple.js +5 -0
- data/tmp/test-support/javascript/typescript-example.ts +26 -0
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de43228c387df735f0bd01eff8f53cfac5bf752474be0f4e901d29d5847781f5
|
4
|
+
data.tar.gz: d78475fd751dff01f1aafb45e995cdeabd2d0561348e86de5cf5f951dcc7b6f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4297b81854c54d4b60672f89156977299549cc012519b178ce206f3108e3e7dc65b9a9d0341b0f218945b9be580f9cc46fb8b5d049a3e5508f4fb4d026e23f5f
|
7
|
+
data.tar.gz: 1338de4b39496a4af2f9485c14178112558399ab308b03d76ad18294e8a5cb6f87f55f5a0990ea0963ae63d3e6a5c49758c5aed072390e2610e2f0e5b52c2723
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,19 @@
|
|
1
1
|
## [1.0.0] - 2024-06-07
|
2
2
|
|
3
|
-
- Initial release
|
3
|
+
- Initial release
|
4
|
+
|
5
|
+
## [1.1.0] - 2024-09-20
|
6
|
+
|
7
|
+
- Introduce `--summary` flag to output summary statistics for churn and complexity
|
8
|
+
- Introduce `--month`, `--quarter`, and `--year` short-hand flags to calculate churn for different time periods relative to the most recent commit
|
9
|
+
|
10
|
+
## [1.2.0] - 2024-09-20
|
11
|
+
|
12
|
+
- Fix bug in CLI where new flags and `--since` would not be recognized
|
13
|
+
- Improve selection of observations included in the output
|
14
|
+
- Fixed calculation of churn that would never be zero
|
15
|
+
|
16
|
+
## [1.3.0] - 2024-09-26
|
17
|
+
|
18
|
+
- Add support for javascript and typescript complexity calculation using eslint
|
19
|
+
- Fixed behavior when --since or short-hand flags were not provided
|
data/README.md
CHANGED
@@ -31,13 +31,20 @@ In order to use the `--java` flag, you must first install PMD manually, and the
|
|
31
31
|
Execute the `churn_vs_complexity` with the applicable arguments. Output in the requested format will be directed to stdout.
|
32
32
|
|
33
33
|
```
|
34
|
-
churn_vs_complexity [options] folder
|
34
|
+
Usage: churn_vs_complexity [options] folder
|
35
35
|
--java Check complexity of java classes
|
36
36
|
--ruby Check complexity of ruby files
|
37
|
+
--js, --ts, --javascript, --typescript
|
38
|
+
Check complexity of javascript and typescript files
|
37
39
|
--csv Format output as CSV
|
38
40
|
--graph Format output as HTML page with Churn vs Complexity graph
|
41
|
+
--summary Output summary statistics (mean and median) for churn and complexity
|
39
42
|
--excluded PATTERN Exclude file paths including this string. Can be used multiple times.
|
40
43
|
--since YYYY-MM-DD Calculate churn after this date
|
44
|
+
-m, --month Calculate churn for the month leading up to the most recent commit
|
45
|
+
-q, --quarter Calculate churn for the quarter leading up to the most recent commit
|
46
|
+
-y, --year Calculate churn for the year leading up to the most recent commit
|
47
|
+
--dry-run Echo the chosen options from the CLI
|
41
48
|
-h, --help Display help
|
42
49
|
```
|
43
50
|
|
@@ -47,6 +54,7 @@ churn_vs_complexity [options] folder
|
|
47
54
|
|
48
55
|
`churn_vs_complexity --java --graph --exclude generated-sources --exclude generated-test-sources --since 2023-01-01 my_java_project > ~/Desktop/java-demo.html`
|
49
56
|
|
57
|
+
`churn_vs_complexity --ruby --summary -m my_ruby_project >> ~/Desktop/monthly-report.txt`
|
50
58
|
|
51
59
|
|
52
60
|
## Development
|
@@ -7,18 +7,21 @@ module ChurnVsComplexity
|
|
7
7
|
module GitCalculator
|
8
8
|
class << self
|
9
9
|
def calculate(folder:, file:, since:)
|
10
|
-
|
11
|
-
|
10
|
+
git_dir = File.join(folder, '.git')
|
11
|
+
earliest_date = [date_of_first_commit(folder:), since].max
|
12
|
+
formatted_date = earliest_date.strftime('%Y-%m-%d')
|
13
|
+
cmd = %Q(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
|
14
|
+
`#{cmd}`.to_i
|
15
|
+
end
|
16
|
+
|
17
|
+
def date_of_latest_commit(folder:)
|
18
|
+
repo(folder).log.first.date
|
12
19
|
end
|
13
20
|
|
14
21
|
private
|
15
22
|
|
16
|
-
def
|
17
|
-
|
18
|
-
formatted_date = since.strftime('%Y-%m-%d')
|
19
|
-
# git log --follow --oneline --since="YYYY-MM-DD" <file_path> | wc -l
|
20
|
-
`git --git-dir #{File.join(folder,
|
21
|
-
'.git',)} --work-tree #{folder} log --follow --oneline --since=#{formatted_date} #{file} | wc -l`.to_i
|
23
|
+
def date_of_first_commit(folder:)
|
24
|
+
repo(folder).log.last&.date&.to_date || Time.at(0).to_date
|
22
25
|
end
|
23
26
|
|
24
27
|
def repo(folder)
|
@@ -9,7 +9,6 @@ module ChurnVsComplexity
|
|
9
9
|
def self.run!
|
10
10
|
# Create an options hash to store parsed options
|
11
11
|
options = { excluded: [] }
|
12
|
-
since = nil
|
13
12
|
|
14
13
|
# Initialize OptionParser
|
15
14
|
OptionParser.new do |opts|
|
@@ -23,6 +22,10 @@ module ChurnVsComplexity
|
|
23
22
|
options[:language] = :ruby
|
24
23
|
end
|
25
24
|
|
25
|
+
opts.on('--js', '--ts', '--javascript', '--typescript', 'Check complexity of javascript and typescript files') do
|
26
|
+
options[:language] = :javascript
|
27
|
+
end
|
28
|
+
|
26
29
|
opts.on('--csv', 'Format output as CSV') do
|
27
30
|
options[:serializer] = :csv
|
28
31
|
end
|
@@ -31,13 +34,34 @@ module ChurnVsComplexity
|
|
31
34
|
options[:serializer] = :graph
|
32
35
|
end
|
33
36
|
|
37
|
+
opts.on('--summary', 'Output summary statistics (mean and median) for churn and complexity') do
|
38
|
+
options[:serializer] = :summary
|
39
|
+
end
|
40
|
+
|
34
41
|
opts.on('--excluded PATTERN',
|
35
42
|
'Exclude file paths including this string. Can be used multiple times.',) do |value|
|
36
43
|
options[:excluded] << value
|
37
44
|
end
|
38
45
|
|
39
46
|
opts.on('--since YYYY-MM-DD', 'Calculate churn after this date') do |value|
|
40
|
-
since = value
|
47
|
+
options[:since] = value
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on('-m', '--month', 'Calculate churn for the month leading up to the most recent commit') do
|
51
|
+
options[:since] = :month
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on('-q', '--quarter', 'Calculate churn for the quarter leading up to the most recent commit') do
|
55
|
+
options[:since] = :quarter
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on('-y', '--year', 'Calculate churn for the year leading up to the most recent commit') do
|
59
|
+
options[:since] = :year
|
60
|
+
end
|
61
|
+
|
62
|
+
opts.on('--dry-run', 'Echo the chosen options from the CLI') do
|
63
|
+
puts options
|
64
|
+
exit
|
41
65
|
end
|
42
66
|
|
43
67
|
opts.on('-h', '--help', 'Display help') do
|
@@ -56,24 +80,11 @@ module ChurnVsComplexity
|
|
56
80
|
|
57
81
|
raise Error, 'No options selected. Use --help for usage information.' if options.empty?
|
58
82
|
|
59
|
-
begin
|
60
|
-
if since.nil?
|
61
|
-
since = Time.at(0).to_date
|
62
|
-
options[:graph_title] = 'Churn vs Complexity'
|
63
|
-
else
|
64
|
-
date_string = since
|
65
|
-
since = Date.strptime(since, '%Y-%m-%d')
|
66
|
-
options[:graph_title] = "Churn vs Complexity since #{date_string}"
|
67
|
-
end
|
68
|
-
rescue StandardError
|
69
|
-
raise Error, "Invalid date #{since}, please use correct format, YYYY-MM-DD"
|
70
|
-
end
|
71
|
-
|
72
83
|
config = Config.new(**options)
|
73
84
|
|
74
85
|
config.validate!
|
75
86
|
|
76
|
-
puts config.to_engine.check(folder
|
87
|
+
puts config.to_engine.check(folder:)
|
77
88
|
end
|
78
89
|
end
|
79
90
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Complexity
|
5
|
+
module ESLintCalculator
|
6
|
+
class << self
|
7
|
+
def folder_based? = false
|
8
|
+
|
9
|
+
def calculate(files:)
|
10
|
+
dir_path = File.join(gem_root, 'tmp', 'eslint-support')
|
11
|
+
script_path = File.join(dir_path, 'complexity-calculator.js')
|
12
|
+
install_command = "npm install --prefix '#{dir_path}'"
|
13
|
+
`#{install_command}`
|
14
|
+
|
15
|
+
|
16
|
+
command = "node #{script_path} '#{files.to_json}'"
|
17
|
+
complexity = `#{command}`
|
18
|
+
|
19
|
+
if complexity.empty?
|
20
|
+
raise Error, "Failed to calculate complexity"
|
21
|
+
end
|
22
|
+
all = JSON.parse(complexity)
|
23
|
+
all.to_h do |abc|
|
24
|
+
[abc['file'], abc['complexity']]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def gem_root
|
31
|
+
File.expand_path('../../..', __dir__)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -5,15 +5,16 @@ require 'flog'
|
|
5
5
|
module ChurnVsComplexity
|
6
6
|
module Complexity
|
7
7
|
module FlogCalculator
|
8
|
-
CONCURRENCY = Etc.nprocessors
|
9
|
-
|
10
8
|
class << self
|
11
9
|
def folder_based? = false
|
12
10
|
|
13
|
-
def calculate(
|
11
|
+
def calculate(files:)
|
14
12
|
flog = Flog.new
|
15
|
-
|
16
|
-
|
13
|
+
# TODO: Run this concurrently
|
14
|
+
files.to_h do |file|
|
15
|
+
flog.flog(file)
|
16
|
+
[file, flog.total_score]
|
17
|
+
end
|
17
18
|
end
|
18
19
|
end
|
19
20
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'etc'
|
4
|
+
|
3
5
|
module ChurnVsComplexity
|
4
6
|
class ConcurrentCalculator
|
5
7
|
CONCURRENCY = Etc.nprocessors
|
@@ -11,7 +13,9 @@ module ChurnVsComplexity
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def calculate(folder:, files:, since:)
|
14
|
-
|
16
|
+
latest_commit_date = @churn.date_of_latest_commit(folder:)
|
17
|
+
@git_period = GitDate.git_period(since, latest_commit_date)
|
18
|
+
schedule_churn_calculation(folder, files[:included], @git_period.effective_start_date)
|
15
19
|
calculate_complexity(folder, files)
|
16
20
|
await_results
|
17
21
|
combine_results
|
@@ -22,11 +26,11 @@ module ChurnVsComplexity
|
|
22
26
|
def calculate_complexity(folder, files)
|
23
27
|
@complexity_results =
|
24
28
|
if @complexity.folder_based?
|
25
|
-
@complexity.calculate(folder:)
|
29
|
+
result = @complexity.calculate(folder:)
|
30
|
+
files[:explicitly_excluded].each { |file| result.delete(file) }
|
31
|
+
result
|
26
32
|
else
|
27
|
-
|
28
|
-
acc.merge!(@complexity.calculate(file:))
|
29
|
-
end
|
33
|
+
@complexity.calculate(files: files[:included])
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
@@ -51,10 +55,14 @@ module ChurnVsComplexity
|
|
51
55
|
end
|
52
56
|
|
53
57
|
def combine_results
|
54
|
-
|
55
|
-
|
56
|
-
|
58
|
+
result = {}
|
59
|
+
result[:values_by_file] = @complexity_results.keys.each_with_object({}) do |file, acc|
|
60
|
+
# File with complexity score might not have churned in queried period,
|
61
|
+
# set zero churn on miss
|
62
|
+
acc[file] = [@churn_results[file] || 0, @complexity_results[file]]
|
57
63
|
end
|
64
|
+
result[:git_period] = @git_period
|
65
|
+
result
|
58
66
|
end
|
59
67
|
end
|
60
68
|
end
|
@@ -6,21 +6,23 @@ module ChurnVsComplexity
|
|
6
6
|
language:,
|
7
7
|
serializer:,
|
8
8
|
excluded: [],
|
9
|
-
|
10
|
-
complexity_validator: ComplexityValidator
|
9
|
+
since: nil,
|
10
|
+
complexity_validator: ComplexityValidator,
|
11
|
+
since_validator: SinceValidator
|
11
12
|
)
|
12
13
|
@language = language
|
13
14
|
@serializer = serializer
|
14
15
|
@excluded = excluded
|
16
|
+
@since = since
|
15
17
|
@complexity_validator = complexity_validator
|
16
|
-
@
|
18
|
+
@since_validator = since_validator
|
17
19
|
end
|
18
20
|
|
19
21
|
def validate!
|
20
|
-
raise Error, "Unsupported language: #{@language}" unless %i[java ruby].include?(@language)
|
21
|
-
raise Error, "Unsupported serializer: #{@serializer}" unless %i[none csv graph].include?(@serializer)
|
22
|
-
raise Error, 'Please provide a title for the graph' if @serializer == :graph && @graph_title.nil?
|
22
|
+
raise Error, "Unsupported language: #{@language}" unless %i[java ruby javascript].include?(@language)
|
23
|
+
raise Error, "Unsupported serializer: #{@serializer}" unless %i[none csv graph summary].include?(@serializer)
|
23
24
|
|
25
|
+
@since_validator.validate!(@since)
|
24
26
|
@complexity_validator.validate!(@language)
|
25
27
|
end
|
26
28
|
|
@@ -32,6 +34,7 @@ module ChurnVsComplexity
|
|
32
34
|
churn:,
|
33
35
|
file_selector: FileSelector::Java.excluding(@excluded),
|
34
36
|
serializer:,
|
37
|
+
since: @since,
|
35
38
|
)
|
36
39
|
when :ruby
|
37
40
|
Engine.concurrent(
|
@@ -39,6 +42,15 @@ module ChurnVsComplexity
|
|
39
42
|
churn:,
|
40
43
|
file_selector: FileSelector::Ruby.excluding(@excluded),
|
41
44
|
serializer:,
|
45
|
+
since: @since,
|
46
|
+
)
|
47
|
+
when :javascript
|
48
|
+
Engine.concurrent(
|
49
|
+
complexity: Complexity::ESLintCalculator,
|
50
|
+
churn:,
|
51
|
+
file_selector: FileSelector::JavaScript.excluding(@excluded),
|
52
|
+
serializer:,
|
53
|
+
since: @since,
|
42
54
|
)
|
43
55
|
end
|
44
56
|
end
|
@@ -54,7 +66,9 @@ module ChurnVsComplexity
|
|
54
66
|
when :csv
|
55
67
|
Serializer::CSV
|
56
68
|
when :graph
|
57
|
-
Serializer::Graph.new
|
69
|
+
Serializer::Graph.new
|
70
|
+
when :summary
|
71
|
+
Serializer::Summary
|
58
72
|
end
|
59
73
|
end
|
60
74
|
|
@@ -66,5 +80,24 @@ module ChurnVsComplexity
|
|
66
80
|
end
|
67
81
|
end
|
68
82
|
end
|
83
|
+
|
84
|
+
module SinceValidator
|
85
|
+
def self.validate!(since)
|
86
|
+
# since can be nil, a date string or a keyword (:month, :quarter, :year)
|
87
|
+
return if since.nil?
|
88
|
+
|
89
|
+
if since.is_a?(Symbol)
|
90
|
+
raise Error, "Invalid since value #{since}" unless %i[month quarter year].include?(since)
|
91
|
+
elsif since.is_a?(String)
|
92
|
+
begin
|
93
|
+
Date.strptime(since, '%Y-%m-%d')
|
94
|
+
rescue StandardError
|
95
|
+
raise Error, "Invalid date #{since}, please use correct format, YYYY-MM-DD"
|
96
|
+
end
|
97
|
+
else
|
98
|
+
raise Error, "Invalid since value #{since}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
69
102
|
end
|
70
103
|
end
|
@@ -2,20 +2,21 @@
|
|
2
2
|
|
3
3
|
module ChurnVsComplexity
|
4
4
|
class Engine
|
5
|
-
def initialize(file_selector:, calculator:, serializer:)
|
5
|
+
def initialize(file_selector:, calculator:, serializer:, since:)
|
6
6
|
@file_selector = file_selector
|
7
7
|
@calculator = calculator
|
8
8
|
@serializer = serializer
|
9
|
+
@since = since
|
9
10
|
end
|
10
11
|
|
11
|
-
def check(folder
|
12
|
+
def check(folder:)
|
12
13
|
files = @file_selector.select_files(folder)
|
13
|
-
result = @calculator.calculate(folder:, files:, since:)
|
14
|
+
result = @calculator.calculate(folder:, files:, since: @since)
|
14
15
|
@serializer.serialize(result)
|
15
16
|
end
|
16
17
|
|
17
|
-
def self.concurrent(complexity:, churn:, serializer: Serializer::None, file_selector: FileSelector::Any)
|
18
|
-
Engine.new(file_selector:, serializer:, calculator: ConcurrentCalculator.new(complexity:, churn:))
|
18
|
+
def self.concurrent(since:, complexity:, churn:, serializer: Serializer::None, file_selector: FileSelector::Any)
|
19
|
+
Engine.new(since:, file_selector:, serializer:, calculator: ConcurrentCalculator.new(complexity:, churn:))
|
19
20
|
end
|
20
21
|
end
|
21
22
|
end
|
@@ -4,20 +4,33 @@ module ChurnVsComplexity
|
|
4
4
|
module FileSelector
|
5
5
|
module Any
|
6
6
|
def self.select_files(folder)
|
7
|
-
Dir.glob("#{folder}/**/*").select { |f| File.file?(f) }
|
7
|
+
included = Dir.glob("#{folder}/**/*").select { |f| File.file?(f) }
|
8
|
+
{ explicitly_excluded: [], included: }
|
8
9
|
end
|
9
10
|
end
|
10
11
|
|
11
12
|
class Excluding
|
12
|
-
def initialize(extensions, excluded)
|
13
|
+
def initialize(extensions, excluded, convert_to_absolute_path = false)
|
13
14
|
@extensions = extensions
|
14
15
|
@excluded = excluded
|
16
|
+
@convert_to_absolute_path = convert_to_absolute_path
|
15
17
|
end
|
16
18
|
|
17
19
|
def select_files(folder)
|
18
|
-
|
19
|
-
|
20
|
+
were_excluded = []
|
21
|
+
were_included = []
|
22
|
+
Dir.glob("#{folder}/**/*").each do |f|
|
23
|
+
if has_excluded_pattern?(f)
|
24
|
+
were_excluded << f
|
25
|
+
elsif has_correct_extension?(f) && File.file?(f)
|
26
|
+
were_included << f
|
27
|
+
end
|
20
28
|
end
|
29
|
+
if @convert_to_absolute_path
|
30
|
+
were_excluded.map! { |f| File.absolute_path(f) }
|
31
|
+
were_included.map! { |f| File.absolute_path(f) }
|
32
|
+
end
|
33
|
+
{ explicitly_excluded: were_excluded, included: were_included }
|
21
34
|
end
|
22
35
|
|
23
36
|
private
|
@@ -42,5 +55,11 @@ module ChurnVsComplexity
|
|
42
55
|
Excluding.new(['.rb'], excluded)
|
43
56
|
end
|
44
57
|
end
|
58
|
+
|
59
|
+
module JavaScript
|
60
|
+
def self.excluding(excluded)
|
61
|
+
Excluding.new(['.js', '.jsx', '.ts', '.tsx'], excluded, true)
|
62
|
+
end
|
63
|
+
end
|
45
64
|
end
|
46
65
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module GitDate
|
5
|
+
def self.git_period(cli_arg_since, latest_commit_date)
|
6
|
+
latest_commit_date = latest_commit_date.to_date
|
7
|
+
if cli_arg_since.nil?
|
8
|
+
NoStartGitPeriod.new(latest_commit_date)
|
9
|
+
elsif cli_arg_since.is_a?(Symbol)
|
10
|
+
AbsoluteGitPeriod.looking_back(relative_period: cli_arg_since, from: latest_commit_date)
|
11
|
+
elsif cli_arg_since.is_a?(String)
|
12
|
+
AbsoluteGitPeriod.between(cli_arg_since, latest_commit_date)
|
13
|
+
else
|
14
|
+
raise Error, "Unexpected since value #{cli_arg_since}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class NoStartGitPeriod
|
19
|
+
attr_reader :end_date
|
20
|
+
|
21
|
+
def initialize(end_date)
|
22
|
+
@end_date = end_date
|
23
|
+
end
|
24
|
+
|
25
|
+
def effective_start_date = Time.at(0).to_date
|
26
|
+
|
27
|
+
def requested_start_date = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
class AbsoluteGitPeriod
|
31
|
+
attr_reader :end_date
|
32
|
+
|
33
|
+
def self.between(cli_arg_since, latest_commit_date)
|
34
|
+
start_date = Date.strptime(cli_arg_since, '%Y-%m-%d')
|
35
|
+
new(start_date, latest_commit_date)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.looking_back(relative_period:, from:)
|
39
|
+
shifter = case relative_period
|
40
|
+
when :month then 1
|
41
|
+
when :quarter then 3
|
42
|
+
when :year then 12
|
43
|
+
else raise Error, "Unexpected since value #{relative_period}"
|
44
|
+
end
|
45
|
+
start_date = from << shifter
|
46
|
+
new(start_date, from)
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(start_date, end_date)
|
50
|
+
@start_date = start_date
|
51
|
+
@end_date = end_date
|
52
|
+
end
|
53
|
+
|
54
|
+
def effective_start_date = @start_date
|
55
|
+
|
56
|
+
def requested_start_date = @start_date
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -2,12 +2,55 @@
|
|
2
2
|
|
3
3
|
module ChurnVsComplexity
|
4
4
|
module Serializer
|
5
|
+
def self.title(result)
|
6
|
+
requested_start_date = result[:git_period].requested_start_date
|
7
|
+
end_date = result[:git_period].end_date
|
8
|
+
if requested_start_date.nil?
|
9
|
+
"Churn until #{end_date.strftime('%Y-%m-%d')} vs complexity"
|
10
|
+
else
|
11
|
+
"Churn between #{requested_start_date.strftime('%Y-%m-%d')} and #{end_date.strftime('%Y-%m-%d')} vs complexity"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
5
15
|
module None
|
6
|
-
def self.serialize(
|
16
|
+
def self.serialize(result) = result
|
17
|
+
end
|
18
|
+
|
19
|
+
module Summary
|
20
|
+
def self.serialize(result)
|
21
|
+
values_by_file = result[:values_by_file]
|
22
|
+
churn_values = values_by_file.map { |_, values| values[0].to_f }
|
23
|
+
complexity_values = values_by_file.map { |_, values| values[1].to_f }
|
24
|
+
|
25
|
+
mean_churn = churn_values.sum / churn_values.size
|
26
|
+
median_churn = churn_values.sort[churn_values.size / 2]
|
27
|
+
mean_complexity = complexity_values.sum / complexity_values.size
|
28
|
+
median_complexity = complexity_values.sort[complexity_values.size / 2]
|
29
|
+
|
30
|
+
product = values_by_file.map { |_, values| values[0].to_f * values[1].to_f }
|
31
|
+
mean_product = product.sum / product.size
|
32
|
+
median_product = product.sort[product.size / 2]
|
33
|
+
|
34
|
+
<<~SUMMARY
|
35
|
+
#{Serializer.title(result)}
|
36
|
+
|
37
|
+
Number of observations: #{values_by_file.size}
|
38
|
+
|
39
|
+
Churn:
|
40
|
+
Mean #{mean_churn}, Median #{median_churn}
|
41
|
+
|
42
|
+
Complexity:
|
43
|
+
Mean #{mean_complexity}, Median #{median_complexity}
|
44
|
+
|
45
|
+
Product of churn and complexity:
|
46
|
+
Mean #{mean_product}, Median #{median_product}
|
47
|
+
SUMMARY
|
48
|
+
end
|
7
49
|
end
|
8
50
|
|
9
51
|
module CSV
|
10
|
-
def self.serialize(
|
52
|
+
def self.serialize(result)
|
53
|
+
values_by_file = result[:values_by_file]
|
11
54
|
values_by_file.map do |file, values|
|
12
55
|
"#{file},#{values[0]},#{values[1]}\n"
|
13
56
|
end.join
|
@@ -15,16 +58,16 @@ module ChurnVsComplexity
|
|
15
58
|
end
|
16
59
|
|
17
60
|
class Graph
|
18
|
-
def initialize(
|
61
|
+
def initialize(template: Graph.load_template_file)
|
19
62
|
@template = template
|
20
|
-
@title = title
|
21
63
|
end
|
22
64
|
|
23
|
-
def serialize(
|
24
|
-
data = values_by_file.map do |file, values|
|
65
|
+
def serialize(result)
|
66
|
+
data = result[:values_by_file].map do |file, values|
|
25
67
|
"{ file_path: '#{file}', churn: #{values[0]}, complexity: #{values[1]} }"
|
26
68
|
end.join(",\n") + "\n"
|
27
|
-
|
69
|
+
title = Serializer.title(result)
|
70
|
+
@template.gsub("// INSERT DATA\n", data).gsub('INSERT TITLE', title)
|
28
71
|
end
|
29
72
|
|
30
73
|
def self.load_template_file
|
data/lib/churn_vs_complexity.rb
CHANGED
@@ -12,6 +12,7 @@ require_relative 'churn_vs_complexity/churn'
|
|
12
12
|
require_relative 'churn_vs_complexity/cli'
|
13
13
|
require_relative 'churn_vs_complexity/config'
|
14
14
|
require_relative 'churn_vs_complexity/serializer'
|
15
|
+
require_relative 'churn_vs_complexity/git_date'
|
15
16
|
|
16
17
|
module ChurnVsComplexity
|
17
18
|
class Error < StandardError; end
|
data/package-lock.json
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
import { ESLint } from 'eslint';
|
2
|
+
|
3
|
+
import eslint from '@eslint/js';
|
4
|
+
import tseslint from 'typescript-eslint';
|
5
|
+
|
6
|
+
async function analyzeComplexity(files) {
|
7
|
+
const overrideConfig = tseslint.config(
|
8
|
+
eslint.configs.recommended,
|
9
|
+
...tseslint.configs.recommended,
|
10
|
+
{
|
11
|
+
rules: {
|
12
|
+
'complexity': ['warn', 0],
|
13
|
+
},
|
14
|
+
|
15
|
+
}
|
16
|
+
);
|
17
|
+
|
18
|
+
const linter = new ESLint({
|
19
|
+
overrideConfigFile: true,
|
20
|
+
overrideConfig,
|
21
|
+
cwd: '/',
|
22
|
+
});
|
23
|
+
|
24
|
+
try {
|
25
|
+
const results = await linter.lintFiles(files);
|
26
|
+
const complexityResults = results.map(result => {
|
27
|
+
const messages = result.messages.filter(msg => msg.ruleId === 'complexity');
|
28
|
+
const complexity = messages.reduce((sum, msg) => {
|
29
|
+
const complexityValue = parseInt(msg.message.match(/\d+/)[0], 10);
|
30
|
+
return sum + complexityValue;
|
31
|
+
}, 0);
|
32
|
+
|
33
|
+
if (complexity === 0) {
|
34
|
+
console.error("File has no complexity", result);
|
35
|
+
}
|
36
|
+
|
37
|
+
return {
|
38
|
+
file: result.filePath,
|
39
|
+
complexity,
|
40
|
+
};
|
41
|
+
});
|
42
|
+
|
43
|
+
console.log(JSON.stringify(complexityResults));
|
44
|
+
} catch (error) {
|
45
|
+
console.error('Error during analysis:', error);
|
46
|
+
process.exit(1);
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
const files = JSON.parse(process.argv[2]);
|
51
|
+
analyzeComplexity(files);
|
@@ -0,0 +1,43 @@
|
|
1
|
+
function analyzeNumber(num) {
|
2
|
+
let result = '';
|
3
|
+
|
4
|
+
if (num < 0) {
|
5
|
+
result += 'negative ';
|
6
|
+
} else if (num > 0) {
|
7
|
+
result += 'positive ';
|
8
|
+
} else {
|
9
|
+
return 'zero';
|
10
|
+
}
|
11
|
+
|
12
|
+
if (num % 2 === 0) {
|
13
|
+
result += 'even ';
|
14
|
+
} else {
|
15
|
+
result += 'odd ';
|
16
|
+
}
|
17
|
+
|
18
|
+
if (num % 3 === 0) {
|
19
|
+
result += 'divisible by 3 ';
|
20
|
+
}
|
21
|
+
|
22
|
+
if (num % 5 === 0) {
|
23
|
+
result += 'divisible by 5 ';
|
24
|
+
}
|
25
|
+
|
26
|
+
if (isPrime(num)) {
|
27
|
+
result += 'prime ';
|
28
|
+
}
|
29
|
+
|
30
|
+
return result.trim();
|
31
|
+
}
|
32
|
+
|
33
|
+
function isPrime(num) {
|
34
|
+
if (num <= 1) return false;
|
35
|
+
for (let i = 2; i <= Math.sqrt(num); i++) {
|
36
|
+
if (num % i === 0) return false;
|
37
|
+
}
|
38
|
+
return true;
|
39
|
+
}
|
40
|
+
|
41
|
+
console.log(analyzeNumber(17));
|
42
|
+
console.log(analyzeNumber(30));
|
43
|
+
console.log(analyzeNumber(-7));
|
@@ -0,0 +1,26 @@
|
|
1
|
+
interface Person {
|
2
|
+
name: string;
|
3
|
+
age: number;
|
4
|
+
}
|
5
|
+
|
6
|
+
function createGreeting(person: Person): string {
|
7
|
+
let greeting = `Hello, ${person.name}!`;
|
8
|
+
|
9
|
+
if (person.age < 18) {
|
10
|
+
greeting += " You're still a minor.";
|
11
|
+
} else if (person.age >= 18 && person.age < 65) {
|
12
|
+
greeting += " You're an adult.";
|
13
|
+
} else {
|
14
|
+
greeting += " You're a senior citizen.";
|
15
|
+
}
|
16
|
+
|
17
|
+
return greeting;
|
18
|
+
}
|
19
|
+
|
20
|
+
const alice: Person = { name: 'Alice', age: 30 };
|
21
|
+
const bob: Person = { name: 'Bob', age: 17 };
|
22
|
+
const charlie: Person = { name: 'Charlie', age: 70 };
|
23
|
+
|
24
|
+
console.log(createGreeting(alice));
|
25
|
+
console.log(createGreeting(bob));
|
26
|
+
console.log(createGreeting(charlie));
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Erik T. Madsen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: flog
|
@@ -62,18 +62,27 @@ files:
|
|
62
62
|
- lib/churn_vs_complexity/churn.rb
|
63
63
|
- lib/churn_vs_complexity/cli.rb
|
64
64
|
- lib/churn_vs_complexity/complexity.rb
|
65
|
+
- lib/churn_vs_complexity/complexity/eslint_calculator.rb
|
65
66
|
- lib/churn_vs_complexity/complexity/flog_calculator.rb
|
66
67
|
- lib/churn_vs_complexity/complexity/pmd_calculator.rb
|
67
68
|
- lib/churn_vs_complexity/concurrent_calculator.rb
|
68
69
|
- lib/churn_vs_complexity/config.rb
|
69
70
|
- lib/churn_vs_complexity/engine.rb
|
70
71
|
- lib/churn_vs_complexity/file_selector.rb
|
72
|
+
- lib/churn_vs_complexity/git_date.rb
|
71
73
|
- lib/churn_vs_complexity/serializer.rb
|
72
74
|
- lib/churn_vs_complexity/version.rb
|
75
|
+
- package-lock.json
|
76
|
+
- tmp/eslint-support/complexity-calculator.js
|
77
|
+
- tmp/eslint-support/package.json
|
73
78
|
- tmp/pmd-support/ruleset.xml
|
74
79
|
- tmp/template/graph.html
|
75
80
|
- tmp/test-support/java/small-example/src/main/java/org/example/Main.java
|
76
81
|
- tmp/test-support/java/small-example/src/main/java/org/example/spice/Checker.java
|
82
|
+
- tmp/test-support/javascript/complex.js
|
83
|
+
- tmp/test-support/javascript/moderate.js
|
84
|
+
- tmp/test-support/javascript/simple.js
|
85
|
+
- tmp/test-support/javascript/typescript-example.ts
|
77
86
|
- tmp/test-support/txt/abc.txt
|
78
87
|
- tmp/test-support/txt/d.txt
|
79
88
|
- tmp/test-support/txt/ef.txt
|
@@ -89,7 +98,7 @@ licenses:
|
|
89
98
|
- MIT
|
90
99
|
metadata:
|
91
100
|
source_code_uri: https://github.com/beatmadsen/churn_vs_complexity
|
92
|
-
changelog_uri: https://github.com/beatmadsen/churn_vs_complexity/CHANGELOG.md
|
101
|
+
changelog_uri: https://github.com/beatmadsen/churn_vs_complexity/blob/main/CHANGELOG.md
|
93
102
|
rubygems_mfa_required: 'true'
|
94
103
|
post_install_message:
|
95
104
|
rdoc_options: []
|