mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +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
This commit is contained in:
1 parent
4c735618f3
commit
abcdde1a65
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,
|
||||
@@ -247,9 +250,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()):
|
||||
|
||||
@@ -131,3 +131,28 @@ def test_cobol_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_pa
|
||||
labels = {node["label"].casefold() for node in result["nodes"]}
|
||||
assert 'kept' in labels
|
||||
assert labels.isdisjoint({'ghost'})
|
||||
|
||||
|
||||
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