jcl-parser 2.2.3__tar.gz → 2.3.0__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.
Files changed (80) hide show
  1. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/PKG-INFO +1 -1
  2. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jcl_parser.egg-info/PKG-INFO +1 -1
  3. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/__init__.py +1 -1
  4. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/cli.py +125 -7
  5. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/explosion.py +106 -0
  6. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/dfsrrc00.py +48 -0
  7. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/validators.py +20 -2
  8. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/pyproject.toml +1 -1
  9. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/LICENSE +0 -0
  10. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/README.md +0 -0
  11. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jcl_parser.egg-info/SOURCES.txt +0 -0
  12. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jcl_parser.egg-info/dependency_links.txt +0 -0
  13. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jcl_parser.egg-info/entry_points.txt +0 -0
  14. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jcl_parser.egg-info/requires.txt +0 -0
  15. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jcl_parser.egg-info/top_level.txt +0 -0
  16. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/__main__.py +0 -0
  17. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/_include_origins.py +0 -0
  18. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/_job_builder.py +0 -0
  19. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/_model_factories.py +0 -0
  20. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/_post_parse.py +0 -0
  21. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/_source_scanner.py +0 -0
  22. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/_statement_parser.py +0 -0
  23. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/builtin_procs.py +0 -0
  24. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/checks.py +0 -0
  25. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/condition_parser.py +0 -0
  26. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/continuation_handler.py +0 -0
  27. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/dataset_lifecycle.py +0 -0
  28. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/errors.py +0 -0
  29. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/include_loader.py +0 -0
  30. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/jcl_file_type.py +0 -0
  31. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/jes_statements.py +0 -0
  32. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/lineage.py +0 -0
  33. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/lint/__init__.py +0 -0
  34. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/lint/base.py +0 -0
  35. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/lint/config.py +0 -0
  36. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/lint/rules.py +0 -0
  37. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/lint/runner.py +0 -0
  38. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/lint/suppress.py +0 -0
  39. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/metrics.py +0 -0
  40. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/model.py +0 -0
  41. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/parameter_ast.py +0 -0
  42. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/parameter_validators.py +0 -0
  43. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/parser.py +0 -0
  44. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/plugins/__init__.py +0 -0
  45. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/plugins/base.py +0 -0
  46. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/proc_loader.py +0 -0
  47. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/profiling.py +0 -0
  48. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/__init__.py +0 -0
  49. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/_control.py +0 -0
  50. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/_invoked.py +0 -0
  51. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/adrdssu.py +0 -0
  52. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/base.py +0 -0
  53. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/binder.py +0 -0
  54. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/bpxbatch.py +0 -0
  55. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/compilers.py +0 -0
  56. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/db2_batch.py +0 -0
  57. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/dsnutilb.py +0 -0
  58. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/ftp.py +0 -0
  59. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/icetool.py +0 -0
  60. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/idcams.py +0 -0
  61. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iebcompr.py +0 -0
  62. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iebdg.py +0 -0
  63. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iebgener.py +0 -0
  64. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iebptpch.py +0 -0
  65. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iebupdte.py +0 -0
  66. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iefbr14.py +0 -0
  67. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iehinitt.py +0 -0
  68. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iehlist.py +0 -0
  69. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/iehprogm.py +0 -0
  70. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/ikjeft01.py +0 -0
  71. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/irxjcl.py +0 -0
  72. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/registry.py +0 -0
  73. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/program_profiles/sort.py +0 -0
  74. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/py.typed +0 -0
  75. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/rc_analysis.py +0 -0
  76. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/serialization.py +0 -0
  77. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/sql_detector.py +0 -0
  78. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/system_symbols.py +0 -0
  79. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/jclparser/utils.py +0 -0
  80. {jcl_parser-2.2.3 → jcl_parser-2.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jcl-parser
3
- Version: 2.2.3
3
+ Version: 2.3.0
4
4
  Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
5
5
  Author-email: Samuel Dion <sam94dion@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jcl-parser
3
- Version: 2.2.3
3
+ Version: 2.3.0
4
4
  Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
5
5
  Author-email: Samuel Dion <sam94dion@gmail.com>
6
6
  License-Expression: AGPL-3.0-or-later
@@ -20,7 +20,7 @@ application, for example::
20
20
  import logging as _logging
21
21
 
22
22
  # Package metadata
23
- __version__ = "2.2.3" # x-release-please-version
23
+ __version__ = "2.3.0" # x-release-please-version
24
24
  __author__ = "JCL Parser Team"
25
25
  __description__ = "Parse IBM JCL scripts into Python objects"
26
26
 
@@ -119,9 +119,15 @@ def _main(argv: list[str] | None = None) -> int:
119
119
  # ("FILE --explode steps"); plain parse_args() hands the whole positional
120
120
  # run to FILE/query before the flag and then rejects "steps".
121
121
  arg_parser = _build_arg_parser()
122
+ if not _argv:
123
+ # A bare `jcl-parser` used to print only "the following arguments
124
+ # are required: FILE"; show the queries and examples instead.
125
+ arg_parser.print_help()
126
+ return 2
122
127
  args = arg_parser.parse_intermixed_args(argv)
123
128
  if args.profile_output and not args.profile:
124
129
  arg_parser.error("--profile-output requires --profile") # exits 2
130
+ _apply_format(args)
125
131
 
126
132
  source: Path | str
127
133
  if args.file == "-":
@@ -160,6 +166,14 @@ def _main(argv: list[str] | None = None) -> int:
160
166
  return _run_query_with_profile(job, args, prof, source)
161
167
 
162
168
 
169
+ def _apply_format(args: argparse.Namespace) -> None:
170
+ """``--format json|sarif`` sets the older ``--json`` / ``--sarif`` flags."""
171
+ if args.format == "json":
172
+ args.json_output = True
173
+ elif args.format == "sarif":
174
+ args.sarif = True
175
+
176
+
163
177
  def _read_stdin() -> str:
164
178
  """The JCL piped on stdin, decoded like a file (``FILE`` is ``-``)."""
165
179
  return read_source(getattr(sys.stdin, "buffer", sys.stdin))
@@ -196,10 +210,70 @@ def _is_step_or_dd_name(job: Job, token: str) -> bool:
196
210
  )
197
211
 
198
212
 
213
+ #: The query words ``jcl-parser FILE <word>`` understands, for typo hints.
214
+ _QUERY_WORDS = (
215
+ "steps",
216
+ "procs",
217
+ "detect",
218
+ "metrics",
219
+ "lint",
220
+ "datasets",
221
+ "sql",
222
+ "analyze",
223
+ "paths",
224
+ "lineage",
225
+ )
226
+
227
+ _HELP_EPILOG = """\
228
+ queries (the words after FILE):
229
+ (none) job summary; with --json the whole parse as JSON
230
+ steps every step, the program or PROC it runs, its DDs
231
+ procs [PROCNAME] inline/external PROCs; --explode shows the expansion
232
+ STEP one step in detail e.g. STEP010
233
+ STEP.DDNAME one DD statement e.g. STEP010.SYSIN
234
+ DDNAME that DD in every step e.g. SYSOUT
235
+ detect is the file a JOB, a PROC, or neither
236
+ metrics step, DD, PROC and dataset counts, complexity
237
+ lint lint rules (--lint-list lists them; --sarif for CI)
238
+ datasets dataset lifecycle: created, read, deleted, passed
239
+ sql DB2 / SQL usage by step
240
+ analyze [STEP] what each utility step (SORT, IDCAMS, ...) does
241
+ paths which steps can run, from the COND / IF logic
242
+
243
+ FILE may be a directory: 'lineage [dot|mermaid]' (the default) maps
244
+ datasets across its jobs, 'lint' and --validate check every file
245
+ (--glob picks them). FILE '-' reads the JCL from stdin.
246
+
247
+ examples:
248
+ jcl-parser PAYROLL.jcl
249
+ jcl-parser PAYROLL.jcl steps --explode --proc-path ./proclib
250
+ jcl-parser PAYROLL.jcl STEP010.SYSIN
251
+ jcl-parser PAYROLL.jcl lint --fail-on warning
252
+ jcl-parser PAYROLL.jcl --validate --format json
253
+ jcl-parser jobs/ lineage mermaid > lineage.mmd
254
+ jcl-lint jobs/*.jcl --format sarif -o jcl.sarif (several files, for CI)
255
+
256
+ exit codes:
257
+ 0 success
258
+ 1 the file could not be read or parsed, a name was not found, or a check
259
+ reported something (--validate errors, lint findings at --fail-on or
260
+ above, dataset / SQL findings)
261
+ 2 the command line was rejected
262
+
263
+ defaults:
264
+ lint reads [tool.jcl-parser.lint] from the nearest pyproject.toml (or
265
+ --config). External PROCs are loaded only with --proc-path, except by
266
+ --validate, which falls back to the JCL_PROCLIB_PATH variable.
267
+ """
268
+
269
+
199
270
  def _build_arg_parser() -> argparse.ArgumentParser:
200
271
  parser = argparse.ArgumentParser(
201
272
  prog="jcl-parser",
202
- description="Parse and inspect IBM JCL files.",
273
+ usage="jcl-parser FILE [QUERY ...] [options]",
274
+ description="Parse and inspect IBM z/OS JCL: jobs, steps, DDs and PROCs.",
275
+ epilog=_HELP_EPILOG,
276
+ formatter_class=argparse.RawDescriptionHelpFormatter,
203
277
  )
204
278
  parser.add_argument(
205
279
  "file",
@@ -210,15 +284,22 @@ def _build_arg_parser() -> argparse.ArgumentParser:
210
284
  "query",
211
285
  nargs="*",
212
286
  default=[],
213
- help=(
214
- "Query: 'steps', 'procs [PROCNAME]', a step name, STEP.DDNAME, "
215
- "or a DD name to find across all steps; for a directory, "
216
- "'lineage [dot|mermaid]'"
217
- ),
287
+ metavar="QUERY",
288
+ help="What to show (see 'queries' below); none prints the job summary",
218
289
  )
219
290
  parser.add_argument(
220
291
  "--json", action="store_true", dest="json_output", help="Output as JSON"
221
292
  )
293
+ parser.add_argument(
294
+ "--format",
295
+ choices=("text", "json", "sarif"),
296
+ default=None,
297
+ help=(
298
+ "Output format: text (default), json (same as --json) or sarif "
299
+ "(same as --sarif; lint and --validate only). The spelling "
300
+ "cobol-parser and jcl-lint use"
301
+ ),
302
+ )
222
303
  parser.add_argument(
223
304
  "--validate", action="store_true", help="Validate the JCL and report errors"
224
305
  )
@@ -469,7 +550,44 @@ def _cmd_step_or_dd(job: Job, query: str, json_out: bool) -> int:
469
550
  return 0
470
551
 
471
552
  # Fall back to DD name across all steps
472
- return _print_dd_across_steps(job, query.upper(), json_out)
553
+ status = _print_dd_across_steps(job, query.upper(), json_out)
554
+ if status != 0:
555
+ hint = _not_found_hint(job, query)
556
+ if hint:
557
+ print(hint, file=sys.stderr)
558
+ return status
559
+
560
+
561
+ def _not_found_hint(job: Job, query: str) -> str | None:
562
+ """A "did you mean" line for a query that named no step or DD.
563
+
564
+ A mistyped query word (``stpes``) used to read only as "No step or DD
565
+ named 'STPES' found", which sends a new user looking for a step.
566
+ """
567
+ import difflib
568
+
569
+ words = difflib.get_close_matches(query.lower(), _QUERY_WORDS, n=1, cutoff=0.6)
570
+ names = sorted(
571
+ {s.name for s in iter_execution_steps(job.steps) if s.name}
572
+ | {
573
+ dd.name
574
+ for s in iter_execution_steps(job.steps)
575
+ for dd in s.dd_statements
576
+ if dd.name
577
+ }
578
+ )
579
+ close = difflib.get_close_matches(query.upper(), names, n=3, cutoff=0.6)
580
+ # JCL names are uppercase: an uppercase query is a misspelt name first,
581
+ # a lowercase one a misspelt query word first.
582
+ name_hint = "Did you mean: " + ", ".join(close) + "?" if close else None
583
+ word_hint = (
584
+ f"Did you mean the query '{words[0]}'? (see 'jcl-parser --help')"
585
+ if words
586
+ else None
587
+ )
588
+ if query == query.upper():
589
+ return name_hint or word_hint
590
+ return word_hint or name_hint
473
591
 
474
592
 
475
593
  def _cmd_directory(path: Path, args: argparse.Namespace) -> int:
@@ -1219,6 +1219,104 @@ def is_exec_override_keyword(key: str) -> bool:
1219
1219
  return key.upper().partition(".")[0] in _EXEC_OVERRIDE_KEYWORDS
1220
1220
 
1221
1221
 
1222
+ _SYMBOL_REFERENCE = re.compile(r"&&|&([A-Z@#$][A-Z0-9@#$]{0,7})", re.IGNORECASE)
1223
+ _INCLUDE_STATEMENT = re.compile(r"^//\S*\s+INCLUDE\s", re.IGNORECASE | re.MULTILINE)
1224
+
1225
+
1226
+ def _referenced_symbols(value: Any, found: set[str], seen: set[int]) -> None:
1227
+ """Add every ``&NAME`` written anywhere in *value* to *found*.
1228
+
1229
+ Walks strings, dicts, lists and the model dataclasses (steps, DDs,
1230
+ conditional blocks) a procedure body is made of. ``&&`` is an escaped
1231
+ ampersand, not a reference.
1232
+ """
1233
+ if isinstance(value, str):
1234
+ for match in _SYMBOL_REFERENCE.finditer(value):
1235
+ if match.group(1):
1236
+ found.add(match.group(1).upper())
1237
+ return
1238
+ if id(value) in seen:
1239
+ return
1240
+ seen.add(id(value))
1241
+ if isinstance(value, dict):
1242
+ for item in value.values():
1243
+ _referenced_symbols(item, found, seen)
1244
+ elif isinstance(value, (list, tuple)):
1245
+ for item in value:
1246
+ _referenced_symbols(item, found, seen)
1247
+ elif isinstance(value, (Step, DDStatement, ConditionalBlock)):
1248
+ for name, item in vars(value).items():
1249
+ if name not in ("comments", "inline_data", "original_proc_call"):
1250
+ _referenced_symbols(item, found, seen)
1251
+
1252
+
1253
+ def call_override_symbols(call: Step) -> set[str]:
1254
+ """Every ``&NAME`` in the overrides coded with PROC call *call*.
1255
+
1256
+ The overriding and added DDs after the call (and their concatenations)
1257
+ and the values of its keyword overrides (``PARM.PS1='&X'``) are merged
1258
+ into the procedure, so the call's symbolics are substituted there too
1259
+ (issue #176).
1260
+ """
1261
+ found: set[str] = set()
1262
+ _referenced_symbols(
1263
+ [
1264
+ call.dd_statements,
1265
+ list(call.dd_overrides.values()),
1266
+ list(call.dd_override_concatenations.values()),
1267
+ [
1268
+ value
1269
+ for key, value in call.parameters.items()
1270
+ if is_exec_override_keyword(key)
1271
+ ],
1272
+ ],
1273
+ found,
1274
+ set(),
1275
+ )
1276
+ return found
1277
+
1278
+
1279
+ def unused_proc_symbols(
1280
+ call: Step, procedure: Procedure, call_references: set[str] | None = None
1281
+ ) -> list[str]:
1282
+ """The symbolics PROC call *call* assigns that *procedure* never uses.
1283
+
1284
+ A symbolic coded on ``EXEC procname`` that is neither declared on the
1285
+ PROC statement nor referenced as ``&NAME`` in the procedure body is a
1286
+ JCL error on z/OS (``IEFC657I THE SYMBOL x WAS NOT USED``), and the
1287
+ expansion silently runs with the PROC's defaults (issue #172). EXEC
1288
+ keyword overrides (``PARM``, ``COND.PS1``, ...) are not symbolics. A
1289
+ reference in the call's overriding or added DDs, or in a keyword
1290
+ override's value, counts as a use (issue #176); *call_references* gives
1291
+ those references when the call's overrides were already substituted
1292
+ (default: :func:`call_override_symbols` of *call*).
1293
+
1294
+ Returns an empty list when the body cannot be fully checked: a procedure
1295
+ with no steps (unloaded), or one whose text has an ``INCLUDE`` (its
1296
+ members' references are not known before explosion).
1297
+ """
1298
+ if not procedure.steps:
1299
+ return []
1300
+ if procedure.source_text and _INCLUDE_STATEMENT.search(procedure.source_text):
1301
+ return []
1302
+ known = {name.upper() for name in procedure.parameters}
1303
+ _referenced_symbols(
1304
+ [procedure.steps, procedure.set_statements, procedure.parameters],
1305
+ known,
1306
+ set(),
1307
+ )
1308
+ if procedure.source_text:
1309
+ _referenced_symbols(procedure.source_text, known, set())
1310
+ known |= call_override_symbols(call) if call_references is None else call_references
1311
+ return [
1312
+ key
1313
+ for key in call.parameters
1314
+ if not is_exec_override_keyword(key)
1315
+ and key.upper() != "PROC"
1316
+ and key.upper() not in known
1317
+ ]
1318
+
1319
+
1222
1320
  def _substitute_proc_call_parameters(
1223
1321
  parameters: dict[str, Any],
1224
1322
  scope: SymbolScope,
@@ -2099,6 +2197,8 @@ def _explode_step(
2099
2197
  # against the job's library order. Pass an immutable tuple: loaders key their
2100
2198
  # caches on the library list, so a hashable value keeps that contract safe.
2101
2199
  _libs = tuple(jcllib_order) if jcllib_order else None
2200
+ # Read before loading resolves the call's overrides (issue #176).
2201
+ call_references = call_override_symbols(step)
2102
2202
  procedure = _load_called_procedure(
2103
2203
  step, proc_loader, _libs, base_scope, call_scope, strict_symbols, warnings
2104
2204
  )
@@ -2121,6 +2221,12 @@ def _explode_step(
2121
2221
  )
2122
2222
 
2123
2223
  _apply_call_overrides(expanded, step, procedure.name or proc_name, warnings)
2224
+ unused = unused_proc_symbols(step, procedure, call_references)
2225
+ if unused and warnings is not None:
2226
+ warnings.append(
2227
+ f"EXEC {step.name or 'PROC'}: symbol(s) {', '.join(unused)} not used "
2228
+ f"by PROC {procedure.name or proc_name} (IEFC657I); its defaults apply"
2229
+ )
2124
2230
  _record_expansion(
2125
2231
  step, procedure, overrides, expanded, depth, audit_records, expansion_map
2126
2232
  )
@@ -13,12 +13,29 @@ comma-delimited sub-parameter of PARM:
13
13
  The profile extracts the application program and PSB name from PARM so that
14
14
  ``analyze_step``/``analyze_job`` can report what application is actually
15
15
  running, not just the region controller.
16
+
17
+ That layout holds only for the batch regions (BMP, DLI, DBB). A message
18
+ processing region codes ``PARM=(MSG,&CL1&CL2&CL3&CL4,...)``: PARM[1] is the
19
+ four three-digit transaction classes it serves and it names no program. For
20
+ MSG, IFP and any other region type, only the region type (and, for MSG, the
21
+ classes) is reported (issue #173).
16
22
  """
17
23
 
18
24
  from __future__ import annotations
19
25
 
26
+ from ..model import Step
20
27
  from .base import ProgramAnalysis, ProgramProfile
21
28
 
29
+ #: Region types whose PARM[1] / PARM[2] are the application program and PSB.
30
+ _BATCH_REGION_TYPES = frozenset({"BMP", "DLI", "DBB"})
31
+
32
+
33
+ def _transaction_classes(field: str) -> list[str]:
34
+ """Split an MPP's ``&CL1&CL2&CL3&CL4`` field into its three-digit classes."""
35
+ if not field.isdigit() or len(field) % 3:
36
+ return []
37
+ return [field[i : i + 3] for i in range(0, len(field), 3)]
38
+
22
39
 
23
40
  class DFSRRC00Profile(ProgramProfile):
24
41
  program_names = frozenset({"DFSRRC00", "DFSRRC02"})
@@ -42,6 +59,9 @@ class DFSRRC00Profile(ProgramProfile):
42
59
  parts = [p.strip() for p in parm.split(",")]
43
60
 
44
61
  region_type = parts[0].upper() if parts and parts[0] else ""
62
+ if region_type and region_type not in _BATCH_REGION_TYPES:
63
+ return _online_region_analysis(step, region_type, parts, parm)
64
+
45
65
  app_program = parts[1].upper() if len(parts) > 1 and parts[1] else ""
46
66
  psb_name = parts[2].upper() if len(parts) > 2 and parts[2] else ""
47
67
 
@@ -67,3 +87,31 @@ class DFSRRC00Profile(ProgramProfile):
67
87
  notes=notes,
68
88
  raw_control=parm or None,
69
89
  )
90
+
91
+
92
+ def _online_region_analysis(
93
+ step: Step, region_type: str, parts: list[str], parm: str
94
+ ) -> ProgramAnalysis:
95
+ """A region whose PARM names no program (MSG, IFP, ...): issue #173."""
96
+ notes = [f"IMS region type: {region_type}"]
97
+ classes = (
98
+ _transaction_classes(parts[1])
99
+ if region_type == "MSG" and len(parts) > 1
100
+ else []
101
+ )
102
+ summary = f"IMS {region_type} region ({step.program})"
103
+ if classes:
104
+ summary += f", classes {' '.join(classes)}"
105
+ notes.append(f"transaction classes: {' '.join(classes)}")
106
+ notes.append(
107
+ f"an IMS {region_type} region runs the transactions scheduled to it; "
108
+ "its PARM names no application program"
109
+ )
110
+ return ProgramAnalysis(
111
+ program=step.program or "DFSRRC00",
112
+ step_name=step.name or "",
113
+ summary=summary,
114
+ dataset_events=[],
115
+ notes=notes,
116
+ raw_control=parm or None,
117
+ )
@@ -321,7 +321,10 @@ def validate_backward_references(job: Job) -> list[ValidationError]:
321
321
 
322
322
 
323
323
  def validate_procedure_references(job: Job) -> list[ValidationError]:
324
- """Warn on ``EXEC procname`` calls whose PROC was not found.
324
+ """Check ``EXEC procname`` calls against the PROCs they call.
325
+
326
+ A symbolic the call assigns that the PROC never declares or references
327
+ is an ERROR (``IEFC657I``, issue #172).
325
328
 
326
329
  :func:`jclparser.parse_jcl` adds every PROC it can load (in-stream,
327
330
  JCLLIB / system PROCLIB under the base path, built-in) to
@@ -330,9 +333,24 @@ def validate_procedure_references(job: Job) -> list[ValidationError]:
330
333
  a WARNING, not an ERROR (issue #116). A strict parse of a job with a
331
334
  ``JCLLIB ORDER`` still fails on it before this runs.
332
335
  """
336
+ from .explosion import unused_proc_symbols
337
+
333
338
  errors: list[ValidationError] = []
334
339
  for step in iter_execution_steps(job.steps):
335
- if step.procedure and not job.get_procedure(step.procedure):
340
+ procedure = job.get_procedure(step.procedure) if step.procedure else None
341
+ if procedure is not None:
342
+ # A symbolic the PROC never declares or references (IEFC657I).
343
+ for name in unused_proc_symbols(step, procedure):
344
+ errors.append(
345
+ ValidationError(
346
+ f"Step '{step.name or ''}': symbol {name} is not used by "
347
+ f"procedure '{step.procedure}' (IEFC657I); the "
348
+ "procedure's defaults apply",
349
+ ErrorType.SEMANTIC_ERROR,
350
+ line_number=step.line_number,
351
+ )
352
+ )
353
+ if step.procedure and procedure is None:
336
354
  errors.append(
337
355
  ValidationError(
338
356
  f"Procedure '{step.procedure}' not found in-stream or in "
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "jcl-parser"
7
- version = "2.2.3"
7
+ version = "2.3.0"
8
8
  description = "Parse IBM JCL (Job Control Language) scripts into Python objects"
9
9
  authors = [
10
10
  {name = "Samuel Dion", email = "sam94dion@gmail.com"}
File without changes
File without changes
File without changes