mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
parent
0fc0211d26
commit
4f7697c43b
13 changed files with 691 additions and 82 deletions
|
|
@ -6,9 +6,9 @@
|
|||
"_rebaselined": "#1956 synth-widening: + go-qualified-base fixture; synthesizeGoInheritanceReferences now emits embeds for qualified_type (pkg.Base), generic_type (Box[T]), pointer, AND interface_type embeds (matching the #1940 legacy leg), reduced to bare names at parity. go-ambiguous gains an embed inherits capture. Linear (~1.01). (Earlier #1956: heritage-bearing scale source so the synth is gated at scale.)"
|
||||
},
|
||||
"cobol": {
|
||||
"fingerprint": "575016f329c0be29eb90db974f750d02a21b4a12515f7029bda312df713b27b0",
|
||||
"fingerprint": "68ee0e95eb9f86f2d92ca35f730f4c2d4d83abc1b5241ae767ff3437780ec8d1",
|
||||
"scaling_budget": 1.5,
|
||||
"_note": "COBOL has no inheritance construct, so its scale source stays flat; unchanged."
|
||||
"_note": "Updated for F17-F23 fixes (P2: TIMES guard, ADD GIVING, SQL AS alias). See PR #1959."
|
||||
},
|
||||
"c": {
|
||||
"fingerprint": "0de009bdbfe095f530fa87eb32bce6ab83092c904f26b3c8fe8d8ab587cf6dc9",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import {
|
|||
import { expandCopies } from './cobol/cobol-copy-expander.js';
|
||||
import { processJclFiles } from './cobol/jcl-processor.js';
|
||||
|
||||
import { logger } from '../logger.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -60,6 +62,7 @@ export interface CobolProcessResult {
|
|||
sets: number;
|
||||
inspects: number;
|
||||
initializes: number;
|
||||
arithmeticOps: number;
|
||||
}
|
||||
|
||||
/** Returns true if the file is a COBOL or copybook file. */
|
||||
|
|
@ -114,6 +117,7 @@ export const processCobol = (
|
|||
sets: 0,
|
||||
inspects: 0,
|
||||
initializes: 0,
|
||||
arithmeticOps: 0,
|
||||
};
|
||||
|
||||
// ── 1. Separate programs, copybooks, and JCL ───────────────────────
|
||||
|
|
@ -174,7 +178,16 @@ export const processCobol = (
|
|||
const moduleNodeIds = new Map<string, string>(); // uppercase program name -> node id
|
||||
|
||||
// ── 3. Process each COBOL program ──────────────────────────────────
|
||||
const raw = parseInt(process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES ?? '', 10);
|
||||
const MAX_COBOL_FILE_SIZE = Number.isFinite(raw) && raw > 0 ? raw : 5 * 1024 * 1024;
|
||||
for (const file of programs) {
|
||||
// File-size guard: skip excessively large files to prevent OOM
|
||||
if (file.content.length > MAX_COBOL_FILE_SIZE) {
|
||||
logger.warn(
|
||||
`[cobol-processor] Skipping oversized file (${(file.content.length / 1024 / 1024).toFixed(1)}MB > ${(MAX_COBOL_FILE_SIZE / 1024 / 1024).toFixed(0)}MB): ${file.path}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const fileNodeId = generateId('File', file.path);
|
||||
// Skip if file node doesn't exist (structure-processor creates it)
|
||||
if (!graph.getNode(fileNodeId)) continue;
|
||||
|
|
@ -214,6 +227,7 @@ export const processCobol = (
|
|||
result.sets += extracted.sets.length;
|
||||
result.inspects += extracted.inspects.length;
|
||||
result.initializes += extracted.initializes.length;
|
||||
result.arithmeticOps += extracted.arithmeticOps.length;
|
||||
}
|
||||
|
||||
// ── 4. Second pass: resolve cross-program CALL targets ─────────────
|
||||
|
|
@ -1226,7 +1240,9 @@ function mapToGraph(
|
|||
|
||||
// ── MOVE data flow -> ACCESSES edges (read/write) ──────────────
|
||||
for (const move of extracted.moves) {
|
||||
const fromPropId = dataItemMap.get(move.from.toUpperCase());
|
||||
// Strip any subscript from the source name for data item lookup
|
||||
const fromBase = stripMoveSubscript(move.from);
|
||||
const fromPropId = dataItemMap.get(fromBase.toUpperCase());
|
||||
const callerId = scopedCallerLookup(move.caller, move.line);
|
||||
|
||||
// One read edge per MOVE (regardless of number of targets)
|
||||
|
|
@ -1243,7 +1259,8 @@ function mapToGraph(
|
|||
|
||||
// One write edge per target
|
||||
for (const target of move.targets) {
|
||||
const toPropId = dataItemMap.get(target.toUpperCase());
|
||||
const toBase = stripMoveSubscript(target);
|
||||
const toPropId = dataItemMap.get(toBase.toUpperCase());
|
||||
if (toPropId) {
|
||||
graph.addRelationship({
|
||||
id: generateId('ACCESSES', `${callerId}->write->${target}:L${move.line}`),
|
||||
|
|
@ -1257,6 +1274,39 @@ function mapToGraph(
|
|||
}
|
||||
}
|
||||
|
||||
// ── Arithmetic operations -> ACCESSES edges ──────────────────
|
||||
for (const arith of extracted.arithmeticOps) {
|
||||
const callerId = scopedCallerLookup(arith.caller, arith.line);
|
||||
// Write edge to target variable
|
||||
const targetBase = stripMoveSubscript(arith.target);
|
||||
const targetPropId = dataItemMap.get(targetBase.toUpperCase());
|
||||
if (targetPropId) {
|
||||
graph.addRelationship({
|
||||
id: generateId('ACCESSES', `${callerId}->arith-write->${arith.target}:L${arith.line}`),
|
||||
type: 'ACCESSES',
|
||||
sourceId: callerId,
|
||||
targetId: targetPropId,
|
||||
confidence: 0.9,
|
||||
reason: 'cobol-arithmetic-write',
|
||||
});
|
||||
}
|
||||
// Read edge for each source operand
|
||||
for (const src of arith.sources) {
|
||||
const srcBase = stripMoveSubscript(src);
|
||||
const srcPropId = dataItemMap.get(srcBase.toUpperCase());
|
||||
if (srcPropId) {
|
||||
graph.addRelationship({
|
||||
id: generateId('ACCESSES', `${callerId}->arith-read->${src}:L${arith.line}`),
|
||||
type: 'ACCESSES',
|
||||
sourceId: callerId,
|
||||
targetId: srcPropId,
|
||||
confidence: 0.9,
|
||||
reason: 'cobol-arithmetic-read',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── File declarations -> Record nodes ──────────────────────────
|
||||
for (const fd of extracted.fileDeclarations) {
|
||||
const fdId = generateId('Record', `${filePath}:${fd.selectName}`);
|
||||
|
|
@ -1398,6 +1448,11 @@ function mapToGraph(
|
|||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Strip parenthesized subscript/reference-modification suffixes */
|
||||
function stripMoveSubscript(name: string): string {
|
||||
return name.replace(/\([^)]*\)/g, '').trim();
|
||||
}
|
||||
|
||||
/** Find the enclosing program name for a given line number (innermost wins). */
|
||||
function findOwningProgramName(
|
||||
lineNum: number,
|
||||
|
|
|
|||
|
|
@ -183,6 +183,19 @@ export interface CobolRegexResults {
|
|||
|
||||
// Phase 4.1: INITIALIZE
|
||||
initializes: Array<{ target: string; line: number; caller: string | null }>;
|
||||
|
||||
// Phase 4.2: Arithmetic operations (COMPUTE, ADD, SUBTRACT, MULTIPLY, DIVIDE)
|
||||
arithmeticOps: Array<{
|
||||
verb: 'COMPUTE' | 'ADD' | 'SUBTRACT' | 'MULTIPLY' | 'DIVIDE';
|
||||
/** Target variable (written to) */
|
||||
target: string;
|
||||
/** Source operand variables (read from) */
|
||||
sources: string[];
|
||||
line: number;
|
||||
caller: string | null;
|
||||
/** For ADD/SUBTRACT/MULTIPLY/DIVIDE with GIVING: the GIVING target */
|
||||
givingTarget?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -306,36 +319,37 @@ const RE_SECTION =
|
|||
/\b(WORKING-STORAGE|LINKAGE|FILE|LOCAL-STORAGE|SCREEN|INPUT-OUTPUT|CONFIGURATION)\s+SECTION\b/i;
|
||||
|
||||
// IDENTIFICATION DIVISION
|
||||
const RE_PROGRAM_ID = /\bPROGRAM-ID\.\s*([A-Z][A-Z0-9-]*)(?:\s+IS\s+COMMON)?/i;
|
||||
const RE_END_PROGRAM = /\bEND\s+PROGRAM\s+([A-Z][A-Z0-9-]*)\s*\./i;
|
||||
const RE_PROGRAM_ID = /\bPROGRAM-ID\.\s*([A-Z0-9][A-Z0-9-]*)(?:\s+IS\s+COMMON)?/i;
|
||||
const RE_END_PROGRAM = /\bEND\s+PROGRAM\s+([A-Z0-9][A-Z0-9-]*)\s*\./i;
|
||||
const RE_AUTHOR = /^\s+AUTHOR\.\s*(.+)/i;
|
||||
const RE_DATE_WRITTEN = /^\s+DATE-WRITTEN\.\s*(.+)/i;
|
||||
const RE_DATE_COMPILED = /^\s+DATE-COMPILED\.\s*(.+)/i;
|
||||
const RE_INSTALLATION = /^\s+INSTALLATION\.\s*(.+)/i;
|
||||
|
||||
// ENVIRONMENT DIVISION — SELECT
|
||||
const RE_SELECT_START = /\bSELECT\s+(?:OPTIONAL\s+)?([A-Z][A-Z0-9-]+)/i;
|
||||
const RE_SELECT_START = /\bSELECT\s+(?:OPTIONAL\s+)?([A-Z0-9][A-Z0-9-]+)/i;
|
||||
|
||||
// DATA DIVISION
|
||||
// ^\s* (not ^\s+) to support both fixed-format (indented) and free-format (trimmed)
|
||||
const RE_FD = /^\s*(?:FD|SD|RD)\s+([A-Z][A-Z0-9-]+)/i;
|
||||
const RE_DATA_ITEM = /^\s*(\d{1,2})\s+([A-Z][A-Z0-9-]+)\s*(.*)/i;
|
||||
const RE_ANONYMOUS_REDEFINES = /^\s*(\d{1,2})\s+REDEFINES\s+([A-Z][A-Z0-9-]+)/i;
|
||||
const RE_88_LEVEL = /^\s*88\s+([A-Z][A-Z0-9-]+)\s+VALUES?\s+(?:ARE\s+)?(.+)/i;
|
||||
const RE_FD = /^\s*(?:FD|SD|RD)\s+([A-Z0-9][A-Z0-9-]+)/i;
|
||||
const RE_DATA_ITEM = /^\s*(\d{1,2})\s+([A-Z0-9][A-Z0-9-]+)\s*(.*)/i;
|
||||
const RE_ANONYMOUS_REDEFINES = /^\s*(\d{1,2})\s+REDEFINES\s+([A-Z0-9][A-Z0-9-]+)/i;
|
||||
const RE_88_LEVEL = /^\s*88\s+([A-Z0-9][A-Z0-9-]+)\s+VALUES?\s+(?:ARE\s+)?(.+)/i;
|
||||
|
||||
// PROCEDURE DIVISION
|
||||
// These patterns support both fixed-format (7 leading spaces) and free-format (any indentation)
|
||||
const RE_PROC_SECTION = /^\s*([A-Z][A-Z0-9-]+)\s+SECTION(?:\s+\d+)?\.\s*$/i;
|
||||
const RE_PROC_PARAGRAPH = /^\s*([A-Z][A-Z0-9-]+)\.\s*$/i;
|
||||
const RE_PERFORM = /\bPERFORM\s+([A-Z][A-Z0-9-]+)(?:\s+(?:THRU|THROUGH)\s+([A-Z][A-Z0-9-]+))?/gi;
|
||||
const RE_PROC_SECTION = /^\s*([A-Z0-9][A-Z0-9-]+)\s+SECTION(?:\s+\d+)?\.\s*$/i;
|
||||
const RE_PROC_PARAGRAPH = /^\s*([A-Z0-9][A-Z0-9-]+)\.\s*$/i;
|
||||
const RE_PERFORM =
|
||||
/\bPERFORM\s+([A-Z0-9][A-Z0-9-]+)(?:\s+(?:THRU|THROUGH)\s+([A-Z0-9][A-Z0-9-]+))?/gi;
|
||||
|
||||
// ALL DIVISIONS
|
||||
// Both double-quoted ("PROG") and single-quoted ('PROG') targets are valid COBOL.
|
||||
// Use separate alternation groups so quotes must match (prevents "PROG' false-matches).
|
||||
const RE_CALL = /\bCALL\s+(?:"([^"]+)"|'([^']+)')/gi;
|
||||
// Dynamic CALL via data item (no quotes): CALL WS-PROGRAM-NAME
|
||||
const RE_CALL_DYNAMIC = /(?<![A-Z0-9-])\bCALL\s+([A-Z][A-Z0-9-]+)(?=\s|\.|$)/gi;
|
||||
const RE_COPY_UNQUOTED = /\bCOPY\s+([A-Z][A-Z0-9-]+)(?:\s|\.)/i;
|
||||
const RE_CALL_DYNAMIC = /(?<![A-Z0-9-])\bCALL\s+([A-Z0-9][A-Z0-9-]+)(?=\s|\.|$)/gi;
|
||||
const RE_COPY_UNQUOTED = /\bCOPY\s+([A-Z0-9][A-Z0-9-]+)(?:\s|\.)/i;
|
||||
const RE_COPY_QUOTED = /\bCOPY\s+(?:"([^"]+)"|'([^']+)')(?:\s|\.)/i;
|
||||
|
||||
// EXEC blocks
|
||||
|
|
@ -346,32 +360,32 @@ const RE_END_EXEC = /\bEND-EXEC\b/i;
|
|||
// GO TO — control flow transfer (same graph semantics as PERFORM)
|
||||
// GO TO — captures first target; GO TO p1 p2 p3 DEPENDING ON x handled below
|
||||
const RE_GOTO =
|
||||
/\bGO\s+TO\s+([A-Z][A-Z0-9-]+(?:\s+[A-Z][A-Z0-9-]+)*?)(?:\s+DEPENDING\s+ON\s+[A-Z][A-Z0-9-]+)?(?:\s*\.|$)/i;
|
||||
/\bGO\s+TO\s+([A-Z0-9][A-Z0-9-]+(?:\s+[A-Z0-9][A-Z0-9-]+)*?)(?:\s+DEPENDING\s+ON\s+[A-Z0-9][A-Z0-9-]+)?(?:\s*\.|$)/i;
|
||||
|
||||
// SORT/MERGE file references
|
||||
const RE_SORT = /\bSORT\s+([A-Z][A-Z0-9-]+)/i;
|
||||
const RE_MERGE = /\bMERGE\s+([A-Z][A-Z0-9-]+)/i;
|
||||
const RE_SORT = /\bSORT\s+([A-Z0-9][A-Z0-9-]+)/i;
|
||||
const RE_MERGE = /\bMERGE\s+([A-Z0-9][A-Z0-9-]+)/i;
|
||||
|
||||
// SEARCH — table access
|
||||
const RE_SEARCH = /\bSEARCH\s+(?:ALL\s+)?([A-Z][A-Z0-9-]+)/i;
|
||||
const RE_SEARCH = /\bSEARCH\s+(?:ALL\s+)?([A-Z0-9][A-Z0-9-]+)/i;
|
||||
|
||||
// CANCEL — program lifecycle
|
||||
const RE_CANCEL = /\bCANCEL\s+(?:"([^"]+)"|'([^']+)')/gi;
|
||||
const RE_CANCEL_DYNAMIC = /(?<![A-Z0-9-])\bCANCEL\s+([A-Z][A-Z0-9-]+)(?=\s|\.|$)/gi;
|
||||
const RE_CANCEL_DYNAMIC = /(?<![A-Z0-9-])\bCANCEL\s+([A-Z0-9][A-Z0-9-]+)(?=\s|\.|$)/gi;
|
||||
|
||||
// Level 66 RENAMES
|
||||
const RE_66_LEVEL = /^\s*66\s+([A-Z][A-Z0-9-]+)\s+RENAMES\s+([A-Z][A-Z0-9-]+)/i;
|
||||
const RE_66_LEVEL = /^\s*66\s+([A-Z0-9][A-Z0-9-]+)\s+RENAMES\s+([A-Z0-9][A-Z0-9-]+)/i;
|
||||
|
||||
// DECLARATIVES boundary and USE AFTER EXCEPTION
|
||||
const RE_DECLARATIVES_START = /^\s*DECLARATIVES\s*\.\s*$/i;
|
||||
const RE_DECLARATIVES_END = /^\s*END\s+DECLARATIVES\s*\.\s*$/i;
|
||||
const RE_USE_AFTER =
|
||||
/\bUSE\s+(?:AFTER\s+)?(?:STANDARD\s+)?(?:EXCEPTION|ERROR)\s+ON\s+([A-Z][A-Z0-9-]+|INPUT|OUTPUT|I-O|EXTEND)\b/i;
|
||||
/\bUSE\s+(?:AFTER\s+)?(?:STANDARD\s+)?(?:EXCEPTION|ERROR)\s+ON\s+([A-Z0-9][A-Z0-9-]+|INPUT|OUTPUT|I-O|EXTEND)\b/i;
|
||||
|
||||
// SET statement (condition, index)
|
||||
//
|
||||
// Catastrophic-backtracking note (CodeQL js/redos): the previous shape
|
||||
// `((?:[A-Z][A-Z0-9-]+(?:\s+OF\s+[A-Z][A-Z0-9-]+)?\s+)+)TO\s+TRUE`
|
||||
// `((?:[A-Z0-9][A-Z0-9-]+(?:\s+OF\s+[A-Z0-9][A-Z0-9-]+)?\s+)+)TO\s+TRUE`
|
||||
// nested `\s+` quantifiers across alternations and was exponential on
|
||||
// inputs like "SET a OF a OF a ... TO TRUE". Replaced with a lazy
|
||||
// dot-match bounded by the explicit `\s+TO\s+TRUE` suffix — `.+?` is
|
||||
|
|
@ -382,7 +396,7 @@ const RE_USE_AFTER =
|
|||
// pathological-input timing assertion exercises the production regex
|
||||
// instead of an inline copy that drifts.
|
||||
export const RE_SET_TO_TRUE = /\bSET\s+(.+?)\s+TO\s+TRUE\b/i;
|
||||
export const RE_SET_INDEX = /\bSET\s+(.+?)\s+(TO|UP\s+BY|DOWN\s+BY)\s+(\d+|[A-Z][A-Z0-9-]+)/i;
|
||||
export const RE_SET_INDEX = /\bSET\s+(.+?)\s+(TO|UP\s+BY|DOWN\s+BY)\s+(\d+|[A-Z0-9][A-Z0-9-]+)/i;
|
||||
|
||||
// INITIALIZE statement — data reset (captures targets before REPLACING/WITH clause)
|
||||
const RE_INITIALIZE = /\bINITIALIZE\s+([\s\S]*?)(?=\bREPLACING\b|\bWITH\b|\.\s*$|$)/i;
|
||||
|
|
@ -409,7 +423,8 @@ const RE_PROC_USING = /\bPROCEDURE\s+DIVISION\s+USING\s+([\s\S]*?)(?:\.|$)/i;
|
|||
const RE_ENTRY = /\bENTRY\s+(?:"([^"]+)"|'([^']+)')(?:\s+USING\s+([\s\S]*?))?(?:\.|$)/i;
|
||||
|
||||
// MOVE statement — captures everything after TO for multi-target extraction
|
||||
const RE_MOVE = /\bMOVE\s+((?:CORRESPONDING|CORR)\s+)?([A-Z][A-Z0-9-]+)\s+TO\s+(.+)/i;
|
||||
const RE_MOVE =
|
||||
/\bMOVE\s+((?:CORRESPONDING|CORR)\s+)?([A-Z0-9][A-Z0-9-]+(?:\([^)]*\))*)\s+TO\s+(.+)/i;
|
||||
const MOVE_SKIP = new Set([
|
||||
'SPACES',
|
||||
'ZEROS',
|
||||
|
|
@ -449,7 +464,7 @@ function extractMoveTargets(afterTo: string): string[] {
|
|||
skipNext = true;
|
||||
continue;
|
||||
}
|
||||
if (/^[A-Z][A-Z0-9-]+$/i.test(token) && !MOVE_SKIP.has(token.toUpperCase())) {
|
||||
if (/^[A-Z0-9][A-Z0-9-]+$/i.test(token) && !MOVE_SKIP.has(token.toUpperCase())) {
|
||||
targets.push(token);
|
||||
}
|
||||
}
|
||||
|
|
@ -605,14 +620,14 @@ function parseDataItemClauses(rest: string): {
|
|||
}
|
||||
|
||||
// REDEFINES <name>
|
||||
const redefMatch = text.match(/\bREDEFINES\s+([A-Z][A-Z0-9-]+)/i);
|
||||
const redefMatch = text.match(/\bREDEFINES\s+([A-Z0-9][A-Z0-9-]+)/i);
|
||||
if (redefMatch) {
|
||||
result.redefines = redefMatch[1];
|
||||
}
|
||||
|
||||
// OCCURS <n> [TO <m>] [TIMES] [DEPENDING ON <field>]
|
||||
const occursMatch = text.match(
|
||||
/\bOCCURS\s+(\d+)(?:\s+TO\s+(\d+))?\s*(?:TIMES\s*)?(?:DEPENDING\s+ON\s+([A-Z][A-Z0-9-]+(?:\s*\([^)]*\))?))?/i,
|
||||
/\bOCCURS\s+(\d+)(?:\s+TO\s+(\d+))?\s*(?:TIMES\s*)?(?:DEPENDING\s+ON\s+([A-Z0-9][A-Z0-9-]+(?:\s*\([^)]*\))?))?/i,
|
||||
);
|
||||
if (occursMatch) {
|
||||
result.occurs = parseInt(occursMatch[1], 10);
|
||||
|
|
@ -653,7 +668,7 @@ function parseDataItemClauses(rest: string): {
|
|||
result.value = numMatch[1];
|
||||
} else {
|
||||
// Try figurative constant or identifier
|
||||
const identMatch = afterValue.match(/^([A-Z][A-Z0-9-]*)/i);
|
||||
const identMatch = afterValue.match(/^([A-Z0-9][A-Z0-9-]*)/i);
|
||||
if (identMatch) result.value = identMatch[1].toUpperCase();
|
||||
}
|
||||
}
|
||||
|
|
@ -721,7 +736,7 @@ function parseSelectStatement(stmt: string, startLine: number): FileDeclaration
|
|||
// Normalize whitespace
|
||||
const text = stmt.replace(/\s+/g, ' ').trim();
|
||||
|
||||
const nameMatch = text.match(/^SELECT\s+(?:OPTIONAL\s+)?([A-Z][A-Z0-9-]+)/i);
|
||||
const nameMatch = text.match(/^SELECT\s+(?:OPTIONAL\s+)?([A-Z0-9][A-Z0-9-]+)/i);
|
||||
if (!nameMatch) return null;
|
||||
|
||||
const result: FileDeclaration = {
|
||||
|
|
@ -730,7 +745,7 @@ function parseSelectStatement(stmt: string, startLine: number): FileDeclaration
|
|||
line: startLine,
|
||||
};
|
||||
|
||||
const assignMatch = text.match(/\bASSIGN\s+(?:TO\s+)?("([^"]+)"|([A-Z][A-Z0-9-]*))/i);
|
||||
const assignMatch = text.match(/\bASSIGN\s+(?:TO\s+)?("([^"]+)"|([A-Z0-9][A-Z0-9-]*))/i);
|
||||
if (assignMatch) {
|
||||
result.assignTo = assignMatch[2] || assignMatch[3] || '';
|
||||
}
|
||||
|
|
@ -747,19 +762,21 @@ function parseSelectStatement(stmt: string, startLine: number): FileDeclaration
|
|||
result.access = accessMatch[1].toUpperCase();
|
||||
}
|
||||
|
||||
const keyMatch = text.match(/\bRECORD\s+KEY\s+(?:IS\s+)?([A-Z][A-Z0-9-]+)/i);
|
||||
const keyMatch = text.match(/\bRECORD\s+KEY\s+(?:IS\s+)?([A-Z0-9][A-Z0-9-]+)/i);
|
||||
if (keyMatch) {
|
||||
result.recordKey = keyMatch[1];
|
||||
}
|
||||
|
||||
// ALTERNATE RECORD KEY
|
||||
const altKeyMatches = text.matchAll(/\bALTERNATE\s+RECORD\s+KEY\s+(?:IS\s+)?([A-Z][A-Z0-9-]+)/gi);
|
||||
const altKeyMatches = text.matchAll(
|
||||
/\bALTERNATE\s+RECORD\s+KEY\s+(?:IS\s+)?([A-Z0-9][A-Z0-9-]+)/gi,
|
||||
);
|
||||
const alternateKeys: string[] = [];
|
||||
for (const m of altKeyMatches) alternateKeys.push(m[1]);
|
||||
if (alternateKeys.length > 0) result.alternateKeys = alternateKeys;
|
||||
|
||||
// FILE STATUS IS / STATUS IS
|
||||
const statusMatch = text.match(/\b(?:FILE\s+)?STATUS\s+(?:IS\s+)?([A-Z][A-Z0-9-]+)/i);
|
||||
const statusMatch = text.match(/\b(?:FILE\s+)?STATUS\s+(?:IS\s+)?([A-Z0-9][A-Z0-9-]+)/i);
|
||||
if (statusMatch) {
|
||||
result.fileStatus = statusMatch[1];
|
||||
}
|
||||
|
|
@ -789,11 +806,12 @@ function parseExecSqlBlock(
|
|||
block: string,
|
||||
line: number,
|
||||
): CobolRegexResults['execSqlBlocks'][number] {
|
||||
// Strip EXEC SQL ... END-EXEC wrapper
|
||||
// Strip EXEC SQL ... END-EXEC wrapper and trailing period
|
||||
const body = block
|
||||
.replace(/\bEXEC\s+SQL\b/i, '')
|
||||
.replace(/\bEND-EXEC\b/i, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\.\s*$/, '')
|
||||
.trim();
|
||||
|
||||
// Determine operation from first SQL keyword
|
||||
|
|
@ -823,18 +841,24 @@ function parseExecSqlBlock(
|
|||
// Extract table names from FROM, INTO (INSERT), UPDATE, DELETE FROM, JOIN
|
||||
const tables: string[] = [];
|
||||
const tablePatterns = [
|
||||
/\bFROM\s+([A-Z][A-Z0-9_]+)/gi,
|
||||
/\bINSERT\s+INTO\s+([A-Z][A-Z0-9_]+)/gi,
|
||||
/\bUPDATE\s+([A-Z][A-Z0-9_]+)/gi,
|
||||
/\bJOIN\s+([A-Z][A-Z0-9_]+)/gi,
|
||||
// FROM table1 [AS alias], table2 [AS alias] … — handle comma-separated
|
||||
// lists with optional AS keyword, terminated by SQL clause keywords
|
||||
// (WHERE, JOIN, GROUP, ON, ORDER, HAVING, UNION, SET, INTO, VALUES, FETCH, FOR, LIMIT, OFFSET, WITH).
|
||||
/\bFROM\s+([A-Z0-9][A-Z0-9_]+(?:\s+(?:AS\s+)?[A-Z0-9][A-Z0-9_]*)?(?:\s*,\s*[A-Z0-9][A-Z0-9_]+(?:\s+(?:AS\s+)?[A-Z0-9][A-Z0-9_]*)?)*)(?:\s+(?:WHERE|JOIN|GROUP|ON|ORDER|HAVING|UNION|SET|INTO|VALUES|FETCH|FOR|LIMIT|OFFSET|WITH)\b|$)/gi,
|
||||
/\bINSERT\s+INTO\s+([A-Z0-9][A-Z0-9_]+)/gi,
|
||||
/\bUPDATE\s+([A-Z0-9][A-Z0-9_]+)/gi,
|
||||
/\bJOIN\s+([A-Z0-9][A-Z0-9_]+)/gi,
|
||||
];
|
||||
for (const re of tablePatterns) {
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(body)) !== null) {
|
||||
const name = m[1].toUpperCase();
|
||||
// Skip host variables and SQL keywords
|
||||
if (!name.startsWith(':') && !tables.includes(name)) {
|
||||
tables.push(name);
|
||||
// Split comma-separated table list and strip aliases
|
||||
const names = m[1].split(',').map((n) => n.trim().split(/\s+/)[0].toUpperCase());
|
||||
for (const name of names) {
|
||||
// Skip host variables and SQL keywords
|
||||
if (!name.startsWith(':') && !tables.includes(name)) {
|
||||
tables.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -849,7 +873,7 @@ function parseExecSqlBlock(
|
|||
|
||||
// Extract host variables: :VARIABLE-NAME (strip the colon)
|
||||
const hostVariables: string[] = [];
|
||||
const hostRe = /:([A-Z][A-Z0-9-]+)/gi;
|
||||
const hostRe = /:([A-Z0-9][A-Z0-9-]+)/gi;
|
||||
let hm: RegExpExecArray | null;
|
||||
while ((hm = hostRe.exec(body)) !== null) {
|
||||
const name = hm[1];
|
||||
|
|
@ -910,24 +934,24 @@ function parseExecCicsBlock(
|
|||
const result: CobolRegexResults['execCicsBlocks'][number] = { line, command };
|
||||
|
||||
// MAP name: MAP('name') or MAP("name") or MAP(IDENTIFIER)
|
||||
const mapMatch = body.match(/\bMAP\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z][A-Z0-9-]+))\s*\)/i);
|
||||
const mapMatch = body.match(/\bMAP\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z0-9][A-Z0-9-]+))\s*\)/i);
|
||||
if (mapMatch) result.mapName = mapMatch[1] ?? mapMatch[2];
|
||||
|
||||
// PROGRAM name: PROGRAM('name') or PROGRAM("name") or PROGRAM(VARIABLE)
|
||||
const progMatch = body.match(/\bPROGRAM\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z][A-Z0-9-]+))\s*\)/i);
|
||||
const progMatch = body.match(/\bPROGRAM\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z0-9][A-Z0-9-]+))\s*\)/i);
|
||||
if (progMatch) {
|
||||
result.programName = progMatch[1] ?? progMatch[2];
|
||||
result.programIsLiteral = !!progMatch[1];
|
||||
}
|
||||
|
||||
// TRANSID: TRANSID('name') or TRANSID("name") or TRANSID(VARIABLE)
|
||||
const transMatch = body.match(/\bTRANSID\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z][A-Z0-9-]+))\s*\)/i);
|
||||
const transMatch = body.match(/\bTRANSID\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z0-9][A-Z0-9-]+))\s*\)/i);
|
||||
if (transMatch) result.transId = transMatch[1] ?? transMatch[2];
|
||||
|
||||
// FILE/DATASET: FILE('name') or DATASET('name') or FILE(VARIABLE)
|
||||
// Used in CICS READ, WRITE, REWRITE, DELETE, STARTBR, READNEXT, READPREV, ENDBR
|
||||
const fileMatch = body.match(
|
||||
/\b(?:FILE|DATASET)\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z][A-Z0-9-]+))\s*\)/i,
|
||||
/\b(?:FILE|DATASET)\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z0-9][A-Z0-9-]+))\s*\)/i,
|
||||
);
|
||||
if (fileMatch) {
|
||||
result.fileName = fileMatch[1] ?? fileMatch[2];
|
||||
|
|
@ -935,19 +959,19 @@ function parseExecCicsBlock(
|
|||
}
|
||||
|
||||
// QUEUE: QUEUE('name') — used in WRITEQ/READQ TS/TD
|
||||
const queueMatch = body.match(/\bQUEUE\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z][A-Z0-9-]+))\s*\)/i);
|
||||
const queueMatch = body.match(/\bQUEUE\s*\(\s*(?:['"]([^'"]+)['"]|([A-Z0-9][A-Z0-9-]+))\s*\)/i);
|
||||
if (queueMatch) result.queueName = queueMatch[1] ?? queueMatch[2];
|
||||
|
||||
// HANDLE ABEND LABEL(paragraph-name) — error handler target
|
||||
const labelMatch = body.match(/\bLABEL\s*\(\s*([A-Z][A-Z0-9-]+)\s*\)/i);
|
||||
const labelMatch = body.match(/\bLABEL\s*\(\s*([A-Z0-9][A-Z0-9-]+)\s*\)/i);
|
||||
if (labelMatch) result.labelName = labelMatch[1];
|
||||
|
||||
// INTO(data-area) — data target (READ INTO, RECEIVE INTO, RETRIEVE INTO, READQ INTO)
|
||||
const intoMatch = body.match(/\bINTO\s*\(\s*([A-Z][A-Z0-9-]+)\s*\)/i);
|
||||
const intoMatch = body.match(/\bINTO\s*\(\s*([A-Z0-9][A-Z0-9-]+)\s*\)/i);
|
||||
if (intoMatch) result.intoField = intoMatch[1];
|
||||
|
||||
// FROM(data-area) — data source (WRITE FROM, SEND FROM, WRITEQ FROM, START FROM)
|
||||
const fromMatch = body.match(/\bFROM\s*\(\s*([A-Z][A-Z0-9-]+)\s*\)/i);
|
||||
const fromMatch = body.match(/\bFROM\s*\(\s*([A-Z0-9][A-Z0-9-]+)\s*\)/i);
|
||||
if (fromMatch) result.fromField = fromMatch[1];
|
||||
|
||||
return result;
|
||||
|
|
@ -972,16 +996,16 @@ function parseExecDliBlock(
|
|||
const pcbMatch = body.match(/\bUSING\s+PCB\s*\(\s*(\d+)\s*\)/i);
|
||||
if (pcbMatch) result.pcbNumber = parseInt(pcbMatch[1], 10);
|
||||
|
||||
const segMatch = body.match(/\bSEGMENT\s*\(\s*([A-Z][A-Z0-9-]*)\s*\)/i);
|
||||
const segMatch = body.match(/\bSEGMENT\s*\(\s*([A-Z0-9][A-Z0-9-]*)\s*\)/i);
|
||||
if (segMatch) result.segmentName = segMatch[1];
|
||||
|
||||
const intoMatch = body.match(/\bINTO\s*\(\s*([A-Z][A-Z0-9-]+)\s*\)/i);
|
||||
const intoMatch = body.match(/\bINTO\s*\(\s*([A-Z0-9][A-Z0-9-]+)\s*\)/i);
|
||||
if (intoMatch) result.intoField = intoMatch[1];
|
||||
|
||||
const fromMatch = body.match(/\bFROM\s*\(\s*([A-Z][A-Z0-9-]+)\s*\)/i);
|
||||
const fromMatch = body.match(/\bFROM\s*\(\s*([A-Z0-9][A-Z0-9-]+)\s*\)/i);
|
||||
if (fromMatch) result.fromField = fromMatch[1];
|
||||
|
||||
const psbMatch = body.match(/\bPSB\s*\(\s*([A-Z][A-Z0-9-]+)\s*\)/i);
|
||||
const psbMatch = body.match(/\bPSB\s*\(\s*([A-Z0-9][A-Z0-9-]+)\s*\)/i);
|
||||
if (psbMatch) result.psbName = psbMatch[1];
|
||||
|
||||
return result;
|
||||
|
|
@ -1028,6 +1052,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
sets: [],
|
||||
inspects: [],
|
||||
initializes: [],
|
||||
arithmeticOps: [],
|
||||
};
|
||||
|
||||
// --- State ---
|
||||
|
|
@ -1451,7 +1476,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
}
|
||||
} else if (
|
||||
currentDivision === 'procedure' &&
|
||||
/(?<![A-Z0-9-])\bCALL\s+(?:"[^"]+"|'[^']+'|[A-Z][A-Z0-9-]+)/i.test(line)
|
||||
/(?<![A-Z0-9-])\bCALL\s+(?:"[^"]+"|'[^']+'|[A-Z0-9][A-Z0-9-]+)/i.test(line)
|
||||
) {
|
||||
// Check if this is a complete single-line CALL (ends with period or END-CALL)
|
||||
if (/\.\s*$/.test(line) || /\bEND-CALL\b/i.test(line)) {
|
||||
|
|
@ -1587,7 +1612,9 @@ export function extractCobolSymbolsWithRegex(
|
|||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((f) => f.replace(/\.$/, ''))
|
||||
.filter((f) => /^[A-Z][A-Z0-9-]+$/i.test(f) && !SORT_CLAUSE_NOISE.has(f.toUpperCase())),
|
||||
.filter(
|
||||
(f) => /^[A-Z0-9][A-Z0-9-]+$/i.test(f) && !SORT_CLAUSE_NOISE.has(f.toUpperCase()),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (givingIdx >= 0) {
|
||||
|
|
@ -1597,16 +1624,18 @@ export function extractCobolSymbolsWithRegex(
|
|||
.trim()
|
||||
.split(/\s+/)
|
||||
.map((f) => f.replace(/\.$/, ''))
|
||||
.filter((f) => /^[A-Z][A-Z0-9-]+$/i.test(f) && !SORT_CLAUSE_NOISE.has(f.toUpperCase())),
|
||||
.filter(
|
||||
(f) => /^[A-Z0-9][A-Z0-9-]+$/i.test(f) && !SORT_CLAUSE_NOISE.has(f.toUpperCase()),
|
||||
),
|
||||
);
|
||||
}
|
||||
// INPUT PROCEDURE IS / OUTPUT PROCEDURE IS → control-flow targets (like PERFORM)
|
||||
// Supports optional THRU/THROUGH range: INPUT PROCEDURE IS proc-start THRU proc-end
|
||||
const inputProcMatch = fullSort.match(
|
||||
/\bINPUT\s+PROCEDURE\s+(?:IS\s+)?([A-Z][A-Z0-9-]+)(?:\s+(?:THRU|THROUGH)\s+([A-Z][A-Z0-9-]+))?/i,
|
||||
/\bINPUT\s+PROCEDURE\s+(?:IS\s+)?([A-Z0-9][A-Z0-9-]+)(?:\s+(?:THRU|THROUGH)\s+([A-Z0-9][A-Z0-9-]+))?/i,
|
||||
);
|
||||
const outputProcMatch = fullSort.match(
|
||||
/\bOUTPUT\s+PROCEDURE\s+(?:IS\s+)?([A-Z][A-Z0-9-]+)(?:\s+(?:THRU|THROUGH)\s+([A-Z][A-Z0-9-]+))?/i,
|
||||
/\bOUTPUT\s+PROCEDURE\s+(?:IS\s+)?([A-Z0-9][A-Z0-9-]+)(?:\s+(?:THRU|THROUGH)\s+([A-Z0-9][A-Z0-9-]+))?/i,
|
||||
);
|
||||
if (inputProcMatch) {
|
||||
result.performs.push({
|
||||
|
|
@ -1632,7 +1661,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
function flushInspect(): void {
|
||||
if (inspectAccum === null) return;
|
||||
const text = inspectAccum;
|
||||
const fieldMatch = text.match(/\bINSPECT\s+([A-Z][A-Z0-9-]+)/i);
|
||||
const fieldMatch = text.match(/\bINSPECT\s+([A-Z0-9][A-Z0-9-]+)/i);
|
||||
if (!fieldMatch) {
|
||||
inspectAccum = null;
|
||||
return;
|
||||
|
|
@ -1643,7 +1672,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
/\bTALLYING\b([\s\S]+?)(?:\bREPLACING\b|\bCONVERTING\b|\.\s*$)/i,
|
||||
);
|
||||
if (tallySection) {
|
||||
const counterRe = /([A-Z][A-Z0-9-]+)\s+FOR\b/gi;
|
||||
const counterRe = /([A-Z0-9][A-Z0-9-]+)\s+FOR\b/gi;
|
||||
let cm: RegExpExecArray | null;
|
||||
while ((cm = counterRe.exec(tallySection[1])) !== null) {
|
||||
counters.push(cm[1]);
|
||||
|
|
@ -1693,10 +1722,10 @@ export function extractCobolSymbolsWithRegex(
|
|||
(s) =>
|
||||
s.length > 0 &&
|
||||
!CALL_USING_FILTER.has(s.toUpperCase()) &&
|
||||
/^[A-Z][A-Z0-9-]+$/i.test(s),
|
||||
/^[A-Z0-9][A-Z0-9-]+$/i.test(s),
|
||||
)
|
||||
: undefined;
|
||||
const retMatch = afterCall.match(/\bRETURNING\s+([A-Z][A-Z0-9-]+)/i);
|
||||
const retMatch = afterCall.match(/\bRETURNING\s+([A-Z0-9][A-Z0-9-]+)/i);
|
||||
const returning = retMatch ? retMatch[1] : undefined;
|
||||
result.calls.push({
|
||||
target: callTarget,
|
||||
|
|
@ -1720,10 +1749,10 @@ export function extractCobolSymbolsWithRegex(
|
|||
(s) =>
|
||||
s.length > 0 &&
|
||||
!CALL_USING_FILTER.has(s.toUpperCase()) &&
|
||||
/^[A-Z][A-Z0-9-]+$/i.test(s),
|
||||
/^[A-Z0-9][A-Z0-9-]+$/i.test(s),
|
||||
)
|
||||
: undefined;
|
||||
const dynRetMatch = afterDynCall.match(/\bRETURNING\s+([A-Z][A-Z0-9-]+)/i);
|
||||
const dynRetMatch = afterDynCall.match(/\bRETURNING\s+([A-Z0-9][A-Z0-9-]+)/i);
|
||||
const dynReturning = dynRetMatch ? dynRetMatch[1] : undefined;
|
||||
result.calls.push({
|
||||
target: dynCallMatch[1],
|
||||
|
|
@ -1933,11 +1962,14 @@ export function extractCobolSymbolsWithRegex(
|
|||
const target = perfMatch[1];
|
||||
// Skip COBOL inline-perform keywords that are not paragraph names
|
||||
if (!PERFORM_KEYWORD_SKIP.has(target.toUpperCase())) {
|
||||
// Also check for "PERFORM identifier TIMES" — the identifier is a
|
||||
// data item count, not a paragraph name (fundamental regex ambiguity).
|
||||
const matchEnd = perfMatch.index! + perfMatch[0].length;
|
||||
const afterTarget = line.substring(matchEnd).trim();
|
||||
if (!/^TIMES\b/i.test(afterTarget)) {
|
||||
// Check for inline PERFORM ... TIMES pattern where the target IS
|
||||
// the counter variable itself (e.g., PERFORM WS-COUNT TIMES).
|
||||
// Out-of-line PERFORM target count TIMES (e.g., PERFORM 2000-PROCESS 3 TIMES)
|
||||
// IS a real paragraph call — do NOT suppress it.
|
||||
const hasTimesClause = /^\s*TIMES\b/i.test(afterTarget);
|
||||
if (!hasTimesClause) {
|
||||
result.performs.push({
|
||||
caller: currentParagraph,
|
||||
target,
|
||||
|
|
@ -1976,7 +2008,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
// MOVE CORRESPONDING is always single-target per COBOL standard
|
||||
const targets = isCorresponding
|
||||
? [moveMatch[3].replace(/\..*$/, '').trim().split(/\s+/)[0]].filter((t) =>
|
||||
/^[A-Z][A-Z0-9-]+$/i.test(t),
|
||||
/^[A-Z0-9][A-Z0-9-]+$/i.test(t),
|
||||
)
|
||||
: extractMoveTargets(moveMatch[3]);
|
||||
|
||||
|
|
@ -1992,13 +2024,169 @@ export function extractCobolSymbolsWithRegex(
|
|||
}
|
||||
}
|
||||
|
||||
// Arithmetic statements — COMPUTE, ADD, SUBTRACT, MULTIPLY, DIVIDE
|
||||
// All extract target (written) and source operands (read) for ACCESSES edges
|
||||
// Mask quoted strings before matching to avoid false positives from
|
||||
// arithmetic keywords inside string literals (e.g., DISPLAY "COMPUTE").
|
||||
const lineForArith = line.replace(/"[^"]*"/g, ' ').replace(/'[^']*'/g, ' ');
|
||||
const arithMatch = lineForArith.match(/\b(COMPUTE|ADD|SUBTRACT|MULTIPLY|DIVIDE)\s+(.+)/i);
|
||||
if (arithMatch) {
|
||||
const verb = arithMatch[1].toUpperCase() as
|
||||
| 'COMPUTE'
|
||||
| 'ADD'
|
||||
| 'SUBTRACT'
|
||||
| 'MULTIPLY'
|
||||
| 'DIVIDE';
|
||||
const rest = arithMatch[2].replace(/\..*$/, '').trim();
|
||||
let target = '';
|
||||
const sources: string[] = [];
|
||||
let givingTarget: string | undefined;
|
||||
|
||||
switch (verb) {
|
||||
case 'COMPUTE': {
|
||||
// COMPUTE target = expression
|
||||
const eqIdx = rest.indexOf('=');
|
||||
if (eqIdx > 0) {
|
||||
target = rest.substring(0, eqIdx).trim().split(/\s+/)[0] || '';
|
||||
const expr = rest.substring(eqIdx + 1).trim();
|
||||
// Extract identifiers from expression (skip literals and operators)
|
||||
const idRe = /[A-Z0-9][A-Z0-9-]*/gi;
|
||||
let idMatch: RegExpExecArray | null;
|
||||
while ((idMatch = idRe.exec(expr)) !== null) {
|
||||
const name = idMatch[0];
|
||||
if (
|
||||
!/^(?:AND|OR|NOT|IN|OF|BY|TO|FROM|DIVIDED|INTO|GIVING|TIMES|PLUS|MINUS|MULTIPLIED)$/i.test(
|
||||
name,
|
||||
)
|
||||
) {
|
||||
if (!sources.includes(name)) sources.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ADD': {
|
||||
// ADD a TO b [GIVING c] — target is after TO or GIVING.
|
||||
// If no TO, try GIVING directly (ADD a GIVING b).
|
||||
const addGiving = rest.match(
|
||||
/\bTO\s+([A-Z0-9][A-Z0-9-]+)(?:\s+GIVING\s+([A-Z0-9][A-Z0-9-]+))?/i,
|
||||
);
|
||||
if (addGiving) {
|
||||
target = addGiving[2] ?? addGiving[1];
|
||||
if (addGiving[2]) givingTarget = addGiving[2];
|
||||
// Everything before TO is sources
|
||||
const beforeTo = rest.substring(0, rest.toUpperCase().indexOf(' TO '));
|
||||
beforeTo.replace(/\b([A-Z0-9][A-Z0-9-]+)\b/gi, (m: string) => {
|
||||
if (!/^(?:ADD|CORRESPONDING|CORR)$/i.test(m) && !sources.includes(m)) {
|
||||
sources.push(m);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
// Non-GIVING ADD A TO B: the TO operand (B) is both read and written
|
||||
// (the existing value is read, added, then stored back). Add B as a
|
||||
// source so both ACCESSES edges are created.
|
||||
if (!addGiving[2]) {
|
||||
if (!sources.includes(target)) sources.push(target);
|
||||
}
|
||||
} else {
|
||||
// No TO — try GIVING directly: ADD a GIVING b
|
||||
const addOnlyGiving = rest.match(/\bGIVING\s+([A-Z0-9][A-Z0-9-]+)/i);
|
||||
if (addOnlyGiving) {
|
||||
target = addOnlyGiving[1];
|
||||
givingTarget = addOnlyGiving[1];
|
||||
// Everything before GIVING is sources
|
||||
const beforeGiving = rest.substring(0, rest.toUpperCase().indexOf(' GIVING '));
|
||||
beforeGiving.replace(/\b([A-Z0-9][A-Z0-9-]+)\b/gi, (m: string) => {
|
||||
if (!/^(?:ADD|CORRESPONDING|CORR)$/i.test(m) && !sources.includes(m)) {
|
||||
sources.push(m);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'SUBTRACT': {
|
||||
// SUBTRACT a FROM b [GIVING c] — target is after FROM or GIVING
|
||||
const subGiving = rest.match(
|
||||
/\bFROM\s+([A-Z0-9][A-Z0-9-]+)(?:\s+GIVING\s+([A-Z0-9][A-Z0-9-]+))?/i,
|
||||
);
|
||||
if (subGiving) {
|
||||
target = subGiving[2] ?? subGiving[1];
|
||||
if (subGiving[2]) givingTarget = subGiving[2];
|
||||
const beforeFrom = rest.substring(0, rest.toUpperCase().indexOf(' FROM '));
|
||||
beforeFrom.replace(/\b([A-Z0-9][A-Z0-9-]+)\b/gi, (m: string) => {
|
||||
if (!/^(?:SUBTRACT|CORRESPONDING|CORR)$/i.test(m) && !sources.includes(m)) {
|
||||
sources.push(m);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MULTIPLY': {
|
||||
// MULTIPLY a BY b [GIVING c] — target is after BY or GIVING
|
||||
const mulGiving = rest.match(
|
||||
/\bBY\s+([A-Z0-9][A-Z0-9-]+)(?:\s+GIVING\s+([A-Z0-9][A-Z0-9-]+))?/i,
|
||||
);
|
||||
if (mulGiving) {
|
||||
target = mulGiving[2] ?? mulGiving[1];
|
||||
if (mulGiving[2]) givingTarget = mulGiving[2];
|
||||
const beforeBy = rest.substring(0, rest.toUpperCase().indexOf(' BY '));
|
||||
beforeBy.replace(/\b([A-Z0-9][A-Z0-9-]+)\b/gi, (m: string) => {
|
||||
if (!/^(?:MULTIPLY|CORRESPONDING|CORR)$/i.test(m) && !sources.includes(m)) {
|
||||
sources.push(m);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'DIVIDE': {
|
||||
// DIVIDE a INTO b [GIVING c] or DIVIDE a BY b [GIVING c]
|
||||
const divInto = rest.match(
|
||||
/\bINTO\s+([A-Z0-9][A-Z0-9-]+)(?:\s+GIVING\s+([A-Z0-9][A-Z0-9-]+))?/i,
|
||||
);
|
||||
const divBy = !divInto
|
||||
? rest.match(/\bBY\s+([A-Z0-9][A-Z0-9-]+)(?:\s+GIVING\s+([A-Z0-9][A-Z0-9-]+))?/i)
|
||||
: null;
|
||||
const divMatch = divInto ?? divBy;
|
||||
if (divMatch) {
|
||||
target = divMatch[2] ?? divMatch[1];
|
||||
if (divMatch[2]) givingTarget = divMatch[2];
|
||||
const beforeKeyword = divInto
|
||||
? rest.substring(0, rest.toUpperCase().indexOf(' INTO '))
|
||||
: rest.substring(0, rest.toUpperCase().indexOf(' BY '));
|
||||
beforeKeyword.replace(/\b([A-Z0-9][A-Z0-9-]+)\b/gi, (m: string) => {
|
||||
if (!/^(?:DIVIDE|CORRESPONDING|CORR)$/i.test(m) && !sources.includes(m)) {
|
||||
sources.push(m);
|
||||
}
|
||||
return m;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (target) {
|
||||
result.arithmeticOps.push({
|
||||
verb,
|
||||
target,
|
||||
sources,
|
||||
line: lineNum,
|
||||
caller: currentParagraph,
|
||||
givingTarget,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// GO TO — control flow transfer (handles GO TO p1 p2 p3 DEPENDING ON x)
|
||||
const gotoMatch = line.match(RE_GOTO);
|
||||
if (gotoMatch) {
|
||||
const targets = gotoMatch[1]
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((t) => /^[A-Z][A-Z0-9-]+$/i.test(t));
|
||||
.filter((t) => /^[A-Z0-9][A-Z0-9-]+$/i.test(t));
|
||||
for (const target of targets) {
|
||||
result.gotos.push({ caller: currentParagraph, target, line: lineNum });
|
||||
}
|
||||
|
|
@ -2046,7 +2234,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
}
|
||||
}
|
||||
}
|
||||
const inspectMatch = line.match(/\bINSPECT\s+([A-Z][A-Z0-9-]+)/i);
|
||||
const inspectMatch = line.match(/\bINSPECT\s+([A-Z0-9][A-Z0-9-]+)/i);
|
||||
if (inspectMatch && inspectAccum === null) {
|
||||
inspectAccum = line;
|
||||
inspectStartLine = lineNum;
|
||||
|
|
@ -2079,7 +2267,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
const targets = setTrueMatch[1]
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((t) => /^[A-Z][A-Z0-9-]+$/i.test(t) && t.toUpperCase() !== 'OF');
|
||||
.filter((t) => /^[A-Z0-9][A-Z0-9-]+$/i.test(t) && t.toUpperCase() !== 'OF');
|
||||
if (targets.length > 0) {
|
||||
result.sets.push({ targets, form: 'to-true', line: lineNum, caller: currentParagraph });
|
||||
}
|
||||
|
|
@ -2089,7 +2277,7 @@ export function extractCobolSymbolsWithRegex(
|
|||
const targets = setIdxMatch[1]
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((t) => /^[A-Z][A-Z0-9-]+$/i.test(t));
|
||||
.filter((t) => /^[A-Z0-9][A-Z0-9-]+$/i.test(t));
|
||||
const mode = setIdxMatch[2].toUpperCase();
|
||||
const form =
|
||||
mode === 'TO'
|
||||
|
|
@ -2114,7 +2302,8 @@ export function extractCobolSymbolsWithRegex(
|
|||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(
|
||||
(t) => /^[A-Z][A-Z0-9-]+$/i.test(t) && !INITIALIZE_CLAUSE_KEYWORDS.has(t.toUpperCase()),
|
||||
(t) =>
|
||||
/^[A-Z0-9][A-Z0-9-]+$/i.test(t) && !INITIALIZE_CLAUSE_KEYWORDS.has(t.toUpperCase()),
|
||||
);
|
||||
for (const target of targets) {
|
||||
result.initializes.push({ target, line: lineNum, caller: currentParagraph });
|
||||
|
|
|
|||
|
|
@ -74,9 +74,13 @@ export function emitCobolScopeCaptures(
|
|||
const endCol = endColFrom(lines[Math.min(endLine, lines.length) - 1] ?? '');
|
||||
|
||||
const progIdLine = findProgramIdLine(cleaned, name);
|
||||
// Determine PROGRAM-ID name column: free-format has no fixed column;
|
||||
// fixed-format uses column 7 (after 6-char sequence area replaced by preprocessing)
|
||||
const isFreeFormat = />>SOURCE\s+(?:FORMAT\s+(?:IS\s+)?)?FREE/i.test(cleaned);
|
||||
const nameCol = isFreeFormat ? findProgramIdNameColumn(lines, progIdLine) : 7;
|
||||
const nameRange =
|
||||
progIdLine !== -1
|
||||
? rangeOf(progIdLine, 7, progIdLine, lines[progIdLine - 1]?.length ?? endCol)
|
||||
? rangeOf(progIdLine, nameCol, progIdLine, lines[progIdLine - 1]?.length ?? endCol)
|
||||
: rangeOf(startLine, startCol, endLine, endCol);
|
||||
|
||||
const grouped: Record<string, Capture> = {
|
||||
|
|
@ -116,9 +120,11 @@ export function emitCobolScopeCaptures(
|
|||
const endCol = endColFrom(lines[Math.min(endLine, lines.length) - 1] ?? '');
|
||||
|
||||
const progIdLine = findProgramIdLine(cleaned, prog.name);
|
||||
const isFreeFormatNested = />>SOURCE\s+(?:FORMAT\s+(?:IS\s+)?)?FREE/i.test(cleaned);
|
||||
const nameColNested = isFreeFormatNested ? findProgramIdNameColumn(lines, progIdLine) : 7;
|
||||
const nameRange =
|
||||
progIdLine !== -1
|
||||
? rangeOf(progIdLine, 7, progIdLine, lines[progIdLine - 1]?.length ?? endCol)
|
||||
? rangeOf(progIdLine, nameColNested, progIdLine, lines[progIdLine - 1]?.length ?? endCol)
|
||||
: rangeOf(startLine, startCol, endLine, endCol);
|
||||
|
||||
const grouped: Record<string, Capture> = {
|
||||
|
|
@ -297,3 +303,19 @@ function findProgramIdLine(cleanedSource: string, programName: string): number {
|
|||
function escapeRegex(s: string): string {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the column position of the program name on the PROGRAM-ID line.
|
||||
* Searches for `PROGRAM-ID. name` and returns the column where name starts.
|
||||
* Returns 0 as fallback if the line can't be parsed (the range will be
|
||||
* from column 0 which is still valid for capture bounds).
|
||||
*/
|
||||
function findProgramIdNameColumn(lines: string[], lineNum: number): number {
|
||||
if (lineNum < 1 || lineNum > lines.length) return 0;
|
||||
const line = lines[lineNum - 1];
|
||||
const m = line.match(/\bPROGRAM-ID\.\s+([A-Z0-9][A-Z0-9-]*)/i);
|
||||
if (!m || m.index === undefined) return 0;
|
||||
// Column = index of start of capture group 1
|
||||
const nameStart = m.index + m[0].length - m[1].length;
|
||||
return nameStart;
|
||||
}
|
||||
|
|
|
|||
18
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/arithmetic-verbs.cbl
vendored
Normal file
18
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/arithmetic-verbs.cbl
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. ARITHOPS.
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 WS-A PIC 9(03) VALUE 10.
|
||||
01 WS-B PIC 9(03) VALUE 20.
|
||||
01 WS-C PIC 9(03) VALUE 30.
|
||||
01 WS-D PIC 9(03) VALUE 40.
|
||||
01 WS-RESULT PIC 9(05).
|
||||
PROCEDURE DIVISION.
|
||||
0000-MAIN.
|
||||
ADD WS-A TO WS-B.
|
||||
SUBTRACT WS-A FROM WS-C.
|
||||
MULTIPLY WS-A BY WS-B.
|
||||
DIVIDE WS-A INTO WS-D.
|
||||
COMPUTE WS-RESULT = WS-A + WS-B.
|
||||
STOP RUN.
|
||||
END PROGRAM ARITHOPS.
|
||||
17
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/digit-leading.cbl
vendored
Normal file
17
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/digit-leading.cbl
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. DIGITLEAD.
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 WS-DUMMY PIC X(01).
|
||||
PROCEDURE DIVISION.
|
||||
1000-MAIN.
|
||||
PERFORM 2000-READ-FILE.
|
||||
PERFORM 2000-READ-FILE THRU 2100-PROCESS.
|
||||
GO TO 9000-EXIT.
|
||||
2000-READ-FILE.
|
||||
MOVE 'R' TO WS-DUMMY.
|
||||
2100-PROCESS.
|
||||
MOVE 'P' TO WS-DUMMY.
|
||||
9000-EXIT.
|
||||
STOP RUN.
|
||||
END PROGRAM DIGITLEAD.
|
||||
9
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/fixed-format-offset.cbl
vendored
Normal file
9
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/fixed-format-offset.cbl
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. OFFSETPGM.
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 WS-FLAG PIC X(01).
|
||||
PROCEDURE DIVISION.
|
||||
MOVE 'X' TO WS-FLAG.
|
||||
STOP RUN.
|
||||
END PROGRAM OFFSETPGM.
|
||||
10
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/free-format.cbl
vendored
Normal file
10
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/free-format.cbl
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
>>SOURCE FORMAT FREE
|
||||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. FREEFMT.
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 WS-NAME PIC X(20).
|
||||
PROCEDURE DIVISION.
|
||||
DISPLAY "hello".
|
||||
STOP RUN.
|
||||
END PROGRAM FREEFMT.
|
||||
16
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/move-subscript.cbl
vendored
Normal file
16
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/move-subscript.cbl
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. MOVESUBS.
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 WS-NAME PIC X(50).
|
||||
01 WS-CUSTOMER PIC X(50).
|
||||
01 CUSTOMER-NAME PIC X(50).
|
||||
01 CUSTOMER-TABLE PIC X(100).
|
||||
01 CUSTOMER-TABLE-IDX PIC 9(03).
|
||||
PROCEDURE DIVISION.
|
||||
0000-MAIN.
|
||||
MOVE CUSTOMER-NAME(1:10) TO WS-NAME.
|
||||
MOVE CUSTOMER-TABLE(CUSTOMER-TABLE-IDX) TO WS-CUSTOMER.
|
||||
MOVE CUSTOMER-TABLE(CUSTOMER-TABLE-IDX)(1:5) TO WS-NAME.
|
||||
STOP RUN.
|
||||
END PROGRAM MOVESUBS.
|
||||
23
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/multi-table-sql.cbl
vendored
Normal file
23
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/multi-table-sql.cbl
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. MULTISQL.
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 WS-DATA PIC X(100).
|
||||
PROCEDURE DIVISION.
|
||||
EXEC SQL
|
||||
SELECT A.CUST_NAME, B.ACCT_BAL
|
||||
FROM CUSTOMER C, ACCOUNT A
|
||||
WHERE C.CUST_ID = A.CUST_ID
|
||||
END-EXEC.
|
||||
EXEC SQL
|
||||
SELECT *
|
||||
FROM CUSTOMER, ACCOUNT
|
||||
WHERE CUSTOMER.ID = ACCOUNT.CUST_ID
|
||||
END-EXEC.
|
||||
EXEC SQL
|
||||
SELECT *
|
||||
FROM INVENTORY
|
||||
WHERE QTY > 0
|
||||
END-EXEC.
|
||||
STOP RUN.
|
||||
END PROGRAM MULTISQL.
|
||||
26
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/perform-times.cbl
vendored
Normal file
26
gitnexus/test/fixtures/lang-resolution/cobol-parsing-coverage/perform-times.cbl
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
IDENTIFICATION DIVISION.
|
||||
PROGRAM-ID. PERFTIMS.
|
||||
DATA DIVISION.
|
||||
WORKING-STORAGE SECTION.
|
||||
01 WS-COUNT PIC 9(03) VALUE 5.
|
||||
01 WS-DONE PIC X(01).
|
||||
PROCEDURE DIVISION.
|
||||
1000-START.
|
||||
PERFORM 2000-PROCESS.
|
||||
PERFORM 2000-PROCESS THRU 2100-CLEANUP.
|
||||
* PERFORM TIMES with inline count — not a paragraph call:
|
||||
PERFORM 2000-PROCESS 3 TIMES.
|
||||
* PERFORM TIMES with identifier — not a paragraph call:
|
||||
PERFORM 2000-PROCESS WS-COUNT TIMES.
|
||||
* PERFORM VARYING — not a paragraph call:
|
||||
PERFORM VARYING WS-COUNT FROM 1 BY 1 UNTIL WS-COUNT > 10
|
||||
CONTINUE
|
||||
END-PERFORM.
|
||||
GO TO 9000-END.
|
||||
2000-PROCESS.
|
||||
MOVE 'X' TO WS-DONE.
|
||||
2100-CLEANUP.
|
||||
MOVE 'Y' TO WS-DONE.
|
||||
9000-END.
|
||||
STOP RUN.
|
||||
END PROGRAM PERFTIMS.
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/**
|
||||
* COBOL parsing-coverage regression tests (F17-F23 from issue #1925).
|
||||
*
|
||||
* Each finding has its own fixture file and exact-count assertions.
|
||||
* These tests must FAIL on main and PASS on the fix branch.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
FIXTURES,
|
||||
getRelationships,
|
||||
getNodesByLabel,
|
||||
runPipelineFromRepo,
|
||||
type PipelineResult,
|
||||
} from './helpers.js';
|
||||
|
||||
const COVERAGE_FIXTURE = path.join(FIXTURES, 'cobol-parsing-coverage');
|
||||
|
||||
describe('COBOL parsing coverage (F17-F23)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
|
||||
skipGraphPhases: true,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
// =====================================================================
|
||||
// F17: Digit-leading paragraph names
|
||||
// =====================================================================
|
||||
describe('F17 — digit-leading paragraph names', () => {
|
||||
const DIGITLEAD_FUNCS = ['1000-MAIN', '2000-READ-FILE', '2100-PROCESS', '9000-EXIT'];
|
||||
|
||||
it('captures digit-leading paragraphs as Function nodes', () => {
|
||||
const funcs = getNodesByLabel(result, 'Function');
|
||||
for (const name of DIGITLEAD_FUNCS) {
|
||||
expect(funcs).toContain(name);
|
||||
}
|
||||
});
|
||||
|
||||
it('PERFORM 2000-READ-FILE resolves to correct paragraph', () => {
|
||||
const perfCalls = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.rel.reason === 'cobol-perform',
|
||||
);
|
||||
const targets = perfCalls.map((e) => e.target);
|
||||
// DIGITLEAD: PERFORM 2000-READ-FILE (twice: bare + THRU first-target)
|
||||
expect(targets.filter((t) => t === '2000-READ-FILE').length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('PERFORM THRU with digit-leading targets emits both edges', () => {
|
||||
const perfThruEdges = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.rel.reason === 'cobol-perform-thru',
|
||||
);
|
||||
const thruTargets = perfThruEdges.map((e) => e.target);
|
||||
// DIGITLEAD: PERFORM 2000-READ-FILE THRU 2100-PROCESS
|
||||
expect(thruTargets).toContain('2100-PROCESS');
|
||||
// PERFTIMS: PERFORM 2000-PROCESS THRU 2100-CLEANUP
|
||||
expect(thruTargets).toContain('2100-CLEANUP');
|
||||
});
|
||||
|
||||
it('GO TO 9000-EXIT resolves to digit-leading target', () => {
|
||||
const gotoCalls = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.rel.reason === 'cobol-goto',
|
||||
);
|
||||
const gotoTargets = gotoCalls.map((e) => e.target);
|
||||
expect(gotoTargets).toContain('9000-EXIT');
|
||||
expect(gotoTargets).toContain('9000-END');
|
||||
});
|
||||
|
||||
// INPUT/OUTPUT PROCEDURE (SORT/MERGE): The regex patterns at
|
||||
// L1605-1610 use the same [A-Z0-9] character class as all other
|
||||
// F17 patterns — verified via code audit that the capture groups
|
||||
// accept digit-leading paragraph names identically to RE_PROC_PARAGRAPH.
|
||||
// Dedicated SORT/INPUT PROCEDURE fixture requires file declarations
|
||||
// (SELECT/ASSIGN) which adds complexity beyond this findings scope.
|
||||
// Deferred to future work; existing regex coverage is confirmed.
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// F18: MOVE source subscript
|
||||
// =====================================================================
|
||||
describe('F18 — MOVE with subscripts', () => {
|
||||
it('emits cobol-move-read ACCESSES edges', () => {
|
||||
const readEdges = getRelationships(result, 'ACCESSES').filter(
|
||||
(e) => e.rel.reason === 'cobol-move-read',
|
||||
);
|
||||
expect(readEdges.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('emits cobol-move-write ACCESSES edges', () => {
|
||||
const writeEdges = getRelationships(result, 'ACCESSES').filter(
|
||||
(e) => e.rel.reason === 'cobol-move-write',
|
||||
);
|
||||
expect(writeEdges.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// F19: Multi-table SQL FROM
|
||||
// =====================================================================
|
||||
describe('F19 — multi-table SQL FROM', () => {
|
||||
it('captures all tables from comma-separated FROM clauses', () => {
|
||||
const sqlAccesses = getRelationships(result, 'ACCESSES').filter(
|
||||
(e) => e.rel.reason === 'sql-select',
|
||||
);
|
||||
// MULTISQL has:
|
||||
// FROM CUSTOMER C, ACCOUNT A → CUSTOMER, ACCOUNT (2 tables)
|
||||
// FROM CUSTOMER, ACCOUNT → CUSTOMER, ACCOUNT (2 tables)
|
||||
// FROM INVENTORY → INVENTORY (1 table)
|
||||
// Total: 5 table references across 3 SQL blocks
|
||||
expect(sqlAccesses.length).toBe(5);
|
||||
const targets = sqlAccesses.map((e) => e.target);
|
||||
expect(targets).toContain('Record:<db>:CUSTOMER');
|
||||
expect(targets).toContain('Record:<db>:ACCOUNT');
|
||||
expect(targets).toContain('Record:<db>:INVENTORY');
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// F20: All 5 arithmetic verbs
|
||||
// =====================================================================
|
||||
describe('F20 — arithmetic verb ACCESSES edges', () => {
|
||||
it('ADD A TO B produces both edges', () => {
|
||||
const arithRead = getRelationships(result, 'ACCESSES').filter(
|
||||
(e) => e.rel.reason === 'cobol-arithmetic-read',
|
||||
);
|
||||
const arithWrite = getRelationships(result, 'ACCESSES').filter(
|
||||
(e) => e.rel.reason === 'cobol-arithmetic-write',
|
||||
);
|
||||
// ARITHOPS fixture:
|
||||
// ADD WS-A TO WS-B → read(WS-A) write(WS-B)
|
||||
// SUBTRACT WS-A FROM WS-C → read(WS-A) write(WS-C)
|
||||
// MULTIPLY WS-A BY WS-B → read(WS-A) write(WS-B)
|
||||
// DIVIDE WS-A INTO WS-D → read(WS-A) write(WS-D)
|
||||
// COMPUTE WS-RESULT = ... → read(WS-A, WS-B) write(WS-RESULT)
|
||||
// Total: 6+ reads, 5 writes
|
||||
expect(arithRead.length).toBeGreaterThanOrEqual(6);
|
||||
expect(arithWrite.length).toBeGreaterThanOrEqual(5);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// F21: Free-format column detection
|
||||
// =====================================================================
|
||||
describe('F21 — free-format PROGRAM-ID column', () => {
|
||||
it('produces Module nodes for both fixtures', () => {
|
||||
const modules = getNodesByLabel(result, 'Module');
|
||||
expect(modules).toContain('FREEFMT');
|
||||
expect(modules).toContain('OFFSETPGM');
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// F22: File-size guard — edge case verification
|
||||
// =====================================================================
|
||||
describe('F22 — file-size guard', () => {
|
||||
// When threshold is below file sizes, files are skipped (no Module nodes).
|
||||
// When threshold is above, files process normally.
|
||||
let skipResult: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES = '100';
|
||||
skipResult = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
|
||||
skipGraphPhases: true,
|
||||
});
|
||||
delete process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES;
|
||||
}, 60000);
|
||||
|
||||
it('file above threshold is skipped — zero Module nodes', () => {
|
||||
// With threshold=100, all fixture files (203-906 bytes) are over the limit.
|
||||
// The guard calls logger.warn with the file path and size — visible in test stderr.
|
||||
const modules = getNodesByLabel(skipResult, 'Module');
|
||||
expect(modules.length).toBe(0);
|
||||
});
|
||||
|
||||
it('file near threshold (below limit) processes normally', async () => {
|
||||
// Set threshold to 10MB — well above all fixture file sizes
|
||||
process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES = String(10 * 1024 * 1024);
|
||||
const norResult = await runPipelineFromRepo(COVERAGE_FIXTURE, () => {}, {
|
||||
skipGraphPhases: true,
|
||||
});
|
||||
delete process.env.GITNEXUS_MAX_COBOL_FILE_SIZE_BYTES;
|
||||
const modules = getNodesByLabel(norResult, 'Module');
|
||||
expect(modules.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// =====================================================================
|
||||
// F23: PERFORM TIMES + THRU on digit-leading targets
|
||||
// =====================================================================
|
||||
describe('F23 — PERFORM TIMES and THRU', () => {
|
||||
it('PERFORM 2000-PROCESS THRU 2100-CLEANUP resolves THRU target', () => {
|
||||
const perfThruEdges = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.rel.reason === 'cobol-perform-thru',
|
||||
);
|
||||
const thruTargets = perfThruEdges.map((e) => e.target);
|
||||
expect(thruTargets).toContain('2100-CLEANUP');
|
||||
});
|
||||
|
||||
it('PERFORM with VARYING does NOT create spurious CALLS edge', () => {
|
||||
// PERFTIMS has PERFORM VARYING WS-COUNT FROM 1 BY 1...
|
||||
// VARYING is in PERFORM_KEYWORD_SKIP, so it should be skipped.
|
||||
// Verify there are no spurious perform edges with target "VARYING"
|
||||
const perfEdges = getRelationships(result, 'CALLS').filter(
|
||||
(e) => e.rel.reason === 'cobol-perform',
|
||||
);
|
||||
const targets = perfEdges.map((e) => e.target);
|
||||
expect(targets).not.toContain('VARYING');
|
||||
expect(targets).not.toContain('WS-COUNT');
|
||||
});
|
||||
|
||||
it('total CALLS count stays reasonable (TIMES with count is real)', () => {
|
||||
// PERFTIMS has: 2 perform (2000-PROCESS + 2000-PROCESS THRU first-target)
|
||||
// + 2 perform for count TIMES (2000-PROCESS 3 TIMES + WS-COUNT TIMES)
|
||||
// + 1 perform-thru + 1 goto = 6 CALLS
|
||||
// DIGITLEAD has: 2 perform + 1 perform-thru + 1 goto = 4 CALLS
|
||||
// Total: 10 across both fixtures
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
expect(calls.length).toBe(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -713,12 +713,14 @@ describe('COBOL full system extraction', () => {
|
|||
expect(getRelationships(result, 'IMPORTS').length).toBe(2);
|
||||
});
|
||||
|
||||
it('produces exactly 25 total ACCESSES edges', () => {
|
||||
// 4 move-read + 5 move-write + 1 move-corresponding-read + 1 move-corresponding-write
|
||||
it('produces exactly 28 total ACCESSES edges', () => {
|
||||
// Original: 4 move-read + 5 move-write + 1 move-corresponding-read + 1 move-corresponding-write
|
||||
// + 1 file-read + 1 map + 1 queue-write
|
||||
// + 1 receive-into + 2 send-from + 1 search + 1 sort-using + 1 sort-giving
|
||||
// + 2 procedure-using + 1 sql-select + 2 call-using
|
||||
expect(getRelationships(result, 'ACCESSES').length).toBe(25);
|
||||
// plus arithmetic: +1 arithmetic-read (WS-AMOUNT) + 1 arithmetic-write (CUST-BALANCE)
|
||||
// plus ADD TO read+write: +1 arithmetic-read for CUST-BALANCE (TO operand is both read+written)
|
||||
expect(getRelationships(result, 'ACCESSES').length).toBe(28);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue