churn_vs_complexity 1.4.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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