churn_vs_complexity 1.4.0 → 1.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +29 -14
- data/README.md +10 -4
- data/lib/churn_vs_complexity/churn.rb +9 -1
- data/lib/churn_vs_complexity/cli/main.rb +46 -0
- data/lib/churn_vs_complexity/cli/parser.rb +91 -0
- data/lib/churn_vs_complexity/cli.rb +11 -94
- data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +6 -0
- data/lib/churn_vs_complexity/complexity/pmd/files_calculator.rb +26 -0
- data/lib/churn_vs_complexity/complexity/pmd/folder_calculator.rb +20 -0
- data/lib/churn_vs_complexity/complexity/{pmd_calculator.rb → pmd.rb} +14 -21
- data/lib/churn_vs_complexity/complexity.rb +1 -1
- data/lib/churn_vs_complexity/complexity_validator.rb +14 -0
- data/lib/churn_vs_complexity/concurrent_calculator.rb +5 -3
- data/lib/churn_vs_complexity/delta/checker.rb +54 -0
- data/lib/churn_vs_complexity/delta/commit_hydrator.rb +22 -0
- data/lib/churn_vs_complexity/delta/complexity_annotator.rb +30 -0
- data/lib/churn_vs_complexity/delta/config.rb +50 -0
- data/lib/churn_vs_complexity/delta/factory.rb +22 -0
- data/lib/churn_vs_complexity/delta/multi_checker.rb +48 -0
- data/lib/churn_vs_complexity/delta/serializer.rb +69 -0
- data/lib/churn_vs_complexity/delta.rb +52 -0
- data/lib/churn_vs_complexity/engine.rb +1 -1
- data/lib/churn_vs_complexity/file_selector.rb +47 -4
- data/lib/churn_vs_complexity/git_strategy.rb +62 -0
- data/lib/churn_vs_complexity/language_validator.rb +9 -0
- data/lib/churn_vs_complexity/normal/config.rb +85 -0
- data/lib/churn_vs_complexity/normal/serializer/csv.rb +16 -0
- data/lib/churn_vs_complexity/normal/serializer/graph.rb +26 -0
- data/lib/churn_vs_complexity/normal/serializer/pass_through.rb +23 -0
- data/lib/churn_vs_complexity/normal/serializer/summary.rb +29 -0
- data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +56 -0
- data/lib/churn_vs_complexity/normal/serializer.rb +29 -0
- data/lib/churn_vs_complexity/normal.rb +45 -0
- data/lib/churn_vs_complexity/timetravel/config.rb +75 -0
- data/lib/churn_vs_complexity/timetravel/factory.rb +12 -0
- data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/quality_calculator.rb +2 -2
- data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/stats_calculator.rb +2 -2
- data/lib/churn_vs_complexity/{serializer/timetravel.rb → timetravel/serializer.rb} +6 -6
- data/lib/churn_vs_complexity/timetravel/traveller.rb +5 -11
- data/lib/churn_vs_complexity/timetravel/worktree.rb +30 -14
- data/lib/churn_vs_complexity/timetravel.rb +36 -39
- data/lib/churn_vs_complexity/version.rb +1 -1
- data/lib/churn_vs_complexity.rb +23 -7
- data/tmp/test-support/delta/ruby-summary.txt +50 -0
- data/tmp/test-support/delta/ruby.csv +12 -0
- metadata +38 -20
- data/.travis.yml +0 -7
- data/lib/churn_vs_complexity/config.rb +0 -159
- data/lib/churn_vs_complexity/serializer/csv.rb +0 -14
- data/lib/churn_vs_complexity/serializer/graph.rb +0 -24
- data/lib/churn_vs_complexity/serializer/pass_through.rb +0 -21
- data/lib/churn_vs_complexity/serializer/summary.rb +0 -27
- data/lib/churn_vs_complexity/serializer/summary_hash.rb +0 -54
- data/lib/churn_vs_complexity/serializer.rb +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 843cae8dad7f09344cbea477eb73ae3d9d57945df8a5955d93d6dfc820b5a8e1
|
4
|
+
data.tar.gz: 5066b98af73acde0f18b233fd12efe6858bb9977c4a9d553970c6ad859db98a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7af6d2c460fd9bb2cb78751d8a7f5cdd3ada48e9026e61ccbd3ef74a22c738bcfea4185984ffea2476a5d4b726f902c881edabf2baefa211884bfb129b59f80c
|
7
|
+
data.tar.gz: c3cf5e4dfd7bea34298fac9d7e2db1b7ffe58ad16acab25e9217dae0f84b43e2da7d89f147027f1ebb3fd52115538aba81a1f05bca5a0f7e13f0ad5d3a7b9aef
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,25 +1,40 @@
|
|
1
|
-
## [1.
|
1
|
+
## [1.5.2] - 2024-10-21
|
2
2
|
|
3
|
-
-
|
3
|
+
- Fixed bug where delta mode validations would fail when the commit was a non-sha value.
|
4
|
+
- Allow HEAD as specified commit in delta mode
|
4
5
|
|
5
|
-
## [1.1
|
6
|
+
## [1.5.1] - 2024-10-15
|
6
7
|
|
7
|
-
-
|
8
|
-
- Introduce `--month`, `--quarter`, and `--year` short-hand flags to calculate churn for different time periods relative to the most recent commit
|
8
|
+
- Fix bug where worktree checkout silently failed
|
9
9
|
|
10
|
-
## [1.2.0] - 2024-09-20
|
11
10
|
|
12
|
-
|
13
|
-
|
14
|
-
-
|
11
|
+
## [1.5.0] - 2024-10-14
|
12
|
+
|
13
|
+
- Added delta mode to annotate changes in individual commits with complexity
|
14
|
+
- Moving PMD cache to the OS temp directory
|
15
|
+
|
16
|
+
## [1.4.0] - 2024-10-10
|
17
|
+
|
18
|
+
- Added timetravel mode to visualise code quality over time
|
19
|
+
- Added alpha, beta, and gamma scores to summaries
|
20
|
+
- Fixed broken Ruby complexity calculation
|
15
21
|
|
16
22
|
## [1.3.0] - 2024-09-26
|
17
23
|
|
18
|
-
-
|
24
|
+
- Added support for javascript and typescript complexity calculation using eslint
|
25
|
+
|
26
|
+
## [1.2.0] - 2024-09-20
|
27
|
+
|
28
|
+
- Fixed bug in CLI where new flags and `--since` would not be recognized
|
29
|
+
- Improved selection of observations included in the output
|
30
|
+
- Fixed calculation of churn that would never be zero
|
19
31
|
- Fixed behavior when --since or short-hand flags were not provided
|
20
32
|
|
21
|
-
## [1.
|
33
|
+
## [1.1.0] - 2024-09-20
|
22
34
|
|
23
|
-
-
|
24
|
-
-
|
25
|
-
|
35
|
+
- Introduced `--summary` flag to output summary statistics for churn and complexity
|
36
|
+
- Introduced `--month`, `--quarter`, and `--year` short-hand flags to calculate churn for different time periods relative to the most recent commit
|
37
|
+
|
38
|
+
## [1.0.0] - 2024-06-07
|
39
|
+
|
40
|
+
- Initial release
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[![Gem Version](https://badge.fury.io/rb/churn_vs_complexity.svg)](https://badge.fury.io/rb/churn_vs_complexity)
|
2
|
+
|
1
3
|
# ChurnVsComplexity
|
2
4
|
|
3
5
|
A tool to visualise code complexity in a project and help direct refactoring efforts.
|
@@ -22,9 +24,11 @@ Or install it yourself as:
|
|
22
24
|
|
23
25
|
$ gem install churn_vs_complexity
|
24
26
|
|
25
|
-
This gem depends on git for churn analysis
|
27
|
+
This gem depends on git for churn analysis.
|
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`.
|
26
30
|
|
27
|
-
|
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.
|
28
32
|
|
29
33
|
## Usage
|
30
34
|
|
@@ -45,10 +49,10 @@ Usage: churn_vs_complexity [options] folder
|
|
45
49
|
-q, --quarter Calculate churn for the quarter leading up to the most recent commit
|
46
50
|
-y, --year Calculate churn for the year leading up to the most recent commit
|
47
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.
|
48
53
|
--dry-run Echo the chosen options from the CLI
|
49
54
|
-h, --help Display help
|
50
|
-
|
51
|
-
|
55
|
+
--version Display version
|
52
56
|
```
|
53
57
|
|
54
58
|
Note that when using the `--timetravel` mode, the semantics of some flags are subtly different from normal mode:
|
@@ -71,6 +75,8 @@ Summary points in timetravel mode instead include an alpha score, which is the s
|
|
71
75
|
|
72
76
|
`churn_vs_complexity --java -m --since 2019-03-01 --timetravel 30 --graph my_java_project > ~/Desktop/timetravel-after-1st-march-2019.html`
|
73
77
|
|
78
|
+
`churn_vs_complexity --delta 1496402e81e68e86c5ac240559099fbe581a9a2g --delta 2845296758861773778d70d96328a5f2a1a9e933 --js --summary my_javascript_project > ~/Desktop/interesting-commits.txt`
|
79
|
+
|
74
80
|
## Development
|
75
81
|
|
76
82
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
@@ -4,6 +4,14 @@ require 'git'
|
|
4
4
|
|
5
5
|
module ChurnVsComplexity
|
6
6
|
module Churn
|
7
|
+
module Disabled
|
8
|
+
def self.calculate(*)
|
9
|
+
raise Error, 'Churn is disabled'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.date_of_latest_commit(*) = :disabled
|
13
|
+
end
|
14
|
+
|
7
15
|
module GitCalculator
|
8
16
|
class << self
|
9
17
|
def calculate(folder:, file:, since:)
|
@@ -11,7 +19,7 @@ module ChurnVsComplexity
|
|
11
19
|
earliest_date = [date_of_first_commit(folder:), since].max
|
12
20
|
formatted_date = earliest_date.strftime('%Y-%m-%d')
|
13
21
|
cmd = %(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
|
14
|
-
|
22
|
+
`(#{cmd}) 2>/dev/null`.to_i
|
15
23
|
end
|
16
24
|
|
17
25
|
def date_of_latest_commit(folder:)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module ChurnVsComplexity
|
6
|
+
module CLI
|
7
|
+
module Main
|
8
|
+
class << self
|
9
|
+
def run!(options, folder)
|
10
|
+
validate_folder!(folder)
|
11
|
+
validate_options!(options)
|
12
|
+
config = config(options)
|
13
|
+
config.validate!
|
14
|
+
|
15
|
+
config.checker.check(folder:)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate_folder!(folder)
|
21
|
+
raise ValidationError, 'No folder selected. Use --help for usage information.' if folder.nil? || folder.empty?
|
22
|
+
raise ValidationError, "Folder #{folder} does not exist" unless File.directory?(folder)
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_options!(options)
|
26
|
+
raise ValidationError, 'No options selected. Use --help for usage information.' if options.empty?
|
27
|
+
raise ValidationError, 'No language selected. Use --help for usage information.' if options[:language].nil?
|
28
|
+
|
29
|
+
return unless options[:serializer].nil?
|
30
|
+
|
31
|
+
raise ValidationError, 'No serializer selected. Use --help for usage information.'
|
32
|
+
end
|
33
|
+
|
34
|
+
def config(options)
|
35
|
+
config_class =
|
36
|
+
case options[:mode]
|
37
|
+
when :timetravel then Timetravel::Config
|
38
|
+
when :delta then Delta::Config
|
39
|
+
else Normal::Config
|
40
|
+
end
|
41
|
+
config_class.new(**options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module ChurnVsComplexity
|
6
|
+
module CLI
|
7
|
+
module Parser
|
8
|
+
def self.create
|
9
|
+
options = { excluded: [] }
|
10
|
+
parser = OptionParser.new do |opts|
|
11
|
+
opts.banner = 'Usage: churn_vs_complexity [options] folder'
|
12
|
+
|
13
|
+
opts.on('--java', 'Check complexity of java classes') do
|
14
|
+
options[:language] = :java
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on('--ruby', 'Check complexity of ruby files') do
|
18
|
+
options[:language] = :ruby
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on('--js', '--ts', '--javascript', '--typescript',
|
22
|
+
'Check complexity of javascript and typescript files',) do
|
23
|
+
options[:language] = :javascript
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.on('--csv', 'Format output as CSV') do
|
27
|
+
options[:serializer] = :csv
|
28
|
+
end
|
29
|
+
|
30
|
+
opts.on('--graph', 'Format output as HTML page with Churn vs Complexity graph') do
|
31
|
+
options[:serializer] = :graph
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on('--summary', 'Output summary statistics (mean and median) for churn and complexity') do
|
35
|
+
options[:serializer] = :summary
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on('--excluded PATTERN',
|
39
|
+
'Exclude file paths including this string. Can be used multiple times.',) do |value|
|
40
|
+
options[:excluded] << value
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on('--since YYYY-MM-DD',
|
44
|
+
'Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date',) do |value|
|
45
|
+
options[:since] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on('-m', '--month', 'Calculate churn for the month leading up to the most recent commit') do
|
49
|
+
options[:relative_period] = :month
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on('-q', '--quarter', 'Calculate churn for the quarter leading up to the most recent commit') do
|
53
|
+
options[:relative_period] = :quarter
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('-y', '--year', 'Calculate churn for the year leading up to the most recent commit') do
|
57
|
+
options[:relative_period] = :year
|
58
|
+
end
|
59
|
+
|
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
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on('--dry-run', 'Echo the chosen options from the CLI') do
|
73
|
+
puts options
|
74
|
+
exit
|
75
|
+
end
|
76
|
+
|
77
|
+
opts.on('-h', '--help', 'Display help') do
|
78
|
+
puts opts
|
79
|
+
exit
|
80
|
+
end
|
81
|
+
|
82
|
+
opts.on('--version', 'Display version') do
|
83
|
+
puts ChurnVsComplexity::VERSION
|
84
|
+
exit
|
85
|
+
end
|
86
|
+
end
|
87
|
+
[parser, options]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -1,101 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
require 'optparse'
|
3
|
+
require_relative 'cli/parser'
|
4
|
+
require_relative 'cli/main'
|
6
5
|
|
7
6
|
module ChurnVsComplexity
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
opts.on('--java', 'Check complexity of java classes') do
|
18
|
-
options[:language] = :java
|
19
|
-
end
|
20
|
-
|
21
|
-
opts.on('--ruby', 'Check complexity of ruby files') do
|
22
|
-
options[:language] = :ruby
|
23
|
-
end
|
24
|
-
|
25
|
-
opts.on('--js', '--ts', '--javascript', '--typescript',
|
26
|
-
'Check complexity of javascript and typescript files',) do
|
27
|
-
options[:language] = :javascript
|
28
|
-
end
|
29
|
-
|
30
|
-
opts.on('--csv', 'Format output as CSV') do
|
31
|
-
options[:serializer] = :csv
|
32
|
-
end
|
33
|
-
|
34
|
-
opts.on('--graph', 'Format output as HTML page with Churn vs Complexity graph') do
|
35
|
-
options[:serializer] = :graph
|
36
|
-
end
|
37
|
-
|
38
|
-
opts.on('--summary', 'Output summary statistics (mean and median) for churn and complexity') do
|
39
|
-
options[:serializer] = :summary
|
40
|
-
end
|
41
|
-
|
42
|
-
opts.on('--excluded PATTERN',
|
43
|
-
'Exclude file paths including this string. Can be used multiple times.',) do |value|
|
44
|
-
options[:excluded] << value
|
45
|
-
end
|
46
|
-
|
47
|
-
opts.on('--since YYYY-MM-DD',
|
48
|
-
'Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date',) do |value|
|
49
|
-
options[:since] = value
|
50
|
-
end
|
51
|
-
|
52
|
-
opts.on('-m', '--month', 'Calculate churn for the month leading up to the most recent commit') do
|
53
|
-
options[:relative_period] = :month
|
54
|
-
end
|
55
|
-
|
56
|
-
opts.on('-q', '--quarter', 'Calculate churn for the quarter leading up to the most recent commit') do
|
57
|
-
options[:relative_period] = :quarter
|
58
|
-
end
|
59
|
-
|
60
|
-
opts.on('-y', '--year', 'Calculate churn for the year leading up to the most recent commit') do
|
61
|
-
options[:relative_period] = :year
|
62
|
-
end
|
63
|
-
|
64
|
-
opts.on('--timetravel N',
|
65
|
-
'Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since',) do |value|
|
66
|
-
options[:mode] = :timetravel
|
67
|
-
options[:jump_days] = value.to_i
|
68
|
-
end
|
69
|
-
|
70
|
-
opts.on('--dry-run', 'Echo the chosen options from the CLI') do
|
71
|
-
puts options
|
72
|
-
exit
|
73
|
-
end
|
74
|
-
|
75
|
-
opts.on('-h', '--help', 'Display help') do
|
76
|
-
puts opts
|
77
|
-
exit
|
78
|
-
end
|
79
|
-
end.parse!
|
80
|
-
|
81
|
-
# First argument that is not an option is the folder
|
82
|
-
folder = ARGV.first
|
83
|
-
|
84
|
-
raise Error, 'No folder selected. Use --help for usage information.' if folder.nil? || folder.empty?
|
85
|
-
|
86
|
-
# Verify that folder exists
|
87
|
-
raise Error, "Folder #{folder} does not exist" unless File.directory?(folder)
|
88
|
-
|
89
|
-
raise Error, 'No options selected. Use --help for usage information.' if options.empty?
|
90
|
-
|
91
|
-
config = Config.new(**options)
|
92
|
-
|
93
|
-
config.validate!
|
94
|
-
|
95
|
-
if options[:mode] == :timetravel
|
96
|
-
puts config.timetravel.go(folder:)
|
97
|
-
else
|
98
|
-
puts config.to_engine.check(folder:)
|
7
|
+
module CLI
|
8
|
+
class << self
|
9
|
+
def run!
|
10
|
+
parser, options = Parser.create
|
11
|
+
parser.parse!
|
12
|
+
# First argument that is not an option is the folder
|
13
|
+
folder = ARGV.first
|
14
|
+
|
15
|
+
puts Main.run!(options, folder)
|
99
16
|
end
|
100
17
|
end
|
101
18
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Complexity
|
5
|
+
module PMD
|
6
|
+
class FilesCalculator
|
7
|
+
def initialize(cache_components:)
|
8
|
+
@cache_components = cache_components
|
9
|
+
end
|
10
|
+
|
11
|
+
def folder_based? = false
|
12
|
+
|
13
|
+
def calculate(files:)
|
14
|
+
return Parser.empty_result if files.empty?
|
15
|
+
|
16
|
+
cache_path = PMD.resolve_cache_path(*@cache_components)
|
17
|
+
files_arg = files.map { |file| "-d #{file}" }.join(' ')
|
18
|
+
command = "pmd check #{files_arg} -R #{PMD.resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{cache_path} 2>/dev/null"
|
19
|
+
output = `#{command}`
|
20
|
+
|
21
|
+
Parser.new.parse(output)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Complexity
|
5
|
+
module PMD
|
6
|
+
module FolderCalculator
|
7
|
+
class << self
|
8
|
+
def folder_based? = true
|
9
|
+
|
10
|
+
def calculate(folder:)
|
11
|
+
cache_path = PMD.resolve_cache_path(folder)
|
12
|
+
output = `pmd check -d #{folder} -R #{PMD.resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{cache_path} 2>/dev/null`
|
13
|
+
|
14
|
+
Parser.new.parse(output)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,19 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'pmd/folder_calculator'
|
4
|
+
require_relative 'pmd/files_calculator'
|
5
|
+
|
3
6
|
module ChurnVsComplexity
|
4
7
|
module Complexity
|
5
|
-
module
|
8
|
+
module PMD
|
6
9
|
CONCURRENCY = Etc.nprocessors
|
7
10
|
|
8
11
|
class << self
|
9
|
-
def
|
12
|
+
def resolve_ruleset_path
|
13
|
+
ruleset_path = File.join(gem_root, 'tmp', 'pmd-support', 'ruleset.xml')
|
14
|
+
raise "ruleset.xml not found in #{ruleset_path}" unless File.exist?(ruleset_path)
|
10
15
|
|
11
|
-
|
12
|
-
|
13
|
-
output = `pmd check -d #{folder} -R #{resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{cache_path}`
|
14
|
-
File.delete(cache_path)
|
16
|
+
ruleset_path
|
17
|
+
end
|
15
18
|
|
16
|
-
|
19
|
+
def resolve_cache_path(*components)
|
20
|
+
File.join(ChurnVsComplexity.tmp_dir_path(*components), 'pmd', 'pmd-cache')
|
17
21
|
end
|
18
22
|
|
19
23
|
def check_dependencies!
|
@@ -24,23 +28,12 @@ module ChurnVsComplexity
|
|
24
28
|
|
25
29
|
private
|
26
30
|
|
27
|
-
def
|
28
|
-
ruleset_path = File.join(gem_root, 'tmp', 'pmd-support', 'ruleset.xml')
|
29
|
-
raise "ruleset.xml not found in #{ruleset_path}" unless File.exist?(ruleset_path)
|
30
|
-
|
31
|
-
ruleset_path
|
32
|
-
end
|
33
|
-
|
34
|
-
def resolve_cache_path
|
35
|
-
File.join(gem_root, 'tmp', 'pmd-support', "pmd-cache-#{Process.pid}")
|
36
|
-
end
|
37
|
-
|
38
|
-
def gem_root
|
39
|
-
File.expand_path('../../..', __dir__)
|
40
|
-
end
|
31
|
+
def gem_root = ROOT_PATH
|
41
32
|
end
|
42
33
|
|
43
34
|
class Parser
|
35
|
+
def self.empty_result = {}
|
36
|
+
|
44
37
|
def parse(output)
|
45
38
|
doc = JSON.parse(output)
|
46
39
|
doc['files'].each_with_object({}) do |file, result|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module ComplexityValidator
|
5
|
+
def self.validate!(language)
|
6
|
+
case language
|
7
|
+
when :java
|
8
|
+
Complexity::PMD.check_dependencies!
|
9
|
+
when :javascript
|
10
|
+
Complexity::ESLintCalculator.check_dependencies!
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -14,8 +14,10 @@ module ChurnVsComplexity
|
|
14
14
|
|
15
15
|
def calculate(folder:, files:, since:)
|
16
16
|
latest_commit_date = @churn.date_of_latest_commit(folder:)
|
17
|
-
|
18
|
-
|
17
|
+
unless latest_commit_date == :disabled
|
18
|
+
@git_period = GitDate.git_period(since, latest_commit_date)
|
19
|
+
schedule_churn_calculation(folder, files[:included], @git_period.effective_start_date)
|
20
|
+
end
|
19
21
|
calculate_complexity(folder, files)
|
20
22
|
await_results
|
21
23
|
combine_results
|
@@ -49,7 +51,7 @@ module ChurnVsComplexity
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def await_results
|
52
|
-
@threads
|
54
|
+
@threads&.each(&:join)
|
53
55
|
rescue StandardError => e
|
54
56
|
raise Error, "Failed to caculate churn: #{e.message}"
|
55
57
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
class Checker
|
6
|
+
def initialize(serializer:, factory:, commit:, language:, excluded:, data_isolation_id: 0)
|
7
|
+
@serializer = serializer
|
8
|
+
@excluded = excluded
|
9
|
+
@factory = factory
|
10
|
+
@commit = commit
|
11
|
+
@language = language
|
12
|
+
@data_isolation_id = data_isolation_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def check(folder:)
|
16
|
+
raise Error, 'Invalid commit' unless valid_commit?(folder:)
|
17
|
+
|
18
|
+
worktree = setup_worktree(folder:)
|
19
|
+
|
20
|
+
changes = @factory.git_strategy(folder: worktree.folder).changes(commit: @commit)
|
21
|
+
result = commit_summary(folder:)
|
22
|
+
unless changes.empty?
|
23
|
+
ComplexityAnnotator.new(factory: @factory, changes:)
|
24
|
+
.enhance(worktree_folder: worktree.folder, language: @language, excluded: @excluded, commit: @commit)
|
25
|
+
result[:changes] = changes
|
26
|
+
end
|
27
|
+
|
28
|
+
@serializer.serialize(result)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def setup_worktree(folder:)
|
34
|
+
worktree = @factory.worktree(root_folder: folder, git_strategy: @factory.git_strategy(folder:),
|
35
|
+
data_isolation_id: @data_isolation_id,)
|
36
|
+
worktree.prepare
|
37
|
+
worktree.checkout(@commit)
|
38
|
+
|
39
|
+
worktree
|
40
|
+
end
|
41
|
+
|
42
|
+
def commit_summary(folder:)
|
43
|
+
CommitHydrator.new(
|
44
|
+
git_strategy: @factory.git_strategy(folder:),
|
45
|
+
serializer: @serializer,
|
46
|
+
).hydrate(@commit)
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid_commit?(folder:)
|
50
|
+
@factory.git_strategy(folder:).valid_commit?(commit: @commit)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
class CommitHydrator
|
6
|
+
def initialize(git_strategy:, serializer:)
|
7
|
+
@git_strategy = git_strategy
|
8
|
+
@serializer = serializer
|
9
|
+
end
|
10
|
+
|
11
|
+
def hydrate(commit_sha)
|
12
|
+
commit = @git_strategy.object(commit_sha)
|
13
|
+
summary = { commit: commit.sha }
|
14
|
+
if @serializer.respond_to?(:has_commit_summary?) && @serializer.has_commit_summary?
|
15
|
+
parent, next_commit = @git_strategy.surrounding(commit:)
|
16
|
+
summary.merge!(parent:, next_commit:)
|
17
|
+
end
|
18
|
+
summary
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ChurnVsComplexity
|
4
|
+
module Delta
|
5
|
+
class ComplexityAnnotator
|
6
|
+
def initialize(factory:, changes:)
|
7
|
+
@factory = factory
|
8
|
+
@changes = changes
|
9
|
+
end
|
10
|
+
|
11
|
+
def enhance(worktree_folder:, language:, excluded:, commit:)
|
12
|
+
@changes.each do |change|
|
13
|
+
change[:full_path] = File.join(worktree_folder, change[:path])
|
14
|
+
end
|
15
|
+
|
16
|
+
files = @changes.reject { |change| change[:type] == :deleted }.map { |change| change[:full_path] }
|
17
|
+
|
18
|
+
engine = @factory.engine(cache_components: [worktree_folder, commit], language:, excluded:, files:)
|
19
|
+
|
20
|
+
values_by_file = engine.check(folder: worktree_folder)[:values_by_file]
|
21
|
+
|
22
|
+
valid_extensions = FileSelector.extensions(language)
|
23
|
+
@changes.select! { |change| valid_extensions.any? { |ext| change[:path].end_with?(ext) } }
|
24
|
+
@changes.each do |annotated_file|
|
25
|
+
annotated_file[:complexity] = values_by_file.dig(annotated_file[:full_path], 1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|