churn_vs_complexity 1.2.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +19 -2
  4. data/TODO +11 -0
  5. data/bin/churn_vs_complexity +5 -0
  6. data/lib/churn_vs_complexity/churn.rb +7 -2
  7. data/lib/churn_vs_complexity/cli.rb +21 -5
  8. data/lib/churn_vs_complexity/complexity/eslint_calculator.rb +34 -0
  9. data/lib/churn_vs_complexity/complexity/flog_calculator.rb +7 -6
  10. data/lib/churn_vs_complexity/complexity/pmd_calculator.rb +5 -2
  11. data/lib/churn_vs_complexity/complexity.rb +1 -0
  12. data/lib/churn_vs_complexity/concurrent_calculator.rb +2 -4
  13. data/lib/churn_vs_complexity/config.rb +81 -17
  14. data/lib/churn_vs_complexity/file_selector.rb +12 -1
  15. data/lib/churn_vs_complexity/git_date.rb +8 -1
  16. data/lib/churn_vs_complexity/serializer/csv.rb +14 -0
  17. data/lib/churn_vs_complexity/serializer/graph.rb +24 -0
  18. data/lib/churn_vs_complexity/serializer/pass_through.rb +21 -0
  19. data/lib/churn_vs_complexity/serializer/summary.rb +27 -0
  20. data/lib/churn_vs_complexity/serializer/summary_hash.rb +54 -0
  21. data/lib/churn_vs_complexity/serializer/timetravel/quality_calculator.rb +38 -0
  22. data/lib/churn_vs_complexity/serializer/timetravel/stats_calculator.rb +60 -0
  23. data/lib/churn_vs_complexity/serializer/timetravel.rb +103 -0
  24. data/lib/churn_vs_complexity/serializer.rb +7 -60
  25. data/lib/churn_vs_complexity/timetravel/traveller.rb +66 -0
  26. data/lib/churn_vs_complexity/timetravel/worktree.rb +56 -0
  27. data/lib/churn_vs_complexity/timetravel.rb +70 -0
  28. data/lib/churn_vs_complexity/version.rb +1 -1
  29. data/lib/churn_vs_complexity.rb +2 -0
  30. data/package-lock.json +6 -0
  31. data/tmp/eslint-support/complexity-calculator.js +51 -0
  32. data/tmp/eslint-support/package.json +11 -0
  33. data/tmp/template/graph.html +1 -4
  34. data/tmp/template/timetravel_graph.html +100 -0
  35. data/tmp/test-support/javascript/complex.js +43 -0
  36. data/tmp/test-support/javascript/moderate.js +12 -0
  37. data/tmp/test-support/javascript/simple.js +5 -0
  38. data/tmp/test-support/javascript/typescript-example.ts +26 -0
  39. data/tmp/timetravel/.keep +0 -0
  40. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d748bfb7d21087a5fbcb360dbc05dcd46b5ec05bf2c2fcc542e2e25368084d78
4
- data.tar.gz: 0d160c582aee2696823061dd7623cf17e9b537d798ae31502eb30b065a61391d
3
+ metadata.gz: 7484ff3a1c015738808226a78087017f6b7aff5ce42d15879023f32df5648717
4
+ data.tar.gz: ad3bdeff5ba32e9d7f414b45173d8928b1c463313557202b96aaf5fdcf059109
5
5
  SHA512:
6
- metadata.gz: f521d39fddad8e8adf05ab0f21d8b8adcc2460f9a57c553b90c73eb60451aafed19ab11977768d494987ff72838c856dd04c3910781ec0da34f53d82d18d9d4e
7
- data.tar.gz: 3ab17db013540128bd428edb54cf0615f47ce50caa19d1ad418261c20efde494bb36ea8e84408e0ca330f7721013a9e8958f3cd5a16565636186508e59bf7360
6
+ metadata.gz: a56e26296acfff22e755c414cab9ee36923e4ccd7181d67691f55feb8f99b5aa738f5d2b7645006f25c632efa346fb535d7fe04923443dedf607772ce1a21323
7
+ data.tar.gz: f406ee696facf7708e792b67ed8b463f1554365b3939deb4bde40c9fab88dfe891997f611727559a283103c5b7234f71d78d5fe108731628cb3fd8a782a73ce5
data/CHANGELOG.md CHANGED
@@ -12,3 +12,14 @@
12
12
  - Fix bug in CLI where new flags and `--since` would not be recognized
13
13
  - Improve selection of observations included in the output
14
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
20
+
21
+ ## [1.4.0] - 2024-10-10
22
+
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
data/README.md CHANGED
@@ -4,7 +4,7 @@ A tool to visualise code complexity in a project and help direct refactoring eff
4
4
 
5
5
  Inspired by [Michael Feathers' article "Getting Empirical about Refactoring"](https://www.agileconnection.com/article/getting-empirical-about-refactoring) and the gem [turbulence](https://rubygems.org/gems/turbulence) by Chad Fowler and others.
6
6
 
7
- This gem was built primarily to support analysis of Java and Ruby repositories, but it can easily be extended.
7
+ This gem currently supports analysis of Java, Ruby, JavaScript, and TypeScript repositories, but it can easily be extended.
8
8
 
9
9
  ## Installation
10
10
 
@@ -34,17 +34,33 @@ Execute the `churn_vs_complexity` with the applicable arguments. Output in the r
34
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
39
41
  --summary Output summary statistics (mean and median) for churn and complexity
40
42
  --excluded PATTERN Exclude file paths including this string. Can be used multiple times.
41
- --since YYYY-MM-DD Calculate churn after this date
43
+ --since YYYY-MM-DD Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date
42
44
  -m, --month Calculate churn for the month leading up to the most recent commit
43
45
  -q, --quarter Calculate churn for the quarter leading up to the most recent commit
44
46
  -y, --year Calculate churn for the year leading up to the most recent commit
47
+ --timetravel N Calculate summary for all commits at intervals of N days throughout project history or from the date specified with --since
48
+ --dry-run Echo the chosen options from the CLI
45
49
  -h, --help Display help
50
+
51
+
46
52
  ```
47
53
 
54
+ Note that when using the `--timetravel` mode, the semantics of some flags are subtly different from normal mode:
55
+
56
+ * `--since YYYY-MM-DD`: Calculate summaries from this date
57
+ * `--month`, `--quarter`, `--year`: Calculate churn for the period leading up to each commit being summarised
58
+
59
+ Timetravel analysis can take many minutes for old and large repositories.
60
+
61
+ Summaries in normal mode include a gamma score, which is an unnormalised harmonic mean of churn and complexity. This allows for comparison of summaries across different projects with the same language, or over time for a single project.
62
+
63
+ Summary points in timetravel mode instead include an alpha score, which is the same harmonic mean of churn and complexity, where churn and complexity values are normalised to a 0-1 range to avoid either churn or complexity dominating the score. The summary points also include a beta score, which is the geometric mean of the normalised churn and complexity values.
48
64
  ## Examples
49
65
 
50
66
  `churn_vs_complexity --ruby --csv my_ruby_project > ~/Desktop/ruby-demo.csv`
@@ -53,6 +69,7 @@ Usage: churn_vs_complexity [options] folder
53
69
 
54
70
  `churn_vs_complexity --ruby --summary -m my_ruby_project >> ~/Desktop/monthly-report.txt`
55
71
 
72
+ `churn_vs_complexity --java -m --since 2019-03-01 --timetravel 30 --graph my_java_project > ~/Desktop/timetravel-after-1st-march-2019.html`
56
73
 
57
74
  ## Development
58
75
 
data/TODO ADDED
@@ -0,0 +1,11 @@
1
+ TODO:
2
+
3
+ - Move Timetravel calculations away from serializer
4
+
5
+ - Database, where we can prepopulate the state of every file and every commit
6
+ - Populate incrementally from each commit in log.
7
+ - Only need to care about deltas between commits, and copy everything else from previous commit.
8
+ - processed_commit table with sha and version of processing logic
9
+ - Unit tests for simpler new classes
10
+ - Integration test for Timetravel
11
+ - Candlebars on mean dots in graph
@@ -6,6 +6,11 @@ require 'churn_vs_complexity'
6
6
 
7
7
  begin
8
8
  ChurnVsComplexity::CLI.run!
9
+ rescue ChurnVsComplexity::ValidationError => e
10
+ warn e.message
11
+ exit 1
9
12
  rescue StandardError => e
13
+ warn e.backtrace
10
14
  warn e.message
15
+ exit 2
11
16
  end
@@ -8,8 +8,9 @@ module ChurnVsComplexity
8
8
  class << self
9
9
  def calculate(folder:, file:, since:)
10
10
  git_dir = File.join(folder, '.git')
11
- formatted_date = since.strftime('%Y-%m-%d')
12
- cmd = %Q(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
11
+ earliest_date = [date_of_first_commit(folder:), since].max
12
+ formatted_date = earliest_date.strftime('%Y-%m-%d')
13
+ cmd = %(git --git-dir #{git_dir} --work-tree #{folder} log --format="%H" --follow --since="#{formatted_date}" -- #{file} | wc -l)
13
14
  `#{cmd}`.to_i
14
15
  end
15
16
 
@@ -19,6 +20,10 @@ module ChurnVsComplexity
19
20
 
20
21
  private
21
22
 
23
+ def date_of_first_commit(folder:)
24
+ repo(folder).log.last&.date&.to_date || Time.at(0).to_date
25
+ end
26
+
22
27
  def repo(folder)
23
28
  repos[folder] ||= Git.open(folder)
24
29
  end
@@ -22,6 +22,11 @@ module ChurnVsComplexity
22
22
  options[:language] = :ruby
23
23
  end
24
24
 
25
+ opts.on('--js', '--ts', '--javascript', '--typescript',
26
+ 'Check complexity of javascript and typescript files',) do
27
+ options[:language] = :javascript
28
+ end
29
+
25
30
  opts.on('--csv', 'Format output as CSV') do
26
31
  options[:serializer] = :csv
27
32
  end
@@ -39,20 +44,27 @@ module ChurnVsComplexity
39
44
  options[:excluded] << value
40
45
  end
41
46
 
42
- opts.on('--since YYYY-MM-DD', 'Calculate churn after this date') do |value|
47
+ opts.on('--since YYYY-MM-DD',
48
+ 'Normal mode: Calculate churn after this date. Timetravel mode: calculate summaries from this date',) do |value|
43
49
  options[:since] = value
44
50
  end
45
51
 
46
52
  opts.on('-m', '--month', 'Calculate churn for the month leading up to the most recent commit') do
47
- options[:since] = :month
53
+ options[:relative_period] = :month
48
54
  end
49
55
 
50
56
  opts.on('-q', '--quarter', 'Calculate churn for the quarter leading up to the most recent commit') do
51
- options[:since] = :quarter
57
+ options[:relative_period] = :quarter
52
58
  end
53
59
 
54
60
  opts.on('-y', '--year', 'Calculate churn for the year leading up to the most recent commit') do
55
- options[:since] = :year
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
56
68
  end
57
69
 
58
70
  opts.on('--dry-run', 'Echo the chosen options from the CLI') do
@@ -80,7 +92,11 @@ module ChurnVsComplexity
80
92
 
81
93
  config.validate!
82
94
 
83
- puts config.to_engine.check(folder:)
95
+ if options[:mode] == :timetravel
96
+ puts config.timetravel.go(folder:)
97
+ else
98
+ puts config.to_engine.check(folder:)
99
+ end
84
100
  end
85
101
  end
86
102
  end
@@ -0,0 +1,34 @@
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
+ command = "node #{script_path} '#{files.to_json}'"
16
+ complexity = `#{command}`
17
+
18
+ raise Error, 'Failed to calculate complexity' if complexity.empty?
19
+
20
+ all = JSON.parse(complexity)
21
+ all.to_h do |abc|
22
+ [abc['file'], abc['complexity']]
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def gem_root
29
+ File.expand_path('../../..', __dir__)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ 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(file:)
14
- flog = Flog.new
15
- flog.flog(file)
16
- { file => flog.total_score }
11
+ def calculate(files:)
12
+ #  TODO: Run this concurrently
13
+ files.to_h do |file|
14
+ flog = Flog.new
15
+ flog.flog(file)
16
+ [file, flog.total_score]
17
+ end
17
18
  end
18
19
  end
19
20
  end
@@ -9,7 +9,10 @@ module ChurnVsComplexity
9
9
  def folder_based? = true
10
10
 
11
11
  def calculate(folder:)
12
- output = `pmd check -d #{folder} -R #{resolve_ruleset_path} -f json -t #{CONCURRENCY} --cache #{resolve_cache_path}`
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)
15
+
13
16
  Parser.new.parse(output)
14
17
  end
15
18
 
@@ -29,7 +32,7 @@ module ChurnVsComplexity
29
32
  end
30
33
 
31
34
  def resolve_cache_path
32
- File.join(gem_root, 'tmp', 'pmd-support', 'pmd-cache')
35
+ File.join(gem_root, 'tmp', 'pmd-support', "pmd-cache-#{Process.pid}")
33
36
  end
34
37
 
35
38
  def gem_root
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'complexity/pmd_calculator'
4
4
  require_relative 'complexity/flog_calculator'
5
+ require_relative 'complexity/eslint_calculator'
5
6
 
6
7
  module ChurnVsComplexity
7
8
  module Complexity
@@ -30,9 +30,7 @@ module ChurnVsComplexity
30
30
  files[:explicitly_excluded].each { |file| result.delete(file) }
31
31
  result
32
32
  else
33
- files[:included].each_with_object({}) do |file, acc|
34
- acc.merge!(@complexity.calculate(file:))
35
- end
33
+ @complexity.calculate(files: files[:included])
36
34
  end
37
35
  end
38
36
 
@@ -59,7 +57,7 @@ module ChurnVsComplexity
59
57
  def combine_results
60
58
  result = {}
61
59
  result[:values_by_file] = @complexity_results.keys.each_with_object({}) do |file, acc|
62
- # File with complexity score might not have churned in queried period,
60
+ # File with complexity score might not have churned in queried period,
63
61
  # set zero churn on miss
64
62
  acc[file] = [@churn_results[file] || 0, @complexity_results[file]]
65
63
  end
@@ -7,25 +7,42 @@ module ChurnVsComplexity
7
7
  serializer:,
8
8
  excluded: [],
9
9
  since: nil,
10
+ relative_period: nil,
10
11
  complexity_validator: ComplexityValidator,
11
- since_validator: SinceValidator
12
+ since_validator: SinceValidator,
13
+ **options
12
14
  )
13
15
  @language = language
14
16
  @serializer = serializer
15
17
  @excluded = excluded
16
18
  @since = since
19
+ @relative_period = relative_period
17
20
  @complexity_validator = complexity_validator
18
21
  @since_validator = since_validator
22
+ @options = options
19
23
  end
20
24
 
21
25
  def validate!
22
- raise Error, "Unsupported language: #{@language}" unless %i[java ruby].include?(@language)
23
- raise Error, "Unsupported serializer: #{@serializer}" unless %i[none csv graph summary].include?(@serializer)
26
+ raise ValidationError, "Unsupported language: #{@language}" unless %i[java ruby javascript].include?(@language)
24
27
 
25
- @since_validator.validate!(@since)
28
+ SerializerValidator.validate!(serializer: @serializer, mode: @options[:mode])
29
+
30
+ @since_validator.validate!(since: @since, relative_period: @relative_period, mode: @options[:mode])
31
+ RelativePeriodValidator.validate!(relative_period: @relative_period, mode: @options[:mode])
26
32
  @complexity_validator.validate!(@language)
27
33
  end
28
34
 
35
+ def timetravel
36
+ engine = timetravel_engine_config.to_engine
37
+ Timetravel::Traveller.new(
38
+ since: @since,
39
+ relative_period: @relative_period,
40
+ engine:,
41
+ jump_days: @options[:jump_days],
42
+ serializer: @serializer,
43
+ )
44
+ end
45
+
29
46
  def to_engine
30
47
  case @language
31
48
  when :java
@@ -34,7 +51,7 @@ module ChurnVsComplexity
34
51
  churn:,
35
52
  file_selector: FileSelector::Java.excluding(@excluded),
36
53
  serializer:,
37
- since: @since,
54
+ since: @since || @relative_period,
38
55
  )
39
56
  when :ruby
40
57
  Engine.concurrent(
@@ -42,13 +59,34 @@ module ChurnVsComplexity
42
59
  churn:,
43
60
  file_selector: FileSelector::Ruby.excluding(@excluded),
44
61
  serializer:,
45
- since: @since,
62
+ since: @since || @relative_period,
63
+ )
64
+ when :javascript
65
+ Engine.concurrent(
66
+ complexity: Complexity::ESLintCalculator,
67
+ churn:,
68
+ file_selector: FileSelector::JavaScript.excluding(@excluded),
69
+ serializer:,
70
+ since: @since || @relative_period,
46
71
  )
47
72
  end
48
73
  end
49
74
 
50
75
  private
51
76
 
77
+ def timetravel_engine_config
78
+ Config.new(
79
+ language: @language,
80
+ serializer: :pass_through,
81
+ excluded: @excluded,
82
+ since: nil, # since has a different meaning in timetravel mode
83
+ relative_period: @relative_period,
84
+ complexity_validator: @complexity_validator,
85
+ since_validator: @since_validator,
86
+ **@options,
87
+ )
88
+ end
89
+
52
90
  def churn = Churn::GitCalculator
53
91
 
54
92
  def serializer
@@ -61,6 +99,8 @@ module ChurnVsComplexity
61
99
  Serializer::Graph.new
62
100
  when :summary
63
101
  Serializer::Summary
102
+ when :pass_through
103
+ Serializer::PassThrough
64
104
  end
65
105
  end
66
106
 
@@ -73,21 +113,45 @@ module ChurnVsComplexity
73
113
  end
74
114
  end
75
115
 
116
+ # TODO: unit test
117
+ module SerializerValidator
118
+ def self.validate!(serializer:, mode:)
119
+ raise ValidationError, "Unsupported serializer: #{serializer}" \
120
+ unless %i[none csv graph summary].include?(serializer)
121
+ raise ValidationError, 'Does not support --summary in --timetravel mode' \
122
+ if serializer == :summary && mode == :timetravel
123
+ end
124
+ end
125
+
126
+ # TODO: unit test
127
+ module RelativePeriodValidator
128
+ def self.validate!(relative_period:, mode:)
129
+ if mode == :timetravel && relative_period.nil?
130
+ raise ValidationError,
131
+ 'Relative period is required in timetravel mode'
132
+ end
133
+ return if relative_period.nil? || %i[month quarter year].include?(relative_period)
134
+
135
+ raise ValidationError, "Invalid relative period #{relative_period}"
136
+ end
137
+ end
138
+
76
139
  module SinceValidator
77
- def self.validate!(since)
140
+ def self.validate!(since:, relative_period:, mode:)
78
141
  # since can be nil, a date string or a keyword (:month, :quarter, :year)
79
142
  return if since.nil?
80
143
 
81
- if since.is_a?(Symbol)
82
- raise Error, "Invalid since value #{since}" unless %i[month quarter year].include?(since)
83
- elsif since.is_a?(String)
84
- begin
85
- Date.strptime(since, '%Y-%m-%d')
86
- rescue StandardError
87
- raise Error, "Invalid date #{since}, please use correct format, YYYY-MM-DD"
88
- end
89
- else
90
- raise Error, "Invalid since value #{since}"
144
+ unless mode == :timetravel || since.nil? || relative_period.nil?
145
+ raise ValidationError,
146
+ '--since and relative period (--month, --quarter, --year) can only be used together in --timetravel mode'
147
+ end
148
+
149
+ raise ValidationError, "Invalid since value #{since}" unless since.is_a?(String)
150
+
151
+ begin
152
+ Date.strptime(since, '%Y-%m-%d')
153
+ rescue Date::Error
154
+ raise ValidationError, "Invalid date #{since}, please use correct format, YYYY-MM-DD"
91
155
  end
92
156
  end
93
157
  end
@@ -10,9 +10,10 @@ module ChurnVsComplexity
10
10
  end
11
11
 
12
12
  class Excluding
13
- def initialize(extensions, excluded)
13
+ def initialize(extensions, excluded, convert_to_absolute_path = false)
14
14
  @extensions = extensions
15
15
  @excluded = excluded
16
+ @convert_to_absolute_path = convert_to_absolute_path
16
17
  end
17
18
 
18
19
  def select_files(folder)
@@ -25,6 +26,10 @@ module ChurnVsComplexity
25
26
  were_included << f
26
27
  end
27
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
28
33
  { explicitly_excluded: were_excluded, included: were_included }
29
34
  end
30
35
 
@@ -50,5 +55,11 @@ module ChurnVsComplexity
50
55
  Excluding.new(['.rb'], excluded)
51
56
  end
52
57
  end
58
+
59
+ module JavaScript
60
+ def self.excluding(excluded)
61
+ Excluding.new(['.js', '.jsx', '.ts', '.tsx'], excluded, true)
62
+ end
63
+ end
53
64
  end
54
65
  end
@@ -2,6 +2,13 @@
2
2
 
3
3
  module ChurnVsComplexity
4
4
  module GitDate
5
+ def self.select_dates_with_at_least_interval(dates, interval)
6
+ ds = dates.sort
7
+ ds.each_with_object([]) do |date, acc|
8
+ acc << date if acc.empty? || date - acc.last >= interval
9
+ end
10
+ end
11
+
5
12
  def self.git_period(cli_arg_since, latest_commit_date)
6
13
  latest_commit_date = latest_commit_date.to_date
7
14
  if cli_arg_since.nil?
@@ -22,7 +29,7 @@ module ChurnVsComplexity
22
29
  @end_date = end_date
23
30
  end
24
31
 
25
- def effective_start_date = Time.at(0)
32
+ def effective_start_date = Time.at(0).to_date
26
33
 
27
34
  def requested_start_date = nil
28
35
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Serializer
5
+ module CSV
6
+ def self.serialize(result)
7
+ values_by_file = result[:values_by_file]
8
+ values_by_file.map do |file, values|
9
+ "#{file},#{values[0]},#{values[1]}\n"
10
+ end.join
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Serializer
5
+ class Graph
6
+ def initialize(template: Graph.load_template_file)
7
+ @template = template
8
+ end
9
+
10
+ def serialize(result)
11
+ data = result[:values_by_file].map do |file, values|
12
+ "{ file_path: '#{file}', churn: #{values[0]}, complexity: #{values[1]} }"
13
+ end.join(",\n") + "\n"
14
+ title = Serializer.title(result)
15
+ @template.gsub("// INSERT DATA\n", data).gsub('INSERT TITLE', title)
16
+ end
17
+
18
+ def self.load_template_file
19
+ file_path = File.expand_path('../../../tmp/template/graph.html', __dir__)
20
+ File.read(file_path)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Serializer
5
+ module PassThrough
6
+ class << self
7
+ def serialize(result)
8
+ values_by_file = result[:values_by_file]
9
+ end_date = result[:git_period].end_date
10
+ values = values_by_file.map do |_, values|
11
+ [values[0].to_f, values[1].to_f]
12
+ end
13
+ {
14
+ end_date:,
15
+ values:,
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Serializer
5
+ module Summary
6
+ def self.serialize(result)
7
+ values_by_file = result[:values_by_file]
8
+ summary = SummaryHash.serialize(result)
9
+
10
+ <<~SUMMARY
11
+ #{Serializer.title(result)}
12
+
13
+ Number of observations: #{values_by_file.size}
14
+
15
+ Churn:
16
+ Mean #{summary[:mean_churn]}, Median #{summary[:median_churn]}
17
+
18
+ Complexity:
19
+ Mean #{summary[:mean_complexity]}, Median #{summary[:median_complexity]}
20
+
21
+ Gamma score:
22
+ Mean #{summary[:mean_gamma_score]}, Median #{summary[:median_gamma_score]}
23
+ SUMMARY
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ChurnVsComplexity
4
+ module Serializer
5
+ module SummaryHash
6
+ class << self
7
+ def serialize(result)
8
+ values_by_file = result[:values_by_file]
9
+ churn_values = values_by_file.map { |_, values| values[0].to_f }
10
+ complexity_values = values_by_file.map { |_, values| values[1].to_f }
11
+
12
+ mean_churn = churn_values.sum / churn_values.size
13
+ median_churn = churn_values.sort[churn_values.size / 2]
14
+ mean_complexity = complexity_values.sum / complexity_values.size
15
+ median_complexity = complexity_values.sort[complexity_values.size / 2]
16
+
17
+ max_churn = churn_values.max
18
+ min_churn = churn_values.min
19
+ max_complexity = complexity_values.max
20
+ min_complexity = complexity_values.min
21
+
22
+ epsilon = 0.0001
23
+ gamma_score = values_by_file.map do |_, values|
24
+ # unnormalised harmonic mean of churn and complexity,
25
+ # since the summary needs to be comparable over time
26
+ churn = values[0].to_f + epsilon
27
+ complexity = values[1].to_f + epsilon
28
+
29
+ (2 * churn * complexity) / (churn + complexity)
30
+ end
31
+
32
+ mean_gamma_score = gamma_score.sum / gamma_score.size
33
+ median_gamma_score = gamma_score.sort[gamma_score.size / 2]
34
+
35
+ end_date = result[:git_period].end_date
36
+
37
+ {
38
+ mean_churn:,
39
+ median_churn:,
40
+ max_churn:,
41
+ min_churn:,
42
+ mean_complexity:,
43
+ median_complexity:,
44
+ max_complexity:,
45
+ min_complexity:,
46
+ mean_gamma_score:,
47
+ median_gamma_score:,
48
+ end_date:,
49
+ }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end