mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 17:09:17 +00:00
fix(cobol): detect fixed-format source that carries sequence numbers
The fixed/free format detector counted a line as fixed-format only when its
first six columns were blank (`^ {6}[ *\-/ ]`). Legacy mainframe COBOL — the
main reason to have a COBOL extractor — puts a sequence NUMBER in columns 1-6,
so every such file scored zero fixed markers and was classified free-format.
In free mode the code is taken verbatim (no column-8 slice), so the sequence
number stays glued to the statement: `000400 MAIN-PARA.` no longer matches the
paragraph anchor `^NAME .? .$`, and the whole procedure layer disappears. A
seven-line program with two paragraphs and a PERFORM collapsed to just the file
and program nodes — zero paragraphs, zero `calls` edges — silently.
Match a sequence area that is six spaces OR six digits, followed by a valid
indicator column (space = code, `*`/`/` = comment, `-` = continuation, `D` =
debug). Once the file is correctly read as fixed-format the existing
column-8..72 slice strips the sequence number and paragraphs/PERFORMs resolve
as they should. Blank-sequence fixed-format and genuinely free-format source are
unaffected (a 6-digit prefix is not something free-format code begins a line
with).
Regression test: a sequence-numbered program now yields its program, both
paragraphs, and the PERFORM `calls` edge — and fails on the old detector.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
This commit is contained in:
1 parent
4c735618f3
commit
96b9522bd5
2 files changed
+35
-1
No files matched your search
@@ -40,8 +40,17 @@ def _code_lines(source: str) -> list[tuple[int, str]]:
|
||||
forced_free = bool(
|
||||
re.search(r">>\s*SOURCE\s+FORMAT\s+(?:IS\s+)?FREE", source, re.IGNORECASE)
|
||||
)
|
||||
# A fixed-format line is a 6-column sequence area followed by the indicator
|
||||
# in column 7. The sequence area is blank on some files but carries a
|
||||
# sequence NUMBER on legacy mainframe source (the historical norm), so match
|
||||
# both: six spaces, or six digits, followed by a valid indicator (space =
|
||||
# code, `*`/`/` = comment, `-` = continuation, `D` = debug). Requiring six
|
||||
# blank columns misclassified every sequence-numbered file as free-format,
|
||||
# which then kept the sequence number in the code and dropped every paragraph
|
||||
# (its `^NAME.$` anchor no longer matched) and PERFORM edge.
|
||||
fixed_markers = sum(
|
||||
bool(re.match(r"^ {6}[ *\-/ ]", line)) for line in physical if line.strip()
|
||||
bool(re.match(r"^(?: {6}|\d{6})[ *\-/dD]", line))
|
||||
for line in physical if line.strip()
|
||||
)
|
||||
nonempty = sum(bool(line.strip()) for line in physical)
|
||||
free = forced_free or fixed_markers < max(1, nonempty // 2)
|
||||
|
||||
@@ -131,3 +131,28 @@ def test_cobol_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_pa
|
||||
labels = {node["label"].casefold() for node in result["nodes"]}
|
||||
assert 'kept' in labels
|
||||
assert labels.isdisjoint({'ghost'})
|
||||
|
||||
|
||||
def test_cobol_fixed_format_with_sequence_numbers(tmp_path):
|
||||
"""Legacy fixed-format source carries a sequence NUMBER in columns 1-6, not
|
||||
blanks. The format detector required six blank columns, so a sequence-numbered
|
||||
file was misread as free-format — the sequence number stayed in the code and
|
||||
every paragraph (its `^NAME.$` anchor no longer matched) and PERFORM edge was
|
||||
lost, leaving only the file and program nodes."""
|
||||
source = tmp_path / "legacy.cob"
|
||||
source.write_text(
|
||||
"000100 IDENTIFICATION DIVISION.\n"
|
||||
"000200 PROGRAM-ID. PAYROLL.\n"
|
||||
"000300 PROCEDURE DIVISION.\n"
|
||||
"000400 MAIN-PARA.\n"
|
||||
"000500 PERFORM INIT-PARA.\n"
|
||||
"000600 INIT-PARA.\n"
|
||||
"000700 DISPLAY 'HELLO'.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
result = extract([source], cache_root=tmp_path)
|
||||
|
||||
labels = {node["label"] for node in result["nodes"]}
|
||||
assert {"PAYROLL", "MAIN-PARA", "INIT-PARA"} <= labels
|
||||
assert ("MAIN-PARA", "INIT-PARA") in _edge_labels(result, "calls")
|
||||
Reference in new issue
Block a user