cobolx 1.0.0
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.
- package/.devcontainer/devcontainer.json +26 -0
- package/.dockerignore +4 -0
- package/.github/workflows/ci.yml +157 -0
- package/Cargo.lock +2245 -0
- package/Cargo.toml +39 -0
- package/bin/check-update.js +44 -0
- package/bin/cobolx.js +81 -0
- package/docker-compose.yml +33 -0
- package/dockerfile +18 -0
- package/dockerfile.test +39 -0
- package/package.json +27 -0
- package/scripts/install.js +145 -0
- package/src/agent/client.rs +1345 -0
- package/src/agent.rs +1 -0
- package/src/cobol/copybook.rs +71 -0
- package/src/cobol/data_parser.rs +290 -0
- package/src/cobol/indexer.rs +256 -0
- package/src/cobol/layout.rs +278 -0
- package/src/cobol/lexer.rs +135 -0
- package/src/cobol/model.rs +196 -0
- package/src/cobol/scanner.rs +72 -0
- package/src/cobol/source_parser.rs +91 -0
- package/src/cobol.rs +8 -0
- package/src/config/config.rs +64 -0
- package/src/config.rs +3 -0
- package/src/lib.rs +6 -0
- package/src/main.rs +20 -0
- package/src/memory/files.rs +155 -0
- package/src/memory/store.rs +406 -0
- package/src/memory.rs +5 -0
- package/src/ui/draw.rs +519 -0
- package/src/ui/tui.rs +812 -0
- package/src/ui.rs +2 -0
- package/tests/indexer_tests.rs +192 -0
- package/tests/memory_store_tests.rs +21 -0
- package/tests/project_files_tests.rs +72 -0
- package/tests/sandbox_tests.rs +178 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
use crate::cobol::model::ParsedDataItem;
|
|
2
|
+
use std::collections::HashMap;
|
|
3
|
+
|
|
4
|
+
#[derive(Debug)]
|
|
5
|
+
struct Frame {
|
|
6
|
+
idx: usize,
|
|
7
|
+
level: u16,
|
|
8
|
+
base_offset: i64,
|
|
9
|
+
cursor: i64,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Clone, Copy)]
|
|
13
|
+
struct Storage {
|
|
14
|
+
bytes: Option<i64>,
|
|
15
|
+
kind: &'static str,
|
|
16
|
+
status: &'static str,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub(crate) fn compute_physical_layout(items: &mut [ParsedDataItem]) {
|
|
20
|
+
let mut stack = vec![Frame {
|
|
21
|
+
idx: usize::MAX,
|
|
22
|
+
level: 0,
|
|
23
|
+
base_offset: 0,
|
|
24
|
+
cursor: 0,
|
|
25
|
+
}];
|
|
26
|
+
let mut known_offsets = HashMap::<String, i64>::with_capacity(items.len());
|
|
27
|
+
|
|
28
|
+
for idx in 0..items.len() {
|
|
29
|
+
let level = items[idx].level;
|
|
30
|
+
while stack.last().is_some_and(|frame| frame.level >= level) {
|
|
31
|
+
close_group(items, &mut stack, &mut known_offsets);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if matches!(level, 66 | 88) {
|
|
35
|
+
items[idx].byte_offset = current_cursor(&stack);
|
|
36
|
+
items[idx].byte_size = Some(0);
|
|
37
|
+
items[idx].storage_kind = Some(
|
|
38
|
+
if level == 66 {
|
|
39
|
+
"rename"
|
|
40
|
+
} else {
|
|
41
|
+
"condition-name"
|
|
42
|
+
}
|
|
43
|
+
.to_string(),
|
|
44
|
+
);
|
|
45
|
+
items[idx].layout_status = Some("no-storage".to_string());
|
|
46
|
+
known_offsets.insert(items[idx].name.clone(), items[idx].byte_offset.unwrap_or(0));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let offset = items[idx]
|
|
51
|
+
.redefines
|
|
52
|
+
.as_ref()
|
|
53
|
+
.and_then(|name| known_offsets.get(name).copied())
|
|
54
|
+
.unwrap_or_else(|| current_cursor(&stack).unwrap_or(0));
|
|
55
|
+
|
|
56
|
+
items[idx].byte_offset = Some(offset);
|
|
57
|
+
|
|
58
|
+
let storage = classify_storage(&items[idx]);
|
|
59
|
+
if storage.kind == "group" {
|
|
60
|
+
items[idx].storage_kind = Some("group".to_string());
|
|
61
|
+
items[idx].layout_status = Some("pending".to_string());
|
|
62
|
+
stack.push(Frame {
|
|
63
|
+
idx,
|
|
64
|
+
level,
|
|
65
|
+
base_offset: offset,
|
|
66
|
+
cursor: offset,
|
|
67
|
+
});
|
|
68
|
+
known_offsets.insert(items[idx].name.clone(), offset);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let occurs = item_occurs(&items[idx]);
|
|
73
|
+
let total_bytes = storage.bytes.map(|bytes| bytes.saturating_mul(occurs));
|
|
74
|
+
items[idx].byte_size = total_bytes;
|
|
75
|
+
items[idx].storage_kind = Some(storage.kind.to_string());
|
|
76
|
+
items[idx].layout_status = Some(storage.status.to_string());
|
|
77
|
+
|
|
78
|
+
if let Some(total) = total_bytes {
|
|
79
|
+
advance_parent(&mut stack, offset.saturating_add(total));
|
|
80
|
+
}
|
|
81
|
+
known_offsets.insert(items[idx].name.clone(), offset);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
while stack.len() > 1 {
|
|
85
|
+
close_group(items, &mut stack, &mut known_offsets);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn close_group(
|
|
90
|
+
items: &mut [ParsedDataItem],
|
|
91
|
+
stack: &mut Vec<Frame>,
|
|
92
|
+
known_offsets: &mut HashMap<String, i64>,
|
|
93
|
+
) {
|
|
94
|
+
let Some(frame) = stack.pop() else {
|
|
95
|
+
return;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let unit_bytes = frame.cursor.saturating_sub(frame.base_offset);
|
|
99
|
+
let total_bytes = unit_bytes.saturating_mul(item_occurs(&items[frame.idx]));
|
|
100
|
+
items[frame.idx].byte_size = Some(total_bytes);
|
|
101
|
+
items[frame.idx].layout_status = Some("derived".to_string());
|
|
102
|
+
advance_parent(stack, frame.base_offset.saturating_add(total_bytes));
|
|
103
|
+
known_offsets.insert(items[frame.idx].name.clone(), frame.base_offset);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
fn current_cursor(stack: &[Frame]) -> Option<i64> {
|
|
107
|
+
stack.last().map(|frame| frame.cursor)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn advance_parent(stack: &mut [Frame], end_offset: i64) {
|
|
111
|
+
if let Some(parent) = stack.last_mut() {
|
|
112
|
+
parent.cursor = parent.cursor.max(end_offset);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fn item_occurs(item: &ParsedDataItem) -> i64 {
|
|
117
|
+
item.occurs.unwrap_or(1).max(1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn classify_storage(item: &ParsedDataItem) -> Storage {
|
|
121
|
+
let usage = item
|
|
122
|
+
.usage_clause
|
|
123
|
+
.as_deref()
|
|
124
|
+
.map(normalize_usage)
|
|
125
|
+
.unwrap_or_else(|| "DISPLAY".to_string());
|
|
126
|
+
|
|
127
|
+
if item.pic.is_none() {
|
|
128
|
+
return match usage.as_str() {
|
|
129
|
+
"COMP-1" | "COMPUTATIONAL-1" => Storage {
|
|
130
|
+
bytes: Some(4),
|
|
131
|
+
kind: "float",
|
|
132
|
+
status: "exact",
|
|
133
|
+
},
|
|
134
|
+
"COMP-2" | "COMPUTATIONAL-2" => Storage {
|
|
135
|
+
bytes: Some(8),
|
|
136
|
+
kind: "float",
|
|
137
|
+
status: "exact",
|
|
138
|
+
},
|
|
139
|
+
"POINTER" => Storage {
|
|
140
|
+
bytes: Some(8),
|
|
141
|
+
kind: "pointer",
|
|
142
|
+
status: "estimated",
|
|
143
|
+
},
|
|
144
|
+
"INDEX" => Storage {
|
|
145
|
+
bytes: Some(4),
|
|
146
|
+
kind: "index",
|
|
147
|
+
status: "estimated",
|
|
148
|
+
},
|
|
149
|
+
_ => Storage {
|
|
150
|
+
bytes: None,
|
|
151
|
+
kind: "group",
|
|
152
|
+
status: "pending",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let pic = item.pic.as_deref().unwrap_or_default();
|
|
158
|
+
match usage.as_str() {
|
|
159
|
+
"COMP-3" | "COMPUTATIONAL-3" | "PACKED-DECIMAL" => {
|
|
160
|
+
let digits = picture_digits(pic);
|
|
161
|
+
Storage {
|
|
162
|
+
bytes: digits.map(|d| (d + 2) / 2),
|
|
163
|
+
kind: "packed-decimal",
|
|
164
|
+
status: "exact",
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
"BINARY" | "COMP" | "COMP-4" | "COMP-5" | "COMPUTATIONAL" | "COMPUTATIONAL-4"
|
|
168
|
+
| "COMPUTATIONAL-5" => {
|
|
169
|
+
let digits = picture_digits(pic);
|
|
170
|
+
Storage {
|
|
171
|
+
bytes: digits.and_then(binary_bytes_for_digits),
|
|
172
|
+
kind: "binary",
|
|
173
|
+
status: "estimated",
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
"COMP-1" | "COMPUTATIONAL-1" => Storage {
|
|
177
|
+
bytes: Some(4),
|
|
178
|
+
kind: "float",
|
|
179
|
+
status: "exact",
|
|
180
|
+
},
|
|
181
|
+
"COMP-2" | "COMPUTATIONAL-2" => Storage {
|
|
182
|
+
bytes: Some(8),
|
|
183
|
+
kind: "float",
|
|
184
|
+
status: "exact",
|
|
185
|
+
},
|
|
186
|
+
"NATIONAL" => Storage {
|
|
187
|
+
bytes: picture_positions(pic).map(|positions| positions.saturating_mul(2)),
|
|
188
|
+
kind: "national",
|
|
189
|
+
status: "estimated",
|
|
190
|
+
},
|
|
191
|
+
_ => {
|
|
192
|
+
let positions = picture_positions(pic);
|
|
193
|
+
let has_national = expanded_picture_chars(pic).any(|ch| ch == 'N');
|
|
194
|
+
Storage {
|
|
195
|
+
bytes: positions.map(|n| if has_national { n.saturating_mul(2) } else { n }),
|
|
196
|
+
kind: "display",
|
|
197
|
+
status: if has_national { "estimated" } else { "exact" },
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn normalize_usage(usage: &str) -> String {
|
|
204
|
+
usage
|
|
205
|
+
.split_whitespace()
|
|
206
|
+
.filter(|part| *part != "IS")
|
|
207
|
+
.collect::<Vec<_>>()
|
|
208
|
+
.join("-")
|
|
209
|
+
.to_ascii_uppercase()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn binary_bytes_for_digits(digits: i64) -> Option<i64> {
|
|
213
|
+
match digits {
|
|
214
|
+
1..=4 => Some(2),
|
|
215
|
+
5..=9 => Some(4),
|
|
216
|
+
10..=18 => Some(8),
|
|
217
|
+
_ => None,
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn picture_digits(pic: &str) -> Option<i64> {
|
|
222
|
+
let digits = expanded_picture_chars(pic)
|
|
223
|
+
.filter(|ch| matches!(*ch, '9' | 'Z' | '*'))
|
|
224
|
+
.count() as i64;
|
|
225
|
+
(digits > 0).then_some(digits)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn picture_positions(pic: &str) -> Option<i64> {
|
|
229
|
+
let positions = expanded_picture_chars(pic)
|
|
230
|
+
.filter(|ch| !matches!(*ch, 'S' | 'V' | 'P'))
|
|
231
|
+
.count() as i64;
|
|
232
|
+
(positions > 0).then_some(positions)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn expanded_picture_chars(pic: &str) -> impl Iterator<Item = char> + '_ {
|
|
236
|
+
PictureChars {
|
|
237
|
+
chars: pic.chars().peekable(),
|
|
238
|
+
repeat_char: None,
|
|
239
|
+
repeat_left: 0,
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
struct PictureChars<I: Iterator<Item = char>> {
|
|
244
|
+
chars: std::iter::Peekable<I>,
|
|
245
|
+
repeat_char: Option<char>,
|
|
246
|
+
repeat_left: usize,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
impl<I: Iterator<Item = char>> Iterator for PictureChars<I> {
|
|
250
|
+
type Item = char;
|
|
251
|
+
|
|
252
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
253
|
+
if self.repeat_left > 0 {
|
|
254
|
+
self.repeat_left -= 1;
|
|
255
|
+
return self.repeat_char;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let ch = self.chars.next()?.to_ascii_uppercase();
|
|
259
|
+
if self.chars.peek() == Some(&'(') {
|
|
260
|
+
self.chars.next();
|
|
261
|
+
let mut n = String::new();
|
|
262
|
+
while let Some(next) = self.chars.peek().copied() {
|
|
263
|
+
self.chars.next();
|
|
264
|
+
if next == ')' {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
n.push(next);
|
|
268
|
+
}
|
|
269
|
+
let repeat = n.parse::<usize>().unwrap_or(1);
|
|
270
|
+
if repeat > 1 {
|
|
271
|
+
self.repeat_char = Some(ch);
|
|
272
|
+
self.repeat_left = repeat - 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
Some(ch)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
use crate::cobol::model::{LogicalLine, Token};
|
|
2
|
+
|
|
3
|
+
pub(crate) fn logical_lines(content: &str) -> Vec<LogicalLine> {
|
|
4
|
+
let mut lines = Vec::new();
|
|
5
|
+
let mut current = String::new();
|
|
6
|
+
let mut current_start = 0usize;
|
|
7
|
+
let mut current_len = 0usize;
|
|
8
|
+
let mut offset = 0usize;
|
|
9
|
+
|
|
10
|
+
for raw_line in content.lines() {
|
|
11
|
+
let raw_start = offset;
|
|
12
|
+
let raw_len = raw_line.len();
|
|
13
|
+
offset += raw_len + 1;
|
|
14
|
+
|
|
15
|
+
let Some(code) = code_line(raw_line) else {
|
|
16
|
+
continue;
|
|
17
|
+
};
|
|
18
|
+
let trimmed = code.trim();
|
|
19
|
+
if trimmed.is_empty() {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if current.is_empty() {
|
|
24
|
+
current_start = raw_start + code.find(trimmed).unwrap_or(0);
|
|
25
|
+
current_len = raw_len;
|
|
26
|
+
} else {
|
|
27
|
+
current.push(' ');
|
|
28
|
+
current_len = (raw_start + raw_len).saturating_sub(current_start);
|
|
29
|
+
}
|
|
30
|
+
current.push_str(trimmed);
|
|
31
|
+
|
|
32
|
+
if has_statement_terminator(trimmed) {
|
|
33
|
+
lines.push(LogicalLine {
|
|
34
|
+
text: std::mem::take(&mut current),
|
|
35
|
+
start_offset: current_start,
|
|
36
|
+
byte_len: current_len,
|
|
37
|
+
});
|
|
38
|
+
current_len = 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if !current.is_empty() {
|
|
43
|
+
lines.push(LogicalLine {
|
|
44
|
+
text: current,
|
|
45
|
+
start_offset: current_start,
|
|
46
|
+
byte_len: current_len,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
lines
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(crate) fn tokenize(line: &str) -> Vec<Token> {
|
|
54
|
+
let bytes = line.as_bytes();
|
|
55
|
+
let mut tokens = Vec::new();
|
|
56
|
+
let mut i = 0usize;
|
|
57
|
+
|
|
58
|
+
while i < bytes.len() {
|
|
59
|
+
let b = bytes[i];
|
|
60
|
+
if b == b'\'' || b == b'"' {
|
|
61
|
+
let quote = b;
|
|
62
|
+
let start = i;
|
|
63
|
+
i += 1;
|
|
64
|
+
let text_start = i;
|
|
65
|
+
while i < bytes.len() && bytes[i] != quote {
|
|
66
|
+
i += 1;
|
|
67
|
+
}
|
|
68
|
+
tokens.push(Token {
|
|
69
|
+
text: line[text_start..i].to_ascii_uppercase(),
|
|
70
|
+
start,
|
|
71
|
+
quoted: true,
|
|
72
|
+
});
|
|
73
|
+
if i < bytes.len() {
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
} else if is_name_byte(b) {
|
|
77
|
+
let start = i;
|
|
78
|
+
while i < bytes.len() && is_name_byte(bytes[i]) {
|
|
79
|
+
i += 1;
|
|
80
|
+
}
|
|
81
|
+
tokens.push(Token {
|
|
82
|
+
text: line[start..i].to_ascii_uppercase(),
|
|
83
|
+
start,
|
|
84
|
+
quoted: false,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
i += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
tokens
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
pub(crate) fn clean_name(name: &str) -> String {
|
|
95
|
+
name.trim_matches('.')
|
|
96
|
+
.trim_matches(',')
|
|
97
|
+
.trim()
|
|
98
|
+
.to_ascii_uppercase()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn code_line(line: &str) -> Option<&str> {
|
|
102
|
+
let trimmed = line.trim_start();
|
|
103
|
+
if trimmed.is_empty() || trimmed.starts_with('*') {
|
|
104
|
+
return None;
|
|
105
|
+
}
|
|
106
|
+
if matches!(line.as_bytes().get(6), Some(b'*' | b'/')) {
|
|
107
|
+
return None;
|
|
108
|
+
}
|
|
109
|
+
line.split("*>").next().map(str::trim_end)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn has_statement_terminator(line: &str) -> bool {
|
|
113
|
+
let mut quote = None::<u8>;
|
|
114
|
+
let bytes = line.as_bytes();
|
|
115
|
+
|
|
116
|
+
for (idx, b) in bytes.iter().enumerate() {
|
|
117
|
+
match (quote, *b) {
|
|
118
|
+
(Some(q), c) if c == q => quote = None,
|
|
119
|
+
(None, b'\'' | b'"') => quote = Some(*b),
|
|
120
|
+
(None, b'.') => {
|
|
121
|
+
let next = bytes.get(idx + 1).copied();
|
|
122
|
+
if next.map_or(true, |c| c.is_ascii_whitespace()) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
_ => {}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn is_name_byte(b: u8) -> bool {
|
|
134
|
+
b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'$' | b'#' | b'@')
|
|
135
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
4
|
+
pub enum CallKind {
|
|
5
|
+
Static,
|
|
6
|
+
Dynamic,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
impl CallKind {
|
|
10
|
+
pub(crate) fn as_str(self) -> &'static str {
|
|
11
|
+
match self {
|
|
12
|
+
CallKind::Static => "static",
|
|
13
|
+
CallKind::Dynamic => "dynamic",
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct CallSummary {
|
|
20
|
+
pub target: String,
|
|
21
|
+
pub kind: CallKind,
|
|
22
|
+
pub using_count: usize,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Debug, Clone)]
|
|
26
|
+
pub struct CopybookSummary {
|
|
27
|
+
pub name: String,
|
|
28
|
+
pub resolved_path: Option<PathBuf>,
|
|
29
|
+
pub has_replacing: bool,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Clone)]
|
|
33
|
+
pub struct ProgramSummary {
|
|
34
|
+
pub name: String,
|
|
35
|
+
pub path: PathBuf,
|
|
36
|
+
pub copybooks: Vec<CopybookSummary>,
|
|
37
|
+
pub calls: Vec<CallSummary>,
|
|
38
|
+
pub data_items: usize,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(Debug, Clone)]
|
|
42
|
+
pub struct IndexReport {
|
|
43
|
+
pub files: Vec<crate::cobol::scanner::CobolFileEntry>,
|
|
44
|
+
pub source_count: usize,
|
|
45
|
+
pub copybook_count: usize,
|
|
46
|
+
pub programs: Vec<ProgramSummary>,
|
|
47
|
+
pub copybook_uses: usize,
|
|
48
|
+
pub resolved_copybooks: usize,
|
|
49
|
+
pub unresolved_copybooks: Vec<String>,
|
|
50
|
+
pub static_calls: usize,
|
|
51
|
+
pub dynamic_calls: usize,
|
|
52
|
+
pub data_items: usize,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl IndexReport {
|
|
56
|
+
pub fn to_message(&self, db_path: &Path) -> String {
|
|
57
|
+
let mut out = format!(
|
|
58
|
+
"Project index initialized.\n Files: {} (sources: {}, copybooks: {})\n Programs: {}\n COPY uses: {} resolved / {} total\n CALL edges: {} static, {} dynamic\n DATA items: {}\n SQLite: {}",
|
|
59
|
+
self.files.len(),
|
|
60
|
+
self.source_count,
|
|
61
|
+
self.copybook_count,
|
|
62
|
+
self.programs.len(),
|
|
63
|
+
self.resolved_copybooks,
|
|
64
|
+
self.copybook_uses,
|
|
65
|
+
self.static_calls,
|
|
66
|
+
self.dynamic_calls,
|
|
67
|
+
self.data_items,
|
|
68
|
+
db_path.to_string_lossy(),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if !self.programs.is_empty() {
|
|
72
|
+
out.push_str("\n\nPrograms:");
|
|
73
|
+
for program in self.programs.iter().take(20) {
|
|
74
|
+
out.push_str(&format!(
|
|
75
|
+
"\n - {} ({})",
|
|
76
|
+
program.name,
|
|
77
|
+
program.path.to_string_lossy()
|
|
78
|
+
));
|
|
79
|
+
if !program.copybooks.is_empty() {
|
|
80
|
+
let names = program
|
|
81
|
+
.copybooks
|
|
82
|
+
.iter()
|
|
83
|
+
.map(|c| {
|
|
84
|
+
if c.resolved_path.is_some() {
|
|
85
|
+
if c.has_replacing {
|
|
86
|
+
format!("{}:resolved+replacing", c.name)
|
|
87
|
+
} else {
|
|
88
|
+
format!("{}:resolved", c.name)
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
format!("{}:missing", c.name)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
.collect::<Vec<_>>()
|
|
95
|
+
.join(", ");
|
|
96
|
+
out.push_str(&format!("\n COPY: {}", names));
|
|
97
|
+
}
|
|
98
|
+
if !program.calls.is_empty() {
|
|
99
|
+
let calls = program
|
|
100
|
+
.calls
|
|
101
|
+
.iter()
|
|
102
|
+
.map(|c| {
|
|
103
|
+
format!("{}({}; USING {})", c.target, c.kind.as_str(), c.using_count)
|
|
104
|
+
})
|
|
105
|
+
.collect::<Vec<_>>()
|
|
106
|
+
.join(", ");
|
|
107
|
+
out.push_str(&format!("\n CALL: {}", calls));
|
|
108
|
+
}
|
|
109
|
+
if program.data_items > 0 {
|
|
110
|
+
out.push_str(&format!("\n DATA: {} item(s)", program.data_items));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if self.programs.len() > 20 {
|
|
114
|
+
out.push_str(&format!(
|
|
115
|
+
"\n ... {} more program(s)",
|
|
116
|
+
self.programs.len() - 20
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if !self.unresolved_copybooks.is_empty() {
|
|
122
|
+
out.push_str("\n\nUnresolved COPY:");
|
|
123
|
+
for name in self.unresolved_copybooks.iter().take(20) {
|
|
124
|
+
out.push_str(&format!("\n - {}", name));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
out
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[derive(Debug)]
|
|
133
|
+
pub(crate) struct ParsedProgram {
|
|
134
|
+
pub(crate) name: String,
|
|
135
|
+
pub(crate) start_offset: usize,
|
|
136
|
+
pub(crate) byte_len: usize,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[derive(Debug)]
|
|
140
|
+
pub(crate) struct ParsedCopy {
|
|
141
|
+
pub(crate) name: String,
|
|
142
|
+
pub(crate) start_offset: usize,
|
|
143
|
+
pub(crate) byte_len: usize,
|
|
144
|
+
pub(crate) replacing_text: Option<String>,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[derive(Debug)]
|
|
148
|
+
pub(crate) struct ParsedCall {
|
|
149
|
+
pub(crate) caller_name: Option<String>,
|
|
150
|
+
pub(crate) target: String,
|
|
151
|
+
pub(crate) kind: CallKind,
|
|
152
|
+
pub(crate) start_offset: usize,
|
|
153
|
+
pub(crate) byte_len: usize,
|
|
154
|
+
pub(crate) using_count: usize,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[derive(Debug)]
|
|
158
|
+
pub(crate) struct ParsedDataItem {
|
|
159
|
+
pub(crate) source_path: PathBuf,
|
|
160
|
+
pub(crate) name: String,
|
|
161
|
+
pub(crate) level: u16,
|
|
162
|
+
pub(crate) parent_name: Option<String>,
|
|
163
|
+
pub(crate) pic: Option<String>,
|
|
164
|
+
pub(crate) usage_clause: Option<String>,
|
|
165
|
+
pub(crate) occurs: Option<i64>,
|
|
166
|
+
pub(crate) redefines: Option<String>,
|
|
167
|
+
pub(crate) section: Option<String>,
|
|
168
|
+
pub(crate) byte_offset: Option<i64>,
|
|
169
|
+
pub(crate) byte_size: Option<i64>,
|
|
170
|
+
pub(crate) storage_kind: Option<String>,
|
|
171
|
+
pub(crate) layout_status: Option<String>,
|
|
172
|
+
pub(crate) start_offset: usize,
|
|
173
|
+
pub(crate) byte_len: usize,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[derive(Debug)]
|
|
177
|
+
pub(crate) struct ParsedFile {
|
|
178
|
+
pub(crate) path: PathBuf,
|
|
179
|
+
pub(crate) programs: Vec<ParsedProgram>,
|
|
180
|
+
pub(crate) copies: Vec<ParsedCopy>,
|
|
181
|
+
pub(crate) calls: Vec<ParsedCall>,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[derive(Debug)]
|
|
185
|
+
pub(crate) struct Token {
|
|
186
|
+
pub(crate) text: String,
|
|
187
|
+
pub(crate) start: usize,
|
|
188
|
+
pub(crate) quoted: bool,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[derive(Debug)]
|
|
192
|
+
pub(crate) struct LogicalLine {
|
|
193
|
+
pub(crate) text: String,
|
|
194
|
+
pub(crate) start_offset: usize,
|
|
195
|
+
pub(crate) byte_len: usize,
|
|
196
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
4
|
+
pub enum CobolFileType {
|
|
5
|
+
Source, // .cbl, .cob, .coo
|
|
6
|
+
Copybook, // .cpy
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
|
10
|
+
pub struct CobolFileEntry {
|
|
11
|
+
pub path: PathBuf,
|
|
12
|
+
pub file_type: CobolFileType,
|
|
13
|
+
pub size_bytes: u64,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Recursively scans `dir` for COBOL source and copybook files, ignoring common directories.
|
|
17
|
+
/// The root directory is always scanned; exclusion rules only apply to subdirectories.
|
|
18
|
+
pub fn scan_sandbox(dir: &Path) -> std::io::Result<Vec<CobolFileEntry>> {
|
|
19
|
+
let mut files = Vec::new();
|
|
20
|
+
scan_dir_entries(dir, &mut files)?;
|
|
21
|
+
files.sort_by(|a, b| a.path.cmp(&b.path));
|
|
22
|
+
Ok(files)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Returns true if a directory name should be excluded from scanning.
|
|
26
|
+
fn should_exclude_dir(name: &str) -> bool {
|
|
27
|
+
name.starts_with('.')
|
|
28
|
+
|| name == "target"
|
|
29
|
+
|| name == "node_modules"
|
|
30
|
+
|| name == "vendor"
|
|
31
|
+
|| name == "build"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Scans entries within `dir`. Does NOT check exclusion on `dir` itself —
|
|
35
|
+
/// callers are responsible for filtering before recursing.
|
|
36
|
+
fn scan_dir_entries(dir: &Path, files: &mut Vec<CobolFileEntry>) -> std::io::Result<()> {
|
|
37
|
+
if !dir.is_dir() {
|
|
38
|
+
return Ok(());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for entry in std::fs::read_dir(dir)? {
|
|
42
|
+
let entry = entry?;
|
|
43
|
+
let path = entry.path();
|
|
44
|
+
if path.is_dir() {
|
|
45
|
+
// Apply exclusion only to child directories, not the root
|
|
46
|
+
if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
|
|
47
|
+
if should_exclude_dir(name) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
scan_dir_entries(&path, files)?;
|
|
52
|
+
} else if path.is_file() {
|
|
53
|
+
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
|
54
|
+
let ext_lower = ext.to_lowercase();
|
|
55
|
+
let file_type = match ext_lower.as_str() {
|
|
56
|
+
"cbl" | "cob" | "coo" => Some(CobolFileType::Source),
|
|
57
|
+
"cpy" => Some(CobolFileType::Copybook),
|
|
58
|
+
_ => None,
|
|
59
|
+
};
|
|
60
|
+
if let Some(ft) = file_type {
|
|
61
|
+
let size_bytes = entry.metadata()?.len();
|
|
62
|
+
files.push(CobolFileEntry {
|
|
63
|
+
path,
|
|
64
|
+
file_type: ft,
|
|
65
|
+
size_bytes,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
Ok(())
|
|
72
|
+
}
|