File size: 1,671 Bytes
5bfc214 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | """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)
|