Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 71 additions & 2 deletions gitgalaxy/core/uow_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
# window runs from its END-EXEC to the next command that sets v again, the next
# paragraph / section header, or `_RESP_WINDOW` characters. Inside it, a
# DFHRESP(x) counts once v has been mentioned (`IF v NOT = DFHRESP(NORMAL)`,
# `EVALUATE v WHEN DFHRESP(NOTFND)`). A RESP_CHECK with no condition is a
# command whose outcome is never tested -- a real finding, not an error.
# `EVALUATE v WHEN DFHRESP(NOTFND)`), and so does a response code tested BY
# NUMBER on v (`v = 0`, `EVALUATE v WHEN 13`), named through _RESP_CODES. A
# RESP_CHECK with no condition is a command whose outcome is never tested -- a
# real finding, not an error.
#
# SCOPE AND NON-SCOPE:
# - Extraction only, per file. The owning paragraph of each row, whether a
Expand Down Expand Up @@ -64,6 +66,68 @@
_COBOL_NAME = r"[A-Z0-9][A-Z0-9-]{0,62}"
_HEADER = re.compile(r"^ {7,10}" + _COBOL_NAME + r"(?:[ \t]{1,20}SECTION)?[ \t]{0,20}\.[ \t]*$", re.I | re.M)
_RESP_WINDOW = 8000
# EIBRESP values a program may test by number instead of DFHRESP(name) (CardDemo
# COSGN00C: `EVALUATE WS-RESP-CD WHEN 0 ... WHEN 13`). A number not listed here is
# kept as the number itself.
_RESP_CODES = {
0: "NORMAL",
1: "ERROR",
4: "EOF",
12: "FILENOTFOUND",
13: "NOTFND",
14: "DUPREC",
15: "DUPKEY",
16: "INVREQ",
17: "IOERR",
18: "NOSPACE",
19: "NOTOPEN",
20: "ENDFILE",
21: "ILLOGIC",
22: "LENGERR",
23: "QZERO",
26: "ITEMERR",
27: "PGMIDERR",
28: "TRANSIDERR",
36: "MAPFAIL",
44: "QIDERR",
70: "NOTAUTH",
}
_NUMBER = r"([0-9]{1,4})(?![0-9.])"
# One EVALUATE-structure word: an EVALUATE opens a level, END-EVALUATE closes one,
# and a WHEN arm carries a number when it is `WHEN n`.
_EVALUATE_WORD = re.compile(
r"(?<![A-Z0-9-])(END-EVALUATE|EVALUATE|WHEN)(?![A-Z0-9-])(?:[ \t\n]{1,200}" + _NUMBER + r")?", re.I
)


def _numeric_checks(window: str, var: str) -> list[str]:
"""The response codes `var` is compared with BY NUMBER in `window`: `var [NOT] =
n` / `var [NOT] EQUAL [TO] n`, and the WHEN n arms of an `EVALUATE var` itself
(a nested EVALUATE's arms skipped). Each as its DFHRESP name when known."""
name = re.escape(var)
found: list[int] = []
compare = re.compile(
rf"(?<![A-Z0-9-]){name}(?![A-Z0-9-])[ \t\n]{{0,200}}(?:NOT[ \t\n]{{1,200}})?"
rf"(?:=|EQUAL(?:[ \t\n]{{1,200}}TO)?)[ \t\n]{{0,200}}" + _NUMBER,
re.I,
)
found += [int(m.group(1)) for m in compare.finditer(window)]
for ev in re.finditer(rf"(?<![A-Z0-9-])EVALUATE[ \t\n]{{1,200}}{name}(?![A-Z0-9-])", window, re.I):
# Only the arms of THIS EVALUATE: a nested EVALUATE's WHENs test something else.
depth = 1
for w in _EVALUATE_WORD.finditer(window, ev.end()):
word = w.group(1).upper()
if word == "EVALUATE":
depth += 1
elif word == "END-EVALUATE":
depth -= 1
if depth == 0:
break
elif depth == 1 and w.group(2):
found.append(int(w.group(2)))
return [_RESP_CODES.get(n, str(n)) for n in found]


_UOW_VERBS = {"SYNCPOINT", "HANDLE", "IGNORE", "PUSH", "POP", "ABEND"}


Expand Down Expand Up @@ -223,6 +287,11 @@ def resolved(operand: Optional[str]) -> Optional[str]:
name = m.group(1).upper()
if name not in checked:
checked.append(name)
# The numeric forms name the field themselves (EVALUATE v / v = n), so they
# read the whole window, not only the text after its first mention.
for name in _numeric_checks(window, var):
if name not in checked:
checked.append(name)
rows.append(
(
pos,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92226,7 +92226,7 @@
"kind": "RESP_CHECK",
"source": "CICS",
"verb": "READ",
"condition": null,
"condition": "NORMAL,NOTFND",
"target": null,
"target_kind": null,
"resp_var": "WS-RESP-CD",
Expand All @@ -92237,7 +92237,9 @@
"uow_validated": true,
"verification": {
"status": "validated",
"notes": [],
"notes": [
"2026-09-24: L211 READ's RESP is tested by number (EVALUATE WS-RESP-CD WHEN 0 / WHEN 13 = NORMAL / NOTFND); numeric RESP checks are now read. The blind census reviewer of this file reported exactly this reading."
],
"census": {
"by": "claude-opus-5-5 (blind fresh-context census reviewer)",
"at": "2026-09-24"
Expand Down
10 changes: 10 additions & 0 deletions tests/cobol_mainframe/test_cobol_answer_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -962,3 +962,13 @@ def test_uow_and_tdq_trigger_readers_are_independent(tmp_path):
"L14 ABEND ABEND c=K001 t=-/- v=- a=NODUMP",
]
assert ak.draft_tdq_triggers(tmp_path)["UOWK.cbl"]["starts"] == ["PRTQ -> PRT1 -> PRTPGM"]


def test_key_reads_resp_codes_tested_by_number():
"""#3453 follow-up: `EVALUATE v WHEN 0 / WHEN 13` tests NORMAL / NOTFND (CardDemo
COSGN00C); a nested EVALUATE's WHEN arms belong to that EVALUATE, not to v."""
text = (
"EVALUATE WS-RESP-CD\n WHEN 0\n EVALUATE WS-MONTH\n WHEN 12 CONTINUE\n END-EVALUATE\n"
" WHEN 13 CONTINUE\nEND-EVALUATE\nIF WS-RESP-CD NOT = 22 CONTINUE END-IF\nIF WS-RESP-CD = 923 CONTINUE END-IF\n"
)
assert ak._uow_numeric(text, "WS-RESP-CD") == {"NORMAL", "NOTFND", "LENGERR", "923"}
25 changes: 25 additions & 0 deletions tests/core_engine/test_uow_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,28 @@ def test_the_resp_window_is_bounded():
started = time.perf_counter()
extract_uow_handlers(src)
assert time.perf_counter() - started < 10.0


def test_resp_codes_tested_by_number_count_as_checks():
# CardDemo COSGN00C: EVALUATE WS-RESP-CD WHEN 0 ... WHEN 13 is NORMAL / NOTFND.
src = (
" READ-SEC.\n"
" EXEC CICS READ DATASET(WS-F) INTO(R) RESP(WS-RESP-CD)\n"
" END-EXEC.\n"
" EVALUATE WS-RESP-CD\n"
" WHEN 0\n"
" EVALUATE WS-MONTH\n"
" WHEN 12 CONTINUE\n"
" END-EVALUATE\n"
" WHEN 13 CONTINUE\n"
" WHEN OTHER CONTINUE\n"
" END-EVALUATE.\n"
" EXEC CICS SEND MAP('M') RESP(WS-RC) END-EXEC.\n"
" IF WS-RC NOT = 0 CONTINUE END-IF.\n"
" EXEC CICS RECEIVE MAP('M') RESP(WS-RC2) END-EXEC.\n"
" IF WS-RC2 = 923 CONTINUE END-IF.\n"
)
checks = [(r[1], r[2]) for r in _rows(src) if r[0] == "RESP_CHECK"]
# WHEN 12 belongs to the nested EVALUATE WS-MONTH, not to the RESP field; an
# unnamed code is kept as the number.
assert checks == [("READ", "NORMAL,NOTFND"), ("SEND", "NORMAL"), ("RECEIVE", "923")]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions tests/tools/cobol_answer_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -2550,6 +2550,55 @@ def draft_mq(repo: Path) -> dict[str, dict[str, Any]]:
_UOW_SKIP = {"HANDLE", "IGNORE", "PUSH", "POP", "ABEND"}


# EIBRESP codes, this tool's own table (CICS Application Programming Reference):
# a RESP field tested by number (`WHEN 13`) names the same condition as DFHRESP(NOTFND).
_UOW_CODES = {
"0": "NORMAL",
"1": "ERROR",
"4": "EOF",
"12": "FILENOTFOUND",
"13": "NOTFND",
"14": "DUPREC",
"15": "DUPKEY",
"16": "INVREQ",
"17": "IOERR",
"18": "NOSPACE",
"19": "NOTOPEN",
"20": "ENDFILE",
"21": "ILLOGIC",
"22": "LENGERR",
"23": "QZERO",
"26": "ITEMERR",
"27": "PGMIDERR",
"28": "TRANSIDERR",
"36": "MAPFAIL",
"44": "QIDERR",
"70": "NOTAUTH",
}


def _uow_numeric(text: str, var: str) -> set[str]:
"""Codes compared with `var` by number: `var [NOT] = n` / `EQUAL [TO] n`, and the
`WHEN n` arms of `EVALUATE var` itself (a nested EVALUATE's arms skipped)."""
out: set[str] = set()
v = re.escape(var)
for m in re.finditer(rf"\b{v}(?![A-Z0-9-])\s*(?:NOT\s+)?(?:=|EQUAL(?:\s+TO)?)\s*(\d+)\b(?!\.\d)", text):
out.add(_UOW_CODES.get(str(int(m.group(1))), str(int(m.group(1)))))
for ev in re.finditer(rf"\bEVALUATE\s+{v}(?![A-Z0-9-])", text):
# Walk this EVALUATE's own level only; a nested one's WHENs are not ours.
level = 0
for tok in re.finditer(r"\b(END-EVALUATE|EVALUATE|WHEN)\b(?:\s+(\d+)\b(?!\.\d))?", text[ev.end() :]):
if tok.group(1) == "EVALUATE":
level += 1
elif tok.group(1) == "END-EVALUATE":
if level == 0:
break
level -= 1
elif level == 0 and tok.group(2):
out.add(_UOW_CODES.get(str(int(tok.group(2))), str(int(tok.group(2)))))
return out


def uow_handler_ops(path: Path) -> list[dict[str, Any]]:
"""Every unit-of-work point, handler, ABEND and RESP check in one COBOL
source, this tool's own reading (see the section header)."""
Expand Down Expand Up @@ -2643,6 +2692,8 @@ def mk(kind: str, source: str, verb: str, off: int, **kw: Any) -> dict[str, Any]
window = src.text[end:limit]
first = re.search(rf"(?<![A-Z0-9-]){re.escape(var)}(?![A-Z0-9-])", window)
seen = set(_UOW_DFHRESP.findall(src.raw_text[end + first.start() : limit])) if first else set()
if first:
seen |= _uow_numeric(src.raw_text[end:limit], var)
out.append(
(pos, 1, mk("RESP_CHECK", "CICS", verb, pos, condition=",".join(sorted(seen)) or None, resp_var=var))
)
Expand Down
Loading