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:
co-authored by
Claude Fable 5
parent
7d624ecfac
commit
41620c60fa
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* CICS Framework Resolver (COBOL)
|
||||
*
|
||||
* Resolves the pseudo-conversational transaction hop: a program ends with
|
||||
* `EXEC CICS RETURN TRANSID('CB00')` (or START), and CICS re-invokes the
|
||||
* program that OWNS transaction CB00 on the next attention key. The
|
||||
* transaction→program mapping lives in the CICS CSD, which is never in the
|
||||
* repo — but by near-universal convention each program declares its own
|
||||
* transaction id as a working-storage constant:
|
||||
*
|
||||
* 05 WS-TRANID PIC X(04) VALUE 'CB00'.
|
||||
*
|
||||
* The COBOL extractor emits `cics-transid:CB00` call references for literal
|
||||
* (or same-file-dereferenced) TRANSID options; this resolver maps the id to
|
||||
* the program module whose TRAN*-named data item declares that VALUE. No
|
||||
* match (an id owned by a program outside the repo) stays unresolved.
|
||||
*/
|
||||
|
||||
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
|
||||
import { Node } from '../../types';
|
||||
|
||||
const TRANSID_REF_PREFIX = 'cics-transid:';
|
||||
/** Data items that name a transaction id by convention. */
|
||||
const TRANID_NAME_RE = /TRAN/i;
|
||||
const VALUE_LITERAL_RE = /\bVALUE\s+['"]([A-Za-z0-9$#@]{1,4})['"]/i;
|
||||
|
||||
/**
|
||||
* transaction id → owning program module, built once per resolution context.
|
||||
* A WeakMap so a per-ref scan of every data node can't go quadratic on
|
||||
* copybook-heavy repos.
|
||||
*/
|
||||
const transidIndexes = new WeakMap<ResolutionContext, Map<string, string>>();
|
||||
|
||||
function buildIndex(context: ResolutionContext): Map<string, string> {
|
||||
const index = new Map<string, string>();
|
||||
const dataNodes: Node[] = [
|
||||
...context.getNodesByKind('variable'),
|
||||
...context.getNodesByKind('field'),
|
||||
...context.getNodesByKind('constant'),
|
||||
];
|
||||
for (const node of dataNodes) {
|
||||
if (node.language !== 'cobol') continue;
|
||||
if (!TRANID_NAME_RE.test(node.name)) continue;
|
||||
const value = node.signature ? VALUE_LITERAL_RE.exec(node.signature) : null;
|
||||
if (!value?.[1]) continue;
|
||||
const tx = value[1].toUpperCase();
|
||||
if (index.has(tx)) continue; // first declaration wins; collisions are rare and ambiguous
|
||||
const moduleNode = context
|
||||
.getNodesInFile(node.filePath)
|
||||
.find((n) => n.kind === 'module' && n.language === 'cobol');
|
||||
if (moduleNode) index.set(tx, moduleNode.id);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
export const cicsResolver: FrameworkResolver = {
|
||||
name: 'cics',
|
||||
languages: ['cobol'],
|
||||
|
||||
detect(context: ResolutionContext): boolean {
|
||||
// Any indexed COBOL program qualifies — the resolver only ever acts on
|
||||
// cics-transid: references, which only the COBOL extractor emits.
|
||||
return context.getNodesByKind('module').some((n) => n.language === 'cobol');
|
||||
},
|
||||
|
||||
// cics-transid:XXXX matches no symbol name — opt it past the
|
||||
// name-exists pre-filter so it reaches resolve().
|
||||
claimsReference(name: string): boolean {
|
||||
return name.startsWith(TRANSID_REF_PREFIX);
|
||||
},
|
||||
|
||||
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
|
||||
if (!ref.referenceName.startsWith(TRANSID_REF_PREFIX)) return null;
|
||||
const tx = ref.referenceName.slice(TRANSID_REF_PREFIX.length).toUpperCase();
|
||||
|
||||
let index = transidIndexes.get(context);
|
||||
if (!index) {
|
||||
index = buildIndex(context);
|
||||
transidIndexes.set(context, index);
|
||||
}
|
||||
|
||||
const targetNodeId = index.get(tx);
|
||||
if (!targetNodeId) return null;
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId,
|
||||
confidence: 0.85,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import { swiftObjcBridgeResolver } from './swift-objc';
|
||||
import { reactNativeBridgeResolver } from './react-native';
|
||||
import { expoModulesResolver } from './expo-modules';
|
||||
import { fabricViewResolver } from './fabric';
|
||||
import { cicsResolver } from './cics';
|
||||
|
||||
/**
|
||||
* All registered framework resolvers
|
||||
@@ -70,6 +71,8 @@ const FRAMEWORK_RESOLVERS: FrameworkResolver[] = [
|
||||
expoModulesResolver,
|
||||
// React Native Fabric / Codegen view components — TS spec → component nodes
|
||||
fabricViewResolver,
|
||||
// CICS pseudo-conversational TRANSID hops (COBOL)
|
||||
cicsResolver,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ImportMapping,
|
||||
} from './types';
|
||||
import { matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, sameLanguageFamily, crossesKnownFamily } from './name-matcher';
|
||||
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef } from './import-resolver';
|
||||
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef } from './import-resolver';
|
||||
import { detectFrameworks } from './frameworks';
|
||||
import { synthesizeCallbackEdges } from './callback-synthesizer';
|
||||
import { createYielder, type MaybeYield } from './cooperative-yield';
|
||||
@@ -815,7 +815,7 @@ export class ReferenceResolver {
|
||||
// If that didn't find the file, do NOT fall back to the symbol
|
||||
// name-matcher — it would mis-connect e.g. "inc/db.php" to an unrelated
|
||||
// db.php elsewhere in the tree (a wrong edge is worse than none, #660).
|
||||
if (isPhpIncludePathRef(ref)) {
|
||||
if (isPhpIncludePathRef(ref) || isCobolCopybookRef(ref)) {
|
||||
return candidates.length > 0
|
||||
? candidates.reduce((best, curr) =>
|
||||
curr.confidence > best.confidence ? curr : best
|
||||
|
||||
Reference in New Issue
Block a user