@monoes/monograph 1.5.1 → 1.5.2
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/.monodesign/hook.cache.json +1 -0
- package/dist/src/analysis/cycles.d.ts.map +1 -1
- package/dist/src/analysis/cycles.js +5 -3
- package/dist/src/analysis/cycles.js.map +1 -1
- package/dist/src/analysis/trace.d.ts.map +1 -1
- package/dist/src/analysis/trace.js +1 -19
- package/dist/src/analysis/trace.js.map +1 -1
- package/dist/src/analysis/worker-pool.d.ts.map +1 -1
- package/dist/src/analysis/worker-pool.js +9 -7
- package/dist/src/analysis/worker-pool.js.map +1 -1
- package/dist/src/cli/skill-gen.d.ts.map +1 -1
- package/dist/src/cli/skill-gen.js +16 -25
- package/dist/src/cli/skill-gen.js.map +1 -1
- package/dist/src/export/html.d.ts.map +1 -1
- package/dist/src/export/html.js +3 -2
- package/dist/src/export/html.js.map +1 -1
- package/dist/src/graph/regex-search.d.ts.map +1 -1
- package/dist/src/graph/regex-search.js +9 -3
- package/dist/src/graph/regex-search.js.map +1 -1
- package/dist/src/pipeline/phases/call-site-extractors.d.ts +17 -0
- package/dist/src/pipeline/phases/call-site-extractors.d.ts.map +1 -0
- package/dist/src/pipeline/phases/call-site-extractors.js +132 -0
- package/dist/src/pipeline/phases/call-site-extractors.js.map +1 -0
- package/dist/src/pipeline/phases/module-resolution.d.ts +10 -0
- package/dist/src/pipeline/phases/module-resolution.d.ts.map +1 -0
- package/dist/src/pipeline/phases/module-resolution.js +301 -0
- package/dist/src/pipeline/phases/module-resolution.js.map +1 -0
- package/dist/src/pipeline/phases/orm.js +1 -2
- package/dist/src/pipeline/phases/orm.js.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.d.ts +2 -25
- package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
- package/dist/src/pipeline/phases/scope-resolution.js +18 -580
- package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
- package/dist/src/pipeline/phases/wildcard-phase.d.ts.map +1 -1
- package/dist/src/pipeline/phases/wildcard-phase.js +6 -1
- package/dist/src/pipeline/phases/wildcard-phase.js.map +1 -1
- package/dist/src/search/hybrid-query.js +1 -1
- package/dist/src/search/hybrid-query.js.map +1 -1
- package/dist/src/web/api.d.ts.map +1 -1
- package/dist/src/web/api.js +17 -14
- package/dist/src/web/api.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/__tests__/pipeline/phases/wildcard-phase.test.ts +39 -4
- package/src/analysis/cycles.ts +7 -3
- package/src/analysis/trace.ts +1 -21
- package/src/analysis/worker-pool.ts +8 -7
- package/src/cli/skill-gen.ts +6 -14
- package/src/export/html.ts +3 -2
- package/src/graph/regex-search.ts +9 -5
- package/src/pipeline/phases/call-site-extractors.ts +169 -0
- package/src/pipeline/phases/module-resolution.ts +294 -0
- package/src/pipeline/phases/orm.ts +1 -2
- package/src/pipeline/phases/scope-resolution.ts +25 -618
- package/src/pipeline/phases/wildcard-phase.ts +5 -1
- package/src/search/hybrid-query.ts +1 -1
- package/src/web/api.ts +17 -13
package/package.json
CHANGED
|
@@ -2,14 +2,22 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import Database from 'better-sqlite3';
|
|
3
3
|
import { wildcardSynthesisPhase } from '../../../pipeline/phases/wildcard-phase.js';
|
|
4
4
|
import type { PipelineContext } from '../../../pipeline/types.js';
|
|
5
|
+
import { CREATE_NODES, CREATE_EDGES } from '../../../storage/schema.js';
|
|
5
6
|
|
|
7
|
+
// Uses the REAL production schema (including the edges FK constraint on
|
|
8
|
+
// source_id/target_id) with foreign_keys actually enabled — the original
|
|
9
|
+
// hand-rolled test schema had neither, so it silently passed inserts that
|
|
10
|
+
// would violate the real edges table's FK constraint in production (issue #40).
|
|
6
11
|
function makeDb(): Database.Database {
|
|
7
12
|
const db = new Database(':memory:');
|
|
13
|
+
db.pragma('foreign_keys = ON');
|
|
14
|
+
db.exec(CREATE_NODES);
|
|
15
|
+
db.exec(CREATE_EDGES);
|
|
8
16
|
db.exec(`
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
INSERT INTO nodes
|
|
12
|
-
|
|
17
|
+
INSERT INTO nodes (id, label, name, norm_label, file_path, start_line, end_line, is_exported)
|
|
18
|
+
VALUES ('n1', 'Function', 'greet', 'greet', '/mod.ts', 1, 5, 1);
|
|
19
|
+
INSERT INTO nodes (id, label, name, norm_label, file_path, is_exported)
|
|
20
|
+
VALUES ('n2', 'File', 'main.ts', 'main.ts', '/main.ts', 0);
|
|
13
21
|
`);
|
|
14
22
|
return db;
|
|
15
23
|
}
|
|
@@ -61,4 +69,31 @@ describe('wildcardSynthesisPhase', () => {
|
|
|
61
69
|
const deps = new Map<string, any>([['parse', parseOutput], ['cross-file', crossFileOutput]]);
|
|
62
70
|
await expect(wildcardSynthesisPhase.execute(makeCtx(db), deps)).resolves.not.toThrow();
|
|
63
71
|
});
|
|
72
|
+
|
|
73
|
+
it('skips a file with no fileNodeIndex entry instead of fabricating a source_id and violating the edges FK constraint (issue #40)', async () => {
|
|
74
|
+
// Regression: a file with no parsed symbols (e.g. blank, or a filetype
|
|
75
|
+
// the parser emits no node for) has no entry in fileNodeIndex. The old
|
|
76
|
+
// code fell back to a fabricated `file:${filePath}` id that was never a
|
|
77
|
+
// real row in `nodes`, so the edge insert below violated the edges
|
|
78
|
+
// table's FOREIGN KEY constraint on source_id and crashed the whole
|
|
79
|
+
// monograph_build. /empty.ts here is exactly that case: it's present in
|
|
80
|
+
// fileContents (parsed) but has NO corresponding node in symbolNodes.
|
|
81
|
+
const db = makeDb();
|
|
82
|
+
const source = `import * as mod from '/mod.ts';\nmod.greet("hello");`;
|
|
83
|
+
const parseOutput = {
|
|
84
|
+
allEdges: [],
|
|
85
|
+
symbolNodes: [
|
|
86
|
+
{ id: 'n1', name: 'greet', label: 'Function', normLabel: 'greet', filePath: '/mod.ts', isExported: true },
|
|
87
|
+
],
|
|
88
|
+
parseErrors: [],
|
|
89
|
+
fileContents: new Map([['/empty.ts', source]]),
|
|
90
|
+
};
|
|
91
|
+
const crossFileOutput = { resolvedEdges: [] };
|
|
92
|
+
const deps = new Map<string, any>([['parse', parseOutput], ['cross-file', crossFileOutput]]);
|
|
93
|
+
|
|
94
|
+
await expect(wildcardSynthesisPhase.execute(makeCtx(db), deps)).resolves.not.toThrow();
|
|
95
|
+
|
|
96
|
+
const edgeCount = (db.prepare('SELECT COUNT(*) as c FROM edges').get() as { c: number }).c;
|
|
97
|
+
expect(edgeCount).toBe(0); // nothing could legitimately attach to a file with no real node
|
|
98
|
+
});
|
|
64
99
|
});
|
package/src/analysis/cycles.ts
CHANGED
|
@@ -203,14 +203,18 @@ export function detectCycles(db: MonographDb): CycleDetectionResult {
|
|
|
203
203
|
const nodeIds = nodeRows.map(n => n.id);
|
|
204
204
|
const sccs = tarjanSCC(adjacency, nodeIds);
|
|
205
205
|
|
|
206
|
-
//
|
|
207
|
-
const cycleSCCs = sccs.filter(scc =>
|
|
206
|
+
// SCCs with size > 1 are multi-node cycles; size-1 SCCs are cycles only if self-referencing
|
|
207
|
+
const cycleSCCs = sccs.filter(scc =>
|
|
208
|
+
scc.length > 1 || (scc.length === 1 && adjacency.get(scc[0])?.includes(scc[0])),
|
|
209
|
+
);
|
|
208
210
|
|
|
209
211
|
const cycles: DependencyCycle[] = [];
|
|
210
212
|
const filesInCyclesSet = new Set<string>();
|
|
211
213
|
|
|
212
214
|
for (const scc of cycleSCCs) {
|
|
213
|
-
const elementaryCycles =
|
|
215
|
+
const elementaryCycles = scc.length === 1
|
|
216
|
+
? [[scc[0], scc[0]]]
|
|
217
|
+
: findCyclesInSCC(scc, adjacency, 20);
|
|
214
218
|
for (const cyclePath of elementaryCycles) {
|
|
215
219
|
// cyclePath is [...nodes, startNode] — last element = first
|
|
216
220
|
const cycleNodes = cyclePath.slice(0, -1);
|
package/src/analysis/trace.ts
CHANGED
|
@@ -226,27 +226,7 @@ export function traceDependency(
|
|
|
226
226
|
|
|
227
227
|
const directSet = new Set(directImporters);
|
|
228
228
|
|
|
229
|
-
// BFS to find transitive importers
|
|
230
|
-
// Build a file→files-it-imports map for internal files
|
|
231
|
-
const fileImports = new Map<string, Set<string>>();
|
|
232
|
-
for (const edge of importEdges) {
|
|
233
|
-
if (!fileImports.has(edge.sourceFile)) {
|
|
234
|
-
fileImports.set(edge.sourceFile, new Set());
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Build a reverse map: file → files that import it
|
|
239
|
-
const importedBy = new Map<string, Set<string>>();
|
|
240
|
-
for (const edge of importEdges) {
|
|
241
|
-
if (!importedBy.has(edge.sourceFile)) {
|
|
242
|
-
importedBy.set(edge.sourceFile, new Set());
|
|
243
|
-
}
|
|
244
|
-
// We track which files import this package, then who imports those files
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// For transitive: BFS from direct importers outward through reverse import graph
|
|
248
|
-
// We need: who imports the direct importers (via internal imports)
|
|
249
|
-
// Build reverse internal-import map from edges where targetPackage looks like a file path
|
|
229
|
+
// BFS to find transitive importers via reverse import graph
|
|
250
230
|
const reverseImportMap = new Map<string, Set<string>>();
|
|
251
231
|
for (const edge of importEdges) {
|
|
252
232
|
if (!reverseImportMap.has(edge.targetPackage)) {
|
|
@@ -44,22 +44,23 @@ export class WorkerPool {
|
|
|
44
44
|
workerData: task.input,
|
|
45
45
|
resourceLimits: { stackSizeMb: this.stackSizeMb },
|
|
46
46
|
});
|
|
47
|
-
|
|
47
|
+
let settled = false;
|
|
48
|
+
const settle = (fn: () => void) => {
|
|
49
|
+
if (settled) return;
|
|
50
|
+
settled = true;
|
|
51
|
+
fn();
|
|
48
52
|
this._active--;
|
|
49
53
|
this._drain();
|
|
50
54
|
};
|
|
51
55
|
worker.on('message', (v: unknown) => {
|
|
52
|
-
task.resolve(v);
|
|
53
|
-
cleanup();
|
|
56
|
+
settle(() => task.resolve(v));
|
|
54
57
|
});
|
|
55
58
|
worker.on('error', (err: unknown) => {
|
|
56
|
-
task.reject(err);
|
|
57
|
-
cleanup();
|
|
59
|
+
settle(() => task.reject(err));
|
|
58
60
|
});
|
|
59
61
|
worker.on('exit', (code: number) => {
|
|
60
62
|
if (code !== 0) {
|
|
61
|
-
task.reject(new Error(`Worker exited with code ${code}`));
|
|
62
|
-
cleanup();
|
|
63
|
+
settle(() => task.reject(new Error(`Worker exited with code ${code}`)));
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
66
|
}
|
package/src/cli/skill-gen.ts
CHANGED
|
@@ -59,12 +59,8 @@ export async function generateSkillFiles(
|
|
|
59
59
|
|
|
60
60
|
const db = openDb(dbPath);
|
|
61
61
|
|
|
62
|
-
let communities: CommunityRow[];
|
|
63
62
|
try {
|
|
64
|
-
|
|
65
|
-
} finally {
|
|
66
|
-
closeDb(db);
|
|
67
|
-
}
|
|
63
|
+
const communities = queryCommunities(db);
|
|
68
64
|
|
|
69
65
|
if (communities.length === 0) {
|
|
70
66
|
return { filesWritten: [], communityCount: 0 };
|
|
@@ -75,15 +71,8 @@ export async function generateSkillFiles(
|
|
|
75
71
|
const filesWritten: string[] = [];
|
|
76
72
|
|
|
77
73
|
for (const community of communities) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
let crossConnections: EdgeRow[];
|
|
81
|
-
try {
|
|
82
|
-
members = queryMembers(db2, community.community_id);
|
|
83
|
-
crossConnections = queryCrossConnections(db2, community.community_id);
|
|
84
|
-
} finally {
|
|
85
|
-
closeDb(db2);
|
|
86
|
-
}
|
|
74
|
+
const members = queryMembers(db, community.community_id);
|
|
75
|
+
const crossConnections = queryCrossConnections(db, community.community_id);
|
|
87
76
|
|
|
88
77
|
const content = renderSkillMarkdown(community, members, crossConnections, repoPath);
|
|
89
78
|
|
|
@@ -94,6 +83,9 @@ export async function generateSkillFiles(
|
|
|
94
83
|
}
|
|
95
84
|
|
|
96
85
|
return { filesWritten, communityCount: communities.length };
|
|
86
|
+
} finally {
|
|
87
|
+
closeDb(db);
|
|
88
|
+
}
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
// ============================================================================
|
package/src/export/html.ts
CHANGED
|
@@ -74,6 +74,7 @@ export function toHtml(nodes: MonographNode[], edges: MonographEdge[]): string {
|
|
|
74
74
|
return {
|
|
75
75
|
id: n.id,
|
|
76
76
|
label: shortLabel,
|
|
77
|
+
_fullName: n.name,
|
|
77
78
|
title: buildTooltip(n, deg),
|
|
78
79
|
group: n.communityId ?? 0,
|
|
79
80
|
color: {
|
|
@@ -398,7 +399,7 @@ function showInfoCard(nodeId) {
|
|
|
398
399
|
if (!node) return;
|
|
399
400
|
const neighbors = network.getConnectedNodes(nodeId);
|
|
400
401
|
document.getElementById('ic-type').textContent = node._nodeType || '';
|
|
401
|
-
document.getElementById('ic-name').textContent = node.label;
|
|
402
|
+
document.getElementById('ic-name').textContent = node._fullName || node.label;
|
|
402
403
|
const fp = node._filePath ? escHtml(node._filePath) : '';
|
|
403
404
|
const fpShort = node._filePath ? escHtml(node._filePath.split('/').pop()) : '';
|
|
404
405
|
document.getElementById('ic-meta').innerHTML =
|
|
@@ -420,7 +421,7 @@ function onSearch(q) {
|
|
|
420
421
|
searchActive = !!q;
|
|
421
422
|
const lq = q.toLowerCase();
|
|
422
423
|
if (!q) { resetHighlight(); return; }
|
|
423
|
-
const matchIds = new Set(ALL_NODES.filter(n => n.label.toLowerCase().includes(lq) || (n._filePath && n._filePath.toLowerCase().includes(lq))).map(n => n.id));
|
|
424
|
+
const matchIds = new Set(ALL_NODES.filter(n => (n._fullName || n.label).toLowerCase().includes(lq) || (n._filePath && n._filePath.toLowerCase().includes(lq))).map(n => n.id));
|
|
424
425
|
nodesDS.update(ALL_NODES.map(n => ({ id: n.id, opacity: matchIds.has(n.id) ? 1 : 0.06 })));
|
|
425
426
|
edgesDS.update(ALL_EDGES.map(e => ({ id: e.id, opacity: matchIds.has(e.from) && matchIds.has(e.to) ? 0.7 : 0.03 })));
|
|
426
427
|
// Focus on first match
|
|
@@ -25,11 +25,13 @@ export function regexSearchNodes(
|
|
|
25
25
|
): RegexNodeMatch[] {
|
|
26
26
|
const re = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
27
27
|
|
|
28
|
+
// Scan all rows and apply regex filter in JS, then cap at limit.
|
|
29
|
+
// LIMIT in SQL would silently miss matches beyond the first N rows.
|
|
28
30
|
const rows = db.prepare(
|
|
29
31
|
`SELECT id, label, name, norm_label, file_path, start_line, end_line,
|
|
30
32
|
community_id, is_exported, language, properties
|
|
31
|
-
FROM nodes
|
|
32
|
-
).all(
|
|
33
|
+
FROM nodes`,
|
|
34
|
+
).all() as {
|
|
33
35
|
id: string;
|
|
34
36
|
label: string;
|
|
35
37
|
name: string;
|
|
@@ -69,9 +71,10 @@ export function regexSearchNodes(
|
|
|
69
71
|
|
|
70
72
|
if (value && re.test(value)) {
|
|
71
73
|
results.push({ node, field });
|
|
72
|
-
break;
|
|
74
|
+
break;
|
|
73
75
|
}
|
|
74
76
|
}
|
|
77
|
+
if (results.length >= limit) break;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
return results;
|
|
@@ -103,8 +106,8 @@ export function regexSearchEdges(
|
|
|
103
106
|
|
|
104
107
|
const rows = db.prepare(
|
|
105
108
|
`SELECT id, source_id, target_id, relation, confidence, confidence_score, reason
|
|
106
|
-
FROM edges
|
|
107
|
-
).all(
|
|
109
|
+
FROM edges`,
|
|
110
|
+
).all() as {
|
|
108
111
|
id: string;
|
|
109
112
|
source_id: string;
|
|
110
113
|
target_id: string;
|
|
@@ -138,6 +141,7 @@ export function regexSearchEdges(
|
|
|
138
141
|
break;
|
|
139
142
|
}
|
|
140
143
|
}
|
|
144
|
+
if (results.length >= limit) break;
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
return results;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ── Call site extraction by language ──────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export interface CallSite {
|
|
4
|
+
callerFileNodeId: string;
|
|
5
|
+
callerFilePath: string;
|
|
6
|
+
calleeRaw: string;
|
|
7
|
+
form: 'method' | 'direct' | 'dynamic';
|
|
8
|
+
receiverName?: string;
|
|
9
|
+
methodName?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ── Language keyword sets ────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const JS_KEYWORDS = new Set([
|
|
15
|
+
'if', 'for', 'while', 'switch', 'return', 'function', 'class', 'new', 'typeof',
|
|
16
|
+
'await', 'catch', 'throw', 'delete', 'void', 'instanceof', 'in', 'of',
|
|
17
|
+
'import', 'export', 'yield', 'async', 'super', 'this', 'const', 'let', 'var',
|
|
18
|
+
'try', 'finally', 'break', 'continue', 'debugger', 'with', 'do',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const PY_KEYWORDS = new Set([
|
|
22
|
+
'if', 'for', 'while', 'with', 'return', 'def', 'class', 'import', 'from',
|
|
23
|
+
'raise', 'yield', 'lambda', 'await', 'async', 'del', 'pass', 'assert',
|
|
24
|
+
'except', 'elif', 'else', 'not', 'and', 'or', 'in', 'is', 'print',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const GO_KEYWORDS = new Set([
|
|
28
|
+
'if', 'for', 'range', 'return', 'func', 'type', 'var', 'const', 'import', 'package',
|
|
29
|
+
'go', 'defer', 'select', 'case', 'default', 'break', 'continue', 'goto', 'fallthrough',
|
|
30
|
+
'chan', 'map', 'struct', 'interface', 'make', 'new', 'len', 'cap', 'append', 'delete',
|
|
31
|
+
'panic', 'recover', 'close', 'switch', 'else',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const JAVA_KEYWORDS = new Set([
|
|
35
|
+
'if', 'for', 'while', 'do', 'switch', 'case', 'return', 'class', 'interface', 'enum',
|
|
36
|
+
'new', 'extends', 'implements', 'import', 'package', 'throw', 'throws', 'catch', 'try',
|
|
37
|
+
'finally', 'static', 'final', 'abstract', 'public', 'private', 'protected', 'void',
|
|
38
|
+
'break', 'continue', 'default', 'else', 'instanceof', 'this', 'super',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const RUST_KEYWORDS = new Set([
|
|
42
|
+
'if', 'let', 'for', 'while', 'loop', 'match', 'return', 'fn', 'struct', 'enum', 'trait',
|
|
43
|
+
'impl', 'use', 'mod', 'pub', 'super', 'self', 'type', 'where', 'in', 'as', 'mut',
|
|
44
|
+
'ref', 'move', 'async', 'await', 'dyn', 'extern', 'crate', 'static', 'const', 'unsafe',
|
|
45
|
+
'break', 'continue', 'else',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
// ── Extension sets ───────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export const TS_JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
51
|
+
export const CJS_MJS_EXTS = new Set(['.cjs', '.mjs']);
|
|
52
|
+
const PY_EXTS = new Set(['.py']);
|
|
53
|
+
const GO_EXTS = new Set(['.go']);
|
|
54
|
+
const JAVA_EXTS = new Set(['.java']);
|
|
55
|
+
const RUST_EXTS = new Set(['.rs']);
|
|
56
|
+
|
|
57
|
+
export function isSupportedExt(ext: string): boolean {
|
|
58
|
+
return TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext) || PY_EXTS.has(ext) || GO_EXTS.has(ext) || JAVA_EXTS.has(ext) || RUST_EXTS.has(ext);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Generic extractor for languages with method.call and direct() patterns ──
|
|
62
|
+
|
|
63
|
+
function extractSimpleCallSites(
|
|
64
|
+
source: string,
|
|
65
|
+
filePath: string,
|
|
66
|
+
fileNodeId: string,
|
|
67
|
+
keywords: Set<string>,
|
|
68
|
+
directCharClass = '[A-Za-z_][\\w]*',
|
|
69
|
+
): CallSite[] {
|
|
70
|
+
const sites: CallSite[] = [];
|
|
71
|
+
const methodPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
72
|
+
let m: RegExpExecArray | null;
|
|
73
|
+
while ((m = methodPattern.exec(source)) !== null) {
|
|
74
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
|
|
75
|
+
}
|
|
76
|
+
const directPattern = new RegExp(`(?<![.[\\w])(${directCharClass})\\s*\\(`, 'g');
|
|
77
|
+
while ((m = directPattern.exec(source)) !== null) {
|
|
78
|
+
const name = m[1]!;
|
|
79
|
+
if (keywords.has(name)) continue;
|
|
80
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct' });
|
|
81
|
+
}
|
|
82
|
+
return sites;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function extractGoCallSites(source: string, filePath: string, fileNodeId: string): CallSite[] {
|
|
86
|
+
return extractSimpleCallSites(source, filePath, fileNodeId, GO_KEYWORDS);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function extractJavaCallSites(source: string, filePath: string, fileNodeId: string): CallSite[] {
|
|
90
|
+
return extractSimpleCallSites(source, filePath, fileNodeId, JAVA_KEYWORDS, '[A-Za-z_$][\\w$]*');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function extractRustCallSites(source: string, filePath: string, fileNodeId: string): CallSite[] {
|
|
94
|
+
return extractSimpleCallSites(source, filePath, fileNodeId, RUST_KEYWORDS);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Main extractor: dispatches by file extension ─────────────────────────────
|
|
98
|
+
|
|
99
|
+
export function extractCallSites(
|
|
100
|
+
source: string,
|
|
101
|
+
filePath: string,
|
|
102
|
+
fileNodeId: string,
|
|
103
|
+
ext: string,
|
|
104
|
+
): CallSite[] {
|
|
105
|
+
if (GO_EXTS.has(ext)) return extractGoCallSites(source, filePath, fileNodeId);
|
|
106
|
+
if (JAVA_EXTS.has(ext)) return extractJavaCallSites(source, filePath, fileNodeId);
|
|
107
|
+
if (RUST_EXTS.has(ext)) return extractRustCallSites(source, filePath, fileNodeId);
|
|
108
|
+
|
|
109
|
+
const sites: CallSite[] = [];
|
|
110
|
+
|
|
111
|
+
if (TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext)) {
|
|
112
|
+
const methodPattern = /(\w+)\.(\w+)\s*(?:<[^>]*>\s*)?\(/g;
|
|
113
|
+
let m: RegExpExecArray | null;
|
|
114
|
+
while ((m = methodPattern.exec(source)) !== null) {
|
|
115
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const chainedPattern = /[)\]]\s*\.(\w+)\s*(?:<[^>]*>\s*)?\(/g;
|
|
119
|
+
while ((m = chainedPattern.exec(source)) !== null) {
|
|
120
|
+
const name = m[1];
|
|
121
|
+
if (JS_KEYWORDS.has(name)) continue;
|
|
122
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `?.${name}`, form: 'method', methodName: name });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const directPattern = /(?<![.[\w])([A-Za-z_$][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
|
|
126
|
+
while ((m = directPattern.exec(source)) !== null) {
|
|
127
|
+
const name = m[1];
|
|
128
|
+
if (JS_KEYWORDS.has(name)) continue;
|
|
129
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct', methodName: name });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const newPattern = /\bnew\s+([A-Z][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
|
|
133
|
+
while ((m = newPattern.exec(source)) !== null) {
|
|
134
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `new ${m[1]}`, form: 'direct', methodName: m[1] });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const dynamicPattern = /\w+\s*\[[\w'"` ]+\]\s*\(/g;
|
|
138
|
+
while ((m = dynamicPattern.exec(source)) !== null) {
|
|
139
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: m[0], form: 'dynamic' });
|
|
140
|
+
}
|
|
141
|
+
} else if (PY_EXTS.has(ext)) {
|
|
142
|
+
const methodPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
143
|
+
let m: RegExpExecArray | null;
|
|
144
|
+
while ((m = methodPattern.exec(source)) !== null) {
|
|
145
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const directPattern = /(?<![.[\w])([A-Za-z_][\w]*)\s*\(/g;
|
|
149
|
+
while ((m = directPattern.exec(source)) !== null) {
|
|
150
|
+
const name = m[1];
|
|
151
|
+
if (PY_KEYWORDS.has(name)) continue;
|
|
152
|
+
sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct', methodName: name });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return sites;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Constructor assignment tracking ──────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
export function extractConstructorAssignments(source: string): Map<string, string> {
|
|
162
|
+
const result = new Map<string, string>();
|
|
163
|
+
const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*new\s+([A-Z][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
|
|
164
|
+
let m: RegExpExecArray | null;
|
|
165
|
+
while ((m = pattern.exec(source)) !== null) {
|
|
166
|
+
result.set(m[1], m[2]);
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|