churn_vs_complexity 1.0.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []