skylos 1.0.9__tar.gz → 1.2.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 (40) hide show
  1. {skylos-1.0.9 → skylos-1.2.0}/PKG-INFO +1 -1
  2. {skylos-1.0.9 → skylos-1.2.0}/README.md +149 -26
  3. {skylos-1.0.9 → skylos-1.2.0}/pyproject.toml +1 -1
  4. {skylos-1.0.9 → skylos-1.2.0}/setup.py +1 -1
  5. {skylos-1.0.9 → skylos-1.2.0}/skylos/__init__.py +1 -1
  6. skylos-1.2.0/skylos/analyzer.py +374 -0
  7. {skylos-1.0.9 → skylos-1.2.0}/skylos/cli.py +105 -5
  8. skylos-1.2.0/skylos/constants.py +42 -0
  9. skylos-1.2.0/skylos/framework_aware.py +158 -0
  10. skylos-1.2.0/skylos/test_aware.py +66 -0
  11. {skylos-1.0.9 → skylos-1.2.0}/skylos/visitor.py +104 -21
  12. {skylos-1.0.9 → skylos-1.2.0}/skylos.egg-info/PKG-INFO +1 -1
  13. {skylos-1.0.9 → skylos-1.2.0}/skylos.egg-info/SOURCES.txt +11 -0
  14. skylos-1.2.0/test/conftest.py +212 -0
  15. skylos-1.2.0/test/test_analyzer.py +612 -0
  16. skylos-1.2.0/test/test_changes_analyzer.py +149 -0
  17. skylos-1.2.0/test/test_cli.py +353 -0
  18. skylos-1.2.0/test/test_constants.py +348 -0
  19. skylos-1.2.0/test/test_framework_aware.py +372 -0
  20. skylos-1.2.0/test/test_integration.py +320 -0
  21. skylos-1.2.0/test/test_test_aware.py +328 -0
  22. skylos-1.2.0/test/test_visitor.py +704 -0
  23. skylos-1.0.9/skylos/analyzer.py +0 -180
  24. skylos-1.0.9/test/test_visitor.py +0 -220
  25. {skylos-1.0.9 → skylos-1.2.0}/setup.cfg +0 -0
  26. {skylos-1.0.9 → skylos-1.2.0}/skylos.egg-info/dependency_links.txt +0 -0
  27. {skylos-1.0.9 → skylos-1.2.0}/skylos.egg-info/entry_points.txt +0 -0
  28. {skylos-1.0.9 → skylos-1.2.0}/skylos.egg-info/requires.txt +0 -0
  29. {skylos-1.0.9 → skylos-1.2.0}/skylos.egg-info/top_level.txt +0 -0
  30. {skylos-1.0.9 → skylos-1.2.0}/test/__init__.py +0 -0
  31. {skylos-1.0.9 → skylos-1.2.0}/test/compare_tools.py +0 -0
  32. {skylos-1.0.9 → skylos-1.2.0}/test/diagnostics.py +0 -0
  33. {skylos-1.0.9 → skylos-1.2.0}/test/sample_repo/__init__.py +0 -0
  34. {skylos-1.0.9 → skylos-1.2.0}/test/sample_repo/app.py +0 -0
  35. {skylos-1.0.9 → skylos-1.2.0}/test/sample_repo/sample_repo/__init__.py +0 -0
  36. {skylos-1.0.9 → skylos-1.2.0}/test/sample_repo/sample_repo/commands.py +0 -0
  37. {skylos-1.0.9 → skylos-1.2.0}/test/sample_repo/sample_repo/models.py +0 -0
  38. {skylos-1.0.9 → skylos-1.2.0}/test/sample_repo/sample_repo/routes.py +0 -0
  39. {skylos-1.0.9 → skylos-1.2.0}/test/sample_repo/sample_repo/utils.py +0 -0
  40. {skylos-1.0.9 → skylos-1.2.0}/test/test_skylos.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 1.0.9
3
+ Version: 1.2.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
@@ -10,12 +10,36 @@
10
10
 
11
11
  > 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
12
 
13
+ ## Table of Contents
14
+
15
+ - [Features](#features)
16
+ - [Benchmark](#benchmark-you-can-find-this-benchmark-test-in-test-folder)
17
+ - [Installation](#installation)
18
+ - [Quick Start](#quick-start)
19
+ - [Understanding Confidence Levels](#understanding-confidence-levels)
20
+ - [Test File Detection](#test-file-detection)
21
+ - [Folder Management](#folder-management)
22
+ - [CLI Options](#cli-options)
23
+ - [Example Output](#example-output)
24
+ - [Interactive Mode](#interactive-mode)
25
+ - [Development](#development)
26
+ - [FAQ](#faq)
27
+ - [Limitations](#limitations)
28
+ - [Troubleshooting](#troubleshooting)
29
+ - [Contributing](#contributing)
30
+ - [Roadmap](#roadmap)
31
+ - [License](#license)
32
+ - [Contact](#contact)
33
+
13
34
  ## Features
14
35
 
15
- * Unused Functions & Methods: Finds functions and methods that are never called
16
- * Unused Classes: Detects classes that are never instantiated or inherited
17
- * Unused Imports: Identifies imports that serve no purpose
18
- * Cross-Module Tracking: Analyzes usage patterns across your entire codebase
36
+ * **Framework-Aware Detection**: Attempt at handling Flask, Django, FastAPI routes and decorators
37
+ * **Test File Exclusion**: Auto excludes test files (you can include it back if you want)
38
+ * **Interactive Cleanup**: Select specific items to remove from CLI
39
+ * **Unused Functions & Methods**: Finds functions and methods that not called
40
+ * **Unused Classes**: Detects classes that are not instantiated or inherited
41
+ * **Unused Imports**: Identifies imports that are not used
42
+ * **Folder Management**: Inclusion/exclusion of directories
19
43
 
20
44
  ## Benchmark (You can find this benchmark test in `test` folder)
21
45
 
@@ -70,10 +94,97 @@ skylos --interactive --dry-run /path/to/your/project
70
94
 
71
95
  # Output to JSON
72
96
  skylos --json /path/to/your/project
97
+
98
+ # With confidence
99
+ skylos path/to/your/file --confidence 20 ## or whatever value u wanna set
73
100
  ```
74
101
 
75
- ## CLI Options
102
+ ## Understanding Confidence Levels
103
+
104
+ Skylos uses a confidence-based system to try to handle Python's dynamic nature and web frameworks.
105
+
106
+ ### How Confidence Works
107
+
108
+ - **Confidence 100**: 100% unused (default imports, obvious dead functions)
109
+ - **Confidence 60**: Default value - conservative detection
110
+ - **Confidence 40**: Framework helpers, functions in web app files
111
+ - **Confidence 20**: Framework routes, decorated functions
112
+ - **Confidence 0**: Show everything
113
+
114
+ ### Framework Detection
115
+
116
+ When Skylos detects web framework imports (Flask, Django, FastAPI), it applies different confidence levels:
117
+
118
+ ```bash
119
+ # only obvious dead codes
120
+ skylos app.py --confidence 60 # THIS IS THE DEFAULT
121
+
122
+ # include unused helpers in framework files
123
+ skylos app.py --confidence 30
124
+
125
+ # include potentially unused routes
126
+ skylos app.py --confidence 20
76
127
 
128
+ # everything.. shows how all potential dead code
129
+ skylos app.py --confidence 0
130
+ ```
131
+
132
+ ## Test File Detection
133
+
134
+ Skylos automatically excludes test files from analysis because test code patterns often appear as "dead code" but are actually called by test frameworks. Should you need to include them in your test, just refer to the [Folder Management](#folder-management)
135
+
136
+ ### What Gets Detected as Test Files
137
+
138
+ **File paths containing:**
139
+ - `/test/` or `/tests/` directories
140
+ - Files ending with `_test.py`
141
+
142
+ **Test imports:**
143
+ - `pytest`, `unittest`, `nose`, `mock`, `responses`
144
+
145
+ **Test decorators:**
146
+ - `@pytest.fixture`, `@pytest.mark.*`
147
+ - `@patch`, `@mock.*`
148
+
149
+ ### Test File Behavior
150
+
151
+ When Skylos detects a test file, it by default, will apply a confidence penalty of 100, which will essentially filter out all dead code detection
152
+
153
+ ```bash
154
+ # This will show 0 dead code because its treated as test file
155
+ /project/tests/test_user.py
156
+ /project/test/helper.py
157
+ /project/utils_test.py
158
+
159
+ # The files will be analyzed normally. Note that it doesn't end with _test.py
160
+ /project/user.py
161
+ /project/test_data.py
162
+ ```
163
+
164
+ ## Including & Excluding Files
165
+
166
+ ### Default Exclusions
167
+ By default, Skylos excludes common folders: `__pycache__`, `.git`, `.pytest_cache`, `.mypy_cache`, `.tox`, `htmlcov`, `.coverage`, `build`, `dist`, `*.egg-info`, `venv`, `.venv`
168
+
169
+ ### Folder Options
170
+ ```bash
171
+ # List default excluded folders
172
+ skylos --list-default-excludes
173
+
174
+ # Exclude single folder (The example here will be venv)
175
+ skylos /path/to/your/project --exclude-folder venv
176
+
177
+ # Exclude multiple folders
178
+ skylos /path/to/your/project --exclude-folder venv --exclude-folder build
179
+
180
+ # Force include normally excluded folders
181
+ skylos /path/to/your/project --include-folder venv
182
+
183
+ # Scan everything (no exclusions)
184
+ skylos path/to/your/project --no-default-excludes
185
+ ```
186
+
187
+ ## CLI Options
77
188
  ```
78
189
  Usage: skylos [OPTIONS] PATH
79
190
 
@@ -81,51 +192,60 @@ Arguments:
81
192
  PATH Path to the Python project to analyze
82
193
 
83
194
  Options:
84
- -h, --help Show this help message and exit
85
- -j, --json Output raw JSON instead of formatted text
86
- -o, --output FILE Write output to file instead of stdout
87
- -v, --verbose Enable verbose output
88
- -i, --interactive Interactively select items to remove
89
- --dry-run Show what would be removed without modifying files
195
+ -h, --help Show this help message and exit
196
+ -j, --json Output raw JSON instead of formatted text
197
+ -o, --output FILE Write output to file instead of stdout
198
+ -v, --verbose Enable verbose output
199
+ -i, --interactive Interactively select items to remove
200
+ --dry-run Show what would be removed without modifying files
201
+ --exclude-folder FOLDER Exclude a folder from analysis (can be used multiple times)
202
+ --include-folder FOLDER Force include a folder that would otherwise be excluded
203
+ --no-default-excludes Don't exclude default folders (__pycache__, .git, venv, etc.)
204
+ --list-default-excludes List the default excluded folders and
205
+ -c, --confidence LEVEL Confidence threshold (0-100). Lower values will show more items.
90
206
  ```
91
207
 
92
208
  ## Example Output
93
209
 
94
210
  ```
95
- 🔍 Python Static Analysis Results
211
+ Python Static Analysis Results
96
212
  ===================================
97
213
 
98
214
  Summary:
99
215
  • Unreachable functions: 48
100
216
  • Unused imports: 8
101
217
 
102
- 📦 Unreachable Functions
218
+ Unreachable Functions
103
219
  ========================
220
+
104
221
  1. module_13.test_function
105
222
  └─ /Users/oha/project/module_13.py:5
106
223
  2. module_13.unused_function
107
224
  └─ /Users/oha/project/module_13.py:13
108
225
  ...
109
226
 
110
- 📥 Unused Imports
227
+
228
+ Unused Imports
111
229
  =================
230
+
112
231
  1. os
113
232
  └─ /Users/oha/project/module_13.py:1
114
233
  2. json
115
234
  └─ /Users/oha/project/module_13.py:3
116
235
  ...
236
+ ```
117
237
 
118
238
  Next steps:
119
- • Use --interactive to select specific items to remove
120
- • Use --dry-run to preview changes before applying them
121
- ```
239
+
240
+ • Use `--interactive` to select specific items to remove
241
+
242
+ • Use `--dry-run` to preview changes before applying them
243
+
122
244
 
123
245
  ## Interactive Mode
124
246
 
125
247
  The interactive mode lets you select specific functions and imports to remove:
126
248
 
127
- ![Interactive Demo](docs/interactive-demo.gif)
128
-
129
249
  1. **Select items**: Use arrow keys and space to select/deselect
130
250
  2. **Confirm changes**: Review selected items before applying
131
251
  3. **Auto-cleanup**: Files are automatically updated
@@ -174,14 +294,20 @@ A: No. Always review results manually, especially for framework code, APIs, and
174
294
  **Q: Why did Ruff underperform?**
175
295
  A: Like all other tools, Ruff is focused on detecting specific, surface-level issues. Tools like Vulture and Skylos are built SPECIFICALLY for dead code detection. It is NOT a specialized dead code detector. If your goal is dead code, then ruff is the wrong tool. It is a good tool but it's like using a wrench to hammer a nail. Good tool, wrong purpose.
176
296
 
297
+ **Q: Why doesn't Skylos detect my unused Flask routes?**
298
+ A: Web framework routes are given low confidence (20) because they might be called by external HTTP requests. Use `--confidence 20` to see them. We acknowledge there are current limitations to this approach so use it sparingly.
299
+
300
+ **Q: What confidence level should I use?**
301
+ A: Start with 60 (default) for safe cleanup. Use 30 for framework applications. Use 20 for more comprehensive auditing.
302
+
177
303
  ## Limitations
178
304
 
179
305
  - **Dynamic code**: `getattr()`, `globals()`, runtime imports are hard to detect
180
- - **Frameworks**: Django models, Flask routes may appear unused but aren't
306
+ - **Frameworks**: Django models, Flask, FastAPI routes may appear unused but aren't
181
307
  - **Test data**: Limited scenarios, your mileage may vary
182
308
  - **False positives**: Always manually review before deleting code
183
309
 
184
- If we can detect 100% of all dead code in any structure, we wouldn't be sitting here. But we definitely tried our best
310
+ 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.
185
311
 
186
312
  ## Troubleshooting
187
313
 
@@ -212,14 +338,11 @@ We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING
212
338
  5. Open a Pull Request
213
339
 
214
340
  ## Roadmap
215
-
216
- - [ ] Expand our test cases
341
+ - [x] Expand our test cases
217
342
  - [ ] Configuration file support
218
- - [ ] Custom analysis rules
219
343
  - [ ] Git hooks integration
220
344
  - [ ] CI/CD integration examples
221
- - [ ] Web interface
222
- - [ ] Support for other languages
345
+ ~~- [] Support for other languages (unless I have contributors, this ain't possible)~~
223
346
  - [ ] Further optimization
224
347
 
225
348
  ## License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skylos"
7
- version = "1.0.9"
7
+ version = "1.2.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"}]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="skylos",
5
- version="1.0.9",
5
+ version="1.2.0",
6
6
  packages=find_packages(),
7
7
  python_requires=">=3.9",
8
8
  install_requires=["inquirer>=3.0.0"],
@@ -1,6 +1,6 @@
1
1
  from skylos.analyzer import analyze
2
2
 
3
- __version__ = "1.0.9"
3
+ __version__ = "1.0.22"
4
4
 
5
5
  def debug_test():
6
6
  return "debug-ok"
@@ -0,0 +1,374 @@
1
+ #!/usr/bin/env python3
2
+ import ast
3
+ import sys
4
+ import json
5
+ import logging
6
+ from pathlib import Path
7
+ from collections import defaultdict
8
+ from skylos.visitor import Visitor
9
+ from skylos.constants import ( DEFAULT_EXCLUDE_FOLDERS, PENALTIES, AUTO_CALLED )
10
+ from skylos.test_aware import TestAwareVisitor
11
+ import os
12
+ import traceback
13
+ from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
14
+
15
+ logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
16
+ logger=logging.getLogger('Skylos')
17
+
18
+ def parse_exclude_folders(user_exclude_folders, use_defaults=True, include_folders=None):
19
+ exclude_set = set()
20
+
21
+ if use_defaults:
22
+ exclude_set.update(DEFAULT_EXCLUDE_FOLDERS)
23
+
24
+ if user_exclude_folders:
25
+ exclude_set.update(user_exclude_folders)
26
+
27
+ if include_folders:
28
+ for folder in include_folders:
29
+ exclude_set.discard(folder)
30
+
31
+ return exclude_set
32
+
33
+ class Skylos:
34
+ def __init__(self):
35
+ self.defs={}
36
+ self.refs=[]
37
+ self.dynamic=set()
38
+ self.exports=defaultdict(set)
39
+
40
+ def _module(self,root,f):
41
+ p=list(f.relative_to(root).parts)
42
+ if p[-1].endswith(".py"):p[-1]=p[-1][:-3]
43
+ if p[-1]=="__init__":p.pop()
44
+ return".".join(p)
45
+
46
+ def _should_exclude_file(self, file_path, root_path, exclude_folders):
47
+ if not exclude_folders:
48
+ return False
49
+
50
+ try:
51
+ rel_path = file_path.relative_to(root_path)
52
+ except ValueError:
53
+ return False
54
+
55
+ path_parts = rel_path.parts
56
+
57
+ for exclude_folder in exclude_folders:
58
+ if "*" in exclude_folder:
59
+ for part in path_parts:
60
+ if part.endswith(exclude_folder.replace("*", "")):
61
+ return True
62
+ else:
63
+ if exclude_folder in path_parts:
64
+ return True
65
+
66
+ return False
67
+
68
+ def _get_python_files(self, path, exclude_folders=None):
69
+ p = Path(path).resolve()
70
+
71
+ if p.is_file():
72
+ return [p], p.parent
73
+
74
+ root = p
75
+ all_files = list(p.glob("**/*.py"))
76
+
77
+ if exclude_folders:
78
+ filtered_files = []
79
+ excluded_count = 0
80
+
81
+ for file_path in all_files:
82
+ if self._should_exclude_file(file_path, root, exclude_folders):
83
+ excluded_count += 1
84
+ continue
85
+ filtered_files.append(file_path)
86
+
87
+ if excluded_count > 0:
88
+ logger.info(f"Excluded {excluded_count} files from analysis")
89
+
90
+ return filtered_files, root
91
+
92
+ return all_files, root
93
+
94
+ def _mark_exports(self):
95
+ for name, d in self.defs.items():
96
+ if d.in_init and not d.simple_name.startswith('_'):
97
+ d.is_exported = True
98
+
99
+ for mod, export_names in self.exports.items():
100
+ for name in export_names:
101
+ for def_name, def_obj in self.defs.items():
102
+ if (def_name.startswith(f"{mod}.") and
103
+ def_obj.simple_name == name and
104
+ def_obj.type != "import"):
105
+ def_obj.is_exported = True
106
+
107
+ def _mark_refs(self):
108
+ import_to_original = {}
109
+ for name, def_obj in self.defs.items():
110
+ if def_obj.type == "import":
111
+ import_name = name.split('.')[-1]
112
+
113
+ for def_name, orig_def in self.defs.items():
114
+ if (orig_def.type != "import" and
115
+ orig_def.simple_name == import_name and
116
+ def_name != name):
117
+ import_to_original[name] = def_name
118
+ break
119
+
120
+ simple_name_lookup = defaultdict(list)
121
+ for d in self.defs.values():
122
+ simple_name_lookup[d.simple_name].append(d)
123
+
124
+ for ref, _ in self.refs:
125
+ if ref in self.defs:
126
+ self.defs[ref].references += 1
127
+
128
+ if ref in import_to_original:
129
+ original = import_to_original[ref]
130
+ self.defs[original].references += 1
131
+ continue
132
+
133
+ simple = ref.split('.')[-1]
134
+ matches = simple_name_lookup.get(simple, [])
135
+ for d in matches:
136
+ d.references += 1
137
+
138
+ for module_name in self.dynamic:
139
+ for def_name, def_obj in self.defs.items():
140
+ if def_obj.name.startswith(f"{module_name}."):
141
+ if def_obj.type in ("function", "method") and not def_obj.simple_name.startswith("_"):
142
+ def_obj.references += 1
143
+
144
+ def _get_base_classes(self, class_name):
145
+ if class_name not in self.defs:
146
+ return []
147
+
148
+ class_def = self.defs[class_name]
149
+
150
+ if hasattr(class_def, 'base_classes'):
151
+ return class_def.base_classes
152
+
153
+ return []
154
+
155
+ def _apply_penalties(self, def_obj, visitor, framework):
156
+ c = 100
157
+
158
+ if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
159
+ c -= PENALTIES["private_name"]
160
+ if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
161
+ c -= PENALTIES["dunder_or_magic"]
162
+ if def_obj.type == "variable" and def_obj.simple_name == "_":
163
+ c -= PENALTIES["underscored_var"]
164
+ if def_obj.in_init and def_obj.type in ("function", "class"):
165
+ c -= PENALTIES["in_init_file"]
166
+ if def_obj.name.split(".")[0] in self.dynamic:
167
+ c -= PENALTIES["dynamic_module"]
168
+ if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
169
+ c -= PENALTIES["test_related"]
170
+
171
+ framework_confidence = detect_framework_usage(def_obj, visitor=framework)
172
+ if framework_confidence is not None:
173
+ c = min(c, framework_confidence)
174
+
175
+ if (def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__")):
176
+ c = 0
177
+
178
+ if def_obj.type == "parameter":
179
+ if def_obj.simple_name in ("self", "cls"):
180
+ c = 0
181
+ elif "." in def_obj.name:
182
+ method_name = def_obj.name.split(".")[-2]
183
+ if method_name.startswith("__") and method_name.endswith("__"):
184
+ c = 0
185
+
186
+ if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
187
+ c = 0
188
+
189
+ if (def_obj.type == "import" and def_obj.name.startswith("__future__.") and
190
+ def_obj.simple_name in ("annotations", "absolute_import", "division",
191
+ "print_function", "unicode_literals", "generator_stop")):
192
+ c = 0
193
+
194
+ def_obj.confidence = max(c, 0)
195
+
196
+ def _apply_heuristics(self):
197
+ class_methods = defaultdict(list)
198
+ for d in self.defs.values():
199
+ if d.type in ("method", "function") and "." in d.name:
200
+ cls = d.name.rsplit(".", 1)[0]
201
+ if cls in self.defs and self.defs[cls].type == "class":
202
+ class_methods[cls].append(d)
203
+
204
+ for cls, methods in class_methods.items():
205
+ if self.defs[cls].references > 0:
206
+ for m in methods:
207
+ if m.simple_name in AUTO_CALLED: # __init__, __enter__, __exit__
208
+ m.references += 1
209
+
210
+ def analyze(self, path, thr=60, exclude_folders=None):
211
+ files, root = self._get_python_files(path, exclude_folders)
212
+
213
+ if not files:
214
+ logger.warning(f"No Python files found in {path}")
215
+ return json.dumps({
216
+ "unused_functions": [],
217
+ "unused_imports": [],
218
+ "unused_classes": [],
219
+ "unused_variables": [],
220
+ "unused_parameters": [],
221
+ "analysis_summary": {
222
+ "total_files": 0,
223
+ "excluded_folders": exclude_folders if exclude_folders else []
224
+ }
225
+ })
226
+
227
+ logger.info(f"Analyzing {len(files)} Python files...")
228
+
229
+ modmap = {}
230
+ for f in files:
231
+ modmap[f] = self._module(root, f)
232
+
233
+ for file in files:
234
+ mod = modmap[file]
235
+ defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
236
+
237
+ # apply penalties while we still have the file-specific flags
238
+ for d in defs:
239
+ self._apply_penalties(d, test_flags, framework_flags)
240
+ self.defs[d.name] = d
241
+
242
+ self.refs.extend(refs)
243
+ self.dynamic.update(dyn)
244
+ self.exports[mod].update(exports)
245
+
246
+ self._mark_refs()
247
+ self._apply_heuristics()
248
+ self._mark_exports()
249
+
250
+ thr = max(0, thr)
251
+
252
+ unused = []
253
+ for d in self.defs.values():
254
+ if d.references == 0 and not d.is_exported and d.confidence > 0 and d.confidence >= thr:
255
+ unused.append(d.to_dict())
256
+
257
+ result = {
258
+ "unused_functions": [],
259
+ "unused_imports": [],
260
+ "unused_classes": [],
261
+ "unused_variables": [],
262
+ "unused_parameters": [],
263
+ "analysis_summary": {
264
+ "total_files": len(files),
265
+ "excluded_folders": exclude_folders if exclude_folders else [],
266
+ }
267
+ }
268
+
269
+ for u in unused:
270
+ if u["type"] in ("function", "method"):
271
+ result["unused_functions"].append(u)
272
+ elif u["type"] == "import":
273
+ result["unused_imports"].append(u)
274
+ elif u["type"] == "class":
275
+ result["unused_classes"].append(u)
276
+ elif u["type"] == "variable":
277
+ result["unused_variables"].append(u)
278
+ elif u["type"] == "parameter":
279
+ result["unused_parameters"].append(u)
280
+
281
+ return json.dumps(result, indent=2)
282
+
283
+ def proc_file(file_or_args, mod=None):
284
+ if mod is None and isinstance(file_or_args, tuple):
285
+ file, mod = file_or_args
286
+ else:
287
+ file = file_or_args
288
+
289
+ try:
290
+ source = Path(file).read_text(encoding="utf-8")
291
+ tree = ast.parse(source)
292
+
293
+ tv = TestAwareVisitor(filename=file)
294
+ tv.visit(tree)
295
+
296
+ fv = FrameworkAwareVisitor(filename=file)
297
+ fv.visit(tree)
298
+
299
+ v = Visitor(mod, file)
300
+ v.visit(tree)
301
+
302
+ return v.defs, v.refs, v.dyn, v.exports, tv, fv
303
+ except Exception as e:
304
+ logger.error(f"{file}: {e}")
305
+ if os.getenv("SKYLOS_DEBUG"):
306
+ logger.error(traceback.format_exc())
307
+ dummy_visitor = TestAwareVisitor(filename=file)
308
+ dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
309
+
310
+ return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
311
+
312
+ def analyze(path,conf=60, exclude_folders=None):
313
+ return Skylos().analyze(path,conf, exclude_folders)
314
+
315
+ if __name__=="__main__":
316
+ if len(sys.argv)>1:
317
+ p=sys.argv[1];c=int(sys.argv[2])if len(sys.argv)>2 else 60
318
+ result = analyze(p,c)
319
+
320
+ data = json.loads(result)
321
+ print("\n🔍 Python Static Analysis Results")
322
+ print("===================================\n")
323
+
324
+ total_items = sum(len(items) for items in data.values())
325
+
326
+ print("Summary:")
327
+ if data["unused_functions"]:
328
+ print(f" • Unreachable functions: {len(data['unused_functions'])}")
329
+ if data["unused_imports"]:
330
+ print(f" • Unused imports: {len(data['unused_imports'])}")
331
+ if data["unused_classes"]:
332
+ print(f" • Unused classes: {len(data['unused_classes'])}")
333
+ if data["unused_variables"]:
334
+ print(f" • Unused variables: {len(data['unused_variables'])}")
335
+
336
+ if data["unused_functions"]:
337
+ print("\n📦 Unreachable Functions")
338
+ print("=======================")
339
+ for i, func in enumerate(data["unused_functions"], 1):
340
+ print(f" {i}. {func['name']}")
341
+ print(f" └─ {func['file']}:{func['line']}")
342
+
343
+ if data["unused_imports"]:
344
+ print("\n📥 Unused Imports")
345
+ print("================")
346
+ for i, imp in enumerate(data["unused_imports"], 1):
347
+ print(f" {i}. {imp['simple_name']}")
348
+ print(f" └─ {imp['file']}:{imp['line']}")
349
+
350
+ if data["unused_classes"]:
351
+ print("\n📋 Unused Classes")
352
+ print("=================")
353
+ for i, cls in enumerate(data["unused_classes"], 1):
354
+ print(f" {i}. {cls['name']}")
355
+ print(f" └─ {cls['file']}:{cls['line']}")
356
+
357
+ if data["unused_variables"]:
358
+ print("\n📊 Unused Variables")
359
+ print("==================")
360
+ for i, var in enumerate(data["unused_variables"], 1):
361
+ print(f" {i}. {var['name']}")
362
+ print(f" └─ {var['file']}:{var['line']}")
363
+
364
+ print("\n" + "─" * 50)
365
+ print(f"Found {total_items} dead code items. Add this badge to your README:")
366
+ print(f"```markdown")
367
+ print(f"![Dead Code: {total_items}](https://img.shields.io/badge/Dead_Code-{total_items}_detected-orange?logo=codacy&logoColor=red)")
368
+ print(f"```")
369
+
370
+ print("\nNext steps:")
371
+ print(" • Use --interactive to select specific items to remove")
372
+ print(" • Use --dry-run to preview changes before applying them")
373
+ else:
374
+ print("Usage: python Skylos.py <path> [confidence_threshold]")