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
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Skunk Report</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* Modern CSS Reset */
|
|
9
|
+
*, *::before, *::after {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* {
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
html, body {
|
|
19
|
+
height: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
body {
|
|
23
|
+
line-height: 1.5;
|
|
24
|
+
-webkit-font-smoothing: antialiased;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
img, picture, video, canvas, svg {
|
|
28
|
+
display: block;
|
|
29
|
+
max-width: 100%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
p, h1, h2, h3, h4, h5, h6 {
|
|
33
|
+
overflow-wrap: break-word;
|
|
34
|
+
}
|
|
35
|
+
/* End Modern CSS Reset */
|
|
36
|
+
|
|
37
|
+
:root {
|
|
38
|
+
/* Color Palette */
|
|
39
|
+
--primary-gradient-start:rgb(8, 58, 19);
|
|
40
|
+
--primary-gradient-end: rgb(8, 98, 44);;
|
|
41
|
+
--background-color: #f5f5f5;
|
|
42
|
+
--text-color: #333;
|
|
43
|
+
--text-color-light: #666;
|
|
44
|
+
--white: #ffffff;
|
|
45
|
+
--border-color: #eee;
|
|
46
|
+
--shadow-color: rgba(0,0,0,0.1);
|
|
47
|
+
|
|
48
|
+
--cards-background-color: #f8f9fa;
|
|
49
|
+
--card-background-color: #ffffff;
|
|
50
|
+
|
|
51
|
+
/* Skunk Score Colors */
|
|
52
|
+
--score-high: #e74c3c;
|
|
53
|
+
--score-medium: #f39c12;
|
|
54
|
+
--score-low: #27ae60;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
body {
|
|
58
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
59
|
+
margin: 0;
|
|
60
|
+
padding: 2rem;
|
|
61
|
+
background-color: var(--background-color);
|
|
62
|
+
color: var(--text-color);
|
|
63
|
+
}
|
|
64
|
+
.container {
|
|
65
|
+
margin: 0 auto;
|
|
66
|
+
background: var(--white);
|
|
67
|
+
border-radius: 2rem;
|
|
68
|
+
box-shadow: 0 2px 10px var(--shadow-color);
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
}
|
|
71
|
+
.header {
|
|
72
|
+
background: linear-gradient(135deg, var(--primary-gradient-start) 0%, var(--primary-gradient-end) 100%);
|
|
73
|
+
color: var(--white);
|
|
74
|
+
padding: 2rem;
|
|
75
|
+
text-align: center;
|
|
76
|
+
display: flex;
|
|
77
|
+
flex-direction: column;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
}
|
|
81
|
+
.header h1 {
|
|
82
|
+
font-size: 3rem;
|
|
83
|
+
font-weight: 300;
|
|
84
|
+
}
|
|
85
|
+
.header p {
|
|
86
|
+
font-size: 1.5rem;
|
|
87
|
+
}
|
|
88
|
+
.cards {
|
|
89
|
+
display: grid;
|
|
90
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
91
|
+
gap: 2rem;
|
|
92
|
+
padding: 2rem;
|
|
93
|
+
background: var(--cards-background-color);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.card {
|
|
97
|
+
background: var(--card-background-color);
|
|
98
|
+
padding: 2rem;
|
|
99
|
+
border-radius: 2rem;
|
|
100
|
+
text-align: center;
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
justify-content: center;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.value {
|
|
107
|
+
font-size: clamp(1rem, 2vw, 1.5rem);
|
|
108
|
+
font-weight: bold;
|
|
109
|
+
color: var(--text-color);
|
|
110
|
+
margin-bottom: 0.5rem;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.label {
|
|
114
|
+
color: var(--text-color-light);
|
|
115
|
+
font-size: clamp(1rem, 2vw, 1.5rem);
|
|
116
|
+
text-transform: uppercase;
|
|
117
|
+
letter-spacing: 0.5px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.footer {
|
|
121
|
+
background: var(--cards-background-color);
|
|
122
|
+
padding: 1rem 2rem;
|
|
123
|
+
text-align: center;
|
|
124
|
+
color: var(--text-color-light);
|
|
125
|
+
border-top: 1px solid var(--border-color);
|
|
126
|
+
font-size: 1rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.table-section {
|
|
130
|
+
padding: 2rem;
|
|
131
|
+
background: var(--white);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.table-section h2 {
|
|
135
|
+
margin-bottom: 1.5rem;
|
|
136
|
+
color: var(--text-color);
|
|
137
|
+
font-size: clamp(1.5rem, 3vw, 2rem);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Responsive Table Styles */
|
|
141
|
+
.skunk-results {
|
|
142
|
+
width: 100%;
|
|
143
|
+
border-collapse: collapse;
|
|
144
|
+
background: var(--white);
|
|
145
|
+
border-radius: 1rem;
|
|
146
|
+
overflow: hidden;
|
|
147
|
+
box-shadow: 0 2px 8px var(--shadow-color);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.skunk-results thead {
|
|
151
|
+
background: var(--primary-gradient-end);
|
|
152
|
+
color: var(--white);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.skunk-results th {
|
|
156
|
+
padding: 1rem;
|
|
157
|
+
text-align: right;
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
font-size: 1rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.skunk-results th:first-child {
|
|
163
|
+
text-align: left;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.table-row {
|
|
167
|
+
border-bottom: 1px solid var(--border-color);
|
|
168
|
+
transition: background-color 0.2s ease;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.table-row:hover {
|
|
172
|
+
background-color: var(--cards-background-color);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.table-row td {
|
|
176
|
+
padding: 1rem;
|
|
177
|
+
border: none;
|
|
178
|
+
text-align: right;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.table-row .filename {
|
|
182
|
+
text-align: left;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.table-row td label {
|
|
186
|
+
display: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.filename {
|
|
190
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
191
|
+
word-break: break-all;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.score {
|
|
195
|
+
font-weight: bold;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Sortable Table Headers */
|
|
199
|
+
.table-header {
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
user-select: none;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.table-header:hover {
|
|
205
|
+
text-decoration: underline;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.table-header-asc::after {
|
|
209
|
+
content: " ▲";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.table-header-desc::after {
|
|
213
|
+
content: " ▼";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* Mobile Card Layout */
|
|
217
|
+
@media screen and (max-width: 800px) {
|
|
218
|
+
.skunk-results thead {
|
|
219
|
+
display: none;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.skunk-results {
|
|
223
|
+
box-shadow: none;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.table-row {
|
|
227
|
+
display: grid;
|
|
228
|
+
grid-template-columns: 1fr;
|
|
229
|
+
height: auto;
|
|
230
|
+
margin-bottom: 1rem;
|
|
231
|
+
border: 1px solid var(--border-color);
|
|
232
|
+
border-radius: 1rem;
|
|
233
|
+
background: var(--card-background-color);
|
|
234
|
+
box-shadow: 0 2px 4px var(--shadow-color);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.table-row .filename {
|
|
238
|
+
background: var(--cards-background-color);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.table-row td {
|
|
242
|
+
display: flex;
|
|
243
|
+
flex-direction: row;
|
|
244
|
+
justify-content: space-between;
|
|
245
|
+
padding: 0.5rem;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.table-row td label {
|
|
249
|
+
display: inline-block;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
</style>
|
|
253
|
+
</head>
|
|
254
|
+
<body>
|
|
255
|
+
<div class="container">
|
|
256
|
+
<div class="header">
|
|
257
|
+
<h1>Skunk Report</h1>
|
|
258
|
+
<p>Code Quality vs. Code Coverage Analysis</p>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<section class="cards">
|
|
262
|
+
<div class="card">
|
|
263
|
+
<div class="value"><%= analysed_modules_count %></div>
|
|
264
|
+
<div class="label">Modules Analysed</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="card">
|
|
267
|
+
<div class="value"><%= skunk_score_total %></div>
|
|
268
|
+
<div class="label">Total Skunk Score</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="card">
|
|
271
|
+
<div class="value"><%= skunk_score_average %></div>
|
|
272
|
+
<div class="label">Average Skunk Score</div>
|
|
273
|
+
</div>
|
|
274
|
+
</section>
|
|
275
|
+
|
|
276
|
+
<section class="table-section">
|
|
277
|
+
<h2>Skunk Analysis</h2>
|
|
278
|
+
<table id="skunkTable" class="skunk-results">
|
|
279
|
+
<thead>
|
|
280
|
+
<tr>
|
|
281
|
+
<th class="table-header">File</th>
|
|
282
|
+
<th class="table-header table-header-desc">Skunk Score</th>
|
|
283
|
+
<th class="table-header">Churn × Cost</th>
|
|
284
|
+
<th class="table-header">Churn</th>
|
|
285
|
+
<th class="table-header">Cost</th>
|
|
286
|
+
<th class="table-header">Coverage</th>
|
|
287
|
+
</tr>
|
|
288
|
+
</thead>
|
|
289
|
+
<tbody>
|
|
290
|
+
<% files.each do |item| %>
|
|
291
|
+
<tr class="table-row">
|
|
292
|
+
<td class="filename">
|
|
293
|
+
<span><%= item.file %></span>
|
|
294
|
+
</td>
|
|
295
|
+
<td class="score">
|
|
296
|
+
<label>Skunk Score</label>
|
|
297
|
+
<span><%= item.skunk_score %></span>
|
|
298
|
+
</td>
|
|
299
|
+
<td>
|
|
300
|
+
<label>Churn × Cost</label>
|
|
301
|
+
<span><%= item.churn_times_cost %></span>
|
|
302
|
+
</td>
|
|
303
|
+
<td>
|
|
304
|
+
<label>Churn</label>
|
|
305
|
+
<span><%= item.churn %></span>
|
|
306
|
+
</td>
|
|
307
|
+
<td>
|
|
308
|
+
<label>Cost</label>
|
|
309
|
+
<span><%= item.cost %></span>
|
|
310
|
+
</td>
|
|
311
|
+
<td>
|
|
312
|
+
<label>Coverage</label>
|
|
313
|
+
<span><%= item.coverage %></span>
|
|
314
|
+
</td>
|
|
315
|
+
</tr>
|
|
316
|
+
<% end %>
|
|
317
|
+
</tbody>
|
|
318
|
+
</table>
|
|
319
|
+
</section>
|
|
320
|
+
|
|
321
|
+
<footer class="footer">
|
|
322
|
+
<p>Generated with Skunk v<%= @skunk_version %> on <%= @generated_at %></p>
|
|
323
|
+
</footer>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<script>
|
|
327
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
328
|
+
const table = document.getElementById("skunkTable");
|
|
329
|
+
const headers = table.querySelectorAll(".table-header");
|
|
330
|
+
const tbody = table.querySelector("tbody");
|
|
331
|
+
const numericColumns = [1, 2, 3, 4, 5];
|
|
332
|
+
|
|
333
|
+
const cellValue = (row, columnIndex) =>
|
|
334
|
+
row.cells[columnIndex].querySelector("span:last-child").textContent.trim();
|
|
335
|
+
|
|
336
|
+
const compareRows = (rowA, rowB, columnIndex, ascending) => {
|
|
337
|
+
const valueA = cellValue(rowA, columnIndex);
|
|
338
|
+
const valueB = cellValue(rowB, columnIndex);
|
|
339
|
+
|
|
340
|
+
if (numericColumns.includes(columnIndex)) {
|
|
341
|
+
return ascending ? Number(valueA) - Number(valueB) : Number(valueB) - Number(valueA);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return ascending ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
headers.forEach((header, columnIndex) => {
|
|
348
|
+
header.addEventListener("click", () => {
|
|
349
|
+
const ascending = !header.classList.contains("table-header-asc");
|
|
350
|
+
const rows = Array.from(tbody.querySelectorAll(".table-row"));
|
|
351
|
+
|
|
352
|
+
headers.forEach((otherHeader) => {
|
|
353
|
+
otherHeader.classList.remove("table-header-asc", "table-header-desc");
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
header.classList.add(ascending ? "table-header-asc" : "table-header-desc");
|
|
357
|
+
|
|
358
|
+
rows
|
|
359
|
+
.sort((rowA, rowB) => compareRows(rowA, rowB, columnIndex, ascending))
|
|
360
|
+
.forEach((row) => tbody.appendChild(row));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
</script>
|
|
365
|
+
</body>
|
|
366
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
require "rubycritic/browser"
|
|
6
|
+
require "rubycritic/generators/html_report"
|
|
7
|
+
|
|
8
|
+
require "skunk/generators/html/overview"
|
|
9
|
+
|
|
10
|
+
module Skunk
|
|
11
|
+
module Generator
|
|
12
|
+
# Generates an HTML report for the analysed modules.
|
|
13
|
+
class HtmlReport < RubyCritic::Generator::HtmlReport
|
|
14
|
+
def initialize(analysed_modules)
|
|
15
|
+
super
|
|
16
|
+
@analysed_modules = analysed_modules
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate_report
|
|
20
|
+
create_directories_and_files
|
|
21
|
+
puts "#{report_name} generated at #{report_location}"
|
|
22
|
+
browser.open unless RubyCritic::Config.no_browser
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_directories_and_files
|
|
26
|
+
Array(generators).each do |generator|
|
|
27
|
+
FileUtils.mkdir_p(generator.file_directory)
|
|
28
|
+
File.write(generator.file_pathname, generator.render)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def generators
|
|
35
|
+
@generators ||= [
|
|
36
|
+
overview_generator
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def overview_generator
|
|
41
|
+
@overview_generator ||= Skunk::Generator::Html::Overview.new(@analysed_modules)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def report_name
|
|
45
|
+
self.class.name.split("::").last
|
|
46
|
+
.gsub(/([a-z])([A-Z])/, '\1 \2')
|
|
47
|
+
.downcase
|
|
48
|
+
.capitalize
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,56 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "skunk/config"
|
|
6
|
+
require "skunk/rubycritic/analysed_modules_collection"
|
|
4
7
|
|
|
5
8
|
module Skunk
|
|
6
9
|
module Generator
|
|
7
10
|
module Json
|
|
8
11
|
# Generates a JSON report for the analysed modules.
|
|
9
|
-
class Simple
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
analysed_modules_count: analysed_modules_count,
|
|
13
|
-
skunk_score_average: skunk_score_average,
|
|
14
|
-
skunk_score_total: skunk_score_total,
|
|
15
|
-
worst_pathname: worst&.pathname,
|
|
16
|
-
worst_score: worst&.skunk_score,
|
|
17
|
-
files: files
|
|
18
|
-
}
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def analysed_modules_count
|
|
24
|
-
@analysed_modules_count ||= non_test_modules.count
|
|
12
|
+
class Simple
|
|
13
|
+
def initialize(analysed_modules)
|
|
14
|
+
@analysed_modules = analysed_modules
|
|
25
15
|
end
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
return 0 if analysed_modules_count.zero?
|
|
17
|
+
FILE_NAME = "skunk_report.json"
|
|
29
18
|
|
|
30
|
-
|
|
19
|
+
def render
|
|
20
|
+
JSON.dump(data)
|
|
31
21
|
end
|
|
32
22
|
|
|
33
|
-
def
|
|
34
|
-
@
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def non_test_modules
|
|
38
|
-
@non_test_modules ||= @analysed_modules.reject do |a_module|
|
|
39
|
-
module_path = a_module.pathname.dirname.to_s
|
|
40
|
-
module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def worst
|
|
45
|
-
@worst ||= sorted_modules.first
|
|
23
|
+
def data
|
|
24
|
+
@analysed_modules.to_hash
|
|
46
25
|
end
|
|
47
26
|
|
|
48
|
-
def
|
|
49
|
-
@
|
|
27
|
+
def file_directory
|
|
28
|
+
@file_directory ||= Pathname.new(Skunk::Config.root)
|
|
50
29
|
end
|
|
51
30
|
|
|
52
|
-
def
|
|
53
|
-
|
|
31
|
+
def file_pathname
|
|
32
|
+
file_directory.join(FILE_NAME)
|
|
54
33
|
end
|
|
55
34
|
end
|
|
56
35
|
end
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "rubycritic/generators/json_report"
|
|
4
|
-
|
|
5
3
|
require "skunk/generators/json/simple"
|
|
6
4
|
|
|
7
5
|
module Skunk
|
|
8
6
|
module Generator
|
|
9
7
|
# Generates a JSON report for the analysed modules.
|
|
10
|
-
class JsonReport
|
|
8
|
+
class JsonReport
|
|
11
9
|
def initialize(analysed_modules)
|
|
12
|
-
super
|
|
13
10
|
@analysed_modules = analysed_modules
|
|
14
11
|
end
|
|
15
12
|
|
|
13
|
+
def generate_report
|
|
14
|
+
FileUtils.mkdir_p(generator.file_directory)
|
|
15
|
+
puts "#{report_name} generated at #{file_path}"
|
|
16
|
+
File.write(file_path, generator.render)
|
|
17
|
+
end
|
|
18
|
+
|
|
16
19
|
private
|
|
17
20
|
|
|
18
21
|
def generator
|
|
19
22
|
Skunk::Generator::Json::Simple.new(@analysed_modules)
|
|
20
23
|
end
|
|
24
|
+
|
|
25
|
+
def report_name
|
|
26
|
+
self.class.name.split("::").last
|
|
27
|
+
.gsub(/([a-z])([A-Z])/, '\1 \2')
|
|
28
|
+
.downcase
|
|
29
|
+
.capitalize
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def file_path
|
|
33
|
+
generator.file_pathname
|
|
34
|
+
end
|
|
21
35
|
end
|
|
22
36
|
end
|
|
23
37
|
end
|
data/lib/skunk/reporter.rb
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "skunk/config"
|
|
4
|
+
|
|
3
5
|
module Skunk
|
|
4
6
|
# Pick the right report generator based on the format specified in the
|
|
5
7
|
# configuration. If the format is not supported, it will default to ConsoleReport.
|
|
6
8
|
module Reporter
|
|
7
|
-
REPORT_GENERATOR_CLASS_FORMATS = %i[json].freeze
|
|
8
|
-
|
|
9
9
|
def self.generate_report(analysed_modules)
|
|
10
|
-
|
|
10
|
+
Config.formats.uniq.each do |format|
|
|
11
11
|
report_generator_class(format).new(analysed_modules).generate_report
|
|
12
12
|
end
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def self.report_generator_class(config_format)
|
|
16
|
-
if
|
|
16
|
+
if Config.supported_format?(config_format)
|
|
17
17
|
require "skunk/generators/#{config_format}_report"
|
|
18
18
|
Generator.const_get("#{config_format.capitalize}Report")
|
|
19
19
|
else
|
|
@@ -3,15 +3,100 @@
|
|
|
3
3
|
require "rubycritic/core/analysed_modules_collection"
|
|
4
4
|
|
|
5
5
|
module RubyCritic
|
|
6
|
-
#
|
|
6
|
+
# Extends RubyCritic::AnalysedModulesCollection to add Skunk analysis methods
|
|
7
7
|
class AnalysedModulesCollection
|
|
8
|
+
# Returns the count of non-test modules
|
|
9
|
+
# @return [Integer]
|
|
10
|
+
def analysed_modules_count
|
|
11
|
+
@analysed_modules_count ||= non_test_modules.count
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Returns the total Skunk score across all non-test modules
|
|
15
|
+
# @return [Float]
|
|
16
|
+
def skunk_score_total
|
|
17
|
+
@skunk_score_total ||= non_test_modules.sum(&:skunk_score)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the average Skunk score across all non-test modules
|
|
21
|
+
# @return [Float]
|
|
8
22
|
def skunk_score_average
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
23
|
+
return 0.0 if analysed_modules_count.zero?
|
|
24
|
+
|
|
25
|
+
(skunk_score_total.to_d / analysed_modules_count).to_f.round(2)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the total churn times cost across all non-test modules
|
|
29
|
+
# @return [Float]
|
|
30
|
+
def total_churn_times_cost
|
|
31
|
+
@total_churn_times_cost ||= non_test_modules.sum(&:churn_times_cost)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the module with the highest Skunk score (worst performing)
|
|
35
|
+
# @return [RubyCritic::AnalysedModule, nil]
|
|
36
|
+
def worst_module
|
|
37
|
+
@worst_module ||= sorted_modules.first
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns modules sorted by Skunk score in descending order (worst first)
|
|
41
|
+
# @return [Array<RubyCritic::AnalysedModule>]
|
|
42
|
+
def sorted_modules
|
|
43
|
+
@sorted_modules ||= non_test_modules.sort_by(&:skunk_score).reverse!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns only non-test modules (excludes test and spec directories)
|
|
47
|
+
# @return [Array<RubyCritic::AnalysedModule>]
|
|
48
|
+
def non_test_modules
|
|
49
|
+
@non_test_modules ||= reject do |a_module|
|
|
50
|
+
test_module?(a_module)
|
|
14
51
|
end
|
|
15
52
|
end
|
|
53
|
+
|
|
54
|
+
# Returns a hash representation of the analysis results
|
|
55
|
+
# @return [Hash]
|
|
56
|
+
def to_hash
|
|
57
|
+
{
|
|
58
|
+
analysed_modules_count: analysed_modules_count,
|
|
59
|
+
skunk_score_total: skunk_score_total,
|
|
60
|
+
skunk_score_average: skunk_score_average,
|
|
61
|
+
total_churn_times_cost: total_churn_times_cost,
|
|
62
|
+
worst_pathname: worst_module&.pathname,
|
|
63
|
+
worst_score: worst_module&.skunk_score,
|
|
64
|
+
files: files_as_hash
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns files as an array of hashes (for JSON serialization)
|
|
69
|
+
# @return [Array<Hash>]
|
|
70
|
+
def files_as_hash
|
|
71
|
+
@files_as_hash ||= sorted_modules.map(&:to_hash)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Determines if a module is a test module based on its path
|
|
77
|
+
# @param a_module [RubyCritic::AnalysedModule] The module to check
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def test_module?(a_module)
|
|
80
|
+
pathname = a_module.pathname
|
|
81
|
+
directory_is_test?(pathname) || filename_is_test?(pathname)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Checks if the directory path indicates a test module
|
|
85
|
+
# @param pathname [Pathname] The pathname to check
|
|
86
|
+
# @return [Boolean]
|
|
87
|
+
# :reek:UtilityFunction
|
|
88
|
+
def directory_is_test?(pathname)
|
|
89
|
+
module_path = pathname.dirname.to_s
|
|
90
|
+
module_path.start_with?("test", "spec") || module_path.end_with?("test", "spec")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Checks if the filename indicates a test module
|
|
94
|
+
# @param pathname [Pathname] The pathname to check
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
# :reek:UtilityFunction
|
|
97
|
+
def filename_is_test?(pathname)
|
|
98
|
+
filename = pathname.basename.to_s
|
|
99
|
+
filename.end_with?("_test.rb", "_spec.rb")
|
|
100
|
+
end
|
|
16
101
|
end
|
|
17
102
|
end
|
data/lib/skunk/version.rb
CHANGED
data/skunk.gemspec
CHANGED
|
@@ -38,19 +38,19 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
39
39
|
spec.require_paths = ["lib"]
|
|
40
40
|
|
|
41
|
+
spec.add_dependency "ostruct", "~> 0.6"
|
|
42
|
+
spec.add_dependency "path_expander", "< 2.0"
|
|
41
43
|
spec.add_dependency "rubycritic", ">= 4.5.2", "< 5.0"
|
|
42
44
|
spec.add_dependency "terminal-table", "~> 3.0"
|
|
43
45
|
|
|
44
46
|
spec.add_development_dependency "codecov", "~> 0.1.16"
|
|
45
|
-
spec.add_development_dependency "debug"
|
|
46
47
|
spec.add_development_dependency "minitest", "< 6"
|
|
47
48
|
spec.add_development_dependency "minitest-around", "~> 0.5.0"
|
|
48
49
|
spec.add_development_dependency "minitest-stub_any_instance", "~> 1.0.2"
|
|
49
50
|
spec.add_development_dependency "minitest-stub-const", "~> 0.6"
|
|
50
51
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
51
52
|
spec.add_development_dependency "reek"
|
|
52
|
-
spec.add_development_dependency "
|
|
53
|
-
spec.add_development_dependency "simplecov", "~> 0.18"
|
|
53
|
+
spec.add_development_dependency "simplecov", ">= 0.18", "< 2"
|
|
54
54
|
spec.add_development_dependency "simplecov-console", "0.5.0"
|
|
55
|
-
spec.add_development_dependency "webmock", "~> 3.
|
|
55
|
+
spec.add_development_dependency "webmock", "~> 3.20.0"
|
|
56
56
|
end
|