jcl-parser 2.0.0__tar.gz → 2.0.1__tar.gz
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.
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/PKG-INFO +1 -1
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jcl_parser.egg-info/PKG-INFO +1 -1
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jcl_parser.egg-info/SOURCES.txt +1 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/__init__.py +1 -1
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/_job_builder.py +12 -2
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/_post_parse.py +91 -31
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/_statement_parser.py +36 -5
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/condition_parser.py +63 -60
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/continuation_handler.py +6 -6
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/dataset_lifecycle.py +229 -110
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/explosion.py +1 -1
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/lint/rules.py +15 -4
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/lint/runner.py +39 -7
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/model.py +41 -1
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/parser.py +111 -32
- jcl_parser-2.0.1/jclparser/program_profiles/_control.py +136 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/base.py +24 -2
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/idcams.py +25 -5
- jcl_parser-2.0.1/jclparser/program_profiles/iebgener.py +197 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/ikjeft01.py +11 -3
- jcl_parser-2.0.1/jclparser/program_profiles/sort.py +144 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/rc_analysis.py +198 -109
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/system_symbols.py +1 -1
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/validators.py +9 -2
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/pyproject.toml +1 -1
- jcl_parser-2.0.0/jclparser/program_profiles/iebgener.py +0 -67
- jcl_parser-2.0.0/jclparser/program_profiles/sort.py +0 -74
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/LICENSE +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/README.md +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jcl_parser.egg-info/dependency_links.txt +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jcl_parser.egg-info/entry_points.txt +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jcl_parser.egg-info/requires.txt +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jcl_parser.egg-info/top_level.txt +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/__main__.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/_model_factories.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/_source_scanner.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/builtin_procs.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/cli.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/errors.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/include_loader.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/jcl_file_type.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/jes_statements.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/lint/__init__.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/lint/base.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/lint/config.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/metrics.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/parameter_ast.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/parameter_validators.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/plugins/__init__.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/plugins/base.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/proc_loader.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/profiling.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/__init__.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/_invoked.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/adrdssu.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/binder.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/bpxbatch.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/compilers.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/db2_batch.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/dfsrrc00.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/dsnutilb.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/ftp.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/icetool.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iebcompr.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iebdg.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iebptpch.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iebupdte.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iefbr14.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iehinitt.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iehlist.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/iehprogm.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/irxjcl.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/program_profiles/registry.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/py.typed +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/sql_detector.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/jclparser/utils.py +0 -0
- {jcl_parser-2.0.0 → jcl_parser-2.0.1}/setup.cfg +0 -0
|
@@ -45,6 +45,7 @@ jclparser/lint/runner.py
|
|
|
45
45
|
jclparser/plugins/__init__.py
|
|
46
46
|
jclparser/plugins/base.py
|
|
47
47
|
jclparser/program_profiles/__init__.py
|
|
48
|
+
jclparser/program_profiles/_control.py
|
|
48
49
|
jclparser/program_profiles/_invoked.py
|
|
49
50
|
jclparser/program_profiles/adrdssu.py
|
|
50
51
|
jclparser/program_profiles/base.py
|
|
@@ -20,7 +20,7 @@ application, for example::
|
|
|
20
20
|
import logging as _logging
|
|
21
21
|
|
|
22
22
|
# Package metadata
|
|
23
|
-
__version__ = "2.0.
|
|
23
|
+
__version__ = "2.0.1" # x-release-please-version
|
|
24
24
|
__author__ = "JCL Parser Team"
|
|
25
25
|
__description__ = "Parse IBM JCL scripts into Python objects"
|
|
26
26
|
|
|
@@ -611,9 +611,19 @@ class JobBuilder:
|
|
|
611
611
|
self.pending_jes_typed.append(typed)
|
|
612
612
|
|
|
613
613
|
def _on_output(self, stmt: Statement, line_num: int) -> None:
|
|
614
|
-
# OUTPUT statement - JES output descriptor; promote to typed model
|
|
614
|
+
# OUTPUT statement - JES output descriptor; promote to typed model.
|
|
615
|
+
# Coded after an EXEC it is step-level, before the first EXEC
|
|
616
|
+
# job-level (or, in a PROC body, procedure-level): DD OUTPUT=*.name
|
|
617
|
+
# resolution depends on which (#85).
|
|
618
|
+
step = self.current_step
|
|
619
|
+
procedure = self.current_procedure
|
|
620
|
+
output = OutputStatement.from_statement(
|
|
621
|
+
stmt,
|
|
622
|
+
step_name=(step.name or "") if step is not None else None,
|
|
623
|
+
procedure_name=procedure.name if procedure is not None else None,
|
|
624
|
+
)
|
|
615
625
|
sink = self.job.output_statements if self.job else self.pending_outputs
|
|
616
|
-
sink.append(
|
|
626
|
+
sink.append(output)
|
|
617
627
|
|
|
618
628
|
def _on_null(self, stmt: Statement, line_num: int) -> None:
|
|
619
629
|
# NULL statement (//) - just ignore, used as delimiter
|
|
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
24
24
|
|
|
25
25
|
from ._model_factories import _has_unresolved_symbol_ref
|
|
26
26
|
from .errors import JCLStructureError, JCLSyntaxError
|
|
27
|
-
from .model import Job, Step, iter_execution_steps
|
|
27
|
+
from .model import Job, OutputStatement, Step, iter_execution_steps
|
|
28
28
|
from .utils import split_source_lines
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
@@ -48,64 +48,124 @@ def _extract_output_ref_names(value: Any) -> list[str]:
|
|
|
48
48
|
Values that do not start with ``*.`` (e.g. a bare nodename, a qualified
|
|
49
49
|
``STEP.PROCSTEP.N`` form without the leading ``*.``, or ``None``) yield
|
|
50
50
|
an empty list. Callers rely on this to distinguish output-statement
|
|
51
|
-
references from other uses of the OUTPUT= parameter.
|
|
51
|
+
references from other uses of the OUTPUT= parameter. The step
|
|
52
|
+
qualifiers are kept by :func:`_extract_output_refs`.
|
|
53
|
+
"""
|
|
54
|
+
return [ref[-1] for ref in _extract_output_refs(value)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract_output_refs(value: Any) -> list[tuple[str, ...]]:
|
|
58
|
+
"""Return the ``*.`` OUTPUT references of a DD ``OUTPUT=`` value, qualified.
|
|
59
|
+
|
|
60
|
+
Each reference is the tuple of its uppercased segments::
|
|
61
|
+
|
|
62
|
+
OUTPUT=*.NAME -> [("NAME",)]
|
|
63
|
+
OUTPUT=(*.STEP.NAME,*.N2) -> [("STEP", "NAME"), ("N2",)]
|
|
64
|
+
OUTPUT=*.STEP.PROCSTEP.NAME -> [("STEP", "PROCSTEP", "NAME")]
|
|
52
65
|
"""
|
|
53
|
-
names: list[str] = []
|
|
54
66
|
items: list[str]
|
|
55
67
|
if isinstance(value, list):
|
|
56
68
|
items = [str(v) for v in value]
|
|
57
69
|
elif isinstance(value, str):
|
|
58
70
|
items = [value]
|
|
59
71
|
else:
|
|
60
|
-
return
|
|
72
|
+
return []
|
|
61
73
|
|
|
74
|
+
refs: list[tuple[str, ...]] = []
|
|
62
75
|
for item in items:
|
|
63
76
|
token = item.strip().strip("()")
|
|
64
77
|
if not token.startswith("*."):
|
|
65
|
-
# Qualified form step.procstep.name uses ".name" — last segment after *
|
|
66
|
-
if ".*" in token:
|
|
67
|
-
# Not a back-reference either
|
|
68
|
-
continue
|
|
69
|
-
continue
|
|
70
|
-
ref = token[2:].strip()
|
|
71
|
-
if not ref:
|
|
72
78
|
continue
|
|
73
|
-
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
segments = tuple(seg.strip().upper() for seg in token[2:].split("."))
|
|
80
|
+
if segments[-1]:
|
|
81
|
+
refs.append(segments)
|
|
82
|
+
return refs
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
#: Key of an OUTPUT statement in :class:`_OutputScopes`:
|
|
86
|
+
#: ``(procedure_name, step_name, name)``, uppercased; ``None`` for a level
|
|
87
|
+
#: the statement is not coded in.
|
|
88
|
+
_OutputKey = tuple[str | None, str | None, str]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _upper(name: str | None) -> str | None:
|
|
92
|
+
return name.upper() if name is not None else None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class _OutputScopes:
|
|
96
|
+
"""The job's OUTPUT statements, by the step (and PROC) they are coded in."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, job: Job) -> None:
|
|
99
|
+
self.by_key: dict[_OutputKey, OutputStatement] = {}
|
|
100
|
+
for out in job.output_statements:
|
|
101
|
+
if out.name:
|
|
102
|
+
key = (
|
|
103
|
+
_upper(out.procedure_name),
|
|
104
|
+
_upper(out.step_name),
|
|
105
|
+
out.name.upper(),
|
|
106
|
+
)
|
|
107
|
+
self.by_key[key] = out # last wins if duplicated
|
|
108
|
+
self.instream_procs = {p.name.upper() for p in job.procedures}
|
|
109
|
+
self.step_procs = {
|
|
110
|
+
step.name.upper(): (step.procedure or "").upper()
|
|
111
|
+
for step in iter_execution_steps(job.steps)
|
|
112
|
+
if step.name
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
def checkable(self, ref: tuple[str, ...]) -> bool:
|
|
116
|
+
"""False for ``*.step.procstep.name`` into a PROC that is not in-stream.
|
|
117
|
+
|
|
118
|
+
Its OUTPUT statements are not part of the job, so whether the
|
|
119
|
+
reference resolves cannot be told.
|
|
120
|
+
"""
|
|
121
|
+
return len(ref) != 3 or self.step_procs.get(ref[0]) in self.instream_procs
|
|
122
|
+
|
|
123
|
+
def resolve(self, ref: tuple[str, ...], step_name: str) -> OutputStatement | None:
|
|
124
|
+
"""The OUTPUT statement *ref* names from a DD in job step *step_name*.
|
|
125
|
+
|
|
126
|
+
``*.name`` is the one in the DD's own step, else the job-level one;
|
|
127
|
+
``*.step.name`` the one in job step *step*; ``*.step.procstep.name``
|
|
128
|
+
the one in step *procstep* of the in-stream PROC job step *step*
|
|
129
|
+
calls.
|
|
130
|
+
"""
|
|
131
|
+
name = ref[-1]
|
|
132
|
+
if len(ref) == 1:
|
|
133
|
+
own = self.by_key.get((None, step_name.upper(), name))
|
|
134
|
+
return own if own is not None else self.by_key.get((None, None, name))
|
|
135
|
+
if len(ref) == 2:
|
|
136
|
+
return self.by_key.get((None, ref[0], name))
|
|
137
|
+
if len(ref) == 3:
|
|
138
|
+
return self.by_key.get((self.step_procs.get(ref[0]), ref[1], name))
|
|
139
|
+
return None
|
|
78
140
|
|
|
79
141
|
|
|
80
142
|
def _resolve_output_refs(job: Job) -> None:
|
|
81
143
|
"""Populate ``DDStatement.output_refs`` for every DD that uses ``OUTPUT=*.X``.
|
|
82
144
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
145
|
+
A reference resolves by scope, not by name alone (#85): see
|
|
146
|
+
:meth:`_OutputScopes.resolve`. Unresolved references (no matching
|
|
147
|
+
:class:`OutputStatement`) are recorded on ``job.unresolved_output_refs``
|
|
148
|
+
as ``(step_name, ref, line_number)`` tuples, *ref* as coded after ``*.``
|
|
149
|
+
(``"NAME"``, ``"STEP.NAME"``), so
|
|
150
|
+
:func:`jclparser.validators.validate_job_structure` can emit warnings
|
|
151
|
+
without re-scanning the parameter string. A reference into a PROC that
|
|
152
|
+
is not in-stream cannot be checked and is neither.
|
|
87
153
|
"""
|
|
88
154
|
if not job:
|
|
89
155
|
return
|
|
90
|
-
|
|
91
|
-
output_by_name: dict[str, Any] = {
|
|
92
|
-
out.name.upper(): out for out in job.output_statements if out.name
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
from .model import iter_execution_steps
|
|
96
|
-
|
|
156
|
+
scopes = _OutputScopes(job)
|
|
97
157
|
for step in iter_execution_steps(job.steps):
|
|
98
158
|
for dd in step.dd_statements:
|
|
99
159
|
value = dd.parameters.get("OUTPUT")
|
|
100
160
|
if value is None:
|
|
101
161
|
continue
|
|
102
|
-
for ref in
|
|
103
|
-
target =
|
|
162
|
+
for ref in _extract_output_refs(value):
|
|
163
|
+
target = scopes.resolve(ref, step.name or "")
|
|
104
164
|
if target is not None:
|
|
105
165
|
dd.output_refs.append(target)
|
|
106
|
-
|
|
166
|
+
elif scopes.checkable(ref):
|
|
107
167
|
job.unresolved_output_refs.append(
|
|
108
|
-
(step.name or "", ref, dd.line_number)
|
|
168
|
+
(step.name or "", ".".join(ref), dd.line_number)
|
|
109
169
|
)
|
|
110
170
|
|
|
111
171
|
|
|
@@ -173,17 +173,36 @@ def _resumes_operands(text: str) -> bool:
|
|
|
173
173
|
return "=" in token.split(",")[0]
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
def
|
|
176
|
+
def _closes_parens(text: str, depth: int) -> bool:
|
|
177
|
+
"""Return True if *text*, read at paren *depth*, brings the depth to 0."""
|
|
178
|
+
in_quotes = False
|
|
179
|
+
for char in text:
|
|
180
|
+
if char == "'":
|
|
181
|
+
in_quotes = not in_quotes
|
|
182
|
+
elif not in_quotes:
|
|
183
|
+
if char == "(":
|
|
184
|
+
depth += 1
|
|
185
|
+
elif char == ")":
|
|
186
|
+
depth -= 1
|
|
187
|
+
if depth <= 0:
|
|
188
|
+
return True
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _find_comment_start(operands: str, depth: int = 0) -> int | None:
|
|
177
193
|
"""Index of the comment field inside *operands*, or ``None`` if there is none.
|
|
178
194
|
|
|
179
195
|
The operand field ends at the first blank that is outside quotes and
|
|
180
196
|
outside parentheses; everything after it is the comment field (z/OS JCL
|
|
181
|
-
Reference, "Comment field").
|
|
197
|
+
Reference, "Comment field"). Inside a parenthesised list, a blank that
|
|
198
|
+
follows a subparameter's comma also starts a comment when the rest of
|
|
199
|
+
the record neither resumes operands nor closes the list: the list goes
|
|
200
|
+
on in the next record. *depth* is the paren depth the record starts at
|
|
201
|
+
(non-zero for a record continuing an open list).
|
|
182
202
|
"""
|
|
183
203
|
if " " not in operands and "\t" not in operands:
|
|
184
204
|
# No blank at all: the operand field runs to the end of the record.
|
|
185
205
|
return None
|
|
186
|
-
depth = 0
|
|
187
206
|
in_quotes = False
|
|
188
207
|
i = 0
|
|
189
208
|
n = len(operands)
|
|
@@ -207,6 +226,16 @@ def _find_comment_start(operands: str) -> int | None:
|
|
|
207
226
|
i = n - len(rest)
|
|
208
227
|
continue
|
|
209
228
|
return i
|
|
229
|
+
elif (
|
|
230
|
+
char.isspace()
|
|
231
|
+
and operands[:i].endswith(",")
|
|
232
|
+
and not _resumes_operands(operands[i:].lstrip())
|
|
233
|
+
and not _closes_parens(operands[i:], depth)
|
|
234
|
+
):
|
|
235
|
+
# A parenthesised list interrupted after a subparameter's
|
|
236
|
+
# comma may carry a comment (z/OS JCL Reference, "Continuing
|
|
237
|
+
# the operand field"); the list resumes on the next record.
|
|
238
|
+
return i
|
|
210
239
|
i += 1
|
|
211
240
|
return None
|
|
212
241
|
|
|
@@ -240,7 +269,9 @@ def _find_then_end(operands: str) -> int | None:
|
|
|
240
269
|
return None
|
|
241
270
|
|
|
242
271
|
|
|
243
|
-
def split_comment_field(
|
|
272
|
+
def split_comment_field(
|
|
273
|
+
line: str, group_operation: str = "", paren_depth: int = 0
|
|
274
|
+
) -> tuple[str, str]:
|
|
244
275
|
"""Split a JCL statement record into ``(statement, comment)``.
|
|
245
276
|
|
|
246
277
|
In JCL a statement's operand field ends at the first blank outside quotes
|
|
@@ -270,7 +301,7 @@ def split_comment_field(line: str, group_operation: str = "") -> tuple[str, str]
|
|
|
270
301
|
if cut is None:
|
|
271
302
|
return line, ""
|
|
272
303
|
return (line[:start] + operands[:cut]).rstrip(), operands[cut:].strip()
|
|
273
|
-
cut = _find_comment_start(operands)
|
|
304
|
+
cut = _find_comment_start(operands, paren_depth)
|
|
274
305
|
if cut is None:
|
|
275
306
|
return line, ""
|
|
276
307
|
return (line[:start] + operands[:cut]).rstrip(), operands[cut:].strip()
|
|
@@ -363,6 +363,20 @@ def _tokenize(text: str) -> list[str]: # noqa: C901 -- complexity baseline, spl
|
|
|
363
363
|
# ---------------------------------------------------------------------------
|
|
364
364
|
|
|
365
365
|
|
|
366
|
+
def _split_step_ref(token: str) -> tuple[str | None, str]:
|
|
367
|
+
"""Split ``[STEP.[INNER.]]ATTR`` into ``(step_name, ATTR)`` (uppercased).
|
|
368
|
+
|
|
369
|
+
The three-level form ``OUTERST.INNERST.ATTR`` (a step of an exploded
|
|
370
|
+
PROC) yields the step name ``OUTERST.INNERST``.
|
|
371
|
+
"""
|
|
372
|
+
if "." not in token:
|
|
373
|
+
return None, token.upper()
|
|
374
|
+
parts = token.split(".", 2) # at most 3 parts
|
|
375
|
+
if len(parts) == 3:
|
|
376
|
+
return f"{parts[0]}.{parts[1]}".upper(), parts[2].upper()
|
|
377
|
+
return parts[0].upper(), parts[1].upper()
|
|
378
|
+
|
|
379
|
+
|
|
366
380
|
class _Parser:
|
|
367
381
|
"""Internal recursive-descent parser for condition expressions."""
|
|
368
382
|
|
|
@@ -419,7 +433,7 @@ class _Parser:
|
|
|
419
433
|
return NotCondition(self._parse_not())
|
|
420
434
|
return self._parse_primary()
|
|
421
435
|
|
|
422
|
-
def _parse_primary(self) -> ConditionNode:
|
|
436
|
+
def _parse_primary(self) -> ConditionNode:
|
|
423
437
|
tok = self._peek()
|
|
424
438
|
if tok is None:
|
|
425
439
|
raise ValueError("Unexpected end of condition expression")
|
|
@@ -439,74 +453,63 @@ class _Parser:
|
|
|
439
453
|
# produced after PROC explosion; "PROCSTEP.INNERSTEP" is used as the
|
|
440
454
|
# step-name key in step_results).
|
|
441
455
|
token = self._consume()
|
|
442
|
-
step_name
|
|
443
|
-
attr: str
|
|
444
|
-
|
|
445
|
-
if "." in token:
|
|
446
|
-
parts = token.split(".", 2) # at most 3 parts
|
|
447
|
-
if len(parts) == 3:
|
|
448
|
-
# Three-level: OUTERST.INNERST.ATTR
|
|
449
|
-
step_name = f"{parts[0]}.{parts[1]}".upper()
|
|
450
|
-
attr = parts[2].upper()
|
|
451
|
-
else:
|
|
452
|
-
step_name = parts[0].upper()
|
|
453
|
-
attr = parts[1].upper()
|
|
454
|
-
else:
|
|
455
|
-
attr = token.upper()
|
|
456
|
+
step_name, attr = _split_step_ref(token)
|
|
456
457
|
|
|
457
458
|
if attr == "ABEND":
|
|
458
459
|
return self._boolean_test(AbendCondition(step_name=step_name))
|
|
459
|
-
|
|
460
460
|
if attr == "RUN":
|
|
461
461
|
return self._boolean_test(RunCondition(step_name=step_name))
|
|
462
|
-
|
|
463
462
|
if attr == "RC":
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
# nothing, and reading it as ``RC EQ 0`` invents one.
|
|
469
|
-
raise ValueError(f"Expected an operator after RC, got {op_tok!r}")
|
|
470
|
-
|
|
471
|
-
self._consume() # consume operator
|
|
463
|
+
return self._rc_comparison(step_name)
|
|
464
|
+
if _RE_DIGITS.match(attr) and step_name is None:
|
|
465
|
+
return self._value_first_comparison(attr)
|
|
466
|
+
raise ValueError(f"Unrecognised condition atom: {token!r}")
|
|
472
467
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
# holds when RC is greater than 4, i.e. ``RC GT 4``.
|
|
487
|
-
if _RE_DIGITS.match(attr):
|
|
488
|
-
op_tok = self._peek()
|
|
489
|
-
op = _OP_MAP.get(op_tok.upper()) if op_tok else None
|
|
490
|
-
if op:
|
|
491
|
-
self._consume()
|
|
492
|
-
# After the operator the user may have written "RC" or a step ref
|
|
493
|
-
next_tok = self._peek()
|
|
494
|
-
ref = next_tok.upper() if next_tok else ""
|
|
495
|
-
if ref == "RC" or ref.endswith(".RC"):
|
|
496
|
-
self._consume()
|
|
497
|
-
ref_step = ref[: -len(".RC")] if ref != "RC" else None
|
|
498
|
-
return ReturnCodeCondition(
|
|
499
|
-
step_name=ref_step or None,
|
|
500
|
-
operator=_MIRRORED_OP[op],
|
|
501
|
-
value=int(attr),
|
|
502
|
-
)
|
|
503
|
-
raise ValueError(f"Expected RC after {attr} {op_tok}, got {next_tok!r}")
|
|
504
|
-
# Fallback: single number, treat as RC EQ value
|
|
468
|
+
def _rc_comparison(self, step_name: str | None) -> ConditionNode:
|
|
469
|
+
"""Parse ``op value`` after ``[STEP.]RC``."""
|
|
470
|
+
op_tok = self._peek()
|
|
471
|
+
op = _OP_MAP.get(op_tok.upper()) if op_tok else None
|
|
472
|
+
if op is None:
|
|
473
|
+
# ``RC`` is not a Boolean: without a comparison it tests
|
|
474
|
+
# nothing, and reading it as ``RC EQ 0`` invents one.
|
|
475
|
+
raise ValueError(f"Expected an operator after RC, got {op_tok!r}")
|
|
476
|
+
self._consume() # consume operator
|
|
477
|
+
|
|
478
|
+
val_tok = self._peek()
|
|
479
|
+
if val_tok is not None and _RE_DIGITS.match(val_tok):
|
|
480
|
+
self._consume()
|
|
505
481
|
return ReturnCodeCondition(
|
|
506
|
-
step_name=step_name, operator=
|
|
482
|
+
step_name=step_name, operator=op, value=int(val_tok)
|
|
507
483
|
)
|
|
508
|
-
|
|
509
|
-
|
|
484
|
+
# A non-numeric value (e.g. an unresolved &SYMBOL) cannot be
|
|
485
|
+
# compared; reading it as 0 would invent a different condition.
|
|
486
|
+
raise ValueError(f"Expected a numeric RC value, got {val_tok!r}")
|
|
487
|
+
|
|
488
|
+
def _value_first_comparison(self, value: str) -> ConditionNode:
|
|
489
|
+
"""Parse ``op [STEP.]RC`` after a leading number (rare).
|
|
490
|
+
|
|
491
|
+
The operands are the other way round, so the operator is mirrored --
|
|
492
|
+
``4 LT RC`` holds when RC is greater than 4, i.e. ``RC GT 4``. A
|
|
493
|
+
bare number with no comparison tests nothing; reading it as
|
|
494
|
+
``RC EQ n`` would invent a condition (issue #86, like the bare
|
|
495
|
+
``RC`` of issue #63), so it is rejected.
|
|
496
|
+
"""
|
|
497
|
+
op_tok = self._peek()
|
|
498
|
+
op = _OP_MAP.get(op_tok.upper()) if op_tok else None
|
|
499
|
+
if op is None:
|
|
500
|
+
raise ValueError(f"Expected an operator after {value}, got {op_tok!r}")
|
|
501
|
+
self._consume()
|
|
502
|
+
next_tok = self._peek()
|
|
503
|
+
ref = next_tok.upper() if next_tok else ""
|
|
504
|
+
if ref != "RC" and not ref.endswith(".RC"):
|
|
505
|
+
raise ValueError(f"Expected RC after {value} {op_tok}, got {next_tok!r}")
|
|
506
|
+
self._consume()
|
|
507
|
+
ref_step = ref[: -len(".RC")] if ref != "RC" else None
|
|
508
|
+
return ReturnCodeCondition(
|
|
509
|
+
step_name=ref_step or None,
|
|
510
|
+
operator=_MIRRORED_OP[op],
|
|
511
|
+
value=int(value),
|
|
512
|
+
)
|
|
510
513
|
|
|
511
514
|
def _boolean_test(self, node: ConditionNode) -> ConditionNode:
|
|
512
515
|
"""Consume an optional ``= TRUE`` / ``= FALSE`` after ABEND or RUN."""
|
|
@@ -300,18 +300,18 @@ class ContinuationLineHandler:
|
|
|
300
300
|
if not next_stripped:
|
|
301
301
|
break
|
|
302
302
|
|
|
303
|
-
# A continuation record
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
303
|
+
# A continuation record has no comment field of its own while
|
|
304
|
+
# a quoted string is being continued (the blank would be inside
|
|
305
|
+
# it). Inside a parenthesised list it may have one after a
|
|
306
|
+
# subparameter's comma, so the open depth is passed along.
|
|
307
307
|
# The comment is only recorded once the record is known to be
|
|
308
308
|
# merged: a record that starts the next statement is stripped
|
|
309
309
|
# (and recorded) again as that statement, which would report
|
|
310
310
|
# the same comment twice.
|
|
311
311
|
next_comment = ""
|
|
312
|
-
if not cached_ctx.in_quotes
|
|
312
|
+
if not cached_ctx.in_quotes:
|
|
313
313
|
next_line, next_comment = split_comment_field(
|
|
314
|
-
next_line, group_operation
|
|
314
|
+
next_line, group_operation, cached_ctx.paren_depth
|
|
315
315
|
)
|
|
316
316
|
|
|
317
317
|
# Check if next line is a continuation, passing cached context
|