Skip to content

Commit bcf19b3

Browse files
committed
Fix report noise from COBOL words, PIC types, and truncated CFG ids.
Reserved words are not variables; PIC V/$ is numeric; COMP-3 prints once; unused paragraphs ignore fall-through; ADD ... GIVING stays one statement.
1 parent 74d50a0 commit bcf19b3

6 files changed

Lines changed: 512 additions & 106 deletions

File tree

‎README.md‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ cargo run --bin gen_dummy -- --count 1000 --outdir dummy-cobol
5454
The parser is not a complete COBOL implementation.
5555

5656
- COPY looks up members next to the source and in `COPYBOOK`, `COPYLIB`, or `copybooks` directories walking up from the file. It does not search a z/OS library (SYSLIB, `COPY` ... `OF` / `IN`). If the member is not found, COPY remains a copybook data item.
57-
- Column-7 `*` lines in IDENTIFICATION are stored as comments. A leading `*` is stripped only when it is the first character of the line, so banners such as `*-----------------------` appear in the report.
5857
- Not a Db2 coprocessor: no DBRM, no generated SQLCA. DATA DIVISION DECLARE TABLE, DECLARE CURSOR, and INCLUDE when the member is missing are not data items.
5958
- `--workers` isolates unwinding panics per file (`catch_unwind`). The process still exits if Rust is built with `panic = "abort"`, on a double panic, or on some stack overflows.
6059

‎src/cobol.rs‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,94 @@ pub static KEYWORDS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
5454
"EXTEND",
5555
"CURRENT-DATE",
5656
"GOBACK",
57+
"GIVING",
58+
"CONVERTING",
59+
"TIMES",
60+
"LINES",
61+
"INSPECT",
62+
"ACCEPT",
63+
"ADD",
64+
"SUBTRACT",
65+
"MULTIPLY",
66+
"DIVIDE",
67+
"REMAINDER",
68+
"ROUNDED",
69+
"END-PERFORM",
70+
"END-EVALUATE",
71+
"EVALUATE",
72+
"WHEN",
73+
"CONTINUE",
74+
"INITIALIZE",
75+
"STRING",
76+
"UNSTRING",
77+
"SEARCH",
78+
"GO",
79+
"GOTO",
80+
"REWRITE",
81+
"DELETE",
82+
"START",
83+
"RELEASE",
84+
"EXIT",
85+
"TRAILING",
86+
"LEADING",
87+
"THRU",
88+
"THROUGH",
89+
"DELIMITED",
90+
"POINTER",
5791
])
5892
});
5993

94+
pub fn is_statement_verb(tok: &str) -> bool {
95+
matches!(
96+
tok.to_uppercase().as_str(),
97+
"MOVE"
98+
| "PERFORM"
99+
| "READ"
100+
| "WRITE"
101+
| "DISPLAY"
102+
| "IF"
103+
| "ELSE"
104+
| "END-IF"
105+
| "STOP"
106+
| "CALL"
107+
| "EXEC"
108+
| "OPEN"
109+
| "CLOSE"
110+
| "FETCH"
111+
| "COMPUTE"
112+
| "SET"
113+
| "ADD"
114+
| "SUBTRACT"
115+
| "MULTIPLY"
116+
| "DIVIDE"
117+
| "ACCEPT"
118+
| "INSPECT"
119+
| "INITIALIZE"
120+
| "STRING"
121+
| "UNSTRING"
122+
| "SEARCH"
123+
| "SORT"
124+
| "MERGE"
125+
| "GOBACK"
126+
| "EXIT"
127+
| "EVALUATE"
128+
| "CONTINUE"
129+
| "GO"
130+
| "GOTO"
131+
| "REWRITE"
132+
| "DELETE"
133+
| "START"
134+
| "RELEASE"
135+
| "RETURN"
136+
| "END-PERFORM"
137+
| "END-EVALUATE"
138+
| "END-READ"
139+
| "END-SEARCH"
140+
| "END-STRING"
141+
| "END-UNSTRING"
142+
)
143+
}
144+
60145
pub static LITERALS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
61146
HashMap::from([
62147
("SPACES", "\" \" (SPACES)"),

‎src/parser.rs‎

Lines changed: 152 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,7 @@ fn infer_type_from_pic(pic: &str) -> String {
7979
if pic.starts_with('X') || pic.contains("X(") {
8080
return "string".to_string();
8181
}
82-
if pic.contains('9') {
83-
if pic.contains('V') || pic.contains('.') {
84-
return "float".to_string();
85-
}
82+
if pic.contains('9') || pic.contains('$') || pic.contains('Z') {
8683
return "numeric".to_string();
8784
}
8885
if pic.contains("COMP") {
@@ -121,6 +118,31 @@ fn is_cobol_comment_or_blank(line: &str) -> bool {
121118
cobol::is_cobol_comment_or_blank(line)
122119
}
123120

121+
fn identification_comment_text(line: &str) -> Option<String> {
122+
if !is_cobol_comment_or_blank(line) {
123+
return None;
124+
}
125+
let t = line.trim();
126+
if t.is_empty() || !t.starts_with('*') {
127+
return None;
128+
}
129+
let body = t
130+
.trim_start_matches('*')
131+
.trim()
132+
.trim_end_matches('*')
133+
.trim();
134+
if body.is_empty() {
135+
return None;
136+
}
137+
if body
138+
.chars()
139+
.all(|c| matches!(c, '-' | '=' | '+' | '*' | '.' | '_' | ' '))
140+
{
141+
return None;
142+
}
143+
Some(body.to_string())
144+
}
145+
124146
fn has_terminator_period(s: &str) -> bool {
125147
cobol::has_terminator_period(s)
126148
}
@@ -228,8 +250,8 @@ fn parse_identification_division(source: &str) -> (String, String, String, Vec<S
228250
}
229251
} else if pending_program_id {
230252
if is_cobol_comment_or_blank(line) {
231-
if line.trim_start().starts_with('*') {
232-
comments.push(line.trim_start_matches('*').trim().to_string());
253+
if let Some(text) = identification_comment_text(line) {
254+
comments.push(text);
233255
}
234256
} else if let Some(tok) = line.split_whitespace().next() {
235257
program_name = tok.trim_end_matches('.').to_string();
@@ -245,8 +267,8 @@ fn parse_identification_division(source: &str) -> (String, String, String, Vec<S
245267
.get(1)
246268
.map(|m| m.as_str().trim_end_matches('.').to_string())
247269
.unwrap_or_default();
248-
} else if line.trim_start().starts_with('*') {
249-
comments.push(line.trim_start_matches('*').trim().to_string());
270+
} else if let Some(text) = identification_comment_text(line) {
271+
comments.push(text);
250272
}
251273
}
252274
}
@@ -358,7 +380,7 @@ fn extract_variable_usage(
358380
.iter()
359381
.filter(|op| {
360382
let opu = cobol::normalize_name(op);
361-
!cobol::KEYWORDS.contains(opu.as_str())
383+
cobol::is_valid_identifier(op)
362384
&& !cobol::is_literal(op)
363385
&& !para_name_map.contains_key(&opu)
364386
})
@@ -592,6 +614,62 @@ fn record_statement(
592614
}
593615
}
594616

617+
fn statement_first_token(line: &str) -> String {
618+
cobol::split_cobol_tokens(line)
619+
.into_iter()
620+
.next()
621+
.map(|s| s.trim_end_matches('.').to_uppercase())
622+
.unwrap_or_default()
623+
}
624+
625+
struct StmtCtx<'a> {
626+
para: &'a mut Option<Paragraph>,
627+
paragraph_name: &'a str,
628+
section: &'a Option<ProcedureSection>,
629+
para_name_map: &'a HashMap<String, String>,
630+
call_graph: &'a mut Vec<CallGraphEntry>,
631+
}
632+
633+
fn flush_pending_statement(pending: &mut Option<(usize, String)>, ctx: &mut StmtCtx<'_>) {
634+
if let Some((line_no, raw)) = pending.take() {
635+
if let Some(ref mut p) = ctx.para {
636+
record_statement(
637+
p,
638+
ctx.paragraph_name,
639+
ctx.section,
640+
&raw,
641+
line_no,
642+
ctx.para_name_map,
643+
ctx.call_graph,
644+
);
645+
}
646+
}
647+
}
648+
649+
fn accumulate_statement(
650+
pending: &mut Option<(usize, String)>,
651+
line: &str,
652+
line_no: usize,
653+
ctx: &mut StmtCtx<'_>,
654+
) {
655+
let tok = statement_first_token(line);
656+
if pending.is_some() && cobol::is_statement_verb(&tok) {
657+
flush_pending_statement(pending, ctx);
658+
}
659+
if let Some((_, buf)) = pending.as_mut() {
660+
buf.push(' ');
661+
buf.push_str(line.trim());
662+
} else {
663+
*pending = Some((line_no, line.trim().to_string()));
664+
}
665+
if pending
666+
.as_ref()
667+
.is_some_and(|(_, raw)| has_terminator_period(raw))
668+
{
669+
flush_pending_statement(pending, ctx);
670+
}
671+
}
672+
595673
fn parse_procedure_division_and_call_graph(
596674
source: &str,
597675
program_name: &str,
@@ -646,6 +724,7 @@ fn parse_procedure_division_and_call_graph(
646724
let mut control_flow_graph = Vec::new();
647725
let mut all_paragraphs_flat: Vec<Paragraph> = Vec::new();
648726
let mut exec_sql: Option<(usize, String)> = None;
727+
let mut stmt_pending: Option<(usize, String)> = None;
649728
for (i, line) in source.lines().enumerate() {
650729
let line = line.trim_end();
651730
if consume_procedure_header(line, &mut in_proc, &mut skip_using) {
@@ -675,6 +754,16 @@ fn parse_procedure_division_and_call_graph(
675754
continue;
676755
}
677756
if starts_exec_sql(line) {
757+
flush_pending_statement(
758+
&mut stmt_pending,
759+
&mut StmtCtx {
760+
para: &mut current_paragraph,
761+
paragraph_name: &current_paragraph_name,
762+
section: &current_section,
763+
para_name_map: &para_name_map,
764+
call_graph: &mut call_graph,
765+
},
766+
);
678767
if ends_exec(line) {
679768
ensure_paragraph(
680769
&mut current_paragraph,
@@ -692,6 +781,16 @@ fn parse_procedure_division_and_call_graph(
692781
continue;
693782
}
694783
if let Some(sec_caps) = re_section.captures(line) {
784+
flush_pending_statement(
785+
&mut stmt_pending,
786+
&mut StmtCtx {
787+
para: &mut current_paragraph,
788+
paragraph_name: &current_paragraph_name,
789+
section: &current_section,
790+
para_name_map: &para_name_map,
791+
call_graph: &mut call_graph,
792+
},
793+
);
695794
finish_paragraph(
696795
&mut current_paragraph,
697796
&mut current_section,
@@ -723,19 +822,30 @@ fn parse_procedure_division_and_call_graph(
723822
program_name,
724823
i + 1,
725824
);
726-
if let Some(ref mut para) = current_paragraph {
727-
record_statement(
728-
para,
729-
&current_paragraph_name,
730-
&current_section,
731-
line,
732-
i + 1,
733-
&para_name_map,
734-
&mut call_graph,
735-
);
736-
}
825+
accumulate_statement(
826+
&mut stmt_pending,
827+
line,
828+
i + 1,
829+
&mut StmtCtx {
830+
para: &mut current_paragraph,
831+
paragraph_name: &current_paragraph_name,
832+
section: &current_section,
833+
para_name_map: &para_name_map,
834+
call_graph: &mut call_graph,
835+
},
836+
);
737837
continue;
738838
}
839+
flush_pending_statement(
840+
&mut stmt_pending,
841+
&mut StmtCtx {
842+
para: &mut current_paragraph,
843+
paragraph_name: &current_paragraph_name,
844+
section: &current_section,
845+
para_name_map: &para_name_map,
846+
call_graph: &mut call_graph,
847+
},
848+
);
739849
finish_paragraph(
740850
&mut current_paragraph,
741851
&mut current_section,
@@ -761,20 +871,31 @@ fn parse_procedure_division_and_call_graph(
761871
program_name,
762872
i + 1,
763873
);
764-
if let Some(ref mut para) = current_paragraph {
765-
record_statement(
766-
para,
767-
&current_paragraph_name,
768-
&current_section,
769-
line,
770-
i + 1,
771-
&para_name_map,
772-
&mut call_graph,
773-
);
774-
}
874+
accumulate_statement(
875+
&mut stmt_pending,
876+
line,
877+
i + 1,
878+
&mut StmtCtx {
879+
para: &mut current_paragraph,
880+
paragraph_name: &current_paragraph_name,
881+
section: &current_section,
882+
para_name_map: &para_name_map,
883+
call_graph: &mut call_graph,
884+
},
885+
);
775886
}
776887
}
777888
}
889+
flush_pending_statement(
890+
&mut stmt_pending,
891+
&mut StmtCtx {
892+
para: &mut current_paragraph,
893+
paragraph_name: &current_paragraph_name,
894+
section: &current_section,
895+
para_name_map: &para_name_map,
896+
call_graph: &mut call_graph,
897+
},
898+
);
778899
finish_paragraph(
779900
&mut current_paragraph,
780901
&mut current_section,

0 commit comments

Comments
 (0)