mockymock 0.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mockymock/__init__.py +1 -0
- mockymock/_lines.py +86 -0
- mockymock/analysis/__init__.py +0 -0
- mockymock/analysis/assertion_labels.py +132 -0
- mockymock/analysis/cics_eib.py +161 -0
- mockymock/analysis/dialect_risk.py +389 -0
- mockymock/analysis/exact_counts.py +210 -0
- mockymock/analysis/flow_mocks.py +411 -0
- mockymock/analysis/flow_targets.py +33 -0
- mockymock/analysis/linkage.py +357 -0
- mockymock/analysis/loops.py +125 -0
- mockymock/analysis/mock_surface.py +938 -0
- mockymock/analysis/refusal/__init__.py +349 -0
- mockymock/analysis/refusal/_explanations.py +372 -0
- mockymock/analysis/refusal/_gates.py +1840 -0
- mockymock/analysis/refusal/_plans.py +703 -0
- mockymock/analysis/refusal/_reachability.py +1369 -0
- mockymock/analysis/refusal/_types.py +136 -0
- mockymock/analysis/stub_expectations.py +225 -0
- mockymock/cli/__init__.py +0 -0
- mockymock/cli/ai_mode.py +372 -0
- mockymock/cli/checks.py +592 -0
- mockymock/cli/config.py +335 -0
- mockymock/cli/discovery.py +382 -0
- mockymock/cli/doctor.py +203 -0
- mockymock/cli/main.py +3990 -0
- mockymock/cli/mainframe_export.py +951 -0
- mockymock/cli/skills/cobol-analysis/SKILL.md +119 -0
- mockymock/cli/skills/cobol-test/SKILL.md +193 -0
- mockymock/cli/skills/cobol-test/reference/intent-assertions.md +72 -0
- mockymock/cli/skills/cobol-test/reference/report.md +92 -0
- mockymock/cli/skills/cobol-test/reference/verification-levels.md +40 -0
- mockymock/cli/skills/mockymock/SKILL.md +272 -0
- mockymock/cli/skills/mockymock/reference/cli-flags.md +528 -0
- mockymock/cli/skills/mockymock/reference/cut-dsl.md +451 -0
- mockymock/cli/skills/mockymock/reference/modes.md +179 -0
- mockymock/cli/skills/mockymock/reference/paragraph-first-workflow.md +279 -0
- mockymock/cli/skills/mockymock/reference/scenario-generation.md +144 -0
- mockymock/cli/skills_cmd.py +216 -0
- mockymock/debug/__init__.py +0 -0
- mockymock/debug/adapter.py +651 -0
- mockymock/debug/call_stack.py +227 -0
- mockymock/debug/gdb_session.py +671 -0
- mockymock/debug/line_map.py +68 -0
- mockymock/debug/variable_map.py +560 -0
- mockymock/dsl/__init__.py +1 -0
- mockymock/dsl/fixture_bundle.py +257 -0
- mockymock/dsl/generator.py +2589 -0
- mockymock/dsl/model.py +430 -0
- mockymock/dsl/parser.py +1317 -0
- mockymock/mutation/__init__.py +26 -0
- mockymock/mutation/operators.py +615 -0
- mockymock/mutation/report.py +227 -0
- mockymock/mutation/runner.py +628 -0
- mockymock/mutation/zero_baseline.py +251 -0
- mockymock/py.typed +0 -0
- mockymock/report/__init__.py +0 -0
- mockymock/report/coverage.py +377 -0
- mockymock/report/formatters.py +259 -0
- mockymock/report/html_report.py +1643 -0
- mockymock/report/journal.py +93 -0
- mockymock/report/json_report.py +327 -0
- mockymock/report/program_analysis.py +495 -0
- mockymock/report/record_rewrite.py +379 -0
- mockymock/report/results.py +185 -0
- mockymock/report/test_report.py +1352 -0
- mockymock/report/trace.py +136 -0
- mockymock/runtime/__init__.py +0 -0
- mockymock/runtime/compiler.py +19 -0
- mockymock/runtime/docker/Dockerfile +20 -0
- mockymock/runtime/docker_toolchain.py +631 -0
- mockymock/runtime/executor.py +37 -0
- mockymock/runtime/sandbox.py +38 -0
- mockymock/runtime/status.py +199 -0
- mockymock/splicer/__init__.py +0 -0
- mockymock/splicer/captures.py +359 -0
- mockymock/splicer/cics_eib.py +118 -0
- mockymock/splicer/cics_resp.py +234 -0
- mockymock/splicer/driver.py +337 -0
- mockymock/splicer/injector.py +519 -0
- mockymock/splicer/io_copy.py +167 -0
- mockymock/splicer/linkage.py +121 -0
- mockymock/splicer/naming.py +66 -0
- mockymock/splicer/orchestrator.py +293 -0
- mockymock/splicer/reset.py +280 -0
- mockymock/splicer/snapshot.py +212 -0
- mockymock/splicer/source_map.py +530 -0
- mockymock/splicer/sqlca.py +404 -0
- mockymock/splicer/stub_writer.py +74 -0
- mockymock/web/__init__.py +1 -0
- mockymock/web/plugin.py +190 -0
- mockymock/web/reports.py +114 -0
- mockymock/web/server.py +335 -0
- mockymock/web/static/app.js +438 -0
- mockymock/web/static/index.html +92 -0
- mockymock/web/static/style.css +410 -0
- mockymock-0.10.1.dist-info/METADATA +301 -0
- mockymock-0.10.1.dist-info/RECORD +102 -0
- mockymock-0.10.1.dist-info/WHEEL +5 -0
- mockymock-0.10.1.dist-info/entry_points.txt +2 -0
- mockymock-0.10.1.dist-info/licenses/LICENSE +235 -0
- mockymock-0.10.1.dist-info/top_level.txt +1 -0
mockymock/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.10.1" # x-release-please-version
|
mockymock/_lines.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Record-boundary line splitting for COBOL source text.
|
|
2
|
+
|
|
3
|
+
`str.splitlines()` breaks on every Unicode line boundary: not only `\\n`,
|
|
4
|
+
`\\r\\n` and `\\r` but also form feed (`\\x0c`, a page eject that legacy
|
|
5
|
+
sources carry inside comment lines), vertical tab, the `\\x1c`-`\\x1e`
|
|
6
|
+
separators, NEL (`\\x85`) and U+2028/2029. cobolparser splits a source's
|
|
7
|
+
records on `\\n` / `\\r\\n` / `\\r` only (its own `_lines.split_source_lines`),
|
|
8
|
+
so every `SourceLocation.line` it hands back counts records that way. A
|
|
9
|
+
mockymock stage that split the same text with `splitlines()` saw one more
|
|
10
|
+
line per such character, and every splice span after it landed one line
|
|
11
|
+
early -- the mock replaced the statement BEFORE the boundary and the real
|
|
12
|
+
CALL / file I/O still ran. Every split of COBOL source text (raw or
|
|
13
|
+
normalized) that is later indexed by an AST line number goes through here.
|
|
14
|
+
|
|
15
|
+
The rule that matters is *agreement with the installed cobolparser*, not
|
|
16
|
+
any one splitting rule: cobolparser releases before `cobolparser._lines`
|
|
17
|
+
existed (1.8.x and earlier, still allowed by the `>=1.7.7` floor) number
|
|
18
|
+
records the `str.splitlines()` way themselves. Splitting on record
|
|
19
|
+
delimiters only against one of those would shift every splice the other
|
|
20
|
+
way. So record-delimiter splitting is used exactly when the parser does
|
|
21
|
+
it, and `str.splitlines()` otherwise.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from importlib.util import find_spec
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
_LINE_BREAK_RE = re.compile(r"\r\n|\r|\n")
|
|
31
|
+
|
|
32
|
+
#: True when the installed cobolparser numbers records on `\\n` / `\\r\\n` /
|
|
33
|
+
#: `\\r` only (it ships `cobolparser._lines`); False for older releases,
|
|
34
|
+
#: which number them the `str.splitlines()` way.
|
|
35
|
+
PARSER_SPLITS_ON_RECORD_DELIMITERS = find_spec("cobolparser._lines") is not None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def split_source_lines(text: str) -> list[str]:
|
|
39
|
+
"""Split `text` into lines the way the installed cobolparser numbers
|
|
40
|
+
them: on `\\n` / `\\r\\n` / `\\r` only when it does
|
|
41
|
+
(`PARSER_SPLITS_ON_RECORD_DELIMITERS`), otherwise `str.splitlines()`.
|
|
42
|
+
Either way: no trailing empty line for a terminating line break, `[]`
|
|
43
|
+
for `""`."""
|
|
44
|
+
if not PARSER_SPLITS_ON_RECORD_DELIMITERS:
|
|
45
|
+
return text.splitlines()
|
|
46
|
+
if not text:
|
|
47
|
+
return []
|
|
48
|
+
parts = _LINE_BREAK_RE.split(text)
|
|
49
|
+
if parts[-1] == "":
|
|
50
|
+
parts.pop()
|
|
51
|
+
return parts
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SourceEncodingError(ValueError):
|
|
55
|
+
"""A `.cbl` / `.cut` input that is not valid UTF-8.
|
|
56
|
+
|
|
57
|
+
mockymock refuses rather than guessing a legacy code page: the spliced
|
|
58
|
+
copy is written back as UTF-8 and compiled, so a cp1252 `é` inside a
|
|
59
|
+
`PIC X` literal would silently become two bytes and change the field's
|
|
60
|
+
length (the copybook path refuses a lossy decode the same way, as
|
|
61
|
+
EXPANSION_WARNING). The message names the file, the byte offset and
|
|
62
|
+
the conversion to run, so the CLI can print it instead of a traceback.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, path: object, exc: UnicodeDecodeError) -> None:
|
|
66
|
+
self.path = path
|
|
67
|
+
self.offset = exc.start
|
|
68
|
+
super().__init__(
|
|
69
|
+
f"{path} is not valid UTF-8 (byte 0x{exc.object[exc.start]:02x} at "
|
|
70
|
+
f"offset {exc.start}); mockymock reads COBOL sources and .cut "
|
|
71
|
+
f"files as UTF-8 -- convert the file first, e.g. "
|
|
72
|
+
f"`iconv -f cp1252 -t utf-8 {path}` for a Windows code-page source"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def read_utf8_source(path: Path, *, keep_newlines: bool = False) -> str:
|
|
77
|
+
"""`path.read_text(encoding="utf-8")`, raising `SourceEncodingError`
|
|
78
|
+
(not a bare UnicodeDecodeError) when the file is not UTF-8.
|
|
79
|
+
`keep_newlines=True` skips universal-newline translation, so a caller
|
|
80
|
+
that rewrites the file in place can see (and keep) its CRLFs."""
|
|
81
|
+
try:
|
|
82
|
+
if keep_newlines:
|
|
83
|
+
return path.read_bytes().decode("utf-8")
|
|
84
|
+
return path.read_text(encoding="utf-8")
|
|
85
|
+
except UnicodeDecodeError as exc:
|
|
86
|
+
raise SourceEncodingError(path, exc) from exc
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Where each `.cut` assertion's expected value came from.
|
|
2
|
+
|
|
3
|
+
An `EXPECT`/`VERIFY` is only as good as the reason its value is the right
|
|
4
|
+
one. Two reasons are worth telling apart, and the `cobol-test` skill (and
|
|
5
|
+
`mockymock report`) asks for them to be written down as a comment line
|
|
6
|
+
directly above the assertions they cover:
|
|
7
|
+
|
|
8
|
+
*> INTENT: BR-003 orders over 1000.00 get 5% off (ORDRPROC.cbl:212)
|
|
9
|
+
EXPECT WS-NET-AMOUNT TO BE 950.00
|
|
10
|
+
|
|
11
|
+
*> CHARACTERIZATION: recorded by run --record-expectations
|
|
12
|
+
EXPECT WS-AUDIT-COUNT TO BE 3
|
|
13
|
+
|
|
14
|
+
INTENT means the value was derived from a business rule, a spec, or the
|
|
15
|
+
developer -- a mismatch is a suspected program bug. CHARACTERIZATION means
|
|
16
|
+
the value is whatever the program produced on some run -- it pins current
|
|
17
|
+
behavior and proves nothing about correctness.
|
|
18
|
+
|
|
19
|
+
A label covers every EXPECT/VERIFY line that follows it, up to the first
|
|
20
|
+
blank line, the next label, the next TESTCASE, or any other directive.
|
|
21
|
+
Other comment lines in between (a `*> STUB EXPECT`, a note) do not end it.
|
|
22
|
+
An assertion no label covers is reported as unlabelled.
|
|
23
|
+
|
|
24
|
+
Pure text analysis: works on any `.cut` that `parse_cut` accepts, needs no
|
|
25
|
+
program and no Docker.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
INTENT = "intent"
|
|
35
|
+
CHARACTERIZATION = "characterization"
|
|
36
|
+
|
|
37
|
+
_LABEL_RE = re.compile(
|
|
38
|
+
r"^\*>\s*(?P<kind>INTENT|CHARACTERI[SZ]ATION)\b\s*:?\s*(?P<note>.*)$",
|
|
39
|
+
re.IGNORECASE,
|
|
40
|
+
)
|
|
41
|
+
_TESTCASE_RE = re.compile(r'^TESTCASE\s+"(?P<name>(?:[^"]|"")*)"', re.IGNORECASE)
|
|
42
|
+
_ASSERTION_RE = re.compile(r"^(?P<kind>EXPECT|VERIFY)\b", re.IGNORECASE)
|
|
43
|
+
_RULE_ID_RE = re.compile(r"\bBR-\d+\b", re.IGNORECASE)
|
|
44
|
+
|
|
45
|
+
# The comment line `--record-expectations` writes above the EXPECTs it
|
|
46
|
+
# recorded (see report/record_rewrite.py), so recorded values are labelled
|
|
47
|
+
# without anyone having to remember to.
|
|
48
|
+
RECORDED_LABEL = "*> CHARACTERIZATION: recorded by mockymock run --record-expectations"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class LabelledAssertion:
|
|
53
|
+
case_name: str
|
|
54
|
+
line: int
|
|
55
|
+
kind: str # "expect" | "verify"
|
|
56
|
+
text: str
|
|
57
|
+
label: str | None = None # INTENT | CHARACTERIZATION | None
|
|
58
|
+
note: str = ""
|
|
59
|
+
label_line: int | None = None
|
|
60
|
+
rule_ids: list[str] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict[str, Any]:
|
|
63
|
+
return {
|
|
64
|
+
"case": self.case_name,
|
|
65
|
+
"line": self.line,
|
|
66
|
+
"kind": self.kind,
|
|
67
|
+
"text": self.text,
|
|
68
|
+
"label": self.label,
|
|
69
|
+
"note": self.note,
|
|
70
|
+
"labelLine": self.label_line,
|
|
71
|
+
"ruleIds": list(self.rule_ids),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def scan_assertion_labels(cut_text: str) -> list[LabelledAssertion]:
|
|
76
|
+
"""Every EXPECT/VERIFY inside a TESTCASE, in file order, with the label
|
|
77
|
+
covering it (if any). Assertions outside any TESTCASE (a malformed file)
|
|
78
|
+
are skipped -- they belong to no case the report could attribute."""
|
|
79
|
+
results: list[LabelledAssertion] = []
|
|
80
|
+
case_name: str | None = None
|
|
81
|
+
label: tuple[str, str, int] | None = None # (kind, note, line)
|
|
82
|
+
|
|
83
|
+
for line_no, raw in enumerate(cut_text.splitlines(), start=1):
|
|
84
|
+
stripped = raw.strip()
|
|
85
|
+
if not stripped:
|
|
86
|
+
label = None
|
|
87
|
+
continue
|
|
88
|
+
testcase = _TESTCASE_RE.match(stripped)
|
|
89
|
+
if testcase:
|
|
90
|
+
case_name = testcase.group("name").replace('""', '"')
|
|
91
|
+
label = None
|
|
92
|
+
continue
|
|
93
|
+
labelled = _LABEL_RE.match(stripped)
|
|
94
|
+
if labelled:
|
|
95
|
+
kind = labelled.group("kind").upper()
|
|
96
|
+
label = (
|
|
97
|
+
INTENT if kind == "INTENT" else CHARACTERIZATION,
|
|
98
|
+
labelled.group("note").strip(),
|
|
99
|
+
line_no,
|
|
100
|
+
)
|
|
101
|
+
continue
|
|
102
|
+
if stripped.startswith("*>"):
|
|
103
|
+
continue
|
|
104
|
+
assertion = _ASSERTION_RE.match(stripped)
|
|
105
|
+
if assertion is None:
|
|
106
|
+
label = None
|
|
107
|
+
continue
|
|
108
|
+
if case_name is None:
|
|
109
|
+
continue
|
|
110
|
+
item = LabelledAssertion(
|
|
111
|
+
case_name=case_name,
|
|
112
|
+
line=line_no,
|
|
113
|
+
kind=assertion.group("kind").lower(),
|
|
114
|
+
text=stripped,
|
|
115
|
+
)
|
|
116
|
+
if label is not None:
|
|
117
|
+
item.label, item.note, item.label_line = label
|
|
118
|
+
item.rule_ids = sorted(
|
|
119
|
+
{match.upper() for match in _RULE_ID_RE.findall(item.note)}
|
|
120
|
+
)
|
|
121
|
+
results.append(item)
|
|
122
|
+
return results
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def summarize_labels(items: list[LabelledAssertion]) -> dict[str, int]:
|
|
126
|
+
"""Counts by label: the numbers the report's headline shows."""
|
|
127
|
+
return {
|
|
128
|
+
"total": len(items),
|
|
129
|
+
INTENT: sum(1 for i in items if i.label == INTENT),
|
|
130
|
+
CHARACTERIZATION: sum(1 for i in items if i.label == CHARACTERIZATION),
|
|
131
|
+
"unlabelled": sum(1 for i in items if i.label is None),
|
|
132
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Detects whether a program references the CICS Execute Interface Block
|
|
2
|
+
(`DFHEIBLK`) or any of its named fields (`EIBCALEN`, `EIBAID`, ...) without
|
|
3
|
+
declaring them anywhere itself -- see
|
|
4
|
+
docs/2026-08-27-cics-eib-linkage-support-design.md.
|
|
5
|
+
|
|
6
|
+
A real CICS translator inserts `DFHEIBLK`'s LINKAGE SECTION layout ahead of
|
|
7
|
+
`cobc`/the native compiler ever seeing the source; mockymock never runs a
|
|
8
|
+
CICS translator (`splicer.injector.build_exec_trap` already avoids needing
|
|
9
|
+
one for `EXEC CICS` command syntax itself, see the design doc's problem
|
|
10
|
+
statement), so a program referencing an EIB field from ordinary COBOL
|
|
11
|
+
outside any `EXEC CICS` block would otherwise fail `cobc` compilation with
|
|
12
|
+
an undeclared-data-name error.
|
|
13
|
+
|
|
14
|
+
Detection here is a pure, side-effect-free query, mirroring
|
|
15
|
+
`analysis.linkage.linkage_plan`'s split from its own guardrail check:
|
|
16
|
+
`cics_eib_plan` answers "is there anything to do" (`None` when the program
|
|
17
|
+
never references any EIB name, or when it already declares its own copy --
|
|
18
|
+
an ordinary non-CICS program, or a program that hand-declares/COPYs
|
|
19
|
+
`DFHEIBLK` itself, either way untouched by this feature). It does not decide
|
|
20
|
+
whether OTHER CICS constructs in the program are safe (`DFHRESP`/`DFHVALUE`
|
|
21
|
+
macro calls, an undeclared `DFHCOMMAREA`) -- that is
|
|
22
|
+
`refusal.check_cics_eib_layout`'s job.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
|
|
30
|
+
from cobolparser.analysis.unused_variables import collect_declared_items
|
|
31
|
+
|
|
32
|
+
DFHEIBLK_NAME = "DFHEIBLK"
|
|
33
|
+
|
|
34
|
+
# The standard IBM CICS Execute Interface Block field names, unchanged
|
|
35
|
+
# across CICS TS releases. See the design doc's "The DFHEIBLK layout"
|
|
36
|
+
# section for the full PICTURE clauses -- this module only needs the names,
|
|
37
|
+
# for reference detection.
|
|
38
|
+
EIB_FIELD_NAMES: frozenset[str] = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"EIBTIME",
|
|
41
|
+
"EIBDATE",
|
|
42
|
+
"EIBTRNID",
|
|
43
|
+
"EIBTASKN",
|
|
44
|
+
"EIBTRMID",
|
|
45
|
+
"EIBCPOSN",
|
|
46
|
+
"EIBCALEN",
|
|
47
|
+
"EIBAID",
|
|
48
|
+
"EIBFN",
|
|
49
|
+
"EIBRCODE",
|
|
50
|
+
"EIBDS",
|
|
51
|
+
"EIBREQID",
|
|
52
|
+
"EIBRSRCE",
|
|
53
|
+
"EIBSYNC",
|
|
54
|
+
"EIBFREE",
|
|
55
|
+
"EIBRECV",
|
|
56
|
+
"EIBATT",
|
|
57
|
+
"EIBEOC",
|
|
58
|
+
"EIBFMH",
|
|
59
|
+
"EIBCOMPL",
|
|
60
|
+
"EIBSIG",
|
|
61
|
+
"EIBCONF",
|
|
62
|
+
"EIBERR",
|
|
63
|
+
"EIBERRCD",
|
|
64
|
+
"EIBSYNRB",
|
|
65
|
+
"EIBNODAT",
|
|
66
|
+
"EIBRESP",
|
|
67
|
+
"EIBRESP2",
|
|
68
|
+
"EIBRLDBK",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
_EIB_NAMES = EIB_FIELD_NAMES | {DFHEIBLK_NAME}
|
|
73
|
+
# Plain `\b...\b` is wrong here: COBOL identifiers may contain hyphens,
|
|
74
|
+
# which are not `\w` characters, so `\bEIBCALEN\b` would false-match inside
|
|
75
|
+
# an unrelated hyphenated name like `WS-EIBCALEN-SAVED` (a `\b` boundary
|
|
76
|
+
# exists at the transition between the non-word `-` and the word-char `E`).
|
|
77
|
+
# Use an explicit lookaround against the full COBOL identifier character
|
|
78
|
+
# class (letters/digits/hyphen) instead, and match case-insensitively --
|
|
79
|
+
# COBOL identifiers are themselves case-insensitive, so a lowercase or
|
|
80
|
+
# mixed-case source reference is exactly as real a reference as an
|
|
81
|
+
# uppercase one.
|
|
82
|
+
# One combined alternation rather than 29 separate patterns: each pattern
|
|
83
|
+
# starts with a lookbehind, which defeats `re`'s literal-prefix scan
|
|
84
|
+
# optimization, so every name cost a full pass over the program text --
|
|
85
|
+
# measured at 0.73s of a 2.5s static-check pass on a 29k-line program, ~15x
|
|
86
|
+
# `refusal.check_cics_eib_layout`'s two-pattern equivalent. Longest name
|
|
87
|
+
# first so a prefix never shadows a longer sibling (EIBRESP vs EIBRESP2);
|
|
88
|
+
# occurrences cannot overlap, since the lookarounds delimit every match
|
|
89
|
+
# with a non-identifier character on both sides.
|
|
90
|
+
_EIB_NAME_RE = re.compile(
|
|
91
|
+
"(?<![A-Za-z0-9-])(?:"
|
|
92
|
+
+ "|".join(sorted((re.escape(n) for n in _EIB_NAMES), key=len, reverse=True))
|
|
93
|
+
+ ")(?![A-Za-z0-9-])",
|
|
94
|
+
re.IGNORECASE,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# The EXEC DLI interface block (DIB). Like the EIB, the DL/I translator
|
|
99
|
+
# normally supplies it, so a program testing DIBSTAT after an EXEC DLI call
|
|
100
|
+
# did not compile under mockymock -- behind a clean lint (issue #145).
|
|
101
|
+
DLIDIB_NAME = "DLIDIB"
|
|
102
|
+
DIB_FIELD_NAMES: frozenset[str] = frozenset(
|
|
103
|
+
{
|
|
104
|
+
"DIBVER",
|
|
105
|
+
"DIBSTAT",
|
|
106
|
+
"DIBSEGM",
|
|
107
|
+
"DIBSEGLV",
|
|
108
|
+
"DIBKFBL",
|
|
109
|
+
"DIBDBDNM",
|
|
110
|
+
"DIBDBORG",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
_DIB_NAMES = DIB_FIELD_NAMES | {DLIDIB_NAME}
|
|
114
|
+
_DIB_NAME_RE = re.compile(
|
|
115
|
+
"(?<![A-Za-z0-9-])(?:"
|
|
116
|
+
+ "|".join(sorted((re.escape(n) for n in _DIB_NAMES), key=len, reverse=True))
|
|
117
|
+
+ ")(?![A-Za-z0-9-])",
|
|
118
|
+
re.IGNORECASE,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class CicsEibPlan:
|
|
124
|
+
"""Everything `splicer.cics_eib`'s synthesis functions need.
|
|
125
|
+
|
|
126
|
+
`referenced_names` is sorted, transparency-message-only: synthesis
|
|
127
|
+
always emits the full fixed `DFHEIBLK` layout regardless of which
|
|
128
|
+
specific fields matched (see module docstring on the design doc's
|
|
129
|
+
reasoning -- a real translator always inserts the whole block).
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
referenced_names: list[str]
|
|
133
|
+
#: DL/I interface block names referenced and undeclared (issue #145);
|
|
134
|
+
#: empty when the program needs no DIB. Synthesized the same way, as a
|
|
135
|
+
#: `01 DLIDIB.` group.
|
|
136
|
+
dib_names: list[str] = field(default_factory=list)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cics_eib_plan(program, lines: list[str]) -> CicsEibPlan | None:
|
|
140
|
+
"""`None` when there is nothing for this feature to do: no EIB name
|
|
141
|
+
referenced anywhere in `lines`, or every referenced name is already
|
|
142
|
+
declared somewhere in the program's own DATA DIVISION (a hand-declared
|
|
143
|
+
or `COPY DFHEIBLK`-expanded copy -- synthesizing a second one would be a
|
|
144
|
+
duplicate-data-name compile error, and the program already has real
|
|
145
|
+
storage for it).
|
|
146
|
+
"""
|
|
147
|
+
text = "\n".join(lines)
|
|
148
|
+
referenced = sorted({m.group(0).upper() for m in _EIB_NAME_RE.finditer(text)})
|
|
149
|
+
dib = sorted({m.group(0).upper() for m in _DIB_NAME_RE.finditer(text)})
|
|
150
|
+
if not referenced and not dib:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
declared = {item.name.upper() for item in collect_declared_items(program)}
|
|
154
|
+
if declared & set(referenced):
|
|
155
|
+
referenced = []
|
|
156
|
+
if declared & _DIB_NAMES:
|
|
157
|
+
dib = []
|
|
158
|
+
if not referenced and not dib:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
return CicsEibPlan(referenced_names=referenced, dib_names=dib)
|