skylos 1.2.2__tar.gz → 2.1.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.2 → skylos-2.1.0}/PKG-INFO +4 -1
- {skylos-1.2.2 → skylos-2.1.0}/README.md +50 -54
- {skylos-1.2.2 → skylos-2.1.0}/pyproject.toml +2 -2
- {skylos-1.2.2 → skylos-2.1.0}/setup.py +7 -2
- {skylos-1.2.2 → skylos-2.1.0}/skylos/__init__.py +1 -1
- {skylos-1.2.2 → skylos-2.1.0}/skylos/analyzer.py +103 -121
- {skylos-1.2.2 → skylos-2.1.0}/skylos/cli.py +66 -109
- skylos-2.1.0/skylos/codemods.py +89 -0
- {skylos-1.2.2 → skylos-2.1.0}/skylos/constants.py +25 -10
- skylos-2.1.0/skylos/framework_aware.py +358 -0
- skylos-2.1.0/skylos/server.py +560 -0
- {skylos-1.2.2 → skylos-2.1.0}/skylos/test_aware.py +0 -1
- skylos-2.1.0/skylos/visitor.py +495 -0
- {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/PKG-INFO +4 -1
- {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/SOURCES.txt +3 -0
- skylos-2.1.0/skylos.egg-info/requires.txt +4 -0
- skylos-2.1.0/test/test_codemods.py +153 -0
- skylos-2.1.0/test/test_framework_aware.py +306 -0
- skylos-1.2.2/skylos/framework_aware.py +0 -158
- skylos-1.2.2/skylos/visitor.py +0 -336
- skylos-1.2.2/skylos.egg-info/requires.txt +0 -1
- skylos-1.2.2/test/test_framework_aware.py +0 -372
- {skylos-1.2.2 → skylos-2.1.0}/setup.cfg +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/dependency_links.txt +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/entry_points.txt +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/top_level.txt +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/__init__.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/compare_tools.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/conftest.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/diagnostics.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/__init__.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/app.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/__init__.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/commands.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/models.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/routes.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/utils.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_analyzer.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_changes_analyzer.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_cli.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_constants.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_integration.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_skylos.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_test_aware.py +0 -0
- {skylos-1.2.2 → skylos-2.1.0}/test/test_visitor.py +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skylos
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: A static analysis tool for Python codebases
|
|
5
5
|
Author-email: oha <aaronoh2015@gmail.com>
|
|
6
6
|
Requires-Python: >=3.9
|
|
7
7
|
Requires-Dist: inquirer>=3.0.0
|
|
8
|
+
Requires-Dist: flask>=2.1.0
|
|
9
|
+
Requires-Dist: flask-cors>=3.0.0
|
|
10
|
+
Requires-Dist: libcst>=1.8.2
|
|
8
11
|
Dynamic: requires-python
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
<img src="assets/SKYLOS.png" alt="Skylos Logo" width="200">
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
|
-
> A static analysis tool for Python codebases written in Python
|
|
12
|
+
> A static analysis tool for Python codebases written in Python 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
|
+
|
|
14
|
+
<div align="center">
|
|
15
|
+
<img src="assets/FE_SS.png" alt="FE" width="800">
|
|
16
|
+
</div>
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
## Table of Contents
|
|
15
20
|
|
|
@@ -17,9 +22,12 @@
|
|
|
17
22
|
- [Benchmark](#benchmark-you-can-find-this-benchmark-test-in-test-folder)
|
|
18
23
|
- [Installation](#installation)
|
|
19
24
|
- [Quick Start](#quick-start)
|
|
20
|
-
- [
|
|
25
|
+
- [Web Interface](#web-interface)
|
|
26
|
+
- [Design](#design)
|
|
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)
|
|
@@ -34,6 +42,7 @@
|
|
|
34
42
|
|
|
35
43
|
## Features
|
|
36
44
|
|
|
45
|
+
* **CST-safe removals:** Uses LibCST to remove selected imports or functions (handles multiline imports, aliases, decorators, async etc..)
|
|
37
46
|
* **Framework-Aware Detection**: Attempt at handling Flask, Django, FastAPI routes and decorators
|
|
38
47
|
* **Test File Exclusion**: Auto excludes test files (you can include it back if you want)
|
|
39
48
|
* **Interactive Cleanup**: Select specific items to remove from CLI
|
|
@@ -74,34 +83,53 @@ pip install skylos
|
|
|
74
83
|
### From Source
|
|
75
84
|
|
|
76
85
|
```bash
|
|
77
|
-
# Clone the repository
|
|
78
86
|
git clone https://github.com/duriantaco/skylos.git
|
|
79
87
|
cd skylos
|
|
80
88
|
|
|
81
|
-
## Install your dependencies
|
|
82
89
|
pip install .
|
|
83
90
|
```
|
|
84
91
|
|
|
85
92
|
## Quick Start
|
|
86
93
|
|
|
87
94
|
```bash
|
|
88
|
-
# Analyze a project
|
|
89
95
|
skylos /path/to/your/project
|
|
90
96
|
|
|
97
|
+
# To launch the front end
|
|
98
|
+
skylos run
|
|
99
|
+
|
|
91
100
|
# Interactive mode - select items to remove
|
|
92
101
|
skylos --interactive /path/to/your/project
|
|
93
102
|
|
|
94
103
|
# Dry run - see what would be removed
|
|
95
104
|
skylos --interactive --dry-run /path/to/your/project
|
|
96
105
|
|
|
97
|
-
# Output to JSON
|
|
98
106
|
skylos --json /path/to/your/project
|
|
99
107
|
|
|
100
108
|
# With confidence
|
|
101
109
|
skylos path/to/your/file --confidence 20 ## or whatever value u wanna set
|
|
102
110
|
```
|
|
103
111
|
|
|
104
|
-
##
|
|
112
|
+
## Web Interface
|
|
113
|
+
|
|
114
|
+
Skylos includes a modern web dashboard for interactive analysis:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
skylos run
|
|
118
|
+
|
|
119
|
+
# Opens browser at http://localhost:5090
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Design
|
|
123
|
+
|
|
124
|
+
### Summary
|
|
125
|
+
|
|
126
|
+
Framework endpoints are often invoked externally (eg, via HTTP, signals), so we use framework aware signals + confidence scores to try avoid false positives while still catching dead codes
|
|
127
|
+
|
|
128
|
+
Name resolution handles aliases and modules, but when things get ambiguous, we rather miss some dead code than accidentally mark live code as dead lmao
|
|
129
|
+
|
|
130
|
+
Tests are excluded by default because their call patterns are noisy. You can opt in when you really need to audit it
|
|
131
|
+
|
|
132
|
+
### Understanding Confidence Levels
|
|
105
133
|
|
|
106
134
|
Skylos uses a confidence-based system to try to handle Python's dynamic nature and web frameworks.
|
|
107
135
|
|
|
@@ -163,6 +191,18 @@ When Skylos detects a test file, it by default, will apply a confidence penalty
|
|
|
163
191
|
/project/test_data.py
|
|
164
192
|
```
|
|
165
193
|
|
|
194
|
+
## Ignoring Pragmas
|
|
195
|
+
|
|
196
|
+
To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
|
|
197
|
+
|
|
198
|
+
Example
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
def with_logging(self, enabled: bool = True) -> "WebPath": # pragma: no skylos
|
|
202
|
+
new_path = WebPath(self._url)
|
|
203
|
+
return new_path
|
|
204
|
+
```
|
|
205
|
+
|
|
166
206
|
## Including & Excluding Files
|
|
167
207
|
|
|
168
208
|
### Default Exclusions
|
|
@@ -170,7 +210,6 @@ By default, Skylos excludes common folders: `__pycache__`, `.git`, `.pytest_cach
|
|
|
170
210
|
|
|
171
211
|
### Folder Options
|
|
172
212
|
```bash
|
|
173
|
-
# List default excluded folders
|
|
174
213
|
skylos --list-default-excludes
|
|
175
214
|
|
|
176
215
|
# Exclude single folder (The example here will be venv)
|
|
@@ -194,8 +233,8 @@ Arguments:
|
|
|
194
233
|
PATH Path to the Python project to analyze
|
|
195
234
|
|
|
196
235
|
Options:
|
|
197
|
-
-h, --help
|
|
198
|
-
|
|
236
|
+
-h, --help Show this help message and exit
|
|
237
|
+
--json Output raw JSON instead of formatted text
|
|
199
238
|
-o, --output FILE Write output to file instead of stdout
|
|
200
239
|
-v, --verbose Enable verbose output
|
|
201
240
|
-i, --interactive Interactively select items to remove
|
|
@@ -207,48 +246,11 @@ Options:
|
|
|
207
246
|
-c, --confidence LEVEL Confidence threshold (0-100). Lower values will show more items.
|
|
208
247
|
```
|
|
209
248
|
|
|
210
|
-
## Example Output
|
|
211
|
-
|
|
212
|
-
```
|
|
213
|
-
Python Static Analysis Results
|
|
214
|
-
===================================
|
|
215
|
-
|
|
216
|
-
Summary:
|
|
217
|
-
• Unreachable functions: 48
|
|
218
|
-
• Unused imports: 8
|
|
219
|
-
|
|
220
|
-
Unreachable Functions
|
|
221
|
-
========================
|
|
222
|
-
|
|
223
|
-
1. module_13.test_function
|
|
224
|
-
└─ /Users/oha/project/module_13.py:5
|
|
225
|
-
2. module_13.unused_function
|
|
226
|
-
└─ /Users/oha/project/module_13.py:13
|
|
227
|
-
...
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Unused Imports
|
|
231
|
-
=================
|
|
232
|
-
|
|
233
|
-
1. os
|
|
234
|
-
└─ /Users/oha/project/module_13.py:1
|
|
235
|
-
2. json
|
|
236
|
-
└─ /Users/oha/project/module_13.py:3
|
|
237
|
-
...
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Next steps:
|
|
241
|
-
|
|
242
|
-
• Use `--interactive` to select specific items to remove
|
|
243
|
-
|
|
244
|
-
• Use `--dry-run` to preview changes before applying them
|
|
245
|
-
|
|
246
|
-
|
|
247
249
|
## Interactive Mode
|
|
248
250
|
|
|
249
251
|
The interactive mode lets you select specific functions and imports to remove:
|
|
250
252
|
|
|
251
|
-
1. **Select items**: Use arrow keys and
|
|
253
|
+
1. **Select items**: Use arrow keys and `spacebar` to select/unselect
|
|
252
254
|
2. **Confirm changes**: Review selected items before applying
|
|
253
255
|
3. **Auto-cleanup**: Files are automatically updated
|
|
254
256
|
|
|
@@ -263,11 +265,9 @@ The interactive mode lets you select specific functions and imports to remove:
|
|
|
263
265
|
### Setup
|
|
264
266
|
|
|
265
267
|
```bash
|
|
266
|
-
# Clone the repository
|
|
267
268
|
git clone https://github.com/duriantaco/skylos.git
|
|
268
269
|
cd skylos
|
|
269
270
|
|
|
270
|
-
# Create a virtual environment
|
|
271
271
|
python -m venv venv
|
|
272
272
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
273
273
|
```
|
|
@@ -275,7 +275,6 @@ source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
|
275
275
|
### Running Tests
|
|
276
276
|
|
|
277
277
|
```bash
|
|
278
|
-
# Run Python tests
|
|
279
278
|
python -m pytest tests/
|
|
280
279
|
```
|
|
281
280
|
|
|
@@ -309,8 +308,6 @@ A: Start with 60 (default) for safe cleanup. Use 30 for framework applications.
|
|
|
309
308
|
- **Test data**: Limited scenarios, your mileage may vary
|
|
310
309
|
- **False positives**: Always manually review before deleting code
|
|
311
310
|
|
|
312
|
-
This is a STATIC code analyzer. If we can detect 100% of all dead code in any structure and framework, we wouldn't be sitting here. But we definitely tried our best.
|
|
313
|
-
|
|
314
311
|
## Troubleshooting
|
|
315
312
|
|
|
316
313
|
### Common Issues
|
|
@@ -344,7 +341,6 @@ We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING
|
|
|
344
341
|
- [ ] Configuration file support
|
|
345
342
|
- [ ] Git hooks integration
|
|
346
343
|
- [ ] CI/CD integration examples
|
|
347
|
-
~~- [] Support for other languages (unless I have contributors, this ain't possible)~~
|
|
348
344
|
- [ ] Further optimization
|
|
349
345
|
|
|
350
346
|
## License
|
|
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "skylos"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "2.1.0"
|
|
8
8
|
requires-python = ">=3.9"
|
|
9
9
|
description = "A static analysis tool for Python codebases"
|
|
10
10
|
authors = [{name = "oha", email = "aaronoh2015@gmail.com"}]
|
|
11
|
-
dependencies = ["inquirer>=3.0.0"]
|
|
11
|
+
dependencies = ["inquirer>=3.0.0", "flask>=2.1.0", "flask-cors>=3.0.0", "libcst>=1.8.2"]
|
|
12
12
|
|
|
13
13
|
[project.scripts]
|
|
14
14
|
skylos = "skylos.cli:main"
|
|
@@ -2,10 +2,15 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="skylos",
|
|
5
|
-
version="1.
|
|
5
|
+
version="2.1.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.1.0",
|
|
11
|
+
"flask-cors>=3.0.0",
|
|
12
|
+
"libcst>=1.8.2"],
|
|
13
|
+
|
|
9
14
|
classifiers=[
|
|
10
15
|
"Development Status :: 4 - Beta",
|
|
11
16
|
"Intended Audience :: Developers",
|
|
@@ -6,61 +6,29 @@ import logging
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from skylos.visitor import Visitor
|
|
9
|
-
from skylos.constants import (
|
|
9
|
+
from skylos.constants import ( PENALTIES, AUTO_CALLED )
|
|
10
10
|
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')
|
|
21
17
|
|
|
22
|
-
def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folders=None):
|
|
23
|
-
exclude_set = set()
|
|
24
|
-
|
|
25
|
-
if use_defaults:
|
|
26
|
-
exclude_set.update(DEFAULT_EXCLUDE_FOLDERS)
|
|
27
|
-
|
|
28
|
-
if user_exclude_folders:
|
|
29
|
-
exclude_set.update(user_exclude_folders)
|
|
30
|
-
|
|
31
|
-
if include_folders:
|
|
32
|
-
for folder in include_folders:
|
|
33
|
-
exclude_set.discard(folder)
|
|
34
|
-
|
|
35
|
-
return exclude_set
|
|
36
|
-
|
|
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
18
|
class Skylos:
|
|
52
19
|
def __init__(self):
|
|
53
20
|
self.defs={}
|
|
54
21
|
self.refs=[]
|
|
55
22
|
self.dynamic=set()
|
|
56
23
|
self.exports=defaultdict(set)
|
|
57
|
-
self.ignored_lines:set[int]=set()
|
|
58
24
|
|
|
59
25
|
def _module(self,root,f):
|
|
60
|
-
p=list(f.relative_to(root).parts)
|
|
61
|
-
if p[-1].endswith(".py"):
|
|
62
|
-
|
|
63
|
-
|
|
26
|
+
p = list(f.relative_to(root).parts)
|
|
27
|
+
if p[-1].endswith(".py"):
|
|
28
|
+
p[-1] = p[-1][:-3]
|
|
29
|
+
if p[-1] == "__init__":
|
|
30
|
+
p.pop()
|
|
31
|
+
return ".".join(p)
|
|
64
32
|
|
|
65
33
|
def _should_exclude_file(self, file_path, root_path, exclude_folders):
|
|
66
34
|
if not exclude_folders:
|
|
@@ -111,9 +79,9 @@ class Skylos:
|
|
|
111
79
|
return all_files, root
|
|
112
80
|
|
|
113
81
|
def _mark_exports(self):
|
|
114
|
-
for name,
|
|
115
|
-
if
|
|
116
|
-
|
|
82
|
+
for name, definition in self.defs.items():
|
|
83
|
+
if definition.in_init and not definition.simple_name.startswith('_'):
|
|
84
|
+
definition.is_exported = True
|
|
117
85
|
|
|
118
86
|
for mod, export_names in self.exports.items():
|
|
119
87
|
for name in export_names:
|
|
@@ -137,23 +105,37 @@ class Skylos:
|
|
|
137
105
|
break
|
|
138
106
|
|
|
139
107
|
simple_name_lookup = defaultdict(list)
|
|
140
|
-
for
|
|
141
|
-
simple_name_lookup[
|
|
108
|
+
for definition in self.defs.values():
|
|
109
|
+
simple_name_lookup[definition.simple_name].append(definition)
|
|
142
110
|
|
|
143
111
|
for ref, _ in self.refs:
|
|
144
112
|
if ref in self.defs:
|
|
145
113
|
self.defs[ref].references += 1
|
|
146
|
-
|
|
147
114
|
if ref in import_to_original:
|
|
148
115
|
original = import_to_original[ref]
|
|
149
116
|
self.defs[original].references += 1
|
|
150
117
|
continue
|
|
151
|
-
|
|
118
|
+
|
|
152
119
|
simple = ref.split('.')[-1]
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
120
|
+
ref_mod = ref.rsplit(".", 1)[0]
|
|
121
|
+
candidates = simple_name_lookup.get(simple, [])
|
|
122
|
+
|
|
123
|
+
if ref_mod:
|
|
124
|
+
filtered = []
|
|
125
|
+
for d in candidates:
|
|
126
|
+
if d.name.startswith(ref_mod + ".") and d.type != "import":
|
|
127
|
+
filtered.append(d)
|
|
128
|
+
candidates = filtered
|
|
129
|
+
else:
|
|
130
|
+
filtered = []
|
|
131
|
+
for d in candidates:
|
|
132
|
+
if d.type != "import":
|
|
133
|
+
filtered.append(d)
|
|
134
|
+
candidates = filtered
|
|
135
|
+
|
|
136
|
+
if len(candidates) == 1:
|
|
137
|
+
candidates[0].references += 1
|
|
138
|
+
|
|
157
139
|
for module_name in self.dynamic:
|
|
158
140
|
for def_name, def_obj in self.defs.items():
|
|
159
141
|
if def_obj.name.startswith(f"{module_name}."):
|
|
@@ -172,59 +154,66 @@ class Skylos:
|
|
|
172
154
|
return []
|
|
173
155
|
|
|
174
156
|
def _apply_penalties(self, def_obj, visitor, framework):
|
|
175
|
-
|
|
176
|
-
|
|
157
|
+
confidence=100
|
|
177
158
|
if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
|
|
178
|
-
|
|
159
|
+
confidence -= PENALTIES["private_name"]
|
|
179
160
|
if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
|
|
180
|
-
|
|
181
|
-
if def_obj.type == "variable" and def_obj.simple_name
|
|
182
|
-
|
|
161
|
+
confidence -= PENALTIES["dunder_or_magic"]
|
|
162
|
+
if def_obj.type == "variable" and def_obj.simple_name.isupper():
|
|
163
|
+
confidence = 0
|
|
183
164
|
if def_obj.in_init and def_obj.type in ("function", "class"):
|
|
184
|
-
|
|
165
|
+
confidence -= PENALTIES["in_init_file"]
|
|
185
166
|
if def_obj.name.split(".")[0] in self.dynamic:
|
|
186
|
-
|
|
167
|
+
confidence -= PENALTIES["dynamic_module"]
|
|
187
168
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
188
|
-
|
|
169
|
+
confidence -= PENALTIES["test_related"]
|
|
189
170
|
|
|
190
171
|
framework_confidence = detect_framework_usage(def_obj, visitor=framework)
|
|
191
172
|
if framework_confidence is not None:
|
|
192
|
-
|
|
173
|
+
confidence = min(confidence, framework_confidence)
|
|
193
174
|
|
|
194
175
|
if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
|
|
195
|
-
|
|
176
|
+
confidence = 0
|
|
196
177
|
|
|
197
178
|
if def_obj.type == "parameter":
|
|
198
179
|
if def_obj.simple_name in ("self", "cls"):
|
|
199
|
-
|
|
180
|
+
confidence = 0
|
|
200
181
|
elif "." in def_obj.name:
|
|
201
182
|
method_name = def_obj.name.split(".")[-2]
|
|
202
183
|
if method_name.startswith("__") and method_name.endswith("__"):
|
|
203
|
-
|
|
184
|
+
confidence = 0
|
|
204
185
|
|
|
205
186
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
206
|
-
|
|
187
|
+
confidence = 0
|
|
207
188
|
|
|
208
189
|
if (def_obj.type == "import" and def_obj.name.startswith("__future__.") and
|
|
209
190
|
def_obj.simple_name in ("annotations", "absolute_import", "division",
|
|
210
191
|
"print_function", "unicode_literals", "generator_stop")):
|
|
211
|
-
|
|
192
|
+
confidence = 0
|
|
212
193
|
|
|
213
|
-
def_obj.confidence = max(
|
|
194
|
+
def_obj.confidence = max(confidence, 0)
|
|
214
195
|
|
|
215
196
|
def _apply_heuristics(self):
|
|
216
197
|
class_methods = defaultdict(list)
|
|
217
|
-
for
|
|
218
|
-
if
|
|
219
|
-
cls =
|
|
198
|
+
for definition in self.defs.values():
|
|
199
|
+
if definition.type in ("method", "function") and "." in definition.name:
|
|
200
|
+
cls = definition.name.rsplit(".", 1)[0]
|
|
220
201
|
if cls in self.defs and self.defs[cls].type == "class":
|
|
221
|
-
class_methods[cls].append(
|
|
202
|
+
class_methods[cls].append(definition)
|
|
222
203
|
|
|
223
204
|
for cls, methods in class_methods.items():
|
|
224
205
|
if self.defs[cls].references > 0:
|
|
225
|
-
for
|
|
226
|
-
if
|
|
227
|
-
|
|
206
|
+
for method in methods:
|
|
207
|
+
if method.simple_name in AUTO_CALLED:
|
|
208
|
+
method.references += 1
|
|
209
|
+
|
|
210
|
+
if (method.simple_name.startswith("visit_") or
|
|
211
|
+
method.simple_name.startswith("leave_") or
|
|
212
|
+
method.simple_name.startswith("transform_")):
|
|
213
|
+
method.references += 1
|
|
214
|
+
|
|
215
|
+
if method.simple_name == "format" and cls.endswith("Formatter"):
|
|
216
|
+
method.references += 1
|
|
228
217
|
|
|
229
218
|
def analyze(self, path, thr=60, exclude_folders=None):
|
|
230
219
|
files, root = self._get_python_files(path, exclude_folders)
|
|
@@ -251,25 +240,11 @@ class Skylos:
|
|
|
251
240
|
|
|
252
241
|
for file in files:
|
|
253
242
|
mod = modmap[file]
|
|
243
|
+
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
|
|
254
244
|
|
|
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
|
|
245
|
+
for definition in defs:
|
|
246
|
+
self._apply_penalties(definition, test_flags, framework_flags)
|
|
247
|
+
self.defs[definition.name] = definition
|
|
273
248
|
|
|
274
249
|
self.refs.extend(refs)
|
|
275
250
|
self.dynamic.update(dyn)
|
|
@@ -278,18 +253,22 @@ class Skylos:
|
|
|
278
253
|
self._mark_refs()
|
|
279
254
|
self._apply_heuristics()
|
|
280
255
|
self._mark_exports()
|
|
281
|
-
|
|
282
|
-
thr = max(0, thr)
|
|
283
256
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
continue
|
|
257
|
+
shown = 0
|
|
258
|
+
|
|
259
|
+
def def_sort_key(d):
|
|
260
|
+
return (d.type, d.name)
|
|
289
261
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
262
|
+
for d in sorted(self.defs.values(), key=def_sort_key):
|
|
263
|
+
if shown >= 50:
|
|
264
|
+
break
|
|
265
|
+
print(f" {d.type:<8} refs={d.references:<2} conf={d.confidence:<3} exported={d.is_exported} line={d.line:<4} {d.name}")
|
|
266
|
+
shown += 1
|
|
267
|
+
|
|
268
|
+
unused = []
|
|
269
|
+
for definition in self.defs.values():
|
|
270
|
+
if definition.references == 0 and not definition.is_exported and definition.confidence > 0 and definition.confidence >= thr:
|
|
271
|
+
unused.append(definition.to_dict())
|
|
293
272
|
|
|
294
273
|
result = {
|
|
295
274
|
"unused_functions": [],
|
|
@@ -299,7 +278,7 @@ class Skylos:
|
|
|
299
278
|
"unused_parameters": [],
|
|
300
279
|
"analysis_summary": {
|
|
301
280
|
"total_files": len(files),
|
|
302
|
-
"excluded_folders": exclude_folders
|
|
281
|
+
"excluded_folders": exclude_folders or [],
|
|
303
282
|
}
|
|
304
283
|
}
|
|
305
284
|
|
|
@@ -325,7 +304,6 @@ def proc_file(file_or_args, mod=None):
|
|
|
325
304
|
|
|
326
305
|
try:
|
|
327
306
|
source = Path(file).read_text(encoding="utf-8")
|
|
328
|
-
ignored = _collect_ignored_lines(source)
|
|
329
307
|
tree = ast.parse(source)
|
|
330
308
|
|
|
331
309
|
tv = TestAwareVisitor(filename=file)
|
|
@@ -333,11 +311,11 @@ def proc_file(file_or_args, mod=None):
|
|
|
333
311
|
|
|
334
312
|
fv = FrameworkAwareVisitor(filename=file)
|
|
335
313
|
fv.visit(tree)
|
|
336
|
-
|
|
314
|
+
fv.finalize()
|
|
337
315
|
v = Visitor(mod, file)
|
|
338
316
|
v.visit(tree)
|
|
339
317
|
|
|
340
|
-
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
318
|
+
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
341
319
|
except Exception as e:
|
|
342
320
|
logger.error(f"{file}: {e}")
|
|
343
321
|
if os.getenv("SKYLOS_DEBUG"):
|
|
@@ -345,55 +323,59 @@ def proc_file(file_or_args, mod=None):
|
|
|
345
323
|
dummy_visitor = TestAwareVisitor(filename=file)
|
|
346
324
|
dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
|
|
347
325
|
|
|
348
|
-
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
326
|
+
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
349
327
|
|
|
350
328
|
def analyze(path,conf=60, exclude_folders=None):
|
|
351
329
|
return Skylos().analyze(path,conf, exclude_folders)
|
|
352
330
|
|
|
353
|
-
if __name__=="__main__":
|
|
331
|
+
if __name__ == "__main__":
|
|
354
332
|
if len(sys.argv)>1:
|
|
355
|
-
p=sys.argv[1]
|
|
356
|
-
|
|
333
|
+
p = sys.argv[1]
|
|
334
|
+
confidence = int(sys.argv[2]) if len(sys.argv) >2 else 60
|
|
335
|
+
result = analyze(p,confidence)
|
|
357
336
|
|
|
358
337
|
data = json.loads(result)
|
|
359
|
-
print("\n
|
|
338
|
+
print("\n Python Static Analysis Results")
|
|
360
339
|
print("===================================\n")
|
|
361
340
|
|
|
362
|
-
total_items =
|
|
341
|
+
total_items = 0
|
|
342
|
+
for key, items in data.items():
|
|
343
|
+
if key.startswith("unused_") and isinstance(items, list):
|
|
344
|
+
total_items += len(items)
|
|
363
345
|
|
|
364
346
|
print("Summary:")
|
|
365
347
|
if data["unused_functions"]:
|
|
366
|
-
print(f"
|
|
348
|
+
print(f" * Unreachable functions: {len(data['unused_functions'])}")
|
|
367
349
|
if data["unused_imports"]:
|
|
368
|
-
print(f"
|
|
350
|
+
print(f" * Unused imports: {len(data['unused_imports'])}")
|
|
369
351
|
if data["unused_classes"]:
|
|
370
|
-
print(f"
|
|
352
|
+
print(f" * Unused classes: {len(data['unused_classes'])}")
|
|
371
353
|
if data["unused_variables"]:
|
|
372
|
-
print(f"
|
|
354
|
+
print(f" * Unused variables: {len(data['unused_variables'])}")
|
|
373
355
|
|
|
374
356
|
if data["unused_functions"]:
|
|
375
|
-
print("\n
|
|
357
|
+
print("\n - Unreachable Functions")
|
|
376
358
|
print("=======================")
|
|
377
359
|
for i, func in enumerate(data["unused_functions"], 1):
|
|
378
360
|
print(f" {i}. {func['name']}")
|
|
379
361
|
print(f" └─ {func['file']}:{func['line']}")
|
|
380
362
|
|
|
381
363
|
if data["unused_imports"]:
|
|
382
|
-
print("\n
|
|
364
|
+
print("\n - Unused Imports")
|
|
383
365
|
print("================")
|
|
384
366
|
for i, imp in enumerate(data["unused_imports"], 1):
|
|
385
367
|
print(f" {i}. {imp['simple_name']}")
|
|
386
368
|
print(f" └─ {imp['file']}:{imp['line']}")
|
|
387
369
|
|
|
388
370
|
if data["unused_classes"]:
|
|
389
|
-
print("\n
|
|
371
|
+
print("\n - Unused Classes")
|
|
390
372
|
print("=================")
|
|
391
373
|
for i, cls in enumerate(data["unused_classes"], 1):
|
|
392
374
|
print(f" {i}. {cls['name']}")
|
|
393
375
|
print(f" └─ {cls['file']}:{cls['line']}")
|
|
394
376
|
|
|
395
377
|
if data["unused_variables"]:
|
|
396
|
-
print("\n
|
|
378
|
+
print("\n - Unused Variables")
|
|
397
379
|
print("==================")
|
|
398
380
|
for i, var in enumerate(data["unused_variables"], 1):
|
|
399
381
|
print(f" {i}. {var['name']}")
|
|
@@ -406,7 +388,7 @@ if __name__=="__main__":
|
|
|
406
388
|
print(f"```")
|
|
407
389
|
|
|
408
390
|
print("\nNext steps:")
|
|
409
|
-
print("
|
|
410
|
-
print("
|
|
391
|
+
print(" * Use --interactive to select specific items to remove")
|
|
392
|
+
print(" * Use --dry-run to preview changes before applying them")
|
|
411
393
|
else:
|
|
412
394
|
print("Usage: python Skylos.py <path> [confidence_threshold]")
|