jcl-parser 2.1.0__tar.gz → 2.2.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.
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/PKG-INFO +69 -11
- jcl_parser-2.2.0/README.md +115 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jcl_parser.egg-info/PKG-INFO +69 -11
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jcl_parser.egg-info/SOURCES.txt +4 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jcl_parser.egg-info/entry_points.txt +1 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jcl_parser.egg-info/requires.txt +3 -1
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/__init__.py +29 -4
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/_include_origins.py +3 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/_job_builder.py +76 -5
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/_statement_parser.py +0 -9
- jcl_parser-2.2.0/jclparser/checks.py +382 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/cli.py +458 -200
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/condition_parser.py +55 -64
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/continuation_handler.py +17 -3
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/errors.py +18 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/explosion.py +103 -79
- jcl_parser-2.2.0/jclparser/lineage.py +498 -0
- jcl_parser-2.2.0/jclparser/lint/__init__.py +47 -0
- jcl_parser-2.2.0/jclparser/lint/config.py +189 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/lint/rules.py +114 -4
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/lint/runner.py +62 -36
- jcl_parser-2.2.0/jclparser/lint/suppress.py +81 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/model.py +68 -4
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/parameter_ast.py +4 -2
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/parameter_validators.py +58 -50
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/parser.py +8 -3
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/profiling.py +5 -1
- jcl_parser-2.2.0/jclparser/program_profiles/adrdssu.py +116 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/binder.py +22 -4
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/iefbr14.py +9 -2
- jcl_parser-2.2.0/jclparser/program_profiles/iehlist.py +87 -0
- jcl_parser-2.2.0/jclparser/program_profiles/iehprogm.py +94 -0
- jcl_parser-2.2.0/jclparser/serialization.py +330 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/utils.py +19 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/validators.py +128 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/pyproject.toml +9 -6
- jcl_parser-2.1.0/README.md +0 -58
- jcl_parser-2.1.0/jclparser/lint/__init__.py +0 -28
- jcl_parser-2.1.0/jclparser/lint/config.py +0 -39
- jcl_parser-2.1.0/jclparser/program_profiles/adrdssu.py +0 -72
- jcl_parser-2.1.0/jclparser/program_profiles/iehlist.py +0 -54
- jcl_parser-2.1.0/jclparser/program_profiles/iehprogm.py +0 -108
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/LICENSE +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jcl_parser.egg-info/dependency_links.txt +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jcl_parser.egg-info/top_level.txt +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/__main__.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/_model_factories.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/_post_parse.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/_source_scanner.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/builtin_procs.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/dataset_lifecycle.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/include_loader.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/jcl_file_type.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/jes_statements.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/lint/base.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/metrics.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/plugins/__init__.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/plugins/base.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/proc_loader.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/__init__.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/_control.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/_invoked.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/base.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/bpxbatch.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/compilers.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/db2_batch.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/dfsrrc00.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/dsnutilb.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/ftp.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/icetool.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/idcams.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/iebcompr.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/iebdg.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/iebgener.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/iebptpch.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/iebupdte.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/iehinitt.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/ikjeft01.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/irxjcl.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/registry.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/program_profiles/sort.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/py.typed +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/rc_analysis.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/sql_detector.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/jclparser/system_symbols.py +0 -0
- {jcl_parser-2.1.0 → jcl_parser-2.2.0}/setup.cfg +0 -0
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jcl-parser
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
|
|
5
5
|
Author-email: Samuel Dion <sam94dion@gmail.com>
|
|
6
|
-
License: AGPL-3.0-or-later
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/landparty/jcl-parser
|
|
8
8
|
Project-URL: Repository, https://github.com/landparty/jcl-parser
|
|
9
|
-
Project-URL: Documentation, https://github.com/landparty/jcl-parser/
|
|
9
|
+
Project-URL: Documentation, https://github.com/landparty/jcl-parser/blob/main/.okf/index.md
|
|
10
10
|
Project-URL: Bug Reports, https://github.com/landparty/jcl-parser/issues
|
|
11
11
|
Keywords: jcl,mainframe,parser,ibm,job-control-language
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -28,12 +27,14 @@ Requires-Dist: pytest>=7.0; extra == "dev"
|
|
|
28
27
|
Requires-Dist: coverage[toml]>=7.0; extra == "dev"
|
|
29
28
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
29
|
Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
|
|
31
|
-
Requires-Dist: ruff
|
|
30
|
+
Requires-Dist: ruff==0.17.3; extra == "dev"
|
|
32
31
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: jsonschema>=4.18; extra == "dev"
|
|
33
33
|
Requires-Dist: tomli>=1.1.0; python_version < "3.11" and extra == "dev"
|
|
34
34
|
Provides-Extra: test
|
|
35
35
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
36
36
|
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
37
|
+
Requires-Dist: jsonschema>=4.18; extra == "test"
|
|
37
38
|
Requires-Dist: tomli>=1.1.0; python_version < "3.11" and extra == "test"
|
|
38
39
|
Provides-Extra: profiling
|
|
39
40
|
Requires-Dist: psutil>=5.9; extra == "profiling"
|
|
@@ -50,7 +51,8 @@ Python library for parsing IBM z/OS JCL (Job Control Language) into structured `
|
|
|
50
51
|
## Install
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
|
-
pip install -
|
|
54
|
+
pip install jcl-parser # from PyPI
|
|
55
|
+
pip install -e ".[dev]" # from a checkout, with the dev tools
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Requires Python 3.10+. No runtime dependencies.
|
|
@@ -58,11 +60,20 @@ Requires Python 3.10+. No runtime dependencies.
|
|
|
58
60
|
## Quick Start
|
|
59
61
|
|
|
60
62
|
```python
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
61
65
|
import jclparser
|
|
62
66
|
|
|
63
|
-
job = jclparser.parse_jcl(
|
|
67
|
+
job = jclparser.parse_jcl(Path("PAYROLL.jcl")) # a str is always JCL text, never a path
|
|
64
68
|
print(job.name)
|
|
65
|
-
|
|
69
|
+
for step in jclparser.iter_execution_steps(job.steps): # also walks IF/THEN/ELSE blocks
|
|
70
|
+
print(step.name, step.program or step.procedure)
|
|
71
|
+
|
|
72
|
+
exploded = jclparser.explode_job(job) # PROC expansion is a separate call
|
|
73
|
+
errors = jclparser.validate_jcl(
|
|
74
|
+
Path("PAYROLL.jcl"), level="structure"
|
|
75
|
+
) # list[ParseError]
|
|
76
|
+
print(job.to_json(indent=2)) # schema: jclparser.JOB_JSON_SCHEMA
|
|
66
77
|
```
|
|
67
78
|
|
|
68
79
|
CLI:
|
|
@@ -71,7 +82,52 @@ CLI:
|
|
|
71
82
|
jcl-parser FILE steps # list all steps
|
|
72
83
|
jcl-parser FILE --validate # validate JCL
|
|
73
84
|
jcl-parser FILE lint # run lint rules
|
|
74
|
-
jcl-parser FILE --json # full parse result as JSON
|
|
85
|
+
jcl-parser FILE --json # full parse result as JSON (job.to_dict())
|
|
86
|
+
jcl-parser --json-schema # JSON Schema of that document
|
|
87
|
+
jcl-parser DIR # cross-job dataset lineage of a directory of jobs
|
|
88
|
+
jcl-parser DIR lineage mermaid # ... as a Mermaid graph (or `dot`)
|
|
89
|
+
jcl-parser DIR lint --sarif # lint every job of a directory, as SARIF
|
|
90
|
+
jcl-lint jobs/ --fail-on warning --format sarif -o jcl.sarif # CI / pre-commit
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Lint in CI
|
|
94
|
+
|
|
95
|
+
Configure the rules in `pyproject.toml`, and switch one off for one statement
|
|
96
|
+
with a comment:
|
|
97
|
+
|
|
98
|
+
```toml
|
|
99
|
+
[tool.jcl-parser.lint]
|
|
100
|
+
ignore = ["JCL-LINT-002"]
|
|
101
|
+
fail-on = "warning"
|
|
102
|
+
[tool.jcl-parser.lint.severity]
|
|
103
|
+
"JCL-LINT-004" = "warning"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```jcl
|
|
107
|
+
//* jcl-lint: disable=JCL-LINT-001
|
|
108
|
+
//MASTER DD DSN=PROD.MASTER.FILE,DISP=SHR
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
pre-commit (`.pre-commit-config.yaml`):
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
- repo: https://github.com/landparty/jcl-parser
|
|
115
|
+
rev: v2.2.0 # first release with the hook
|
|
116
|
+
hooks:
|
|
117
|
+
- id: jcl-lint
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
GitHub Actions (findings appear in code scanning):
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
permissions:
|
|
124
|
+
security-events: write
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@v4
|
|
127
|
+
- uses: landparty/jcl-parser@v2.2.0 # first release with the action
|
|
128
|
+
with:
|
|
129
|
+
paths: jcl
|
|
130
|
+
glob: "**/*.jcl"
|
|
75
131
|
```
|
|
76
132
|
|
|
77
133
|
## Commands
|
|
@@ -83,7 +139,7 @@ pytest -m unit
|
|
|
83
139
|
|
|
84
140
|
# Lint / format / type-check
|
|
85
141
|
ruff check . && ruff format .
|
|
86
|
-
mypy
|
|
142
|
+
python scripts/mypy_ratchet.py # mypy strict against the known-error baseline, as CI runs it
|
|
87
143
|
|
|
88
144
|
# Vendor sync (after changes to jclparser/) — rm first: plain `cp -r` nests
|
|
89
145
|
# into dest/jclparser/jclparser/ and leaves stale files when dest exists
|
|
@@ -92,7 +148,9 @@ rm -rf ../backend/_vendor/jcl-parser/jclparser && cp -r jclparser/ ../backend/_v
|
|
|
92
148
|
|
|
93
149
|
## Documentation
|
|
94
150
|
|
|
95
|
-
Full reference: [`.okf/index.md`](.okf/index.md)
|
|
151
|
+
Full reference: [`.okf/index.md`](.okf/index.md). Contributors: start with [`CLAUDE.md`](CLAUDE.md)
|
|
152
|
+
(commands, architecture, invariants) and [`.okf/development/invariants.md`](.okf/development/invariants.md).
|
|
153
|
+
Changes are listed in [`CHANGELOG.md`](CHANGELOG.md) (releases) and [`.okf/log.md`](.okf/log.md).
|
|
96
154
|
|
|
97
155
|
## License
|
|
98
156
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# jcl-parser
|
|
2
|
+
|
|
3
|
+
Python library for parsing IBM z/OS JCL (Job Control Language) into structured `Job`, `Step`, and `DD` objects. Supports PROC expansion, validation, lint rules, SQL detection, and dataset lifecycle analysis.
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](#commands)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install jcl-parser # from PyPI
|
|
13
|
+
pip install -e ".[dev]" # from a checkout, with the dev tools
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Python 3.10+. No runtime dependencies.
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import jclparser
|
|
24
|
+
|
|
25
|
+
job = jclparser.parse_jcl(Path("PAYROLL.jcl")) # a str is always JCL text, never a path
|
|
26
|
+
print(job.name)
|
|
27
|
+
for step in jclparser.iter_execution_steps(job.steps): # also walks IF/THEN/ELSE blocks
|
|
28
|
+
print(step.name, step.program or step.procedure)
|
|
29
|
+
|
|
30
|
+
exploded = jclparser.explode_job(job) # PROC expansion is a separate call
|
|
31
|
+
errors = jclparser.validate_jcl(
|
|
32
|
+
Path("PAYROLL.jcl"), level="structure"
|
|
33
|
+
) # list[ParseError]
|
|
34
|
+
print(job.to_json(indent=2)) # schema: jclparser.JOB_JSON_SCHEMA
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
CLI:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
jcl-parser FILE steps # list all steps
|
|
41
|
+
jcl-parser FILE --validate # validate JCL
|
|
42
|
+
jcl-parser FILE lint # run lint rules
|
|
43
|
+
jcl-parser FILE --json # full parse result as JSON (job.to_dict())
|
|
44
|
+
jcl-parser --json-schema # JSON Schema of that document
|
|
45
|
+
jcl-parser DIR # cross-job dataset lineage of a directory of jobs
|
|
46
|
+
jcl-parser DIR lineage mermaid # ... as a Mermaid graph (or `dot`)
|
|
47
|
+
jcl-parser DIR lint --sarif # lint every job of a directory, as SARIF
|
|
48
|
+
jcl-lint jobs/ --fail-on warning --format sarif -o jcl.sarif # CI / pre-commit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Lint in CI
|
|
52
|
+
|
|
53
|
+
Configure the rules in `pyproject.toml`, and switch one off for one statement
|
|
54
|
+
with a comment:
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
[tool.jcl-parser.lint]
|
|
58
|
+
ignore = ["JCL-LINT-002"]
|
|
59
|
+
fail-on = "warning"
|
|
60
|
+
[tool.jcl-parser.lint.severity]
|
|
61
|
+
"JCL-LINT-004" = "warning"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```jcl
|
|
65
|
+
//* jcl-lint: disable=JCL-LINT-001
|
|
66
|
+
//MASTER DD DSN=PROD.MASTER.FILE,DISP=SHR
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
pre-commit (`.pre-commit-config.yaml`):
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- repo: https://github.com/landparty/jcl-parser
|
|
73
|
+
rev: v2.2.0 # first release with the hook
|
|
74
|
+
hooks:
|
|
75
|
+
- id: jcl-lint
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
GitHub Actions (findings appear in code scanning):
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
permissions:
|
|
82
|
+
security-events: write
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/checkout@v4
|
|
85
|
+
- uses: landparty/jcl-parser@v2.2.0 # first release with the action
|
|
86
|
+
with:
|
|
87
|
+
paths: jcl
|
|
88
|
+
glob: "**/*.jcl"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Commands
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Tests
|
|
95
|
+
pytest
|
|
96
|
+
pytest -m unit
|
|
97
|
+
|
|
98
|
+
# Lint / format / type-check
|
|
99
|
+
ruff check . && ruff format .
|
|
100
|
+
python scripts/mypy_ratchet.py # mypy strict against the known-error baseline, as CI runs it
|
|
101
|
+
|
|
102
|
+
# Vendor sync (after changes to jclparser/) — rm first: plain `cp -r` nests
|
|
103
|
+
# into dest/jclparser/jclparser/ and leaves stale files when dest exists
|
|
104
|
+
rm -rf ../backend/_vendor/jcl-parser/jclparser && cp -r jclparser/ ../backend/_vendor/jcl-parser/jclparser
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
Full reference: [`.okf/index.md`](.okf/index.md). Contributors: start with [`CLAUDE.md`](CLAUDE.md)
|
|
110
|
+
(commands, architecture, invariants) and [`.okf/development/invariants.md`](.okf/development/invariants.md).
|
|
111
|
+
Changes are listed in [`CHANGELOG.md`](CHANGELOG.md) (releases) and [`.okf/log.md`](.okf/log.md).
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
GNU Affero General Public License v3.0 or later — Copyright © 2026 Samuel Dion. See [LICENSE](LICENSE).
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jcl-parser
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
|
|
5
5
|
Author-email: Samuel Dion <sam94dion@gmail.com>
|
|
6
|
-
License: AGPL-3.0-or-later
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/landparty/jcl-parser
|
|
8
8
|
Project-URL: Repository, https://github.com/landparty/jcl-parser
|
|
9
|
-
Project-URL: Documentation, https://github.com/landparty/jcl-parser/
|
|
9
|
+
Project-URL: Documentation, https://github.com/landparty/jcl-parser/blob/main/.okf/index.md
|
|
10
10
|
Project-URL: Bug Reports, https://github.com/landparty/jcl-parser/issues
|
|
11
11
|
Keywords: jcl,mainframe,parser,ibm,job-control-language
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
15
14
|
Classifier: Operating System :: OS Independent
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -28,12 +27,14 @@ Requires-Dist: pytest>=7.0; extra == "dev"
|
|
|
28
27
|
Requires-Dist: coverage[toml]>=7.0; extra == "dev"
|
|
29
28
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
29
|
Requires-Dist: pytest-benchmark>=4.0; extra == "dev"
|
|
31
|
-
Requires-Dist: ruff
|
|
30
|
+
Requires-Dist: ruff==0.17.3; extra == "dev"
|
|
32
31
|
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: jsonschema>=4.18; extra == "dev"
|
|
33
33
|
Requires-Dist: tomli>=1.1.0; python_version < "3.11" and extra == "dev"
|
|
34
34
|
Provides-Extra: test
|
|
35
35
|
Requires-Dist: pytest>=7.0; extra == "test"
|
|
36
36
|
Requires-Dist: pytest-cov>=4.0; extra == "test"
|
|
37
|
+
Requires-Dist: jsonschema>=4.18; extra == "test"
|
|
37
38
|
Requires-Dist: tomli>=1.1.0; python_version < "3.11" and extra == "test"
|
|
38
39
|
Provides-Extra: profiling
|
|
39
40
|
Requires-Dist: psutil>=5.9; extra == "profiling"
|
|
@@ -50,7 +51,8 @@ Python library for parsing IBM z/OS JCL (Job Control Language) into structured `
|
|
|
50
51
|
## Install
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
|
-
pip install -
|
|
54
|
+
pip install jcl-parser # from PyPI
|
|
55
|
+
pip install -e ".[dev]" # from a checkout, with the dev tools
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Requires Python 3.10+. No runtime dependencies.
|
|
@@ -58,11 +60,20 @@ Requires Python 3.10+. No runtime dependencies.
|
|
|
58
60
|
## Quick Start
|
|
59
61
|
|
|
60
62
|
```python
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
|
|
61
65
|
import jclparser
|
|
62
66
|
|
|
63
|
-
job = jclparser.parse_jcl(
|
|
67
|
+
job = jclparser.parse_jcl(Path("PAYROLL.jcl")) # a str is always JCL text, never a path
|
|
64
68
|
print(job.name)
|
|
65
|
-
|
|
69
|
+
for step in jclparser.iter_execution_steps(job.steps): # also walks IF/THEN/ELSE blocks
|
|
70
|
+
print(step.name, step.program or step.procedure)
|
|
71
|
+
|
|
72
|
+
exploded = jclparser.explode_job(job) # PROC expansion is a separate call
|
|
73
|
+
errors = jclparser.validate_jcl(
|
|
74
|
+
Path("PAYROLL.jcl"), level="structure"
|
|
75
|
+
) # list[ParseError]
|
|
76
|
+
print(job.to_json(indent=2)) # schema: jclparser.JOB_JSON_SCHEMA
|
|
66
77
|
```
|
|
67
78
|
|
|
68
79
|
CLI:
|
|
@@ -71,7 +82,52 @@ CLI:
|
|
|
71
82
|
jcl-parser FILE steps # list all steps
|
|
72
83
|
jcl-parser FILE --validate # validate JCL
|
|
73
84
|
jcl-parser FILE lint # run lint rules
|
|
74
|
-
jcl-parser FILE --json # full parse result as JSON
|
|
85
|
+
jcl-parser FILE --json # full parse result as JSON (job.to_dict())
|
|
86
|
+
jcl-parser --json-schema # JSON Schema of that document
|
|
87
|
+
jcl-parser DIR # cross-job dataset lineage of a directory of jobs
|
|
88
|
+
jcl-parser DIR lineage mermaid # ... as a Mermaid graph (or `dot`)
|
|
89
|
+
jcl-parser DIR lint --sarif # lint every job of a directory, as SARIF
|
|
90
|
+
jcl-lint jobs/ --fail-on warning --format sarif -o jcl.sarif # CI / pre-commit
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Lint in CI
|
|
94
|
+
|
|
95
|
+
Configure the rules in `pyproject.toml`, and switch one off for one statement
|
|
96
|
+
with a comment:
|
|
97
|
+
|
|
98
|
+
```toml
|
|
99
|
+
[tool.jcl-parser.lint]
|
|
100
|
+
ignore = ["JCL-LINT-002"]
|
|
101
|
+
fail-on = "warning"
|
|
102
|
+
[tool.jcl-parser.lint.severity]
|
|
103
|
+
"JCL-LINT-004" = "warning"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```jcl
|
|
107
|
+
//* jcl-lint: disable=JCL-LINT-001
|
|
108
|
+
//MASTER DD DSN=PROD.MASTER.FILE,DISP=SHR
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
pre-commit (`.pre-commit-config.yaml`):
|
|
112
|
+
|
|
113
|
+
```yaml
|
|
114
|
+
- repo: https://github.com/landparty/jcl-parser
|
|
115
|
+
rev: v2.2.0 # first release with the hook
|
|
116
|
+
hooks:
|
|
117
|
+
- id: jcl-lint
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
GitHub Actions (findings appear in code scanning):
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
permissions:
|
|
124
|
+
security-events: write
|
|
125
|
+
steps:
|
|
126
|
+
- uses: actions/checkout@v4
|
|
127
|
+
- uses: landparty/jcl-parser@v2.2.0 # first release with the action
|
|
128
|
+
with:
|
|
129
|
+
paths: jcl
|
|
130
|
+
glob: "**/*.jcl"
|
|
75
131
|
```
|
|
76
132
|
|
|
77
133
|
## Commands
|
|
@@ -83,7 +139,7 @@ pytest -m unit
|
|
|
83
139
|
|
|
84
140
|
# Lint / format / type-check
|
|
85
141
|
ruff check . && ruff format .
|
|
86
|
-
mypy
|
|
142
|
+
python scripts/mypy_ratchet.py # mypy strict against the known-error baseline, as CI runs it
|
|
87
143
|
|
|
88
144
|
# Vendor sync (after changes to jclparser/) — rm first: plain `cp -r` nests
|
|
89
145
|
# into dest/jclparser/jclparser/ and leaves stale files when dest exists
|
|
@@ -92,7 +148,9 @@ rm -rf ../backend/_vendor/jcl-parser/jclparser && cp -r jclparser/ ../backend/_v
|
|
|
92
148
|
|
|
93
149
|
## Documentation
|
|
94
150
|
|
|
95
|
-
Full reference: [`.okf/index.md`](.okf/index.md)
|
|
151
|
+
Full reference: [`.okf/index.md`](.okf/index.md). Contributors: start with [`CLAUDE.md`](CLAUDE.md)
|
|
152
|
+
(commands, architecture, invariants) and [`.okf/development/invariants.md`](.okf/development/invariants.md).
|
|
153
|
+
Changes are listed in [`CHANGELOG.md`](CHANGELOG.md) (releases) and [`.okf/log.md`](.okf/log.md).
|
|
96
154
|
|
|
97
155
|
## License
|
|
98
156
|
|
|
@@ -16,6 +16,7 @@ jclparser/_post_parse.py
|
|
|
16
16
|
jclparser/_source_scanner.py
|
|
17
17
|
jclparser/_statement_parser.py
|
|
18
18
|
jclparser/builtin_procs.py
|
|
19
|
+
jclparser/checks.py
|
|
19
20
|
jclparser/cli.py
|
|
20
21
|
jclparser/condition_parser.py
|
|
21
22
|
jclparser/continuation_handler.py
|
|
@@ -25,6 +26,7 @@ jclparser/explosion.py
|
|
|
25
26
|
jclparser/include_loader.py
|
|
26
27
|
jclparser/jcl_file_type.py
|
|
27
28
|
jclparser/jes_statements.py
|
|
29
|
+
jclparser/lineage.py
|
|
28
30
|
jclparser/metrics.py
|
|
29
31
|
jclparser/model.py
|
|
30
32
|
jclparser/parameter_ast.py
|
|
@@ -34,6 +36,7 @@ jclparser/proc_loader.py
|
|
|
34
36
|
jclparser/profiling.py
|
|
35
37
|
jclparser/py.typed
|
|
36
38
|
jclparser/rc_analysis.py
|
|
39
|
+
jclparser/serialization.py
|
|
37
40
|
jclparser/sql_detector.py
|
|
38
41
|
jclparser/system_symbols.py
|
|
39
42
|
jclparser/utils.py
|
|
@@ -43,6 +46,7 @@ jclparser/lint/base.py
|
|
|
43
46
|
jclparser/lint/config.py
|
|
44
47
|
jclparser/lint/rules.py
|
|
45
48
|
jclparser/lint/runner.py
|
|
49
|
+
jclparser/lint/suppress.py
|
|
46
50
|
jclparser/plugins/__init__.py
|
|
47
51
|
jclparser/plugins/base.py
|
|
48
52
|
jclparser/program_profiles/__init__.py
|
|
@@ -4,8 +4,9 @@ pytest>=7.0
|
|
|
4
4
|
coverage[toml]>=7.0
|
|
5
5
|
pytest-cov>=4.0
|
|
6
6
|
pytest-benchmark>=4.0
|
|
7
|
-
ruff
|
|
7
|
+
ruff==0.17.3
|
|
8
8
|
mypy>=1.0
|
|
9
|
+
jsonschema>=4.18
|
|
9
10
|
|
|
10
11
|
[dev:python_version < "3.11"]
|
|
11
12
|
tomli>=1.1.0
|
|
@@ -16,6 +17,7 @@ psutil>=5.9
|
|
|
16
17
|
[test]
|
|
17
18
|
pytest>=7.0
|
|
18
19
|
pytest-cov>=4.0
|
|
20
|
+
jsonschema>=4.18
|
|
19
21
|
|
|
20
22
|
[test:python_version < "3.11"]
|
|
21
23
|
tomli>=1.1.0
|
|
@@ -20,7 +20,7 @@ application, for example::
|
|
|
20
20
|
import logging as _logging
|
|
21
21
|
|
|
22
22
|
# Package metadata
|
|
23
|
-
__version__ = "2.
|
|
23
|
+
__version__ = "2.2.0" # x-release-please-version
|
|
24
24
|
__author__ = "JCL Parser Team"
|
|
25
25
|
__description__ = "Parse IBM JCL scripts into Python objects"
|
|
26
26
|
|
|
@@ -90,6 +90,15 @@ from .jes_statements import (
|
|
|
90
90
|
XmitStatement,
|
|
91
91
|
parse_jes_control,
|
|
92
92
|
)
|
|
93
|
+
from .lineage import (
|
|
94
|
+
DatasetLineage,
|
|
95
|
+
DatasetUse,
|
|
96
|
+
JobDependency,
|
|
97
|
+
JobStream,
|
|
98
|
+
LineageReport,
|
|
99
|
+
build_lineage,
|
|
100
|
+
load_job_stream,
|
|
101
|
+
)
|
|
93
102
|
from .lint import (
|
|
94
103
|
LintConfig,
|
|
95
104
|
LintRule,
|
|
@@ -143,6 +152,12 @@ from .rc_analysis import (
|
|
|
143
152
|
FeasibilityReport,
|
|
144
153
|
compute_feasible_paths,
|
|
145
154
|
)
|
|
155
|
+
from .serialization import (
|
|
156
|
+
JOB_JSON_SCHEMA,
|
|
157
|
+
SCHEMA_VERSION,
|
|
158
|
+
job_to_dict,
|
|
159
|
+
job_to_json,
|
|
160
|
+
)
|
|
146
161
|
from .sql_detector import (
|
|
147
162
|
DB2_PROGRAMS,
|
|
148
163
|
SQLDetectionResult,
|
|
@@ -167,9 +182,6 @@ from .validators import (
|
|
|
167
182
|
# default unless the consuming application configures handlers.
|
|
168
183
|
_logging.getLogger(__name__).addHandler(_logging.NullHandler())
|
|
169
184
|
|
|
170
|
-
# Initialize global plugin registry
|
|
171
|
-
_registry = get_plugin_registry()
|
|
172
|
-
|
|
173
185
|
# Public API - what gets imported with "from jclparser import *"
|
|
174
186
|
__all__ = [ # noqa: RUF022 — grouped by API tier, not alphabetically
|
|
175
187
|
# ── Tier 1: Stable public API ─────────────────────────────────────────────
|
|
@@ -246,6 +258,19 @@ __all__ = [ # noqa: RUF022 — grouped by API tier, not alphabetically
|
|
|
246
258
|
"LineOrigin",
|
|
247
259
|
"resolve_includes",
|
|
248
260
|
"resolve_includes_with_origins",
|
|
261
|
+
# ── Tier 2: JSON export ───────────────────────────────────────────────────
|
|
262
|
+
"job_to_dict",
|
|
263
|
+
"job_to_json",
|
|
264
|
+
"JOB_JSON_SCHEMA",
|
|
265
|
+
"SCHEMA_VERSION",
|
|
266
|
+
# ── Tier 2: Cross-job dataset lineage ─────────────────────────────────────
|
|
267
|
+
"load_job_stream",
|
|
268
|
+
"build_lineage",
|
|
269
|
+
"JobStream",
|
|
270
|
+
"LineageReport",
|
|
271
|
+
"DatasetLineage",
|
|
272
|
+
"DatasetUse",
|
|
273
|
+
"JobDependency",
|
|
249
274
|
# ── Tier 2: Dataset lifecycle analysis ───────────────────────────────────
|
|
250
275
|
"DatasetEvent",
|
|
251
276
|
"analyze_dataset_lifecycle",
|
|
@@ -90,6 +90,9 @@ class IncludeLineMap:
|
|
|
90
90
|
(step_name, ref, self.translate(line))
|
|
91
91
|
for step_name, ref, line in job.unresolved_output_refs
|
|
92
92
|
]
|
|
93
|
+
job.conditional_errors[:] = [
|
|
94
|
+
(self.translate(line), message) for line, message in job.conditional_errors
|
|
95
|
+
]
|
|
93
96
|
|
|
94
97
|
def _relabel_object(self, obj: Any, seen: set[int]) -> None:
|
|
95
98
|
"""Translate ``line_number`` on *obj* and every model object under it."""
|
|
@@ -68,6 +68,8 @@ logger = logging.getLogger(__name__)
|
|
|
68
68
|
|
|
69
69
|
#: A record that looks like a JCL ``EXEC`` statement (``//name EXEC ...``).
|
|
70
70
|
_RE_EXEC_RECORD = re.compile(r"^//\S+\s+EXEC(\s|$)")
|
|
71
|
+
#: A JOB statement record (named; ``//*`` comments never reach the check).
|
|
72
|
+
_RE_JOB_RECORD = re.compile(r"^//\S+\s+JOB(\s|$)")
|
|
71
73
|
|
|
72
74
|
#: ``/*`` statements JES3 itself processes (RJP workstation sign-on/off); every
|
|
73
75
|
#: other ``/*`` control statement is JES2-only.
|
|
@@ -203,6 +205,14 @@ class JobBuilder:
|
|
|
203
205
|
#: The open ``CNTL`` block, whose program control records are collected
|
|
204
206
|
#: verbatim until its ``ENDCNTL``.
|
|
205
207
|
self.current_cntl: Statement | None = None
|
|
208
|
+
#: Line of the null statement (``//``) that ended the job, or None.
|
|
209
|
+
self.null_line: int | None = None
|
|
210
|
+
#: Line numbers of the records after it, which are not part of the job.
|
|
211
|
+
self.after_null_lines: list[int] = []
|
|
212
|
+
#: Misplaced ``IF``/``THEN``/``ELSE``/``ENDIF`` statements as
|
|
213
|
+
#: ``(line, message)``; finish() copies them to
|
|
214
|
+
#: :attr:`Job.conditional_errors`.
|
|
215
|
+
self.conditional_errors: list[tuple[int, str]] = []
|
|
206
216
|
|
|
207
217
|
# ------------------------------------------------------------------ #
|
|
208
218
|
# Line-level handling (before statement parsing)
|
|
@@ -465,6 +475,15 @@ class JobBuilder:
|
|
|
465
475
|
f"Line {cond.line_number}: IF block in PROC {procedure.name} "
|
|
466
476
|
f"has no matching ENDIF; closed at PEND (line {line_num})"
|
|
467
477
|
)
|
|
478
|
+
self.conditional_errors.append(
|
|
479
|
+
(
|
|
480
|
+
cond.line_number,
|
|
481
|
+
(
|
|
482
|
+
f"IF statement in PROC {procedure.name} has no "
|
|
483
|
+
"matching ENDIF before PEND"
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
)
|
|
468
487
|
self._warn_proc_orphaned_dds(procedure, line_num)
|
|
469
488
|
self.proc_orphaned_dds = []
|
|
470
489
|
self.proc_cond_depth = 0
|
|
@@ -660,12 +679,31 @@ class JobBuilder:
|
|
|
660
679
|
def _switch_branch(self, branch: str, line_num: int) -> None:
|
|
661
680
|
"""Handle ``THEN`` / ``ELSE``: make *branch* the open IF's active one."""
|
|
662
681
|
open_if = self._innermost_if()
|
|
663
|
-
if open_if is
|
|
664
|
-
self.conditional_stack[-1] = (open_if[0], branch)
|
|
665
|
-
else:
|
|
682
|
+
if open_if is None:
|
|
666
683
|
self._warnings_sink().append(
|
|
667
684
|
f"Line {line_num}: {branch} with no matching IF; ignored"
|
|
668
685
|
)
|
|
686
|
+
self.conditional_errors.append(
|
|
687
|
+
(line_num, f"{branch} statement with no matching IF")
|
|
688
|
+
)
|
|
689
|
+
return
|
|
690
|
+
if branch == "ELSE" and open_if[1] == "ELSE":
|
|
691
|
+
# An IF has at most one ELSE clause; the second one's steps are
|
|
692
|
+
# still read into the ELSE branch.
|
|
693
|
+
self._warnings_sink().append(
|
|
694
|
+
f"Line {line_num}: second ELSE for the IF on line "
|
|
695
|
+
f"{open_if[0].line_number}"
|
|
696
|
+
)
|
|
697
|
+
self.conditional_errors.append(
|
|
698
|
+
(
|
|
699
|
+
line_num,
|
|
700
|
+
(
|
|
701
|
+
"ELSE statement repeats the ELSE of the IF on line "
|
|
702
|
+
f"{open_if[0].line_number}"
|
|
703
|
+
),
|
|
704
|
+
)
|
|
705
|
+
)
|
|
706
|
+
self.conditional_stack[-1] = (open_if[0], branch)
|
|
669
707
|
|
|
670
708
|
def _close_if(self, line_num: int) -> None:
|
|
671
709
|
"""Handle ``ENDIF``: leave the innermost open IF block."""
|
|
@@ -675,6 +713,9 @@ class JobBuilder:
|
|
|
675
713
|
self._warnings_sink().append(
|
|
676
714
|
f"Line {line_num}: ENDIF with no matching IF; ignored"
|
|
677
715
|
)
|
|
716
|
+
self.conditional_errors.append(
|
|
717
|
+
(line_num, "ENDIF statement with no matching IF")
|
|
718
|
+
)
|
|
678
719
|
|
|
679
720
|
def _on_jes(self, stmt: Statement, line_num: int) -> None:
|
|
680
721
|
raw_params = stmt.parameters.get("_raw", "")
|
|
@@ -725,8 +766,23 @@ class JobBuilder:
|
|
|
725
766
|
sink.append(output)
|
|
726
767
|
|
|
727
768
|
def _on_null(self, stmt: Statement, line_num: int) -> None:
|
|
728
|
-
#
|
|
729
|
-
|
|
769
|
+
# The null statement (//) marks the end of the job on z/OS: what
|
|
770
|
+
# follows, up to the next JOB statement, is not part of it (#124).
|
|
771
|
+
# Before the JOB statement it delimits nothing and is ignored.
|
|
772
|
+
if self.job is not None and self.null_line is None:
|
|
773
|
+
self.null_line = line_num
|
|
774
|
+
|
|
775
|
+
def skip_after_null(self, line: str, line_num: int) -> bool:
|
|
776
|
+
"""True if *line* follows the null statement that ended the job.
|
|
777
|
+
|
|
778
|
+
Such a record is not part of the job, so the caller skips it; it is
|
|
779
|
+
counted for the warning :meth:`finish` adds. A JOB statement is not
|
|
780
|
+
skipped: a second job is still the "Multiple JOB statements" error.
|
|
781
|
+
"""
|
|
782
|
+
if self.null_line is None or _RE_JOB_RECORD.match(line):
|
|
783
|
+
return False
|
|
784
|
+
self.after_null_lines.append(line_num)
|
|
785
|
+
return True
|
|
730
786
|
|
|
731
787
|
def _on_control(self, stmt: Statement, line_num: int) -> None:
|
|
732
788
|
"""Record a COMMAND / SCHEDULE / CNTL / NOTIFY statement verbatim."""
|
|
@@ -878,9 +934,20 @@ class JobBuilder:
|
|
|
878
934
|
self._flush_pending_statements(job)
|
|
879
935
|
self._warn_orphaned_dds(job)
|
|
880
936
|
self._warn_unclosed_ifs(job)
|
|
937
|
+
self._warn_after_null(job)
|
|
881
938
|
|
|
882
939
|
return job
|
|
883
940
|
|
|
941
|
+
def _warn_after_null(self, job: Job) -> None:
|
|
942
|
+
if self.null_line is not None and self.after_null_lines:
|
|
943
|
+
job.warnings.append(
|
|
944
|
+
f"Line {self.null_line}: null statement (//) ends job "
|
|
945
|
+
f"{job.name or '(unnamed)'}; {len(self.after_null_lines)} "
|
|
946
|
+
f"record(s) after it (lines {self.after_null_lines[0]}-"
|
|
947
|
+
f"{self.after_null_lines[-1]}) are not part of the job and "
|
|
948
|
+
"were ignored"
|
|
949
|
+
)
|
|
950
|
+
|
|
884
951
|
def _job_or_procedure_member(self) -> Job:
|
|
885
952
|
"""The parsed Job, or a synthetic one for a JOB-less PROC member."""
|
|
886
953
|
if self.job is not None:
|
|
@@ -969,3 +1036,7 @@ class JobBuilder:
|
|
|
969
1036
|
job.warnings.append(
|
|
970
1037
|
f"Line {_cond.line_number}: IF block has no matching ENDIF"
|
|
971
1038
|
)
|
|
1039
|
+
self.conditional_errors.append(
|
|
1040
|
+
(_cond.line_number, "IF statement has no matching ENDIF")
|
|
1041
|
+
)
|
|
1042
|
+
job.conditional_errors.extend(sorted(self.conditional_errors))
|
|
@@ -358,15 +358,6 @@ def split_comment_field(
|
|
|
358
358
|
return (line[:start] + operands[:cut]).rstrip(), operands[cut:].strip()
|
|
359
359
|
|
|
360
360
|
|
|
361
|
-
def strip_comment_field(line: str, group_operation: str = "") -> str:
|
|
362
|
-
"""Return *line* without its trailing comment field.
|
|
363
|
-
|
|
364
|
-
Thin wrapper over :func:`split_comment_field` for callers that do not need
|
|
365
|
-
the comment text.
|
|
366
|
-
"""
|
|
367
|
-
return split_comment_field(line, group_operation)[0]
|
|
368
|
-
|
|
369
|
-
|
|
370
361
|
def _find_statement_plugin(line: str, config: ParserConfig) -> StatementPlugin | None:
|
|
371
362
|
"""Return the first plugin *config* enables that can parse *line*, if any."""
|
|
372
363
|
for plugin in get_plugin_registry().get_statement_plugins():
|