#!/usr/bin/env python3
"""
Pre-commit hook to detect silent exception swallowing.

Flags ``except Exception: pass`` and ``except: pass`` patterns where no
logging, re-raise, or meaningful handling occurs. At minimum a
``logger.debug()`` should be present so failures are traceable.

Legitimate suppression (e.g. optional cleanup, best-effort parsing)
should use an inline ``# noqa: silent-exception`` comment to opt out.
"""

import ast
import sys


NOQA_MARKER = "noqa: silent-exception"


class SilentExceptionChecker(ast.NodeVisitor):
    """AST visitor that flags except handlers whose body is only ``pass``."""

    def __init__(self, filename: str, lines: list[str]):
        self.filename = filename
        self.lines = lines
        self.issues: list[tuple[int, str]] = []

    def visit_ExceptHandler(self, node: ast.ExceptHandler):
        # Only flag broad catches: bare ``except:`` or ``except Exception:``
        if node.type is not None and not (
            isinstance(node.type, ast.Name) and node.type.id == "Exception"
        ):
            self.generic_visit(node)
            return

        if self._is_silent(node) and not self._has_noqa(node):
            kind = "except:" if node.type is None else "except Exception:"
            self.issues.append(
                (
                    node.lineno,
                    f"Silent `{kind} pass` — add at least "
                    f"`logger.debug(...)` or `# {NOQA_MARKER}` to suppress",
                )
            )

        self.generic_visit(node)

    # ------------------------------------------------------------------

    @staticmethod
    def _is_silent(handler: ast.ExceptHandler) -> bool:
        """True when the handler body contains only ``pass`` statements."""
        for stmt in handler.body:
            if isinstance(stmt, ast.Pass):
                continue
            # Any raise, call, assignment, etc. counts as handling
            return False
        return True

    def _has_noqa(self, handler: ast.ExceptHandler) -> bool:
        """True when the ``except`` or handler body lines have a noqa comment."""
        # Check the except line itself and all body lines
        for node in [handler] + handler.body:
            idx = node.lineno - 1
            if 0 <= idx < len(self.lines):
                if NOQA_MARKER in self.lines[idx]:
                    return True
        return False


def check_file(filepath: str) -> list[tuple[int, str]]:
    try:
        with open(filepath, encoding="utf-8") as f:
            source = f.read()
    except Exception as exc:
        return [(0, f"Cannot read file: {exc}")]

    try:
        tree = ast.parse(source, filename=filepath)
    except SyntaxError:
        return []

    lines = source.splitlines()
    checker = SilentExceptionChecker(filepath, lines)
    checker.visit(tree)
    return checker.issues


def main() -> int:
    exit_code = 0
    for filepath in sys.argv[1:]:
        for lineno, msg in check_file(filepath):
            print(f"{filepath}:{lineno}: {msg}")
            exit_code = 1

    if exit_code:
        print()
        print(
            "Hint: add logging (logger.debug/warning) or "
            f"suppress with `# {NOQA_MARKER}`"
        )

    return exit_code


if __name__ == "__main__":
    sys.exit(main())
