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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions gitgalaxy/core/prism.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,21 @@ def __init__(
self.PYTHON_DOC_PATTERN = re.compile(PRISM_CONFIG.get("PYTHON_DOC_PATTERN", ""), re.M)
self.PHP_HEREDOC_PATTERN = re.compile(PRISM_CONFIG.get("PHP_HEREDOC_PATTERN", ""), re.M)

# #2610: JCL's `//*` comment is a whole-line positional prefix that the
# line_exclusive delimiter stripper can't express (every JCL *statement*
# also starts with `//`, so a bare `//` delimiter would comment out the
# entire language -- which is why gitgalaxy_config.py's per-language
# delimiter list for jcl is deliberately empty). Matched per-line in
# _strip_jcl_comments instead. The negative lookahead keeps the ten
# JES3 control statements (`//*MAIN`, `//*FORMAT`, ...) in the code
# stream: they share the `//*` prefix but are real statements, not
# comments. Deliberately case-SENSITIVE -- JES3 verbs are only valid
# uppercase, while a lowercase `//*main story...` prose comment must
# still strip.
self.JCL_COMMENT_LINE_PATTERN = re.compile(
r"^//\*(?!(?:MAIN|FORMAT|NET|DATASET|ENDDATASET|PROCESS|ENDPROCESS|OPERATOR|PAUSE|ROUTE)\b)"
)

self.logger.info(f"Structural Scanner Online | Calibrated {len(self.REGEX_MATRIX)} syntax rules.")

def split_streams(self, content: str, primary_lang: str) -> PrismResult:
Expand Down Expand Up @@ -375,6 +390,16 @@ def _strip_segment_comments(self, text: str, lang_id: str, family: str) -> tuple
lits.extend(pos_lits.splitlines())
return code, "\n".join(lits)

if lang_id == "jcl":
# #2610: jcl is nominally line_exclusive but its delimiter list is
# (correctly) empty -- see JCL_COMMENT_LINE_PATTERN's construction
# note. Without this branch, every `//*` comment line stayed in the
# code stream, so jcl's comment surface (doc/ownership/debt rules,
# doc_loc) was structurally dead engine-wide.
code, jcl_lits = self._strip_jcl_comments(text)
lits.extend(jcl_lits)
return code, "\n".join(lits)

if family == "line_exclusive":
code, single_lits = self._strip_single_line_comments(text, lang_id)
if single_lits:
Expand Down Expand Up @@ -1052,6 +1077,29 @@ def unmask(chunk: str) -> str:
# 3. Final Logic Unmasking
return unmask(protected_code), lits

def _strip_jcl_comments(self, text: str) -> tuple[str, list[str]]:
"""
Whole-line `//*` comment stripping for JCL (#2610).

JCL has no inline comment form this engine models -- the comment
statement is the entire physical line, prefix-anchored at column 1 --
so this is a pure per-line partition with no literal shielding needed
(a `//*` mid-line is operand text, and a line not starting `//*` can
never become a comment partway through). Line count is preserved by
emitting an empty code line per stripped comment, mirroring
_strip_positional_comments, so downstream spatial line numbers stay
aligned with the raw file. JES3 control verbs (`//*MAIN` etc.) are
excluded by JCL_COMMENT_LINE_PATTERN and stay in the code stream.
"""
code, lits = [], []
for line in text.split("\n"):
if self.JCL_COMMENT_LINE_PATTERN.match(line):
lits.append(line)
code.append("")
else:
code.append(line)
return "\n".join(code), lits

def _strip_positional_comments(
self, text: str, abap_mode: bool = False, cobol_mode: bool = False
) -> tuple[str, str]:
Expand Down
43 changes: 40 additions & 3 deletions gitgalaxy/standards/language_standards/languages/jcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import re
from typing import Any

from .._shared_patterns import GLOBAL_FRAGILE_DEBT, GLOBAL_PLANNED_DEBT

DEFINITION: dict[str, Any] = {
"_meta": {
"target_version": "IBM z/OS JCL",
Expand Down Expand Up @@ -80,9 +82,32 @@
"high_risk_execution": re.compile(r"\bPGM=[A-Za-z0-9_#$@]+\b", re.I),
# I/O (Data Set Names and Sysouts)
"io": re.compile(r"\b(DSN|DSNAME|SYSOUT|SYSPRINT|DISP=)\b", re.I),
# JCL doesn't have traditional code equivalents for these, keep them null to prevent crashes
"safety": None,
# #2610: JCL's error handling is the COND= operand -- a return-code
# test deciding whether a step runs after a prior step's outcome.
# Unanchored (like io's DISP=/PGM= operands) because COND= routinely
# sits on a `//` continuation line, not the EXEC line itself (the same
# real-corpus shape the args rule's #2482 note documents). The negative
# lookahead keeps the plain bypass forms (COND=EVEN / COND=ONLY) out of
# safety: those are the *absence* of a return-code test and belong to
# safety_bypasses below. A combined form like COND=((4,LT),EVEN)
# deliberately counts BOTH -- it carries a real RC test and a run-even-
# after-abend bypass at once.
"safety": re.compile(r"\bCOND=(?!(?:EVEN|ONLY)\b)", re.I),
"api": None,
# #2610: COND=EVEN ("run even if a prior step abended") and COND=ONLY
# ("run only after an abend") execute a step in spite of upstream
# failure -- JCL's native ignore-the-error idiom. Two alternatives:
# the bare form, and the parenthesized combined form
# (COND=((4,LT),EVEN)), whose scan is the bounded one-level-paren
# idiom -- the two branches are disjoint on their first character
# ("(" vs not) and the inner star sits inside literal parens, so no
# position ever partitions ambiguously (ReDoS-safe), and neither
# branch can cross a newline or escape the COND value's own parens to
# reach an unrelated EVEN/ONLY later on the line.
"safety_bypasses": re.compile(
r"\bCOND=(?:EVEN|ONLY)\b|\bCOND=\((?:[^\n()]|\([^\n()]*\))*?\b(?:EVEN|ONLY)\b",
re.I,
),
# BUG FIX: unanchored -- `\bSET\s+NAME=` matched "SET" anywhere in the
# file, including inline SYSIN card data (`//SYSIN DD *` ... `/*`) that
# isn't a JCL statement at all (e.g. an embedded SQL/shell/config
Expand Down Expand Up @@ -118,7 +143,19 @@
# captured garbage from that *different* line (including its own "//*"
# prefix) instead of correctly failing to match. Bounded to `[ \t]+`.
"ownership": re.compile(r"^//\*[ \t]*(?:Author|Created by|Maintainer):[ \t]+(.*)", re.I | re.M),
"telemetry": None,
# #2610: MSGLEVEL= (what the job log records: statements/allocations)
# and MSGCLASS= (where the log goes) are JCL's observability dials --
# the closest native equivalent of configuring a logger. Unanchored
# like the other operand rules (JOB-card operands continue across `//`
# lines the same way EXEC's do).
"telemetry": re.compile(r"\bMSG(?:LEVEL|CLASS)=", re.I),
"debug_prints": None,
# #2610: comment-anchored debt markers. Dead rules before the #2610
# prism fix (jcl's comment stream was always empty); now that `//*`
# lines reach comment_analysis, a `//* TODO ...` banner in a real job
# deck counts the same way it does in cobol. Shared global patterns,
# same as cobol.py.
"planned_debt": GLOBAL_PLANNED_DEBT,
"fragile_debt": GLOBAL_FRAGILE_DEBT,
},
}
46 changes: 46 additions & 0 deletions tests/core_engine/test_prism.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,52 @@ def test_prism_strips_comments_against_the_real_config():
assert "a comment" not in result["code_stream"]
assert "a comment" in result["comment_stream"]

# jcl (#2610): nominally line_exclusive but with a deliberately empty
# delimiter list (`//` prefixes every statement too), served by its own
# whole-line `//*` stripper instead
result = real_prism.split_streams("//* a comment\n//STEP1 EXEC PGM=IEFBR14\n", "jcl")
assert "a comment" not in result["code_stream"]
assert "a comment" in result["comment_stream"]
assert "EXEC PGM=IEFBR14" in result["code_stream"]


def test_prism_jcl_comment_stripping_details():
"""
#2610: JCL `//*` whole-line comments move to the comment stream while
everything `//`-statement-shaped stays code -- including the ten JES3
control statements (`//*MAIN` etc.), which share the comment's prefix
but are real statements (uppercase-only, hence the guard being
case-sensitive while a lowercase `//*main ...` prose comment still
strips). Line count must be preserved so downstream spatial line
numbers stay aligned with the raw file.
"""
from gitgalaxy.standards.gitgalaxy_config import LEXICAL_FAMILY_HEURISTICS
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

real_prism = Prism(LEXICAL_FAMILY_HEURISTICS, LANGUAGE_DEFINITIONS)
deck = (
"//* banner comment\n"
"//*MAIN CLASS=A\n"
"//*main lowercase prose comment\n"
"//ROSETTA JOB\n"
"//STEP1 EXEC PGM=IEFBR14\n"
"//SYSIN DD *\n"
"SET X=1 payload line, not JCL\n"
"/*\n"
)
result = real_prism.split_streams(deck, "jcl")
code, comments = result["code_stream"], result["comment_stream"]

assert "banner comment" not in code and "banner comment" in comments
assert "lowercase prose comment" not in code and "lowercase prose comment" in comments
# JES3 control statement stays code, never a "comment"
assert "//*MAIN CLASS=A" in code and "//*MAIN" not in comments
# ordinary statements, inline SYSIN payload, and the /* delimiter stay code
assert "//ROSETTA JOB" in code and "EXEC PGM=IEFBR14" in code
assert "payload line" in code and "/*" in code
# line alignment: stripped lines are blanked, not deleted
assert code.count("\n") == deck.count("\n")


def test_prism_sub_families_fix_the_standard_block_delimiter_gap():
"""
Expand Down
63 changes: 63 additions & 0 deletions tests/extraction/languages/test_jcl_strict.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,26 @@
("structural_boundaries", "//$DD DD DUMMY", "// DUMMY"),
("structural_boundaries", "//INC INCLUDE MEMBER=A", "INCLUDE MEMBER=A"),
("structural_boundaries", "//@SET SET X=Y", "SET X=Y"),

# #2610: safety = COND= return-code tests; the bare bypass forms are
# excluded (they belong to safety_bypasses, not safety)
("safety", "//S1 EXEC PGM=X,COND=(4,LT)", "//S2 EXEC PGM=Y,COND=EVEN"),
("safety", "// COND=(0,NE,STEP1)", "//S2 EXEC PGM=Y,COND=ONLY"),
("safety", "//S3 EXEC PGM=Z,COND=((4,LT),EVEN)", "//S4 EXEC PGM=W,CONDX=(4,LT)"),

# #2610: safety_bypasses = COND=EVEN / COND=ONLY (run despite abend)
("safety_bypasses", "//S2 EXEC PGM=Y,COND=EVEN", "//S1 EXEC PGM=X,COND=(4,LT)"),
("safety_bypasses", "//S2 EXEC PGM=Y,COND=ONLY", "//S1 EXEC PGM=X,COND=(4,LT,STEP1)"),
("safety_bypasses", "//S3 EXEC PGM=Z,COND=((4,LT),EVEN)", "//S4 EXEC PGM=W,COND=(4,LT),PARM='EVENT'"),

# #2610: telemetry = job-log verbosity/routing operands
("telemetry", "//J JOB 1,MSGLEVEL=(1,1)", "//S1 EXEC PGM=X"),
("telemetry", "//J JOB 1,MSGCLASS=H", "//J JOB 1,CLASS=A"),

# #2610: comment-anchored debt markers (shared GLOBAL_* patterns; only
# meaningful now that prism routes //* lines to the comment stream)
("planned_debt", "//* TODO wire the FTP step", "//* all wired up here"),
("fragile_debt", "//* HACK: overrides the region size", "//* routine banner comment"),
]


Expand Down Expand Up @@ -294,6 +314,49 @@ def test_jcl_args_redos_immunity():
assert_redos_immune(pattern, many_fake_hops, timeout_sec=3.0)


def test_jcl_cond_safety_vs_bypass_partition():
"""
#2610: the two COND= rules partition by semantics, not by keyword --
a plain RC test is safety only, a bare EVEN/ONLY is bypass only, and
the combined form carries both (a real RC test AND a run-after-abend
bypass on the same step). An EVEN-shaped token *outside* the COND
value's own parentheses must not leak into the bypass count.
"""
safety = JCL_RULES["safety"]
bypass = JCL_RULES["safety_bypasses"]

plain = "//S1 EXEC PGM=X,COND=(4,LT)"
assert safety.search(plain) and not bypass.search(plain)

bare_even = "//S2 EXEC PGM=Y,COND=EVEN"
assert bypass.search(bare_even) and not safety.search(bare_even)

combined = "//S3 EXEC PGM=Z,COND=((4,LT),EVEN)"
assert safety.search(combined) and bypass.search(combined)

# EVEN-ish text later on the line, outside the COND parens, is not a bypass
outside = "//S4 EXEC PGM=W,COND=(4,LT),PARM='EVENT'"
assert safety.search(outside) and not bypass.search(outside)

# continuation-line COND= (the same real-corpus shape #2482 documents
# for PARM=) still counts -- the rule is operand-anchored, not line-anchored
continuation = "//S5 EXEC PGM=V,\n// COND=ONLY"
assert bypass.search(continuation)


def test_jcl_cond_bypass_redos_immunity():
"""
#2610: the combined-form branch's scan is the bounded one-level-paren
idiom -- alternatives disjoint on their first character, inner star
inside literal parens -- fed here with an adversarial run that never
closes and never reaches EVEN/ONLY (the shape that would matter if
the alternation partitioned ambiguously).
"""
pattern = JCL_RULES["safety_bypasses"]
assert_redos_immune(pattern, "//X EXEC PGM=Y,COND=(" + "(A)," * 20000, timeout_sec=3.0)
assert_redos_immune(pattern, "//X EXEC PGM=Y,COND=(" + "A" * 100000, timeout_sec=3.0)


def test_jcl_lexical_family_no_block_terminator_state_to_confuse():
"""
Lexical-family audit: jcl is `line_exclusive` -- no block comment
Expand Down
Loading
Loading