churn_vs_complexity 1.0.0 → 1.3.0

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