Class: Countless::Statistics

Inherits:
Object
  • Object
show all
Defined in:
lib/countless/statistics.rb

Overview

The source code statistics displaying handler.

Heavily stolen from: bit.ly/3qpvgfu

rubocop:disable Metrics/ClassLength because of the calculation

and formatting logic

Defined Under Namespace

Classes: Calculator

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*dirs) ⇒ Countless::Statistics

Initialize a new source code statistics displaying handler. When no configurations are passed in directly, we fallback to the configured statistics directories of the gem.

rubocop:disable Metrics/AbcSize because of the

directory/config resolving

rubocop:disable Metrics/PerceivedComplexity dito rubocop:disable Metrics/CyclomaticComplexity dito rubocop:disable Metrics/MethodLength dito



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/countless/statistics.rb', line 26

def initialize(*dirs)
  base_path = Countless.configuration.base_path

  # Resolve the given directory configurations to actual files
  dirs = (dirs.presence || Countless.statistic_directories)
  @dirs = dirs.each_with_object([]) do |cur, memo|
    copy = cur.deep_dup
    copy[:files] = Array(copy[:files])

    if copy[:pattern].is_a? Regexp
      copy[:files] += Dir[
        File.join(copy[:dir] || base_path, '**/*')
      ].select { |path| File.file?(path) && copy[:pattern].match?(path) }
    else
      copy[:files] += Dir[copy[:pattern]]
    end

    copy[:files].uniq!
    memo << copy if copy[:files].present?
  end

  @statistics = calculate_statistics
  @total = calculate_total if @dirs.length > 1
end

Instance Attribute Details

#dirsObject (readonly)

Make the extracted information accessible



12
13
14
# File 'lib/countless/statistics.rb', line 12

def dirs
  @dirs
end

#statisticsObject (readonly)

Make the extracted information accessible



12
13
14
# File 'lib/countless/statistics.rb', line 12

def statistics
  @statistics
end

#totalObject (readonly)

Make the extracted information accessible



12
13
14
# File 'lib/countless/statistics.rb', line 12

def total
  @total
end

Instance Method Details

#calculate_codeInteger

Calculate the total lines of code.



103
104
105
106
# File 'lib/countless/statistics.rb', line 103

def calculate_code
  @statistics.values.reject { |conf| conf[:test] }
             .map { |conf| conf[:stats].code_lines }.sum
end

#calculate_file_statistics(name, files) ⇒ Countless::Statistics::Calculator

Setup a new Calculator for the given directory/pattern in order to extract the individual file statistics and calculate the sub-totals.

We match the pattern against the individual file name and the relative file path. This allows top-level only matches.



92
93
94
95
96
97
98
# File 'lib/countless/statistics.rb', line 92

def calculate_file_statistics(name, files)
  Calculator.new(name: name).tap do |calc|
    Cloc.stats(*files).each do |path, stats|
      calc.add_by_file_path(path, **stats)
    end
  end
end

#calculate_statisticsHash{String => Hash{Symbol => Mixed}}

Calculate all statistics for the configured directories and pass back a named hash.



71
72
73
74
75
76
77
78
79
# File 'lib/countless/statistics.rb', line 71

def calculate_statistics
  @dirs.map do |conf|
    [
      conf[:name],
      conf.merge(stats: calculate_file_statistics(conf[:name],
                                                  conf[:files]))
    ]
  end.to_h
end

#calculate_testsInteger

Calculate the total lines of testing code.



111
112
113
114
# File 'lib/countless/statistics.rb', line 111

def calculate_tests
  @statistics.values.select { |conf| conf[:test] }
             .map { |conf| conf[:stats].code_lines }.sum
end

#calculate_totalCountless::Statistics::Calculator

Calculate the total statistics of all sub-statistics for the configured directories.



59
60
61
62
63
64
# File 'lib/countless/statistics.rb', line 59

def calculate_total
  calculator = Calculator.new(name: 'Total')
  @statistics.values.each_with_object(calculator) do |conf, total|
    total.add(conf[:stats])
  end
end

#code_test_stats_lineString

Return the final meta statistics line.



189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/countless/statistics.rb', line 189

def code_test_stats_line
  code  = calculate_code
  tests = calculate_tests
  ratio = tests.fdiv(code)
  ratio = '0' if ratio.nan?

  res = [
    "Code LOC: #{code}",
    "Test LOC: #{tests}",
    "Code to Test Ratio: 1:#{format('%.1f', ratio)}"
  ].join(' ' * 5)
  "  #{res}"
end

#to_sString

Convert the code statistics to a formatted string buffer.

rubocop:disable Metrics/MethodLength because of the complex formatting

logic with fully dynamic columns widths

rubocop:disable Metrics/PerceivedComplexity dito rubocop:disable Metrics/CyclomaticComplexity dito rubocop:disable Metrics/AbcSize dito



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/countless/statistics.rb', line 125

def to_s
  col_sizes = {}
  rows = to_table.map do |row|
    next row unless row.is_a?(Array)

    row = row.map(&:to_s)
    cols = row.map(&:length).each_with_index.map { |len, idx| [idx, [len]] }
    col_sizes.deep_merge!(cols.to_h) { |_, left, right| left + right }
    row
  end

  # Calculate the correct column sizes
  col_sizes = col_sizes.values.each_with_object([]) do |widths, memo|
    memo << widths.max + 2
  end

  # Enforce the correct column sizes per row
  splitter = ([0] + col_sizes + [0]).map { |size| '-' * size }.join('+')
  rows.each_with_object([]) do |row, memo|
    next memo << splitter if row == :splitter
    next memo << row if row.is_a?(String)

    cols = row.each_with_index.map do |col, idx|
      meth = idx.zero? ? :ljust : :rjust
      col.send(meth, col_sizes[idx] - 2)
    end
    memo << "| #{cols.join(' | ')} |"
  end.join("\n")
end

#to_tableArray<Array<String, Integer>, Symbol>

Convert the code statistics to a processable table structure. Each element in the resulting array is a single line, while array elements reflect columns. The special :splitter row value will be converted later by #to_s.

rubocop:disable Metrics/MethodLength because of the table construction



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/countless/statistics.rb', line 167

def to_table
  table = [
    :splitter,
    %w[Name Lines LOC Comments Classes Methods M/C LOC/M],
    :splitter
  ]
  @statistics.each_value { |conf| table << conf[:stats].to_h.values }
  table << :splitter

  if @total
    table << @total.to_h.values
    table << :splitter
  end

  table << code_test_stats_line
  table
end