countless 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @TODO: This is just for testing purposes here. Keep it exactly like that.
4
+
5
+ require 'countless'
6
+ load 'countless/rake_tasks.rake'
@@ -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