skylos 1.0.7__py3-none-any.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 +8 -0
- skylos/analyzer.py +180 -0
- skylos/cli.py +332 -0
- skylos/visitor.py +181 -0
- skylos-1.0.7.dist-info/METADATA +8 -0
- skylos-1.0.7.dist-info/RECORD +21 -0
- skylos-1.0.7.dist-info/WHEEL +5 -0
- skylos-1.0.7.dist-info/entry_points.txt +2 -0
- skylos-1.0.7.dist-info/top_level.txt +2 -0
- test/__init__.py +0 -0
- test/compare_tools.py +604 -0
- test/diagnostics.py +364 -0
- test/sample_repo/__init__.py +0 -0
- test/sample_repo/app.py +13 -0
- test/sample_repo/sample_repo/__init__.py +0 -0
- test/sample_repo/sample_repo/commands.py +81 -0
- test/sample_repo/sample_repo/models.py +122 -0
- test/sample_repo/sample_repo/routes.py +89 -0
- test/sample_repo/sample_repo/utils.py +36 -0
- test/test_skylos.py +456 -0
- test/test_visitor.py +220 -0
skylos/__init__.py
ADDED
skylos/analyzer.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import ast,sys,json,logging,re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from skylos.visitor import Visitor
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
|
|
8
|
+
logger=logging.getLogger('Skylos')
|
|
9
|
+
|
|
10
|
+
AUTO_CALLED={"__init__","__enter__","__exit__"}
|
|
11
|
+
TEST_BASE_CLASSES = {"TestCase", "AsyncioTestCase", "unittest.TestCase", "unittest.AsyncioTestCase"}
|
|
12
|
+
TEST_METHOD_PATTERN = re.compile(r"^test_\w+$")
|
|
13
|
+
MAGIC_METHODS={f"__{n}__"for n in["init","new","call","getattr","getattribute","enter","exit","str","repr","hash","eq","ne","lt","gt","le","ge","iter","next","contains","len","getitem","setitem","delitem","iadd","isub","imul","itruediv","ifloordiv","imod","ipow","ilshift","irshift","iand","ixor","ior","round","format","dir","abs","complex","int","float","bool","bytes","reduce","await","aiter","anext","add","sub","mul","truediv","floordiv","mod","divmod","pow","lshift","rshift","and","or","xor","radd","rsub","rmul","rtruediv","rfloordiv","rmod","rdivmod","rpow","rlshift","rrshift","rand","ror","rxor"]}
|
|
14
|
+
|
|
15
|
+
class Skylos:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.defs={}
|
|
18
|
+
self.refs=[]
|
|
19
|
+
self.dynamic=set()
|
|
20
|
+
self.exports=defaultdict(set)
|
|
21
|
+
|
|
22
|
+
def _module(self,root,f):
|
|
23
|
+
p=list(f.relative_to(root).parts)
|
|
24
|
+
if p[-1].endswith(".py"):p[-1]=p[-1][:-3]
|
|
25
|
+
if p[-1]=="__init__":p.pop()
|
|
26
|
+
return".".join(p)
|
|
27
|
+
|
|
28
|
+
def _mark_exports(self):
|
|
29
|
+
|
|
30
|
+
for name, d in self.defs.items():
|
|
31
|
+
if d.in_init and not d.simple_name.startswith('_'):
|
|
32
|
+
d.is_exported = True
|
|
33
|
+
|
|
34
|
+
for mod, export_names in self.exports.items():
|
|
35
|
+
for name in export_names:
|
|
36
|
+
for def_name, def_obj in self.defs.items():
|
|
37
|
+
if (def_name.startswith(f"{mod}.") and
|
|
38
|
+
def_obj.simple_name == name and
|
|
39
|
+
def_obj.type != "import"):
|
|
40
|
+
def_obj.is_exported = True
|
|
41
|
+
|
|
42
|
+
def _mark_refs(self):
|
|
43
|
+
import_to_original = {}
|
|
44
|
+
for name, def_obj in self.defs.items():
|
|
45
|
+
if def_obj.type == "import":
|
|
46
|
+
import_name = name.split('.')[-1]
|
|
47
|
+
|
|
48
|
+
for def_name, orig_def in self.defs.items():
|
|
49
|
+
if (orig_def.type != "import" and
|
|
50
|
+
orig_def.simple_name == import_name and
|
|
51
|
+
def_name != name):
|
|
52
|
+
import_to_original[name] = def_name
|
|
53
|
+
break
|
|
54
|
+
|
|
55
|
+
simple_name_lookup = defaultdict(list)
|
|
56
|
+
for d in self.defs.values():
|
|
57
|
+
simple_name_lookup[d.simple_name].append(d)
|
|
58
|
+
|
|
59
|
+
for ref, file in self.refs:
|
|
60
|
+
if ref in self.defs:
|
|
61
|
+
self.defs[ref].references += 1
|
|
62
|
+
|
|
63
|
+
if ref in import_to_original:
|
|
64
|
+
original = import_to_original[ref]
|
|
65
|
+
self.defs[original].references += 1
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
simple = ref.split('.')[-1]
|
|
69
|
+
matches = simple_name_lookup.get(simple, [])
|
|
70
|
+
for d in matches:
|
|
71
|
+
d.references += 1
|
|
72
|
+
|
|
73
|
+
def _get_base_classes(self, class_name):
|
|
74
|
+
"""Get base classes for a given class name"""
|
|
75
|
+
if class_name not in self.defs:
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
class_def = self.defs[class_name]
|
|
79
|
+
|
|
80
|
+
if hasattr(class_def, 'base_classes'):
|
|
81
|
+
return class_def.base_classes
|
|
82
|
+
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
def _apply_heuristics(self):
|
|
86
|
+
|
|
87
|
+
class_methods=defaultdict(list)
|
|
88
|
+
for d in self.defs.values():
|
|
89
|
+
if d.type in("method","function") and"." in d.name:
|
|
90
|
+
cls=d.name.rsplit(".",1)[0]
|
|
91
|
+
if cls in self.defs and self.defs[cls].type=="class":
|
|
92
|
+
class_methods[cls].append(d)
|
|
93
|
+
|
|
94
|
+
for cls,methods in class_methods.items():
|
|
95
|
+
if self.defs[cls].references>0:
|
|
96
|
+
for m in methods:
|
|
97
|
+
if m.simple_name in AUTO_CALLED:m.references+=1
|
|
98
|
+
|
|
99
|
+
for d in self.defs.values():
|
|
100
|
+
if d.simple_name in MAGIC_METHODS or d.simple_name.startswith("__")and d.simple_name.endswith("__"):d.confidence=0
|
|
101
|
+
if not d.simple_name.startswith("_")and d.type in("function","method","class"):d.confidence=min(d.confidence,90)
|
|
102
|
+
if d.in_init and d.type in("function","class"):d.confidence=min(d.confidence,85)
|
|
103
|
+
if d.name.split(".")[0] in self.dynamic:d.confidence=min(d.confidence,50)
|
|
104
|
+
|
|
105
|
+
for d in self.defs.values():
|
|
106
|
+
if d.type == "method" and TEST_METHOD_PATTERN.match(d.simple_name):
|
|
107
|
+
# check if its in a class that inherits from a test base class
|
|
108
|
+
class_name = d.name.rsplit(".", 1)[0]
|
|
109
|
+
class_simple_name = class_name.split(".")[-1]
|
|
110
|
+
# class name suggests it's a test class, ignore test methods
|
|
111
|
+
if "Test" in class_simple_name or class_simple_name.endswith("TestCase"):
|
|
112
|
+
d.confidence = 0
|
|
113
|
+
|
|
114
|
+
def analyze(self, path, thr=60):
|
|
115
|
+
p = Path(path).resolve()
|
|
116
|
+
files = [p] if p.is_file() else list(p.glob("**/*.py"))
|
|
117
|
+
root = p.parent if p.is_file() else p
|
|
118
|
+
|
|
119
|
+
modmap = {}
|
|
120
|
+
for f in files:
|
|
121
|
+
modmap[f] = self._module(root, f)
|
|
122
|
+
|
|
123
|
+
for file in files:
|
|
124
|
+
mod = modmap[file]
|
|
125
|
+
defs, refs, dyn, exports = proc_file(file, mod)
|
|
126
|
+
|
|
127
|
+
for d in defs:
|
|
128
|
+
self.defs[d.name] = d
|
|
129
|
+
self.refs.extend(refs)
|
|
130
|
+
self.dynamic.update(dyn)
|
|
131
|
+
self.exports[mod].update(exports)
|
|
132
|
+
|
|
133
|
+
self._mark_refs()
|
|
134
|
+
self._apply_heuristics()
|
|
135
|
+
self._mark_exports()
|
|
136
|
+
|
|
137
|
+
# for name, d in self.defs.items():
|
|
138
|
+
# print(f" {d.type} '{name}': {d.references} refs, exported: {d.is_exported}, confidence: {d.confidence}")
|
|
139
|
+
|
|
140
|
+
thr = max(0, thr)
|
|
141
|
+
|
|
142
|
+
unused = []
|
|
143
|
+
for d in self.defs.values():
|
|
144
|
+
if d.references == 0 and not d.is_exported and d.confidence >= thr:
|
|
145
|
+
unused.append(d.to_dict())
|
|
146
|
+
|
|
147
|
+
result = {"unused_functions": [], "unused_imports": [], "unused_classes": []}
|
|
148
|
+
for u in unused:
|
|
149
|
+
if u["type"] in ("function", "method"):
|
|
150
|
+
result["unused_functions"].append(u)
|
|
151
|
+
elif u["type"] == "import":
|
|
152
|
+
result["unused_imports"].append(u)
|
|
153
|
+
elif u["type"] == "class":
|
|
154
|
+
result["unused_classes"].append(u)
|
|
155
|
+
|
|
156
|
+
return json.dumps(result, indent=2)
|
|
157
|
+
|
|
158
|
+
def proc_file(file_or_args, mod=None):
|
|
159
|
+
if mod is None and isinstance(file_or_args, tuple):
|
|
160
|
+
file, mod = file_or_args
|
|
161
|
+
else:
|
|
162
|
+
file = file_or_args
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
tree = ast.parse(Path(file).read_text(encoding="utf-8"))
|
|
166
|
+
v = Visitor(mod, file)
|
|
167
|
+
v.visit(tree)
|
|
168
|
+
return v.defs, v.refs, v.dyn, v.exports
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"{file}: {e}")
|
|
171
|
+
return [], [], set(), set()
|
|
172
|
+
|
|
173
|
+
def analyze(path,conf=60):return Skylos().analyze(path,conf)
|
|
174
|
+
|
|
175
|
+
if __name__=="__main__":
|
|
176
|
+
if len(sys.argv)>1:
|
|
177
|
+
p=sys.argv[1];c=int(sys.argv[2])if len(sys.argv)>2 else 60
|
|
178
|
+
print(analyze(p,c))
|
|
179
|
+
else:
|
|
180
|
+
print("Usage: python Skylos.py <path> [confidence_threshold]")
|
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()
|