"""AST-level null-pointer injection for JavaScript using esprima.""" from typing import Optional import esprima from app.dataset.rules.base import Mutation, MutationRule class JSNoneReferenceRule(MutationRule): """Remove a `if (x !== null)` guard in JavaScript. Uses the Python port of `esprima` to locate the guard statement and replaces it with the body, leaving a potential null dereference. """ name = "js_none_reference" language = "javascript" defect_type = "null_pointer" def detect_and_mutate(self, source: str, filename: str = "") -> Optional[Mutation]: # Modern JS is usually ESM: try module grammar first, then script. try: tree = esprima.parseModule(source, loc=True) except Exception: try: tree = esprima.parseScript(source, loc=True) except Exception: return None def walk(node): yield node for key in node.__dict__: child = getattr(node, key) if isinstance(child, list): for item in child: if hasattr(item, "type"): yield from walk(item) elif hasattr(child, "type"): yield from walk(child) for node in walk(tree): if node.type != "IfStatement": continue cond = node.test if ( cond.type == "BinaryExpression" and cond.operator == "!==" and cond.right.type == "Literal" and cond.right.value is None ): var_name = getattr(cond.left, "name", str(cond.left)) start = node.loc.start.line end = node.consequent.loc.end.line lines = source.splitlines(keepends=True) # Drop guard header and closing brace, keep body (1-based -> 0-based) body_start = node.consequent.loc.start.line body_end = node.consequent.loc.end.line - 1 body_lines = lines[body_start:body_end] dedented = [] for line in body_lines: if line.startswith(" "): dedented.append(line[4:]) else: dedented.append(line) mutated = "".join(lines[: start - 1] + dedented + lines[end:]) return Mutation( defect_type=self.defect_type, language=self.language, line_start=start, line_end=end, mutated_source=mutated, reference_fix=f"Add `if ({var_name} !== null)` guard before dereferencing.", description=f"Removed null-check guard for '{var_name}'.", ) return None