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:
abhay-codes07 authored and safishamsi committed 2026-09-25 15:44:08 +01:00
1 parent 81afa16bd9
commit 45ceb2273a
2 files changed
+41 -4

No files matched your search

+16 -4
View File
@@ -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()):
+25
View File
@@ -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