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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +29 -14
  4. data/README.md +10 -4
  5. data/lib/churn_vs_complexity/churn.rb +9 -1
  6. data/lib/churn_vs_complexity/cli/main.rb +46 -0
  7. data/lib/churn_vs_complexity/cli/parser.rb +91 -0
  8. data/lib/churn_vs_complexity/cli.rb +11 -94
  9. data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +6 -0
  10. data/lib/churn_vs_complexity/complexity/pmd/files_calculator.rb +26 -0
  11. data/lib/churn_vs_complexity/complexity/pmd/folder_calculator.rb +20 -0
  12. data/lib/churn_vs_complexity/complexity/{pmd_calculator.rb → pmd.rb} +14 -21
  13. data/lib/churn_vs_complexity/complexity.rb +1 -1
  14. data/lib/churn_vs_complexity/complexity_validator.rb +14 -0
  15. data/lib/churn_vs_complexity/concurrent_calculator.rb +5 -3
  16. data/lib/churn_vs_complexity/delta/checker.rb +54 -0
  17. data/lib/churn_vs_complexity/delta/commit_hydrator.rb +22 -0
  18. data/lib/churn_vs_complexity/delta/complexity_annotator.rb +30 -0
  19. data/lib/churn_vs_complexity/delta/config.rb +50 -0
  20. data/lib/churn_vs_complexity/delta/factory.rb +22 -0
  21. data/lib/churn_vs_complexity/delta/multi_checker.rb +48 -0
  22. data/lib/churn_vs_complexity/delta/serializer.rb +69 -0
  23. data/lib/churn_vs_complexity/delta.rb +52 -0
  24. data/lib/churn_vs_complexity/engine.rb +1 -1
  25. data/lib/churn_vs_complexity/file_selector.rb +47 -4
  26. data/lib/churn_vs_complexity/git_strategy.rb +62 -0
  27. data/lib/churn_vs_complexity/language_validator.rb +9 -0
  28. data/lib/churn_vs_complexity/normal/config.rb +85 -0
  29. data/lib/churn_vs_complexity/normal/serializer/csv.rb +16 -0
  30. data/lib/churn_vs_complexity/normal/serializer/graph.rb +26 -0
  31. data/lib/churn_vs_complexity/normal/serializer/pass_through.rb +23 -0
  32. data/lib/churn_vs_complexity/normal/serializer/summary.rb +29 -0
  33. data/lib/churn_vs_complexity/normal/serializer/summary_hash.rb +56 -0
  34. data/lib/churn_vs_complexity/normal/serializer.rb +29 -0
  35. data/lib/churn_vs_complexity/normal.rb +45 -0
  36. data/lib/churn_vs_complexity/timetravel/config.rb +75 -0
  37. data/lib/churn_vs_complexity/timetravel/factory.rb +12 -0
  38. data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/quality_calculator.rb +2 -2
  39. data/lib/churn_vs_complexity/{serializer/timetravel → timetravel/serializer}/stats_calculator.rb +2 -2
  40. data/lib/churn_vs_complexity/{serializer/timetravel.rb → timetravel/serializer.rb} +6 -6
  41. data/lib/churn_vs_complexity/timetravel/traveller.rb +5 -11
  42. data/lib/churn_vs_complexity/timetravel/worktree.rb +30 -14
  43. data/lib/churn_vs_complexity/timetravel.rb +36 -39
  44. data/lib/churn_vs_complexity/version.rb +1 -1
  45. data/lib/churn_vs_complexity.rb +23 -7
  46. data/tmp/test-support/delta/ruby-summary.txt +50 -0
  47. data/tmp/test-support/delta/ruby.csv +12 -0
  48. metadata +38 -20
  49. data/.travis.yml +0 -7
  50. data/lib/churn_vs_complexity/config.rb +0 -159
  51. data/lib/churn_vs_complexity/serializer/csv.rb +0 -14
  52. data/lib/churn_vs_complexity/serializer/graph.rb +0 -24
  53. data/lib/churn_vs_complexity/serializer/pass_through.rb +0 -21
  54. data/lib/churn_vs_complexity/serializer/summary.rb +0 -27
  55. data/lib/churn_vs_complexity/serializer/summary_hash.rb +0 -54
  56. data/lib/churn_vs_complexity/serializer.rb +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7484ff3a1c015738808226a78087017f6b7aff5ce42d15879023f32df5648717
4
- data.tar.gz: ad3bdeff5ba32e9d7f414b45173d8928b1c463313557202b96aaf5fdcf059109
3
+ metadata.gz: 843cae8dad7f09344cbea477eb73ae3d9d57945df8a5955d93d6dfc820b5a8e1
4
+ data.tar.gz: 5066b98af73acde0f18b233fd12efe6858bb9977c4a9d553970c6ad859db98a9
5
5
  SHA512:
6
- metadata.gz: a56e26296acfff22e755c414cab9ee36923e4ccd7181d67691f55feb8f99b5aa738f5d2b7645006f25c632efa346fb535d7fe04923443dedf607772ce1a21323
7
- data.tar.gz: f406ee696facf7708e792b67ed8b463f1554365b3939deb4bde40c9fab88dfe891997f611727559a283103c5b7234f71d78d5fe108731628cb3fd8a782a73ce5
6
+ metadata.gz: 7af6d2c460fd9bb2cb78751d8a7f5cdd3ada48e9026e61ccbd3ef74a22c738bcfea4185984ffea2476a5d4b726f902c881edabf2baefa211884bfb129b59f80c
7
+ data.tar.gz: c3cf5e4dfd7bea34298fac9d7e2db1b7ffe58ad16acab25e9217dae0f84b43e2da7d89f147027f1ebb3fd52115538aba81a1f05bca5a0f7e13f0ad5d3a7b9aef
data/.rubocop.yml CHANGED
@@ -9,6 +9,9 @@ Layout/LineLength:
9
9
  Style/Documentation:
10
10
  Enabled: false
11
11
 
12
+ Style/ModuleFunction:
13
+ Enabled: false
14
+
12
15
  Style/TrailingCommaInArrayLiteral:
13
16
  Enabled: true
14
17
  EnforcedStyleForMultiline: consistent_comma
data/CHANGELOG.md CHANGED
@@ -1,25 +1,40 @@
1
- ## [1.0.0] - 2024-06-07
1
+ ## [1.5.2] - 2024-10-21
2
2
 
3
- - Initial release
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.0] - 2024-09-20
6
+ ## [1.5.1] - 2024-10-15
6
7
 
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
8
+ - Fix bug where worktree checkout silently failed
9
9
 
10
- ## [1.2.0] - 2024-09-20
11
10
 
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
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
- - Add support for javascript and typescript complexity calculation using eslint
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.4.0] - 2024-10-10
33
+ ## [1.1.0] - 2024-09-20
22
34
 
23
- - Add timetravel mode to visualise code quality over time
24
- - Add alpha, beta, and gamma scores to summaries
25
- - Fixed broken Ruby complexity calculation
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 and [PMD](https://pmd.github.io) for complexity analysis of JVM based languages.
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
- 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`.
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
- `#{cmd}`.to_i
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
- require 'date'
4
- require 'time'
5
- require 'optparse'
3
+ require_relative 'cli/parser'
4
+ require_relative 'cli/main'
6
5
 
7
6
  module ChurnVsComplexity
8
- class CLI
9
- def self.run!
10
- # Create an options hash to store parsed options
11
- options = { excluded: [] }
12
-
13
- # Initialize OptionParser
14
- OptionParser.new do |opts|
15
- opts.banner = 'Usage: churn_vs_complexity [options] folder'
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
@@ -23,6 +23,12 @@ module ChurnVsComplexity
23
23
  end
24
24
  end
25
25
 
26
+ def check_dependencies!
27
+ `npm --version`
28
+ rescue Errno::ENOENT
29
+ raise Error, 'Needs node and npm installed'
30
+ end
31
+
26
32
  private
27
33
 
28
34
  def gem_root
@@ -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 PMDCalculator
8
+ module PMD
6
9
  CONCURRENCY = Etc.nprocessors
7
10
 
8
11
  class << self
9
- def folder_based? = true
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
- def calculate(folder:)
12
- cache_path = resolve_cache_path
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
- Parser.new.parse(output)
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 resolve_ruleset_path
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|
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'complexity/pmd_calculator'
3
+ require_relative 'complexity/pmd'
4
4
  require_relative 'complexity/flog_calculator'
5
5
  require_relative 'complexity/eslint_calculator'
6
6
 
@@ -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
- @git_period = GitDate.git_period(since, latest_commit_date)
18
- schedule_churn_calculation(folder, files[:included], @git_period.effective_start_date)
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.each(&:join)
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