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.

Files changed (45) hide show
  1. {skylos-1.2.2 → skylos-2.1.0}/PKG-INFO +4 -1
  2. {skylos-1.2.2 → skylos-2.1.0}/README.md +50 -54
  3. {skylos-1.2.2 → skylos-2.1.0}/pyproject.toml +2 -2
  4. {skylos-1.2.2 → skylos-2.1.0}/setup.py +7 -2
  5. {skylos-1.2.2 → skylos-2.1.0}/skylos/__init__.py +1 -1
  6. {skylos-1.2.2 → skylos-2.1.0}/skylos/analyzer.py +103 -121
  7. {skylos-1.2.2 → skylos-2.1.0}/skylos/cli.py +66 -109
  8. skylos-2.1.0/skylos/codemods.py +89 -0
  9. {skylos-1.2.2 → skylos-2.1.0}/skylos/constants.py +25 -10
  10. skylos-2.1.0/skylos/framework_aware.py +358 -0
  11. skylos-2.1.0/skylos/server.py +560 -0
  12. {skylos-1.2.2 → skylos-2.1.0}/skylos/test_aware.py +0 -1
  13. skylos-2.1.0/skylos/visitor.py +495 -0
  14. {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/PKG-INFO +4 -1
  15. {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/SOURCES.txt +3 -0
  16. skylos-2.1.0/skylos.egg-info/requires.txt +4 -0
  17. skylos-2.1.0/test/test_codemods.py +153 -0
  18. skylos-2.1.0/test/test_framework_aware.py +306 -0
  19. skylos-1.2.2/skylos/framework_aware.py +0 -158
  20. skylos-1.2.2/skylos/visitor.py +0 -336
  21. skylos-1.2.2/skylos.egg-info/requires.txt +0 -1
  22. skylos-1.2.2/test/test_framework_aware.py +0 -372
  23. {skylos-1.2.2 → skylos-2.1.0}/setup.cfg +0 -0
  24. {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/dependency_links.txt +0 -0
  25. {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/entry_points.txt +0 -0
  26. {skylos-1.2.2 → skylos-2.1.0}/skylos.egg-info/top_level.txt +0 -0
  27. {skylos-1.2.2 → skylos-2.1.0}/test/__init__.py +0 -0
  28. {skylos-1.2.2 → skylos-2.1.0}/test/compare_tools.py +0 -0
  29. {skylos-1.2.2 → skylos-2.1.0}/test/conftest.py +0 -0
  30. {skylos-1.2.2 → skylos-2.1.0}/test/diagnostics.py +0 -0
  31. {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/__init__.py +0 -0
  32. {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/app.py +0 -0
  33. {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/__init__.py +0 -0
  34. {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/commands.py +0 -0
  35. {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/models.py +0 -0
  36. {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/routes.py +0 -0
  37. {skylos-1.2.2 → skylos-2.1.0}/test/sample_repo/sample_repo/utils.py +0 -0
  38. {skylos-1.2.2 → skylos-2.1.0}/test/test_analyzer.py +0 -0
  39. {skylos-1.2.2 → skylos-2.1.0}/test/test_changes_analyzer.py +0 -0
  40. {skylos-1.2.2 → skylos-2.1.0}/test/test_cli.py +0 -0
  41. {skylos-1.2.2 → skylos-2.1.0}/test/test_constants.py +0 -0
  42. {skylos-1.2.2 → skylos-2.1.0}/test/test_integration.py +0 -0
  43. {skylos-1.2.2 → skylos-2.1.0}/test/test_skylos.py +0 -0
  44. {skylos-1.2.2 → skylos-2.1.0}/test/test_test_aware.py +0 -0
  45. {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.2.2
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 (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.
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
- - [Understanding Confidence Levels](#understanding-confidence-levels)
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
- ## Understanding Confidence Levels
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 Show this help message and exit
198
- -j, --json Output raw JSON instead of formatted text
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 space to select/deselect
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.2.2"
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.2.2",
5
+ version="2.1.0",
6
6
  packages=find_packages(),
7
7
  python_requires=">=3.9",
8
- install_requires=["inquirer>=3.0.0"],
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",
@@ -1,6 +1,6 @@
1
1
  from skylos.analyzer import analyze
2
2
 
3
- __version__ = "1.0.22"
3
+ __version__ = "2.1.0"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
@@ -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 ( DEFAULT_EXCLUDE_FOLDERS, PENALTIES, AUTO_CALLED )
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"):p[-1]=p[-1][:-3]
62
- if p[-1]=="__init__":p.pop()
63
- return".".join(p)
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, d in self.defs.items():
115
- if d.in_init and not d.simple_name.startswith('_'):
116
- d.is_exported = True
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 d in self.defs.values():
141
- simple_name_lookup[d.simple_name].append(d)
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
- matches = simple_name_lookup.get(simple, [])
154
- for d in matches:
155
- d.references += 1
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
- c = 100
176
-
157
+ confidence=100
177
158
  if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
178
- c -= PENALTIES["private_name"]
159
+ confidence -= PENALTIES["private_name"]
179
160
  if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
180
- c -= PENALTIES["dunder_or_magic"]
181
- if def_obj.type == "variable" and def_obj.simple_name == "_":
182
- c -= PENALTIES["underscored_var"]
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
- c -= PENALTIES["in_init_file"]
165
+ confidence -= PENALTIES["in_init_file"]
185
166
  if def_obj.name.split(".")[0] in self.dynamic:
186
- c -= PENALTIES["dynamic_module"]
167
+ confidence -= PENALTIES["dynamic_module"]
187
168
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
188
- c -= PENALTIES["test_related"]
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
- c = min(c, framework_confidence)
173
+ confidence = min(confidence, framework_confidence)
193
174
 
194
175
  if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
195
- c = 0
176
+ confidence = 0
196
177
 
197
178
  if def_obj.type == "parameter":
198
179
  if def_obj.simple_name in ("self", "cls"):
199
- c = 0
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
- c = 0
184
+ confidence = 0
204
185
 
205
186
  if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
206
- c = 0
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
- c = 0
192
+ confidence = 0
212
193
 
213
- def_obj.confidence = max(c, 0)
194
+ def_obj.confidence = max(confidence, 0)
214
195
 
215
196
  def _apply_heuristics(self):
216
197
  class_methods = defaultdict(list)
217
- for d in self.defs.values():
218
- if d.type in ("method", "function") and "." in d.name:
219
- cls = d.name.rsplit(".", 1)[0]
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(d)
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 m in methods:
226
- if m.simple_name in AUTO_CALLED: # __init__, __enter__, __exit__
227
- m.references += 1
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
- result = proc_file(file, mod)
256
-
257
- if len(result) == 7: ##new
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
- unused = []
285
- for d in self.defs.values():
286
- # skip anything on line that carries ignore pragma
287
- if d.line in self.ignored_lines:
288
- continue
257
+ shown = 0
258
+
259
+ def def_sort_key(d):
260
+ return (d.type, d.name)
289
261
 
290
- if (d.references == 0 and not d.is_exported
291
- and d.confidence >= thr):
292
- unused.append(d.to_dict())
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 if exclude_folders else [],
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, ignored
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, set()
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];c=int(sys.argv[2])if len(sys.argv)>2 else 60
356
- result = analyze(p,c)
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🔍 Python Static Analysis Results")
338
+ print("\n Python Static Analysis Results")
360
339
  print("===================================\n")
361
340
 
362
- total_items = sum(len(items) for items in data.values())
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" Unreachable functions: {len(data['unused_functions'])}")
348
+ print(f" * Unreachable functions: {len(data['unused_functions'])}")
367
349
  if data["unused_imports"]:
368
- print(f" Unused imports: {len(data['unused_imports'])}")
350
+ print(f" * Unused imports: {len(data['unused_imports'])}")
369
351
  if data["unused_classes"]:
370
- print(f" Unused classes: {len(data['unused_classes'])}")
352
+ print(f" * Unused classes: {len(data['unused_classes'])}")
371
353
  if data["unused_variables"]:
372
- print(f" Unused variables: {len(data['unused_variables'])}")
354
+ print(f" * Unused variables: {len(data['unused_variables'])}")
373
355
 
374
356
  if data["unused_functions"]:
375
- print("\n📦 Unreachable Functions")
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📥 Unused Imports")
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📋 Unused Classes")
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📊 Unused Variables")
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(" Use --interactive to select specific items to remove")
410
- print(" Use --dry-run to preview changes before applying them")
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]")