mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
fix(cobol): link both endpoints of a PERFORM ... THRU range
`PERFORM A THRU Z` (and `THROUGH`) runs the whole paragraph range A..Z, but the
PERFORM pattern captured only the first name, so a `calls` edge was emitted to A
and the range-end Z got none. "What performs Z?" then answered nothing even
though this paragraph does, and any reachability walk stopped short of the
range's tail.
Capture the optional `THRU`/`THROUGH` target and enqueue both endpoints for
resolution against the paragraph table. Intermediate paragraphs in the range are
not individually linked (that would need paragraph source ordering); the two
endpoints are the reliable, common signal and both now resolve. A plain
`PERFORM A` is unchanged.
Regression test: `PERFORM A-PARA THRU Z-PARA` now links MAIN to both A-PARA and
Z-PARA (fails on the old single-capture pattern).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit abcdde1a65)
This commit is contained in:
1 parent
81afa16bd9
commit
45ceb2273a
2 files changed
+41
-4
No files matched your search
@@ -13,7 +13,10 @@ _PROGRAM = re.compile(rf"\bPROGRAM-ID\s*\.\s*({_NAME})", re.IGNORECASE)
|
||||
_PARAGRAPH = re.compile(
|
||||
rf"^({_NAME})(?:\s+(SECTION))?\s*\.\s*$", re.IGNORECASE
|
||||
)
|
||||
_PERFORM = re.compile(rf"\bPERFORM\s+({_NAME})\b", re.IGNORECASE)
|
||||
_PERFORM = re.compile(
|
||||
rf"\bPERFORM\s+({_NAME})(?:\s+(?:THRU|THROUGH)\s+({_NAME}))?\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
_COPY = re.compile(
|
||||
rf"\bCOPY\s+(?:'([^']+)'|\"([^\"]+)\"|({_NAME}(?:\.[A-Z0-9-]+)?))",
|
||||
re.IGNORECASE,
|
||||
@@ -256,9 +259,18 @@ def extract_cobol(path: Path) -> dict:
|
||||
|
||||
masked = _mask_strings(upper)
|
||||
for match in _PERFORM.finditer(masked):
|
||||
name = match.group(1).upper()
|
||||
if name not in _PERFORM_MODIFIERS:
|
||||
pending_performs.append((current_scope, current_program, name, line))
|
||||
# `PERFORM A THRU Z` runs the range A..Z, so both endpoints are
|
||||
# performed — link both. Only the entry paragraph was captured
|
||||
# before, leaving the range-end with no inbound edge ("what performs
|
||||
# Z?" answered nothing even though this paragraph does). Intermediate
|
||||
# paragraphs in the range are not individually linked (that needs
|
||||
# source ordering); the two endpoints are the reliable, common signal.
|
||||
for group in (match.group(1), match.group(2)):
|
||||
if group is None:
|
||||
continue
|
||||
name = group.upper()
|
||||
if name not in _PERFORM_MODIFIERS:
|
||||
pending_performs.append((current_scope, current_program, name, line))
|
||||
|
||||
for match in _CALL.finditer(upper):
|
||||
if _starts_inside_string(upper, match.start()):
|
||||
|
||||
@@ -156,3 +156,28 @@ def test_cobol_fixed_format_with_sequence_numbers(tmp_path):
|
||||
labels = {node["label"] for node in result["nodes"]}
|
||||
assert {"PAYROLL", "MAIN-PARA", "INIT-PARA"} <= labels
|
||||
assert ("MAIN-PARA", "INIT-PARA") in _edge_labels(result, "calls")
|
||||
|
||||
|
||||
def test_cobol_perform_thru_links_both_range_endpoints(tmp_path):
|
||||
"""`PERFORM A THRU Z` runs the range A..Z, so both endpoints are performed.
|
||||
Only the entry paragraph was linked before, leaving the range-end with no
|
||||
inbound `calls` edge."""
|
||||
source = tmp_path / "range.cbl"
|
||||
source.write_text(
|
||||
" IDENTIFICATION DIVISION.\n"
|
||||
" PROGRAM-ID. RANGE.\n"
|
||||
" PROCEDURE DIVISION.\n"
|
||||
" MAIN-PARA.\n"
|
||||
" PERFORM A-PARA THRU Z-PARA.\n"
|
||||
" A-PARA.\n"
|
||||
" DISPLAY 'A'.\n"
|
||||
" Z-PARA.\n"
|
||||
" DISPLAY 'Z'.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([source], cache_root=tmp_path)
|
||||
|
||||
calls = _edge_labels(result, "calls")
|
||||
assert ("MAIN-PARA", "A-PARA") in calls
|
||||
assert ("MAIN-PARA", "Z-PARA") in calls
|
||||
Reference in new issue
Block a user