Skip to content

Commit a91d530

Browse files
committed
Add analysis class
1 parent 2ddb680 commit a91d530

File tree

13 files changed

+454
-130
lines changed

13 files changed

+454
-130
lines changed

.reek.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ detectors:
2323
- initialize
2424
- Skunk::Cli::Application#execute
2525
- Skunk::Cli::Options::Argv#parse
26+
- Skunk::Analysis#test_module?
27+
- add_mock_methods_to_module
2628
TooManyInstanceVariables:
2729
exclude:
2830
- Skunk::Generator::Html::FileData
@@ -38,10 +40,20 @@ detectors:
3840
- Skunk::Command::StatusSharer#share_url_empty?
3941
- Skunk::Configuration#supported_format?
4042
- Skunk::Configuration#supported_formats
43+
- Skunk::Analysis#test_module?
44+
- Skunk::ConfigTest#setup
4145
ManualDispatch:
4246
exclude:
4347
- Skunk::Config#self.method_missing
4448
- Skunk::Config#self.respond_to_missing?
4549
BooleanParameter:
4650
exclude:
4751
- Skunk::Config#self.respond_to_missing?
52+
DuplicateMethodCall:
53+
exclude:
54+
- Skunk::ConfigTest#test_add_format_ignores_duplicates
55+
FeatureEnvy:
56+
exclude:
57+
- Skunk::Command::StatusReporter#table
58+
- Skunk::Generator::HtmlReport#create_directories_and_files
59+
- add_mock_methods_to_module

.rubocop_todo.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2025-10-17 11:20:00 UTC using RuboCop version 1.81.1.
3+
# on 2025-10-17 16:20:40 UTC using RuboCop version 1.81.1.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
@@ -36,11 +36,11 @@ Lint/MissingSuper:
3636
Metrics/AbcSize:
3737
Max: 18
3838

39-
# Offense count: 8
39+
# Offense count: 12
4040
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
4141
# AllowedMethods: refine
4242
Metrics/BlockLength:
43-
Max: 82
43+
Max: 233
4444

4545
# Offense count: 2
4646
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.

lib/skunk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "skunk/version"
4+
require "skunk/analysis"
45

56
# Knows how to calculate the `SkunkScore` for each file analyzed by `RubyCritic`
67
# and `SimpleCov`

lib/skunk/analysis.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# frozen_string_literal: true
2+
3+
module Skunk
4+
# Centralized service for analyzing Skunk metrics from analysed modules.
5+
# This class encapsulates all the core business logic for calculating
6+
# Skunk scores, filtering test modules, and providing aggregated statistics.
7+
#
8+
# @example
9+
# analysis = Skunk::Analysis.new(analysed_modules)
10+
# puts "Total Skunk Score: #{analysis.skunk_score_total}"
11+
# puts "Average: #{analysis.skunk_score_average}"
12+
# puts "Worst module: #{analysis.worst_module.pathname}"
13+
class Analysis
14+
attr_reader :analysed_modules
15+
16+
# @param analysed_modules [RubyCritic::AnalysedModulesCollection] Collection of analysed modules
17+
def initialize(analysed_modules)
18+
@analysed_modules = analysed_modules
19+
end
20+
21+
# Returns the count of non-test modules
22+
# @return [Integer]
23+
def analysed_modules_count
24+
@analysed_modules_count ||= non_test_modules.count
25+
end
26+
27+
# Returns the total Skunk score across all non-test modules
28+
# @return [Float]
29+
def skunk_score_total
30+
@skunk_score_total ||= non_test_modules.sum(&:skunk_score)
31+
end
32+
33+
# Returns the average Skunk score across all non-test modules
34+
# @return [Float]
35+
def skunk_score_average
36+
return 0.0 if analysed_modules_count.zero?
37+
38+
(skunk_score_total.to_d / analysed_modules_count).to_f.round(2)
39+
end
40+
41+
# Returns the total churn times cost across all non-test modules
42+
# @return [Float]
43+
def total_churn_times_cost
44+
@total_churn_times_cost ||= non_test_modules.sum(&:churn_times_cost)
45+
end
46+
47+
# Returns the module with the highest Skunk score (worst performing)
48+
# @return [RubyCritic::AnalysedModule, nil]
49+
def worst_module
50+
@worst_module ||= sorted_modules.first
51+
end
52+
53+
# Returns modules sorted by Skunk score in descending order (worst first)
54+
# @return [Array<RubyCritic::AnalysedModule>]
55+
def sorted_modules
56+
@sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
57+
end
58+
59+
# Returns only non-test modules (excludes test and spec directories)
60+
# @return [Array<RubyCritic::AnalysedModule>]
61+
def non_test_modules
62+
@non_test_modules ||= analysed_modules.reject do |a_module|
63+
test_module?(a_module)
64+
end
65+
end
66+
67+
# Returns a hash representation of the analysis results
68+
# @return [Hash]
69+
def to_hash
70+
{
71+
analysed_modules_count: analysed_modules_count,
72+
skunk_score_total: skunk_score_total,
73+
skunk_score_average: skunk_score_average,
74+
total_churn_times_cost: total_churn_times_cost,
75+
worst_pathname: worst_module&.pathname,
76+
worst_score: worst_module&.skunk_score,
77+
files: files_as_hash
78+
}
79+
end
80+
81+
private
82+
83+
# Returns files as an array of hashes (for JSON serialization)
84+
# @return [Array<Hash>]
85+
def files_as_hash
86+
@files_as_hash ||= sorted_modules.map(&:to_hash)
87+
end
88+
89+
# Determines if a module is a test module based on its path
90+
# @param a_module [RubyCritic::AnalysedModule] The module to check
91+
# @return [Boolean]
92+
def test_module?(a_module)
93+
pathname = a_module.pathname
94+
module_path = pathname.dirname.to_s
95+
filename = pathname.basename.to_s
96+
97+
# Check if directory starts or ends with test/spec
98+
directory_is_test = module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
99+
100+
# Check if filename ends with _test.rb or _spec.rb
101+
filename_is_test = filename.end_with?("_test.rb", "_spec.rb")
102+
103+
directory_is_test || filename_is_test
104+
end
105+
end
106+
end

lib/skunk/commands/status_reporter.rb

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
require "erb"
44
require "rubycritic/commands/status_reporter"
55
require "terminal-table"
6+
require "skunk/analysis"
67

78
module Skunk
89
module Command
910
# Knows how to report status for stinky files
1011
class StatusReporter < RubyCritic::Command::StatusReporter
1112
attr_accessor :analysed_modules
1213

14+
def initialize(options = {})
15+
super(options)
16+
@analysis = nil
17+
end
18+
1319
HEADINGS = %w[file skunk_score churn_times_cost churn cost coverage].freeze
1420
HEADINGS_WITHOUT_FILE = HEADINGS - %w[file]
1521
HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding
@@ -28,6 +34,7 @@ class StatusReporter < RubyCritic::Command::StatusReporter
2834
# Returns a status message with a table of all analysed_modules and
2935
# a skunk score average
3036
def update_status_message
37+
@analysis = Skunk::Analysis.new(analysed_modules)
3138
opts = table_options.merge(headings: HEADINGS, rows: table)
3239

3340
_ttable = Terminal::Table.new(opts)
@@ -38,36 +45,27 @@ def update_status_message
3845
private
3946

4047
def analysed_modules_count
41-
@analysed_modules_count ||= non_test_modules.count
42-
end
43-
44-
def non_test_modules
45-
@non_test_modules ||= analysed_modules.reject do |a_module|
46-
module_path = a_module.pathname.dirname.to_s
47-
module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
48-
end
48+
@analysis.analysed_modules_count
4949
end
5050

5151
def worst
52-
@worst ||= sorted_modules.first
52+
@analysis.worst_module
5353
end
5454

5555
def sorted_modules
56-
@sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
56+
@analysis.sorted_modules
5757
end
5858

5959
def total_skunk_score
60-
@total_skunk_score ||= non_test_modules.sum(&:skunk_score)
60+
@analysis.skunk_score_total
6161
end
6262

6363
def total_churn_times_cost
64-
non_test_modules.sum(&:churn_times_cost)
64+
@analysis.total_churn_times_cost
6565
end
6666

6767
def skunk_score_average
68-
return 0 if analysed_modules_count.zero?
69-
70-
(total_skunk_score.to_d / analysed_modules_count).to_f.round(2)
68+
@analysis.skunk_score_average
7169
end
7270

7371
def table_options

lib/skunk/generators/html/overview.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
require "rubycritic/generators/html/base"
44

55
require "skunk/generators/html/path_truncator"
6-
require "skunk/generators/html/skunk_data"
6+
require "skunk/generators/html/file_data"
7+
require "skunk/analysis"
78

89
module Skunk
910
module Generator
@@ -19,7 +20,9 @@ def self.erb_template(template_path)
1920

2021
def initialize(analysed_modules)
2122
@analysed_modules = analysed_modules
22-
@data = SkunkData.new(analysed_modules)
23+
@analysis = Skunk::Analysis.new(analysed_modules)
24+
@generated_at = Time.now.strftime("%Y-%m-%d %H:%M:%S")
25+
@skunk_version = Skunk::VERSION
2326
end
2427

2528
def file_name
@@ -29,6 +32,24 @@ def file_name
2932
def render
3033
TEMPLATE.result(binding)
3134
end
35+
36+
def analysed_modules_count
37+
@analysis.analysed_modules_count
38+
end
39+
40+
def skunk_score_total
41+
@analysis.skunk_score_total
42+
end
43+
44+
def skunk_score_average
45+
@analysis.skunk_score_average
46+
end
47+
48+
def files
49+
@files ||= @analysis.sorted_modules.map do |module_data|
50+
FileData.new(module_data)
51+
end
52+
end
3253
end
3354
end
3455
end

lib/skunk/generators/html/skunk_data.rb

Lines changed: 0 additions & 59 deletions
This file was deleted.

lib/skunk/generators/html/templates/skunk_overview.html.erb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,15 +242,15 @@
242242

243243
<section class="cards">
244244
<div class="card">
245-
<div class="value"><%= @data.analysed_modules_count %></div>
245+
<div class="value"><%= analysed_modules_count %></div>
246246
<div class="label">Modules Analysed</div>
247247
</div>
248248
<div class="card">
249-
<div class="value"><%= @data.skunk_score_total %></div>
249+
<div class="value"><%= skunk_score_total %></div>
250250
<div class="label">Total Skunk Score</div>
251251
</div>
252252
<div class="card">
253-
<div class="value"><%= @data.skunk_score_average %></div>
253+
<div class="value"><%= skunk_score_average %></div>
254254
<div class="label">Average Skunk Score</div>
255255
</div>
256256
</section>
@@ -269,7 +269,7 @@
269269
</tr>
270270
</thead>
271271
<tbody>
272-
<% @data.files.each do |item| %>
272+
<% files.each do |item| %>
273273
<tr class="table-row">
274274
<td class="filename">
275275
<span><%= item.file %></span>
@@ -301,7 +301,7 @@
301301
</section>
302302

303303
<footer class="footer">
304-
<p>Generated with Skunk v<%= @data.skunk_version %> on <%= @data.generated_at %></p>
304+
<p>Generated with Skunk v<%= @skunk_version %> on <%= @generated_at %></p>
305305
</footer>
306306
</div>
307307
</body>

0 commit comments

Comments
 (0)