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