ballpython 2.0.7__py3-none-any.whl → 2.1.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ballpython
3
- Version: 2.0.7
3
+ Version: 2.1.0
4
4
  Summary: The Ultimate Static Python Intelligence, Healing, Type Verification, and Security Suite
5
5
  Author: Developer
6
6
  License-Expression: MIT
@@ -5,30 +5,30 @@ ballpython/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  pycleaner/__init__.py,sha256=VzmbjUip6i0wpKh-s6XBwgBkIeUgI1duGyKDIiv7Pls,3042
6
6
  pycleaner/__main__.py,sha256=8QBFrPyxId563F6cipPMNVEBTG8TIZUgxQfp8gk1VSI,135
7
7
  pycleaner/baseline.py,sha256=ljED3x3akie0SB3fzhVno_1UHlI2id-9vta0NIGC0mw,3857
8
- pycleaner/cache.py,sha256=WhCiMkuL6ryWVr3l3XrYLLoI7A3bRF8jNxpOyedNW8M,10761
9
- pycleaner/cli.py,sha256=ELGV5hzerbER4uSNmioKCRDpMUYWd6HjJnwVgUyjfE0,72953
8
+ pycleaner/cache.py,sha256=T3JIxYg8ZezRbSpXYXPD-SyMUkjg4nfKsdO0IuNypOY,10774
9
+ pycleaner/cli.py,sha256=NswwV_3cbHiL63Pj6qDtSpV_98zXvGo7UuBHwNAxpQ8,73752
10
10
  pycleaner/complexity_analyzer.py,sha256=j2ktzgR-isWE4xWMe56N3JFLLuYCi-S84BIlRBe0v-I,14975
11
11
  pycleaner/config.py,sha256=XhloyaY_9pdwbQuc3QaAPCerLeXz-i1da4xT50h-pjY,8514
12
- pycleaner/dead_code_detector.py,sha256=4JhZujswvPIcYbF_is3_umTGf6q5Iu6iDWbmRQsDy7s,26984
13
- pycleaner/dependency_auditor.py,sha256=CsNMboUdB5Ug-xaeajAykMBCpUzCXQDCCbwwVjt5awk,18456
12
+ pycleaner/dead_code_detector.py,sha256=uufGt7EtGusQdtbX3rUCHIrF8IAu-S16jrViBJdnV-g,52092
13
+ pycleaner/dependency_auditor.py,sha256=EmSqr_ayUqmdSZ7Nm4QAT4qfikF9VToLL8eXVecIDpw,23395
14
14
  pycleaner/discovery.py,sha256=dt3OmHg886hy_Z0p80ccMyDkITQxz4biaJY_zB3MU_g,6151
15
15
  pycleaner/explanations.py,sha256=2OOU_l3YL5o_Br_6fP6MF8HlY75-1JRmw3eTYT2lu8M,14219
16
16
  pycleaner/import_resolver.py,sha256=Wgrch335AQVsnDv5-7F0l4Zm2oQPkNebPEgTRa7ayAg,32855
17
- pycleaner/linter_formatter.py,sha256=gGl4sLTC5CcuTVHC2QkrkqGaqS_HJ9uUa1TFS5qkmoU,21001
18
- pycleaner/modernizer.py,sha256=WpOLNYPDxKCAe3fj5tF4iHliR-8nzvjHU79SVZYWepk,16797
17
+ pycleaner/linter_formatter.py,sha256=wdTjwguPWzkJ9KRMcQpFHEirb3ogc9qay69yiphz16Y,21356
18
+ pycleaner/modernizer.py,sha256=kd9_G5EzdW_O-9zm1hA6v33Sb__UbpMZzSvopF-UK5w,16820
19
19
  pycleaner/pipeline.py,sha256=ZG7_9PNz0jBadFrZoiI7juLtUJXglGQhLgQhrLJqEac,20612
20
20
  pycleaner/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- pycleaner/security_scanner.py,sha256=ik485wW_aDbjV2X7HVPQy8uArkj406Jnn_Me78Og_n8,23444
22
- pycleaner/syntax_healer.py,sha256=RLuUjxMLqgHYzllX_juriJxIgaQv8e0cQIryVvzZeOE,27325
21
+ pycleaner/security_scanner.py,sha256=8nyRtoCjBYAB_sI5azlNsEMm2S0s2zE7fPhc7QCHWR0,23527
22
+ pycleaner/syntax_healer.py,sha256=5VYclqkI3WFnm6yfG0zRLMkHN4QMU4aN4ZYCcSUsmZk,27408
23
23
  pycleaner/taint_engine.py,sha256=-bBq38YoQgbO2FHCDxDctZpXWQTqwuS56CEWFNOY000,26870
24
24
  pycleaner/test_generator.py,sha256=MM3t0Dx5lyJB5s144IKXq2tXrE8ohKcSvicPR3c9MlU,19680
25
25
  pycleaner/type_checker.py,sha256=zkUqi-BNLUzJT0tIECNh7_7GP29Y7_6fMXhVuguwMKg,37881
26
26
  pycleaner/typeshed_resolver.py,sha256=N-kWU_CI5DG59lQMkt7wMW9sHYjNCBdfFS42b4y2Kr4,12742
27
- pycleaner/verifier.py,sha256=IEopcBfPLQXWOJxKV297150lpXBd3uGe7iv6CODJr6k,19305
27
+ pycleaner/verifier.py,sha256=QlBwnsrhWWRK3LzvJjpNix474drCNp6LP7FiUgJxMsg,19333
28
28
  pycleaner/frameworks/__init__.py,sha256=SOuSRl39af86eoQx7Qm8rTJGD4jsnEvgyhhQ6cVmRVA,3462
29
29
  pycleaner/frameworks/plugins.py,sha256=qunMzS3q7xp1GEGxqkuhrnm-raiSRhYQMzmDxsEhSUg,11452
30
- ballpython-2.0.7.dist-info/METADATA,sha256=RNLcZfpGvUsfStSio5seuuMfq9p3YGWn86Z0IstIk6o,3466
31
- ballpython-2.0.7.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
- ballpython-2.0.7.dist-info/entry_points.txt,sha256=6ux-5UxoVSmlK5p9M-GDVt24sCdwTQq9wgRDkpMIYU8,81
33
- ballpython-2.0.7.dist-info/top_level.txt,sha256=u6lmy8RWZ_saxCZAQTL7EijB4sunegOiVyk5fHjpwLk,21
34
- ballpython-2.0.7.dist-info/RECORD,,
30
+ ballpython-2.1.0.dist-info/METADATA,sha256=98OjKH0i9ih05fQ9JX2eriRljEboAEOVIgk4TwQ9hQ8,3466
31
+ ballpython-2.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
32
+ ballpython-2.1.0.dist-info/entry_points.txt,sha256=6ux-5UxoVSmlK5p9M-GDVt24sCdwTQq9wgRDkpMIYU8,81
33
+ ballpython-2.1.0.dist-info/top_level.txt,sha256=u6lmy8RWZ_saxCZAQTL7EijB4sunegOiVyk5fHjpwLk,21
34
+ ballpython-2.1.0.dist-info/RECORD,,
pycleaner/cache.py CHANGED
@@ -15,6 +15,7 @@ from __future__ import annotations
15
15
  import ast
16
16
  import hashlib
17
17
  import json
18
+ import logging
18
19
  import sqlite3
19
20
  import time
20
21
  from pathlib import Path
@@ -22,6 +23,7 @@ from typing import TYPE_CHECKING, Any
22
23
 
23
24
  if TYPE_CHECKING:
24
25
  from pycleaner.pipeline import CleanResult
26
+
25
27
  from pycleaner.verifier import VerificationTier
26
28
 
27
29
 
@@ -46,7 +48,7 @@ def extract_file_dependencies(source: str) -> list[str]:
46
48
  elif isinstance(node, ast.ImportFrom) and node.module:
47
49
  deps.append(node.module.split(".")[0])
48
50
  except SyntaxError:
49
- pass # Best-effort dependency extraction; syntax errors handled downstream
51
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
50
52
  return sorted(set(deps))
51
53
 
52
54
 
pycleaner/cli.py CHANGED
@@ -157,6 +157,11 @@ def _register_fix_subparsers(subparsers: Any) -> None:
157
157
  def _register_analysis_subparsers(subparsers: Any) -> None:
158
158
  audit_p = subparsers.add_parser("audit", help="Audit project dependencies")
159
159
  _add_common_args(audit_p)
160
+ audit_p.add_argument(
161
+ "--fix",
162
+ action="store_true",
163
+ help="Auto-fix requirements.txt and pyproject.toml and prune unused packages",
164
+ )
160
165
  audit_p.add_argument(
161
166
  "--fix-deps", action="store_true", help="Auto-fix requirements.txt"
162
167
  )
@@ -897,15 +902,13 @@ def _render_audit_terminal_output(
897
902
  f" [red]Missing:[/red] {', '.join(sorted(audit_report.missing_packages))}"
898
903
  )
899
904
  if not should_fix:
900
- print_msg(
901
- " [dim]Tip: Run with -a/--all or --fix-deps to append them[/dim]"
902
- )
905
+ print_msg(" [dim]Tip: Run with -a/--all or --fix to append them[/dim]")
903
906
  if audit_report.unused_packages:
904
907
  print_msg(
905
908
  f" [yellow]Unused:[/yellow] {', '.join(sorted(audit_report.unused_packages))}"
906
909
  )
907
910
  if audit_report.fixed_requirements:
908
- print_msg(" [green]requirements.txt synchronized.[/green]")
911
+ print_msg(" [green]Project dependencies synchronized on disk.[/green]")
909
912
 
910
913
 
911
914
  def _run_fix_audit(
@@ -918,8 +921,18 @@ def _run_fix_audit(
918
921
  ) -> DependencyAuditReport:
919
922
  exclude_patterns = config.exclude if config else ()
920
923
  auditor = DependencyAuditor(root_dir, exclude_patterns=exclude_patterns)
921
- fix_any = getattr(args, "fix_deps", False) or getattr(args, "fix_all", False)
922
- prune_any = getattr(args, "prune_deps", False) or getattr(args, "fix_all", False)
924
+ fix_any = (
925
+ getattr(args, "fix_deps", False)
926
+ or getattr(args, "fix_all", False)
927
+ or getattr(args, "fix", False)
928
+ or apply_changes
929
+ )
930
+ prune_any = (
931
+ getattr(args, "prune_deps", False)
932
+ or getattr(args, "fix_all", False)
933
+ or getattr(args, "fix", False)
934
+ or apply_changes
935
+ )
923
936
  should_fix = apply_changes and fix_any
924
937
  should_prune = apply_changes and prune_any
925
938
  audit_report = auditor.audit(
@@ -1009,6 +1022,12 @@ def _cmd_fix(
1009
1022
  config=config,
1010
1023
  target_files=py_files,
1011
1024
  )
1025
+ if opts.apply_changes and (
1026
+ audit_report.fixed_requirements
1027
+ or getattr(audit_report, "fixed_pyproject", False)
1028
+ ):
1029
+ state.changed_count += 1
1030
+
1012
1031
  print_msg(
1013
1032
  f"\n[bold]Summary: {len(py_files)} inspected, {state.changed_count} updated, {state.error_count} errors.[/bold]"
1014
1033
  )
@@ -1412,8 +1431,10 @@ def _render_audit_cli_output(
1412
1431
  )
1413
1432
  if not audit_report.missing_packages and not audit_report.unused_packages:
1414
1433
  print_msg(" [green]All dependencies clean.[/green]")
1415
- if audit_report.fixed_requirements:
1416
- print_msg(" [green]requirements.txt synchronized.[/green]")
1434
+ if audit_report.fixed_requirements or getattr(
1435
+ audit_report, "fixed_pyproject", False
1436
+ ):
1437
+ print_msg(" [green]Project dependencies synchronized on disk.[/green]")
1417
1438
 
1418
1439
  return (
1419
1440
  1
@@ -1427,10 +1448,12 @@ def _cmd_audit(args: argparse.Namespace, config: PyCleanerConfig, print_msg) ->
1427
1448
  root_dir = find_project_root(args.paths[0] if args.paths else None)
1428
1449
 
1429
1450
  is_check = getattr(args, "check", False)
1451
+ fix_flag = getattr(args, "fix", False) or getattr(args, "fix_deps", False)
1452
+ prune_flag = getattr(args, "fix", False) or getattr(args, "prune_deps", False)
1430
1453
  auditor = DependencyAuditor(root_dir, exclude_patterns=config.exclude)
1431
1454
  audit_report = auditor.audit(
1432
- fix=getattr(args, "fix_deps", False) and not is_check,
1433
- prune_unused=getattr(args, "prune_deps", False) and not is_check,
1455
+ fix=fix_flag and not is_check,
1456
+ prune_unused=prune_flag and not is_check,
1434
1457
  )
1435
1458
 
1436
1459
  if getattr(args, "json", False):
@@ -1964,6 +1987,8 @@ def _run_ultimate_phases(
1964
1987
  print_msg(
1965
1988
  "\n[bold blue]Phase 2: Project Dependency Audit & Reconciliation[/bold blue]"
1966
1989
  )
1990
+ args.fix = True
1991
+ args.prune_deps = True
1967
1992
  audit_rc = _cmd_audit(args, config, print_msg)
1968
1993
  print_msg(
1969
1994
  "\n[bold blue]Phase 3: Bidirectional Type Verification & Typeshed Resolution[/bold blue]"
@@ -1981,7 +2006,7 @@ def _run_ultimate_phases(
1981
2006
  def _cmd_ultimate(
1982
2007
  args: argparse.Namespace, config: PyCleanerConfig, print_msg: Any, console: Any
1983
2008
  ) -> int:
1984
- """The Ultimate Python Tool flagship runner: executes full multi-layer analysis."""
2009
+ """The Ultimate Python Tool flagship runner: executes full multi-layer analysis and automated healing."""
1985
2010
  print_msg(
1986
2011
  "[bold magenta]=== PyCleaner Ultimate: Full Spectrum Analysis & Healing ===[/bold magenta]\n"
1987
2012
  )
@@ -1989,14 +2014,15 @@ def _cmd_ultimate(
1989
2014
  args, config, print_msg, console
1990
2015
  )
1991
2016
  print_msg(
1992
- "\n[bold blue]Phase 6: Structural Complexity & Dead Code Discovery[/bold blue]"
2017
+ "\n[bold blue]Phase 6: Structural Complexity & Dead Code Discovery & Healing[/bold blue]"
1993
2018
  )
1994
2019
  _cmd_complexity(args, config, print_msg, console)
1995
- _cmd_dead_code(args, config, print_msg, console)
2020
+ args.fix = True
2021
+ dead_rc = _cmd_dead_code(args, config, print_msg, console)
1996
2022
  print_msg(
1997
2023
  "\n[bold magenta]=== Ultimate Python Tool: Analysis Complete ===[/bold magenta]"
1998
2024
  )
1999
- return max(fix_rc, audit_rc, type_rc, taint_rc, scan_rc)
2025
+ return max(fix_rc, audit_rc, type_rc, taint_rc, scan_rc, dead_rc)
2000
2026
 
2001
2027
 
2002
2028
  def _collect_file_mtimes(files: Sequence[Path]) -> dict[Path, float]:
@@ -8,6 +8,7 @@ unreachable code after return/raise/break/continue, and empty pass branches.
8
8
  from __future__ import annotations
9
9
 
10
10
  import ast
11
+ import logging
11
12
  from collections.abc import Sequence
12
13
  from dataclasses import dataclass, field
13
14
  from pathlib import Path
@@ -201,6 +202,199 @@ class _ReferenceCollector(ast.NodeVisitor):
201
202
  self.generic_visit(node)
202
203
 
203
204
 
205
+ def _is_pure_expression(node: ast.AST | None) -> bool:
206
+ """Determine if an AST expression is pure (guaranteed free of side effects)."""
207
+ if node is None:
208
+ return True
209
+ if isinstance(node, ast.Constant):
210
+ return True
211
+ if isinstance(node, ast.Name):
212
+ return True
213
+ if isinstance(node, (ast.Tuple, ast.List, ast.Set)):
214
+ return all(_is_pure_expression(elt) for elt in node.elts)
215
+ if isinstance(node, ast.Dict):
216
+ return all(
217
+ (k is None or _is_pure_expression(k)) and _is_pure_expression(v)
218
+ for k, v in zip(node.keys, node.values)
219
+ )
220
+ if isinstance(node, ast.UnaryOp):
221
+ return _is_pure_expression(node.operand)
222
+ if isinstance(node, ast.BinOp):
223
+ return _is_pure_expression(node.left) and _is_pure_expression(node.right)
224
+ if isinstance(node, ast.BoolOp):
225
+ return all(_is_pure_expression(v) for v in node.values)
226
+ if isinstance(node, ast.Compare):
227
+ return _is_pure_expression(node.left) and all(
228
+ _is_pure_expression(c) for c in node.comparators
229
+ )
230
+ if isinstance(node, ast.JoinedStr):
231
+ for val in node.values:
232
+ if isinstance(val, ast.FormattedValue) and not _is_pure_expression(
233
+ val.value
234
+ ):
235
+ return False
236
+ return True
237
+ if isinstance(node, ast.Lambda):
238
+ return True
239
+ return False
240
+
241
+
242
+ def _walk_stmts_in_scope(body: list[ast.stmt]) -> list[tuple[ast.stmt, list[ast.stmt]]]:
243
+ """Yield (stmt, parent_body) for statements within this scope without descending into nested functions/classes."""
244
+ result: list[tuple[ast.stmt, list[ast.stmt]]] = []
245
+ for s in body:
246
+ result.append((s, body))
247
+ if isinstance(s, (ast.If, ast.While, ast.For, ast.AsyncFor)):
248
+ result.extend(_walk_stmts_in_scope(s.body))
249
+ if s.orelse:
250
+ result.extend(_walk_stmts_in_scope(s.orelse))
251
+ elif isinstance(s, ast.Try):
252
+ result.extend(_walk_stmts_in_scope(s.body))
253
+ for h in s.handlers:
254
+ result.extend(_walk_stmts_in_scope(h.body))
255
+ if s.orelse:
256
+ result.extend(_walk_stmts_in_scope(s.orelse))
257
+ if s.finalbody:
258
+ result.extend(_walk_stmts_in_scope(s.finalbody))
259
+ elif isinstance(s, (ast.With, ast.AsyncWith)):
260
+ result.extend(_walk_stmts_in_scope(s.body))
261
+ elif hasattr(ast, "Match") and isinstance(s, ast.Match):
262
+ for case in s.cases:
263
+ result.extend(_walk_stmts_in_scope(case.body))
264
+ return result
265
+
266
+
267
+ def _has_logging_import(tree: ast.AST) -> bool:
268
+ for node in ast.walk(tree):
269
+ if isinstance(node, ast.Import):
270
+ for alias in node.names:
271
+ if alias.name == "logging":
272
+ return True
273
+ elif isinstance(node, ast.ImportFrom):
274
+ if node.module and (
275
+ node.module == "logging" or node.module.startswith("logging.")
276
+ ):
277
+ return True
278
+ return False
279
+
280
+
281
+ def _detect_unused_locals_in_function(
282
+ func_node: ast.FunctionDef | ast.AsyncFunctionDef,
283
+ filepath: str,
284
+ ) -> list[DeadCodeItem]:
285
+ items: list[DeadCodeItem] = []
286
+ args_set: set[str] = set()
287
+ for a in func_node.args.posonlyargs:
288
+ args_set.add(a.arg)
289
+ for a in func_node.args.args:
290
+ args_set.add(a.arg)
291
+ for a in func_node.args.kwonlyargs:
292
+ args_set.add(a.arg)
293
+ if func_node.args.vararg:
294
+ args_set.add(func_node.args.vararg.arg)
295
+ if func_node.args.kwarg:
296
+ args_set.add(func_node.args.kwarg.arg)
297
+
298
+ explicit_globals: set[str] = set()
299
+ has_dynamic = False
300
+ loaded_names: set[str] = set()
301
+
302
+ for sub in ast.walk(func_node):
303
+ if isinstance(sub, (ast.Global, ast.Nonlocal)):
304
+ explicit_globals.update(sub.names)
305
+ elif isinstance(sub, ast.Name) and isinstance(sub.ctx, ast.Load):
306
+ loaded_names.add(sub.id)
307
+ elif isinstance(sub, ast.Call):
308
+ func = sub.func
309
+ if isinstance(func, ast.Name) and func.id in (
310
+ "locals",
311
+ "vars",
312
+ "eval",
313
+ "exec",
314
+ ):
315
+ has_dynamic = True
316
+
317
+ if has_dynamic:
318
+ return items
319
+
320
+ stmts_in_scope = _walk_stmts_in_scope(func_node.body)
321
+ seen_unused: set[str] = set()
322
+ for stmt, _ in stmts_in_scope:
323
+ if isinstance(stmt, ast.Assign):
324
+ for target in stmt.targets:
325
+ if isinstance(target, ast.Name):
326
+ name = target.id
327
+ if (
328
+ not name.startswith("_")
329
+ and name not in args_set
330
+ and name not in explicit_globals
331
+ and name not in loaded_names
332
+ and name not in seen_unused
333
+ ):
334
+ seen_unused.add(name)
335
+ items.append(
336
+ DeadCodeItem(
337
+ filepath=filepath,
338
+ lineno=target.lineno,
339
+ end_lineno=getattr(target, "end_lineno", target.lineno),
340
+ name=name,
341
+ kind="variable",
342
+ reason=f"Local variable '{name}' is assigned in '{func_node.name}' but never used",
343
+ confidence="high",
344
+ )
345
+ )
346
+ elif isinstance(target, (ast.Tuple, ast.List)):
347
+ for elt in target.elts:
348
+ if isinstance(elt, ast.Name):
349
+ name = elt.id
350
+ if (
351
+ not name.startswith("_")
352
+ and name not in args_set
353
+ and name not in explicit_globals
354
+ and name not in loaded_names
355
+ and name not in seen_unused
356
+ ):
357
+ seen_unused.add(name)
358
+ items.append(
359
+ DeadCodeItem(
360
+ filepath=filepath,
361
+ lineno=elt.lineno,
362
+ end_lineno=getattr(
363
+ elt, "end_lineno", elt.lineno
364
+ ),
365
+ name=name,
366
+ kind="variable",
367
+ reason=f"Local variable '{name}' is assigned in '{func_node.name}' but never used",
368
+ confidence="high",
369
+ )
370
+ )
371
+ elif isinstance(stmt, ast.AnnAssign):
372
+ if isinstance(stmt.target, ast.Name):
373
+ name = stmt.target.id
374
+ if (
375
+ not name.startswith("_")
376
+ and name not in args_set
377
+ and name not in explicit_globals
378
+ and name not in loaded_names
379
+ and name not in seen_unused
380
+ ):
381
+ seen_unused.add(name)
382
+ items.append(
383
+ DeadCodeItem(
384
+ filepath=filepath,
385
+ lineno=stmt.target.lineno,
386
+ end_lineno=getattr(
387
+ stmt.target, "end_lineno", stmt.target.lineno
388
+ ),
389
+ name=name,
390
+ kind="variable",
391
+ reason=f"Local variable '{name}' is assigned in '{func_node.name}' but never used",
392
+ confidence="high",
393
+ )
394
+ )
395
+ return items
396
+
397
+
204
398
  class _UnreachableCodeDetector(ast.NodeVisitor):
205
399
  """Detects code after unconditional return/raise/break/continue and empty branches."""
206
400
 
@@ -215,6 +409,7 @@ class _UnreachableCodeDetector(ast.NodeVisitor):
215
409
 
216
410
  def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
217
411
  self._check_body(node.body)
412
+ self.items.extend(_detect_unused_locals_in_function(node, self.filepath))
218
413
  self.generic_visit(node)
219
414
 
220
415
  visit_AsyncFunctionDef = visit_FunctionDef
@@ -721,38 +916,380 @@ class _DeadCodePrunerCollector(ast.NodeVisitor):
721
916
 
722
917
 
723
918
  class DeadCodeFixer:
724
- """Surgically eliminates unreachable statements, dead branches, and redundant pass statements."""
919
+ """Surgically eliminates unreachable statements, dead branches, heals empty except blocks, and fixes unused variables."""
725
920
 
726
921
  def fix(self, source: str, filename: str = "<stdin>") -> DeadCodeFixResult:
727
922
  current_code = source
728
923
  all_pruned: list[str] = []
729
924
 
730
- for _ in range(2):
925
+ for _ in range(3):
926
+ changed_this_pass = False
927
+
928
+ # --- Pass 1: Empty Except Healer ---
731
929
  try:
732
930
  tree = ast.parse(current_code, filename=filename)
733
931
  except SyntaxError:
734
932
  break
735
933
 
736
- collector = _DeadCodePrunerCollector()
737
- collector.visit(tree)
738
- if not collector.deletions:
934
+ except_handlers_to_heal: list[tuple[int, int]] = []
935
+ for node in ast.walk(tree):
936
+ if isinstance(node, ast.ExceptHandler):
937
+ if len(node.body) == 1 and isinstance(node.body[0], ast.Pass):
938
+ pass_node = node.body[0]
939
+ except_handlers_to_heal.append((node.lineno, pass_node.lineno))
940
+
941
+ if except_handlers_to_heal:
942
+ lines = current_code.splitlines(keepends=True)
943
+ needs_logging_import = not _has_logging_import(tree)
944
+
945
+ # Sort by line number descending so replacements do not shift earlier lines
946
+ for handler_line, pass_line in sorted(
947
+ except_handlers_to_heal, key=lambda x: x[1], reverse=True
948
+ ):
949
+ idx = pass_line - 1
950
+ if 0 <= idx < len(lines):
951
+ orig_line = lines[idx]
952
+ indent = orig_line[: len(orig_line) - len(orig_line.lstrip())]
953
+ lines[idx] = (
954
+ f"{indent}logging.getLogger(__name__).debug("
955
+ f"'Suppressed exception', exc_info=True)\n"
956
+ )
957
+ all_pruned.append(
958
+ f"Healed empty except block at line {handler_line} with debug logging"
959
+ )
960
+ changed_this_pass = True
961
+
962
+ if needs_logging_import and changed_this_pass:
963
+ insert_idx = 0
964
+ while insert_idx < len(lines) and (
965
+ lines[insert_idx].startswith("#!")
966
+ or "coding:" in lines[insert_idx]
967
+ or "coding=" in lines[insert_idx]
968
+ ):
969
+ insert_idx += 1
970
+
971
+ doc_node = (
972
+ tree.body[0]
973
+ if tree.body
974
+ and isinstance(tree.body[0], ast.Expr)
975
+ and isinstance(tree.body[0].value, ast.Constant)
976
+ and isinstance(tree.body[0].value.value, str)
977
+ else None
978
+ )
979
+ if (
980
+ doc_node
981
+ and hasattr(doc_node, "end_lineno")
982
+ and doc_node.end_lineno is not None
983
+ ):
984
+ insert_idx = max(insert_idx, doc_node.end_lineno)
985
+
986
+ for stmt in tree.body:
987
+ if (
988
+ isinstance(stmt, ast.ImportFrom)
989
+ and stmt.module == "__future__"
990
+ and hasattr(stmt, "end_lineno")
991
+ and stmt.end_lineno is not None
992
+ ):
993
+ insert_idx = max(insert_idx, stmt.end_lineno)
994
+
995
+ lines.insert(insert_idx, "import logging\n")
996
+ all_pruned.append("Added 'import logging' for healed except block")
997
+
998
+ candidate = "".join(lines)
999
+ try:
1000
+ ast.parse(candidate, filename=filename)
1001
+ current_code = candidate
1002
+ except SyntaxError:
1003
+ logging.getLogger(__name__).debug(
1004
+ "Suppressed exception", exc_info=True
1005
+ )
1006
+
1007
+ # --- Pass 2: Dead Variable Fixer ---
1008
+ try:
1009
+ tree = ast.parse(current_code, filename=filename)
1010
+ except SyntaxError:
739
1011
  break
740
1012
 
741
- lines = current_code.splitlines(keepends=True)
742
- sorted_deletions = sorted(
743
- collector.deletions, key=lambda x: x[0], reverse=True
744
- )
745
- for start_line, end_line, desc in sorted_deletions:
746
- del lines[start_line - 1 : end_line]
747
- all_pruned.append(desc)
1013
+ renames: list[tuple[int, int, int, str, str]] = []
1014
+ deletions: list[tuple[int, int, list[ast.stmt], str]] = []
1015
+
1016
+ # 2a. Function-level local variables
1017
+ for node in ast.walk(tree):
1018
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
1019
+ args_set: set[str] = set()
1020
+ for a in node.args.posonlyargs:
1021
+ args_set.add(a.arg)
1022
+ for a in node.args.args:
1023
+ args_set.add(a.arg)
1024
+ for a in node.args.kwonlyargs:
1025
+ args_set.add(a.arg)
1026
+ if node.args.vararg:
1027
+ args_set.add(node.args.vararg.arg)
1028
+ if node.args.kwarg:
1029
+ args_set.add(node.args.kwarg.arg)
1030
+
1031
+ explicit_globals: set[str] = set()
1032
+ has_dynamic = False
1033
+ loaded_names: set[str] = set()
1034
+
1035
+ for sub in ast.walk(node):
1036
+ if isinstance(sub, (ast.Global, ast.Nonlocal)):
1037
+ explicit_globals.update(sub.names)
1038
+ elif isinstance(sub, ast.Name) and isinstance(
1039
+ sub.ctx, ast.Load
1040
+ ):
1041
+ loaded_names.add(sub.id)
1042
+ elif isinstance(sub, ast.Call):
1043
+ func = sub.func
1044
+ if isinstance(func, ast.Name) and func.id in (
1045
+ "locals",
1046
+ "vars",
1047
+ "eval",
1048
+ "exec",
1049
+ ):
1050
+ has_dynamic = True
1051
+
1052
+ if has_dynamic:
1053
+ continue
748
1054
 
749
- candidate = "".join(lines)
1055
+ stmts_in_scope = _walk_stmts_in_scope(node.body)
1056
+ for stmt, parent_body in stmts_in_scope:
1057
+ if isinstance(stmt, ast.Assign):
1058
+ if len(stmt.targets) == 1 and isinstance(
1059
+ stmt.targets[0], ast.Name
1060
+ ):
1061
+ target = stmt.targets[0]
1062
+ name = target.id
1063
+ if (
1064
+ not name.startswith("_")
1065
+ and name not in args_set
1066
+ and name not in explicit_globals
1067
+ and name not in loaded_names
1068
+ ):
1069
+ if _is_pure_expression(stmt.value):
1070
+ deletions.append(
1071
+ (
1072
+ stmt.lineno,
1073
+ getattr(
1074
+ stmt, "end_lineno", stmt.lineno
1075
+ ),
1076
+ parent_body,
1077
+ name,
1078
+ )
1079
+ )
1080
+ else:
1081
+ if hasattr(target, "col_offset") and hasattr(
1082
+ target, "end_col_offset"
1083
+ ):
1084
+ renames.append(
1085
+ (
1086
+ target.lineno,
1087
+ target.col_offset,
1088
+ target.end_col_offset,
1089
+ f"_{name}",
1090
+ name,
1091
+ )
1092
+ )
1093
+ elif len(stmt.targets) == 1 and isinstance(
1094
+ stmt.targets[0], (ast.Tuple, ast.List)
1095
+ ):
1096
+ for elt in stmt.targets[0].elts:
1097
+ if isinstance(elt, ast.Name):
1098
+ name = elt.id
1099
+ if (
1100
+ not name.startswith("_")
1101
+ and name not in args_set
1102
+ and name not in explicit_globals
1103
+ and name not in loaded_names
1104
+ and hasattr(elt, "col_offset")
1105
+ and hasattr(elt, "end_col_offset")
1106
+ ):
1107
+ renames.append(
1108
+ (
1109
+ elt.lineno,
1110
+ elt.col_offset,
1111
+ elt.end_col_offset,
1112
+ f"_{name}",
1113
+ name,
1114
+ )
1115
+ )
1116
+ elif isinstance(stmt, ast.AnnAssign):
1117
+ if isinstance(stmt.target, ast.Name):
1118
+ name = stmt.target.id
1119
+ if (
1120
+ not name.startswith("_")
1121
+ and name not in args_set
1122
+ and name not in explicit_globals
1123
+ and name not in loaded_names
1124
+ ):
1125
+ if stmt.value is not None and _is_pure_expression(
1126
+ stmt.value
1127
+ ):
1128
+ deletions.append(
1129
+ (
1130
+ stmt.lineno,
1131
+ getattr(
1132
+ stmt, "end_lineno", stmt.lineno
1133
+ ),
1134
+ parent_body,
1135
+ name,
1136
+ )
1137
+ )
1138
+ else:
1139
+ target = stmt.target
1140
+ if hasattr(target, "col_offset") and hasattr(
1141
+ target, "end_col_offset"
1142
+ ):
1143
+ renames.append(
1144
+ (
1145
+ target.lineno,
1146
+ target.col_offset,
1147
+ target.end_col_offset,
1148
+ f"_{name}",
1149
+ name,
1150
+ )
1151
+ )
1152
+
1153
+ # 2b. Module-level script variables (when not an __init__.py package export)
1154
+ if not filename.endswith("__init__.py"):
1155
+ module_loads: set[str] = set()
1156
+ module_exports: set[str] = set()
1157
+ for sub in ast.walk(tree):
1158
+ if isinstance(sub, ast.Name) and isinstance(sub.ctx, ast.Load):
1159
+ module_loads.add(sub.id)
1160
+ elif (
1161
+ isinstance(sub, ast.Assign)
1162
+ and any(
1163
+ isinstance(t, ast.Name) and t.id == "__all__"
1164
+ for t in sub.targets
1165
+ )
1166
+ and isinstance(sub.value, (ast.List, ast.Tuple, ast.Set))
1167
+ ):
1168
+ for elt in sub.value.elts:
1169
+ if isinstance(elt, ast.Constant) and isinstance(
1170
+ elt.value, str
1171
+ ):
1172
+ module_exports.add(elt.value)
1173
+
1174
+ for stmt in tree.body:
1175
+ if (
1176
+ isinstance(stmt, ast.Assign)
1177
+ and len(stmt.targets) == 1
1178
+ and isinstance(stmt.targets[0], ast.Name)
1179
+ ):
1180
+ target = stmt.targets[0]
1181
+ name = target.id
1182
+ if (
1183
+ not name.startswith("_")
1184
+ and name != "__all__"
1185
+ and name not in module_loads
1186
+ and name not in module_exports
1187
+ ):
1188
+ if _is_pure_expression(stmt.value):
1189
+ deletions.append(
1190
+ (
1191
+ stmt.lineno,
1192
+ getattr(stmt, "end_lineno", stmt.lineno),
1193
+ tree.body,
1194
+ name,
1195
+ )
1196
+ )
1197
+ else:
1198
+ if hasattr(target, "col_offset") and hasattr(
1199
+ target, "end_col_offset"
1200
+ ):
1201
+ renames.append(
1202
+ (
1203
+ target.lineno,
1204
+ target.col_offset,
1205
+ target.end_col_offset,
1206
+ f"_{name}",
1207
+ name,
1208
+ )
1209
+ )
1210
+
1211
+ if renames or deletions:
1212
+ lines = current_code.splitlines(keepends=True)
1213
+
1214
+ # 1. Apply renames line-by-line, ordered by column descending
1215
+ renames_by_line: dict[int, list[tuple[int, int, str, str]]] = {}
1216
+ for lineno, col, end_col, new_text, name in renames:
1217
+ renames_by_line.setdefault(lineno, []).append(
1218
+ (col, end_col, new_text, name)
1219
+ )
1220
+
1221
+ for lineno, line_renames in renames_by_line.items():
1222
+ idx = lineno - 1
1223
+ if 0 <= idx < len(lines):
1224
+ line_str = lines[idx]
1225
+ for col, end_col, new_text, name in sorted(
1226
+ line_renames, key=lambda x: x[0], reverse=True
1227
+ ):
1228
+ line_str = line_str[:col] + new_text + line_str[end_col:]
1229
+ all_pruned.append(
1230
+ f"Prefixed unused variable '{name}' with '_' at line {lineno} to preserve side effects"
1231
+ )
1232
+ changed_this_pass = True
1233
+ lines[idx] = line_str
1234
+
1235
+ # 2. Apply deletions ordered by start line descending
1236
+ for start_line, end_line, parent_body, name in sorted(
1237
+ deletions, key=lambda x: x[0], reverse=True
1238
+ ):
1239
+ start_idx = start_line - 1
1240
+ if 0 <= start_idx < len(lines):
1241
+ if len(parent_body) == 1:
1242
+ orig_line = lines[start_idx]
1243
+ indent = orig_line[
1244
+ : len(orig_line) - len(orig_line.lstrip())
1245
+ ]
1246
+ lines[start_idx:end_line] = [f"{indent}pass\n"]
1247
+ else:
1248
+ del lines[start_idx:end_line]
1249
+ all_pruned.append(
1250
+ f"Pruned unused local variable '{name}' assignment at line {start_line}"
1251
+ )
1252
+ changed_this_pass = True
1253
+
1254
+ candidate = "".join(lines)
1255
+ try:
1256
+ ast.parse(candidate, filename=filename)
1257
+ current_code = candidate
1258
+ except SyntaxError:
1259
+ logging.getLogger(__name__).debug(
1260
+ "Suppressed exception", exc_info=True
1261
+ )
1262
+
1263
+ # --- Pass 3: Unreachable Code & Redundant Pass ---
750
1264
  try:
751
- ast.parse(candidate, filename=filename)
752
- current_code = candidate
1265
+ tree = ast.parse(current_code, filename=filename)
753
1266
  except SyntaxError:
754
1267
  break
755
1268
 
1269
+ collector = _DeadCodePrunerCollector()
1270
+ collector.visit(tree)
1271
+ if collector.deletions:
1272
+ lines = current_code.splitlines(keepends=True)
1273
+ sorted_deletions = sorted(
1274
+ collector.deletions, key=lambda x: x[0], reverse=True
1275
+ )
1276
+ for start_line, end_line, desc in sorted_deletions:
1277
+ del lines[start_line - 1 : end_line]
1278
+ all_pruned.append(desc)
1279
+ changed_this_pass = True
1280
+
1281
+ candidate = "".join(lines)
1282
+ try:
1283
+ ast.parse(candidate, filename=filename)
1284
+ current_code = candidate
1285
+ except SyntaxError:
1286
+ logging.getLogger(__name__).debug(
1287
+ "Suppressed exception", exc_info=True
1288
+ )
1289
+
1290
+ if not changed_this_pass:
1291
+ break
1292
+
756
1293
  return DeadCodeFixResult(
757
1294
  code=current_code,
758
1295
  changed=current_code != source,
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import ast
12
12
  import importlib.metadata
13
+ import logging
13
14
  import re
14
15
  import shutil
15
16
  import sys
@@ -31,6 +32,7 @@ class DependencyAuditReport:
31
32
  missing_packages: set[str] = field(default_factory=set)
32
33
  unused_packages: set[str] = field(default_factory=set)
33
34
  fixed_requirements: bool = False
35
+ fixed_pyproject: bool = False
34
36
  details: list[str] = field(default_factory=list)
35
37
 
36
38
 
@@ -99,7 +101,7 @@ class DependencyAuditor:
99
101
  try:
100
102
  dists.update(importlib.metadata.packages_distributions())
101
103
  except AttributeError:
102
- pass
104
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
103
105
 
104
106
  site_packages = self._find_venv_site_packages()
105
107
  if site_packages and site_packages.is_dir():
@@ -116,9 +118,11 @@ class DependencyAuditor:
116
118
  if top_mod:
117
119
  dists.setdefault(top_mod, []).insert(0, dist_name)
118
120
  except OSError:
119
- pass
121
+ logging.getLogger(__name__).debug(
122
+ "Suppressed exception", exc_info=True
123
+ )
120
124
  except OSError:
121
- pass
125
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
122
126
 
123
127
  return dists
124
128
 
@@ -226,7 +230,7 @@ class DependencyAuditor:
226
230
  local_mods.add(str(proj_name))
227
231
  local_mods.add(str(proj_name).replace("-", "_"))
228
232
  except Exception:
229
- pass # Ignore invalid or non-standard pyproject.toml configuration
233
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
230
234
 
231
235
  # 3. Discover local .py files and subpackages (including PEP 420 namespace packages)
232
236
  search_dirs = [self.root_dir]
@@ -331,7 +335,7 @@ class DependencyAuditor:
331
335
  declared[canon] = dep
332
336
  optional_pkgs.add(canon)
333
337
  except Exception:
334
- pass # Best-effort reading; continue if pyproject.toml is unparseable
338
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
335
339
  return declared, optional_pkgs
336
340
 
337
341
  def read_declared_dependencies(self) -> dict[str, str]:
@@ -374,7 +378,10 @@ class DependencyAuditor:
374
378
 
375
379
  @staticmethod
376
380
  def _build_audit_details(
377
- missing: set[str], unused: set[str], fixed: bool
381
+ missing: set[str],
382
+ unused: set[str],
383
+ fixed_reqs: bool,
384
+ fixed_pyproj: bool = False,
378
385
  ) -> list[str]:
379
386
  details: list[str] = []
380
387
  if missing:
@@ -385,10 +392,14 @@ class DependencyAuditor:
385
392
  details.append(
386
393
  f"Unused dependencies (declared but not imported): {', '.join(sorted(unused))}"
387
394
  )
388
- if fixed:
395
+ if fixed_reqs:
389
396
  details.append(
390
397
  "Updated requirements.txt successfully (backup saved as requirements.txt.bak)"
391
398
  )
399
+ if fixed_pyproj:
400
+ details.append(
401
+ "Updated pyproject.toml successfully (backup saved as pyproject.toml.bak)"
402
+ )
392
403
  return details
393
404
 
394
405
  def audit(
@@ -397,7 +408,7 @@ class DependencyAuditor:
397
408
  prune_unused: bool = False,
398
409
  target_files: Sequence[Path] | None = None,
399
410
  ) -> DependencyAuditReport:
400
- """Audit dependencies and optionally update requirements.txt."""
411
+ """Audit dependencies and optionally update requirements.txt and pyproject.toml."""
401
412
  all_imports = self.scan_codebase_imports(target_files=target_files)
402
413
  local_mods = self.identify_local_modules()
403
414
 
@@ -414,13 +425,18 @@ class DependencyAuditor:
414
425
  )
415
426
 
416
427
  fixed_reqs = False
428
+ fixed_pyproj = False
417
429
  if fix and (missing_packages or (prune_unused and unused_packages)):
418
- fixed_reqs = self._update_requirements(
419
- missing_packages, unused_packages if prune_unused else set()
420
- )
430
+ to_remove = unused_packages if prune_unused else set()
431
+ if (self.root_dir / "pyproject.toml").is_file():
432
+ fixed_pyproj = self._update_pyproject_toml(missing_packages, to_remove)
433
+ if (self.root_dir / "requirements.txt").is_file():
434
+ fixed_reqs = self._update_requirements(missing_packages, to_remove)
435
+ elif not fixed_pyproj and missing_packages:
436
+ fixed_reqs = self._update_requirements(missing_packages, set())
421
437
 
422
438
  details = self._build_audit_details(
423
- missing_packages, unused_packages, fixed_reqs
439
+ missing_packages, unused_packages, fixed_reqs, fixed_pyproj
424
440
  )
425
441
 
426
442
  return DependencyAuditReport(
@@ -429,10 +445,109 @@ class DependencyAuditor:
429
445
  required_packages=set(declared.keys()),
430
446
  missing_packages=missing_packages,
431
447
  unused_packages=unused_packages,
432
- fixed_requirements=fixed_reqs,
448
+ fixed_requirements=fixed_reqs or fixed_pyproj,
449
+ fixed_pyproject=fixed_pyproj,
433
450
  details=details,
434
451
  )
435
452
 
453
+ def _update_pyproject_toml(
454
+ self, missing: set[str], unused_to_remove: set[str]
455
+ ) -> bool:
456
+ """Prune unused dependencies and append missing ones in pyproject.toml with safety checks."""
457
+ pyproject_file = self.root_dir / "pyproject.toml"
458
+ if not pyproject_file.is_file():
459
+ return False
460
+
461
+ content = pyproject_file.read_text(encoding="utf-8", errors="ignore")
462
+ remove_canons = {
463
+ self.canonicalize_name(m.group(1))
464
+ for u in unused_to_remove
465
+ if (m := re.match(r"^([a-zA-Z0-9_.-]+)", u))
466
+ }
467
+ local_canons = {
468
+ self.canonicalize_name(m) for m in self.identify_local_modules()
469
+ }
470
+ safe_missing = {
471
+ pkg for pkg in missing if self.canonicalize_name(pkg) not in local_canons
472
+ }
473
+
474
+ if not remove_canons and not safe_missing:
475
+ return False
476
+
477
+ lines = content.splitlines(keepends=True)
478
+ new_lines: list[str] = []
479
+ in_dependencies_array = False
480
+ modified = False
481
+
482
+ for line in lines:
483
+ stripped = line.strip()
484
+
485
+ if re.match(r"^dependencies\s*=\s*\[", stripped):
486
+ if "]" in stripped:
487
+ bracket_content = stripped[
488
+ stripped.find("[") + 1 : stripped.rfind("]")
489
+ ]
490
+ items = [
491
+ it.strip() for it in bracket_content.split(",") if it.strip()
492
+ ]
493
+ kept_items: list[str] = []
494
+ for item in items:
495
+ m = re.search(r'["\']([a-zA-Z0-9_.-]+)', item)
496
+ if m and self.canonicalize_name(m.group(1)) in remove_canons:
497
+ modified = True
498
+ continue
499
+ kept_items.append(item)
500
+ for sm in sorted(safe_missing):
501
+ kept_items.append(f'"{sm}"')
502
+ modified = True
503
+ indent = line[: len(line) - len(line.lstrip())]
504
+ new_lines.append(
505
+ f"{indent}dependencies = [{', '.join(kept_items)}]\n"
506
+ )
507
+ continue
508
+ else:
509
+ in_dependencies_array = True
510
+ new_lines.append(line)
511
+ continue
512
+
513
+ if in_dependencies_array:
514
+ if "]" in stripped:
515
+ in_dependencies_array = False
516
+ for sm in sorted(safe_missing):
517
+ indent = " "
518
+ new_lines.append(f'{indent}"{sm}",\n')
519
+ modified = True
520
+ new_lines.append(line)
521
+ continue
522
+
523
+ pkg_match = re.search(r'["\']([a-zA-Z0-9_.-]+)', stripped)
524
+ if pkg_match:
525
+ canon = self.canonicalize_name(pkg_match.group(1))
526
+ if canon in remove_canons:
527
+ modified = True
528
+ continue
529
+ new_lines.append(line)
530
+ continue
531
+
532
+ poetry_pkg_match = re.match(r"^([a-zA-Z0-9_.-]+)\s*=", stripped)
533
+ if poetry_pkg_match:
534
+ canon = self.canonicalize_name(poetry_pkg_match.group(1))
535
+ if canon in remove_canons and canon != "python":
536
+ modified = True
537
+ continue
538
+
539
+ new_lines.append(line)
540
+
541
+ if modified:
542
+ backup_file = pyproject_file.with_suffix(".toml.bak")
543
+ try:
544
+ shutil.copy2(pyproject_file, backup_file)
545
+ except OSError:
546
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
547
+ pyproject_file.write_text("".join(new_lines), encoding="utf-8")
548
+ return True
549
+ return False
550
+
436
551
  def _update_requirements(
437
552
  self, missing: set[str], unused_to_remove: set[str]
438
553
  ) -> bool:
@@ -445,7 +560,7 @@ class DependencyAuditor:
445
560
  try:
446
561
  shutil.copy2(req_file, backup_file)
447
562
  except OSError:
448
- pass # Continue if backup creation fails
563
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
449
564
  existing_lines = req_file.read_text(
450
565
  encoding="utf-8", errors="ignore"
451
566
  ).splitlines()
@@ -9,6 +9,7 @@ fallbacks via autoflake, isort, black, and built-in pure-Python AST pruning.
9
9
  from __future__ import annotations
10
10
 
11
11
  import ast
12
+ import logging
12
13
  import re
13
14
  import shutil
14
15
  import subprocess
@@ -169,7 +170,7 @@ class LinterFormatter:
169
170
  return output, True
170
171
  except OSError:
171
172
  # Fall back to pure-Python import pruning if autoflake CLI fails
172
- pass
173
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
173
174
  return source, False
174
175
 
175
176
  def _isort_cli_sort(self, code: str) -> str | None:
@@ -187,7 +188,7 @@ class LinterFormatter:
187
188
  return res if res != code else None
188
189
  except OSError:
189
190
  # Fall back to isort Python module or pure-Python import sorter
190
- pass
191
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
191
192
  return None
192
193
 
193
194
  def _isort_module_sort(self, code: str) -> str | None:
@@ -269,7 +270,7 @@ class LinterFormatter:
269
270
  return res, res != source
270
271
  except OSError:
271
272
  # Fall back to Black or pure-Python formatter if ruff CLI execution fails
272
- pass
273
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
273
274
  return None
274
275
 
275
276
  def _format_with_black(self, source: str) -> tuple[str, bool, list[str]] | None:
@@ -286,7 +287,7 @@ class LinterFormatter:
286
287
  return res, res != source, ["Formatted with black CLI fallback"]
287
288
  except OSError:
288
289
  # Fall back to black module or pure-Python formatter
289
- pass
290
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
290
291
  try:
291
292
  import black # type: ignore
292
293
 
@@ -443,7 +444,7 @@ class LinterFormatter:
443
444
  )
444
445
  except SyntaxError:
445
446
  # Return unmodified source if candidate code has syntax issues
446
- pass
447
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
447
448
  return source, False, []
448
449
 
449
450
  def _pure_python_sort_imports(self, source: str) -> tuple[str, bool, list[str]]:
pycleaner/modernizer.py CHANGED
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
 
15
15
  import ast
16
16
  import io
17
+ import logging
17
18
  import re
18
19
  import tokenize
19
20
  from dataclasses import dataclass, field
@@ -414,7 +415,7 @@ class Modernizer:
414
415
  ):
415
416
  insert_idx = getattr(tree.body[0], "end_lineno", 1)
416
417
  except SyntaxError:
417
- pass # Fall back to default index if docstring extraction fails
418
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
418
419
 
419
420
  future_stmt = "from __future__ import annotations\n\n"
420
421
  if insert_idx < len(lines):
@@ -9,6 +9,7 @@ and assert statements used for input validation.
9
9
  from __future__ import annotations
10
10
 
11
11
  import ast
12
+ import logging
12
13
  import re
13
14
  from collections.abc import Sequence
14
15
  from dataclasses import dataclass, field
@@ -524,7 +525,7 @@ class SecurityScanner:
524
525
  findings.extend(detector.findings)
525
526
  except SyntaxError:
526
527
  # Code with syntax errors cannot be AST-parsed; regex checks still run
527
- pass
528
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
528
529
 
529
530
  findings.extend(self._detect_secrets(source, filename))
530
531
 
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import ast
11
11
  import io
12
+ import logging
12
13
  import re
13
14
  import tokenize
14
15
  from collections.abc import Sequence
@@ -695,7 +696,7 @@ class SyntaxHealer:
695
696
  stack.pop()
696
697
  except tokenize.TokenError:
697
698
  # Incomplete token stream will be healed by delimiter reconstruction
698
- pass
699
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
699
700
  return stack
700
701
 
701
702
  @staticmethod
pycleaner/verifier.py CHANGED
@@ -15,6 +15,7 @@ import ast
15
15
  import dataclasses
16
16
  import enum
17
17
  import inspect
18
+ import logging
18
19
  import multiprocessing as mp
19
20
  import random
20
21
  import sys
@@ -211,12 +212,12 @@ def _worker_differential_fuzz(
211
212
  }
212
213
  )
213
214
  except Exception:
214
- pass # Suppress secondary exceptions if IPC pipe fails to send crash reason
215
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
215
216
  finally:
216
217
  try:
217
218
  conn.close()
218
219
  except Exception:
219
- pass # Best-effort resource cleanup for IPC connection
220
+ logging.getLogger(__name__).debug("Suppressed exception", exc_info=True)
220
221
 
221
222
 
222
223
  class DeterministicInputSynthesizer: