"""AST-level resource leak injection for Python.""" import ast from typing import Optional from app.dataset.rules.base import Mutation, MutationRule class ResourceLeakRule(MutationRule): """Convert a `with open(...)` block into an unclosed `open(...).read()`. Uses `ast` to locate a with-statement managing a file resource and replaces it with a direct call chain that leaks the file handle. """ name = "resource_leak" language = "python" defect_type = "resource_not_closed" def detect_and_mutate(self, source: str, filename: str = "") -> Optional[Mutation]: try: tree = ast.parse(source) except SyntaxError: return None for node in ast.walk(tree): if not isinstance(node, ast.With): continue first_item = node.items[0] ctx = first_item.context_expr if isinstance(ctx, ast.Call) and isinstance(ctx.func, ast.Name) and ctx.func.id == "open": start = node.lineno end = getattr(node, "end_lineno", node.lineno) or node.lineno lines = source.splitlines(keepends=True) # Keep the with header expression but replace 'with open(...)' by 'f = open(...)' header = lines[start - 1] header_expr = header.split("with ", 1)[1].split(" as ", 1)[0].strip().rstrip(":\n") var = header.split(" as ", 1)[1].strip().rstrip(":\n") if " as " in header else "f" body = lines[start:end] dedented = [] for line in body[1:]: if line.startswith(" "): dedented.append(line[4:]) else: dedented.append(line) replacement = [f"{var} = {header_expr}\n"] + dedented mutated = "".join(lines[: start - 1] + replacement + lines[end:]) return Mutation( defect_type=self.defect_type, language=self.language, line_start=start, line_end=end, mutated_source=mutated, reference_fix="Use `with open(...) as f:` to ensure the file is closed.", description="Replaced context-managed open() with an unclosed file handle.", ) return None