"""AST-level concurrency issue injection for JavaScript using esprima.""" from typing import Optional import esprima from app.dataset.rules.base import Mutation, MutationRule class JSConcurrencyRule(MutationRule): """Remove an `await mutex.acquire()` / `mutex.release()` pair. Uses `esprima` to locate a try block followed by a finally that releases a mutex and removes the finally/release, exposing a race. """ name = "js_concurrency" language = "javascript" defect_type = "concurrency_issue" 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 != "TryStatement" or not node.finalizer: continue start = node.loc.start.line end = node.finalizer.loc.end.line lines = source.splitlines(keepends=True) # Drop the entire finally block; the try block ends on the same line as finally starts finally_start = node.finalizer.loc.start.line - 1 mutated = "".join(lines[:finally_start] + [" }\n"] + lines[end:]) return Mutation( defect_type=self.defect_type, language=self.language, line_start=start, line_end=end, mutated_source=mutated, reference_fix="Restore mutex release in finally to protect the critical section.", description="Removed mutex release, exposing a race condition.", ) return None