skylos 1.0.10__py3-none-any.whl → 2.5.2__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.
- skylos/__init__.py +9 -3
- skylos/analyzer.py +674 -168
- skylos/cfg_visitor.py +60 -0
- skylos/cli.py +719 -235
- skylos/codemods.py +277 -0
- skylos/config.py +50 -0
- skylos/constants.py +78 -0
- skylos/gatekeeper.py +147 -0
- skylos/linter.py +18 -0
- skylos/rules/base.py +20 -0
- skylos/rules/danger/calls.py +119 -0
- skylos/rules/danger/danger.py +157 -0
- skylos/rules/danger/danger_cmd/cmd_flow.py +75 -0
- skylos/rules/danger/danger_fs/__init__.py +0 -0
- skylos/rules/danger/danger_fs/path_flow.py +79 -0
- skylos/rules/danger/danger_net/__init__.py +0 -0
- skylos/rules/danger/danger_net/ssrf_flow.py +80 -0
- skylos/rules/danger/danger_sql/__init__.py +0 -0
- skylos/rules/danger/danger_sql/sql_flow.py +245 -0
- skylos/rules/danger/danger_sql/sql_raw_flow.py +96 -0
- skylos/rules/danger/danger_web/__init__.py +0 -0
- skylos/rules/danger/danger_web/xss_flow.py +170 -0
- skylos/rules/danger/taint.py +110 -0
- skylos/rules/quality/__init__.py +0 -0
- skylos/rules/quality/complexity.py +95 -0
- skylos/rules/quality/logic.py +96 -0
- skylos/rules/quality/nesting.py +101 -0
- skylos/rules/quality/structure.py +99 -0
- skylos/rules/secrets.py +325 -0
- skylos/server.py +554 -0
- skylos/visitor.py +502 -90
- skylos/visitors/__init__.py +0 -0
- skylos/visitors/framework_aware.py +437 -0
- skylos/visitors/test_aware.py +74 -0
- skylos-2.5.2.dist-info/METADATA +21 -0
- skylos-2.5.2.dist-info/RECORD +42 -0
- {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/WHEEL +1 -1
- {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/top_level.txt +0 -1
- skylos-1.0.10.dist-info/METADATA +0 -8
- skylos-1.0.10.dist-info/RECORD +0 -21
- test/compare_tools.py +0 -604
- test/diagnostics.py +0 -364
- test/sample_repo/app.py +0 -13
- test/sample_repo/sample_repo/commands.py +0 -81
- test/sample_repo/sample_repo/models.py +0 -122
- test/sample_repo/sample_repo/routes.py +0 -89
- test/sample_repo/sample_repo/utils.py +0 -36
- test/test_skylos.py +0 -456
- test/test_visitor.py +0 -220
- {test → skylos/rules}/__init__.py +0 -0
- {test/sample_repo → skylos/rules/danger}/__init__.py +0 -0
- {test/sample_repo/sample_repo → skylos/rules/danger/danger_cmd}/__init__.py +0 -0
- {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/entry_points.txt +0 -0
skylos/cli.py
CHANGED
|
@@ -2,335 +2,819 @@ import argparse
|
|
|
2
2
|
import json
|
|
3
3
|
import sys
|
|
4
4
|
import logging
|
|
5
|
-
import
|
|
5
|
+
from skylos.constants import parse_exclude_folders, DEFAULT_EXCLUDE_FOLDERS
|
|
6
|
+
from skylos.server import start_server
|
|
7
|
+
from skylos.analyzer import analyze as run_analyze
|
|
8
|
+
from skylos.codemods import (
|
|
9
|
+
remove_unused_import_cst,
|
|
10
|
+
remove_unused_function_cst,
|
|
11
|
+
comment_out_unused_import_cst,
|
|
12
|
+
comment_out_unused_function_cst,
|
|
13
|
+
)
|
|
14
|
+
from skylos.config import load_config
|
|
15
|
+
from skylos.gatekeeper import run_gate_interaction
|
|
16
|
+
import pathlib
|
|
6
17
|
import skylos
|
|
18
|
+
from collections import defaultdict
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
24
|
+
from rich.theme import Theme
|
|
25
|
+
from rich.logging import RichHandler
|
|
26
|
+
from rich.rule import Rule
|
|
27
|
+
from rich.tree import Tree
|
|
7
28
|
|
|
8
29
|
try:
|
|
9
30
|
import inquirer
|
|
31
|
+
|
|
10
32
|
INTERACTIVE_AVAILABLE = True
|
|
11
33
|
except ImportError:
|
|
12
34
|
INTERACTIVE_AVAILABLE = False
|
|
13
35
|
|
|
36
|
+
|
|
14
37
|
class Colors:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
BOLD =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
# kept for compatibility with some strings. rich styles are primary use
|
|
39
|
+
RED = "\033[91m"
|
|
40
|
+
GREEN = "\033[92m"
|
|
41
|
+
YELLOW = "\033[93m"
|
|
42
|
+
BLUE = "\033[94m"
|
|
43
|
+
MAGENTA = "\033[95m"
|
|
44
|
+
CYAN = "\033[96m"
|
|
45
|
+
BOLD = "\033[1m"
|
|
46
|
+
RESET = "\033[0m"
|
|
47
|
+
GRAY = "\033[90m"
|
|
48
|
+
|
|
26
49
|
|
|
27
50
|
class CleanFormatter(logging.Formatter):
|
|
28
|
-
"""Custom formatter that removes timestamps and log levels for clean output"""
|
|
29
51
|
def format(self, record):
|
|
30
52
|
return record.getMessage()
|
|
31
53
|
|
|
54
|
+
|
|
32
55
|
def setup_logger(output_file=None):
|
|
33
|
-
|
|
56
|
+
theme = Theme(
|
|
57
|
+
{
|
|
58
|
+
"good": "bold green",
|
|
59
|
+
"warn": "bold yellow",
|
|
60
|
+
"bad": "bold red",
|
|
61
|
+
"muted": "dim",
|
|
62
|
+
"brand": "bold cyan",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
console = Console(theme=theme)
|
|
66
|
+
|
|
67
|
+
logger = logging.getLogger("skylos")
|
|
34
68
|
logger.setLevel(logging.INFO)
|
|
35
|
-
|
|
36
69
|
logger.handlers.clear()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
logger.addHandler(
|
|
43
|
-
|
|
70
|
+
|
|
71
|
+
rich_handler = RichHandler(
|
|
72
|
+
console=console, show_time=False, show_path=False, markup=True
|
|
73
|
+
)
|
|
74
|
+
rich_handler.setFormatter(CleanFormatter())
|
|
75
|
+
logger.addHandler(rich_handler)
|
|
76
|
+
|
|
44
77
|
if output_file:
|
|
45
|
-
file_formatter = logging.Formatter(
|
|
78
|
+
file_formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
46
79
|
file_handler = logging.FileHandler(output_file)
|
|
47
80
|
file_handler.setFormatter(file_formatter)
|
|
48
81
|
logger.addHandler(file_handler)
|
|
49
|
-
|
|
82
|
+
|
|
50
83
|
logger.propagate = False
|
|
51
|
-
|
|
84
|
+
logger.console = console
|
|
52
85
|
return logger
|
|
53
86
|
|
|
54
|
-
|
|
87
|
+
|
|
88
|
+
def remove_unused_import(file_path, import_name, line_number):
|
|
89
|
+
path = pathlib.Path(file_path)
|
|
90
|
+
|
|
55
91
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if original_line.startswith(f'import {import_name}'):
|
|
63
|
-
lines[line_idx] = ''
|
|
64
|
-
elif original_line.startswith('import ') and f' {import_name}' in original_line:
|
|
65
|
-
parts = original_line.split(' ', 1)[1].split(',')
|
|
66
|
-
new_parts = [p.strip() for p in parts if p.strip() != import_name]
|
|
67
|
-
if new_parts:
|
|
68
|
-
lines[line_idx] = f'import {", ".join(new_parts)}\n'
|
|
69
|
-
else:
|
|
70
|
-
lines[line_idx] = ''
|
|
71
|
-
elif original_line.startswith('from ') and import_name in original_line:
|
|
72
|
-
if f'import {import_name}' in original_line and ',' not in original_line:
|
|
73
|
-
lines[line_idx] = ''
|
|
74
|
-
else:
|
|
75
|
-
parts = original_line.split('import ', 1)[1].split(',')
|
|
76
|
-
new_parts = [p.strip() for p in parts if p.strip() != import_name]
|
|
77
|
-
if new_parts:
|
|
78
|
-
prefix = original_line.split(' import ')[0]
|
|
79
|
-
lines[line_idx] = f'{prefix} import {", ".join(new_parts)}\n'
|
|
80
|
-
else:
|
|
81
|
-
lines[line_idx] = ''
|
|
82
|
-
|
|
83
|
-
with open(file_path, 'w') as f:
|
|
84
|
-
f.writelines(lines)
|
|
85
|
-
|
|
92
|
+
src = path.read_text(encoding="utf-8")
|
|
93
|
+
new_code, changed = remove_unused_import_cst(src, import_name, line_number)
|
|
94
|
+
if not changed:
|
|
95
|
+
return False
|
|
96
|
+
path.write_text(new_code, encoding="utf-8")
|
|
86
97
|
return True
|
|
98
|
+
|
|
87
99
|
except Exception as e:
|
|
88
100
|
logging.error(f"Failed to remove import {import_name} from {file_path}: {e}")
|
|
89
101
|
return False
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
|
|
104
|
+
def remove_unused_function(file_path, function_name, line_number):
|
|
105
|
+
path = pathlib.Path(file_path)
|
|
106
|
+
|
|
92
107
|
try:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
f.write('\n'.join(new_lines) + '\n')
|
|
127
|
-
|
|
128
|
-
return True
|
|
129
|
-
|
|
108
|
+
src = path.read_text(encoding="utf-8")
|
|
109
|
+
new_code, changed = remove_unused_function_cst(src, function_name, line_number)
|
|
110
|
+
if not changed:
|
|
111
|
+
return False
|
|
112
|
+
path.write_text(new_code, encoding="utf-8")
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logging.error(
|
|
117
|
+
f"Failed to remove function {function_name} from {file_path}: {e}"
|
|
118
|
+
)
|
|
119
|
+
return False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def comment_out_unused_import(
|
|
123
|
+
file_path, import_name, line_number, marker="SKYLOS DEADCODE"
|
|
124
|
+
):
|
|
125
|
+
path = pathlib.Path(file_path)
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
src = path.read_text(encoding="utf-8")
|
|
129
|
+
new_code, changed = comment_out_unused_import_cst(
|
|
130
|
+
src, import_name, line_number, marker=marker
|
|
131
|
+
)
|
|
132
|
+
if not changed:
|
|
133
|
+
return False
|
|
134
|
+
path.write_text(new_code, encoding="utf-8")
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logging.error(
|
|
139
|
+
f"Failed to comment out import {import_name} from {file_path}: {e}"
|
|
140
|
+
)
|
|
130
141
|
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def comment_out_unused_function(
|
|
145
|
+
file_path, function_name, line_number, marker="SKYLOS DEADCODE"
|
|
146
|
+
):
|
|
147
|
+
path = pathlib.Path(file_path)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
src = path.read_text(encoding="utf-8")
|
|
151
|
+
new_code, changed = comment_out_unused_function_cst(
|
|
152
|
+
src, function_name, line_number, marker=marker
|
|
153
|
+
)
|
|
154
|
+
if not changed:
|
|
155
|
+
return False
|
|
156
|
+
path.write_text(new_code, encoding="utf-8")
|
|
157
|
+
return True
|
|
158
|
+
|
|
131
159
|
except Exception as e:
|
|
132
|
-
logging.error(
|
|
160
|
+
logging.error(
|
|
161
|
+
f"Failed to comment out function {function_name} from {file_path}: {e}"
|
|
162
|
+
)
|
|
133
163
|
return False
|
|
134
164
|
|
|
135
|
-
|
|
165
|
+
|
|
166
|
+
def _shorten_path(file_path, root_path=None):
|
|
167
|
+
if not file_path:
|
|
168
|
+
return "?"
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
p = pathlib.Path(file_path)
|
|
172
|
+
except TypeError:
|
|
173
|
+
return str(file_path)
|
|
174
|
+
|
|
175
|
+
if root_path is not None:
|
|
176
|
+
try:
|
|
177
|
+
root = pathlib.Path(root_path)
|
|
178
|
+
if root.is_file():
|
|
179
|
+
root = root.parent
|
|
180
|
+
p = p.resolve()
|
|
181
|
+
root = root.resolve()
|
|
182
|
+
rel = p.relative_to(root)
|
|
183
|
+
return str(rel)
|
|
184
|
+
except Exception:
|
|
185
|
+
return p.name
|
|
186
|
+
|
|
187
|
+
return str(p)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def interactive_selection(
|
|
191
|
+
console: Console, unused_functions, unused_imports, root_path=None
|
|
192
|
+
):
|
|
136
193
|
if not INTERACTIVE_AVAILABLE:
|
|
137
|
-
|
|
194
|
+
console.print(
|
|
195
|
+
"[bad]Interactive mode requires 'inquirer'. Install with: pip install inquirer[/bad]"
|
|
196
|
+
)
|
|
138
197
|
return [], []
|
|
139
|
-
|
|
198
|
+
|
|
140
199
|
selected_functions = []
|
|
141
200
|
selected_imports = []
|
|
142
|
-
|
|
201
|
+
|
|
143
202
|
if unused_functions:
|
|
144
|
-
|
|
145
|
-
|
|
203
|
+
console.print(
|
|
204
|
+
"\n[brand][bold]Select unused functions to remove (space to select):[/bold][/brand]"
|
|
205
|
+
)
|
|
206
|
+
|
|
146
207
|
function_choices = []
|
|
147
208
|
for item in unused_functions:
|
|
148
|
-
|
|
209
|
+
short = _shorten_path(item.get("file"), root_path)
|
|
210
|
+
choice_text = f"{item['name']} ({short}:{item['line']})"
|
|
149
211
|
function_choices.append((choice_text, item))
|
|
150
|
-
|
|
212
|
+
|
|
151
213
|
questions = [
|
|
152
|
-
inquirer.Checkbox(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
214
|
+
inquirer.Checkbox(
|
|
215
|
+
"functions",
|
|
216
|
+
message="Select functions to remove",
|
|
217
|
+
choices=function_choices,
|
|
218
|
+
)
|
|
156
219
|
]
|
|
157
|
-
|
|
158
220
|
answers = inquirer.prompt(questions)
|
|
159
221
|
if answers:
|
|
160
|
-
selected_functions = answers[
|
|
161
|
-
|
|
222
|
+
selected_functions = answers["functions"]
|
|
223
|
+
|
|
162
224
|
if unused_imports:
|
|
163
|
-
|
|
164
|
-
|
|
225
|
+
console.print(
|
|
226
|
+
"\n[brand][bold]Select unused imports to act on (space to select):[/bold][/brand]"
|
|
227
|
+
)
|
|
228
|
+
|
|
165
229
|
import_choices = []
|
|
166
230
|
for item in unused_imports:
|
|
167
|
-
|
|
231
|
+
short = _shorten_path(item.get("file"), root_path)
|
|
232
|
+
choice_text = f"{item['name']} ({short}:{item['line']})"
|
|
168
233
|
import_choices.append((choice_text, item))
|
|
169
|
-
|
|
234
|
+
|
|
170
235
|
questions = [
|
|
171
|
-
inquirer.Checkbox(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
236
|
+
inquirer.Checkbox(
|
|
237
|
+
"imports", message="Select imports to remove", choices=import_choices
|
|
238
|
+
)
|
|
175
239
|
]
|
|
176
|
-
|
|
177
240
|
answers = inquirer.prompt(questions)
|
|
178
241
|
if answers:
|
|
179
|
-
selected_imports = answers[
|
|
180
|
-
|
|
242
|
+
selected_imports = answers["imports"]
|
|
243
|
+
|
|
181
244
|
return selected_functions, selected_imports
|
|
182
245
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
246
|
+
|
|
247
|
+
def print_badge(
|
|
248
|
+
dead_code_count,
|
|
249
|
+
logger,
|
|
250
|
+
*,
|
|
251
|
+
danger_enabled=False,
|
|
252
|
+
danger_count=0,
|
|
253
|
+
quality_enabled=False,
|
|
254
|
+
quality_count=0,
|
|
255
|
+
):
|
|
256
|
+
console: Console = logger.console
|
|
257
|
+
console.print(Rule(style="muted"))
|
|
258
|
+
|
|
259
|
+
has_dead_code = dead_code_count > 0
|
|
260
|
+
has_danger = danger_enabled and danger_count > 0
|
|
261
|
+
has_quality = quality_enabled and quality_count > 0
|
|
262
|
+
|
|
263
|
+
if not has_dead_code and not has_danger and not has_quality:
|
|
264
|
+
console.print(
|
|
265
|
+
Panel.fit(
|
|
266
|
+
"[good]Your code is 100% dead-code free![/good]\nAdd this badge to your README:",
|
|
267
|
+
border_style="good",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
console.print("```markdown")
|
|
271
|
+
console.print(
|
|
272
|
+
""
|
|
273
|
+
)
|
|
274
|
+
console.print("```")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
headline = f"Found {dead_code_count} dead-code items"
|
|
278
|
+
if danger_enabled:
|
|
279
|
+
headline += f" and {danger_count} security issues"
|
|
280
|
+
if quality_enabled:
|
|
281
|
+
headline += f" and {quality_count} quality issues"
|
|
282
|
+
headline += ". Add this badge to your README:"
|
|
283
|
+
|
|
284
|
+
console.print(Panel.fit(headline, border_style="warn"))
|
|
285
|
+
console.print("```markdown")
|
|
286
|
+
console.print(
|
|
287
|
+
f""
|
|
288
|
+
)
|
|
289
|
+
console.print("```")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def render_results(console: Console, result, tree=False, root_path=None):
|
|
293
|
+
summ = result.get("analysis_summary", {})
|
|
294
|
+
console.print(
|
|
295
|
+
Panel.fit(
|
|
296
|
+
f"[brand]Python Static Analysis Results[/brand]\n[muted]Analyzed {summ.get('total_files', '?')} file(s)[/muted]",
|
|
297
|
+
border_style="brand",
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _pill(label, n, ok_style="good", bad_style="bad"):
|
|
302
|
+
if n == 0:
|
|
303
|
+
style = ok_style
|
|
304
|
+
else:
|
|
305
|
+
style = bad_style
|
|
306
|
+
return f"[{style}]{label}: {n}[/{style}]"
|
|
307
|
+
|
|
308
|
+
console.print(
|
|
309
|
+
" ".join(
|
|
310
|
+
[
|
|
311
|
+
_pill("Unreachable", len(result.get("unused_functions", []))),
|
|
312
|
+
_pill("Unused imports", len(result.get("unused_imports", []))),
|
|
313
|
+
_pill("Unused params", len(result.get("unused_parameters", []))),
|
|
314
|
+
_pill("Unused vars", len(result.get("unused_variables", []))),
|
|
315
|
+
_pill("Unused classes", len(result.get("unused_classes", []))),
|
|
316
|
+
_pill(
|
|
317
|
+
"Quality", len(result.get("quality", []) or []), bad_style="warn"
|
|
318
|
+
),
|
|
319
|
+
]
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
console.print()
|
|
323
|
+
|
|
324
|
+
def _render_unused(title, items, name_key="name"):
|
|
325
|
+
if not items:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
console.rule(f"[bold]{title}")
|
|
329
|
+
|
|
330
|
+
table = Table(expand=True)
|
|
331
|
+
table.add_column("#", style="muted", width=3)
|
|
332
|
+
table.add_column("Name", style="bold")
|
|
333
|
+
table.add_column("Location", style="muted", overflow="fold")
|
|
334
|
+
|
|
335
|
+
for i, item in enumerate(items, 1):
|
|
336
|
+
nm = item.get(name_key) or item.get("simple_name") or "<?>"
|
|
337
|
+
short = _shorten_path(item.get("file"), root_path)
|
|
338
|
+
loc = f"{short}:{item.get('line', item.get('lineno', '?'))}"
|
|
339
|
+
table.add_row(str(i), nm, loc)
|
|
340
|
+
|
|
341
|
+
console.print(table)
|
|
342
|
+
console.print()
|
|
343
|
+
|
|
344
|
+
def _render_quality(items):
|
|
345
|
+
if not items:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
console.rule("[bold red]Quality Issues")
|
|
349
|
+
table = Table(expand=True)
|
|
350
|
+
table.add_column("#", style="muted", width=3)
|
|
351
|
+
table.add_column("Type", style="yellow", width=12)
|
|
352
|
+
table.add_column("Function", style="bold")
|
|
353
|
+
table.add_column("Detail")
|
|
354
|
+
table.add_column("Location", style="muted", width=36)
|
|
355
|
+
|
|
356
|
+
for i, quality in enumerate(items, 1):
|
|
357
|
+
kind = (quality.get("kind") or quality.get("metric") or "quality").title()
|
|
358
|
+
func = quality.get("name") or quality.get("simple_name") or "<?>"
|
|
359
|
+
loc = f"{quality.get('basename', '?')}:{quality.get('line', '?')}"
|
|
360
|
+
value = quality.get("value") or quality.get("complexity")
|
|
361
|
+
thr = quality.get("threshold")
|
|
362
|
+
length = quality.get("length")
|
|
363
|
+
|
|
364
|
+
if quality.get("kind") == "nesting":
|
|
365
|
+
detail = f"Deep nesting: depth {value}"
|
|
366
|
+
else:
|
|
367
|
+
detail = f"Cyclomatic complexity: {value}"
|
|
368
|
+
if thr is not None:
|
|
369
|
+
detail += f" (target ≤ {thr})"
|
|
370
|
+
if length is not None:
|
|
371
|
+
detail += f", {length} lines"
|
|
372
|
+
table.add_row(str(i), kind, func, detail, loc)
|
|
373
|
+
|
|
374
|
+
console.print(table)
|
|
375
|
+
console.print(
|
|
376
|
+
"[muted]Tip: split helpers, add early returns, flatten branches.[/muted]\n"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
def _render_secrets(items):
|
|
380
|
+
if not items:
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
console.rule("[bold red]Secrets")
|
|
384
|
+
table = Table(expand=True)
|
|
385
|
+
table.add_column("#", style="muted", width=3)
|
|
386
|
+
table.add_column("Provider", style="yellow", width=14)
|
|
387
|
+
table.add_column("Message")
|
|
388
|
+
table.add_column("Preview", style="muted", width=18)
|
|
389
|
+
table.add_column("Location", style="muted", overflow="fold")
|
|
390
|
+
|
|
391
|
+
for i, s in enumerate(items[:100], 1):
|
|
392
|
+
prov = s.get("provider") or "generic"
|
|
393
|
+
msg = s.get("message") or "Secret detected"
|
|
394
|
+
prev = s.get("preview") or "****"
|
|
395
|
+
short = _shorten_path(s.get("file"), root_path)
|
|
396
|
+
loc = f"{short}:{s.get('line', '?')}"
|
|
397
|
+
table.add_row(str(i), prov, msg, prev, loc)
|
|
398
|
+
|
|
399
|
+
console.print(table)
|
|
400
|
+
console.print()
|
|
401
|
+
|
|
402
|
+
def render_tree(console: Console, result, root_path=None):
|
|
403
|
+
by_file = defaultdict(list)
|
|
404
|
+
|
|
405
|
+
def _add_unused(items, kind):
|
|
406
|
+
for u in items or []:
|
|
407
|
+
file = u.get("file")
|
|
408
|
+
if not file:
|
|
409
|
+
continue
|
|
410
|
+
line = u.get("line") or u.get("lineno") or 1
|
|
411
|
+
name = u.get("name") or u.get("simple_name") or "<?>"
|
|
412
|
+
msg = f"Unused {kind}: {name}"
|
|
413
|
+
by_file[file].append((line, "info", msg))
|
|
414
|
+
|
|
415
|
+
def _add_findings(items, kind, default_sev="medium"):
|
|
416
|
+
for f in items or []:
|
|
417
|
+
file = f.get("file")
|
|
418
|
+
if not file:
|
|
419
|
+
continue
|
|
420
|
+
line = f.get("line") or 1
|
|
421
|
+
sev = (f.get("severity") or default_sev).lower()
|
|
422
|
+
rule = f.get("rule_id")
|
|
423
|
+
msg = f.get("message") or kind
|
|
424
|
+
if rule:
|
|
425
|
+
msg = f"[{rule}] {msg}"
|
|
426
|
+
by_file[file].append((line, sev, msg))
|
|
427
|
+
|
|
428
|
+
_add_unused(result.get("unused_functions"), "function")
|
|
429
|
+
_add_unused(result.get("unused_imports"), "import")
|
|
430
|
+
_add_unused(result.get("unused_classes"), "class")
|
|
431
|
+
_add_unused(result.get("unused_variables"), "variable")
|
|
432
|
+
_add_unused(result.get("unused_parameters"), "parameter")
|
|
433
|
+
|
|
434
|
+
_add_findings(result.get("danger"), "security", default_sev="high")
|
|
435
|
+
_add_findings(result.get("secrets"), "secret", default_sev="high")
|
|
436
|
+
_add_findings(result.get("quality"), "quality", default_sev="medium")
|
|
437
|
+
|
|
438
|
+
if not by_file:
|
|
439
|
+
console.print("[good]No findings to display.[/good]")
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
root_label = str(root_path) if root_path is not None else "Skylos results"
|
|
443
|
+
tree = Tree(f"[brand]{root_label}[/brand]")
|
|
444
|
+
|
|
445
|
+
for file in sorted(by_file.keys()):
|
|
446
|
+
short = _shorten_path(file, root_path)
|
|
447
|
+
file_node = tree.add(f"[bold]{short}[/bold]")
|
|
448
|
+
|
|
449
|
+
for line, sev, msg in sorted(by_file[file], key=lambda t: t[0]):
|
|
450
|
+
if sev == "high" or sev == "critical":
|
|
451
|
+
style = "bad"
|
|
452
|
+
elif sev == "medium":
|
|
453
|
+
style = "warn"
|
|
454
|
+
else:
|
|
455
|
+
style = "muted"
|
|
456
|
+
file_node.add(f"[{style}]L{line}[/{style}] {msg}")
|
|
457
|
+
|
|
458
|
+
console.print(tree)
|
|
459
|
+
|
|
460
|
+
def _render_danger(items):
|
|
461
|
+
if not items:
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
console.rule("[bold red]Security Issues")
|
|
465
|
+
table = Table(expand=True)
|
|
466
|
+
table.add_column("#", style="muted", width=3)
|
|
467
|
+
table.add_column("Rule", style="yellow", width=18)
|
|
468
|
+
table.add_column("Severity", width=10)
|
|
469
|
+
table.add_column("Message", overflow="fold")
|
|
470
|
+
table.add_column("Location", style="muted", width=36, overflow="fold")
|
|
471
|
+
|
|
472
|
+
for i, d in enumerate(items[:100], 1):
|
|
473
|
+
rule = d.get("rule_id") or "UNKNOWN"
|
|
474
|
+
sev = (d.get("severity") or "UNKNOWN").title()
|
|
475
|
+
msg = d.get("message") or "Issue detected"
|
|
476
|
+
short = _shorten_path(d.get("file"), root_path)
|
|
477
|
+
loc = f"{short}:{d.get('line', '?')}"
|
|
478
|
+
table.add_row(str(i), rule, sev, msg, loc)
|
|
479
|
+
|
|
480
|
+
console.print(table)
|
|
481
|
+
console.print()
|
|
482
|
+
|
|
483
|
+
if tree:
|
|
484
|
+
render_tree(console, result, root_path=root_path)
|
|
485
|
+
else:
|
|
486
|
+
_render_unused(
|
|
487
|
+
"Unreachable Functions", result.get("unused_functions", []), name_key="name"
|
|
488
|
+
)
|
|
489
|
+
_render_unused(
|
|
490
|
+
"Unused Imports", result.get("unused_imports", []), name_key="name"
|
|
491
|
+
)
|
|
492
|
+
_render_unused(
|
|
493
|
+
"Unused Parameters", result.get("unused_parameters", []), name_key="name"
|
|
494
|
+
)
|
|
495
|
+
_render_unused(
|
|
496
|
+
"Unused Variables", result.get("unused_variables", []), name_key="name"
|
|
497
|
+
)
|
|
498
|
+
_render_unused(
|
|
499
|
+
"Unused Classes", result.get("unused_classes", []), name_key="name"
|
|
500
|
+
)
|
|
501
|
+
_render_secrets(result.get("secrets", []) or [])
|
|
502
|
+
_render_danger(result.get("danger", []) or [])
|
|
503
|
+
_render_quality(result.get("quality", []) or [])
|
|
504
|
+
|
|
505
|
+
def run_init():
|
|
506
|
+
|
|
507
|
+
console = Console()
|
|
508
|
+
path = pathlib.Path("pyproject.toml")
|
|
186
509
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
510
|
+
template = """
|
|
511
|
+
[tool.skylos]
|
|
512
|
+
# Analysis Settings
|
|
513
|
+
complexity = 10
|
|
514
|
+
nesting = 3
|
|
515
|
+
max_args = 5
|
|
516
|
+
max_lines = 50
|
|
517
|
+
ignore = [] # e.g. ["SKY-L002", "SKY-C304"]
|
|
518
|
+
|
|
519
|
+
[tool.skylos.gate]
|
|
520
|
+
# Gatekeeper Settings (skylos --gate)
|
|
521
|
+
fail_on_critical = true
|
|
522
|
+
max_security = 0
|
|
523
|
+
max_quality = 10
|
|
524
|
+
strict = false
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
if path.exists():
|
|
528
|
+
content = path.read_text(encoding="utf-8")
|
|
529
|
+
if "[tool.skylos]" in content:
|
|
530
|
+
console.print("[warn]pyproject.toml already contains [tool.skylos] configuration.[/warn]")
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
console.print("[brand]Appending Skylos configuration to existing pyproject.toml...[/brand]")
|
|
534
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
535
|
+
f.write("\n" + template)
|
|
192
536
|
else:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
537
|
+
console.print("[brand]Creating new pyproject.toml...[/brand]")
|
|
538
|
+
path.write_text(template.strip(), encoding="utf-8")
|
|
539
|
+
|
|
540
|
+
console.print("[good] ** Configuration initialized! You can now edit pyproject.toml[/good]")
|
|
541
|
+
|
|
542
|
+
def main():
|
|
543
|
+
|
|
544
|
+
if len(sys.argv) > 1 and sys.argv[1] == "init":
|
|
545
|
+
run_init()
|
|
546
|
+
sys.exit(0)
|
|
547
|
+
|
|
548
|
+
if len(sys.argv) > 1 and sys.argv[1] == "run":
|
|
549
|
+
try:
|
|
550
|
+
start_server()
|
|
551
|
+
return
|
|
552
|
+
except ImportError:
|
|
553
|
+
print(f"{Colors.RED}Error: Flask is required {Colors.RESET}")
|
|
554
|
+
print(
|
|
555
|
+
f"{Colors.YELLOW}Install with: pip install flask flask-cors{Colors.RESET}"
|
|
556
|
+
)
|
|
557
|
+
sys.exit(1)
|
|
197
558
|
|
|
198
|
-
def main() -> None:
|
|
199
559
|
parser = argparse.ArgumentParser(
|
|
200
560
|
description="Detect unreachable functions and unused imports in a Python project"
|
|
201
561
|
)
|
|
202
|
-
parser.add_argument("path", help="Path to the Python project
|
|
562
|
+
parser.add_argument("path", help="Path to the Python project")
|
|
563
|
+
parser.add_argument(
|
|
564
|
+
"--gate",
|
|
565
|
+
action="store_true",
|
|
566
|
+
help="Run as a quality gate (block deployment on failure)"
|
|
567
|
+
)
|
|
568
|
+
parser.add_argument(
|
|
569
|
+
"--table", action="store_true", help="(deprecated) Show findings in table"
|
|
570
|
+
)
|
|
571
|
+
parser.add_argument(
|
|
572
|
+
"--tree", action="store_true", help="Show findings in tree format"
|
|
573
|
+
)
|
|
574
|
+
parser.add_argument(
|
|
575
|
+
"--version",
|
|
576
|
+
action="version",
|
|
577
|
+
version=f"skylos {skylos.__version__}",
|
|
578
|
+
help="Show version and exit",
|
|
579
|
+
)
|
|
580
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
203
581
|
parser.add_argument(
|
|
204
|
-
"--
|
|
582
|
+
"--comment-out",
|
|
205
583
|
action="store_true",
|
|
206
|
-
help="
|
|
584
|
+
help="Comment out selected dead code instead of deleting item",
|
|
585
|
+
)
|
|
586
|
+
parser.add_argument("--output", "-o", type=str, help="Write output to file")
|
|
587
|
+
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose")
|
|
588
|
+
parser.add_argument(
|
|
589
|
+
"--confidence",
|
|
590
|
+
"-c",
|
|
591
|
+
type=int,
|
|
592
|
+
default=60,
|
|
593
|
+
help="Confidence threshold (0-100). Lower = include more. Default: 60",
|
|
207
594
|
)
|
|
208
595
|
parser.add_argument(
|
|
209
|
-
"--
|
|
210
|
-
"-o",
|
|
211
|
-
type=str,
|
|
212
|
-
help="Write output to file instead of stdout",
|
|
596
|
+
"--interactive", "-i", action="store_true", help="Select items to remove"
|
|
213
597
|
)
|
|
214
598
|
parser.add_argument(
|
|
215
|
-
"--
|
|
599
|
+
"--dry-run", action="store_true", help="Show what would be removed"
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
parser.add_argument(
|
|
603
|
+
"--exclude-folder",
|
|
604
|
+
action="append",
|
|
605
|
+
dest="exclude_folders",
|
|
606
|
+
help=(
|
|
607
|
+
"Exclude a folder from analysis (can be used multiple times). By default, common folders like __pycache__, "
|
|
608
|
+
".git, venv are excluded. Use --no-default-excludes to disable default exclusions."
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
parser.add_argument(
|
|
612
|
+
"--include-folder",
|
|
613
|
+
action="append",
|
|
614
|
+
dest="include_folders",
|
|
615
|
+
help=(
|
|
616
|
+
"Force include a folder that would otherwise be excluded (overrides both default and custom exclusions). "
|
|
617
|
+
"Example: --include-folder venv"
|
|
618
|
+
),
|
|
619
|
+
)
|
|
620
|
+
parser.add_argument(
|
|
621
|
+
"--no-default-excludes",
|
|
622
|
+
action="store_true",
|
|
623
|
+
help="Do not exclude default folders (__pycache__, .git, venv, etc.). Only exclude folders with --exclude-folder.",
|
|
624
|
+
)
|
|
625
|
+
parser.add_argument(
|
|
626
|
+
"--list-default-excludes",
|
|
216
627
|
action="store_true",
|
|
217
|
-
help="
|
|
628
|
+
help="List the default excluded folders and exit.",
|
|
629
|
+
)
|
|
630
|
+
parser.add_argument(
|
|
631
|
+
"--secrets", action="store_true", help="Scan for API keys. Off by default."
|
|
218
632
|
)
|
|
219
633
|
parser.add_argument(
|
|
220
|
-
"--
|
|
634
|
+
"--danger",
|
|
221
635
|
action="store_true",
|
|
222
|
-
help="
|
|
636
|
+
help="Scan for security issues. Off by default.",
|
|
223
637
|
)
|
|
224
638
|
parser.add_argument(
|
|
225
|
-
"--
|
|
639
|
+
"--quality",
|
|
226
640
|
action="store_true",
|
|
227
|
-
help="
|
|
641
|
+
help="Run code quality checks. Off by default.",
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
parser.add_argument(
|
|
645
|
+
"command",
|
|
646
|
+
nargs="*",
|
|
647
|
+
help="Command to run if gate passes"
|
|
228
648
|
)
|
|
229
649
|
|
|
230
650
|
args = parser.parse_args()
|
|
651
|
+
project_root = pathlib.Path(args.path).resolve()
|
|
652
|
+
if project_root.is_file():
|
|
653
|
+
project_root = project_root.parent
|
|
654
|
+
|
|
231
655
|
logger = setup_logger(args.output)
|
|
232
|
-
|
|
656
|
+
console = logger.console
|
|
657
|
+
|
|
658
|
+
if args.list_default_excludes:
|
|
659
|
+
console.print("[brand]Default excluded folders:[/brand]")
|
|
660
|
+
for folder in sorted(DEFAULT_EXCLUDE_FOLDERS):
|
|
661
|
+
console.print(f" {folder}")
|
|
662
|
+
console.print(f"\n[muted]Total: {len(DEFAULT_EXCLUDE_FOLDERS)} folders[/muted]")
|
|
663
|
+
console.print("\nUse --no-default-excludes to disable these exclusions")
|
|
664
|
+
console.print("Use --include-folder <folder> to force include specific folders")
|
|
665
|
+
return
|
|
666
|
+
|
|
233
667
|
if args.verbose:
|
|
234
668
|
logger.setLevel(logging.DEBUG)
|
|
235
669
|
logger.debug(f"Analyzing path: {args.path}")
|
|
670
|
+
if args.exclude_folders:
|
|
671
|
+
logger.debug(f"Excluding folders: {args.exclude_folders}")
|
|
672
|
+
|
|
673
|
+
use_defaults = not args.no_default_excludes
|
|
674
|
+
final_exclude_folders = parse_exclude_folders(
|
|
675
|
+
user_exclude_folders=args.exclude_folders,
|
|
676
|
+
use_defaults=use_defaults,
|
|
677
|
+
include_folders=args.include_folders,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
if not args.json:
|
|
681
|
+
if final_exclude_folders:
|
|
682
|
+
console.print(
|
|
683
|
+
f"[warn] Excluding:[/warn] {', '.join(sorted(final_exclude_folders))}"
|
|
684
|
+
)
|
|
685
|
+
else:
|
|
686
|
+
console.print("[good] No folders excluded[/good]")
|
|
236
687
|
|
|
237
688
|
try:
|
|
238
|
-
|
|
689
|
+
with Progress(
|
|
690
|
+
SpinnerColumn(style="brand"),
|
|
691
|
+
TextColumn("[brand]Skylos[/brand] analyzing your code…"),
|
|
692
|
+
transient=True,
|
|
693
|
+
console=console,
|
|
694
|
+
) as progress:
|
|
695
|
+
progress.add_task("analyze", total=None)
|
|
696
|
+
result_json = run_analyze(
|
|
697
|
+
args.path,
|
|
698
|
+
conf=args.confidence,
|
|
699
|
+
enable_secrets=bool(args.secrets),
|
|
700
|
+
enable_danger=bool(args.danger),
|
|
701
|
+
enable_quality=bool(args.quality),
|
|
702
|
+
exclude_folders=list(final_exclude_folders),
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
if args.json:
|
|
706
|
+
print(result_json)
|
|
707
|
+
return
|
|
708
|
+
|
|
239
709
|
result = json.loads(result_json)
|
|
710
|
+
|
|
240
711
|
except Exception as e:
|
|
241
712
|
logger.error(f"Error during analysis: {e}")
|
|
242
713
|
sys.exit(1)
|
|
243
714
|
|
|
244
|
-
if args.
|
|
245
|
-
|
|
246
|
-
|
|
715
|
+
if args.interactive:
|
|
716
|
+
unused_functions = result.get("unused_functions", [])
|
|
717
|
+
unused_imports = result.get("unused_imports", [])
|
|
247
718
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
719
|
+
if not (unused_functions or unused_imports):
|
|
720
|
+
console.print("[good]No unused functions/imports to process.[/good]")
|
|
721
|
+
else:
|
|
722
|
+
selected_functions, selected_imports = interactive_selection(
|
|
723
|
+
console, unused_functions, unused_imports, root_path=project_root
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
if selected_functions or selected_imports:
|
|
727
|
+
if not args.dry_run:
|
|
728
|
+
if args.comment_out:
|
|
729
|
+
action_func_fn = comment_out_unused_function
|
|
730
|
+
action_func_imp = comment_out_unused_import
|
|
731
|
+
action_past = "Commented out"
|
|
732
|
+
action_verb = "comment out"
|
|
733
|
+
else:
|
|
734
|
+
action_func_fn = remove_unused_function
|
|
735
|
+
action_func_imp = remove_unused_import
|
|
736
|
+
action_past = "Removed"
|
|
737
|
+
action_verb = "remove"
|
|
738
|
+
|
|
739
|
+
if INTERACTIVE_AVAILABLE:
|
|
740
|
+
confirm_q = [
|
|
741
|
+
inquirer.Confirm(
|
|
742
|
+
"confirm",
|
|
743
|
+
message="Proceed with changes?",
|
|
744
|
+
default=False,
|
|
745
|
+
)
|
|
746
|
+
]
|
|
747
|
+
answers = inquirer.prompt(confirm_q)
|
|
748
|
+
proceed = answers and answers.get("confirm")
|
|
749
|
+
else:
|
|
750
|
+
proceed = True
|
|
751
|
+
|
|
752
|
+
if proceed:
|
|
753
|
+
console.print(f"[warn]Applying changes…[/warn]")
|
|
754
|
+
for func in selected_functions:
|
|
755
|
+
ok = action_func_fn(
|
|
756
|
+
func["file"], func["name"], func["line"]
|
|
757
|
+
)
|
|
758
|
+
if ok:
|
|
759
|
+
console.print(
|
|
760
|
+
f"[good] ✓ {action_past} function:[/good] {func['name']}"
|
|
761
|
+
)
|
|
762
|
+
else:
|
|
763
|
+
console.print(
|
|
764
|
+
f"[bad] x Failed to {action_verb} function:[/bad] {func['name']}"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
for imp in selected_imports:
|
|
768
|
+
ok = action_func_imp(imp["file"], imp["name"], imp["line"])
|
|
769
|
+
if ok:
|
|
770
|
+
console.print(
|
|
771
|
+
f"[good] ✓ {action_past} import:[/good] {imp['name']}"
|
|
772
|
+
)
|
|
773
|
+
else:
|
|
774
|
+
console.print(
|
|
775
|
+
f"[bad] x Failed to {action_verb} import:[/bad] {imp['name']}"
|
|
776
|
+
)
|
|
777
|
+
console.print(f"[good]Cleanup complete![/good]")
|
|
778
|
+
else:
|
|
779
|
+
console.print(f"[warn]Operation cancelled.[/warn]")
|
|
301
780
|
else:
|
|
302
|
-
|
|
781
|
+
console.print(f"[warn]Dry run — no files modified.[/warn]")
|
|
303
782
|
else:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
783
|
+
console.print("[muted]No items selected.[/muted]")
|
|
784
|
+
|
|
785
|
+
render_results(console, result, tree=args.tree, root_path=project_root)
|
|
786
|
+
|
|
787
|
+
unused_total = sum(
|
|
788
|
+
len(result.get(k, []))
|
|
789
|
+
for k in (
|
|
790
|
+
"unused_functions",
|
|
791
|
+
"unused_imports",
|
|
792
|
+
"unused_variables",
|
|
793
|
+
"unused_classes",
|
|
794
|
+
"unused_parameters",
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
danger_count = len(result.get("danger", []) or [])
|
|
798
|
+
quality_count = len(result.get("quality", []) or [])
|
|
799
|
+
print_badge(
|
|
800
|
+
unused_total,
|
|
801
|
+
logging.getLogger("skylos"),
|
|
802
|
+
danger_enabled=bool(danger_count),
|
|
803
|
+
danger_count=danger_count,
|
|
804
|
+
quality_enabled=bool(quality_count),
|
|
805
|
+
quality_count=quality_count,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
if args.gate:
|
|
809
|
+
cfg = load_config(project_root)
|
|
326
810
|
|
|
327
|
-
|
|
328
|
-
|
|
811
|
+
cmd = args.command
|
|
812
|
+
if cmd and cmd[0] == "--":
|
|
813
|
+
cmd = cmd[1:]
|
|
814
|
+
|
|
815
|
+
exit_code = run_gate_interaction(result, cfg, cmd)
|
|
816
|
+
sys.exit(exit_code)
|
|
329
817
|
|
|
330
|
-
if unused_functions or unused_imports:
|
|
331
|
-
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
332
|
-
logger.info(f" • Use --interactive to select specific items to remove")
|
|
333
|
-
logger.info(f" • Use --dry-run to preview changes before applying them")
|
|
334
818
|
|
|
335
819
|
if __name__ == "__main__":
|
|
336
|
-
main()
|
|
820
|
+
main()
|