skylos 2.1.2__tar.gz → 2.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of skylos might be problematic. Click here for more details.

Files changed (48) hide show
  1. {skylos-2.1.2 → skylos-2.2.2}/PKG-INFO +1 -1
  2. {skylos-2.1.2 → skylos-2.2.2}/README.md +222 -3
  3. {skylos-2.1.2 → skylos-2.2.2}/pyproject.toml +1 -1
  4. {skylos-2.1.2 → skylos-2.2.2}/setup.py +1 -1
  5. skylos-2.2.2/skylos/__init__.py +10 -0
  6. {skylos-2.1.2 → skylos-2.2.2}/skylos/analyzer.py +20 -5
  7. {skylos-2.1.2 → skylos-2.2.2}/skylos/cli.py +94 -19
  8. skylos-2.2.2/skylos/codemods.py +238 -0
  9. skylos-2.2.2/skylos/rules/secrets.py +268 -0
  10. {skylos-2.1.2 → skylos-2.2.2}/skylos/server.py +1 -12
  11. {skylos-2.1.2 → skylos-2.2.2}/skylos.egg-info/PKG-INFO +1 -1
  12. {skylos-2.1.2 → skylos-2.2.2}/skylos.egg-info/SOURCES.txt +6 -2
  13. skylos-2.2.2/test/sample_repo/__init__.py +0 -0
  14. skylos-2.2.2/test/sample_repo/sample_repo/__init__.py +0 -0
  15. skylos-2.2.2/test/test_secrets.py +179 -0
  16. skylos-2.1.2/skylos/__init__.py +0 -8
  17. skylos-2.1.2/skylos/codemods.py +0 -89
  18. {skylos-2.1.2 → skylos-2.2.2}/setup.cfg +0 -0
  19. {skylos-2.1.2 → skylos-2.2.2}/skylos/constants.py +0 -0
  20. {skylos-2.1.2/test → skylos-2.2.2/skylos/rules}/__init__.py +0 -0
  21. {skylos-2.1.2 → skylos-2.2.2}/skylos/visitor.py +0 -0
  22. {skylos-2.1.2/test/sample_repo → skylos-2.2.2/skylos/visitors}/__init__.py +0 -0
  23. {skylos-2.1.2/skylos → skylos-2.2.2/skylos/visitors}/framework_aware.py +0 -0
  24. {skylos-2.1.2/skylos → skylos-2.2.2/skylos/visitors}/test_aware.py +0 -0
  25. {skylos-2.1.2 → skylos-2.2.2}/skylos.egg-info/dependency_links.txt +0 -0
  26. {skylos-2.1.2 → skylos-2.2.2}/skylos.egg-info/entry_points.txt +0 -0
  27. {skylos-2.1.2 → skylos-2.2.2}/skylos.egg-info/requires.txt +0 -0
  28. {skylos-2.1.2 → skylos-2.2.2}/skylos.egg-info/top_level.txt +0 -0
  29. {skylos-2.1.2/test/sample_repo/sample_repo → skylos-2.2.2/test}/__init__.py +0 -0
  30. {skylos-2.1.2 → skylos-2.2.2}/test/compare_tools.py +0 -0
  31. {skylos-2.1.2 → skylos-2.2.2}/test/conftest.py +0 -0
  32. {skylos-2.1.2 → skylos-2.2.2}/test/diagnostics.py +0 -0
  33. {skylos-2.1.2 → skylos-2.2.2}/test/sample_repo/app.py +0 -0
  34. {skylos-2.1.2 → skylos-2.2.2}/test/sample_repo/sample_repo/commands.py +0 -0
  35. {skylos-2.1.2 → skylos-2.2.2}/test/sample_repo/sample_repo/models.py +0 -0
  36. {skylos-2.1.2 → skylos-2.2.2}/test/sample_repo/sample_repo/routes.py +0 -0
  37. {skylos-2.1.2 → skylos-2.2.2}/test/sample_repo/sample_repo/utils.py +0 -0
  38. {skylos-2.1.2 → skylos-2.2.2}/test/test_analyzer.py +0 -0
  39. {skylos-2.1.2 → skylos-2.2.2}/test/test_changes_analyzer.py +0 -0
  40. {skylos-2.1.2 → skylos-2.2.2}/test/test_cli.py +0 -0
  41. {skylos-2.1.2 → skylos-2.2.2}/test/test_codemods.py +0 -0
  42. {skylos-2.1.2 → skylos-2.2.2}/test/test_constants.py +0 -0
  43. {skylos-2.1.2 → skylos-2.2.2}/test/test_framework_aware.py +0 -0
  44. {skylos-2.1.2 → skylos-2.2.2}/test/test_integration.py +0 -0
  45. {skylos-2.1.2 → skylos-2.2.2}/test/test_new_behaviours.py +0 -0
  46. {skylos-2.1.2 → skylos-2.2.2}/test/test_skylos.py +0 -0
  47. {skylos-2.1.2 → skylos-2.2.2}/test/test_test_aware.py +0 -0
  48. {skylos-2.1.2 → skylos-2.2.2}/test/test_visitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 2.1.2
3
+ Version: 2.2.2
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -32,6 +32,7 @@
32
32
  - [Example Output](#example-output)
33
33
  - [Interactive Mode](#interactive-mode)
34
34
  - [Development](#development)
35
+ - [CI/CD (Pre-commit & GitHub Actions)](#cicd-pre-commit--github-actions)
35
36
  - [FAQ](#faq)
36
37
  - [Limitations](#limitations)
37
38
  - [Troubleshooting](#troubleshooting)
@@ -51,6 +52,8 @@
51
52
  * **Unused Imports**: Identifies imports that are not used
52
53
  * **Folder Management**: Inclusion/exclusion of directories
53
54
  * **Ignore Pragmas**: Skip lines tagged with `# pragma: no skylos`, `# pragma: no cover`, or `# noqa`
55
+ **NEW** **Secrets Scanning (PoC, opt-in)**: Detects API keys & secrets (GitHub, GitLab, Slack, Stripe, AWS, Google, SendGrid, Twilio, private key blocks)
56
+
54
57
 
55
58
  ## Benchmark (You can find this benchmark test in `test` folder)
56
59
 
@@ -94,12 +97,17 @@ pip install .
94
97
  ```bash
95
98
  skylos /path/to/your/project
96
99
 
100
+ skylos /path/to/your/project --secrets ## include api key scan
101
+
97
102
  # To launch the front end
98
103
  skylos run
99
104
 
100
105
  # Interactive mode - select items to remove
101
106
  skylos --interactive /path/to/your/project
102
107
 
108
+ # Comment out items
109
+ skylos . --interactive --comment-out
110
+
103
111
  # Dry run - see what would be removed
104
112
  skylos --interactive --dry-run /path/to/your/project
105
113
 
@@ -193,7 +201,7 @@ When Skylos detects a test file, it by default, will apply a confidence penalty
193
201
 
194
202
  ## Ignoring Pragmas
195
203
 
196
- To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
204
+ 1. To ignore any warning, indicate `# pragma: no skylos` **ON THE SAME LINE** as the function/class you want to ignore
197
205
 
198
206
  Example
199
207
 
@@ -203,6 +211,14 @@ Example
203
211
  return new_path
204
212
  ```
205
213
 
214
+ 2. To suppress a **secret** on a line, add: `# skylos: ignore[SKY-S101]`
215
+
216
+ Example
217
+ ```python
218
+
219
+ API_KEY = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # skylos: ignore[SKY-S101]
220
+ ```
221
+
206
222
  ## Including & Excluding Files
207
223
 
208
224
  ### Default Exclusions
@@ -244,6 +260,7 @@ Options:
244
260
  --no-default-excludes Don't exclude default folders (__pycache__, .git, venv, etc.)
245
261
  --list-default-excludes List the default excluded folders and
246
262
  -c, --confidence LEVEL Confidence threshold (0-100). Lower values will show more items.
263
+ -- secrets Scan for api keys/secrets
247
264
  ```
248
265
 
249
266
  ## Interactive Mode
@@ -254,6 +271,206 @@ The interactive mode lets you select specific functions and imports to remove:
254
271
  2. **Confirm changes**: Review selected items before applying
255
272
  3. **Auto-cleanup**: Files are automatically updated
256
273
 
274
+ ## CI/CD (Pre-commit & GitHub Actions)
275
+
276
+ Pick **one** (or use **both**)
277
+
278
+ 1. Pre-commit (local + CI): runs Skylos before commits/PRs.
279
+ - You must install pre-commit locally once. Skylos gets installed automatically by the hook.
280
+
281
+ 2. GitHub Actions: runs Skylos on pushes/PRs in CI.
282
+ - No local install needed
283
+
284
+ ### Option A — Pre-commit (local + CI)
285
+
286
+ 1. Create or edit `.pre-commit-config.yaml` at the repo root:
287
+
288
+ **A: Skylos hook repo**
289
+ ```yaml
290
+ ## .pre-commit-config.yaml
291
+ repos:
292
+ - repo: https://github.com/duriantaco/skylos
293
+ rev: v2.2.2
294
+ hooks:
295
+ - id: skylos-scan
296
+ name: skylos report
297
+ entry: python -m skylos.cli
298
+ language: python
299
+ types_or: [python]
300
+ pass_filenames: false
301
+ require_serial: true
302
+ args: [".", "--output", "report.json", "--confidence", "70"]
303
+
304
+ - repo: local
305
+ hooks:
306
+ - id: skylos-fail-on-findings
307
+ name: skylos
308
+ env:
309
+ SKYLOS_SOFT: "1"
310
+ language: python
311
+ language_version: python3
312
+ pass_filenames: false
313
+ require_serial: true
314
+ entry: >
315
+ python -c "import os, json, sys, pathlib;
316
+ p=pathlib.Path('report.json');
317
+
318
+ if not p.exists():
319
+ sys.exit(0);
320
+
321
+ data=json.loads(p.read_text(encoding='utf-8'));
322
+
323
+ count = 0
324
+ for v in data.values():
325
+ if isinstance(v, list):
326
+ count += len(v)
327
+
328
+ print(f'[skylos] findings: {count}');
329
+ sys.exit(0 if os.getenv('SKYLOS_SOFT') or count==0 else 1)"
330
+ ```
331
+ **B: self-contained local hook**
332
+
333
+ ```yaml
334
+ repos:
335
+ - repo: local
336
+ hooks:
337
+ - id: skylos-scan
338
+ name: skylos report
339
+ language: python
340
+ entry: python -m skylos.cli
341
+ pass_filenames: false
342
+ require_serial: true
343
+ additional_dependencies: [skylos==2.2.2]
344
+ args: [".", "--output", "report.json", "--confidence", "70"]
345
+
346
+ - id: skylos-fail-on-findings
347
+ name: skylos (soft)
348
+ language: python
349
+ language_version: python3
350
+ pass_filenames: false
351
+ require_serial: true
352
+ entry: >
353
+ python -c "import os, json, sys, pathlib;
354
+ p=pathlib.Path('report.json');
355
+
356
+ if not p.exists():
357
+ sys.exit(0);
358
+
359
+ data=json.loads(p.read_text(encoding='utf-8'));
360
+
361
+ count = 0
362
+ for v in data.values():
363
+ if isinstance(v, list):
364
+ count += len(v)
365
+
366
+ print(f'[skylos] findings: {count}');
367
+ sys.exit(0 if os.getenv('SKYLOS_SOFT') or count==0 else 1)"
368
+ ```
369
+
370
+ **Install requirements:**
371
+
372
+ You must install pre-commit locally once:
373
+ ```bash
374
+ pip install pre-commit
375
+ pre-commit install
376
+ ```
377
+
378
+ 2. pre-commit run --all-files
379
+
380
+
381
+ 3. Run the same hooks in CI (GitHub Actions): create .github/workflows/pre-commit.yml:
382
+
383
+ ```yaml
384
+ name: pre-commit
385
+ on: [push, pull_request]
386
+ jobs:
387
+ run:
388
+ runs-on: ubuntu-latest
389
+ steps:
390
+ - uses: actions/checkout@v4
391
+ - uses: actions/setup-python@v5
392
+ with: { python-version: "3.11", cache: "pip" }
393
+ - uses: pre-commit/action@v3.0.1
394
+ with: { extra_args: --all-files }
395
+ ```
396
+
397
+ **Pre commit behavior:** the second hook is soft by default (SKYLOS_SOFT=1). This means that it prints findings and passes. You can remove the env/logic if you want pre-commit to block commits on finding
398
+
399
+ ### Option B — Github Actions
400
+
401
+ 1. Create .github/workflows/skylos.yml:
402
+
403
+ ```yaml
404
+ name: Skylos Deadcode Scan
405
+
406
+ on:
407
+ pull_request:
408
+ push:
409
+ branches: [ main, master ]
410
+ workflow_dispatch:
411
+
412
+ jobs:
413
+ scan:
414
+ runs-on: ubuntu-latest
415
+ env:
416
+ SKYLOS_STRICT: ${{ vars.SKYLOS_STRICT || 'false' }}
417
+ steps:
418
+ - uses: actions/checkout@v4
419
+
420
+ - uses: actions/setup-python@v5
421
+ with:
422
+ python-version: '3.11'
423
+ cache: 'pip'
424
+
425
+ - name: Install Skylos
426
+ run: pip install skylos
427
+
428
+ - name: Run Skylos
429
+ env:
430
+ REPORT: skylos_${{ github.run_number }}_${{ github.sha }}.json
431
+ run: |
432
+ echo "REPORT=$REPORT" >> "$GITHUB_OUTPUT"
433
+ skylos . --json > "$REPORT"
434
+ id: scan
435
+
436
+ - name: Fail if there are findings
437
+ continue-on-error: ${{ env.SKYLOS_STRICT != 'true' }}
438
+ env:
439
+ REPORT: ${{ steps.scan.outputs.REPORT }}
440
+ run: |
441
+ python - << 'PY'
442
+ import json, sys, os
443
+ report = os.environ["REPORT"]
444
+ data = json.load(open(report, "r", encoding="utf-8"))
445
+ count = 0
446
+ for value in data.values():
447
+ if isinstance(value, list):
448
+ count += len(value)
449
+ print(f"Findings: {count}")
450
+ if count > 0:
451
+ print(f"::warning title=Skylos findings::{count} potential issues found. See {report}")
452
+ sys.exit(1 if count > 0 else 0)
453
+ PY
454
+
455
+ - name: Upload report artifact
456
+ if: always()
457
+ uses: actions/upload-artifact@v4
458
+ with:
459
+ name: ${{ steps.scan.outputs.REPORT }}
460
+ path: ${{ steps.scan.outputs.REPORT }}
461
+
462
+ - name: Summarize in job log
463
+ if: always()
464
+ run: |
465
+ echo "Skylos report: ${{ steps.scan.outputs.REPORT }}" >> $GITHUB_STEP_SUMMARY
466
+ ```
467
+
468
+ **To make the job fail on findings (strict mode)**:
469
+
470
+ 1. Go to GitHub -> Settings -> Secrets and variables -> Actions -> Variables
471
+
472
+ 2. Add variable SKYLOS_STRICT with value true
473
+
257
474
  ## Development
258
475
 
259
476
  ### Prerequisites
@@ -307,6 +524,7 @@ A: Start with 60 (default) for safe cleanup. Use 30 for framework applications.
307
524
  - **Frameworks**: Django models, Flask, FastAPI routes may appear unused but aren't
308
525
  - **Test data**: Limited scenarios, your mileage may vary
309
526
  - **False positives**: Always manually review before deleting code
527
+ - **Secrets PoC**: May emit both a provider hit and a generic high-entropy hit for the same token. All tokens are detected only in py files (`.py`, `.pyi`, `.pyw`).
310
528
 
311
529
  ## Troubleshooting
312
530
 
@@ -339,9 +557,10 @@ We welcome contributions! Please read our [Contributing Guidelines](CONTRIBUTING
339
557
  ## Roadmap
340
558
  - [x] Expand our test cases
341
559
  - [ ] Configuration file support
342
- - [ ] Git hooks integration
343
- - [ ] CI/CD integration examples
560
+ - [x] Git hooks integration
561
+ - [x] CI/CD integration examples
344
562
  - [ ] Further optimization
563
+ - [ ] Add new rules
345
564
 
346
565
  ## License
347
566
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "skylos"
7
- version = "2.1.2"
7
+ version = "2.2.2"
8
8
  requires-python = ">=3.9"
9
9
  description = "A static analysis tool for Python codebases"
10
10
  authors = [{name = "oha", email = "aaronoh2015@gmail.com"}]
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="skylos",
5
- version="2.1.2",
5
+ version="2.2.2",
6
6
  packages=find_packages(),
7
7
  python_requires=">=3.9",
8
8
  install_requires=[
@@ -0,0 +1,10 @@
1
+ __version__ = "2.2.2"
2
+
3
+ def analyze(*args, **kwargs):
4
+ from .analyzer import analyze as _analyze
5
+ return _analyze(*args, **kwargs)
6
+
7
+ def debug_test():
8
+ return "debug-ok"
9
+
10
+ __all__ = ["analyze", "debug_test", "__version__"]
@@ -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')
@@ -237,7 +238,7 @@ class Skylos:
237
238
  if method.simple_name == "format" and cls.endswith("Formatter"):
238
239
  method.references += 1
239
240
 
240
- def analyze(self, path, thr=60, exclude_folders=None):
241
+ def analyze(self, path, thr=60, exclude_folders= None, enable_secrets = False):
241
242
  files, root = self._get_python_files(path, exclude_folders)
242
243
 
243
244
  if not files:
@@ -260,6 +261,7 @@ class Skylos:
260
261
  for f in files:
261
262
  modmap[f] = self._module(root, f)
262
263
 
264
+ all_secrets = []
263
265
  for file in files:
264
266
  mod = modmap[file]
265
267
  defs, refs, dyn, exports, test_flags, framework_flags = proc_file(file, mod)
@@ -272,6 +274,16 @@ class Skylos:
272
274
  self.dynamic.update(dyn)
273
275
  self.exports[mod].update(exports)
274
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
+
275
287
  self._mark_refs()
276
288
  self._apply_heuristics()
277
289
  self._mark_exports()
@@ -303,6 +315,9 @@ class Skylos:
303
315
  "excluded_folders": exclude_folders or [],
304
316
  }
305
317
  }
318
+
319
+ if enable_secrets and all_secrets:
320
+ result["secrets"] = all_secrets
306
321
 
307
322
  for u in unused:
308
323
  if u["type"] in ("function", "method"):
@@ -355,8 +370,8 @@ def proc_file(file_or_args, mod=None):
355
370
 
356
371
  return [], [], set(), set(), dummy_visitor, dummy_framework_visitor
357
372
 
358
- def analyze(path,conf=60, exclude_folders=None):
359
- 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)
360
375
 
361
376
  if __name__ == "__main__":
362
377
  if len(sys.argv)>1:
@@ -8,6 +8,8 @@ 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
13
15
  import skylos
@@ -80,6 +82,32 @@ def remove_unused_function(file_path, function_name, line_number):
80
82
  except Exception as e:
81
83
  logging.error(f"Failed to remove function {function_name} from {file_path}: {e}")
82
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
83
111
 
84
112
  def interactive_selection(logger, unused_functions, unused_imports):
85
113
  if not INTERACTIVE_AVAILABLE:
@@ -110,7 +138,7 @@ def interactive_selection(logger, unused_functions, unused_imports):
110
138
  selected_functions = answers['functions']
111
139
 
112
140
  if unused_imports:
113
- logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to remove (hit spacebar to select):{Colors.RESET}")
141
+ logger.info(f"\n{Colors.MAGENTA}{Colors.BOLD}Select unused imports to act on (hit spacebar to select):{Colors.RESET}")
114
142
 
115
143
  import_choices = []
116
144
 
@@ -172,6 +200,13 @@ def main():
172
200
  action="store_true",
173
201
  help="Output raw JSON",
174
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
+
175
210
  parser.add_argument(
176
211
  "--output",
177
212
  "-o",
@@ -232,12 +267,15 @@ def main():
232
267
  help="List the default excluded folders and exit."
233
268
  )
234
269
 
270
+ parser.add_argument("--secrets", action="store_true",
271
+ help="Scan for API keys. Off by default.")
272
+
235
273
  args = parser.parse_args()
236
274
 
237
275
  if args.list_default_excludes:
238
276
  print("Default excluded folders:")
239
277
  for folder in sorted(DEFAULT_EXCLUDE_FOLDERS):
240
- print(f" {folder}")
278
+ print(f" {folder}")
241
279
  print(f"\nTotal: {len(DEFAULT_EXCLUDE_FOLDERS)} folders")
242
280
  print("\nUse --no-default-excludes to disable these exclusions")
243
281
  print("Use --include-folder <folder> to force include specific folders")
@@ -266,7 +304,7 @@ def main():
266
304
  logger.info(f"{Colors.GREEN}📁 No folders excluded{Colors.RESET}")
267
305
 
268
306
  try:
269
- 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))
270
308
  result = json.loads(result_json)
271
309
 
272
310
  except Exception as e:
@@ -282,6 +320,7 @@ def main():
282
320
  unused_parameters = result.get("unused_parameters", [])
283
321
  unused_variables = result.get("unused_variables", [])
284
322
  unused_classes = result.get("unused_classes", [])
323
+ secrets_findings = result.get("secrets", [])
285
324
 
286
325
  logger.info(f"{Colors.CYAN}{Colors.BOLD} Python Static Analysis Results{Colors.RESET}")
287
326
  logger.info(f"{Colors.CYAN}{'=' * 35}{Colors.RESET}")
@@ -292,49 +331,75 @@ def main():
292
331
  logger.info(f" * Unused parameters: {Colors.YELLOW}{len(unused_parameters)}{Colors.RESET}")
293
332
  logger.info(f" * Unused variables: {Colors.YELLOW}{len(unused_variables)}{Colors.RESET}")
294
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}")
295
336
 
296
337
  if args.interactive and (unused_functions or unused_imports):
297
338
  logger.info(f"\n{Colors.BOLD}Interactive Mode:{Colors.RESET}")
298
339
  selected_functions, selected_imports = interactive_selection(logger, unused_functions, unused_imports)
299
340
 
300
341
  if selected_functions or selected_imports:
301
- logger.info(f"\n{Colors.BOLD}Selected items to remove:{Colors.RESET}")
342
+ logger.info(f"\n{Colors.BOLD}Selected items to process:{Colors.RESET}")
302
343
 
303
344
  if selected_functions:
304
- logger.info(f" Functions: {len(selected_functions)}")
345
+ logger.info(f" Functions: {len(selected_functions)}")
305
346
  for func in selected_functions:
306
- logger.info(f" - {func['name']} ({func['file']}:{func['line']})")
347
+ logger.info(f" - {func['name']} ({func['file']}: {func['line']})")
307
348
 
308
349
  if selected_imports:
309
- logger.info(f" Imports: {len(selected_imports)}")
350
+ logger.info(f" Imports: {len(selected_imports)}")
310
351
  for imp in selected_imports:
311
- logger.info(f" - {imp['name']} ({imp['file']}:{imp['line']})")
352
+ logger.info(f" - {imp['name']} ({imp['file']}: {imp['line']})")
312
353
 
313
354
  if not args.dry_run:
355
+ if args.comment_out:
356
+ confirm_verb = "comment out"
357
+ else:
358
+ confirm_verb = "remove"
359
+
314
360
  questions = [
315
361
  inquirer.Confirm('confirm',
316
- message="Are you sure you want to remove these items?",
362
+ message="Are you sure you want to process these items?",
317
363
  default=False)
318
364
  ]
319
365
  answers = inquirer.prompt(questions)
320
366
 
321
367
  if answers and answers['confirm']:
322
- logger.info(f"\n{Colors.YELLOW}Removing selected items...{Colors.RESET}")
323
-
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
+
324
379
  for func in selected_functions:
325
- success = remove_unused_function(func['file'], func['name'], func['line'])
380
+ success = action_func(func['file'], func['name'], func['line'])
381
+
326
382
  if success:
327
- logger.info(f" {Colors.GREEN} {Colors.RESET} Removed function: {func['name']}")
383
+ logger.info(f" {Colors.GREEN} {Colors.RESET} {action_past} function: {func['name']}")
328
384
  else:
329
- logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {func['name']}")
330
-
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
+
331
395
  for imp in selected_imports:
332
- success = remove_unused_import(imp['file'], imp['name'], imp['line'])
396
+ success = import_func(imp['file'], imp['name'], imp['line'])
397
+
333
398
  if success:
334
- logger.info(f" {Colors.GREEN} {Colors.RESET} Removed import: {imp['name']}")
399
+ logger.info(f" {Colors.GREEN} {Colors.RESET} {action_past} import: {imp['name']}")
335
400
  else:
336
- logger.error(f" {Colors.RED} x {Colors.RESET} Failed to remove: {imp['name']}")
337
-
401
+ logger.error(f" {Colors.RED} x {Colors.RESET} Failed to {action_verb}: {imp['name']}")
402
+
338
403
  logger.info(f"\n{Colors.GREEN}Cleanup complete!{Colors.RESET}")
339
404
  else:
340
405
  logger.info(f"\n{Colors.YELLOW}Operation cancelled.{Colors.RESET}")
@@ -389,6 +454,16 @@ def main():
389
454
  else:
390
455
  logger.info(f"\n{Colors.GREEN}✓ All classes are being used!{Colors.RESET}")
391
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}")
466
+
392
467
  dead_code_count = len(unused_functions) + len(unused_imports) + len(unused_variables) + len(unused_classes) + len(unused_parameters)
393
468
 
394
469
  print_badge(dead_code_count, logger)