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.
@@ -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