skunk 0.5.4 → 0.6.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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +8 -2
- data/.gitignore +2 -0
- data/.reek.yml +29 -0
- data/.rubocop_todo.yml +22 -19
- data/CHANGELOG.md +15 -1
- data/CONTRIBUTING.md +7 -1
- data/Gemfile +8 -3
- data/README.md +53 -2
- data/Rakefile +11 -4
- data/lib/skunk/cli/application.rb +5 -13
- data/lib/skunk/cli/options/argv.rb +17 -9
- data/lib/skunk/commands/compare.rb +12 -2
- data/lib/skunk/commands/default.rb +0 -3
- data/lib/skunk/commands/shareable.rb +7 -0
- data/lib/skunk/commands/status_reporter.rb +5 -82
- data/lib/skunk/commands/status_sharer.rb +25 -10
- data/lib/skunk/config.rb +128 -0
- data/lib/skunk/generators/console/simple.rb +92 -0
- data/lib/skunk/generators/console_report.rb +48 -0
- data/lib/skunk/generators/html/file_data.rb +21 -0
- data/lib/skunk/generators/html/overview.rb +61 -0
- data/lib/skunk/generators/html/path_truncator.rb +49 -0
- data/lib/skunk/generators/html/templates/skunk_overview.html.erb +366 -0
- data/lib/skunk/generators/html_report.rb +52 -0
- data/lib/skunk/generators/json/simple.rb +16 -37
- data/lib/skunk/generators/json_report.rb +18 -4
- data/lib/skunk/reporter.rb +4 -4
- data/lib/skunk/rubycritic/analysed_modules_collection.rb +91 -6
- data/lib/skunk/version.rb +1 -1
- data/skunk.gemspec +4 -4
- metadata +48 -37
|
@@ -23,7 +23,7 @@ module Skunk
|
|
|
23
23
|
response = post_payload
|
|
24
24
|
|
|
25
25
|
@status_message =
|
|
26
|
-
if Net::HTTPOK
|
|
26
|
+
if response.is_a?(Net::HTTPOK)
|
|
27
27
|
data = JSON.parse response.body
|
|
28
28
|
"Shared at: #{File.join(base_url, data['id'])}"
|
|
29
29
|
else
|
|
@@ -33,22 +33,22 @@ module Skunk
|
|
|
33
33
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
|
-
# :reek:UtilityFunction
|
|
37
36
|
def base_url
|
|
38
|
-
|
|
37
|
+
share_url_empty? ? DEFAULT_URL : share_url
|
|
39
38
|
end
|
|
40
39
|
|
|
41
40
|
def json_summary
|
|
42
41
|
result = {
|
|
43
|
-
total_skunk_score:
|
|
44
|
-
analysed_modules_count: analysed_modules_count,
|
|
45
|
-
skunk_score_average: skunk_score_average,
|
|
42
|
+
total_skunk_score: analysed_modules.skunk_score_total,
|
|
43
|
+
analysed_modules_count: analysed_modules.analysed_modules_count,
|
|
44
|
+
skunk_score_average: analysed_modules.skunk_score_average,
|
|
46
45
|
skunk_version: Skunk::VERSION
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
if
|
|
48
|
+
if analysed_modules&.worst_module
|
|
49
|
+
worst = analysed_modules.worst_module
|
|
50
50
|
result[:worst_skunk_score] = {
|
|
51
|
-
file: worst.pathname
|
|
51
|
+
file: worst.pathname,
|
|
52
52
|
skunk_score: worst.skunk_score
|
|
53
53
|
}
|
|
54
54
|
end
|
|
@@ -57,12 +57,26 @@ module Skunk
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
def json_results
|
|
60
|
-
sorted_modules.map(&:to_hash)
|
|
60
|
+
analysed_modules.sorted_modules.map(&:to_hash)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
# :reek:UtilityFunction
|
|
64
64
|
def not_sharing?
|
|
65
|
-
|
|
65
|
+
!share_enabled? && share_url_empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @return [Boolean] Check if sharing is enabled via environment variable
|
|
69
|
+
def share_enabled?
|
|
70
|
+
ENV["SHARE"] == "true"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Boolean] Check if share URL is empty
|
|
74
|
+
def share_url_empty?
|
|
75
|
+
share_url.empty?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def share_url
|
|
79
|
+
@share_url ||= ENV["SHARE_URL"].to_s
|
|
66
80
|
end
|
|
67
81
|
|
|
68
82
|
def payload
|
|
@@ -79,6 +93,7 @@ module Skunk
|
|
|
79
93
|
def post_payload
|
|
80
94
|
req = Net::HTTP::Post.new(url)
|
|
81
95
|
req.body = payload
|
|
96
|
+
req.content_type = "application/json"
|
|
82
97
|
|
|
83
98
|
http = Net::HTTP.new(url.hostname, url.port)
|
|
84
99
|
if url.scheme == "https"
|
data/lib/skunk/config.rb
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubycritic/configuration"
|
|
4
|
+
|
|
5
|
+
module Skunk
|
|
6
|
+
# Utility module for format validation
|
|
7
|
+
module FormatValidator
|
|
8
|
+
# Supported output formats
|
|
9
|
+
SUPPORTED_FORMATS = %i[json html console].freeze
|
|
10
|
+
|
|
11
|
+
# Check if a format is supported
|
|
12
|
+
# @param format [Symbol] Format to check
|
|
13
|
+
# @return [Boolean] True if format is supported
|
|
14
|
+
def self.supported_format?(format)
|
|
15
|
+
SUPPORTED_FORMATS.include?(format)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Get all supported formats
|
|
19
|
+
# @return [Array<Symbol>] All supported formats
|
|
20
|
+
def self.supported_formats
|
|
21
|
+
SUPPORTED_FORMATS.dup
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Configuration class for Skunk that supports formats
|
|
26
|
+
# Similar to RubyCritic::Configuration but focused only on Skunk's needs
|
|
27
|
+
class Configuration
|
|
28
|
+
# Default format
|
|
29
|
+
DEFAULT_FORMAT = :console
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@formats = [DEFAULT_FORMAT]
|
|
33
|
+
@root = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def set(options = {})
|
|
37
|
+
self.formats = options[:formats] if options.key?(:formats)
|
|
38
|
+
self.root = options[:root] if options.key?(:root)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Get the configured formats
|
|
42
|
+
# @return [Array<Symbol>] Array of format symbols
|
|
43
|
+
attr_reader :formats
|
|
44
|
+
|
|
45
|
+
# Set the formats with validation
|
|
46
|
+
# @param format_list [Array<Symbol>, Symbol] Format(s) to set
|
|
47
|
+
def formats=(format_list)
|
|
48
|
+
format_array = Array(format_list)
|
|
49
|
+
@formats = format_array.select { |format| FormatValidator.supported_format?(format) }
|
|
50
|
+
@formats = [DEFAULT_FORMAT] if @formats.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Path where reports are written. An explicit value (from --out) wins;
|
|
54
|
+
# otherwise it follows RubyCritic::Config.root, which RubyCritic sets on
|
|
55
|
+
# every run, so a stale value from an earlier run is never reused.
|
|
56
|
+
def root
|
|
57
|
+
@root || RubyCritic::Config.root || File.expand_path("tmp/rubycritic", Dir.pwd)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [Boolean] True when the output path was set explicitly
|
|
61
|
+
def root_set?
|
|
62
|
+
!!@root
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def root=(path)
|
|
66
|
+
path_str = path.to_s
|
|
67
|
+
@root = path_str.empty? ? nil : File.expand_path(path_str)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Add a format to the existing list
|
|
71
|
+
# @param format [Symbol] Format to add
|
|
72
|
+
def add_format(format)
|
|
73
|
+
return unless FormatValidator.supported_format?(format)
|
|
74
|
+
|
|
75
|
+
@formats << format unless @formats.include?(format)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Remove a format from the list
|
|
79
|
+
# @param format [Symbol] Format to remove
|
|
80
|
+
def remove_format(format)
|
|
81
|
+
@formats.delete(format)
|
|
82
|
+
@formats = [DEFAULT_FORMAT] if @formats.empty?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Check if a format is supported
|
|
86
|
+
# @param format [Symbol] Format to check
|
|
87
|
+
# @return [Boolean] True if format is supported
|
|
88
|
+
def supported_format?(format)
|
|
89
|
+
FormatValidator.supported_format?(format)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get all supported formats
|
|
93
|
+
# @return [Array<Symbol>] All supported formats
|
|
94
|
+
def supported_formats
|
|
95
|
+
FormatValidator.supported_formats
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Reset to default configuration
|
|
99
|
+
def reset
|
|
100
|
+
@formats = [DEFAULT_FORMAT]
|
|
101
|
+
@root = nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Config module that delegates to Configuration instance
|
|
106
|
+
# Similar to RubyCritic::Config pattern
|
|
107
|
+
module Config
|
|
108
|
+
def self.configuration
|
|
109
|
+
@configuration ||= Configuration.new
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.set(options = {})
|
|
113
|
+
configuration.set(options)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.method_missing(method, *args, &block)
|
|
117
|
+
if configuration.respond_to?(method)
|
|
118
|
+
configuration.public_send(method, *args, &block)
|
|
119
|
+
else
|
|
120
|
+
super
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.respond_to_missing?(symbol, include_private = false)
|
|
125
|
+
configuration.respond_to?(symbol, include_private) || super
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "terminal-table"
|
|
5
|
+
|
|
6
|
+
module Skunk
|
|
7
|
+
module Generator
|
|
8
|
+
module Console
|
|
9
|
+
# Generates a console report for the analysed modules.
|
|
10
|
+
class Simple
|
|
11
|
+
def initialize(analysed_modules)
|
|
12
|
+
@analysed_modules = analysed_modules
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
HEADINGS = %w[file skunk_score churn_times_cost churn cost coverage].freeze
|
|
16
|
+
HEADINGS_WITHOUT_FILE = HEADINGS - %w[file]
|
|
17
|
+
HEADINGS_WITHOUT_FILE_WIDTH = HEADINGS_WITHOUT_FILE.size * 17 # padding
|
|
18
|
+
|
|
19
|
+
TEMPLATE = ERB.new(<<~TEMPL
|
|
20
|
+
<%= _ttable %>
|
|
21
|
+
|
|
22
|
+
SkunkScore Total: <%= total_skunk_score %>
|
|
23
|
+
Modules Analysed: <%= analysed_modules_count %>
|
|
24
|
+
SkunkScore Average: <%= skunk_score_average %>
|
|
25
|
+
<% if worst %>Worst SkunkScore: <%= worst.skunk_score %> (<%= worst.pathname %>)<% end %>
|
|
26
|
+
|
|
27
|
+
Generated with Skunk v<%= Skunk::VERSION %>
|
|
28
|
+
TEMPL
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def render
|
|
32
|
+
opts = table_options.merge(headings: HEADINGS, rows: table)
|
|
33
|
+
_ttable = Terminal::Table.new(opts)
|
|
34
|
+
TEMPLATE.result(binding)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def analysed_modules_count
|
|
40
|
+
@analysed_modules.analysed_modules_count
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def worst
|
|
44
|
+
@analysed_modules.worst_module
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def sorted_modules
|
|
48
|
+
@analysed_modules.sorted_modules
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def total_skunk_score
|
|
52
|
+
@analysed_modules.skunk_score_total
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def total_churn_times_cost
|
|
56
|
+
@analysed_modules.total_churn_times_cost
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def skunk_score_average
|
|
60
|
+
@analysed_modules.skunk_score_average
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def table_options
|
|
64
|
+
return { style: { width: 100 } } if sorted_modules.empty?
|
|
65
|
+
|
|
66
|
+
max = sorted_modules.max_by { |a_mod| a_mod.pathname.to_s.length }
|
|
67
|
+
width = max.pathname.to_s.length + HEADINGS_WITHOUT_FILE_WIDTH
|
|
68
|
+
{
|
|
69
|
+
style: {
|
|
70
|
+
width: width
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def table
|
|
76
|
+
@analysed_modules.files_as_hash.map { |file_hash| self.class.format_hash_row(file_hash) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.format_hash_row(file_hash)
|
|
80
|
+
[
|
|
81
|
+
file_hash[:file],
|
|
82
|
+
file_hash[:skunk_score],
|
|
83
|
+
file_hash[:churn_times_cost],
|
|
84
|
+
file_hash[:churn],
|
|
85
|
+
file_hash[:cost],
|
|
86
|
+
file_hash[:coverage]
|
|
87
|
+
]
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "terminal-table"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
require "skunk/generators/console/simple"
|
|
9
|
+
require "skunk/config"
|
|
10
|
+
|
|
11
|
+
module Skunk
|
|
12
|
+
module Generator
|
|
13
|
+
# Generates a console report for the analysed modules.
|
|
14
|
+
class ConsoleReport
|
|
15
|
+
FILE_NAME = "skunk_report.txt"
|
|
16
|
+
|
|
17
|
+
def initialize(analysed_modules)
|
|
18
|
+
@analysed_modules = analysed_modules
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def generate_report
|
|
22
|
+
content = generator.render
|
|
23
|
+
puts content
|
|
24
|
+
write_file(content) if Skunk::Config.root_set?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def generator
|
|
30
|
+
@generator ||= Skunk::Generator::Console::Simple.new(@analysed_modules)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Only written when --out is given, so a plain `skunk` run stays free of filesystem side effects
|
|
34
|
+
def write_file(content)
|
|
35
|
+
FileUtils.mkdir_p(file_directory)
|
|
36
|
+
File.write(file_pathname, content)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def file_directory
|
|
40
|
+
@file_directory ||= Pathname.new(Skunk::Config.root)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def file_pathname
|
|
44
|
+
file_directory.join(FILE_NAME)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,61 @@
|
|
|
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/file_data"
|
|
7
|
+
require "skunk/rubycritic/analysed_modules_collection"
|
|
8
|
+
|
|
9
|
+
module Skunk
|
|
10
|
+
module Generator
|
|
11
|
+
module Html
|
|
12
|
+
# Generates an HTML overview report for the analysed modules.
|
|
13
|
+
class Overview < RubyCritic::Generator::Html::Base
|
|
14
|
+
def self.erb_template(template_path)
|
|
15
|
+
ERB.new(File.read(File.join(TEMPLATES_DIR, template_path)))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
TEMPLATES_DIR = File.expand_path("templates", __dir__)
|
|
19
|
+
TEMPLATE = erb_template("skunk_overview.html.erb")
|
|
20
|
+
|
|
21
|
+
def initialize(analysed_modules)
|
|
22
|
+
@analysed_modules = analysed_modules
|
|
23
|
+
@generated_at = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
|
24
|
+
@skunk_version = Skunk::VERSION
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def file_name
|
|
28
|
+
"skunk_report.html"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render
|
|
32
|
+
TEMPLATE.result(binding)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def analysed_modules_count
|
|
36
|
+
@analysed_modules.analysed_modules_count
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def skunk_score_total
|
|
40
|
+
@analysed_modules.skunk_score_total
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def skunk_score_average
|
|
44
|
+
@analysed_modules.skunk_score_average
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def files
|
|
48
|
+
@files ||= @analysed_modules.sorted_modules.map do |module_data|
|
|
49
|
+
FileData.new(module_data)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def root_directory
|
|
56
|
+
@root_directory ||= Pathname.new(Skunk::Config.root)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -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
|