"""Shared diff-to-patch-lines logic (spec §3.2's `patch_lines_vuln`), used by every source loader so patch line numbers are always relative to the extracted function body itself — never to the original file's absolute line numbers. See DECISIONS.md "SecVulEval patch-line numbers were file-absolute, not function-relative" for why this needed to be a shared, source-agnostic computation rather than trusting any one dataset's own line-number field. """ from __future__ import annotations import difflib def diff_and_patch_lines(func_vuln: str, func_patch: str) -> tuple[str, list[int]]: """Unified diff of the function pair + 1-based line numbers in func_vuln that were removed/changed by the patch (spec §3.2 `patch_lines_vuln`). A pure insertion (e.g. a missing null-check added, CWE-476/CWE-20 style fixes) has no line in func_vuln that was literally changed, but the patch location is still meaningful for localization: we anchor to the line immediately preceding the insertion point (1-based), or line 1 if the insertion is at the very top. """ vuln_lines = func_vuln.splitlines(keepends=True) patch_lines = func_patch.splitlines(keepends=True) diff = "".join( difflib.unified_diff(vuln_lines, patch_lines, fromfile="vuln", tofile="patch") ) changed: set[int] = set() sm = difflib.SequenceMatcher(a=vuln_lines, b=patch_lines, autojunk=False) for tag, i1, i2, _j1, _j2 in sm.get_opcodes(): if tag in ("replace", "delete"): changed.update(range(i1 + 1, i2 + 1)) # 1-based elif tag == "insert": changed.add(max(1, i1)) return diff, sorted(changed)