Skip to content

Commit 357ee5a

Browse files
committed
Implement HTML report generation and update report formats
Closes #50 - Allow Skunk to HTML output the results it generates - Added support for generating HTML reports alongside existing JSON format. - Introduced new classes for HTML report generation, including Overview, OverviewData, and FileData. - Updated the Reporter module to include HTML in the report generator formats. - Created a template for the HTML report with a responsive design. - Refactored JSON report generation to improve code organization and clarity.
1 parent 6ea68fe commit 357ee5a

File tree

11 files changed

+669
-15
lines changed

11 files changed

+669
-15
lines changed

.reek.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ detectors:
77
FeatureEnvy:
88
exclude:
99
- Skunk::Command::StatusReporter#table
10+
- Skunk::Generator::HtmlReport#create_directories_and_files
1011
InstanceVariableAssumption:
1112
exclude:
1213
- Skunk::Cli::Options::Argv
@@ -22,6 +23,10 @@ detectors:
2223
- initialize
2324
- Skunk::Cli::Application#execute
2425
- Skunk::Cli::Options::Argv#parse
26+
TooManyInstanceVariables:
27+
exclude:
28+
- Skunk::Generator::Html::FileData
29+
- Skunk::Generator::Html::OverviewData
2530
UtilityFunction:
2631
exclude:
2732
- capture_output_streams

.rubocop_todo.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2025-05-02 20:16:50 UTC using RuboCop version 1.75.4.
3+
# on 2025-10-08 01:52:15 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
77
# versions of RuboCop, may require this file to be generated again.
88

99
# Offense count: 1
10-
# Configuration parameters: Severity, Include.
11-
# Include: **/*.gemspec
10+
# Configuration parameters: Severity.
1211
Gemspec/RequiredRubyVersion:
1312
Exclude:
1413
- 'skunk.gemspec'
@@ -36,11 +35,11 @@ Lint/MissingSuper:
3635
Metrics/AbcSize:
3736
Max: 18
3837

39-
# Offense count: 7
38+
# Offense count: 9
4039
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
4140
# AllowedMethods: refine
4241
Metrics/BlockLength:
43-
Max: 76
42+
Max: 131
4443

4544
# Offense count: 2
4645
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
module Skunk
4+
module Generator
5+
module Html
6+
# Data object for individual file information in the HTML overview report
7+
class FileData
8+
attr_reader :file, :skunk_score, :churn_times_cost, :churn, :cost, :coverage
9+
10+
def initialize(module_data)
11+
@file = PathTruncator.truncate(module_data.pathname)
12+
@skunk_score = module_data.skunk_score
13+
@churn_times_cost = module_data.churn_times_cost
14+
@churn = module_data.churn
15+
@cost = module_data.cost.round(2)
16+
@coverage = module_data.coverage.round(2)
17+
end
18+
end
19+
end
20+
end
21+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
require "rubycritic/generators/html/base"
4+
5+
require "skunk/generators/html/path_truncator"
6+
require "skunk/generators/html/overview_data"
7+
8+
module Skunk
9+
module Generator
10+
module Html
11+
# Generates an HTML overview report for the analysed modules.
12+
class Overview < RubyCritic::Generator::Html::Base
13+
def self.erb_template(template_path)
14+
ERB.new(File.read(File.join(TEMPLATES_DIR, template_path)))
15+
end
16+
17+
TEMPLATES_DIR = File.expand_path("templates", __dir__)
18+
TEMPLATE = erb_template("skunk_overview.html.erb")
19+
20+
def initialize(analysed_modules)
21+
super
22+
@analysed_modules = analysed_modules
23+
@data = OverviewData.new(analysed_modules)
24+
end
25+
26+
def file_name
27+
"skunk_overview.html"
28+
end
29+
30+
def render
31+
data = @data
32+
TEMPLATE.result(binding)
33+
end
34+
end
35+
end
36+
end
37+
end
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require "skunk/generators/html/file_data"
4+
require "skunk/generators/html/path_truncator"
5+
6+
module Skunk
7+
module Generator
8+
module Html
9+
# Data object for the HTML overview report
10+
class OverviewData
11+
attr_reader :generated_at, :skunk_version,
12+
:analysed_modules_count, :skunk_score_total, :skunk_score_average,
13+
:worst_pathname, :worst_score, :files
14+
15+
def initialize(analysed_modules)
16+
@analysed_modules = analysed_modules
17+
@generated_at = Time.now.strftime("%Y-%m-%d %H:%M:%S")
18+
@skunk_version = Skunk::VERSION
19+
20+
@analysed_modules_count = non_test_modules.count
21+
@skunk_score_total = non_test_modules.sum(&:skunk_score)
22+
@skunk_score_average = calculate_average
23+
@worst_pathname = PathTruncator.truncate(find_worst_module&.pathname)
24+
@worst_score = find_worst_module&.skunk_score
25+
@files = build_files
26+
end
27+
28+
private
29+
30+
def non_test_modules
31+
@non_test_modules ||= @analysed_modules.reject do |a_module|
32+
module_path = a_module.pathname.dirname.to_s
33+
module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
34+
end
35+
end
36+
37+
def calculate_average
38+
return 0 if @analysed_modules_count.zero?
39+
40+
(@skunk_score_total.to_f / @analysed_modules_count).round(2)
41+
end
42+
43+
def find_worst_module
44+
@find_worst_module ||= sorted_modules.first
45+
end
46+
47+
def sorted_modules
48+
@sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
49+
end
50+
51+
def build_files
52+
@build_files ||= sorted_modules.map do |module_data|
53+
FileData.new(module_data)
54+
end
55+
end
56+
end
57+
end
58+
end
59+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
module Skunk
4+
module Generator
5+
module Html
6+
# Utility class for truncating file paths to show only the relevant project structure
7+
class PathTruncator
8+
attr_reader :file_path
9+
10+
# Common project folder names to truncate from
11+
PROJECT_FOLDERS = %w[app lib src test spec features db].freeze
12+
13+
# Truncates a file path to show only the relevant project structure
14+
# starting from the first project folder found
15+
#
16+
# @param file_path [String] The full file path to truncate
17+
# @return [String] The truncated path starting from the project folder
18+
# :reek:NilCheck
19+
def self.truncate(file_path)
20+
return file_path if file_path.nil?
21+
22+
new(file_path).truncate
23+
end
24+
25+
def initialize(file_path)
26+
@file_path = file_path.to_s
27+
end
28+
29+
# :reek:TooManyStatements
30+
def truncate
31+
return file_path if file_path.empty?
32+
33+
path_parts = file_path.split("/")
34+
folder_index = path_parts.find_index do |part|
35+
PROJECT_FOLDERS.include?(part)
36+
end
37+
38+
if folder_index
39+
# rubocop:disable Style/SlicingWithRange
40+
path_parts[folder_index..-1].join("/")
41+
# rubocop:enable Style/SlicingWithRange
42+
else
43+
file_path
44+
end
45+
end
46+
end
47+
end
48+
end
49+
end

0 commit comments

Comments
 (0)