skylos 0.0.5__cp310-cp310-macosx_11_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +30 -0
- skylos/_core.cpython-310-darwin.so +0 -0
- skylos/cli.py +332 -0
- skylos-0.0.5.dist-info/METADATA +306 -0
- skylos-0.0.5.dist-info/RECORD +7 -0
- skylos-0.0.5.dist-info/WHEEL +4 -0
- skylos-0.0.5.dist-info/entry_points.txt +2 -0
skylos/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from importlib import metadata as _md
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = _md.version(__name__)
|
|
5
|
+
except _md.PackageNotFoundError:
|
|
6
|
+
__version__ = "0.0.0.dev0"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from ._core import analyze as _analyze
|
|
10
|
+
|
|
11
|
+
def analyze(path: str) -> str:
|
|
12
|
+
"""Return the JSON produced by Rust's ``analyze``."""
|
|
13
|
+
return _analyze(path)
|
|
14
|
+
|
|
15
|
+
# ------------------------------------------------------------------ #
|
|
16
|
+
# helpers the tests expect
|
|
17
|
+
# ------------------------------------------------------------------ #
|
|
18
|
+
def debug_test() -> str:
|
|
19
|
+
return "debug-ok"
|
|
20
|
+
|
|
21
|
+
__all__ = ["analyze", "debug_test", "__version__"]
|
|
22
|
+
|
|
23
|
+
def _patch_metadata() -> None:
|
|
24
|
+
orig_version = _md.version
|
|
25
|
+
def _fake_version(name: str): # type: ignore
|
|
26
|
+
if name == __name__:
|
|
27
|
+
return __version__
|
|
28
|
+
return orig_version(name)
|
|
29
|
+
_md.version = _fake_version
|
|
30
|
+
_patch_metadata()
|
|
Binary file
|
skylos/cli.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import logging
|
|
5
|
+
import ast
|
|
6
|
+
import skylos
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import inquirer
|
|
10
|
+
INTERACTIVE_AVAILABLE = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
INTERACTIVE_AVAILABLE = False
|
|
13
|
+
|
|
14
|
+
class Colors:
|
|
15
|
+
RED = '\033[91m'
|
|
16
|
+
GREEN = '\033[92m'
|
|
17
|
+
YELLOW = '\033[93m'
|
|
18
|
+
BLUE = '\033[94m'
|
|
19
|
+
MAGENTA = '\033[95m'
|
|
20
|
+
CYAN = '\033[96m'
|
|
21
|
+
WHITE = '\033[97m'
|
|
22
|
+
BOLD = '\033[1m'
|
|
23
|
+
UNDERLINE = '\033[4m'
|
|
24
|
+
RESET = '\033[0m'
|
|
25
|
+
GRAY = '\033[90m'
|
|
26
|
+
|
|
27
|
+
class ColoredFormatter(logging.Formatter):
|
|
28
|
+
def format(self, record):
|
|
29
|
+
return super().format(record)
|
|
30
|
+
|
|
31
|
+
def setup_logger(output_file=None):
|
|
32
|
+
logger = logging.getLogger('skylos')
|
|
33
|
+
logger.setLevel(logging.INFO)
|
|
34
|
+
|
|
35
|
+
logger.handlers.clear()
|
|
36
|
+
|
|
37
|
+
formatter = ColoredFormatter('%(message)s')
|
|
38
|
+
|
|
39
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
40
|
+
console_handler.setFormatter(formatter)
|
|
41
|
+
logger.addHandler(console_handler)
|
|
42
|
+
|
|
43
|
+
if output_file:
|
|
44
|
+
file_handler = logging.FileHandler(output_file)
|
|
45
|
+
file_handler.setFormatter(formatter)
|
|
46
|
+
logger.addHandler(file_handler)
|
|
47
|
+
|
|
48
|
+
return logger
|
|
49
|
+
|
|
50
|
+
def remove_unused_import(file_path: str, import_name: str, line_number: int) -> bool:
|
|
51
|
+
try:
|
|
52
|
+
with open(file_path, 'r') as f:
|
|
53
|
+
lines = f.readlines()
|
|
54
|
+
|
|
55
|
+
line_idx = line_number - 1
|
|
56
|
+
original_line = lines[line_idx].strip()
|
|
57
|
+
|
|
58
|
+
if original_line.startswith(f'import {import_name}'):
|
|
59
|
+
lines[line_idx] = ''
|
|
60
|
+
elif original_line.startswith('import ') and f' {import_name}' in original_line:
|
|
61
|
+
parts = original_line.split(' ', 1)[1].split(',')
|
|
62
|
+
new_parts = [p.strip() for p in parts if p.strip() != import_name]
|
|
63
|
+
if new_parts:
|
|
64
|
+
lines[line_idx] = f'import {", ".join(new_parts)}\n'
|
|
65
|
+
else:
|
|
66
|
+
lines[line_idx] = ''
|
|
67
|
+
elif original_line.startswith('from ') and import_name in original_line:
|
|
68
|
+
if f'import {import_name}' in original_line and ',' not in original_line:
|
|
69
|
+
lines[line_idx] = ''
|
|
70
|
+
else:
|
|
71
|
+
parts = original_line.split('import ', 1)[1].split(',')
|
|
72
|
+
new_parts = [p.strip() for p in parts if p.strip() != import_name]
|
|
73
|
+
if new_parts:
|
|
74
|
+
prefix = original_line.split(' import ')[0]
|
|
75
|
+
lines[line_idx] = f'{prefix} import {", ".join(new_parts)}\n'
|
|
76
|
+
else:
|
|
77
|
+
lines[line_idx] = ''
|
|
78
|
+
|
|
79
|
+
with open(file_path, 'w') as f:
|
|
80
|
+
f.writelines(lines)
|
|
81
|
+
|
|
82
|
+
return True
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logging.error(f"Failed to remove import {import_name} from {file_path}: {e}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
def remove_unused_function(file_path: str, function_name: str, line_number: int) -> bool:
|
|
88
|
+
try:
|
|
89
|
+
with open(file_path, 'r') as f:
|
|
90
|
+
content = f.read()
|
|
91
|
+
|
|
92
|
+
tree = ast.parse(content)
|
|
93
|
+
|
|
94
|
+
lines = content.splitlines()
|
|
95
|
+
for node in ast.walk(tree):
|
|
96
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
97
|
+
if (node.name in function_name and
|
|
98
|
+
node.lineno == line_number):
|
|
99
|
+
|
|
100
|
+
start_line = node.lineno - 1
|
|
101
|
+
|
|
102
|
+
if node.decorator_list:
|
|
103
|
+
start_line = node.decorator_list[0].lineno - 1
|
|
104
|
+
|
|
105
|
+
end_line = len(lines)
|
|
106
|
+
base_indent = len(lines[start_line]) - len(lines[start_line].lstrip())
|
|
107
|
+
|
|
108
|
+
for i in range(node.end_lineno, len(lines)):
|
|
109
|
+
if lines[i].strip() == '':
|
|
110
|
+
continue
|
|
111
|
+
current_indent = len(lines[i]) - len(lines[i].lstrip())
|
|
112
|
+
if current_indent <= base_indent and lines[i].strip():
|
|
113
|
+
end_line = i
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
while end_line < len(lines) and lines[end_line].strip() == '':
|
|
117
|
+
end_line += 1
|
|
118
|
+
|
|
119
|
+
new_lines = lines[:start_line] + lines[end_line:]
|
|
120
|
+
|
|
121
|
+
with open(file_path, 'w') as f:
|
|
122
|
+
f.write('\n'.join(new_lines) + '\n')
|
|
123
|
+
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
return False
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
def interactive_selection(logger, unused_functions, unused_imports):
|
|
132
|
+
if not INTERACTIVE_AVAILABLE:
|
|
133
|
+
logger.error("Interactive mode requires 'inquirer' package. Install with: pip install inquirer")
|
|
134
|
+
return [], []
|
|
135
|
+
|
|
136
|
+
selected_functions = []
|
|
137
|
+
selected_imports = []
|
|
138
|
+
|
|
139
|
+
if unused_functions:
|
|
140
|
+
logger.info(f"\n{Colors.CYAN}{Colors.BOLD}Select unused functions to remove:{Colors.RESET}")
|
|
141
|
+
|
|
142
|
+
function_choices = []
|
|
143
|
+
for item in unused_functions:
|
|
144
|
+
choice_text = f"{item['name']} ({item['file']}:{item['line']})"
|
|
145
|
+
function_choices.append((choice_text, item))
|
|
146
|
+
|
|
147
|
+
questions = [
|
|
148
|
+
inquirer.Checkbox('functions',
|
|
149
|
+
message="Select functions to remove",
|
|
150
|
+
choices=function_choices,
|
|
151
|
+
)
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
answers = inquirer.prompt(questions)
|
|
155
|
+
if answers:
|
|
156
|
+
selected_functions = answers['functions']
|
|
157
|
+
|
|
158
|
+
if unused_imports:
|
|
159
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove:{Colors.RESET}")
|
|
160
|
+
|
|
161
|
+
import_choices = []
|
|
162
|
+
for item in unused_imports:
|
|
163
|
+
choice_text = f"{item['name']} ({item['file']}:{item['line']})"
|
|
164
|
+
import_choices.append((choice_text, item))
|
|
165
|
+
|
|
166
|
+
questions = [
|
|
167
|
+
inquirer.Checkbox('imports',
|
|
168
|
+
message="Select imports to remove",
|
|
169
|
+
choices=import_choices,
|
|
170
|
+
)
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
answers = inquirer.prompt(questions)
|
|
174
|
+
if answers:
|
|
175
|
+
selected_imports = answers['imports']
|
|
176
|
+
|
|
177
|
+
return selected_functions, selected_imports
|
|
178
|
+
|
|
179
|
+
def main() -> None:
|
|
180
|
+
parser = argparse.ArgumentParser(
|
|
181
|
+
description="Detect unreachable functions and unused imports in a Python project"
|
|
182
|
+
)
|
|
183
|
+
parser.add_argument("path", help="Path to the Python project to analyze")
|
|
184
|
+
parser.add_argument(
|
|
185
|
+
"--json",
|
|
186
|
+
action="store_true",
|
|
187
|
+
help="Output raw JSON instead of formatted text",
|
|
188
|
+
)
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"--output",
|
|
191
|
+
"-o",
|
|
192
|
+
type=str,
|
|
193
|
+
help="Write output to file instead of stdout",
|
|
194
|
+
)
|
|
195
|
+
parser.add_argument(
|
|
196
|
+
"--verbose", "-v",
|
|
197
|
+
action="store_true",
|
|
198
|
+
help="Enable verbose output"
|
|
199
|
+
)
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--interactive", "-i",
|
|
202
|
+
action="store_true",
|
|
203
|
+
help="Interactively select items to remove (requires inquirer)"
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"--dry-run",
|
|
207
|
+
action="store_true",
|
|
208
|
+
help="Show what would be removed without actually modifying files"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
args = parser.parse_args()
|
|
212
|
+
logger = setup_logger(args.output)
|
|
213
|
+
|
|
214
|
+
if args.verbose:
|
|
215
|
+
logger.setLevel(logging.DEBUG)
|
|
216
|
+
logger.debug(f"Analyzing path: {args.path}")
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
result_json = skylos.analyze(args.path)
|
|
220
|
+
result = json.loads(result_json)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(f"Error during analysis: {e}")
|
|
223
|
+
sys.exit(1)
|
|
224
|
+
|
|
225
|
+
if args.json:
|
|
226
|
+
logger.info(result_json)
|
|
227
|
+
else:
|
|
228
|
+
unused_functions = result.get("unused_functions", [])
|
|
229
|
+
unused_imports = result.get("unused_imports", [])
|
|
230
|
+
|
|
231
|
+
logger.info(f"\n{Colors.CYAN}{Colors.BOLD}🔍 Python Static Analysis Results{Colors.RESET}")
|
|
232
|
+
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
233
|
+
|
|
234
|
+
logger.info(f"\n{Colors.BOLD}Summary:{Colors.RESET}")
|
|
235
|
+
logger.info(f" • Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
|
|
236
|
+
logger.info(f" • Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
|
|
237
|
+
|
|
238
|
+
if args.interactive and (unused_functions or unused_imports):
|
|
239
|
+
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
240
|
+
selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
|
|
241
|
+
|
|
242
|
+
if selected_functions or selected_imports:
|
|
243
|
+
logger.info(f"\n{Colors.BOLD}Selected items to remove:{Colors.RESET}")
|
|
244
|
+
|
|
245
|
+
if selected_functions:
|
|
246
|
+
logger.info(f" Functions: {len(selected_functions)}")
|
|
247
|
+
for func in selected_functions:
|
|
248
|
+
logger.info(f" - {func['name']} ({func['file']}:{func['line']})")
|
|
249
|
+
|
|
250
|
+
if selected_imports:
|
|
251
|
+
logger.info(f" Imports: {len(selected_imports)}")
|
|
252
|
+
for imp in selected_imports:
|
|
253
|
+
logger.info(f" - {imp['name']} ({imp['file']}:{imp['line']})")
|
|
254
|
+
|
|
255
|
+
if not args.dry_run:
|
|
256
|
+
questions = [
|
|
257
|
+
inquirer.Confirm('confirm',
|
|
258
|
+
message="Are you sure you want to remove these items?",
|
|
259
|
+
default=False)
|
|
260
|
+
]
|
|
261
|
+
answers = inquirer.prompt(questions)
|
|
262
|
+
|
|
263
|
+
if answers and answers['confirm']:
|
|
264
|
+
logger.info(f"\n{Colors.YELLOW}Removing selected items...{Colors.RESET}")
|
|
265
|
+
|
|
266
|
+
for func in selected_functions:
|
|
267
|
+
success = remove_unused_function(func['file'], func['name'], func['line'])
|
|
268
|
+
if success:
|
|
269
|
+
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed function: {func['name']}")
|
|
270
|
+
else:
|
|
271
|
+
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {func['name']}")
|
|
272
|
+
|
|
273
|
+
for imp in selected_imports:
|
|
274
|
+
success = remove_unused_import(imp['file'], imp['name'], imp['line'])
|
|
275
|
+
if success:
|
|
276
|
+
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed import: {imp['name']}")
|
|
277
|
+
else:
|
|
278
|
+
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {imp['name']}")
|
|
279
|
+
|
|
280
|
+
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
281
|
+
else:
|
|
282
|
+
logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
|
|
283
|
+
else:
|
|
284
|
+
logger.info(f"\n{Colors.YELLOW}Dry run - no files were modified.{Colors.RESET}")
|
|
285
|
+
else:
|
|
286
|
+
logger.info(f"\n{Colors.BLUE}No items selected.{Colors.RESET}")
|
|
287
|
+
|
|
288
|
+
else:
|
|
289
|
+
if unused_functions:
|
|
290
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD}📦 Unreachable Functions{Colors.RESET}")
|
|
291
|
+
logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
|
|
292
|
+
for i, item in enumerate(unused_functions, 1):
|
|
293
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
|
|
294
|
+
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
295
|
+
else:
|
|
296
|
+
logger.info(f"\n{Colors.GREEN}✓ All functions are reachable!{Colors.RESET}")
|
|
297
|
+
|
|
298
|
+
if unused_imports:
|
|
299
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}📥 Unused Imports{Colors.RESET}")
|
|
300
|
+
logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
|
|
301
|
+
for i, item in enumerate(unused_imports, 1):
|
|
302
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
|
|
303
|
+
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
304
|
+
else:
|
|
305
|
+
logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
|
|
306
|
+
|
|
307
|
+
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
308
|
+
logger.info(f"{Colors.GRAY}Analysis complete.{Colors.RESET}")
|
|
309
|
+
|
|
310
|
+
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
311
|
+
logger.info(f"{Colors.GRAY}Analysis complete.{Colors.RESET}")
|
|
312
|
+
|
|
313
|
+
dead_code_count = len(unused_functions) + len(unused_imports)
|
|
314
|
+
|
|
315
|
+
if dead_code_count == 0:
|
|
316
|
+
logger.info(f"\n✨ Your code is 100% dead code free! Add this badge to your README:")
|
|
317
|
+
logger.info("```markdown")
|
|
318
|
+
logger.info("")
|
|
319
|
+
logger.info("```")
|
|
320
|
+
else:
|
|
321
|
+
logger.info(f"Found {dead_code_count} dead code items. Add this badge to your README:")
|
|
322
|
+
logger.info("```markdown")
|
|
323
|
+
logger.info(f"")
|
|
324
|
+
logger.info("```")
|
|
325
|
+
|
|
326
|
+
if unused_functions or unused_imports:
|
|
327
|
+
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
328
|
+
logger.info(f" • Use --interactive to select specific items to remove")
|
|
329
|
+
logger.info(f" • Use --dry-run to preview changes before applying them")
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skylos
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Requires-Dist: inquirer>=3.0.0
|
|
5
|
+
Summary: A static analysis tool for Python codebases
|
|
6
|
+
Author-email: oha <aaronoh2015@gmail.com>
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
9
|
+
|
|
10
|
+
# Skylos 🔍
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
<div align="center">
|
|
18
|
+
<img src="assets/SKYLOS.png" alt="Skylos Logo" width="200">
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
> A static analysis tool for Python codebases written in Rust that detects unreachable functions and unused imports, aka dead code.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- **Unreachable Function Detection**: Find functions and methods that are never called aka dead code
|
|
26
|
+
- **Unused Import Detection**: Imports that are never used
|
|
27
|
+
- **High Performance**: Built with Rust
|
|
28
|
+
- **Nice Output**: Colorized CLI output
|
|
29
|
+
- **Interactive Mode**: Select and remove specific items interactively
|
|
30
|
+
- **Dry Run Support**: Preview changes before applying them
|
|
31
|
+
- **Auto-removal**: Auto clean up
|
|
32
|
+
- **Cross-module Analysis**: Tracks imports and calls across your entire project
|
|
33
|
+
- **Nice badges!!**: Pin your badge if you get 100% clean code!
|
|
34
|
+
|
|
35
|
+
## Benchmark (You can find this benchmark test in `test/sample_project`)
|
|
36
|
+
|
|
37
|
+
| Tool | Time (s) | Functions | Imports | Total |
|
|
38
|
+
|------|----------|-----------|---------|-------|
|
|
39
|
+
| Skylos | 0.039 | 48 | 8 | 56 |
|
|
40
|
+
| Vulture (100%) | 0.040 | 0 | 3 | 3 |
|
|
41
|
+
| Vulture (60%) | 0.041 | 28 | 3 | 31 |
|
|
42
|
+
| Vulture (0%) | 0.041 | 28 | 3 | 31 |
|
|
43
|
+
| Flake8 | 0.274 | 0 | 8 | 8 |
|
|
44
|
+
| Pylint | 0.285 | 0 | 6 | 6 |
|
|
45
|
+
| Dead | 0.035 | 0 | 0 | 0 |
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
### Basic Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install skylos
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### With Interactive Features
|
|
56
|
+
|
|
57
|
+
For the interactive selection mode, install with:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install skylos[interactive]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### From Source
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Clone the repository
|
|
67
|
+
git clone https://github.com/duriantaco/skylos.git
|
|
68
|
+
cd skylos
|
|
69
|
+
|
|
70
|
+
# Install maturin (if not already installed)
|
|
71
|
+
pip install maturin
|
|
72
|
+
|
|
73
|
+
# Build and install
|
|
74
|
+
maturin develop
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Quick Start
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Analyze a project
|
|
81
|
+
skylos /path/to/your/project
|
|
82
|
+
|
|
83
|
+
# Interactive mode - select items to remove
|
|
84
|
+
skylos /path/to/your/project --interactive
|
|
85
|
+
|
|
86
|
+
# Dry run - see what would be removed
|
|
87
|
+
skylos /path/to/your/project --interactive --dry-run
|
|
88
|
+
|
|
89
|
+
# Output to JSON
|
|
90
|
+
skylos /path/to/your/project --json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## CLI Options
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Usage: skylos [OPTIONS] PATH
|
|
97
|
+
|
|
98
|
+
Arguments:
|
|
99
|
+
PATH Path to the Python project to analyze
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
-h, --help Show this help message and exit
|
|
103
|
+
-j, --json Output raw JSON instead of formatted text
|
|
104
|
+
-o, --output FILE Write output to file instead of stdout
|
|
105
|
+
-v, --verbose Enable verbose output
|
|
106
|
+
-i, --interactive Interactively select items to remove
|
|
107
|
+
--dry-run Show what would be removed without modifying files
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Example Output
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
🔍 Python Static Analysis Results
|
|
114
|
+
===================================
|
|
115
|
+
|
|
116
|
+
Summary:
|
|
117
|
+
• Unreachable functions: 48
|
|
118
|
+
• Unused imports: 8
|
|
119
|
+
|
|
120
|
+
📦 Unreachable Functions
|
|
121
|
+
========================
|
|
122
|
+
1. module_13.test_function
|
|
123
|
+
└─ /Users/oha/project/module_13.py:5
|
|
124
|
+
2. module_13.unused_function
|
|
125
|
+
└─ /Users/oha/project/module_13.py:13
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
📥 Unused Imports
|
|
129
|
+
=================
|
|
130
|
+
1. os
|
|
131
|
+
└─ /Users/oha/project/module_13.py:1
|
|
132
|
+
2. json
|
|
133
|
+
└─ /Users/oha/project/module_13.py:3
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
Next steps:
|
|
137
|
+
• Use --interactive to select specific items to remove
|
|
138
|
+
• Use --dry-run to preview changes before applying them
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Interactive Mode
|
|
142
|
+
|
|
143
|
+
The interactive mode lets you select specific functions and imports to remove:
|
|
144
|
+
|
|
145
|
+

|
|
146
|
+
|
|
147
|
+
1. **Select items**: Use arrow keys and space to select/deselect
|
|
148
|
+
2. **Confirm changes**: Review selected items before applying
|
|
149
|
+
3. **Auto-cleanup**: Files are automatically updated
|
|
150
|
+
|
|
151
|
+
## Architecture
|
|
152
|
+
|
|
153
|
+
Skylos uses a hybrid architecture combining Rust and Python:
|
|
154
|
+
|
|
155
|
+
- **Rust Core**: Fast tree-sitter based parsing and analysis
|
|
156
|
+
- **Python CLI**: User-friendly interface and file manipulation
|
|
157
|
+
- **maturin**: Seamless Python-Rust integration
|
|
158
|
+
|
|
159
|
+
### Core Components
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
skylos/
|
|
163
|
+
├── src/lib.rs # Main Rust analysis engine
|
|
164
|
+
├── src/queries.rs # Tree-sitter query definitions
|
|
165
|
+
├── src/types.rs # Data structures for results
|
|
166
|
+
├── src/utils.rs # Helper functions
|
|
167
|
+
├── skylos/
|
|
168
|
+
│ └── cli.py # Python CLI interface
|
|
169
|
+
└── pyproject.toml # Project configuration
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
### Prerequisites
|
|
175
|
+
|
|
176
|
+
- Python ≥3.8
|
|
177
|
+
- Rust and Cargo
|
|
178
|
+
- maturin
|
|
179
|
+
|
|
180
|
+
### Setup
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Clone the repository
|
|
184
|
+
git clone https://github.com/duriantaco/skylos.git
|
|
185
|
+
cd skylos
|
|
186
|
+
|
|
187
|
+
# Create a virtual environment
|
|
188
|
+
python -m venv venv
|
|
189
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
190
|
+
|
|
191
|
+
# Install development dependencies
|
|
192
|
+
pip install maturin inquirer pytest
|
|
193
|
+
|
|
194
|
+
# Build in development mode
|
|
195
|
+
maturin develop
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Running Tests
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Run Rust tests
|
|
202
|
+
cd src
|
|
203
|
+
cargo test
|
|
204
|
+
|
|
205
|
+
# Run Python tests
|
|
206
|
+
python -m pytest tests/
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Adding New Features
|
|
210
|
+
|
|
211
|
+
1. **Rust Analysis**: Add new queries in `src/queries.rs`
|
|
212
|
+
2. **Python CLI**: Extend functionality in `skylos/cli.py`
|
|
213
|
+
3. **Documentation**: Update this README and add docstrings
|
|
214
|
+
|
|
215
|
+
## Configuration
|
|
216
|
+
|
|
217
|
+
Skylos supports configuration via `.skylos.toml` (coming soon):
|
|
218
|
+
|
|
219
|
+
```toml
|
|
220
|
+
[analysis]
|
|
221
|
+
# Exclude patterns
|
|
222
|
+
exclude = [
|
|
223
|
+
"*/migrations/*",
|
|
224
|
+
"*/tests/*",
|
|
225
|
+
"__pycache__"
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
# Custom patterns for dead code detection
|
|
229
|
+
patterns = [
|
|
230
|
+
"test_*", # Test functions
|
|
231
|
+
"_*_internal" # Internal functions
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
[output]
|
|
235
|
+
# Default output format
|
|
236
|
+
format = "colored" # Options: colored, plain, json
|
|
237
|
+
|
|
238
|
+
# Color scheme
|
|
239
|
+
colors = "default" # Options: default, dark, light
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Limitations
|
|
243
|
+
|
|
244
|
+
- Occassionally there will be false positives and false negatives especially for extreme edge cases
|
|
245
|
+
|
|
246
|
+
- The library is currently available **ONLY** for python
|
|
247
|
+
|
|
248
|
+
## Troubleshooting
|
|
249
|
+
|
|
250
|
+
### Common Issues
|
|
251
|
+
|
|
252
|
+
1. **Tree-sitter Errors**
|
|
253
|
+
```
|
|
254
|
+
Error: tree-sitter parse failed
|
|
255
|
+
```
|
|
256
|
+
Ensure your Python files have valid syntax.
|
|
257
|
+
|
|
258
|
+
2. **Permission Errors**
|
|
259
|
+
```
|
|
260
|
+
Error: Permission denied when removing function
|
|
261
|
+
```
|
|
262
|
+
Check file permissions before running in interactive mode.
|
|
263
|
+
|
|
264
|
+
3. **Missing Dependencies**
|
|
265
|
+
```
|
|
266
|
+
Interactive mode requires 'inquirer' package
|
|
267
|
+
```
|
|
268
|
+
Install with: `pip install skylos[interactive]`
|
|
269
|
+
|
|
270
|
+
## Contributing
|
|
271
|
+
|
|
272
|
+
We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting pull requests.
|
|
273
|
+
|
|
274
|
+
### Quick Contribution Guide
|
|
275
|
+
|
|
276
|
+
1. Fork the repository
|
|
277
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
278
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
279
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
280
|
+
5. Open a Pull Request
|
|
281
|
+
|
|
282
|
+
## Roadmap
|
|
283
|
+
|
|
284
|
+
- [ ] Configuration file support (White lists etc)
|
|
285
|
+
- [ ] Custom analysis rules
|
|
286
|
+
- [ ] Git hooks integration
|
|
287
|
+
- [ ] CI/CD integration examples
|
|
288
|
+
- [ ] Web interface
|
|
289
|
+
- [ ] Support for other languages
|
|
290
|
+
- [ ] Further optimization
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
295
|
+
|
|
296
|
+
## Acknowledgments
|
|
297
|
+
|
|
298
|
+
- [tree-sitter](https://tree-sitter.github.io/) for the powerful parsing framework
|
|
299
|
+
- [maturin](https://github.com/PyO3/maturin) for seamless Rust-Python integration
|
|
300
|
+
- [inquirer](https://github.com/magmax/python-inquirer) for the interactive CLI
|
|
301
|
+
|
|
302
|
+
## Contact
|
|
303
|
+
|
|
304
|
+
- **Author**: oha
|
|
305
|
+
- **Email**: aaronoh2015@gmail.com
|
|
306
|
+
- **GitHub**: [@duriantaco](https://github.com/duriantaco)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
skylos-0.0.5.dist-info/METADATA,sha256=xPYSIsBtZPtS0uy2PJYgwmv-fhHVXni4K9YWRCXTVl8,7643
|
|
2
|
+
skylos-0.0.5.dist-info/WHEEL,sha256=4VRjfqJoqedpTbRAmkkL5YkIwJdBmw_bJCNF4WxUw2Q,104
|
|
3
|
+
skylos-0.0.5.dist-info/entry_points.txt,sha256=0_aXKDSaSH1REotD0g5GU419ykfL-S5H0XFtmDwRHyU,41
|
|
4
|
+
skylos/__init__.py,sha256=xHdVS6MpULFr3-JZSxs4oKh9P2_FdV3lCuJU48Aanr8,846
|
|
5
|
+
skylos/_core.cpython-310-darwin.so,sha256=3Itsh1TLzYkHNKZbHVvr9RKYPeVVOueSiACTP0Hiy6Q,2675760
|
|
6
|
+
skylos/cli.py,sha256=Pq71Gtc3d8E7JVYnEORK_LoLclG96A6b76SIGZh-C6s,13821
|
|
7
|
+
skylos-0.0.5.dist-info/RECORD,,
|