skylos 1.0.8__tar.gz → 1.0.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- {skylos-1.0.8 → skylos-1.0.10}/PKG-INFO +1 -1
- {skylos-1.0.8 → skylos-1.0.10}/pyproject.toml +1 -1
- {skylos-1.0.8 → skylos-1.0.10}/setup.py +1 -1
- {skylos-1.0.8 → skylos-1.0.10}/skylos/__init__.py +1 -1
- {skylos-1.0.8 → skylos-1.0.10}/skylos/analyzer.py +76 -16
- {skylos-1.0.8 → skylos-1.0.10}/skylos/cli.py +100 -96
- {skylos-1.0.8 → skylos-1.0.10}/skylos/visitor.py +83 -11
- {skylos-1.0.8 → skylos-1.0.10}/skylos.egg-info/PKG-INFO +1 -1
- {skylos-1.0.8 → skylos-1.0.10}/README.md +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/setup.cfg +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/skylos.egg-info/SOURCES.txt +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/skylos.egg-info/dependency_links.txt +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/skylos.egg-info/entry_points.txt +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/skylos.egg-info/requires.txt +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/skylos.egg-info/top_level.txt +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/__init__.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/compare_tools.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/diagnostics.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/sample_repo/__init__.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/sample_repo/app.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/sample_repo/sample_repo/__init__.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/sample_repo/sample_repo/commands.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/sample_repo/sample_repo/models.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/sample_repo/sample_repo/routes.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/sample_repo/sample_repo/utils.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/test_skylos.py +0 -0
- {skylos-1.0.8 → skylos-1.0.10}/test/test_visitor.py +0 -0
|
@@ -26,7 +26,6 @@ class Skylos:
|
|
|
26
26
|
return".".join(p)
|
|
27
27
|
|
|
28
28
|
def _mark_exports(self):
|
|
29
|
-
|
|
30
29
|
for name, d in self.defs.items():
|
|
31
30
|
if d.in_init and not d.simple_name.startswith('_'):
|
|
32
31
|
d.is_exported = True
|
|
@@ -71,7 +70,6 @@ class Skylos:
|
|
|
71
70
|
d.references += 1
|
|
72
71
|
|
|
73
72
|
def _get_base_classes(self, class_name):
|
|
74
|
-
"""Get base classes for a given class name"""
|
|
75
73
|
if class_name not in self.defs:
|
|
76
74
|
return []
|
|
77
75
|
|
|
@@ -83,7 +81,6 @@ class Skylos:
|
|
|
83
81
|
return []
|
|
84
82
|
|
|
85
83
|
def _apply_heuristics(self):
|
|
86
|
-
|
|
87
84
|
class_methods=defaultdict(list)
|
|
88
85
|
for d in self.defs.values():
|
|
89
86
|
if d.type in("method","function") and"." in d.name:
|
|
@@ -97,17 +94,21 @@ class Skylos:
|
|
|
97
94
|
if m.simple_name in AUTO_CALLED:m.references+=1
|
|
98
95
|
|
|
99
96
|
for d in self.defs.values():
|
|
100
|
-
if d.simple_name in MAGIC_METHODS or d.simple_name.startswith("__")and d.simple_name.endswith("__"):
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if d.
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
if d.simple_name in MAGIC_METHODS or (d.simple_name.startswith("__") and d.simple_name.endswith("__")):
|
|
98
|
+
d.confidence = 0
|
|
99
|
+
|
|
100
|
+
if not d.simple_name.startswith("_") and d.type in ("function", "method", "class"):
|
|
101
|
+
d.confidence = min(d.confidence, 90)
|
|
102
|
+
|
|
103
|
+
if d.in_init and d.type in ("function", "class"):
|
|
104
|
+
d.confidence = min(d.confidence, 85)
|
|
105
|
+
|
|
106
|
+
if d.name.split(".")[0] in self.dynamic:
|
|
107
|
+
d.confidence = min(d.confidence, 60)
|
|
108
|
+
|
|
106
109
|
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
110
|
class_name = d.name.rsplit(".", 1)[0]
|
|
109
111
|
class_simple_name = class_name.split(".")[-1]
|
|
110
|
-
# class name suggests it's a test class, ignore test methods
|
|
111
112
|
if "Test" in class_simple_name or class_simple_name.endswith("TestCase"):
|
|
112
113
|
d.confidence = 0
|
|
113
114
|
|
|
@@ -134,9 +135,6 @@ class Skylos:
|
|
|
134
135
|
self._apply_heuristics()
|
|
135
136
|
self._mark_exports()
|
|
136
137
|
|
|
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
138
|
thr = max(0, thr)
|
|
141
139
|
|
|
142
140
|
unused = []
|
|
@@ -144,7 +142,13 @@ class Skylos:
|
|
|
144
142
|
if d.references == 0 and not d.is_exported and d.confidence >= thr:
|
|
145
143
|
unused.append(d.to_dict())
|
|
146
144
|
|
|
147
|
-
result = {
|
|
145
|
+
result = {
|
|
146
|
+
"unused_functions": [],
|
|
147
|
+
"unused_imports": [],
|
|
148
|
+
"unused_classes": [],
|
|
149
|
+
"unused_variables": []
|
|
150
|
+
}
|
|
151
|
+
|
|
148
152
|
for u in unused:
|
|
149
153
|
if u["type"] in ("function", "method"):
|
|
150
154
|
result["unused_functions"].append(u)
|
|
@@ -152,6 +156,8 @@ class Skylos:
|
|
|
152
156
|
result["unused_imports"].append(u)
|
|
153
157
|
elif u["type"] == "class":
|
|
154
158
|
result["unused_classes"].append(u)
|
|
159
|
+
elif u["type"] == "variable":
|
|
160
|
+
result["unused_variables"].append(u)
|
|
155
161
|
|
|
156
162
|
return json.dumps(result, indent=2)
|
|
157
163
|
|
|
@@ -175,6 +181,60 @@ def analyze(path,conf=60):return Skylos().analyze(path,conf)
|
|
|
175
181
|
if __name__=="__main__":
|
|
176
182
|
if len(sys.argv)>1:
|
|
177
183
|
p=sys.argv[1];c=int(sys.argv[2])if len(sys.argv)>2 else 60
|
|
178
|
-
|
|
184
|
+
result = analyze(p,c)
|
|
185
|
+
|
|
186
|
+
data = json.loads(result)
|
|
187
|
+
print("\n🔍 Python Static Analysis Results")
|
|
188
|
+
print("===================================\n")
|
|
189
|
+
|
|
190
|
+
total_items = sum(len(items) for items in data.values())
|
|
191
|
+
|
|
192
|
+
print("Summary:")
|
|
193
|
+
if data["unused_functions"]:
|
|
194
|
+
print(f" • Unreachable functions: {len(data['unused_functions'])}")
|
|
195
|
+
if data["unused_imports"]:
|
|
196
|
+
print(f" • Unused imports: {len(data['unused_imports'])}")
|
|
197
|
+
if data["unused_classes"]:
|
|
198
|
+
print(f" • Unused classes: {len(data['unused_classes'])}")
|
|
199
|
+
if data["unused_variables"]:
|
|
200
|
+
print(f" • Unused variables: {len(data['unused_variables'])}")
|
|
201
|
+
|
|
202
|
+
if data["unused_functions"]:
|
|
203
|
+
print("\n📦 Unreachable Functions")
|
|
204
|
+
print("=======================")
|
|
205
|
+
for i, func in enumerate(data["unused_functions"], 1):
|
|
206
|
+
print(f" {i}. {func['name']}")
|
|
207
|
+
print(f" └─ {func['file']}:{func['line']}")
|
|
208
|
+
|
|
209
|
+
if data["unused_imports"]:
|
|
210
|
+
print("\n📥 Unused Imports")
|
|
211
|
+
print("================")
|
|
212
|
+
for i, imp in enumerate(data["unused_imports"], 1):
|
|
213
|
+
print(f" {i}. {imp['simple_name']}")
|
|
214
|
+
print(f" └─ {imp['file']}:{imp['line']}")
|
|
215
|
+
|
|
216
|
+
if data["unused_classes"]:
|
|
217
|
+
print("\n📋 Unused Classes")
|
|
218
|
+
print("=================")
|
|
219
|
+
for i, cls in enumerate(data["unused_classes"], 1):
|
|
220
|
+
print(f" {i}. {cls['name']}")
|
|
221
|
+
print(f" └─ {cls['file']}:{cls['line']}")
|
|
222
|
+
|
|
223
|
+
if data["unused_variables"]:
|
|
224
|
+
print("\n📊 Unused Variables")
|
|
225
|
+
print("==================")
|
|
226
|
+
for i, var in enumerate(data["unused_variables"], 1):
|
|
227
|
+
print(f" {i}. {var['name']}")
|
|
228
|
+
print(f" └─ {var['file']}:{var['line']}")
|
|
229
|
+
|
|
230
|
+
print("\n" + "─" * 50)
|
|
231
|
+
print(f"Found {total_items} dead code items. Add this badge to your README:")
|
|
232
|
+
print(f"```markdown")
|
|
233
|
+
print(f"")
|
|
234
|
+
print(f"```")
|
|
235
|
+
|
|
236
|
+
print("\nNext steps:")
|
|
237
|
+
print(" • Use --interactive to select specific items to remove")
|
|
238
|
+
print(" • Use --dry-run to preview changes before applying them")
|
|
179
239
|
else:
|
|
180
240
|
print("Usage: python Skylos.py <path> [confidence_threshold]")
|
|
@@ -24,9 +24,10 @@ class Colors:
|
|
|
24
24
|
RESET = '\033[0m'
|
|
25
25
|
GRAY = '\033[90m'
|
|
26
26
|
|
|
27
|
-
class
|
|
27
|
+
class CleanFormatter(logging.Formatter):
|
|
28
|
+
"""Custom formatter that removes timestamps and log levels for clean output"""
|
|
28
29
|
def format(self, record):
|
|
29
|
-
return
|
|
30
|
+
return record.getMessage()
|
|
30
31
|
|
|
31
32
|
def setup_logger(output_file=None):
|
|
32
33
|
logger = logging.getLogger('skylos')
|
|
@@ -34,17 +35,20 @@ def setup_logger(output_file=None):
|
|
|
34
35
|
|
|
35
36
|
logger.handlers.clear()
|
|
36
37
|
|
|
37
|
-
formatter =
|
|
38
|
+
formatter = CleanFormatter()
|
|
38
39
|
|
|
39
40
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
40
41
|
console_handler.setFormatter(formatter)
|
|
41
42
|
logger.addHandler(console_handler)
|
|
42
43
|
|
|
43
44
|
if output_file:
|
|
45
|
+
file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
|
44
46
|
file_handler = logging.FileHandler(output_file)
|
|
45
|
-
file_handler.setFormatter(
|
|
47
|
+
file_handler.setFormatter(file_formatter)
|
|
46
48
|
logger.addHandler(file_handler)
|
|
47
49
|
|
|
50
|
+
logger.propagate = False
|
|
51
|
+
|
|
48
52
|
return logger
|
|
49
53
|
|
|
50
54
|
def remove_unused_import(file_path: str, import_name: str, line_number: int) -> bool:
|
|
@@ -176,6 +180,21 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
176
180
|
|
|
177
181
|
return selected_functions, selected_imports
|
|
178
182
|
|
|
183
|
+
def print_badge(dead_code_count: int, logger):
|
|
184
|
+
"""Print appropriate badge based on dead code count"""
|
|
185
|
+
logger.info(f"\n{Colors.GRAY}{'─' * 50}{Colors.RESET}")
|
|
186
|
+
|
|
187
|
+
if dead_code_count == 0:
|
|
188
|
+
logger.info(f"✨ Your code is 100% dead code free! Add this badge to your README:")
|
|
189
|
+
logger.info("```markdown")
|
|
190
|
+
logger.info("")
|
|
191
|
+
logger.info("```")
|
|
192
|
+
else:
|
|
193
|
+
logger.info(f"Found {dead_code_count} dead code items. Add this badge to your README:")
|
|
194
|
+
logger.info("```markdown")
|
|
195
|
+
logger.info(f"")
|
|
196
|
+
logger.info("```")
|
|
197
|
+
|
|
179
198
|
def main() -> None:
|
|
180
199
|
parser = argparse.ArgumentParser(
|
|
181
200
|
description="Detect unreachable functions and unused imports in a Python project"
|
|
@@ -224,109 +243,94 @@ def main() -> None:
|
|
|
224
243
|
|
|
225
244
|
if args.json:
|
|
226
245
|
logger.info(result_json)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
unused_functions = result.get("unused_functions", [])
|
|
249
|
+
unused_imports = result.get("unused_imports", [])
|
|
250
|
+
|
|
251
|
+
logger.info(f"{Colors.CYAN}{Colors.BOLD}🔍 Python Static Analysis Results{Colors.RESET}")
|
|
252
|
+
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
253
|
+
|
|
254
|
+
logger.info(f"\n{Colors.BOLD}Summary:{Colors.RESET}")
|
|
255
|
+
logger.info(f" • Unreachable functions: {Colors.YELLOW}{len(unused_functions)}{Colors.RESET}")
|
|
256
|
+
logger.info(f" • Unused imports: {Colors.YELLOW}{len(unused_imports)}{Colors.RESET}")
|
|
257
|
+
|
|
258
|
+
if args.interactive and (unused_functions or unused_imports):
|
|
259
|
+
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
260
|
+
selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
|
|
237
261
|
|
|
238
|
-
if
|
|
239
|
-
logger.info(f"\n{Colors.BOLD}
|
|
240
|
-
|
|
262
|
+
if selected_functions or selected_imports:
|
|
263
|
+
logger.info(f"\n{Colors.BOLD}Selected items to remove:{Colors.RESET}")
|
|
264
|
+
|
|
265
|
+
if selected_functions:
|
|
266
|
+
logger.info(f" Functions: {len(selected_functions)}")
|
|
267
|
+
for func in selected_functions:
|
|
268
|
+
logger.info(f" - {func['name']} ({func['file']}:{func['line']})")
|
|
241
269
|
|
|
242
|
-
if
|
|
243
|
-
logger.info(f"
|
|
270
|
+
if selected_imports:
|
|
271
|
+
logger.info(f" Imports: {len(selected_imports)}")
|
|
272
|
+
for imp in selected_imports:
|
|
273
|
+
logger.info(f" - {imp['name']} ({imp['file']}:{imp['line']})")
|
|
274
|
+
|
|
275
|
+
if not args.dry_run:
|
|
276
|
+
questions = [
|
|
277
|
+
inquirer.Confirm('confirm',
|
|
278
|
+
message="Are you sure you want to remove these items?",
|
|
279
|
+
default=False)
|
|
280
|
+
]
|
|
281
|
+
answers = inquirer.prompt(questions)
|
|
244
282
|
|
|
245
|
-
if
|
|
246
|
-
logger.info(f"
|
|
283
|
+
if answers and answers['confirm']:
|
|
284
|
+
logger.info(f"\n{Colors.YELLOW}Removing selected items...{Colors.RESET}")
|
|
285
|
+
|
|
247
286
|
for func in selected_functions:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
287
|
+
success = remove_unused_function(func['file'], func['name'], func['line'])
|
|
288
|
+
if success:
|
|
289
|
+
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed function: {func['name']}")
|
|
290
|
+
else:
|
|
291
|
+
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {func['name']}")
|
|
292
|
+
|
|
252
293
|
for imp in selected_imports:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
message="Are you sure you want to remove these items?",
|
|
259
|
-
default=False)
|
|
260
|
-
]
|
|
261
|
-
answers = inquirer.prompt(questions)
|
|
294
|
+
success = remove_unused_import(imp['file'], imp['name'], imp['line'])
|
|
295
|
+
if success:
|
|
296
|
+
logger.info(f" {Colors.GREEN}✓{Colors.RESET} Removed import: {imp['name']}")
|
|
297
|
+
else:
|
|
298
|
+
logger.error(f" {Colors.RED}✗{Colors.RESET} Failed to remove: {imp['name']}")
|
|
262
299
|
|
|
263
|
-
|
|
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}")
|
|
300
|
+
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
283
301
|
else:
|
|
284
|
-
logger.info(f"\n{Colors.YELLOW}
|
|
302
|
+
logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
|
|
285
303
|
else:
|
|
286
|
-
logger.info(f"\n{Colors.
|
|
304
|
+
logger.info(f"\n{Colors.YELLOW}Dry run - no files were modified.{Colors.RESET}")
|
|
305
|
+
else:
|
|
306
|
+
logger.info(f"\n{Colors.BLUE}No items selected.{Colors.RESET}")
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
if unused_functions:
|
|
310
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD}📦 Unreachable Functions{Colors.RESET}")
|
|
311
|
+
logger.info(f"{Colors.RED}{'=' * 23}{Colors.RESET}")
|
|
312
|
+
for i, item in enumerate(unused_functions, 1):
|
|
313
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.RED}{item['name']}{Colors.RESET}")
|
|
314
|
+
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
315
|
+
else:
|
|
316
|
+
logger.info(f"\n{Colors.GREEN}✓ All functions are reachable!{Colors.RESET}")
|
|
287
317
|
|
|
318
|
+
if unused_imports:
|
|
319
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}📥 Unused Imports{Colors.RESET}")
|
|
320
|
+
logger.info(f"{Colors.MAGENTA}{'=' * 16}{Colors.RESET}")
|
|
321
|
+
for i, item in enumerate(unused_imports, 1):
|
|
322
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.MAGENTA}{item['name']}{Colors.RESET}")
|
|
323
|
+
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
288
324
|
else:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
logger.info(f"\n{Colors.GREEN}✓ All imports are being used!{Colors.RESET}")
|
|
326
|
+
|
|
327
|
+
dead_code_count = len(unused_functions) + len(unused_imports)
|
|
328
|
+
print_badge(dead_code_count, logger)
|
|
325
329
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
+
if unused_functions or unused_imports:
|
|
331
|
+
logger.info(f"\n{Colors.BOLD}Next steps:{Colors.RESET}")
|
|
332
|
+
logger.info(f" • Use --interactive to select specific items to remove")
|
|
333
|
+
logger.info(f" • Use --dry-run to preview changes before applying them")
|
|
330
334
|
|
|
331
335
|
if __name__ == "__main__":
|
|
332
336
|
main()
|
|
@@ -62,7 +62,23 @@ class Visitor(ast.NodeVisitor):
|
|
|
62
62
|
if n in self.alias:return self.alias[n]
|
|
63
63
|
if n in PYTHON_BUILTINS:return n
|
|
64
64
|
return f"{self.mod}.{n}"if self.mod else n
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
def visit_annotation(self, node):
|
|
67
|
+
if node is not None:
|
|
68
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
69
|
+
self.visit_string_annotation(node.value)
|
|
70
|
+
elif hasattr(node, 's') and isinstance(node.s, str):
|
|
71
|
+
self.visit_string_annotation(node.s)
|
|
72
|
+
else:
|
|
73
|
+
self.visit(node)
|
|
74
|
+
|
|
75
|
+
def visit_string_annotation(self, annotation_str):
|
|
76
|
+
try:
|
|
77
|
+
parsed = ast.parse(annotation_str, mode='eval')
|
|
78
|
+
self.visit(parsed.body)
|
|
79
|
+
except:
|
|
80
|
+
pass
|
|
81
|
+
|
|
66
82
|
def visit_Import(self,node):
|
|
67
83
|
for a in node.names:
|
|
68
84
|
full=a.name
|
|
@@ -81,6 +97,23 @@ class Visitor(ast.NodeVisitor):
|
|
|
81
97
|
self.alias[a.asname or a.name]=full
|
|
82
98
|
self.add_def(full,"import",node.lineno)
|
|
83
99
|
|
|
100
|
+
def visit_arguments(self, args):
|
|
101
|
+
for arg in args.args:
|
|
102
|
+
self.visit_annotation(arg.annotation)
|
|
103
|
+
for arg in args.posonlyargs:
|
|
104
|
+
self.visit_annotation(arg.annotation)
|
|
105
|
+
for arg in args.kwonlyargs:
|
|
106
|
+
self.visit_annotation(arg.annotation)
|
|
107
|
+
if args.vararg:
|
|
108
|
+
self.visit_annotation(args.vararg.annotation)
|
|
109
|
+
if args.kwarg:
|
|
110
|
+
self.visit_annotation(args.kwarg.annotation)
|
|
111
|
+
for default in args.defaults:
|
|
112
|
+
self.visit(default)
|
|
113
|
+
for default in args.kw_defaults:
|
|
114
|
+
if default:
|
|
115
|
+
self.visit(default)
|
|
116
|
+
|
|
84
117
|
def visit_FunctionDef(self,node):
|
|
85
118
|
outer_scope_prefix = '.'.join(self.current_function_scope) + '.' if self.current_function_scope else ''
|
|
86
119
|
|
|
@@ -94,8 +127,13 @@ class Visitor(ast.NodeVisitor):
|
|
|
94
127
|
self.add_def(qualified_name,"method"if self.cls else"function",node.lineno)
|
|
95
128
|
|
|
96
129
|
self.current_function_scope.append(node.name)
|
|
130
|
+
|
|
97
131
|
for d_node in node.decorator_list:
|
|
98
132
|
self.visit(d_node)
|
|
133
|
+
|
|
134
|
+
self.visit_arguments(node.args)
|
|
135
|
+
self.visit_annotation(node.returns)
|
|
136
|
+
|
|
99
137
|
for stmt in node.body:
|
|
100
138
|
self.visit(stmt)
|
|
101
139
|
self.current_function_scope.pop()
|
|
@@ -105,10 +143,40 @@ class Visitor(ast.NodeVisitor):
|
|
|
105
143
|
def visit_ClassDef(self,node):
|
|
106
144
|
cname=f"{self.mod}.{node.name}"
|
|
107
145
|
self.add_def(cname,"class",node.lineno)
|
|
146
|
+
|
|
147
|
+
for base in node.bases:
|
|
148
|
+
self.visit(base)
|
|
149
|
+
for keyword in node.keywords:
|
|
150
|
+
self.visit(keyword.value)
|
|
151
|
+
for decorator in node.decorator_list:
|
|
152
|
+
self.visit(decorator)
|
|
153
|
+
|
|
108
154
|
prev=self.cls;self.cls=node.name
|
|
109
155
|
for b in node.body:self.visit(b)
|
|
110
156
|
self.cls=prev
|
|
111
157
|
|
|
158
|
+
def visit_AnnAssign(self, node):
|
|
159
|
+
self.visit_annotation(node.annotation)
|
|
160
|
+
if node.value:
|
|
161
|
+
self.visit(node.value)
|
|
162
|
+
self.visit(node.target)
|
|
163
|
+
|
|
164
|
+
def visit_AugAssign(self, node):
|
|
165
|
+
self.visit(node.target)
|
|
166
|
+
self.visit(node.value)
|
|
167
|
+
|
|
168
|
+
def visit_Subscript(self, node):
|
|
169
|
+
self.visit(node.value)
|
|
170
|
+
self.visit(node.slice)
|
|
171
|
+
|
|
172
|
+
def visit_Slice(self, node):
|
|
173
|
+
if node.lower:
|
|
174
|
+
self.visit(node.lower)
|
|
175
|
+
if node.upper:
|
|
176
|
+
self.visit(node.upper)
|
|
177
|
+
if node.step:
|
|
178
|
+
self.visit(node.step)
|
|
179
|
+
|
|
112
180
|
def visit_Assign(self, node):
|
|
113
181
|
for target in node.targets:
|
|
114
182
|
if isinstance(target, ast.Name) and target.id == "__all__":
|
|
@@ -148,16 +216,6 @@ class Visitor(ast.NodeVisitor):
|
|
|
148
216
|
func_name = parent.slice.value
|
|
149
217
|
self.add_ref(func_name)
|
|
150
218
|
self.add_ref(f"{self.mod}.{func_name}")
|
|
151
|
-
|
|
152
|
-
elif (isinstance(node.func, ast.Attribute) and
|
|
153
|
-
node.func.attr == "format" and
|
|
154
|
-
isinstance(node.func.value, ast.Constant) and
|
|
155
|
-
isinstance(node.func.value.value, str)):
|
|
156
|
-
fmt = node.func.value.value
|
|
157
|
-
if any(isinstance(k.arg, str) and k.arg is None for k in node.keywords):
|
|
158
|
-
for _, n, _, _ in re.findall(r'\{([^}:!]+)', fmt):
|
|
159
|
-
if n:
|
|
160
|
-
self.add_ref(self.qual(n))
|
|
161
219
|
|
|
162
220
|
def visit_Name(self,node):
|
|
163
221
|
if isinstance(node.ctx,ast.Load):
|
|
@@ -169,6 +227,20 @@ class Visitor(ast.NodeVisitor):
|
|
|
169
227
|
if isinstance(node.ctx,ast.Load)and isinstance(node.value,ast.Name):
|
|
170
228
|
self.add_ref(f"{self.qual(node.value.id)}.{node.attr}")
|
|
171
229
|
|
|
230
|
+
def visit_keyword(self, node):
|
|
231
|
+
self.visit(node.value)
|
|
232
|
+
|
|
233
|
+
def visit_withitem(self, node):
|
|
234
|
+
self.visit(node.context_expr)
|
|
235
|
+
if node.optional_vars:
|
|
236
|
+
self.visit(node.optional_vars)
|
|
237
|
+
|
|
238
|
+
def visit_ExceptHandler(self, node):
|
|
239
|
+
if node.type:
|
|
240
|
+
self.visit(node.type)
|
|
241
|
+
for stmt in node.body:
|
|
242
|
+
self.visit(stmt)
|
|
243
|
+
|
|
172
244
|
def generic_visit(self, node):
|
|
173
245
|
for field, value in ast.iter_fields(node):
|
|
174
246
|
if isinstance(value, list):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|