skylos 2.1.1__py3-none-any.whl → 2.2.2__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 +5 -3
- skylos/analyzer.py +54 -9
- skylos/cli.py +107 -22
- skylos/codemods.py +154 -5
- skylos/constants.py +2 -3
- skylos/rules/__init__.py +0 -0
- skylos/rules/secrets.py +268 -0
- skylos/server.py +1 -12
- skylos/visitor.py +97 -24
- skylos/visitors/__init__.py +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/METADATA +1 -1
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/RECORD +20 -15
- test/test_analyzer.py +6 -43
- test/test_new_behaviours.py +52 -0
- test/test_secrets.py +179 -0
- /skylos/{framework_aware.py → visitors/framework_aware.py} +0 -0
- /skylos/{test_aware.py → visitors/test_aware.py} +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/WHEEL +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/entry_points.txt +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/top_level.txt +0 -0
skylos/__init__.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
__version__ = "2.2.2"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
def analyze(*args, **kwargs):
|
|
4
|
+
from .analyzer import analyze as _analyze
|
|
5
|
+
return _analyze(*args, **kwargs)
|
|
4
6
|
|
|
5
7
|
def debug_test():
|
|
6
8
|
return "debug-ok"
|
|
7
9
|
|
|
8
|
-
__all__ = ["analyze", "debug_test", "__version__"]
|
|
10
|
+
__all__ = ["analyze", "debug_test", "__version__"]
|
skylos/analyzer.py
CHANGED
|
@@ -7,10 +7,11 @@ from pathlib import Path
|
|
|
7
7
|
from collections import defaultdict
|
|
8
8
|
from skylos.visitor import Visitor
|
|
9
9
|
from skylos.constants import ( PENALTIES, AUTO_CALLED )
|
|
10
|
-
from skylos.test_aware import TestAwareVisitor
|
|
10
|
+
from skylos.visitors.test_aware import TestAwareVisitor
|
|
11
|
+
from skylos.rules.secrets import scan_ctx as _secrets_scan_ctx
|
|
11
12
|
import os
|
|
12
13
|
import traceback
|
|
13
|
-
from skylos.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
14
|
+
from skylos.visitors.framework_aware import FrameworkAwareVisitor, detect_framework_usage
|
|
14
15
|
|
|
15
16
|
logging.basicConfig(level=logging.INFO,format='%(asctime)s - %(levelname)s - %(message)s')
|
|
16
17
|
logger=logging.getLogger('Skylos')
|
|
@@ -155,18 +156,40 @@ class Skylos:
|
|
|
155
156
|
|
|
156
157
|
def _apply_penalties(self, def_obj, visitor, framework):
|
|
157
158
|
confidence=100
|
|
159
|
+
|
|
160
|
+
if getattr(visitor, "ignore_lines", None) and def_obj.line in visitor.ignore_lines:
|
|
161
|
+
def_obj.confidence = 0
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if def_obj.type == "variable" and def_obj.simple_name == "_":
|
|
165
|
+
def_obj.confidence = 0
|
|
166
|
+
return
|
|
167
|
+
|
|
158
168
|
if def_obj.simple_name.startswith("_") and not def_obj.simple_name.startswith("__"):
|
|
159
169
|
confidence -= PENALTIES["private_name"]
|
|
170
|
+
|
|
160
171
|
if def_obj.simple_name.startswith("__") and def_obj.simple_name.endswith("__"):
|
|
161
172
|
confidence -= PENALTIES["dunder_or_magic"]
|
|
162
|
-
|
|
163
|
-
confidence = 0
|
|
173
|
+
|
|
164
174
|
if def_obj.in_init and def_obj.type in ("function", "class"):
|
|
165
175
|
confidence -= PENALTIES["in_init_file"]
|
|
176
|
+
|
|
166
177
|
if def_obj.name.split(".")[0] in self.dynamic:
|
|
167
178
|
confidence -= PENALTIES["dynamic_module"]
|
|
179
|
+
|
|
168
180
|
if visitor.is_test_file or def_obj.line in visitor.test_decorated_lines:
|
|
169
181
|
confidence -= PENALTIES["test_related"]
|
|
182
|
+
|
|
183
|
+
if def_obj.type == "variable" and getattr(framework, "dataclass_fields", None):
|
|
184
|
+
if def_obj.name in framework.dataclass_fields:
|
|
185
|
+
def_obj.confidence = 0
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if def_obj.type == "variable":
|
|
189
|
+
fr = getattr(framework, "first_read_lineno", {}).get(def_obj.name)
|
|
190
|
+
if fr is not None and fr >= def_obj.line:
|
|
191
|
+
def_obj.confidence = 0
|
|
192
|
+
return
|
|
170
193
|
|
|
171
194
|
framework_confidence = detect_framework_usage(def_obj, visitor=framework)
|
|
172
195
|
if framework_confidence is not None:
|
|
@@ -215,7 +238,7 @@ class Skylos:
|
|
|
215
238
|
if method.simple_name == "format" and cls.endswith("Formatter"):
|
|
216
239
|
method.references += 1
|
|
217
240
|
|
|
218
|
-
def analyze(self, path, thr=60, exclude_folders=None):
|
|
241
|
+
def analyze(self, path, thr=60, exclude_folders= None, enable_secrets = False):
|
|
219
242
|
files, root = self._get_python_files(path, exclude_folders)
|
|
220
243
|
|
|
221
244
|
if not files:
|
|
@@ -238,6 +261,7 @@ class Skylos:
|
|
|
238
261
|
for f in files:
|
|
239
262
|
modmap[f] = self._module(root, f)
|
|
240
263
|
|
|
264
|
+
all_secrets = []
|
|
241
265
|
for file in files:
|
|
242
266
|
mod = modmap[file]
|
|
243
267
|
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
|
|
@@ -250,6 +274,16 @@ class Skylos:
|
|
|
250
274
|
self.dynamic.update(dyn)
|
|
251
275
|
self.exports[mod].update(exports)
|
|
252
276
|
|
|
277
|
+
if enable_secrets and _secrets_scan_ctx is not None:
|
|
278
|
+
try:
|
|
279
|
+
src_lines = Path(file).read_text(encoding="utf-8", errors="ignore").splitlines(True)
|
|
280
|
+
ctx = {"relpath": str(file), "lines": src_lines, "tree": None}
|
|
281
|
+
findings = list(_secrets_scan_ctx(ctx))
|
|
282
|
+
if findings:
|
|
283
|
+
all_secrets.extend(findings)
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
253
287
|
self._mark_refs()
|
|
254
288
|
self._apply_heuristics()
|
|
255
289
|
self._mark_exports()
|
|
@@ -262,7 +296,7 @@ class Skylos:
|
|
|
262
296
|
for d in sorted(self.defs.values(), key=def_sort_key):
|
|
263
297
|
if shown >= 50:
|
|
264
298
|
break
|
|
265
|
-
print(f" {d.type
|
|
299
|
+
print(f" type={d.type} refs={d.references} conf={d.confidence} exported={d.is_exported} line={d.line} name={d.name}")
|
|
266
300
|
shown += 1
|
|
267
301
|
|
|
268
302
|
unused = []
|
|
@@ -281,6 +315,9 @@ class Skylos:
|
|
|
281
315
|
"excluded_folders": exclude_folders or [],
|
|
282
316
|
}
|
|
283
317
|
}
|
|
318
|
+
|
|
319
|
+
if enable_secrets and all_secrets:
|
|
320
|
+
result["secrets"] = all_secrets
|
|
284
321
|
|
|
285
322
|
for u in unused:
|
|
286
323
|
if u["type"] in ("function", "method"):
|
|
@@ -304,10 +341,13 @@ def proc_file(file_or_args, mod=None):
|
|
|
304
341
|
|
|
305
342
|
try:
|
|
306
343
|
source = Path(file).read_text(encoding="utf-8")
|
|
307
|
-
|
|
344
|
+
ignore_lines = {i for i, line in enumerate(source.splitlines(), start=1)
|
|
345
|
+
if "pragma: no skylos" in line}
|
|
346
|
+
tree = ast.parse(source)
|
|
308
347
|
|
|
309
348
|
tv = TestAwareVisitor(filename=file)
|
|
310
349
|
tv.visit(tree)
|
|
350
|
+
tv.ignore_lines = ignore_lines
|
|
311
351
|
|
|
312
352
|
fv = FrameworkAwareVisitor(filename=file)
|
|
313
353
|
fv.visit(tree)
|
|
@@ -315,18 +355,23 @@ def proc_file(file_or_args, mod=None):
|
|
|
315
355
|
v = Visitor(mod, file)
|
|
316
356
|
v.visit(tree)
|
|
317
357
|
|
|
358
|
+
fv.dataclass_fields = getattr(v, "dataclass_fields", set())
|
|
359
|
+
fv.first_read_lineno = getattr(v, "first_read_lineno", {})
|
|
360
|
+
|
|
318
361
|
return v.defs, v.refs, v.dyn, v.exports, tv, fv
|
|
362
|
+
|
|
319
363
|
except Exception as e:
|
|
320
364
|
logger.error(f"{file}: {e}")
|
|
321
365
|
if os.getenv("SKYLOS_DEBUG"):
|
|
322
366
|
logger.error(traceback.format_exc())
|
|
323
367
|
dummy_visitor = TestAwareVisitor(filename=file)
|
|
368
|
+
dummy_visitor.ignore_lines = set()
|
|
324
369
|
dummy_framework_visitor = FrameworkAwareVisitor(filename=file)
|
|
325
370
|
|
|
326
371
|
return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
|
|
327
372
|
|
|
328
|
-
def analyze(path,conf=60, exclude_folders=None):
|
|
329
|
-
return Skylos().analyze(path,conf, exclude_folders)
|
|
373
|
+
def analyze(path, conf=60, exclude_folders=None, enable_secrets=False):
|
|
374
|
+
return Skylos().analyze(path,conf, exclude_folders, enable_secrets)
|
|
330
375
|
|
|
331
376
|
if __name__ == "__main__":
|
|
332
377
|
if len(sys.argv)>1:
|
skylos/cli.py
CHANGED
|
@@ -8,8 +8,11 @@ from skylos.analyzer import analyze as run_analyze
|
|
|
8
8
|
from skylos.codemods import (
|
|
9
9
|
remove_unused_import_cst,
|
|
10
10
|
remove_unused_function_cst,
|
|
11
|
+
comment_out_unused_import_cst,
|
|
12
|
+
comment_out_unused_function_cst,
|
|
11
13
|
)
|
|
12
14
|
import pathlib
|
|
15
|
+
import skylos
|
|
13
16
|
|
|
14
17
|
try:
|
|
15
18
|
import inquirer
|
|
@@ -79,6 +82,32 @@ def remove_unused_function(file_path, function_name, line_number):
|
|
|
79
82
|
except Exception as e:
|
|
80
83
|
logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
|
|
81
84
|
return False
|
|
85
|
+
|
|
86
|
+
def comment_out_unused_import(file_path, import_name, line_number, marker="SKYLOS DEADCODE"):
|
|
87
|
+
path = pathlib.Path(file_path)
|
|
88
|
+
try:
|
|
89
|
+
src = path.read_text(encoding="utf-8")
|
|
90
|
+
new_code, changed = comment_out_unused_import_cst(src, import_name, line_number, marker=marker)
|
|
91
|
+
if not changed:
|
|
92
|
+
return False
|
|
93
|
+
path.write_text(new_code, encoding="utf-8")
|
|
94
|
+
return True
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logging.error(f"Failed to comment out import {import_name} from {file_path}: {e}")
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def comment_out_unused_function(file_path, function_name, line_number, marker="SKYLOS DEADCODE"):
|
|
100
|
+
path = pathlib.Path(file_path)
|
|
101
|
+
try:
|
|
102
|
+
src = path.read_text(encoding="utf-8")
|
|
103
|
+
new_code, changed = comment_out_unused_function_cst(src, function_name, line_number, marker=marker)
|
|
104
|
+
if not changed:
|
|
105
|
+
return False
|
|
106
|
+
path.write_text(new_code, encoding="utf-8")
|
|
107
|
+
return True
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logging.error(f"Failed to comment out function {function_name} from {file_path}: {e}")
|
|
110
|
+
return False
|
|
82
111
|
|
|
83
112
|
def interactive_selection(logger, unused_functions, unused_imports):
|
|
84
113
|
if not INTERACTIVE_AVAILABLE:
|
|
@@ -109,7 +138,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
|
|
|
109
138
|
selected_functions = answers['functions']
|
|
110
139
|
|
|
111
140
|
if unused_imports:
|
|
112
|
-
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to
|
|
141
|
+
logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to act on (hit spacebar to select):{Colors.RESET}")
|
|
113
142
|
|
|
114
143
|
import_choices = []
|
|
115
144
|
|
|
@@ -144,7 +173,7 @@ def print_badge(dead_code_count: int, logger):
|
|
|
144
173
|
logger.info(f"")
|
|
145
174
|
logger.info("```")
|
|
146
175
|
|
|
147
|
-
def main()
|
|
176
|
+
def main():
|
|
148
177
|
if len(sys.argv) > 1 and sys.argv[1] == 'run':
|
|
149
178
|
try:
|
|
150
179
|
start_server()
|
|
@@ -158,11 +187,26 @@ def main() -> None:
|
|
|
158
187
|
description="Detect unreachable functions and unused imports in a Python project"
|
|
159
188
|
)
|
|
160
189
|
parser.add_argument("path", help="Path to the Python project")
|
|
190
|
+
|
|
191
|
+
parser.add_argument(
|
|
192
|
+
"--version",
|
|
193
|
+
action="version",
|
|
194
|
+
version=f"skylos {skylos.__version__}",
|
|
195
|
+
help="Show version and exit"
|
|
196
|
+
)
|
|
197
|
+
|
|
161
198
|
parser.add_argument(
|
|
162
199
|
"--json",
|
|
163
200
|
action="store_true",
|
|
164
201
|
help="Output raw JSON",
|
|
165
202
|
)
|
|
203
|
+
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--comment-out",
|
|
206
|
+
action="store_true",
|
|
207
|
+
help="Comment out selected dead code instead of deleting it",
|
|
208
|
+
)
|
|
209
|
+
|
|
166
210
|
parser.add_argument(
|
|
167
211
|
"--output",
|
|
168
212
|
"-o",
|
|
@@ -223,12 +267,15 @@ def main() -> None:
|
|
|
223
267
|
help="List the default excluded folders and exit."
|
|
224
268
|
)
|
|
225
269
|
|
|
270
|
+
parser.add_argument("--secrets", action="store_true",
|
|
271
|
+
help="Scan for API keys. Off by default.")
|
|
272
|
+
|
|
226
273
|
args = parser.parse_args()
|
|
227
274
|
|
|
228
275
|
if args.list_default_excludes:
|
|
229
276
|
print("Default excluded folders:")
|
|
230
277
|
for folder in sorted(DEFAULT_EXCLUDE_FOLDERS):
|
|
231
|
-
print(f"
|
|
278
|
+
print(f" {folder}")
|
|
232
279
|
print(f"\nTotal: {len(DEFAULT_EXCLUDE_FOLDERS)} folders")
|
|
233
280
|
print("\nUse --no-default-excludes to disable these exclusions")
|
|
234
281
|
print("Use --include-folder <folder> to force include specific folders")
|
|
@@ -257,7 +304,7 @@ def main() -> None:
|
|
|
257
304
|
logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
|
|
258
305
|
|
|
259
306
|
try:
|
|
260
|
-
result_json = run_analyze(args.path, conf=args.confidence, exclude_folders=list(final_exclude_folders))
|
|
307
|
+
result_json = run_analyze(args.path, conf=args.confidence, enable_secrets=bool(args.secrets), exclude_folders=list(final_exclude_folders))
|
|
261
308
|
result = json.loads(result_json)
|
|
262
309
|
|
|
263
310
|
except Exception as e:
|
|
@@ -273,6 +320,7 @@ def main() -> None:
|
|
|
273
320
|
unused_parameters = result.get("unused_parameters", [])
|
|
274
321
|
unused_variables = result.get("unused_variables", [])
|
|
275
322
|
unused_classes = result.get("unused_classes", [])
|
|
323
|
+
secrets_findings = result.get("secrets", [])
|
|
276
324
|
|
|
277
325
|
logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
|
|
278
326
|
logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
|
|
@@ -283,49 +331,75 @@ def main() -> None:
|
|
|
283
331
|
logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
|
|
284
332
|
logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
|
|
285
333
|
logger.info(f" * Unused classes: {Colors.YELLOW}{len(unused_classes)}{Colors.RESET}")
|
|
334
|
+
if secrets_findings:
|
|
335
|
+
logger.info(f" * Secrets: {Colors.RED}{len(secrets_findings)}{Colors.RESET}")
|
|
286
336
|
|
|
287
337
|
if args.interactive and (unused_functions or unused_imports):
|
|
288
338
|
logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
|
|
289
339
|
selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
|
|
290
340
|
|
|
291
341
|
if selected_functions or selected_imports:
|
|
292
|
-
logger.info(f"\n{Colors.BOLD}Selected items to
|
|
342
|
+
logger.info(f"\n{Colors.BOLD}Selected items to process:{Colors.RESET}")
|
|
293
343
|
|
|
294
344
|
if selected_functions:
|
|
295
|
-
logger.info(f"
|
|
345
|
+
logger.info(f" Functions: {len(selected_functions)}")
|
|
296
346
|
for func in selected_functions:
|
|
297
|
-
logger.info(f"
|
|
347
|
+
logger.info(f" - {func['name']} ({func['file']}: {func['line']})")
|
|
298
348
|
|
|
299
349
|
if selected_imports:
|
|
300
|
-
logger.info(f"
|
|
350
|
+
logger.info(f" Imports: {len(selected_imports)}")
|
|
301
351
|
for imp in selected_imports:
|
|
302
|
-
logger.info(f"
|
|
352
|
+
logger.info(f" - {imp['name']} ({imp['file']}: {imp['line']})")
|
|
303
353
|
|
|
304
354
|
if not args.dry_run:
|
|
355
|
+
if args.comment_out:
|
|
356
|
+
confirm_verb = "comment out"
|
|
357
|
+
else:
|
|
358
|
+
confirm_verb = "remove"
|
|
359
|
+
|
|
305
360
|
questions = [
|
|
306
361
|
inquirer.Confirm('confirm',
|
|
307
|
-
message="Are you sure you want to
|
|
362
|
+
message="Are you sure you want to process these items?",
|
|
308
363
|
default=False)
|
|
309
364
|
]
|
|
310
365
|
answers = inquirer.prompt(questions)
|
|
311
366
|
|
|
312
367
|
if answers and answers['confirm']:
|
|
313
|
-
|
|
314
|
-
|
|
368
|
+
action = "Commenting out" if args.comment_out else "Removing"
|
|
369
|
+
logger.info(f"\n{Colors.YELLOW}{action} selected items...{Colors.RESET}")
|
|
370
|
+
|
|
371
|
+
action_func = comment_out_unused_function if args.comment_out else remove_unused_function
|
|
372
|
+
if args.comment_out:
|
|
373
|
+
action_past = "Commented out"
|
|
374
|
+
action_verb = "comment out"
|
|
375
|
+
else:
|
|
376
|
+
action_past = "Removed"
|
|
377
|
+
action_verb = "remove"
|
|
378
|
+
|
|
315
379
|
for func in selected_functions:
|
|
316
|
-
success =
|
|
380
|
+
success = action_func(func['file'], func['name'], func['line'])
|
|
381
|
+
|
|
317
382
|
if success:
|
|
318
|
-
logger.info(f" {Colors.GREEN} {Colors.RESET}
|
|
383
|
+
logger.info(f" {Colors.GREEN} ✓ {Colors.RESET} {action_past} function: {func['name']}")
|
|
319
384
|
else:
|
|
320
|
-
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to
|
|
321
|
-
|
|
385
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to {action_verb}: {func['name']}")
|
|
386
|
+
|
|
387
|
+
import_func = comment_out_unused_import if args.comment_out else remove_unused_import
|
|
388
|
+
if args.comment_out:
|
|
389
|
+
action_past = "Commented out"
|
|
390
|
+
action_verb = "comment out"
|
|
391
|
+
else:
|
|
392
|
+
action_past = "Removed"
|
|
393
|
+
action_verb = "remove"
|
|
394
|
+
|
|
322
395
|
for imp in selected_imports:
|
|
323
|
-
success =
|
|
396
|
+
success = import_func(imp['file'], imp['name'], imp['line'])
|
|
397
|
+
|
|
324
398
|
if success:
|
|
325
|
-
logger.info(f" {Colors.GREEN} {Colors.RESET}
|
|
399
|
+
logger.info(f" {Colors.GREEN} ✓ {Colors.RESET} {action_past} import: {imp['name']}")
|
|
326
400
|
else:
|
|
327
|
-
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to
|
|
328
|
-
|
|
401
|
+
logger.error(f" {Colors.RED} x {Colors.RESET} Failed to {action_verb}: {imp['name']}")
|
|
402
|
+
|
|
329
403
|
logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
|
|
330
404
|
else:
|
|
331
405
|
logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
|
|
@@ -368,6 +442,8 @@ def main() -> None:
|
|
|
368
442
|
for i, item in enumerate(unused_variables, 1):
|
|
369
443
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
|
|
370
444
|
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
445
|
+
else:
|
|
446
|
+
logger.info(f"\n{Colors.GREEN}✓ All variables are being used!{Colors.RESET}")
|
|
371
447
|
|
|
372
448
|
if unused_classes:
|
|
373
449
|
logger.info(f"\n{Colors.YELLOW}{Colors.BOLD} - Unused Classes{Colors.RESET}")
|
|
@@ -375,9 +451,18 @@ def main() -> None:
|
|
|
375
451
|
for i, item in enumerate(unused_classes, 1):
|
|
376
452
|
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{Colors.YELLOW}{item['name']}{Colors.RESET}")
|
|
377
453
|
logger.info(f" {Colors.GRAY}└─ {item['file']}:{item['line']}{Colors.RESET}")
|
|
378
|
-
|
|
379
454
|
else:
|
|
380
|
-
logger.info(f"\n{Colors.GREEN}✓ All
|
|
455
|
+
logger.info(f"\n{Colors.GREEN}✓ All classes are being used!{Colors.RESET}")
|
|
456
|
+
|
|
457
|
+
if secrets_findings:
|
|
458
|
+
logger.info(f"\n{Colors.RED}{Colors.BOLD} - Secrets{Colors.RESET}")
|
|
459
|
+
logger.info(f"{Colors.RED}{'=' * 9}{Colors.RESET}")
|
|
460
|
+
for i, s in enumerate(secrets_findings[:20], 1):
|
|
461
|
+
provider = s.get("provider", "generic")
|
|
462
|
+
where = f"{s.get('file','?')}:{s.get('line','?')}"
|
|
463
|
+
prev = s.get("preview", "****")
|
|
464
|
+
msg = s.get("message", "Secret detected")
|
|
465
|
+
logger.info(f"{Colors.GRAY}{i:2d}. {Colors.RESET}{msg} [{provider}] {Colors.GRAY}({where}){Colors.RESET} -> {prev}")
|
|
381
466
|
|
|
382
467
|
dead_code_count = len(unused_functions) + len(unused_imports) + len(unused_variables) + len(unused_classes) + len(unused_parameters)
|
|
383
468
|
|
skylos/codemods.py
CHANGED
|
@@ -2,6 +2,155 @@ from __future__ import annotations
|
|
|
2
2
|
import libcst as cst
|
|
3
3
|
from libcst.metadata import PositionProvider
|
|
4
4
|
|
|
5
|
+
class _CommentOutBlock(cst.CSTTransformer):
|
|
6
|
+
|
|
7
|
+
METADATA_DEPENDENCIES = (PositionProvider,)
|
|
8
|
+
|
|
9
|
+
def __init__(self, module_code, marker = "SKYLOS DEADCODE"):
|
|
10
|
+
self.module_code = module_code.splitlines(True)
|
|
11
|
+
self.marker = marker
|
|
12
|
+
|
|
13
|
+
def _comment_block(self, start_line, end_line):
|
|
14
|
+
lines = self.module_code[start_line - 1:end_line]
|
|
15
|
+
out = []
|
|
16
|
+
out.append(cst.EmptyLine(comment=cst.Comment(f"# {self.marker} START (lines {start_line}-{end_line})")))
|
|
17
|
+
for raw in lines:
|
|
18
|
+
out.append(cst.EmptyLine(comment=cst.Comment("# " + raw.rstrip("\n"))))
|
|
19
|
+
out.append(cst.EmptyLine(comment=cst.Comment(f"# {self.marker} END")))
|
|
20
|
+
return out
|
|
21
|
+
|
|
22
|
+
class _CommentOutFunctionAtLine(_CommentOutBlock):
|
|
23
|
+
def __init__(self, func_name, target_line, module_code, marker):
|
|
24
|
+
super().__init__(module_code, marker)
|
|
25
|
+
self.func_name = func_name
|
|
26
|
+
self.target_line = target_line
|
|
27
|
+
self.changed = False
|
|
28
|
+
|
|
29
|
+
def _is_target(self, node: cst.CSTNode):
|
|
30
|
+
pos = self.get_metadata(PositionProvider, node, None)
|
|
31
|
+
return pos and pos.start.line == self.target_line
|
|
32
|
+
|
|
33
|
+
def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
|
|
34
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
35
|
+
self.changed = True
|
|
36
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
37
|
+
return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
|
|
38
|
+
return updated
|
|
39
|
+
|
|
40
|
+
def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
|
|
41
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
42
|
+
self.changed = True
|
|
43
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
44
|
+
return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
|
|
45
|
+
return updated
|
|
46
|
+
|
|
47
|
+
class _CommentOutImportAtLine(_CommentOutBlock):
|
|
48
|
+
|
|
49
|
+
def __init__(self, target_name, target_line, module_code, marker):
|
|
50
|
+
super().__init__(module_code, marker)
|
|
51
|
+
self.target_name = target_name
|
|
52
|
+
self.target_line = target_line
|
|
53
|
+
self.changed = False
|
|
54
|
+
|
|
55
|
+
def _is_target_line(self, node: cst.CSTNode):
|
|
56
|
+
pos = self.get_metadata(PositionProvider, node, None)
|
|
57
|
+
return bool(pos and (pos.start.line <= self.target_line <= pos.end.line))
|
|
58
|
+
|
|
59
|
+
def _render_single_alias_text(self, head, alias: cst.ImportAlias, is_from):
|
|
60
|
+
if is_from:
|
|
61
|
+
alias_txt = alias.name.code
|
|
62
|
+
if alias.asname:
|
|
63
|
+
alias_txt += f" as {alias.asname.name.value}"
|
|
64
|
+
return f"from {head} import {alias_txt}"
|
|
65
|
+
else:
|
|
66
|
+
alias_txt = alias.name.code
|
|
67
|
+
if alias.asname:
|
|
68
|
+
alias_txt += f" as {alias.asname.name.value}"
|
|
69
|
+
return f"import {alias_txt}"
|
|
70
|
+
|
|
71
|
+
def _split_aliases(self, aliases, head, is_from):
|
|
72
|
+
kept = []
|
|
73
|
+
removed_for_comment= []
|
|
74
|
+
for alias in list(aliases):
|
|
75
|
+
bound = _bound_name_for_import_alias(alias)
|
|
76
|
+
if bound == self.target_name:
|
|
77
|
+
self.changed = True
|
|
78
|
+
removed_for_comment.append(self._render_single_alias_text(head, alias, is_from))
|
|
79
|
+
else:
|
|
80
|
+
kept.append(alias)
|
|
81
|
+
return kept, removed_for_comment
|
|
82
|
+
|
|
83
|
+
def leave_Import(self, orig: cst.Import, updated: cst.Import):
|
|
84
|
+
if not self._is_target_line(orig):
|
|
85
|
+
return updated
|
|
86
|
+
|
|
87
|
+
head = ""
|
|
88
|
+
kept, removed = self._split_aliases(updated.names, head, is_from=False)
|
|
89
|
+
|
|
90
|
+
if not removed:
|
|
91
|
+
return updated
|
|
92
|
+
|
|
93
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
94
|
+
if not kept:
|
|
95
|
+
return cst.FlattenSentinel(self._comment_block(pos.start.line, pos.end.line))
|
|
96
|
+
|
|
97
|
+
commented = []
|
|
98
|
+
for txt in removed:
|
|
99
|
+
comment = cst.Comment(f"# {self.marker}: {txt}")
|
|
100
|
+
commented.append(cst.EmptyLine(comment=comment))
|
|
101
|
+
|
|
102
|
+
kept_import = updated.with_changes(names=tuple(kept))
|
|
103
|
+
all_nodes = [kept_import] + commented
|
|
104
|
+
return cst.FlattenSentinel(all_nodes)
|
|
105
|
+
|
|
106
|
+
def leave_ImportFrom(self, orig: cst.ImportFrom, updated: cst.ImportFrom):
|
|
107
|
+
if not self._is_target_line(orig) or isinstance(updated.names, cst.ImportStar):
|
|
108
|
+
return updated
|
|
109
|
+
|
|
110
|
+
if updated.relative:
|
|
111
|
+
dots = "." * len(updated.relative)
|
|
112
|
+
else:
|
|
113
|
+
dots = ""
|
|
114
|
+
|
|
115
|
+
if updated.module is not None:
|
|
116
|
+
modname = updated.module.code
|
|
117
|
+
else:
|
|
118
|
+
modname = ""
|
|
119
|
+
|
|
120
|
+
mod = f"{dots}{modname}"
|
|
121
|
+
|
|
122
|
+
kept, removed = self._split_aliases(list(updated.names), mod, is_from=True)
|
|
123
|
+
|
|
124
|
+
if not removed:
|
|
125
|
+
return updated
|
|
126
|
+
pos = self.get_metadata(PositionProvider, orig)
|
|
127
|
+
|
|
128
|
+
if not kept:
|
|
129
|
+
comment_block = self._comment_block(pos.start.line, pos.end.line)
|
|
130
|
+
return cst.FlattenSentinel(comment_block)
|
|
131
|
+
|
|
132
|
+
commented = []
|
|
133
|
+
for txt in removed:
|
|
134
|
+
comment = cst.Comment(f"# {self.marker}: {txt}")
|
|
135
|
+
commented.append(cst.EmptyLine(comment=comment))
|
|
136
|
+
|
|
137
|
+
updated_import = updated.with_changes(names=tuple(kept))
|
|
138
|
+
all_nodes = [updated_import] + commented
|
|
139
|
+
|
|
140
|
+
return cst.FlattenSentinel(all_nodes)
|
|
141
|
+
|
|
142
|
+
def comment_out_unused_function_cst(code, func_name, line_number, marker = "SKYLOS DEADCODE"):
|
|
143
|
+
wrapper = cst.MetadataWrapper(cst.parse_module(code))
|
|
144
|
+
tx = _CommentOutFunctionAtLine(func_name, line_number, code, marker)
|
|
145
|
+
new_mod = wrapper.visit(tx)
|
|
146
|
+
return new_mod.code, tx.changed
|
|
147
|
+
|
|
148
|
+
def comment_out_unused_import_cst(code, import_name, line_number, marker = "SKYLOS DEADCODE"):
|
|
149
|
+
wrapper = cst.MetadataWrapper(cst.parse_module(code))
|
|
150
|
+
tx = _CommentOutImportAtLine(import_name, line_number, code, marker)
|
|
151
|
+
new_mod = wrapper.visit(tx)
|
|
152
|
+
return new_mod.code, tx.changed
|
|
153
|
+
|
|
5
154
|
def _bound_name_for_import_alias(alias: cst.ImportAlias):
|
|
6
155
|
if alias.asname:
|
|
7
156
|
return alias.asname.name.value
|
|
@@ -20,8 +169,8 @@ class _RemoveImportAtLine(cst.CSTTransformer):
|
|
|
20
169
|
|
|
21
170
|
def _is_target_line(self, node: cst.CSTNode):
|
|
22
171
|
pos = self.get_metadata(PositionProvider, node, None)
|
|
23
|
-
return bool(pos and pos.start.line
|
|
24
|
-
|
|
172
|
+
return bool(pos and (pos.start.line <= self.target_line <= pos.end.line))
|
|
173
|
+
|
|
25
174
|
def _filter_aliases(self, aliases):
|
|
26
175
|
kept = []
|
|
27
176
|
for alias in aliases:
|
|
@@ -61,16 +210,16 @@ class _RemoveFunctionAtLine(cst.CSTTransformer):
|
|
|
61
210
|
|
|
62
211
|
def _is_target(self, node: cst.CSTNode):
|
|
63
212
|
pos = self.get_metadata(PositionProvider, node, None)
|
|
64
|
-
return
|
|
213
|
+
return pos and pos.start.line == self.target_line
|
|
65
214
|
|
|
66
215
|
def leave_FunctionDef(self, orig: cst.FunctionDef, updated: cst.FunctionDef):
|
|
67
|
-
if self._is_target(orig) and (orig.name.value
|
|
216
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
68
217
|
self.changed = True
|
|
69
218
|
return cst.RemoveFromParent()
|
|
70
219
|
return updated
|
|
71
220
|
|
|
72
221
|
def leave_AsyncFunctionDef(self, orig: cst.AsyncFunctionDef, updated: cst.AsyncFunctionDef):
|
|
73
|
-
if self._is_target(orig) and (orig.name.value
|
|
222
|
+
if self._is_target(orig) and (orig.name.value == self.func_name):
|
|
74
223
|
self.changed = True
|
|
75
224
|
return cst.RemoveFromParent()
|
|
76
225
|
|
skylos/constants.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from pathlib import Path
|
|
3
2
|
|
|
4
3
|
PENALTIES = {
|
|
5
4
|
"private_name": 80,
|
|
@@ -35,10 +34,10 @@ DEFAULT_EXCLUDE_FOLDERS = {
|
|
|
35
34
|
"htmlcov", ".coverage", "build", "dist", "*.egg-info", "venv", ".venv"
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
def is_test_path(p
|
|
37
|
+
def is_test_path(p):
|
|
39
38
|
return bool(TEST_FILE_RE.search(str(p)))
|
|
40
39
|
|
|
41
|
-
def is_framework_path(p
|
|
40
|
+
def is_framework_path(p):
|
|
42
41
|
return bool(FRAMEWORK_FILE_RE.search(str(p)))
|
|
43
42
|
|
|
44
43
|
def parse_exclude_folders(user_exclude_folders= None, use_defaults= True, include_folders= None):
|
skylos/rules/__init__.py
ADDED
|
File without changes
|