From 45ceb2273afaf33c6594fa3748dec195cce420c1 Mon Sep 17 00:00:00 2001 From: abhay-codes07 Date: Thu, 24 Sep 2026 23:21:32 +0530 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q (cherry picked from commit abcdde1a6578e2d280df0f659d6f8269cd7ea124) --- graphify/extractors/cobol.py | 20 ++++++++++++++++---- tests/test_cobol_extractor.py | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/graphify/extractors/cobol.py b/graphify/extractors/cobol.py index 91e1929fa..73a439cd9 100644 --- a/graphify/extractors/cobol.py +++ b/graphify/extractors/cobol.py @@ -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()): diff --git a/tests/test_cobol_extractor.py b/tests/test_cobol_extractor.py index f08c5e7a8..2a5156c23 100644 --- a/tests/test_cobol_extractor.py +++ b/tests/test_cobol_extractor.py @@ -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