"""AST-level boundary condition injection for Python.""" import ast from typing import Optional from app.dataset.rules.base import Mutation, MutationRule class BoundaryErrorRule(MutationRule): """Mutate a list-index boundary check from `< len(seq)` to `<= len(seq)`. Uses `ast` to find comparisons guarding index access and flips the operator so the boundary becomes off-by-one. """ name = "boundary_error" language = "python" defect_type = "boundary_condition_error" 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.If): continue test = node.test if isinstance(test, ast.Compare) and isinstance(test.left, ast.Name): if len(test.ops) == 1 and isinstance(test.ops[0], ast.Lt): # i < len(x) -> i <= len(x) comparator = test.comparators[0] if isinstance(comparator, ast.Call) and isinstance(comparator.func, ast.Name) and comparator.func.id == "len": lines = source.splitlines(keepends=True) line = lines[node.lineno - 1] mutated_line = line.replace("< len(", "<= len(", 1) if mutated_line == line: continue mutated = "".join(lines[: node.lineno - 1] + [mutated_line] + lines[node.lineno:]) return Mutation( defect_type=self.defect_type, language=self.language, line_start=node.lineno, line_end=getattr(node, "end_lineno", node.lineno) or node.lineno, mutated_source=mutated, reference_fix="Use strict `< len(seq)` to avoid index-out-of-range.", description="Changed index boundary check to off-by-one (<= len).", ) return None