mcp-for-i 0.1.13 → 0.2.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/README.md +17 -0
- package/dist/config/store.js +229 -109
- package/dist/controlplane/auth.js +128 -0
- package/dist/controlplane/clientIntegrationService.js +83 -4
- package/dist/controlplane/commands.js +14 -9
- package/dist/controlplane/connectionService.js +110 -5
- package/dist/controlplane/connectionTest.js +329 -0
- package/dist/controlplane/errors.js +12 -0
- package/dist/controlplane/runtimeService.js +98 -26
- package/dist/controlplane/server.js +122 -11
- package/dist/controlplane/serverShared.js +25 -6
- package/dist/controlplane/serviceManager.js +113 -100
- package/dist/controlplane/ui.js +1742 -920
- package/dist/ibmi/CompileTools.js +29 -1
- package/dist/ibmi/IBMiContent.js +435 -63
- package/dist/ibmi/IbmDocs.js +685 -0
- package/dist/ibmi/LocalLanguageActions.js +129 -3
- package/dist/ibmi/Search.js +158 -52
- package/dist/ibmi/Tools.js +7 -10
- package/dist/ibmi/client.js +220 -31
- package/dist/ibmi/evfevent.js +256 -0
- package/dist/ibmi/knownHosts.js +190 -0
- package/dist/ibmi/libraryList.js +93 -0
- package/dist/ibmi/sourceInspect.js +815 -0
- package/dist/ibmi/sqlJob.js +888 -0
- package/dist/ibmi/sqlParse.js +317 -0
- package/dist/ibmi/tn5250/certTrust.js +59 -0
- package/dist/ibmi/tn5250/client.js +310 -0
- package/dist/ibmi/tn5250/datastream.js +109 -0
- package/dist/ibmi/tn5250/ebcdic.js +74 -0
- package/dist/ibmi/tn5250/index.js +6 -0
- package/dist/ibmi/tn5250/screen.js +827 -0
- package/dist/ibmi/tn5250/telnet.js +186 -0
- package/dist/ibmi/variables.js +18 -7
- package/dist/index.js +43 -22
- package/dist/mcp/annotations.js +155 -0
- package/dist/mcp/audit.js +490 -133
- package/dist/mcp/confirm.js +48 -0
- package/dist/mcp/context.js +43 -5
- package/dist/mcp/deployMethods.js +192 -0
- package/dist/mcp/logger.js +5 -10
- package/dist/mcp/modules/bulkMembers.js +476 -0
- package/dist/mcp/modules/changedLines.js +189 -0
- package/dist/mcp/modules/compare.js +198 -0
- package/dist/mcp/modules/compile.js +515 -0
- package/dist/mcp/modules/ifsOps.js +123 -0
- package/dist/mcp/modules/index.js +38 -0
- package/dist/mcp/modules/inspect.js +811 -0
- package/dist/mcp/modules/libl.js +246 -0
- package/dist/mcp/modules/members.js +171 -0
- package/dist/mcp/modules/objects.js +339 -0
- package/dist/mcp/modules/system.js +266 -0
- package/dist/mcp/modules/tn5250.js +515 -0
- package/dist/mcp/policy.js +412 -0
- package/dist/mcp/tools.js +1700 -698
- package/dist/security/credentialStore.js +62 -13
- package/dist/security/localPaths.js +138 -0
- package/dist/security/redact.js +79 -0
- package/dist/util/atomicWrite.js +101 -0
- package/dist/util/fileLock.js +206 -0
- package/docs/capabilities.md +50 -7
- package/docs/configuration.md +189 -60
- package/docs/examples.md +109 -73
- package/docs/overview.md +15 -15
- package/docs/security.md +101 -16
- package/docs/tools.md +363 -188
- package/package.json +56 -53
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
// Source-type aware outline and dependency extraction for IBM i source
|
|
2
|
+
// members. The parsers are deliberately lightweight: they work line by line
|
|
3
|
+
// over an already loaded snapshot, never touch the host, and favour precision
|
|
4
|
+
// (no false dependencies such as "PGM" from `CALL PGM(X)`) over completeness.
|
|
5
|
+
const MAX_RESULTS = 1_000;
|
|
6
|
+
const RPG_TYPES = new Set(["RPGLE", "SQLRPGLE", "RPG", "RPG38", "RPG36", "SQLRPG", "RPGLEINC", "RPGINC", "RPGLECPY", "RPGLEH"]);
|
|
7
|
+
const CL_TYPES = new Set(["CLLE", "CLP", "CL", "CLP38", "CLLEINC", "CLINC", "CLLECPY"]);
|
|
8
|
+
const SQL_TYPES = new Set([
|
|
9
|
+
"SQL", "SQLPRC", "SQLPROC", "SQLUDF", "SQLFUNC", "SQLUDT", "SQLTYPE", "SQLTRG", "SQLSEQ", "SQLTABLE",
|
|
10
|
+
"SQLVIEW", "SQLINDEX", "SQLALIAS", "SQLMASK", "SQLPERM", "SQLVAR"
|
|
11
|
+
]);
|
|
12
|
+
const DDS_TYPES = new Set(["PF", "LF", "DSPF", "PRTF", "ICFF", "PF38", "LF38", "DSPF38", "PRTF38"]);
|
|
13
|
+
const COBOL_TYPES = new Set(["CBLLE", "CBL", "SQLCBLLE", "SQLCBL", "CBL38", "CBLLEINC"]);
|
|
14
|
+
const C_TYPES = new Set(["C", "CPP", "CLE", "SQLC", "SQLCPP", "H", "HPP"]);
|
|
15
|
+
const NAME = "[A-Za-z0-9_#$@\\u00A3\\u00A7]+";
|
|
16
|
+
export function detectSourceLanguage(sourceType, lines) {
|
|
17
|
+
const type = String(sourceType || "").trim().toUpperCase();
|
|
18
|
+
if (RPG_TYPES.has(type))
|
|
19
|
+
return "rpg";
|
|
20
|
+
if (CL_TYPES.has(type))
|
|
21
|
+
return "cl";
|
|
22
|
+
if (SQL_TYPES.has(type))
|
|
23
|
+
return "sql";
|
|
24
|
+
if (DDS_TYPES.has(type))
|
|
25
|
+
return "dds";
|
|
26
|
+
if (COBOL_TYPES.has(type))
|
|
27
|
+
return "cobol";
|
|
28
|
+
if (C_TYPES.has(type))
|
|
29
|
+
return "c";
|
|
30
|
+
if (/^\*\*free\b/i.test(lines[0] || ""))
|
|
31
|
+
return "rpg";
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
export function inspectSource(lines, sourceType) {
|
|
35
|
+
const language = detectSourceLanguage(sourceType, lines);
|
|
36
|
+
const collector = new Collector();
|
|
37
|
+
let format;
|
|
38
|
+
switch (language) {
|
|
39
|
+
case "rpg":
|
|
40
|
+
format = inspectRpg(lines, collector);
|
|
41
|
+
break;
|
|
42
|
+
case "cl":
|
|
43
|
+
inspectCl(lines, collector);
|
|
44
|
+
break;
|
|
45
|
+
case "sql":
|
|
46
|
+
inspectSqlSource(lines, collector);
|
|
47
|
+
break;
|
|
48
|
+
case "dds":
|
|
49
|
+
inspectDds(lines, String(sourceType || "").toUpperCase(), collector);
|
|
50
|
+
break;
|
|
51
|
+
case "cobol":
|
|
52
|
+
inspectCobol(lines, collector);
|
|
53
|
+
break;
|
|
54
|
+
case "c":
|
|
55
|
+
inspectC(lines, collector);
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
inspectUnknown(lines, collector);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
language,
|
|
62
|
+
...(format ? { format } : {}),
|
|
63
|
+
symbols: collector.symbols.slice(0, MAX_RESULTS),
|
|
64
|
+
dependencies: [...collector.dependencies.values()].slice(0, MAX_RESULTS)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function outlineSource(lines, sourceType) {
|
|
68
|
+
return inspectSource(lines, sourceType).symbols;
|
|
69
|
+
}
|
|
70
|
+
export function dependenciesFromSource(lines, sourceType) {
|
|
71
|
+
return inspectSource(lines, sourceType).dependencies;
|
|
72
|
+
}
|
|
73
|
+
class Collector {
|
|
74
|
+
symbols = [];
|
|
75
|
+
dependencies = new Map();
|
|
76
|
+
symbol(name, kind, line) {
|
|
77
|
+
const clean = String(name || "").trim();
|
|
78
|
+
if (!clean || this.symbols.length >= MAX_RESULTS)
|
|
79
|
+
return;
|
|
80
|
+
this.symbols.push({ name: clean, kind, line });
|
|
81
|
+
}
|
|
82
|
+
dependency(name, kind, line, via, dynamic = false, preserveCase = false) {
|
|
83
|
+
// Bound procedure names are case-sensitive, so quoted ones keep their case.
|
|
84
|
+
const clean = preserveCase ? String(name || "").trim() : normalizeObjectName(name);
|
|
85
|
+
if (!clean)
|
|
86
|
+
return;
|
|
87
|
+
const key = `${kind}:${clean}`;
|
|
88
|
+
if (this.dependencies.has(key))
|
|
89
|
+
return;
|
|
90
|
+
this.dependencies.set(key, { name: clean, kind, line, via, ...(dynamic ? { dynamic: true } : {}) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function normalizeObjectName(value) {
|
|
94
|
+
let name = String(value || "").trim();
|
|
95
|
+
if (!name)
|
|
96
|
+
return "";
|
|
97
|
+
if (name.startsWith("'") && name.endsWith("'") && name.length >= 2) {
|
|
98
|
+
name = name.slice(1, -1).replace(/''/g, "'").trim();
|
|
99
|
+
// A quoted object name is folded; anything else (an IFS path) keeps its case.
|
|
100
|
+
return /^[A-Za-z0-9_#$@]{1,10}$/.test(name) ? name.toUpperCase() : name;
|
|
101
|
+
}
|
|
102
|
+
if (name.startsWith("\"") && name.endsWith("\""))
|
|
103
|
+
return name;
|
|
104
|
+
return name.replace(/[),;.]+$/, "").toUpperCase();
|
|
105
|
+
}
|
|
106
|
+
function unquote(value) {
|
|
107
|
+
const trimmed = String(value || "").trim();
|
|
108
|
+
if (trimmed.length >= 2 && ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith("\"") && trimmed.endsWith("\"")))) {
|
|
109
|
+
return trimmed.slice(1, -1).replace(/''/g, "'");
|
|
110
|
+
}
|
|
111
|
+
return trimmed;
|
|
112
|
+
}
|
|
113
|
+
function inspectRpg(lines, out) {
|
|
114
|
+
const fullyFree = /^\*\*free\b/i.test(lines[0] || "");
|
|
115
|
+
const state = { buffer: "", bufferLine: 0, sqlLine: 0 };
|
|
116
|
+
for (let index = fullyFree ? 1 : 0; index < lines.length; index++) {
|
|
117
|
+
const raw = lines[index] ?? "";
|
|
118
|
+
const lineNo = index + 1;
|
|
119
|
+
// `**CTDATA`, `**FTRANS` or a bare `**` start compile-time data.
|
|
120
|
+
if (index > 0 && raw.startsWith("**"))
|
|
121
|
+
break;
|
|
122
|
+
if (fullyFree) {
|
|
123
|
+
if (rpgDirective(raw.trimStart(), lineNo, out))
|
|
124
|
+
continue;
|
|
125
|
+
appendFree(state, stripFreeComment(raw), lineNo, out);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (raw.length < 7 && raw.trim() === "")
|
|
129
|
+
continue;
|
|
130
|
+
const spec = (raw[5] || " ").toUpperCase();
|
|
131
|
+
const col7 = raw[6] || " ";
|
|
132
|
+
const fromCol7 = raw.slice(6);
|
|
133
|
+
if (col7 === "*")
|
|
134
|
+
continue; // fixed-form comment line
|
|
135
|
+
if (col7 === "/") {
|
|
136
|
+
if (spec === "C" && /^\/EXEC\s+SQL\b/i.test(fromCol7)) {
|
|
137
|
+
flushFree(state, out);
|
|
138
|
+
state.sqlBuffer = fromCol7.replace(/^\/EXEC\s+SQL\b/i, "").slice(0, 74);
|
|
139
|
+
state.sqlLine = lineNo;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (spec === "C" && /^\/END-EXEC\b/i.test(fromCol7)) {
|
|
143
|
+
if (typeof state.sqlBuffer === "string")
|
|
144
|
+
sqlStatementDependencies(state.sqlBuffer, state.sqlLine, out, "exec sql");
|
|
145
|
+
state.sqlBuffer = undefined;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
rpgDirective(fromCol7, lineNo, out);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (spec === "C" && col7 === "+" && typeof state.sqlBuffer === "string") {
|
|
152
|
+
state.sqlBuffer += " " + raw.slice(7, 80);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (spec === " " || spec === "") {
|
|
156
|
+
// Free-form code in columns 8-80 of a non-**FREE member.
|
|
157
|
+
if (col7 === " " || col7 === "") {
|
|
158
|
+
const code = raw.slice(7, 80);
|
|
159
|
+
if (rpgDirective(code.trimStart(), lineNo, out))
|
|
160
|
+
continue;
|
|
161
|
+
appendFree(state, stripFreeComment(code), lineNo, out);
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if ("HFDICOP".includes(spec)) {
|
|
166
|
+
flushFree(state, out);
|
|
167
|
+
fixedSpec(lines, index, spec, state, out);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
flushFree(state, out);
|
|
171
|
+
return fullyFree ? "free" : "fixed";
|
|
172
|
+
}
|
|
173
|
+
function rpgDirective(text, lineNo, out) {
|
|
174
|
+
const match = /^\/(COPY|INCLUDE)\s+([^\s]+(?:\s*,\s*[^\s]+)?)/i.exec(text);
|
|
175
|
+
if (!match)
|
|
176
|
+
return /^\/(FREE|END-FREE|TITLE|EJECT|SPACE|DEFINE|UNDEFINE|IF|ELSEIF|ELSE|ENDIF|EOF|SET|RESTORE)\b/i.test(text);
|
|
177
|
+
const target = match[2].replace(/\s+/g, "");
|
|
178
|
+
out.dependency(target, "copy", lineNo, `/${match[1].toUpperCase()}`);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
function stripFreeComment(text) {
|
|
182
|
+
let inQuote = false;
|
|
183
|
+
for (let index = 0; index < text.length; index++) {
|
|
184
|
+
const ch = text[index];
|
|
185
|
+
if (ch === "'")
|
|
186
|
+
inQuote = !inQuote;
|
|
187
|
+
else if (!inQuote && ch === "/" && text[index + 1] === "/")
|
|
188
|
+
return text.slice(0, index);
|
|
189
|
+
}
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
192
|
+
function appendFree(state, text, lineNo, out) {
|
|
193
|
+
if (!text.trim())
|
|
194
|
+
return;
|
|
195
|
+
if (!state.buffer.trim())
|
|
196
|
+
state.bufferLine = lineNo;
|
|
197
|
+
state.buffer += " " + text;
|
|
198
|
+
let separator = findStatementEnd(state.buffer);
|
|
199
|
+
while (separator >= 0) {
|
|
200
|
+
const statement = state.buffer.slice(0, separator);
|
|
201
|
+
freeStatement(statement.trim(), state.bufferLine, out);
|
|
202
|
+
state.buffer = state.buffer.slice(separator + 1);
|
|
203
|
+
state.bufferLine = lineNo;
|
|
204
|
+
separator = findStatementEnd(state.buffer);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function findStatementEnd(text) {
|
|
208
|
+
let inQuote = false;
|
|
209
|
+
for (let index = 0; index < text.length; index++) {
|
|
210
|
+
const ch = text[index];
|
|
211
|
+
if (ch === "'")
|
|
212
|
+
inQuote = !inQuote;
|
|
213
|
+
else if (!inQuote && ch === ";")
|
|
214
|
+
return index;
|
|
215
|
+
}
|
|
216
|
+
return -1;
|
|
217
|
+
}
|
|
218
|
+
function flushFree(state, out) {
|
|
219
|
+
if (state.buffer.trim())
|
|
220
|
+
freeStatement(state.buffer.trim(), state.bufferLine, out);
|
|
221
|
+
state.buffer = "";
|
|
222
|
+
}
|
|
223
|
+
function freeStatement(statement, lineNo, out) {
|
|
224
|
+
const text = statement.replace(/\s+/g, " ");
|
|
225
|
+
if (!text)
|
|
226
|
+
return;
|
|
227
|
+
let match;
|
|
228
|
+
if ((match = new RegExp(`^dcl-proc (${NAME})`, "i").exec(text))) {
|
|
229
|
+
out.symbol(match[1], "procedure", lineNo);
|
|
230
|
+
}
|
|
231
|
+
else if ((match = new RegExp(`^begsr (${NAME})`, "i").exec(text))) {
|
|
232
|
+
out.symbol(match[1], "subroutine", lineNo);
|
|
233
|
+
}
|
|
234
|
+
else if ((match = new RegExp(`^dcl-pr (${NAME})(.*)$`, "i").exec(text))) {
|
|
235
|
+
out.symbol(match[1], "prototype", lineNo);
|
|
236
|
+
prototypeDependencies(match[1], match[2], lineNo, out);
|
|
237
|
+
}
|
|
238
|
+
else if ((match = new RegExp(`^dcl-ds (${NAME})`, "i").exec(text))) {
|
|
239
|
+
if (match[1].toUpperCase() !== "*N")
|
|
240
|
+
out.symbol(match[1], "data-structure", lineNo);
|
|
241
|
+
}
|
|
242
|
+
else if ((match = new RegExp(`^dcl-f (${NAME})(.*)$`, "i").exec(text))) {
|
|
243
|
+
out.symbol(match[1], "file", lineNo);
|
|
244
|
+
out.dependency(externalFileName(match[1], match[2]), "file", lineNo, "dcl-f");
|
|
245
|
+
}
|
|
246
|
+
else if ((match = new RegExp(`^callp(?:\\([a-z ]*\\))? (${NAME})`, "i").exec(text))) {
|
|
247
|
+
out.dependency(match[1], "call", lineNo, "callp");
|
|
248
|
+
}
|
|
249
|
+
else if (/^exec sql\b/i.test(text)) {
|
|
250
|
+
sqlStatementDependencies(text.replace(/^exec sql\b/i, ""), lineNo, out, "exec sql");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function externalFileName(name, keywords) {
|
|
254
|
+
const extdesc = /\bEXTDESC\s*\(\s*'([^']+)'\s*\)/i.exec(keywords);
|
|
255
|
+
if (extdesc)
|
|
256
|
+
return extdesc[1];
|
|
257
|
+
const extfile = /\bEXTFILE\s*\(\s*'([^']+)'\s*\)/i.exec(keywords);
|
|
258
|
+
if (extfile)
|
|
259
|
+
return extfile[1];
|
|
260
|
+
return name;
|
|
261
|
+
}
|
|
262
|
+
function prototypeDependencies(prototype, keywords, lineNo, out) {
|
|
263
|
+
const extpgm = /\bEXTPGM\b\s*(?:\(([^)]*)\))?/i.exec(keywords);
|
|
264
|
+
if (extpgm) {
|
|
265
|
+
const target = extpgm[1] ? lastExternalName(extpgm[1]) : { name: prototype, dynamic: false };
|
|
266
|
+
out.dependency(target.name || prototype, "program", lineNo, "extpgm", target.dynamic);
|
|
267
|
+
}
|
|
268
|
+
const extproc = /\bEXTPROC\b\s*(?:\(([^)]*)\))?/i.exec(keywords);
|
|
269
|
+
if (extproc) {
|
|
270
|
+
const target = extproc[1] ? lastExternalName(extproc[1]) : { name: prototype, dynamic: false };
|
|
271
|
+
out.dependency(target.name || prototype, "procedure", lineNo, "extproc", target.dynamic, Boolean(target.name) && !target.dynamic);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function lastExternalName(argument) {
|
|
275
|
+
const quoted = [...argument.matchAll(/'((?:[^']|'')*)'/g)];
|
|
276
|
+
if (quoted.length > 0) {
|
|
277
|
+
// EXTPROC(*JAVA:'class':'method') names a method; the class is the target.
|
|
278
|
+
const pick = /^\s*\*JAVA\b/i.test(argument) ? quoted[0] : quoted[quoted.length - 1];
|
|
279
|
+
return { name: pick[1].replace(/''/g, "'"), dynamic: false };
|
|
280
|
+
}
|
|
281
|
+
const parts = argument.split(":").map(part => part.trim()).filter(part => part && !part.startsWith("*"));
|
|
282
|
+
// EXTPROC(*DCLCASE) and similar special values keep the prototype name.
|
|
283
|
+
if (parts.length === 0)
|
|
284
|
+
return { name: "", dynamic: false };
|
|
285
|
+
// An unquoted operand is a named constant or variable resolved at run time.
|
|
286
|
+
return { name: parts[parts.length - 1], dynamic: true };
|
|
287
|
+
}
|
|
288
|
+
function fixedSpec(lines, index, spec, state, out) {
|
|
289
|
+
const raw = lines[index];
|
|
290
|
+
const lineNo = index + 1;
|
|
291
|
+
switch (spec) {
|
|
292
|
+
case "F": {
|
|
293
|
+
const name = raw.slice(6, 16).trim();
|
|
294
|
+
if (!name)
|
|
295
|
+
return;
|
|
296
|
+
out.symbol(name, "file", lineNo);
|
|
297
|
+
out.dependency(externalFileName(name, raw.slice(43, 80)), "file", lineNo, "f-spec");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
case "D": {
|
|
301
|
+
const name = longName(raw.slice(6, 21).trim(), state);
|
|
302
|
+
if (name === undefined)
|
|
303
|
+
return;
|
|
304
|
+
const definition = raw.slice(23, 25).trim().toUpperCase();
|
|
305
|
+
if (definition === "PR" && name) {
|
|
306
|
+
out.symbol(name, "prototype", lineNo);
|
|
307
|
+
let keywords = raw.slice(43, 80);
|
|
308
|
+
for (let next = index + 1; next < lines.length; next++) {
|
|
309
|
+
const line = lines[next] || "";
|
|
310
|
+
if ((line[5] || "").toUpperCase() !== "D" || line[6] === "*")
|
|
311
|
+
break;
|
|
312
|
+
if (line.slice(6, 21).trim() || line.slice(23, 25).trim())
|
|
313
|
+
break;
|
|
314
|
+
keywords += " " + line.slice(43, 80);
|
|
315
|
+
}
|
|
316
|
+
prototypeDependencies(name, keywords, lineNo, out);
|
|
317
|
+
}
|
|
318
|
+
else if (definition === "DS" && name) {
|
|
319
|
+
out.symbol(name, "data-structure", lineNo);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
case "P": {
|
|
324
|
+
const name = longName(raw.slice(6, 21).trim(), state);
|
|
325
|
+
if (name && raw.slice(23, 25).trim().toUpperCase() === "B")
|
|
326
|
+
out.symbol(name, "procedure", lineNo);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
case "C": {
|
|
330
|
+
const opcode = raw.slice(25, 35).trim().toUpperCase().replace(/\(.*\)$/, "");
|
|
331
|
+
const factor1 = raw.slice(11, 25).trim();
|
|
332
|
+
const factor2 = raw.slice(35, 49).trim();
|
|
333
|
+
if (opcode === "BEGSR" && factor1) {
|
|
334
|
+
out.symbol(factor1, "subroutine", lineNo);
|
|
335
|
+
}
|
|
336
|
+
else if (opcode === "CALL" && factor2) {
|
|
337
|
+
out.dependency(unquote(factor2), "program", lineNo, "call", !factor2.startsWith("'"));
|
|
338
|
+
}
|
|
339
|
+
else if (opcode === "CALLB" && factor2) {
|
|
340
|
+
out.dependency(unquote(factor2), "procedure", lineNo, "callb", !factor2.startsWith("'"), factor2.startsWith("'"));
|
|
341
|
+
}
|
|
342
|
+
else if (opcode === "CALLP") {
|
|
343
|
+
const match = new RegExp(`^(${NAME})`).exec(raw.slice(35, 80).trim());
|
|
344
|
+
if (match)
|
|
345
|
+
out.dependency(match[1], "call", lineNo, "callp");
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
default:
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Fixed-form names longer than the name field end in "..." and continue on
|
|
354
|
+
// the next specification. Returns undefined while a long name is pending.
|
|
355
|
+
function longName(field, state) {
|
|
356
|
+
if (field.endsWith("...")) {
|
|
357
|
+
state.pendingLongName = (state.pendingLongName || "") + field.slice(0, -3);
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
if (state.pendingLongName) {
|
|
361
|
+
const name = state.pendingLongName + field;
|
|
362
|
+
state.pendingLongName = undefined;
|
|
363
|
+
return name;
|
|
364
|
+
}
|
|
365
|
+
return field;
|
|
366
|
+
}
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// SQL (standalone source and embedded statements)
|
|
369
|
+
const SQL_NOT_TABLES = new Set([
|
|
370
|
+
"SELECT", "TABLE", "LATERAL", "FINAL", "NEW", "OLD", "UNNEST", "XMLTABLE", "JSON_TABLE", "VALUES", "SET",
|
|
371
|
+
"OF", "WHERE", "ONLY", "WITH", "AS", "ON", "USING", "SYSIBM.SYSDUMMY1"
|
|
372
|
+
]);
|
|
373
|
+
const SQL_CLAUSE_WORDS = new Set([
|
|
374
|
+
"WHERE", "GROUP", "ORDER", "HAVING", "FETCH", "UNION", "EXCEPT", "INTERSECT", "JOIN", "INNER", "LEFT",
|
|
375
|
+
"RIGHT", "FULL", "CROSS", "EXCEPTION", "ON", "USING", "SET", "FOR", "WITH", "LIMIT", "OFFSET", "INTO",
|
|
376
|
+
"VALUES", "OPTIMIZE", "WHEN", "THEN", "SKIP", "WAIT", "USE", "NATURAL", "OUTER"
|
|
377
|
+
]);
|
|
378
|
+
export function sqlTableReferences(sqlText) {
|
|
379
|
+
const text = stripSqlNoise(sqlText);
|
|
380
|
+
const tokens = text.match(/"[^"]*"(?:[./]"[^"]*"|[./][A-Za-z0-9_#$@]+)*|[A-Za-z0-9_#$@:]+(?:[./](?:"[^"]*"|[A-Za-z0-9_#$@]+))*|[(),;]/g) || [];
|
|
381
|
+
const upper = tokens.map(token => token.startsWith("\"") ? token : token.toUpperCase());
|
|
382
|
+
const cteNames = new Set();
|
|
383
|
+
for (let index = 0; index + 2 < upper.length; index++) {
|
|
384
|
+
if ((upper[index] === "WITH" || upper[index] === "," || upper[index] === "RECURSIVE") && upper[index + 2] === "AS" && upper[index + 3] === "(") {
|
|
385
|
+
cteNames.add(upper[index + 1]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const found = [];
|
|
389
|
+
const isIdentifier = (token) => Boolean(token) && /^(?:"|[A-Za-z0-9_#$@])/.test(token) && !token.startsWith(":") && !/^\d/.test(token);
|
|
390
|
+
const addTable = (token, via) => {
|
|
391
|
+
if (!isIdentifier(token))
|
|
392
|
+
return false;
|
|
393
|
+
const name = token.startsWith("\"") ? token : token.toUpperCase();
|
|
394
|
+
if (SQL_NOT_TABLES.has(name) || cteNames.has(name))
|
|
395
|
+
return false;
|
|
396
|
+
found.push({ name, kind: "table", via });
|
|
397
|
+
return true;
|
|
398
|
+
};
|
|
399
|
+
// Parentheses that open a subquery may contain FROM; parentheses of a
|
|
400
|
+
// function call such as EXTRACT(YEAR FROM x) or TRIM(' ' FROM x) may not.
|
|
401
|
+
const parens = [];
|
|
402
|
+
for (let index = 0; index < upper.length; index++) {
|
|
403
|
+
const token = upper[index];
|
|
404
|
+
const previous = upper[index - 1];
|
|
405
|
+
const next = upper[index + 1];
|
|
406
|
+
if (token === "(") {
|
|
407
|
+
parens.push(next === "SELECT" || next === "WITH" || next === "VALUES");
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (token === ")") {
|
|
411
|
+
parens.pop();
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (token === "FROM" || token === "JOIN") {
|
|
415
|
+
if (parens.length > 0 && !parens[parens.length - 1])
|
|
416
|
+
continue;
|
|
417
|
+
if (token === "FROM" && (previous === "DISTINCT" || previous === "IS"))
|
|
418
|
+
continue;
|
|
419
|
+
if (token === "FROM" && upper.slice(Math.max(0, index - 4), index).includes("FETCH"))
|
|
420
|
+
continue;
|
|
421
|
+
if (!isIdentifier(tokens[index + 1]) || SQL_NOT_TABLES.has(upper[index + 1]))
|
|
422
|
+
continue;
|
|
423
|
+
addTable(tokens[index + 1], token.toLowerCase());
|
|
424
|
+
if (token !== "FROM")
|
|
425
|
+
continue;
|
|
426
|
+
// FROM a [AS] x, b [AS] y
|
|
427
|
+
let cursor = index + 2;
|
|
428
|
+
while (cursor < upper.length) {
|
|
429
|
+
if (upper[cursor] === "AS")
|
|
430
|
+
cursor++;
|
|
431
|
+
if (isIdentifier(upper[cursor]) && !SQL_CLAUSE_WORDS.has(upper[cursor]) && upper[cursor] !== ",")
|
|
432
|
+
cursor++;
|
|
433
|
+
if (upper[cursor] !== ",")
|
|
434
|
+
break;
|
|
435
|
+
if (!isIdentifier(tokens[cursor + 1]) || SQL_NOT_TABLES.has(upper[cursor + 1]))
|
|
436
|
+
break;
|
|
437
|
+
addTable(tokens[cursor + 1], "from");
|
|
438
|
+
cursor += 2;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else if (token === "INTO" && (previous === "INSERT" || previous === "MERGE")) {
|
|
442
|
+
addTable(tokens[index + 1], previous.toLowerCase() + " into");
|
|
443
|
+
}
|
|
444
|
+
else if (token === "UPDATE" && previous !== "FOR" && previous !== "ON" && previous !== "OR" && next !== "OF" && next !== "SET") {
|
|
445
|
+
addTable(tokens[index + 1], "update");
|
|
446
|
+
}
|
|
447
|
+
else if (token === "REFERENCES") {
|
|
448
|
+
addTable(tokens[index + 1], "references");
|
|
449
|
+
}
|
|
450
|
+
else if (token === "CALL" && isIdentifier(tokens[index + 1])) {
|
|
451
|
+
found.push({ name: upper[index + 1], kind: "procedure", via: "call" });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
const index = /\bINDEX\s+(?:"[^"]*"|[A-Za-z0-9_#$@./]+)\s+ON\s+("[^"]*"|[A-Za-z0-9_#$@./]+)/i.exec(text);
|
|
455
|
+
if (index)
|
|
456
|
+
addTable(index[1], "index on");
|
|
457
|
+
const trigger = /\bTRIGGER\b[\s\S]*?\bON\s+("[^"]*"|[A-Za-z0-9_#$@./]+)/i.exec(text);
|
|
458
|
+
if (trigger)
|
|
459
|
+
addTable(trigger[1], "trigger on");
|
|
460
|
+
return found;
|
|
461
|
+
}
|
|
462
|
+
function stripSqlNoise(sqlText) {
|
|
463
|
+
return String(sqlText || "")
|
|
464
|
+
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
465
|
+
.replace(/--[^\n]*/g, " ")
|
|
466
|
+
.replace(/'(?:[^']|'')*'/g, "''")
|
|
467
|
+
.replace(/\s+/g, " ");
|
|
468
|
+
}
|
|
469
|
+
function sqlStatementDependencies(sqlText, lineNo, out, via) {
|
|
470
|
+
for (const reference of sqlTableReferences(sqlText)) {
|
|
471
|
+
out.dependency(reference.name, reference.kind === "procedure" ? "procedure" : "sql-table", lineNo, `${via}: ${reference.via}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function inspectSqlSource(lines, out) {
|
|
475
|
+
let buffer = "";
|
|
476
|
+
let startLine = 0;
|
|
477
|
+
let inComment = false;
|
|
478
|
+
const flush = () => {
|
|
479
|
+
if (!buffer.trim())
|
|
480
|
+
return;
|
|
481
|
+
const statement = buffer.replace(/\s+/g, " ").trim();
|
|
482
|
+
const create = /\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:UNIQUE\s+|ENCODED\s+VECTOR\s+)?(TABLE|VIEW|INDEX|PROCEDURE|FUNCTION|TRIGGER|SEQUENCE|ALIAS|VARIABLE|MASK|PERMISSION|TYPE)\s+("[^"]*"|[A-Za-z0-9_#$@./]+)/i.exec(statement);
|
|
483
|
+
if (create)
|
|
484
|
+
out.symbol(create[2].startsWith("\"") ? create[2] : create[2].toUpperCase(), create[1].toLowerCase(), startLine);
|
|
485
|
+
sqlStatementDependencies(statement, startLine, out, "sql");
|
|
486
|
+
buffer = "";
|
|
487
|
+
};
|
|
488
|
+
lines.forEach((rawLine, index) => {
|
|
489
|
+
let line = rawLine;
|
|
490
|
+
if (inComment) {
|
|
491
|
+
const end = line.indexOf("*/");
|
|
492
|
+
if (end < 0)
|
|
493
|
+
return;
|
|
494
|
+
line = line.slice(end + 2);
|
|
495
|
+
inComment = false;
|
|
496
|
+
}
|
|
497
|
+
line = line.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
498
|
+
const open = line.indexOf("/*");
|
|
499
|
+
if (open >= 0) {
|
|
500
|
+
line = line.slice(0, open);
|
|
501
|
+
inComment = true;
|
|
502
|
+
}
|
|
503
|
+
line = line.replace(/--.*$/, "");
|
|
504
|
+
let rest = line;
|
|
505
|
+
while (rest.length > 0) {
|
|
506
|
+
const end = findStatementEnd(rest);
|
|
507
|
+
const piece = end >= 0 ? rest.slice(0, end) : rest;
|
|
508
|
+
if (piece.trim() && !buffer.trim())
|
|
509
|
+
startLine = index + 1;
|
|
510
|
+
buffer += " " + piece;
|
|
511
|
+
if (end < 0)
|
|
512
|
+
break;
|
|
513
|
+
flush();
|
|
514
|
+
rest = rest.slice(end + 1);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
flush();
|
|
518
|
+
}
|
|
519
|
+
export function parseClParameters(text) {
|
|
520
|
+
const parameters = [];
|
|
521
|
+
let index = 0;
|
|
522
|
+
const source = String(text || "");
|
|
523
|
+
while (index < source.length) {
|
|
524
|
+
while (index < source.length && /\s/.test(source[index]))
|
|
525
|
+
index++;
|
|
526
|
+
if (index >= source.length)
|
|
527
|
+
break;
|
|
528
|
+
let token = "";
|
|
529
|
+
let depth = 0;
|
|
530
|
+
let inQuote = false;
|
|
531
|
+
while (index < source.length) {
|
|
532
|
+
const ch = source[index];
|
|
533
|
+
if (inQuote) {
|
|
534
|
+
token += ch;
|
|
535
|
+
if (ch === "'") {
|
|
536
|
+
if (source[index + 1] === "'") {
|
|
537
|
+
token += "'";
|
|
538
|
+
index++;
|
|
539
|
+
}
|
|
540
|
+
else
|
|
541
|
+
inQuote = false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else if (ch === "'") {
|
|
545
|
+
inQuote = true;
|
|
546
|
+
token += ch;
|
|
547
|
+
}
|
|
548
|
+
else if (ch === "(") {
|
|
549
|
+
depth++;
|
|
550
|
+
token += ch;
|
|
551
|
+
}
|
|
552
|
+
else if (ch === ")") {
|
|
553
|
+
depth = Math.max(0, depth - 1);
|
|
554
|
+
token += ch;
|
|
555
|
+
}
|
|
556
|
+
else if (/\s/.test(ch) && depth === 0) {
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
token += ch;
|
|
561
|
+
}
|
|
562
|
+
index++;
|
|
563
|
+
}
|
|
564
|
+
const keyword = /^([A-Za-z][A-Za-z0-9]*)\(([\s\S]*)\)$/.exec(token);
|
|
565
|
+
if (keyword)
|
|
566
|
+
parameters.push({ keyword: keyword[1].toUpperCase(), value: keyword[2].trim() });
|
|
567
|
+
else
|
|
568
|
+
parameters.push({ value: token });
|
|
569
|
+
}
|
|
570
|
+
return parameters;
|
|
571
|
+
}
|
|
572
|
+
function clParameter(parameters, keyword, position) {
|
|
573
|
+
const named = parameters.find(parameter => parameter.keyword === keyword);
|
|
574
|
+
if (named)
|
|
575
|
+
return named.value;
|
|
576
|
+
const positional = parameters.filter(parameter => !parameter.keyword);
|
|
577
|
+
return positional[position]?.value;
|
|
578
|
+
}
|
|
579
|
+
function clStatements(lines) {
|
|
580
|
+
const statements = [];
|
|
581
|
+
let buffer = "";
|
|
582
|
+
let startLine = 0;
|
|
583
|
+
let inComment = false;
|
|
584
|
+
lines.forEach((rawLine, index) => {
|
|
585
|
+
let line = "";
|
|
586
|
+
let inQuote = false;
|
|
587
|
+
for (let position = 0; position < rawLine.length; position++) {
|
|
588
|
+
const ch = rawLine[position];
|
|
589
|
+
if (inComment) {
|
|
590
|
+
if (ch === "*" && rawLine[position + 1] === "/") {
|
|
591
|
+
inComment = false;
|
|
592
|
+
position++;
|
|
593
|
+
}
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
if (ch === "'")
|
|
597
|
+
inQuote = !inQuote;
|
|
598
|
+
if (!inQuote && ch === "/" && rawLine[position + 1] === "*") {
|
|
599
|
+
inComment = true;
|
|
600
|
+
position++;
|
|
601
|
+
line += " ";
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
line += ch;
|
|
605
|
+
}
|
|
606
|
+
const trimmed = line.trimEnd();
|
|
607
|
+
if (!trimmed.trim() && !buffer)
|
|
608
|
+
return;
|
|
609
|
+
if (!buffer)
|
|
610
|
+
startLine = index + 1;
|
|
611
|
+
if (trimmed.endsWith("+")) {
|
|
612
|
+
buffer += trimmed.slice(0, -1) + " ";
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (trimmed.endsWith("-")) {
|
|
616
|
+
buffer += trimmed.slice(0, -1);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
buffer += trimmed;
|
|
620
|
+
if (buffer.trim())
|
|
621
|
+
statements.push({ text: buffer.trim(), line: startLine });
|
|
622
|
+
buffer = "";
|
|
623
|
+
});
|
|
624
|
+
if (buffer.trim())
|
|
625
|
+
statements.push({ text: buffer.trim(), line: startLine });
|
|
626
|
+
return statements;
|
|
627
|
+
}
|
|
628
|
+
function inspectCl(lines, out) {
|
|
629
|
+
for (const statement of clStatements(lines)) {
|
|
630
|
+
let text = statement.text;
|
|
631
|
+
const label = /^([A-Za-z@#$][A-Za-z0-9_@#$]*)\s*:\s*/.exec(text);
|
|
632
|
+
if (label) {
|
|
633
|
+
out.symbol(label[1].toUpperCase(), "label", statement.line);
|
|
634
|
+
text = text.slice(label[0].length);
|
|
635
|
+
}
|
|
636
|
+
clCommand(text, statement.line, out, 0);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function clCommand(text, lineNo, out, depth) {
|
|
640
|
+
const match = /^([A-Za-z@#$*][A-Za-z0-9_@#$]*\/)?([A-Za-z@#$][A-Za-z0-9_@#$]*)\s*([\s\S]*)$/.exec(text.trim());
|
|
641
|
+
if (!match || depth > 3)
|
|
642
|
+
return;
|
|
643
|
+
const command = match[2].toUpperCase();
|
|
644
|
+
const parameters = parseClParameters(match[3]);
|
|
645
|
+
const value = (keyword, position) => clParameter(parameters, keyword, position);
|
|
646
|
+
switch (command) {
|
|
647
|
+
case "PGM":
|
|
648
|
+
out.symbol("PGM", "program-entry", lineNo);
|
|
649
|
+
return;
|
|
650
|
+
case "SUBR": {
|
|
651
|
+
const name = value("SUBR", 0);
|
|
652
|
+
if (name)
|
|
653
|
+
out.symbol(name.toUpperCase(), "subroutine", lineNo);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
case "CALL": {
|
|
657
|
+
const program = value("PGM", 0);
|
|
658
|
+
if (program)
|
|
659
|
+
out.dependency(unquote(program), "program", lineNo, "call", program.startsWith("&"));
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
case "CALLPRC": {
|
|
663
|
+
const procedure = value("PRC", 0);
|
|
664
|
+
if (procedure)
|
|
665
|
+
out.dependency(unquote(procedure), "procedure", lineNo, "callprc", procedure.startsWith("&"), procedure.startsWith("'"));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
case "DCLF": {
|
|
669
|
+
const file = value("FILE", 0);
|
|
670
|
+
if (file)
|
|
671
|
+
out.dependency(file, "file", lineNo, "dclf", file.includes("&"));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
case "OVRDBF": {
|
|
675
|
+
const target = value("TOFILE", 1);
|
|
676
|
+
if (target && !/^\*FILE$/i.test(target))
|
|
677
|
+
out.dependency(target.replace(/\s.*$/, ""), "file", lineNo, "ovrdbf tofile");
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
case "INCLUDE": {
|
|
681
|
+
const member = value("SRCMBR", 0);
|
|
682
|
+
const file = value("SRCFILE", 1);
|
|
683
|
+
if (member)
|
|
684
|
+
out.dependency(file && !/^\*/.test(file) ? `${file}(${member})` : member, "copy", lineNo, "include");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
case "RUNSQL": {
|
|
688
|
+
const sql = value("SQL", 0);
|
|
689
|
+
if (sql)
|
|
690
|
+
sqlStatementDependencies(unquote(sql), lineNo, out, "runsql");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
case "SBMJOB": {
|
|
694
|
+
const nested = value("CMD", 0);
|
|
695
|
+
if (nested)
|
|
696
|
+
clCommand(nested, lineNo, out, depth + 1);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
default:
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// DDS (PF, LF, DSPF, PRTF)
|
|
705
|
+
function inspectDds(lines, sourceType, out) {
|
|
706
|
+
const includeFields = sourceType.startsWith("PF") || sourceType.startsWith("LF");
|
|
707
|
+
lines.forEach((raw, index) => {
|
|
708
|
+
const lineNo = index + 1;
|
|
709
|
+
if ((raw[5] || "A").toUpperCase() !== "A" && raw[5] !== " " && raw[5] !== undefined)
|
|
710
|
+
return;
|
|
711
|
+
if (raw[6] === "*")
|
|
712
|
+
return;
|
|
713
|
+
const nameType = (raw[16] || " ").toUpperCase();
|
|
714
|
+
const name = raw.slice(18, 28).trim();
|
|
715
|
+
if (nameType === "R" && name)
|
|
716
|
+
out.symbol(name.toUpperCase(), "record-format", lineNo);
|
|
717
|
+
else if (nameType === "K" && name)
|
|
718
|
+
out.symbol(name.toUpperCase(), "key", lineNo);
|
|
719
|
+
else if (nameType === " " && name && includeFields)
|
|
720
|
+
out.symbol(name.toUpperCase(), "field", lineNo);
|
|
721
|
+
const keywords = raw.slice(44, 80);
|
|
722
|
+
for (const match of keywords.matchAll(/\b(REF|REFFLD|PFILE|JFILE)\s*\(([^)]*)\)/gi)) {
|
|
723
|
+
const keyword = match[1].toUpperCase();
|
|
724
|
+
const operands = match[2].trim().split(/\s+/).filter(Boolean);
|
|
725
|
+
if (keyword === "REF") {
|
|
726
|
+
if (operands[0])
|
|
727
|
+
out.dependency(operands[0], "file", lineNo, "ref");
|
|
728
|
+
}
|
|
729
|
+
else if (keyword === "REFFLD") {
|
|
730
|
+
const file = operands[1];
|
|
731
|
+
if (file && !/^\*SRC$/i.test(file))
|
|
732
|
+
out.dependency(file, "file", lineNo, "reffld");
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
for (const file of operands)
|
|
736
|
+
out.dependency(file, "file", lineNo, keyword.toLowerCase());
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
// ---------------------------------------------------------------------------
|
|
742
|
+
// COBOL, C and unknown source
|
|
743
|
+
function inspectCobol(lines, out) {
|
|
744
|
+
let sqlBuffer;
|
|
745
|
+
let sqlLine = 0;
|
|
746
|
+
lines.forEach((raw, index) => {
|
|
747
|
+
const lineNo = index + 1;
|
|
748
|
+
if (raw[6] === "*" || raw[6] === "/")
|
|
749
|
+
return;
|
|
750
|
+
const code = raw.slice(7, 72);
|
|
751
|
+
if (typeof sqlBuffer === "string") {
|
|
752
|
+
const end = /\bEND-EXEC\b/i.exec(code);
|
|
753
|
+
sqlBuffer += " " + (end ? code.slice(0, end.index) : code);
|
|
754
|
+
if (end) {
|
|
755
|
+
sqlStatementDependencies(sqlBuffer, sqlLine, out, "exec sql");
|
|
756
|
+
sqlBuffer = undefined;
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const execSql = /\bEXEC\s+SQL\b/i.exec(code);
|
|
761
|
+
if (execSql) {
|
|
762
|
+
const rest = code.slice(execSql.index + execSql[0].length);
|
|
763
|
+
const end = /\bEND-EXEC\b/i.exec(rest);
|
|
764
|
+
if (end)
|
|
765
|
+
sqlStatementDependencies(rest.slice(0, end.index), lineNo, out, "exec sql");
|
|
766
|
+
else {
|
|
767
|
+
sqlBuffer = rest;
|
|
768
|
+
sqlLine = lineNo;
|
|
769
|
+
}
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
let match;
|
|
773
|
+
if ((match = /\bPROGRAM-ID\.?\s+([A-Za-z0-9_#$@-]+)/i.exec(code)))
|
|
774
|
+
out.symbol(match[1].toUpperCase(), "program", lineNo);
|
|
775
|
+
else if ((match = /^\s{0,3}([A-Za-z0-9][A-Za-z0-9-]*)\s+SECTION\.\s*$/i.exec(code)))
|
|
776
|
+
out.symbol(match[1].toUpperCase(), "section", lineNo);
|
|
777
|
+
else if ((match = /^\s{0,3}([A-Za-z0-9][A-Za-z0-9-]*)\.\s*$/.exec(code)))
|
|
778
|
+
out.symbol(match[1].toUpperCase(), "paragraph", lineNo);
|
|
779
|
+
if ((match = /^\s*COPY\s+("[^"]+"|'[^']+'|[A-Za-z0-9_#$@-]+)/i.exec(code)))
|
|
780
|
+
out.dependency(match[1].replace(/["]/g, ""), "copy", lineNo, "copy");
|
|
781
|
+
for (const call of code.matchAll(/\bCALL\s+(?:PROCEDURE\s+|LINKAGE\s+\S+\s+)?('[^']+'|"[^"]+"|[A-Za-z0-9_#$@-]+)/gi)) {
|
|
782
|
+
const target = call[1];
|
|
783
|
+
const literal = /^['"]/.test(target);
|
|
784
|
+
out.dependency(target.replace(/["]/g, "'"), "program", lineNo, "call", !literal);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
function inspectC(lines, out) {
|
|
789
|
+
lines.forEach((raw, index) => {
|
|
790
|
+
const lineNo = index + 1;
|
|
791
|
+
const include = /^\s*#\s*include\s*[<"]([^>"]+)[>"]/.exec(raw);
|
|
792
|
+
if (include)
|
|
793
|
+
out.dependency(include[1], "include", lineNo, "#include");
|
|
794
|
+
const execSql = /\bEXEC\s+SQL\b([^;]*)/i.exec(raw);
|
|
795
|
+
if (execSql)
|
|
796
|
+
sqlStatementDependencies(execSql[1], lineNo, out, "exec sql");
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
function inspectUnknown(lines, out) {
|
|
800
|
+
lines.forEach((raw, index) => {
|
|
801
|
+
const lineNo = index + 1;
|
|
802
|
+
const directive = /^\s*\/(COPY|INCLUDE)\s+([^\s]+)/i.exec(raw.length > 6 && raw[6] === "/" ? raw.slice(6) : raw);
|
|
803
|
+
if (directive)
|
|
804
|
+
out.dependency(directive[2], "copy", lineNo, `/${directive[1].toUpperCase()}`);
|
|
805
|
+
const include = /^\s*#\s*include\s*[<"]([^>"]+)[>"]/.exec(raw);
|
|
806
|
+
if (include)
|
|
807
|
+
out.dependency(include[1], "include", lineNo, "#include");
|
|
808
|
+
const procedure = new RegExp(`\\bdcl-proc\\s+(${NAME})`, "i").exec(raw);
|
|
809
|
+
if (procedure)
|
|
810
|
+
out.symbol(procedure[1], "procedure", lineNo);
|
|
811
|
+
const subroutine = new RegExp(`\\bbegsr\\s+(${NAME})`, "i").exec(raw);
|
|
812
|
+
if (subroutine)
|
|
813
|
+
out.symbol(subroutine[1], "subroutine", lineNo);
|
|
814
|
+
});
|
|
815
|
+
}
|