skylos 1.2.1__tar.gz → 2.0.0__tar.gz
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.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- {skylos-1.2.1 → skylos-2.0.0}/PKG-INFO +1 -1
- {skylos-1.2.1 → skylos-2.0.0}/README.md +35 -0
- {skylos-1.2.1 → skylos-2.0.0}/pyproject.toml +1 -1
- {skylos-1.2.1 → skylos-2.0.0}/setup.py +5 -2
- {skylos-1.2.1 → skylos-2.0.0}/skylos/__init__.py +1 -1
- {skylos-1.2.1 → skylos-2.0.0}/skylos/analyzer.py +58 -92
- {skylos-1.2.1 → skylos-2.0.0}/skylos/cli.py +46 -36
- {skylos-1.2.1 → skylos-2.0.0}/skylos/constants.py +25 -10
- {skylos-1.2.1 → skylos-2.0.0}/skylos/framework_aware.py +8 -2
- skylos-2.0.0/skylos/server.py +560 -0
- {skylos-1.2.1 → skylos-2.0.0}/skylos/test_aware.py +0 -1
- {skylos-1.2.1 → skylos-2.0.0}/skylos/visitor.py +70 -56
- {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/PKG-INFO +1 -1
- {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/SOURCES.txt +1 -0
- {skylos-1.2.1 → skylos-2.0.0}/setup.cfg +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/dependency_links.txt +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/entry_points.txt +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/requires.txt +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/skylos.egg-info/top_level.txt +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/__init__.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/compare_tools.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/conftest.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/diagnostics.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/__init__.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/app.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/__init__.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/commands.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/models.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/routes.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/sample_repo/sample_repo/utils.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_analyzer.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_changes_analyzer.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_cli.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_constants.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_framework_aware.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_integration.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_skylos.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_test_aware.py +0 -0
- {skylos-1.2.1 → skylos-2.0.0}/test/test_visitor.py +0 -0
|
@@ -11,15 +11,23 @@
|
|
|
11
11
|
|
|
12
12
|
> A static analysis tool for Python codebases written in Python (formerly was written in Rust but we ditched that) that detects unreachable functions and unused imports, aka dead code. Faster and better results than many alternatives like Flake8 and Pylint, and finding more dead code than Vulture in our tests with comparable speed.
|
|
13
13
|
|
|
14
|
+
<div align="center">
|
|
15
|
+
<img src="assets/FE_SS.png" alt="FE" width="800">
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
|
|
14
19
|
## Table of Contents
|
|
15
20
|
|
|
16
21
|
- [Features](#features)
|
|
17
22
|
- [Benchmark](#benchmark-you-can-find-this-benchmark-test-in-test-folder)
|
|
18
23
|
- [Installation](#installation)
|
|
19
24
|
- [Quick Start](#quick-start)
|
|
25
|
+
- [Web Interface](#web-interface)
|
|
20
26
|
- [Understanding Confidence Levels](#understanding-confidence-levels)
|
|
21
27
|
- [Test File Detection](#test-file-detection)
|
|
22
28
|
- [Folder Management](#folder-management)
|
|
29
|
+
- [Ignoring Pragmas](#ignoring-pragmas)
|
|
30
|
+
- [Including & Excluding Files](#including--excluding-files)
|
|
23
31
|
- [CLI Options](#cli-options)
|
|
24
32
|
- [Example Output](#example-output)
|
|
25
33
|
- [Interactive Mode](#interactive-mode)
|
|
@@ -41,6 +49,7 @@
|
|
|
41
49
|
* **Unused Classes**: Detects classes that are not instantiated or inherited
|
|
42
50
|
* **Unused Imports**: Identifies imports that are not used
|
|
43
51
|
* **Folder Management**: Inclusion/exclusion of directories
|
|
52
|
+
* **Ignore Pragmas**: Skip lines tagged with `# pragma: no skylos`, `# pragma: no cover`, or `# noqa`
|
|
44
53
|
|
|
45
54
|
## Benchmark (You can find this benchmark test in `test` folder)
|
|
46
55
|
|
|
@@ -87,6 +96,9 @@ pip install .
|
|
|
87
96
|
# Analyze a project
|
|
88
97
|
skylos /path/to/your/project
|
|
89
98
|
|
|
99
|
+
# To launch the front end
|
|
100
|
+
skylos run
|
|
101
|
+
|
|
90
102
|
# Interactive mode - select items to remove
|
|
91
103
|
skylos --interactive /path/to/your/project
|
|
92
104
|
|
|
@@ -100,6 +112,17 @@ skylos --json /path/to/your/project
|
|
|
100
112
|
skylos path/to/your/file --confidence 20 ## or whatever value u wanna set
|
|
101
113
|
```
|
|
102
114
|
|
|
115
|
+
## Web Interface
|
|
116
|
+
|
|
117
|
+
Skylos includes a modern web dashboard for interactive analysis:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Start web interface
|
|
121
|
+
skylos run
|
|
122
|
+
|
|
123
|
+
# Opens browser at http://localhost:5090
|
|
124
|
+
```
|
|
125
|
+
|
|
103
126
|
## Understanding Confidence Levels
|
|
104
127
|
|
|
105
128
|
Skylos uses a confidence-based system to try to handle Python's dynamic nature and web frameworks.
|
|
@@ -162,6 +185,18 @@ When Skylos detects a test file, it by default, will apply a confidence penalty
|
|
|
162
185
|
/project/test_data.py
|
|
163
186
|
```
|
|
164
187
|
|
|
188
|
+
## Ignoring Pragmas
|
|
189
|
+
|
|
190
|
+
To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
|
|
191
|
+
|
|
192
|
+
Example
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
def with_logging(self, enabled: bool = True) -> "WebPath": # pragma: no skylos
|
|
196
|
+
new_path = WebPath(self._url)
|
|
197
|
+
return new_path
|
|
198
|
+
```
|
|
199
|
+
|
|
165
200
|
## Including & Excluding Files
|
|
166
201
|
|
|
167
202
|
### Default Exclusions
|
|
@@ -2,10 +2,13 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="skylos",
|
|
5
|
-
version="
|
|
5
|
+
version="2.0.0",
|
|
6
6
|
packages=find_packages(),
|
|
7
7
|
python_requires=">=3.9",
|
|
8
|
-
install_requires=[
|
|
8
|
+
install_requires=[
|
|
9
|
+
"inquirer>=3.0.0",
|
|
10
|
+
"flask>=2.0.0",
|
|
11
|
+
"flask-cors>=3.0.0"],
|
|
9
12
|
classifiers=[
|
|
10
13
|
"Development Status :: 4 - Beta",
|
|
11
14
|
"Intended Audience :: Developers",
|
|
@@ -11,10 +11,6 @@ from skylos.test_aware import TestAwareVisitor
|
|
|
11
11
|
import os
|
|
12
12
|
import traceback
|
|
13
13
|
from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
14
|
-
import io
|
|
15
|
-
import tokenize
|
|
16
|
-
import re
|
|
17
|
-
import warnings
|
|
18
14
|
|
|
19
15
|
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
|
|
20
16
|
logger=logging.getLogger('Skylos')
|
|
@@ -34,33 +30,20 @@ def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folde
|
|
|
34
30
|
|
|
35
31
|
return exclude_set
|
|
36
32
|
|
|
37
|
-
IGNORE_PATTERNS = (
|
|
38
|
-
r"#\s*pragma:\s*no\s+skylos", ## our own pragma
|
|
39
|
-
r"#\s*pragma:\s*no\s+cover",
|
|
40
|
-
r"#\s*noqa(?:\b|:)", # flake8 style
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
def _collect_ignored_lines(source: str) -> set[int]:
|
|
44
|
-
ignores = set()
|
|
45
|
-
for tok in tokenize.generate_tokens(io.StringIO(source).readline):
|
|
46
|
-
if tok.type == tokenize.COMMENT:
|
|
47
|
-
if any(re.search(pat, tok.string, flags=re.I) for pat in IGNORE_PATTERNS):
|
|
48
|
-
ignores.add(tok.start[0])
|
|
49
|
-
return ignores
|
|
50
|
-
|
|
51
33
|
class Skylos:
|
|
52
34
|
def __init__(self):
|
|
53
35
|
self.defs={}
|
|
54
36
|
self.refs=[]
|
|
55
37
|
self.dynamic=set()
|
|
56
38
|
self.exports=defaultdict(set)
|
|
57
|
-
self.ignored_lines:set[int]=set()
|
|
58
39
|
|
|
59
40
|
def _module(self,root,f):
|
|
60
|
-
p=list(f.relative_to(root).parts)
|
|
61
|
-
if p[-1].endswith(".py"):
|
|
62
|
-
|
|
63
|
-
|
|
41
|
+
p = list(f.relative_to(root).parts)
|
|
42
|
+
if p[-1].endswith(".py"):
|
|
43
|
+
p[-1] = p[-1][:-3]
|
|
44
|
+
if p[-1] == "__init__":
|
|
45
|
+
p.pop()
|
|
46
|
+
return ".".join(p)
|
|
64
47
|
|
|
65
48
|
def _should_exclude_file(self, file_path, root_path, exclude_folders):
|
|
66
49
|
if not exclude_folders:
|
|
@@ -111,9 +94,9 @@ class Skylos:
|
|
|
111
94
|
return all_files, root
|
|
112
95
|
|
|
113
96
|
def _mark_exports(self):
|
|
114
|
-
for name,
|
|
115
|
-
if
|
|
116
|
-
|
|
97
|
+
for name, definition in self.defs.items():
|
|
98
|
+
if definition.in_init and not definition.simple_name.startswith('_'):
|
|
99
|
+
definition.is_exported = True
|
|
117
100
|
|
|
118
101
|
for mod, export_names in self.exports.items():
|
|
119
102
|
for name in export_names:
|
|
@@ -137,8 +120,8 @@ class Skylos:
|
|
|
137
120
|
break
|
|
138
121
|
|
|
139
122
|
simple_name_lookup = defaultdict(list)
|
|
140
|
-
for
|
|
141
|
-
simple_name_lookup[
|
|
123
|
+
for definition in self.defs.values():
|
|
124
|
+
simple_name_lookup[definition.simple_name].append(definition)
|
|
142
125
|
|
|
143
126
|
for ref, _ in self.refs:
|
|
144
127
|
if ref in self.defs:
|
|
@@ -151,8 +134,8 @@ class Skylos:
|
|
|
151
134
|
|
|
152
135
|
simple = ref.split('.')[-1]
|
|
153
136
|
matches = simple_name_lookup.get(simple, [])
|
|
154
|
-
for
|
|
155
|
-
|
|
137
|
+
for definition in matches:
|
|
138
|
+
definition.references += 1
|
|
156
139
|
|
|
157
140
|
for module_name in self.dynamic:
|
|
158
141
|
for def_name, def_obj in self.defs.items():
|
|
@@ -172,59 +155,58 @@ class Skylos:
|
|
|
172
155
|
return []
|
|
173
156
|
|
|
174
157
|
def _apply_penalties(self, def_obj, visitor, framework):
|
|
175
|
-
|
|
176
|
-
|
|
158
|
+
confidence=100
|
|
177
159
|
if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
|
|
178
|
-
|
|
160
|
+
confidence -= PENALTIES["private_name"]
|
|
179
161
|
if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
|
|
180
|
-
|
|
162
|
+
confidence -= PENALTIES["dunder_or_magic"]
|
|
181
163
|
if def_obj.type == "variable" and def_obj.simple_name == "_":
|
|
182
|
-
|
|
164
|
+
confidence -= PENALTIES["underscored_var"]
|
|
183
165
|
if def_obj.in_init and def_obj.type in ("function", "class"):
|
|
184
|
-
|
|
166
|
+
confidence -= PENALTIES["in_init_file"]
|
|
185
167
|
if def_obj.name.split(".")[0] in self.dynamic:
|
|
186
|
-
|
|
168
|
+
confidence -= PENALTIES["dynamic_module"]
|
|
187
169
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
188
|
-
|
|
170
|
+
confidence -= PENALTIES["test_related"]
|
|
189
171
|
|
|
190
172
|
framework_confidence = detect_framework_usage(def_obj, visitor=framework)
|
|
191
173
|
if framework_confidence is not None:
|
|
192
|
-
|
|
174
|
+
confidence = min(confidence, framework_confidence)
|
|
193
175
|
|
|
194
176
|
if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
|
|
195
|
-
|
|
177
|
+
confidence = 0
|
|
196
178
|
|
|
197
179
|
if def_obj.type == "parameter":
|
|
198
180
|
if def_obj.simple_name in ("self", "cls"):
|
|
199
|
-
|
|
181
|
+
confidence = 0
|
|
200
182
|
elif "." in def_obj.name:
|
|
201
183
|
method_name = def_obj.name.split(".")[-2]
|
|
202
184
|
if method_name.startswith("__") and method_name.endswith("__"):
|
|
203
|
-
|
|
185
|
+
confidence = 0
|
|
204
186
|
|
|
205
187
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
206
|
-
|
|
188
|
+
confidence = 0
|
|
207
189
|
|
|
208
190
|
if (def_obj.type == "import" and def_obj.name.startswith("__future__.") and
|
|
209
191
|
def_obj.simple_name in ("annotations", "absolute_import", "division",
|
|
210
192
|
"print_function", "unicode_literals", "generator_stop")):
|
|
211
|
-
|
|
193
|
+
confidence = 0
|
|
212
194
|
|
|
213
|
-
def_obj.confidence = max(
|
|
195
|
+
def_obj.confidence = max(confidence, 0)
|
|
214
196
|
|
|
215
197
|
def _apply_heuristics(self):
|
|
216
198
|
class_methods = defaultdict(list)
|
|
217
|
-
for
|
|
218
|
-
if
|
|
219
|
-
cls =
|
|
199
|
+
for definition in self.defs.values():
|
|
200
|
+
if definition.type in ("method", "function") and "." in definition.name:
|
|
201
|
+
cls = definition.name.rsplit(".", 1)[0]
|
|
220
202
|
if cls in self.defs and self.defs[cls].type == "class":
|
|
221
|
-
class_methods[cls].append(
|
|
203
|
+
class_methods[cls].append(definition)
|
|
222
204
|
|
|
223
205
|
for cls, methods in class_methods.items():
|
|
224
206
|
if self.defs[cls].references > 0:
|
|
225
|
-
for
|
|
226
|
-
if
|
|
227
|
-
|
|
207
|
+
for method in methods:
|
|
208
|
+
if method.simple_name in AUTO_CALLED:
|
|
209
|
+
method.references += 1
|
|
228
210
|
|
|
229
211
|
def analyze(self, path, thr=60, exclude_folders=None):
|
|
230
212
|
files, root = self._get_python_files(path, exclude_folders)
|
|
@@ -251,25 +233,11 @@ class Skylos:
|
|
|
251
233
|
|
|
252
234
|
for file in files:
|
|
253
235
|
mod = modmap[file]
|
|
236
|
+
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
|
|
254
237
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
defs, refs, dyn, exports, test_flags, framework_flags, ignored = result
|
|
259
|
-
self.ignored_lines.update(ignored)
|
|
260
|
-
else: ##legacy
|
|
261
|
-
warnings.warn(
|
|
262
|
-
"proc_file() now returns 7 values (added ignored_lines). "
|
|
263
|
-
"The 6-value form is deprecated and will disappear.",
|
|
264
|
-
DeprecationWarning,
|
|
265
|
-
stacklevel=2,
|
|
266
|
-
)
|
|
267
|
-
defs, refs, dyn, exports, test_flags, framework_flags = result
|
|
268
|
-
|
|
269
|
-
# apply penalties while we still have the file-specific flags
|
|
270
|
-
for d in defs:
|
|
271
|
-
self._apply_penalties(d, test_flags, framework_flags)
|
|
272
|
-
self.defs[d.name] = d
|
|
238
|
+
for definition in defs:
|
|
239
|
+
self._apply_penalties(definition, test_flags, framework_flags)
|
|
240
|
+
self.defs[definition.name] = definition
|
|
273
241
|
|
|
274
242
|
self.refs.extend(refs)
|
|
275
243
|
self.dynamic.update(dyn)
|
|
@@ -282,11 +250,9 @@ class Skylos:
|
|
|
282
250
|
thr = max(0, thr)
|
|
283
251
|
|
|
284
252
|
unused = []
|
|
285
|
-
for
|
|
286
|
-
if
|
|
287
|
-
|
|
288
|
-
and d.line not in self.ignored_lines):
|
|
289
|
-
unused.append(d.to_dict())
|
|
253
|
+
for definition in self.defs.values():
|
|
254
|
+
if definition.references == 0 and not definition.is_exported and definition.confidence > 0 and definition.confidence >= thr:
|
|
255
|
+
unused.append(definition.to_dict())
|
|
290
256
|
|
|
291
257
|
result = {
|
|
292
258
|
"unused_functions": [],
|
|
@@ -322,7 +288,6 @@ def proc_file(file_or_args, mod=None):
|
|
|
322
288
|
|
|
323
289
|
try:
|
|
324
290
|
source = Path(file).read_text(encoding="utf-8")
|
|
325
|
-
ignored = _collect_ignored_lines(source)
|
|
326
291
|
tree = ast.parse(source)
|
|
327
292
|
|
|
328
293
|
tv = TestAwareVisitor(filename=file)
|
|
@@ -334,7 +299,7 @@ def proc_file(file_or_args, mod=None):
|
|
|
334
299
|
v = Visitor(mod, file)
|
|
335
300
|
v.visit(tree)
|
|
336
301
|
|
|
337
|
-
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
302
|
+
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
338
303
|
except Exception as e:
|
|
339
304
|
logger.error(f"{file}: {e}")
|
|
340
305
|
if os.getenv("SKYLOS_DEBUG"):
|
|
@@ -342,55 +307,56 @@ def proc_file(file_or_args, mod=None):
|
|
|
342
307
|
dummy_visitor = TestAwareVisitor(filename=file)
|
|
343
308
|
dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
|
|
344
309
|
|
|
345
|
-
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
310
|
+
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
346
311
|
|
|
347
312
|
def analyze(path,conf=60, exclude_folders=None):
|
|
348
313
|
return Skylos().analyze(path,conf, exclude_folders)
|
|
349
314
|
|
|
350
|
-
if __name__=="__main__":
|
|
315
|
+
if __name__ == "__main__":
|
|
351
316
|
if len(sys.argv)>1:
|
|
352
|
-
p=sys.argv[1]
|
|
353
|
-
|
|
317
|
+
p = sys.argv[1]
|
|
318
|
+
confidence = int(sys.argv[2]) if len(sys.argv) >2 else 60
|
|
319
|
+
result = analyze(p,confidence)
|
|
354
320
|
|
|
355
321
|
data = json.loads(result)
|
|
356
|
-
print("\n
|
|
322
|
+
print("\n Python Static Analysis Results")
|
|
357
323
|
print("===================================\n")
|
|
358
324
|
|
|
359
325
|
total_items = sum(len(items) for items in data.values())
|
|
360
326
|
|
|
361
327
|
print("Summary:")
|
|
362
328
|
if data["unused_functions"]:
|
|
363
|
-
print(f"
|
|
329
|
+
print(f" * Unreachable functions: {len(data['unused_functions'])}")
|
|
364
330
|
if data["unused_imports"]:
|
|
365
|
-
print(f"
|
|
331
|
+
print(f" * Unused imports: {len(data['unused_imports'])}")
|
|
366
332
|
if data["unused_classes"]:
|
|
367
|
-
print(f"
|
|
333
|
+
print(f" * Unused classes: {len(data['unused_classes'])}")
|
|
368
334
|
if data["unused_variables"]:
|
|
369
|
-
print(f"
|
|
335
|
+
print(f" * Unused variables: {len(data['unused_variables'])}")
|
|
370
336
|
|
|
371
337
|
if data["unused_functions"]:
|
|
372
|
-
print("\n
|
|
338
|
+
print("\n - Unreachable Functions")
|
|
373
339
|
print("=======================")
|
|
374
340
|
for i, func in enumerate(data["unused_functions"], 1):
|
|
375
341
|
print(f" {i}. {func['name']}")
|
|
376
342
|
print(f" └─ {func['file']}:{func['line']}")
|
|
377
343
|
|
|
378
344
|
if data["unused_imports"]:
|
|
379
|
-
print("\n
|
|
345
|
+
print("\n - Unused Imports")
|
|
380
346
|
print("================")
|
|
381
347
|
for i, imp in enumerate(data["unused_imports"], 1):
|
|
382
348
|
print(f" {i}. {imp['simple_name']}")
|
|
383
349
|
print(f" └─ {imp['file']}:{imp['line']}")
|
|
384
350
|
|
|
385
351
|
if data["unused_classes"]:
|
|
386
|
-
print("\n
|
|
352
|
+
print("\n - Unused Classes")
|
|
387
353
|
print("=================")
|
|
388
354
|
for i, cls in enumerate(data["unused_classes"], 1):
|
|
389
355
|
print(f" {i}. {cls['name']}")
|
|
390
356
|
print(f" └─ {cls['file']}:{cls['line']}")
|
|
391
357
|
|
|
392
358
|
if data["unused_variables"]:
|
|
393
|
-
print("\n
|
|
359
|
+
print("\n - Unused Variables")
|
|
394
360
|
print("==================")
|
|
395
361
|
for i, var in enumerate(data["unused_variables"], 1):
|
|
396
362
|
print(f" {i}. {var['name']}")
|
|
@@ -403,7 +369,7 @@ if __name__=="__main__":
|
|
|
403
369
|
print(f"```")
|
|
404
370
|
|
|
405
371
|
print("\nNext steps:")
|
|
406
|
-
print("
|
|
407
|
-
print("
|
|
372
|
+
print(" * Use --interactive to select specific items to remove")
|
|
373
|
+
print(" * Use --dry-run to preview changes before applying them")
|
|
408
374
|
else:
|
|
409
375
|
print("Usage: python Skylos.py <path> [confidence_threshold]")
|