feat(extraction): add COBOL language support (.cbl/.cob/.cpy) (#590, #648) (#1161)

Programs, sections/paragraphs (reconstructed extents over the grammar's
flat header stream), PERFORM/THRU/GO TO/CALL call edges, COPY copybook
imports incl. standalone .cpy fragments, DATA DIVISION records/fields/
88-levels with write-site impact references, and CICS flows: EXEC
LINK/XCTL program targets (literal + same-file VALUE deref), EXEC SQL
INCLUDE, and pseudo-conversational RETURN/START TRANSID hops resolved
to the owning program via a CICS framework resolver. Fixed and free
source format (free format via a scanner wide-mode sentinel).

Grammar: vendored wasm built from a patched yutaro-sakamoto/
tree-sitter-cobol (EXEC blocks as an external-scanner token, copybook
fragment entry point, single-quote continuation, COPY REPLACING
pseudo-text, NOT=, CALL GIVING, ENTRY, FREE, bitwise ops, abbreviated
relations, COBOL-2002 usages, and more). Patch + provenance + upstream
PR draft in docs/grammars/. Parse health: AWS CardDemo 43/44 native
(upstream: 9/31), 44/44 through preParse; copybooks 28/29; CobolCraft
free-format 17/17 (upstream: 0); NIST COBOL85 unchanged at 373/382.

Copybook members resolve to files like C includes (basename index,
name-matcher short-circuit so compiler-supplied members stay honestly
unresolved): CardDemo imports 5 -> 285. Impact proof: ACCT-CURR-BAL
(CVACT01Y copybook) surfaces its 4 writer programs cross-file.

Also: run-all.sh now neutralizes the ambient prompt-hook in both A/B
arms (CODEGRAPH_NO_PROMPT_HOOK=1); COBOL corpus entries for agent-eval.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-03 09:17:53 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 7d624ecfac
commit 41620c60fa
16 changed files with 1612 additions and 6 deletions
+91
View File
@@ -46,6 +46,14 @@ export function resolveImportPath(
language: Language,
context: ResolutionContext
): string | null {
// COBOL COPY/EXEC SQL INCLUDE names a copybook member, not a path — the
// compiler searches a library, so we match against indexed file basenames.
// Must run before isExternalImport: a bare member name would otherwise be
// misclassified as an external package.
if (language === 'cobol') {
return resolveCobolCopybook(importPath, fromFile, context);
}
// Skip external/npm packages — but pass the context so the
// bare-specifier heuristic can consult the project's tsconfig
// alias map first (custom prefixes like `@components/*` would
@@ -76,6 +84,57 @@ export function resolveImportPath(
return null;
}
/**
* COBOL copybook lookup: `COPY CVACT01Y` (or `EXEC SQL INCLUDE X`) names a
* library member resolved by the compiler's copybook search path, so we match
* the member against indexed file basenames, case-insensitively. `.cpy` wins
* over a same-named program; a same-directory hit wins within a tier. The
* stem index is built once per resolution context (a per-ref scan of every
* file node would go quadratic on copybook-heavy repos).
*/
const cobolCopybookIndexes = new WeakMap<ResolutionContext, Map<string, string[]>>();
function resolveCobolCopybook(
member: string,
fromFile: string,
context: ResolutionContext
): string | null {
let index = cobolCopybookIndexes.get(context);
if (!index) {
index = new Map();
for (const fileNode of context.getNodesByKind('file')) {
const normalized = fileNode.filePath.replace(/\\/g, '/');
const base = normalized.split('/').pop() ?? '';
const dot = base.lastIndexOf('.');
const stem = (dot > 0 ? base.slice(0, dot) : base).toLowerCase();
const paths = index.get(stem);
if (paths) paths.push(fileNode.filePath);
else index.set(stem, [fileNode.filePath]);
}
cobolCopybookIndexes.set(context, index);
}
const candidates = index.get(member.toLowerCase());
if (!candidates || candidates.length === 0) return null;
const fromDir = fromFile.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
let best: string | null = null;
let bestScore = -1;
for (const candidate of candidates) {
const normalized = candidate.replace(/\\/g, '/');
const ext = normalized.slice(normalized.lastIndexOf('.')).toLowerCase();
let score = 0;
if (ext === '.cpy') score += 4;
else if (ext === '.cbl' || ext === '.cob' || ext === '.cobol') score += 2;
if (normalized.split('/').slice(0, -1).join('/') === fromDir) score += 1;
if (score > bestScore) {
bestScore = score;
best = candidate;
}
}
return best;
}
/**
* C and C++ standard library header names (without delimiters).
* Used by isExternalImport to filter system includes from resolution.
@@ -547,6 +606,15 @@ export function isPhpIncludePathRef(ref: UnresolvedRef): boolean {
);
}
/**
* Is this a COBOL COPY / EXEC SQL INCLUDE copybook reference? These resolve
* to files only (or stay unresolved for compiler-supplied members) — never
* to a same-named symbol via the name-matcher.
*/
export function isCobolCopybookRef(ref: UnresolvedRef): boolean {
return ref.language === 'cobol' && ref.referenceKind === 'imports';
}
/**
* Resolve a PHP include/require path to a project-relative file path.
*
@@ -1165,6 +1233,29 @@ export function resolveViaImport(
return null;
}
// COBOL COPY / EXEC SQL INCLUDE — resolve the copybook member to a
// file→file edge, mirroring the C/C++ include branch above. A member that
// matches no indexed file (compiler-supplied copybooks like SQLCA/DFHAID)
// stays unresolved — callers must not fall back to the symbol name-matcher,
// which would connect it to a same-named import symbol elsewhere.
if (isCobolCopybookRef(ref)) {
const resolvedPath = resolveImportPath(ref.referenceName, ref.filePath, ref.language!, context);
if (!resolvedPath) return null;
const basename = resolvedPath.split('/').pop()!;
const fileNode = context
.getNodesByName(basename)
.find((n) => n.kind === 'file' && n.filePath === resolvedPath);
if (fileNode) {
return {
original: ref,
targetNodeId: fileNode.id,
confidence: 0.9,
resolvedBy: 'import',
};
}
return null;
}
// PHP include/require — resolve the static string path to a file→file
// edge, mirroring the C/C++ branch above. Distinguish include PATHS from
// namespace `use` symbols by shape: an include path contains a slash or a