code-prune 1.0.0__py3-none-any.whl
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.
- code_prune/.codepruneignore +9 -0
- code_prune/__init__.py +25 -0
- code_prune/cli.py +475 -0
- code_prune/config.ini +2 -0
- code_prune/config.py +44 -0
- code_prune/logging.ini +28 -0
- code_prune/parser.py +67 -0
- code_prune/reporter.py +360 -0
- code_prune/scanner.py +800 -0
- code_prune/store.py +266 -0
- code_prune/symbol_ignore +18 -0
- code_prune-1.0.0.dist-info/METADATA +164 -0
- code_prune-1.0.0.dist-info/RECORD +16 -0
- code_prune-1.0.0.dist-info/WHEEL +4 -0
- code_prune-1.0.0.dist-info/entry_points.txt +3 -0
- code_prune-1.0.0.dist-info/licenses/LICENSE +21 -0
code_prune/reporter.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Report generation for code_prune: JSON and HTML output writers.
|
|
2
|
+
|
|
3
|
+
Author: Ron Webb
|
|
4
|
+
Since: 1.0.0
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import html as _html
|
|
10
|
+
import json
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
|
|
16
|
+
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
17
|
+
|
|
18
|
+
_CSS = """
|
|
19
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
20
|
+
body {
|
|
21
|
+
font-family: 'Segoe UI', Arial, sans-serif;
|
|
22
|
+
background: #f5f7fa;
|
|
23
|
+
color: #2c3e50;
|
|
24
|
+
padding: 2em;
|
|
25
|
+
}
|
|
26
|
+
header {
|
|
27
|
+
background: #1a3a5c;
|
|
28
|
+
color: #ffffff;
|
|
29
|
+
border-radius: 8px;
|
|
30
|
+
padding: 1.2em 1.8em;
|
|
31
|
+
margin-bottom: 1.5em;
|
|
32
|
+
}
|
|
33
|
+
header h1 { font-size: 1.6em; font-weight: 700; letter-spacing: 0.02em; }
|
|
34
|
+
header p { font-size: 0.9em; margin-top: 0.3em; opacity: 0.85; }
|
|
35
|
+
.summary {
|
|
36
|
+
font-size: 0.9em;
|
|
37
|
+
color: #555;
|
|
38
|
+
margin-bottom: 1em;
|
|
39
|
+
}
|
|
40
|
+
.table-wrap { overflow-x: auto; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,.12); }
|
|
41
|
+
table {
|
|
42
|
+
border-collapse: collapse;
|
|
43
|
+
width: 100%;
|
|
44
|
+
background: #ffffff;
|
|
45
|
+
font-size: 0.88em;
|
|
46
|
+
}
|
|
47
|
+
thead tr { background: #1a3a5c; color: #ffffff; }
|
|
48
|
+
th {
|
|
49
|
+
padding: 0.75em 1em;
|
|
50
|
+
text-align: left;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
letter-spacing: 0.03em;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
}
|
|
55
|
+
th.sortable { cursor: pointer; user-select: none; }
|
|
56
|
+
th.sortable:hover { background: #254e80; }
|
|
57
|
+
th.sortable .sort-icon { margin-left: 0.4em; font-size: 0.8em; }
|
|
58
|
+
th.sortable.asc .sort-icon::after { content: '\\25b2'; }
|
|
59
|
+
th.sortable.desc .sort-icon::after { content: '\\25bc'; }
|
|
60
|
+
th.sortable:not(.asc):not(.desc) .sort-icon::after { content: '\\21c5'; opacity: 0.6; }
|
|
61
|
+
.filter-row th { background: #1a3a5c; padding: 0.3em 1em 0.5em; }
|
|
62
|
+
.filter-row input {
|
|
63
|
+
width: 100%;
|
|
64
|
+
padding: 0.3em 0.5em;
|
|
65
|
+
border: 1px solid #3d6b9e;
|
|
66
|
+
border-radius: 4px;
|
|
67
|
+
background: #1e4575;
|
|
68
|
+
color: #fff;
|
|
69
|
+
font-size: 0.85em;
|
|
70
|
+
outline: none;
|
|
71
|
+
}
|
|
72
|
+
.filter-row input::placeholder { color: #aac4e8; }
|
|
73
|
+
.filter-row input:focus { border-color: #7eb3e8; background: #255085; }
|
|
74
|
+
td {
|
|
75
|
+
padding: 0.6em 1em;
|
|
76
|
+
border-bottom: 1px solid #e8ecf0;
|
|
77
|
+
vertical-align: top;
|
|
78
|
+
}
|
|
79
|
+
tbody tr.alt-row { background: #f0f4f8; }
|
|
80
|
+
tbody tr:hover { background: #dde9f7; }
|
|
81
|
+
td.row-num { text-align: right; color: #7f8c8d; font-variant-numeric: tabular-nums; width: 3em; }
|
|
82
|
+
td.category code {
|
|
83
|
+
background: #eef2f7;
|
|
84
|
+
border-radius: 3px;
|
|
85
|
+
padding: 0.1em 0.4em;
|
|
86
|
+
font-family: 'Consolas', 'Courier New', monospace;
|
|
87
|
+
font-size: 0.9em;
|
|
88
|
+
}
|
|
89
|
+
td.location { font-family: 'Consolas', 'Courier New', monospace; font-size: 0.85em; color: #555; }
|
|
90
|
+
td.name code { font-family: 'Consolas', 'Courier New', monospace; font-size: 0.9em; }
|
|
91
|
+
td.message { word-break: break-word; }
|
|
92
|
+
footer { margin-top: 1.5em; font-size: 0.8em; color: #aaa; text-align: center; }
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _write_json(
|
|
97
|
+
findings: list[dict[str, object]],
|
|
98
|
+
path: Path,
|
|
99
|
+
run_date: str,
|
|
100
|
+
paths_scanned: list[str],
|
|
101
|
+
categories: list[str],
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Write findings as a JSON file.
|
|
104
|
+
|
|
105
|
+
Author: Ron Webb
|
|
106
|
+
Since: 1.0.0
|
|
107
|
+
"""
|
|
108
|
+
report = {
|
|
109
|
+
"application": "code-prune",
|
|
110
|
+
"version": __version__,
|
|
111
|
+
"run-date": run_date,
|
|
112
|
+
"paths-scanned": paths_scanned,
|
|
113
|
+
"categories": categories,
|
|
114
|
+
"findings": findings,
|
|
115
|
+
}
|
|
116
|
+
path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _build_thead_html() -> str:
|
|
120
|
+
"""Return the HTML ``<thead>`` element with sortable headers and a filter row.
|
|
121
|
+
|
|
122
|
+
Author: Ron Webb
|
|
123
|
+
Since: 1.0.0
|
|
124
|
+
"""
|
|
125
|
+
return (
|
|
126
|
+
" <thead>\n"
|
|
127
|
+
" <tr>"
|
|
128
|
+
"<th>#</th>"
|
|
129
|
+
'<th class="sortable" onclick="codePruneSortBy(0)">Category<span class="sort-icon"></span></th>'
|
|
130
|
+
'<th class="sortable" onclick="codePruneSortBy(1)">Location<span class="sort-icon"></span></th>'
|
|
131
|
+
'<th class="sortable" onclick="codePruneSortBy(2)">Name<span class="sort-icon"></span></th>'
|
|
132
|
+
"<th>Message</th>"
|
|
133
|
+
"</tr>\n"
|
|
134
|
+
' <tr class="filter-row">'
|
|
135
|
+
"<th></th>"
|
|
136
|
+
'<th><input type="text" id="filter-category" placeholder="Filter category\u2026"'
|
|
137
|
+
' oninput="codePruneFilter()"></th>'
|
|
138
|
+
'<th><input type="text" id="filter-location" placeholder="Filter location\u2026"'
|
|
139
|
+
' oninput="codePruneFilter()"></th>'
|
|
140
|
+
'<th><input type="text" id="filter-name" placeholder="Filter name\u2026"'
|
|
141
|
+
' oninput="codePruneFilter()"></th>'
|
|
142
|
+
"<th></th>"
|
|
143
|
+
"</tr>\n"
|
|
144
|
+
" </thead>\n"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _build_html_scaffold( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
149
|
+
run_date: str,
|
|
150
|
+
total: int,
|
|
151
|
+
rows_html: str,
|
|
152
|
+
script: str,
|
|
153
|
+
paths_scanned: list[str],
|
|
154
|
+
categories: list[str],
|
|
155
|
+
) -> str:
|
|
156
|
+
"""Wrap the table body and script into a complete HTML document.
|
|
157
|
+
|
|
158
|
+
Author: Ron Webb
|
|
159
|
+
Since: 1.0.0
|
|
160
|
+
"""
|
|
161
|
+
thead = _build_thead_html()
|
|
162
|
+
paths_html = "<br>".join(_html.escape(p, quote=False) for p in paths_scanned)
|
|
163
|
+
categories_html = (
|
|
164
|
+
", ".join(_html.escape(c, quote=False) for c in categories) or "(none)"
|
|
165
|
+
)
|
|
166
|
+
return (
|
|
167
|
+
"<!DOCTYPE html>\n"
|
|
168
|
+
'<html lang="en">\n'
|
|
169
|
+
"<head>\n"
|
|
170
|
+
' <meta charset="UTF-8">\n'
|
|
171
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1">\n'
|
|
172
|
+
f" <title>code-prune {__version__} Report</title>\n"
|
|
173
|
+
" <style>\n"
|
|
174
|
+
f"{_CSS}"
|
|
175
|
+
" </style>\n"
|
|
176
|
+
"</head>\n"
|
|
177
|
+
"<body>\n"
|
|
178
|
+
" <header>\n"
|
|
179
|
+
f" <h1>code-prune {__version__} Report</h1>\n"
|
|
180
|
+
f" <p>Date Run: {run_date}</p>\n"
|
|
181
|
+
f" <p>Paths scanned: {paths_html}</p>\n"
|
|
182
|
+
f" <p>Categories: {categories_html}</p>\n"
|
|
183
|
+
" </header>\n"
|
|
184
|
+
f' <p class="summary">Found {total} finding(s)</p>\n'
|
|
185
|
+
' <div class="table-wrap">\n'
|
|
186
|
+
" <table>\n"
|
|
187
|
+
f"{thead}"
|
|
188
|
+
" <tbody>\n"
|
|
189
|
+
f"{rows_html}\n"
|
|
190
|
+
" </tbody>\n"
|
|
191
|
+
" </table>\n"
|
|
192
|
+
" </div>\n"
|
|
193
|
+
f" <footer>Generated by code-prune {__version__}</footer>\n"
|
|
194
|
+
f"{script}"
|
|
195
|
+
"</body>\n"
|
|
196
|
+
"</html>\n"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_SCRIPT = (
|
|
201
|
+
"<script>\n"
|
|
202
|
+
"(function () {\n"
|
|
203
|
+
" var sortCol = -1, sortDir = 1;\n"
|
|
204
|
+
"\n"
|
|
205
|
+
" function sortBy(col) {\n"
|
|
206
|
+
" if (sortCol === col) { sortDir = -sortDir; }\n"
|
|
207
|
+
" else { sortCol = col; sortDir = 1; }\n"
|
|
208
|
+
" applySort();\n"
|
|
209
|
+
" }\n"
|
|
210
|
+
"\n"
|
|
211
|
+
" function applySort() {\n"
|
|
212
|
+
" var tbody = document.querySelector('tbody');\n"
|
|
213
|
+
" var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));\n"
|
|
214
|
+
" var keys = ['category', 'location', 'name'];\n"
|
|
215
|
+
" rows.sort(function (a, b) {\n"
|
|
216
|
+
" if (sortCol < 0 || sortCol > 2) return 0;\n"
|
|
217
|
+
" var key = keys[sortCol];\n"
|
|
218
|
+
" var va = (a.dataset[key] || '').toLowerCase();\n"
|
|
219
|
+
" var vb = (b.dataset[key] || '').toLowerCase();\n"
|
|
220
|
+
" if (va < vb) return -sortDir;\n"
|
|
221
|
+
" if (va > vb) return sortDir;\n"
|
|
222
|
+
" return 0;\n"
|
|
223
|
+
" });\n"
|
|
224
|
+
" rows.forEach(function (r) { tbody.appendChild(r); });\n"
|
|
225
|
+
" updateNumbers();\n"
|
|
226
|
+
" restripe();\n"
|
|
227
|
+
" updateSortIcons();\n"
|
|
228
|
+
" }\n"
|
|
229
|
+
"\n"
|
|
230
|
+
" function updateSortIcons() {\n"
|
|
231
|
+
" var ths = document.querySelectorAll('thead tr:first-child th.sortable');\n"
|
|
232
|
+
" ths.forEach(function (th, i) {\n"
|
|
233
|
+
" th.classList.remove('asc', 'desc');\n"
|
|
234
|
+
" if (i === sortCol) { th.classList.add(sortDir === 1 ? 'asc' : 'desc'); }\n"
|
|
235
|
+
" });\n"
|
|
236
|
+
" }\n"
|
|
237
|
+
"\n"
|
|
238
|
+
" function applyFilter() {\n"
|
|
239
|
+
" var catFilter = document.getElementById('filter-category').value.toLowerCase();\n"
|
|
240
|
+
" var locFilter = document.getElementById('filter-location').value.toLowerCase();\n"
|
|
241
|
+
" var nameFilter = document.getElementById('filter-name').value.toLowerCase();\n"
|
|
242
|
+
" var tbody = document.querySelector('tbody');\n"
|
|
243
|
+
" tbody.querySelectorAll('tr').forEach(function (row) {\n"
|
|
244
|
+
" var catMatch = !catFilter || row.dataset.category.toLowerCase().indexOf(catFilter) !== -1;\n"
|
|
245
|
+
" var locMatch = !locFilter || row.dataset.location.toLowerCase().indexOf(locFilter) !== -1;\n"
|
|
246
|
+
" var nameMatch = !nameFilter || row.dataset.name.toLowerCase().indexOf(nameFilter) !== -1;\n"
|
|
247
|
+
" row.style.display = (catMatch && locMatch && nameMatch) ? '' : 'none';\n"
|
|
248
|
+
" });\n"
|
|
249
|
+
" updateNumbers();\n"
|
|
250
|
+
" restripe();\n"
|
|
251
|
+
" }\n"
|
|
252
|
+
"\n"
|
|
253
|
+
" function updateNumbers() {\n"
|
|
254
|
+
" var num = 1;\n"
|
|
255
|
+
" document.querySelectorAll('tbody tr').forEach(function (row) {\n"
|
|
256
|
+
" if (row.style.display !== 'none') {\n"
|
|
257
|
+
" row.querySelector('.row-num').textContent = num++;\n"
|
|
258
|
+
" }\n"
|
|
259
|
+
" });\n"
|
|
260
|
+
" }\n"
|
|
261
|
+
"\n"
|
|
262
|
+
" function restripe() {\n"
|
|
263
|
+
" var num = 0;\n"
|
|
264
|
+
" document.querySelectorAll('tbody tr').forEach(function (row) {\n"
|
|
265
|
+
" if (row.style.display !== 'none') {\n"
|
|
266
|
+
" num++;\n"
|
|
267
|
+
" row.classList.toggle('alt-row', num % 2 === 0);\n"
|
|
268
|
+
" }\n"
|
|
269
|
+
" });\n"
|
|
270
|
+
" }\n"
|
|
271
|
+
"\n"
|
|
272
|
+
" window.codePruneSortBy = sortBy;\n"
|
|
273
|
+
" window.codePruneFilter = applyFilter;\n"
|
|
274
|
+
"})();\n"
|
|
275
|
+
"</script>\n"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _build_html(
|
|
280
|
+
findings: list[dict[str, object]],
|
|
281
|
+
run_date: str,
|
|
282
|
+
paths_scanned: list[str],
|
|
283
|
+
categories: list[str],
|
|
284
|
+
) -> str:
|
|
285
|
+
"""Build an HTML report string for the given findings.
|
|
286
|
+
|
|
287
|
+
Author: Ron Webb
|
|
288
|
+
Since: 1.0.0
|
|
289
|
+
"""
|
|
290
|
+
rows: list[str] = []
|
|
291
|
+
for idx, finding in enumerate(findings, start=1):
|
|
292
|
+
category = str(finding["category"])
|
|
293
|
+
location = f"{finding['file']}:{finding['line']}:{finding['column']}"
|
|
294
|
+
name = str(finding["name"])
|
|
295
|
+
message = str(finding["message"])
|
|
296
|
+
alt = " alt-row" if idx % 2 == 0 else ""
|
|
297
|
+
rows.append(
|
|
298
|
+
f' <tr class="data-row{alt}"'
|
|
299
|
+
f' data-idx="{idx}"'
|
|
300
|
+
f' data-category="{_html.escape(category, quote=True)}"'
|
|
301
|
+
f' data-location="{_html.escape(location, quote=True)}"'
|
|
302
|
+
f' data-name="{_html.escape(name, quote=True)}">'
|
|
303
|
+
f'<td class="row-num">{idx}</td>'
|
|
304
|
+
f'<td class="category"><code>{_html.escape(category, quote=False)}</code></td>'
|
|
305
|
+
f'<td class="location">{_html.escape(location, quote=False)}</td>'
|
|
306
|
+
f'<td class="name"><code>{_html.escape(name, quote=False)}</code></td>'
|
|
307
|
+
f'<td class="message">{_html.escape(message, quote=False)}</td>'
|
|
308
|
+
f"</tr>"
|
|
309
|
+
)
|
|
310
|
+
rows_html = "\n".join(rows)
|
|
311
|
+
return _build_html_scaffold(
|
|
312
|
+
run_date, len(findings), rows_html, _SCRIPT, paths_scanned, categories
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _write_html(
|
|
317
|
+
findings: list[dict[str, object]],
|
|
318
|
+
path: Path,
|
|
319
|
+
run_date: str,
|
|
320
|
+
paths_scanned: list[str],
|
|
321
|
+
categories: list[str],
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Write findings as an HTML report file.
|
|
324
|
+
|
|
325
|
+
Author: Ron Webb
|
|
326
|
+
Since: 1.0.0
|
|
327
|
+
"""
|
|
328
|
+
path.write_text(
|
|
329
|
+
_build_html(findings, run_date, paths_scanned, categories), encoding="utf-8"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def write_outputs( # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
334
|
+
findings: list[dict[str, object]],
|
|
335
|
+
output_dir: Path,
|
|
336
|
+
stem: str,
|
|
337
|
+
fmt: str,
|
|
338
|
+
paths_scanned: list[str],
|
|
339
|
+
categories: list[str] | None = None,
|
|
340
|
+
) -> list[Path]:
|
|
341
|
+
"""Write output files according to the requested format.
|
|
342
|
+
|
|
343
|
+
Returns the list of paths written.
|
|
344
|
+
|
|
345
|
+
Author: Ron Webb
|
|
346
|
+
Since: 1.0.0
|
|
347
|
+
"""
|
|
348
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
349
|
+
run_date = datetime.now().strftime(_DATE_FORMAT)
|
|
350
|
+
categories = categories or []
|
|
351
|
+
written: list[Path] = []
|
|
352
|
+
if fmt in ("json", "all"):
|
|
353
|
+
json_path = output_dir / f"{stem}.json"
|
|
354
|
+
_write_json(findings, json_path, run_date, paths_scanned, categories)
|
|
355
|
+
written.append(json_path)
|
|
356
|
+
if fmt in ("html", "all"):
|
|
357
|
+
html_path = output_dir / f"{stem}.html"
|
|
358
|
+
_write_html(findings, html_path, run_date, paths_scanned, categories)
|
|
359
|
+
written.append(html_path)
|
|
360
|
+
return written
|