countless 1.0.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 +7 -0
- data/.editorconfig +30 -0
- data/.github/workflows/documentation.yml +39 -0
- data/.github/workflows/test.yml +69 -0
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.rubocop.yml +58 -0
- data/.simplecov +5 -0
- data/.yardopts +6 -0
- data/Appraisals +25 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Guardfile +44 -0
- data/LICENSE +21 -0
- data/Makefile +151 -0
- data/README.md +282 -0
- data/Rakefile +27 -0
- data/bin/cloc +17236 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/countless.gemspec +44 -0
- data/doc/assets/project.svg +68 -0
- data/docker-compose.yml +8 -0
- data/gemfiles/rails_5.2.gemfile +9 -0
- data/gemfiles/rails_6.0.gemfile +9 -0
- data/gemfiles/rails_6.1.gemfile +9 -0
- data/gemfiles/rails_7.0.gemfile +9 -0
- data/lib/countless/annotations.rb +243 -0
- data/lib/countless/cloc.rb +67 -0
- data/lib/countless/configuration.rb +209 -0
- data/lib/countless/extensions/configuration_handling.rb +83 -0
- data/lib/countless/rake_tasks.rake +31 -0
- data/lib/countless/rake_tasks.rb +6 -0
- data/lib/countless/statistics.rb +320 -0
- data/lib/countless/version.rb +23 -0
- data/lib/countless.rb +35 -0
- metadata +282 -0
@@ -0,0 +1,209 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Countless
|
4
|
+
# The configuration for the countless gem.
|
5
|
+
#
|
6
|
+
# rubocop:disable Metrics/ClassLength because of the various defaults
|
7
|
+
# rubocop:disable Metrics/BlockLength ditoditodito
|
8
|
+
class Configuration
|
9
|
+
include ActiveSupport::Configurable
|
10
|
+
|
11
|
+
# The base/root path of the project to work on. This path is used as a
|
12
|
+
# prefix to all relative path/file configurations.
|
13
|
+
config_accessor(:base_path) do
|
14
|
+
# Check for a Rake invoked call
|
15
|
+
if defined? Rake
|
16
|
+
path = Rake.application.rakefile_location
|
17
|
+
path = Rake.application.original_dir unless path.present?
|
18
|
+
next path
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check for Rails as fallback
|
22
|
+
next Rails.root if defined? Rails
|
23
|
+
|
24
|
+
# Use the current working directory
|
25
|
+
# of the process as last resort
|
26
|
+
Dir.pwd
|
27
|
+
end
|
28
|
+
|
29
|
+
# The path to the CLOC (https://github.com/AlDanial/cloc) binary. The gem
|
30
|
+
# comes with a bundled version of the utility, ready to be used. But you
|
31
|
+
# can also change the used binary path in order to use a different version
|
32
|
+
# which you manually provisioned.
|
33
|
+
config_accessor(:cloc_path) { File.expand_path('../../bin/cloc', __dir__) }
|
34
|
+
|
35
|
+
# We allow to configure additional file extensions to consider for
|
36
|
+
# statistics calculation. They will be included in the default list. This
|
37
|
+
# way you can easily extend the list.
|
38
|
+
config_accessor(:additional_stats_file_extensions) { [] }
|
39
|
+
|
40
|
+
# All the file extensions to consider for statistics calculation
|
41
|
+
config_accessor(:stats_file_extensions) do
|
42
|
+
%w[rb js jsx ts tsx css scss coffee rake erb haml h c cpp rs] +
|
43
|
+
additional_stats_file_extensions
|
44
|
+
end
|
45
|
+
|
46
|
+
# We allow to configure additional application object types. They will be
|
47
|
+
# included in the default list. This way you can easily extend the list.
|
48
|
+
config_accessor(:additional_stats_app_object_types) { [] }
|
49
|
+
|
50
|
+
# Configure the application (in the root +app+ directory) object types,
|
51
|
+
# they will be added as regular directories as well as their testing
|
52
|
+
# counter parts (minitest/RSpec)
|
53
|
+
config_accessor(:stats_app_object_types) do
|
54
|
+
%w[channels consumers controllers dashboards decorators fields helpers
|
55
|
+
jobs mailboxes mailers models policies serializers services uploaders
|
56
|
+
validators value_objects views] + additional_stats_app_object_types
|
57
|
+
end
|
58
|
+
|
59
|
+
# We allow to configure additional statistics directories. They will be
|
60
|
+
# included in the default list. This way you can easily extend the list.
|
61
|
+
config_accessor(:additional_stats_directories) { [] }
|
62
|
+
|
63
|
+
# A list of custom base directories in an application / gem
|
64
|
+
config_accessor(:stats_base_directories) do
|
65
|
+
[
|
66
|
+
{ name: 'JavaScripts', dir: 'app/assets/javascripts' },
|
67
|
+
{ name: 'Stylesheets', dir: 'app/assets/stylesheets' },
|
68
|
+
{ name: 'JavaScript', dir: 'app/javascript' },
|
69
|
+
{ name: 'API', dir: 'app/api' },
|
70
|
+
{ name: 'API tests', dir: 'test/api', test: true },
|
71
|
+
{ name: 'API specs', dir: 'spec/api', test: true },
|
72
|
+
{ name: 'APIs', dir: 'app/apis' },
|
73
|
+
{ name: 'API tests', dir: 'test/apis', test: true },
|
74
|
+
{ name: 'API specs', dir: 'spec/apis', test: true },
|
75
|
+
{ name: 'Libraries', dir: 'app/lib' },
|
76
|
+
{ name: 'Library tests', dir: 'test/lib', test: true },
|
77
|
+
{ name: 'Library specs', dir: 'spec/lib', test: true },
|
78
|
+
{ name: 'Libraries', dir: 'lib' },
|
79
|
+
{ name: 'Library tests', dir: 'test/lib', test: true },
|
80
|
+
{ name: 'Library specs', dir: 'spec/lib', test: true }
|
81
|
+
] + additional_stats_directories
|
82
|
+
end
|
83
|
+
|
84
|
+
# We allow to configure additional detailed statistics patterns. They will
|
85
|
+
# be included in the default list. This way you can easily extend the list.
|
86
|
+
config_accessor(:additional_detailed_stats_patterns) { {} }
|
87
|
+
|
88
|
+
# All the detailed statistics (class/method and tests/examples) patterns
|
89
|
+
# which will be used for parsing the source files to gather the metrics
|
90
|
+
config_accessor(:detailed_stats_patterns) do
|
91
|
+
{
|
92
|
+
ruby: {
|
93
|
+
extensions: %w[rb rake],
|
94
|
+
class: /^\s*class\s+[_A-Z]/, # regular Ruby classes
|
95
|
+
method: Regexp.union(
|
96
|
+
[
|
97
|
+
/^\s*def\s+[_a-z]/, # regular Ruby methods
|
98
|
+
/^\s*def test_/, # minitest
|
99
|
+
/^\s*x?it(\s+|\()['"_a-z]/ # RSpec
|
100
|
+
]
|
101
|
+
)
|
102
|
+
},
|
103
|
+
javascript: {
|
104
|
+
extensions: %w[js jsx ts tsx],
|
105
|
+
class: /^\s*class\s+[_A-Z]/,
|
106
|
+
method: Regexp.union(
|
107
|
+
[
|
108
|
+
/function(\s+[_a-zA-Z][\da-zA-Z]*)?\s*\(/, # regular method
|
109
|
+
/^\s*x?it(\s+|\()['"_a-z]/, # jsspec, jasmine, jest
|
110
|
+
/^\s*test(\s+|\()['"_a-z]/, # jest
|
111
|
+
/^\s*QUnit.test(\s+|\()['"_a-z]/ # qunit
|
112
|
+
]
|
113
|
+
)
|
114
|
+
},
|
115
|
+
coffee: {
|
116
|
+
extensions: %w[coffee],
|
117
|
+
class: /^\s*class\s+[_A-Z]/,
|
118
|
+
method: /[-=]>/
|
119
|
+
},
|
120
|
+
rust: {
|
121
|
+
extensions: %(rs),
|
122
|
+
class: /^\s*struct\s+[_A-Z]/,
|
123
|
+
method: Regexp.union(
|
124
|
+
[
|
125
|
+
/^\s*fn\s+[_a-z]/, # regular Rust methods
|
126
|
+
/#\[test\]/ # methods with test config
|
127
|
+
]
|
128
|
+
)
|
129
|
+
},
|
130
|
+
c_cpp: {
|
131
|
+
extensions: %(h c cpp),
|
132
|
+
class: /^\s*(struct|class)\s+[_a-z]/i,
|
133
|
+
method: /^\s*\w.* \w.*\(.*\)\s*{/m
|
134
|
+
}
|
135
|
+
}.deep_merge(additional_detailed_stats_patterns)
|
136
|
+
end
|
137
|
+
|
138
|
+
# We allow to configure additional annotation directories. They will be
|
139
|
+
# included in the default list. This way you can easily extend the list.
|
140
|
+
config_accessor(:additional_annotations_directories) { [] }
|
141
|
+
|
142
|
+
# Configure the directories which should be checked for annotations
|
143
|
+
config_accessor(:annotations_directories) do
|
144
|
+
%w[app config db src lib test tests spec doc docs] +
|
145
|
+
additional_annotations_directories
|
146
|
+
end
|
147
|
+
|
148
|
+
# We allow to configure additional annotation files/patterns. They will be
|
149
|
+
# included in the default list. This way you can easily extend the list.
|
150
|
+
config_accessor(:additional_annotations_files) { [] }
|
151
|
+
|
152
|
+
# Configure the files/patterns which should be checked for annotations
|
153
|
+
config_accessor(:annotations_files) do
|
154
|
+
%w[
|
155
|
+
Appraisals
|
156
|
+
CHANGELOG.md
|
157
|
+
CODE_OF_CONDUCT.md
|
158
|
+
config.ru
|
159
|
+
docker-compose.yml
|
160
|
+
Dockerfile
|
161
|
+
Envfile
|
162
|
+
Gemfile
|
163
|
+
*.gemspec
|
164
|
+
Makefile
|
165
|
+
Rakefile
|
166
|
+
README.md
|
167
|
+
] + additional_annotations_files
|
168
|
+
end
|
169
|
+
|
170
|
+
# We allow to configure additional annotation tags. They will be included
|
171
|
+
# in the default list. This way you can easily extend the list.
|
172
|
+
config_accessor(:additional_annotation_tags) { [] }
|
173
|
+
|
174
|
+
# Configure the annotation tags which will be search
|
175
|
+
config_accessor(:annotation_tags) do
|
176
|
+
%w[OPTIMIZE FIXME TODO TESTME DEPRECATEME] + additional_annotation_tags
|
177
|
+
end
|
178
|
+
|
179
|
+
# We allow to configure additional annotation patterns. They will be
|
180
|
+
# included in the default list. This way you can easily extend the list.
|
181
|
+
config_accessor(:additional_annotation_patterns) { {} }
|
182
|
+
|
183
|
+
# Configure all known file extensions of annotations files
|
184
|
+
config_accessor(:annotation_patterns) do
|
185
|
+
{
|
186
|
+
hashtag: {
|
187
|
+
files: %w[Appraisals Dockerfile Envfile Gemfile Rakefile
|
188
|
+
Makefile Appraisals],
|
189
|
+
extensions: %w[builder md ru rb rake yml yaml ruby gemspec toml],
|
190
|
+
regex: ->(tag) { /#\s*(#{tag}):?\s*(.*)$/ }
|
191
|
+
},
|
192
|
+
double_slash: {
|
193
|
+
extensions: %w[css js jsx ts tsx rust c h],
|
194
|
+
regex: ->(tag) { %r{//\s*(#{tag}):?\s*(.*)$} }
|
195
|
+
},
|
196
|
+
erb: {
|
197
|
+
extensions: %w[erb],
|
198
|
+
regex: ->(tag) { /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ }
|
199
|
+
},
|
200
|
+
haml: {
|
201
|
+
extensions: %w[haml],
|
202
|
+
regex: ->(tag) { /-#\s*(#{tag}):?\s*(.*)$/ }
|
203
|
+
}
|
204
|
+
}.deep_merge(additional_annotation_patterns)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
# rubocop:enable Metrics/ClassLength
|
208
|
+
# rubocop:enable Metrics/BlockLength
|
209
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Countless
|
4
|
+
module Extensions
|
5
|
+
# A top-level gem-module extension to handle configuration needs.
|
6
|
+
#
|
7
|
+
# rubocop:disable Metrics/BlockLength because this is how
|
8
|
+
# an +ActiveSupport::Concern+ looks like
|
9
|
+
module ConfigurationHandling
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
# Retrieve the current configuration object.
|
14
|
+
#
|
15
|
+
# @return [Configuration] the current configuration object
|
16
|
+
def configuration
|
17
|
+
@configuration ||= Configuration.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Configure the concern by providing a block which takes
|
21
|
+
# care of this task. Example:
|
22
|
+
#
|
23
|
+
# Countless.configure do |conf|
|
24
|
+
# # conf.xyz = [..]
|
25
|
+
# end
|
26
|
+
def configure
|
27
|
+
yield(configuration)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Reset the current configuration with the default one.
|
31
|
+
def reset_configuration!
|
32
|
+
@configuration = Configuration.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# A shortcut to the configured CLOC binary.
|
36
|
+
delegate :cloc_path, to: :configuration
|
37
|
+
|
38
|
+
# Get an assembled list of directories which should be
|
39
|
+
# checked for code statistics.
|
40
|
+
#
|
41
|
+
# @return [Array<Hash{Symbol => Mixed}>] the statistics directories
|
42
|
+
#
|
43
|
+
# rubocop:disable Metrics/MethodLength because of the
|
44
|
+
# configuration assembling
|
45
|
+
# rubocop:disable Metrics/AbcSize dito
|
46
|
+
# rubocop:disable Metrics/CyclomaticComplexity dito
|
47
|
+
def statistic_directories
|
48
|
+
conf = configuration
|
49
|
+
pattern_suffix = "/**/*.{#{conf.stats_file_extensions.join(',')}}"
|
50
|
+
|
51
|
+
res = conf.stats_base_directories.deep_dup
|
52
|
+
conf.stats_app_object_types.each do |type|
|
53
|
+
one_type = type.singularize.titleize
|
54
|
+
many_types = type.pluralize.titleize
|
55
|
+
|
56
|
+
res << { name: many_types, dir: "app/#{type}" }
|
57
|
+
res << { name: "#{one_type} tests",
|
58
|
+
dir: "test/#{type}", test: true }
|
59
|
+
res << { name: "#{one_type} specs",
|
60
|
+
dir: "specs/#{type}", test: true }
|
61
|
+
end
|
62
|
+
|
63
|
+
res.each do |cur|
|
64
|
+
# Add the configured base dir, when we hit a relative dir config
|
65
|
+
cur[:dir] = "#{conf.base_path}/#{cur[:dir]}" \
|
66
|
+
unless (cur[:dir] || '').start_with? '/'
|
67
|
+
# Add the default pattern, when no user configured pattern
|
68
|
+
# is present
|
69
|
+
cur[:pattern] ||= "#{cur[:dir]}#{pattern_suffix}"
|
70
|
+
# Fallback to regular code, when not otherwise configured
|
71
|
+
cur[:test] ||= false
|
72
|
+
end
|
73
|
+
|
74
|
+
res.sort_by { |cur| [cur[:test].to_s, cur[:name]] }
|
75
|
+
end
|
76
|
+
# rubocop:enable Metrics/MethodLength
|
77
|
+
# rubocop:enable Metrics/AbcSize
|
78
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
79
|
+
end
|
80
|
+
end
|
81
|
+
# rubocop:enable Metrics/BlockLength
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'pp'
|
5
|
+
|
6
|
+
desc 'Report code statistics (KLOCs, etc)'
|
7
|
+
task :stats do
|
8
|
+
puts Countless::Statistics.new.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Enumerate all annotations'
|
12
|
+
task :notes do
|
13
|
+
puts Countless::Annotations.new.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
namespace :notes do
|
17
|
+
Countless.configuration.annotation_tags.each do |annotation|
|
18
|
+
task annotation.downcase.to_sym do
|
19
|
+
puts Countless::Annotations.new("@?#{annotation}").to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
task :custom do
|
24
|
+
annotation = ENV.fetch('ANNOTATION')
|
25
|
+
puts Countless::Annotations.new("@?#{annotation}").to_s
|
26
|
+
rescue KeyError
|
27
|
+
puts 'No annotation was specified.'
|
28
|
+
puts "Usage: ANNOTATION='FIXME' rake notes:custom"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,320 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Countless
|
4
|
+
# The source code statistics displaying handler.
|
5
|
+
#
|
6
|
+
# Heavily stolen from: https://bit.ly/3qpvgfu
|
7
|
+
#
|
8
|
+
# rubocop:disable Metrics/ClassLength because of the calculation
|
9
|
+
# and formatting logic
|
10
|
+
class Statistics
|
11
|
+
# Make the extracted information accessible
|
12
|
+
attr_reader :dirs, :statistics, :total
|
13
|
+
|
14
|
+
# Initialize a new source code statistics displaying handler. When no
|
15
|
+
# configurations are passed in directly, we fallback to the configured
|
16
|
+
# statistics directories of the gem.
|
17
|
+
#
|
18
|
+
# @param dirs [Array<Hash{Symbol => Mixed}>] the configurations
|
19
|
+
# @return [Countless::Statistics] the new instance
|
20
|
+
#
|
21
|
+
# rubocop:disable Metrics/AbcSize because of the
|
22
|
+
# directory/config resolving
|
23
|
+
# rubocop:disable Metrics/PerceivedComplexity dito
|
24
|
+
# rubocop:disable Metrics/CyclomaticComplexity dito
|
25
|
+
# rubocop:disable Metrics/MethodLength dito
|
26
|
+
def initialize(*dirs)
|
27
|
+
base_path = Countless.configuration.base_path
|
28
|
+
|
29
|
+
# Resolve the given directory configurations to actual files
|
30
|
+
dirs = (dirs.presence || Countless.statistic_directories)
|
31
|
+
@dirs = dirs.each_with_object([]) do |cur, memo|
|
32
|
+
copy = cur.deep_dup
|
33
|
+
copy[:files] = Array(copy[:files])
|
34
|
+
|
35
|
+
if copy[:pattern].is_a? Regexp
|
36
|
+
copy[:files] += Dir[
|
37
|
+
File.join(copy[:dir] || base_path, '**/*')
|
38
|
+
].select { |path| File.file?(path) && copy[:pattern].match?(path) }
|
39
|
+
else
|
40
|
+
copy[:files] += Dir[copy[:pattern]]
|
41
|
+
end
|
42
|
+
|
43
|
+
copy[:files].uniq!
|
44
|
+
memo << copy if copy[:files].present?
|
45
|
+
end
|
46
|
+
|
47
|
+
@statistics = calculate_statistics
|
48
|
+
@total = calculate_total if @dirs.length > 1
|
49
|
+
end
|
50
|
+
# rubocop:enable Metrics/AbcSize
|
51
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
52
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
53
|
+
# rubocop:enable Metrics/MethodLength
|
54
|
+
|
55
|
+
# Calculate the total statistics of all sub-statistics for the configured
|
56
|
+
# directories.
|
57
|
+
#
|
58
|
+
# @return [Countless::Statistics::Calculator] the total statistics
|
59
|
+
def calculate_total
|
60
|
+
calculator = Calculator.new(name: 'Total')
|
61
|
+
@statistics.values.each_with_object(calculator) do |conf, total|
|
62
|
+
total.add(conf[:stats])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Calculate all statistics for the configured directories and pass back a
|
67
|
+
# named hash.
|
68
|
+
#
|
69
|
+
# @return [Hash{String => Hash{Symbol => Mixed}}] the statistics
|
70
|
+
# per configuration
|
71
|
+
def calculate_statistics
|
72
|
+
@dirs.map do |conf|
|
73
|
+
[
|
74
|
+
conf[:name],
|
75
|
+
conf.merge(stats: calculate_file_statistics(conf[:name],
|
76
|
+
conf[:files]))
|
77
|
+
]
|
78
|
+
end.to_h
|
79
|
+
end
|
80
|
+
|
81
|
+
# Setup a new +Calculator+ for the given directory/pattern in order to
|
82
|
+
# extract the individual file statistics and calculate the sub-totals.
|
83
|
+
#
|
84
|
+
# We match the pattern against the individual file name and the relative
|
85
|
+
# file path. This allows top-level only matches.
|
86
|
+
#
|
87
|
+
# @param name [String] the name/description/label of the directory
|
88
|
+
# @param files [Array<String, Pathname>] the files to extract
|
89
|
+
# statistics from
|
90
|
+
# @return [Countless::Statistics::Calculator] the calculator runtime
|
91
|
+
# for the given directory/pattern
|
92
|
+
def calculate_file_statistics(name, files)
|
93
|
+
Calculator.new(name: name).tap do |calc|
|
94
|
+
Cloc.stats(*files).each do |path, stats|
|
95
|
+
calc.add_by_file_path(path, **stats)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Calculate the total lines of code.
|
101
|
+
#
|
102
|
+
# @return [Integer] the total lines of code
|
103
|
+
def calculate_code
|
104
|
+
@statistics.values.reject { |conf| conf[:test] }
|
105
|
+
.map { |conf| conf[:stats].code_lines }.sum
|
106
|
+
end
|
107
|
+
|
108
|
+
# Calculate the total lines of testing code.
|
109
|
+
#
|
110
|
+
# @return [Integer] the total lines of testing code
|
111
|
+
def calculate_tests
|
112
|
+
@statistics.values.select { |conf| conf[:test] }
|
113
|
+
.map { |conf| conf[:stats].code_lines }.sum
|
114
|
+
end
|
115
|
+
|
116
|
+
# Convert the code statistics to a formatted string buffer.
|
117
|
+
#
|
118
|
+
# @return [String] the formatted code statistics
|
119
|
+
#
|
120
|
+
# rubocop:disable Metrics/MethodLength because of the complex formatting
|
121
|
+
# logic with fully dynamic columns widths
|
122
|
+
# rubocop:disable Metrics/PerceivedComplexity dito
|
123
|
+
# rubocop:disable Metrics/CyclomaticComplexity dito
|
124
|
+
# rubocop:disable Metrics/AbcSize dito
|
125
|
+
def to_s
|
126
|
+
col_sizes = {}
|
127
|
+
rows = to_table.map do |row|
|
128
|
+
next row unless row.is_a?(Array)
|
129
|
+
|
130
|
+
row = row.map(&:to_s)
|
131
|
+
cols = row.map(&:length).each_with_index.map { |len, idx| [idx, [len]] }
|
132
|
+
col_sizes.deep_merge!(cols.to_h) { |_, left, right| left + right }
|
133
|
+
row
|
134
|
+
end
|
135
|
+
|
136
|
+
# Calculate the correct column sizes
|
137
|
+
col_sizes = col_sizes.values.each_with_object([]) do |widths, memo|
|
138
|
+
memo << widths.max + 2
|
139
|
+
end
|
140
|
+
|
141
|
+
# Enforce the correct column sizes per row
|
142
|
+
splitter = ([0] + col_sizes + [0]).map { |size| '-' * size }.join('+')
|
143
|
+
rows.each_with_object([]) do |row, memo|
|
144
|
+
next memo << splitter if row == :splitter
|
145
|
+
next memo << row if row.is_a?(String)
|
146
|
+
|
147
|
+
cols = row.each_with_index.map do |col, idx|
|
148
|
+
meth = idx.zero? ? :ljust : :rjust
|
149
|
+
col.send(meth, col_sizes[idx] - 2)
|
150
|
+
end
|
151
|
+
memo << "| #{cols.join(' | ')} |"
|
152
|
+
end.join("\n")
|
153
|
+
end
|
154
|
+
# rubocop:enable Metrics/MethodLength
|
155
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
156
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
157
|
+
# rubocop:enable Metrics/AbcSize
|
158
|
+
|
159
|
+
# Convert the code statistics to a processable table structure. Each
|
160
|
+
# element in the resulting array is a single line, while array elements
|
161
|
+
# reflect columns. The special +:splitter+ row value will be converted
|
162
|
+
# later by +#to_s+.
|
163
|
+
#
|
164
|
+
# @return [Array<Array<String, Integer>, Symbol>] the raw table
|
165
|
+
#
|
166
|
+
# rubocop:disable Metrics/MethodLength because of the table construction
|
167
|
+
def to_table
|
168
|
+
table = [
|
169
|
+
:splitter,
|
170
|
+
%w[Name Lines LOC Comments Classes Methods M/C LOC/M],
|
171
|
+
:splitter
|
172
|
+
]
|
173
|
+
@statistics.each_value { |conf| table << conf[:stats].to_h.values }
|
174
|
+
table << :splitter
|
175
|
+
|
176
|
+
if @total
|
177
|
+
table << @total.to_h.values
|
178
|
+
table << :splitter
|
179
|
+
end
|
180
|
+
|
181
|
+
table << code_test_stats_line
|
182
|
+
table
|
183
|
+
end
|
184
|
+
# rubocop:enable Metrics/MethodLength
|
185
|
+
|
186
|
+
# Return the final meta statistics line.
|
187
|
+
#
|
188
|
+
# @return [String] the meta statistics line
|
189
|
+
def code_test_stats_line
|
190
|
+
code = calculate_code
|
191
|
+
tests = calculate_tests
|
192
|
+
ratio = tests.fdiv(code)
|
193
|
+
ratio = '0' if ratio.nan?
|
194
|
+
|
195
|
+
res = [
|
196
|
+
"Code LOC: #{code}",
|
197
|
+
"Test LOC: #{tests}",
|
198
|
+
"Code to Test Ratio: 1:#{format('%.1f', ratio)}"
|
199
|
+
].join(' ' * 5)
|
200
|
+
" #{res}"
|
201
|
+
end
|
202
|
+
|
203
|
+
# The source code statistics calculator which holds the data of a single
|
204
|
+
# runtime.
|
205
|
+
#
|
206
|
+
# Heavily stolen from: https://bit.ly/3tk7ZgJ
|
207
|
+
class Calculator
|
208
|
+
# Expose each metric as simple readers
|
209
|
+
attr_reader :name, :lines, :code_lines, :comment_lines,
|
210
|
+
:classes, :methods
|
211
|
+
|
212
|
+
# Setup a new source code statistics calculator instance.
|
213
|
+
#
|
214
|
+
# @param name [String, nil] the name of the calculated path
|
215
|
+
# @param lines [Integer] the initial lines count
|
216
|
+
# @param code_lines [Integer] the initial code lines count
|
217
|
+
# @param comment_lines [Integer] the initial comment lines count
|
218
|
+
# @param classes [Integer] the initial classes count
|
219
|
+
# @param methods [Integer] the initial methods count
|
220
|
+
# @return [Countless::Statistics::Calculator] the new instance
|
221
|
+
#
|
222
|
+
# rubocop:disable Metrics/ParameterLists because of the
|
223
|
+
# various metrics we support
|
224
|
+
def initialize(name: nil, lines: 0, code_lines: 0, comment_lines: 0,
|
225
|
+
classes: 0, methods: 0)
|
226
|
+
@name = name
|
227
|
+
@lines = lines
|
228
|
+
@code_lines = code_lines
|
229
|
+
@comment_lines = comment_lines
|
230
|
+
@classes = classes
|
231
|
+
@methods = methods
|
232
|
+
end
|
233
|
+
# rubocop:enable Metrics/ParameterLists
|
234
|
+
|
235
|
+
# Add the metrics from another calculator instance to the current one.
|
236
|
+
#
|
237
|
+
# @param calculator [Countless::Statistics::Calculator] the other
|
238
|
+
# calculator instance to fetch metrics from
|
239
|
+
def add(calculator)
|
240
|
+
@lines += calculator.lines
|
241
|
+
@code_lines += calculator.code_lines
|
242
|
+
@comment_lines += calculator.comment_lines
|
243
|
+
@classes += calculator.classes
|
244
|
+
@methods += calculator.methods
|
245
|
+
end
|
246
|
+
|
247
|
+
# Parse and add statistics of a single file by path.
|
248
|
+
#
|
249
|
+
# @param path [String] the path of the file
|
250
|
+
# @param stats [Hash{Symbol => Integer}] addtional CLOC statistics
|
251
|
+
def add_by_file_path(path, **stats)
|
252
|
+
@lines += stats.fetch(:total, 0)
|
253
|
+
@code_lines += stats.fetch(:code, 0)
|
254
|
+
@comment_lines += stats.fetch(:comment, 0)
|
255
|
+
add_details_by_file_path(path)
|
256
|
+
end
|
257
|
+
|
258
|
+
# Analyse a given input file and extract the corresponding detailed
|
259
|
+
# metrics. (class and method counts) Afterwards apply the new metrics to
|
260
|
+
# the current calculator instance metrics.
|
261
|
+
#
|
262
|
+
# @param path [String] the path of the file
|
263
|
+
#
|
264
|
+
# rubocop:disable Metrics/AbcSize because of the pattern search by file
|
265
|
+
# extension and pattern matching on each line afterwards
|
266
|
+
# rubocop:disable Metrics/CyclomaticComplexity dito
|
267
|
+
# rubocop:disable Metrics/PerceivedComplexity dito
|
268
|
+
def add_details_by_file_path(path)
|
269
|
+
all_patterns = Countless.configuration.detailed_stats_patterns
|
270
|
+
|
271
|
+
ext = path.split('.').last
|
272
|
+
patterns = all_patterns.find do |_, conf|
|
273
|
+
conf[:extensions].include? ext
|
274
|
+
end&.last
|
275
|
+
|
276
|
+
# When no detailed patterns are configured for this file,
|
277
|
+
# we skip further processing
|
278
|
+
return unless patterns
|
279
|
+
|
280
|
+
# Walk through the given file, line by line
|
281
|
+
File.read(path).lines.each do |line|
|
282
|
+
@classes += 1 if patterns[:class]&.match? line
|
283
|
+
@methods += 1 if patterns[:method]&.match? line
|
284
|
+
end
|
285
|
+
end
|
286
|
+
# rubocop:enable Metrics/AbcSize
|
287
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
288
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
289
|
+
|
290
|
+
# Return the methods per classes.
|
291
|
+
#
|
292
|
+
# @return [Integer] the methods per classes
|
293
|
+
def m_over_c
|
294
|
+
methods / classes
|
295
|
+
rescue StandardError
|
296
|
+
0
|
297
|
+
end
|
298
|
+
|
299
|
+
# Return the lines of code per methods.
|
300
|
+
#
|
301
|
+
# @return [Integer] the lines of code per methods
|
302
|
+
def loc_over_m
|
303
|
+
code_lines / methods
|
304
|
+
rescue StandardError
|
305
|
+
0
|
306
|
+
end
|
307
|
+
|
308
|
+
# Convert the current calculator instance to a simple hash.
|
309
|
+
#
|
310
|
+
# @return [Hash{Symbol => Mixed}] the calculator values as simple hash
|
311
|
+
def to_h
|
312
|
+
%i[
|
313
|
+
name lines code_lines comment_lines
|
314
|
+
classes methods m_over_c loc_over_m
|
315
|
+
].each_with_object({}) { |key, memo| memo[key] = send(key) }
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
# rubocop:enable Metrics/ClassLength
|
320
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The gem version details.
|
4
|
+
module Countless
|
5
|
+
# The version of the +countless+ gem
|
6
|
+
VERSION = '1.0.0'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Returns the version of gem as a string.
|
10
|
+
#
|
11
|
+
# @return [String] the gem version as string
|
12
|
+
def version
|
13
|
+
VERSION
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the version of the gem as a +Gem::Version+.
|
17
|
+
#
|
18
|
+
# @return [Gem::Version] the gem version as object
|
19
|
+
def gem_version
|
20
|
+
Gem::Version.new VERSION
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|