chiasmus 0.1.14 → 0.1.15
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/dist/graph/extractor.js +271 -65
- package/dist/graph/extractor.js.map +1 -1
- package/dist/graph/native-analyses.d.ts +12 -4
- package/dist/graph/native-analyses.d.ts.map +1 -1
- package/dist/graph/native-analyses.js +165 -53
- package/dist/graph/native-analyses.js.map +1 -1
- package/dist/skills/library.d.ts.map +1 -1
- package/dist/skills/library.js +69 -53
- package/dist/skills/library.js.map +1 -1
- package/dist/solvers/prolog-solver.d.ts.map +1 -1
- package/dist/solvers/prolog-solver.js +10 -0
- package/dist/solvers/prolog-solver.js.map +1 -1
- package/package.json +1 -1
package/dist/graph/extractor.js
CHANGED
|
@@ -74,10 +74,52 @@ function extractFromTree(tree, filePath, lang, defines, calls, imports, exports,
|
|
|
74
74
|
}
|
|
75
75
|
else {
|
|
76
76
|
const scopeStack = [];
|
|
77
|
-
|
|
77
|
+
// Per-file alias map: local name → imported name. Populated as
|
|
78
|
+
// `import_statement` nodes are encountered during the walk, read by
|
|
79
|
+
// `resolveCallee` so a call to `bar()` from `import { foo as bar }`
|
|
80
|
+
// records an edge to `foo`, matching the exported name on the other
|
|
81
|
+
// side of the module boundary. ES imports are syntactically required
|
|
82
|
+
// to appear before any executable code, so building the map lazily
|
|
83
|
+
// during the walk is safe.
|
|
84
|
+
const aliasMap = new Map();
|
|
85
|
+
// Function-reference queue: every `foo` identifier passed as a direct
|
|
86
|
+
// call argument is recorded here during the walk, then resolved after
|
|
87
|
+
// the walk against the file's final defines + imports. Deferred
|
|
88
|
+
// resolution is necessary because the reference can appear *before*
|
|
89
|
+
// the callee is defined in source order (e.g. `names.map(quoteIfNeeded)`
|
|
90
|
+
// above a `function quoteIfNeeded(...)` declaration).
|
|
91
|
+
const pendingRefs = [];
|
|
92
|
+
walkNode(tree.rootNode, filePath, lang, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
93
|
+
// Resolve pending references. A ref becomes a real edge iff the target
|
|
94
|
+
// name is a function/method defined in this file or an import binding
|
|
95
|
+
// this file owns. Non-matching names are dropped so local variable
|
|
96
|
+
// identifiers (req, opts, config, etc.) don't pollute the graph.
|
|
97
|
+
const knownNames = new Set();
|
|
98
|
+
for (const d of defines) {
|
|
99
|
+
if (d.file !== filePath)
|
|
100
|
+
continue;
|
|
101
|
+
if (d.kind === "function" || d.kind === "method" || d.kind === "class") {
|
|
102
|
+
knownNames.add(d.name);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const imp of imports) {
|
|
106
|
+
if (imp.file === filePath)
|
|
107
|
+
knownNames.add(imp.name);
|
|
108
|
+
}
|
|
109
|
+
for (const ref of pendingRefs) {
|
|
110
|
+
if (!knownNames.has(ref.callee))
|
|
111
|
+
continue;
|
|
112
|
+
if (ref.caller === ref.callee)
|
|
113
|
+
continue;
|
|
114
|
+
const key = `${ref.caller}->${ref.callee}`;
|
|
115
|
+
if (callSet.has(key))
|
|
116
|
+
continue;
|
|
117
|
+
callSet.add(key);
|
|
118
|
+
calls.push({ caller: ref.caller, callee: ref.callee });
|
|
119
|
+
}
|
|
78
120
|
}
|
|
79
121
|
}
|
|
80
|
-
function walkNode(node, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet) {
|
|
122
|
+
function walkNode(node, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet) {
|
|
81
123
|
const type = node.type;
|
|
82
124
|
switch (type) {
|
|
83
125
|
case "function_declaration": {
|
|
@@ -85,7 +127,7 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
85
127
|
if (name) {
|
|
86
128
|
defines.push({ file: filePath, name, kind: "function", line: node.startPosition.row + 1 });
|
|
87
129
|
scopeStack.push(name);
|
|
88
|
-
walkChildren(node, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet);
|
|
130
|
+
walkChildren(node, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
89
131
|
scopeStack.pop();
|
|
90
132
|
return; // already walked children
|
|
91
133
|
}
|
|
@@ -101,7 +143,7 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
101
143
|
contains.push({ parent: className, child: name });
|
|
102
144
|
}
|
|
103
145
|
scopeStack.push(name);
|
|
104
|
-
walkChildren(node, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet);
|
|
146
|
+
walkChildren(node, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
105
147
|
scopeStack.pop();
|
|
106
148
|
return;
|
|
107
149
|
}
|
|
@@ -112,7 +154,7 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
112
154
|
if (name) {
|
|
113
155
|
defines.push({ file: filePath, name, kind: "class", line: node.startPosition.row + 1 });
|
|
114
156
|
scopeStack.push(name);
|
|
115
|
-
walkChildren(node, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet);
|
|
157
|
+
walkChildren(node, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
116
158
|
scopeStack.pop();
|
|
117
159
|
return;
|
|
118
160
|
}
|
|
@@ -130,12 +172,12 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
130
172
|
const name = nameNode.text;
|
|
131
173
|
defines.push({ file: filePath, name, kind: "function", line: node.startPosition.row + 1 });
|
|
132
174
|
scopeStack.push(name);
|
|
133
|
-
walkChildren(valueNode, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet);
|
|
175
|
+
walkChildren(valueNode, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
134
176
|
scopeStack.pop();
|
|
135
177
|
foundArrow = true;
|
|
136
178
|
}
|
|
137
179
|
else if (valueNode) {
|
|
138
|
-
walkChildren(child, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet);
|
|
180
|
+
walkChildren(child, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
139
181
|
}
|
|
140
182
|
}
|
|
141
183
|
}
|
|
@@ -144,7 +186,7 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
144
186
|
break;
|
|
145
187
|
}
|
|
146
188
|
case "call_expression": {
|
|
147
|
-
const callee = resolveCallee(node);
|
|
189
|
+
const callee = resolveCallee(node, aliasMap);
|
|
148
190
|
const caller = scopeStack.length > 0 ? scopeStack[scopeStack.length - 1] : null;
|
|
149
191
|
if (callee && caller) {
|
|
150
192
|
const key = `${caller}->${callee}`;
|
|
@@ -153,6 +195,29 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
153
195
|
calls.push({ caller, callee });
|
|
154
196
|
}
|
|
155
197
|
}
|
|
198
|
+
// Record identifier arguments as potential function references.
|
|
199
|
+
// Passing a fn by reference (arr.map(fn), emitter.on("sig", fn))
|
|
200
|
+
// doesn't generate a call_expression for `fn` itself, so without
|
|
201
|
+
// this pass the target looks unused and dead-code analysis flags
|
|
202
|
+
// it as dead. We collect the (caller, argIdentifier) pairs here
|
|
203
|
+
// and resolve them against the file's known function names after
|
|
204
|
+
// the walk finishes.
|
|
205
|
+
if (caller) {
|
|
206
|
+
const argsNode = node.childForFieldName("arguments");
|
|
207
|
+
if (argsNode) {
|
|
208
|
+
for (let i = 0; i < argsNode.childCount; i++) {
|
|
209
|
+
const arg = argsNode.child(i);
|
|
210
|
+
if (arg.type !== "identifier")
|
|
211
|
+
continue;
|
|
212
|
+
const argName = arg.text;
|
|
213
|
+
// Rewrite through aliasMap so a reference to an aliased
|
|
214
|
+
// import resolves to the canonical exported name, matching
|
|
215
|
+
// what resolveCallee does for direct calls.
|
|
216
|
+
const canonical = aliasMap.get(argName) ?? argName;
|
|
217
|
+
pendingRefs.push({ caller, callee: canonical });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
156
221
|
break; // fall through to walk children (nested calls)
|
|
157
222
|
}
|
|
158
223
|
case "import_statement": {
|
|
@@ -161,7 +226,7 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
161
226
|
if (source) {
|
|
162
227
|
const importClause = node.children.find((c) => c.type === "import_clause");
|
|
163
228
|
if (importClause) {
|
|
164
|
-
extractImportNames(importClause, filePath, source, imports);
|
|
229
|
+
extractImportNames(importClause, filePath, source, imports, aliasMap);
|
|
165
230
|
}
|
|
166
231
|
}
|
|
167
232
|
return; // no need to walk deeper
|
|
@@ -222,21 +287,29 @@ function walkNode(node, filePath, language, scopeStack, defines, calls, imports,
|
|
|
222
287
|
break; // fall through to walk children (may contain function_declaration etc.)
|
|
223
288
|
}
|
|
224
289
|
}
|
|
225
|
-
walkChildren(node, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet);
|
|
290
|
+
walkChildren(node, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
226
291
|
}
|
|
227
|
-
function walkChildren(node, filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet) {
|
|
292
|
+
function walkChildren(node, filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet) {
|
|
228
293
|
for (let i = 0; i < node.childCount; i++) {
|
|
229
|
-
walkNode(node.child(i), filePath, language, scopeStack, defines, calls, imports, exports, contains, callSet);
|
|
294
|
+
walkNode(node.child(i), filePath, language, scopeStack, aliasMap, pendingRefs, defines, calls, imports, exports, contains, callSet);
|
|
230
295
|
}
|
|
231
296
|
}
|
|
232
|
-
/**
|
|
233
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Resolve the callee name from a call_expression node. If the identifier
|
|
299
|
+
* matches a local alias from `import { foo as bar }`, the call is
|
|
300
|
+
* rewritten to the original export name (`foo`) so cross-file analyses
|
|
301
|
+
* see the link. Member-expression callees aren't rewritten — they reach
|
|
302
|
+
* into an object, not a top-level binding.
|
|
303
|
+
*/
|
|
304
|
+
function resolveCallee(callNode, aliasMap) {
|
|
234
305
|
const fnNode = callNode.childForFieldName("function");
|
|
235
306
|
if (!fnNode)
|
|
236
307
|
return null;
|
|
237
308
|
switch (fnNode.type) {
|
|
238
|
-
case "identifier":
|
|
239
|
-
|
|
309
|
+
case "identifier": {
|
|
310
|
+
const name = fnNode.text;
|
|
311
|
+
return aliasMap.get(name) ?? name;
|
|
312
|
+
}
|
|
240
313
|
case "member_expression": {
|
|
241
314
|
// obj.method() → method, this.method() → method
|
|
242
315
|
const property = fnNode.childForFieldName("property");
|
|
@@ -265,23 +338,36 @@ function findEnclosingClassName(node) {
|
|
|
265
338
|
}
|
|
266
339
|
return null;
|
|
267
340
|
}
|
|
268
|
-
/**
|
|
269
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Extract import names from an import_clause. For each named specifier
|
|
343
|
+
* we push an ImportsFact keyed by the *imported* name (so the imports
|
|
344
|
+
* list reflects the exported identifier as seen by the module being
|
|
345
|
+
* imported), and — when the local binding differs (an `as` alias) —
|
|
346
|
+
* register a `local → imported` entry in `aliasMap` so call-site
|
|
347
|
+
* resolution can rewrite calls through the alias back to the canonical
|
|
348
|
+
* name used by the call graph.
|
|
349
|
+
*/
|
|
350
|
+
function extractImportNames(clause, filePath, source, imports, aliasMap) {
|
|
270
351
|
for (let i = 0; i < clause.childCount; i++) {
|
|
271
352
|
const child = clause.child(i);
|
|
272
353
|
// Default import: import foo from './bar'
|
|
273
354
|
if (child.type === "identifier") {
|
|
274
355
|
imports.push({ file: filePath, name: child.text, source });
|
|
275
356
|
}
|
|
276
|
-
// Named imports: import { foo, bar } from './
|
|
357
|
+
// Named imports: import { foo, bar, baz as qux } from './mod'
|
|
277
358
|
if (child.type === "named_imports") {
|
|
278
359
|
for (let j = 0; j < child.childCount; j++) {
|
|
279
360
|
const spec = child.child(j);
|
|
280
|
-
if (spec.type
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
361
|
+
if (spec.type !== "import_specifier")
|
|
362
|
+
continue;
|
|
363
|
+
const name = spec.childForFieldName("name")?.text;
|
|
364
|
+
if (!name)
|
|
365
|
+
continue;
|
|
366
|
+
imports.push({ file: filePath, name, source });
|
|
367
|
+
// `alias` field only exists when the specifier has an `as` clause.
|
|
368
|
+
const alias = spec.childForFieldName("alias")?.text;
|
|
369
|
+
if (alias && alias !== name) {
|
|
370
|
+
aliasMap.set(alias, name);
|
|
285
371
|
}
|
|
286
372
|
}
|
|
287
373
|
}
|
|
@@ -588,20 +674,70 @@ function extractGoReceiverType(receiver) {
|
|
|
588
674
|
return null;
|
|
589
675
|
}
|
|
590
676
|
// ── Clojure extraction ──────────────────────────────────────────────
|
|
591
|
-
/**
|
|
677
|
+
/**
|
|
678
|
+
* Get the textual name of a Clojure symbol, preserving any namespace
|
|
679
|
+
* prefix. tree-sitter-clojure parses a qualified symbol like `db/query`
|
|
680
|
+
* into a sym_lit containing a `sym_ns` child ("db") and a `sym_name`
|
|
681
|
+
* child ("query"); returning just the `sym_name` loses the ns info and
|
|
682
|
+
* conflates cross-namespace calls. Reconstruct `ns/name` when both are
|
|
683
|
+
* present, otherwise fall back to the bare `sym_name`.
|
|
684
|
+
*/
|
|
592
685
|
function cljSymName(node) {
|
|
593
686
|
if (node.type === "sym_name")
|
|
594
687
|
return node.text;
|
|
595
688
|
if (node.type === "sym_lit") {
|
|
689
|
+
let ns = null;
|
|
690
|
+
let name = null;
|
|
596
691
|
for (let i = 0; i < node.childCount; i++) {
|
|
597
|
-
|
|
598
|
-
|
|
692
|
+
const c = node.child(i);
|
|
693
|
+
if (c.type === "sym_ns")
|
|
694
|
+
ns = c.text;
|
|
695
|
+
else if (c.type === "sym_name" && name === null)
|
|
696
|
+
name = c.text;
|
|
599
697
|
}
|
|
698
|
+
if (name === null)
|
|
699
|
+
return null;
|
|
700
|
+
return ns ? `${ns}/${name}` : name;
|
|
600
701
|
}
|
|
601
702
|
return null;
|
|
602
703
|
}
|
|
603
|
-
/**
|
|
604
|
-
|
|
704
|
+
/**
|
|
705
|
+
* Resolve a Clojure call target to its canonical form. Rules:
|
|
706
|
+
* - `alias/name` where alias is in aliasMap → `<full-ns>/name`
|
|
707
|
+
* - `prefix/name` where prefix is unknown → returned as-is (already fully
|
|
708
|
+
* qualified, or an alias we couldn't resolve — either way, leave it)
|
|
709
|
+
* - bare `name` where `<currentNs>/name` is defined in this file → qualify
|
|
710
|
+
* to the current ns (same-file reference)
|
|
711
|
+
* - bare `name` otherwise → returned as-is (external, clojure.core, etc.)
|
|
712
|
+
*
|
|
713
|
+
* When `currentNs` is null (file has no ns form) the bare path just returns
|
|
714
|
+
* the input, preserving the legacy "bare names everywhere" behavior for
|
|
715
|
+
* script-style files.
|
|
716
|
+
*/
|
|
717
|
+
function qualifyCljName(name, ctx) {
|
|
718
|
+
const slashIdx = name.indexOf("/");
|
|
719
|
+
if (slashIdx >= 0) {
|
|
720
|
+
const prefix = name.slice(0, slashIdx);
|
|
721
|
+
const local = name.slice(slashIdx + 1);
|
|
722
|
+
const fullNs = ctx.aliasMap.get(prefix);
|
|
723
|
+
if (fullNs)
|
|
724
|
+
return `${fullNs}/${local}`;
|
|
725
|
+
return name;
|
|
726
|
+
}
|
|
727
|
+
if (ctx.currentNs === null)
|
|
728
|
+
return name;
|
|
729
|
+
const candidate = `${ctx.currentNs}/${name}`;
|
|
730
|
+
if (ctx.inFileDefns.has(candidate))
|
|
731
|
+
return candidate;
|
|
732
|
+
return name;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Extract ns form: (ns foo.bar (:require [baz.qux :as q] [x.y :refer [z]])).
|
|
736
|
+
* Populates `imports` with the required namespaces and `aliasMap` with any
|
|
737
|
+
* `:as` bindings so the call extractor can resolve `q/some-fn` to
|
|
738
|
+
* `baz.qux/some-fn`.
|
|
739
|
+
*/
|
|
740
|
+
function cljExtractNs(listNode, filePath, imports, aliasMap) {
|
|
605
741
|
let symIdx = -1;
|
|
606
742
|
for (let i = 0; i < listNode.childCount; i++) {
|
|
607
743
|
if (listNode.child(i).type === "sym_lit") {
|
|
@@ -638,16 +774,38 @@ function cljExtractNs(listNode, filePath, imports) {
|
|
|
638
774
|
for (let k = j + 1; k < child.childCount; k++) {
|
|
639
775
|
const vec = child.child(k);
|
|
640
776
|
if (vec.type === "vec_lit") {
|
|
641
|
-
// First sym_lit in vector is the required namespace
|
|
777
|
+
// First sym_lit in vector is the required namespace; scan for
|
|
778
|
+
// a trailing `:as alias` pair so alias → full-ns can be
|
|
779
|
+
// resolved at call sites.
|
|
780
|
+
let reqNs = null;
|
|
642
781
|
for (let l = 0; l < vec.childCount; l++) {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
imports.push({ file: filePath, name: reqNs, source: reqNs });
|
|
647
|
-
}
|
|
782
|
+
const vc = vec.child(l);
|
|
783
|
+
if (vc.type === "sym_lit") {
|
|
784
|
+
reqNs = cljSymName(vc);
|
|
648
785
|
break;
|
|
649
786
|
}
|
|
650
787
|
}
|
|
788
|
+
if (!reqNs)
|
|
789
|
+
continue;
|
|
790
|
+
imports.push({ file: filePath, name: reqNs, source: reqNs });
|
|
791
|
+
for (let l = 0; l < vec.childCount; l++) {
|
|
792
|
+
const vc = vec.child(l);
|
|
793
|
+
if (vc.type !== "kwd_lit")
|
|
794
|
+
continue;
|
|
795
|
+
const kname = vc.children?.find((c) => c.type === "kwd_name")?.text;
|
|
796
|
+
if (kname !== "as")
|
|
797
|
+
continue;
|
|
798
|
+
for (let m = l + 1; m < vec.childCount; m++) {
|
|
799
|
+
const asSym = vec.child(m);
|
|
800
|
+
if (asSym.type === "sym_lit") {
|
|
801
|
+
const alias = cljSymName(asSym);
|
|
802
|
+
if (alias)
|
|
803
|
+
aliasMap.set(alias, reqNs);
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
651
809
|
}
|
|
652
810
|
}
|
|
653
811
|
}
|
|
@@ -736,14 +894,17 @@ function cljMethodImplName(listNode) {
|
|
|
736
894
|
* extend-protocol form, extracting call edges from each method impl body
|
|
737
895
|
* using the method name as the caller.
|
|
738
896
|
*/
|
|
739
|
-
function cljWalkDispatchMethods(parentList,
|
|
897
|
+
function cljWalkDispatchMethods(parentList, ctx, calls, callSet) {
|
|
740
898
|
for (let i = 0; i < parentList.childCount; i++) {
|
|
741
899
|
const child = parentList.child(i);
|
|
742
900
|
if (child.type !== "list_lit")
|
|
743
901
|
continue;
|
|
744
902
|
const methodName = cljMethodImplName(child);
|
|
745
903
|
if (methodName) {
|
|
746
|
-
|
|
904
|
+
// Method implementations are attributed to the bare method name
|
|
905
|
+
// (not qualified): dispatch method names are looked up by unqualified
|
|
906
|
+
// identifier and usually match a defprotocol entry elsewhere.
|
|
907
|
+
cljExtractCalls(child, methodName, ctx, calls, callSet);
|
|
747
908
|
}
|
|
748
909
|
}
|
|
749
910
|
}
|
|
@@ -763,7 +924,12 @@ const CLJ_RECOGNIZED_TOPLEVEL = new Set([
|
|
|
763
924
|
/** Walk a Clojure AST and extract defines, calls, imports */
|
|
764
925
|
function walkClojure(rootNode, filePath, defines, calls, imports, exports, callSet) {
|
|
765
926
|
let nsName = null;
|
|
927
|
+
const aliasMap = new Map();
|
|
766
928
|
const definesBeforePhase1 = defines.length;
|
|
929
|
+
// Phase 1 pushes bare names and records the boundaries; after phase 1
|
|
930
|
+
// finishes we'll rewrite them in-place to qualified form once we know the
|
|
931
|
+
// final ns. Deferring the rewrite avoids having to know the ns before
|
|
932
|
+
// seeing the (ns ...) form, which can appear anywhere in the file.
|
|
767
933
|
// ── Phase 1: collect top-level definitions ────────────────────────
|
|
768
934
|
// In addition to defn/defn-, we also register defmulti, defprotocol
|
|
769
935
|
// (and its declared methods), defrecord, deftype, definterface,
|
|
@@ -773,9 +939,9 @@ function walkClojure(rootNode, filePath, defines, calls, imports, exports, callS
|
|
|
773
939
|
const child = rootNode.child(i);
|
|
774
940
|
if (child.type !== "list_lit")
|
|
775
941
|
continue;
|
|
776
|
-
// ns form — harvest requires and
|
|
777
|
-
// as the top-level caller in phase 2.
|
|
778
|
-
const ns = cljExtractNs(child, filePath, imports);
|
|
942
|
+
// ns form — harvest requires, :as aliases, and the namespace name for
|
|
943
|
+
// use as the top-level caller in phase 2.
|
|
944
|
+
const ns = cljExtractNs(child, filePath, imports, aliasMap);
|
|
779
945
|
if (ns) {
|
|
780
946
|
nsName = ns;
|
|
781
947
|
continue;
|
|
@@ -848,21 +1014,53 @@ function walkClojure(rootNode, filePath, defines, calls, imports, exports, callS
|
|
|
848
1014
|
}
|
|
849
1015
|
}
|
|
850
1016
|
}
|
|
1017
|
+
// Post-phase-1 qualification: now that the (ns ...) form has been seen
|
|
1018
|
+
// (or confirmed absent), rewrite each define pushed during phase 1 to
|
|
1019
|
+
// its namespace-qualified form. `defprotocol`/`definterface` method rows
|
|
1020
|
+
// (kind === "function" nested inside a class define) are qualified too —
|
|
1021
|
+
// they look identical to defn from the graph's perspective. Classes
|
|
1022
|
+
// (defrecord/deftype/definterface) are also qualified.
|
|
1023
|
+
//
|
|
1024
|
+
// If nsName is null the file is in legacy "bare" mode and names stay as
|
|
1025
|
+
// they were pushed — this keeps script-style .clj fixtures working.
|
|
1026
|
+
if (nsName !== null) {
|
|
1027
|
+
for (let k = definesBeforePhase1; k < defines.length; k++) {
|
|
1028
|
+
defines[k].name = `${nsName}/${defines[k].name}`;
|
|
1029
|
+
}
|
|
1030
|
+
// Exports were pushed alongside defines in phase 1. They were appended
|
|
1031
|
+
// after definesBeforePhase1 as we went, but exports and defines are
|
|
1032
|
+
// separate arrays — rewrite exports for this file by walking from the
|
|
1033
|
+
// pre-phase-1 length of the exports array.
|
|
1034
|
+
// We don't have a snapshot of the export length, so scan the tail for
|
|
1035
|
+
// entries belonging to this file and qualify those whose name still
|
|
1036
|
+
// looks bare.
|
|
1037
|
+
for (let k = exports.length - 1; k >= 0 && exports[k].file === filePath; k--) {
|
|
1038
|
+
if (!exports[k].name.includes("/")) {
|
|
1039
|
+
exports[k].name = `${nsName}/${exports[k].name}`;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
851
1043
|
// Snapshot the set of names defined in *this file* during phase 1.
|
|
852
1044
|
// Phase 2's cljExtractCalls uses this to recognize in-file references:
|
|
853
1045
|
// whenever it encounters a sym_lit whose name matches one of these,
|
|
854
|
-
// it emits a reference edge.
|
|
855
|
-
//
|
|
856
|
-
// patterns where a fn is passed by value rather than called directly.
|
|
1046
|
+
// it emits a reference edge. Names here are already qualified if the
|
|
1047
|
+
// file had an ns form.
|
|
857
1048
|
const inFileDefns = new Set();
|
|
858
1049
|
for (let k = definesBeforePhase1; k < defines.length; k++) {
|
|
859
1050
|
inFileDefns.add(defines[k].name);
|
|
860
1051
|
}
|
|
1052
|
+
const ctx = { currentNs: nsName, aliasMap, inFileDefns };
|
|
861
1053
|
// Synthetic caller for top-level side-effecting forms. If the file has
|
|
862
1054
|
// no ns declaration, fall back to the file path — it's still a unique
|
|
863
1055
|
// identifier that downstream analyses can treat as "always live".
|
|
864
1056
|
const topLevelCaller = nsName ?? `<toplevel:${filePath}>`;
|
|
865
1057
|
// ── Phase 2: walk bodies for call edges ──────────────────────────
|
|
1058
|
+
//
|
|
1059
|
+
// Phase-2 callers are qualified whenever the file has an ns form: `defn
|
|
1060
|
+
// foo` in `ns myapp.core` becomes the caller `myapp.core/foo`. This
|
|
1061
|
+
// matches the (already rewritten) entries in `defines` so cross-file
|
|
1062
|
+
// analyses see a consistent graph.
|
|
1063
|
+
const qualifyCaller = (bare) => nsName !== null ? `${nsName}/${bare}` : bare;
|
|
866
1064
|
for (let i = 0; i < rootNode.childCount; i++) {
|
|
867
1065
|
const child = rootNode.child(i);
|
|
868
1066
|
if (child.type !== "list_lit")
|
|
@@ -871,7 +1069,7 @@ function walkClojure(rootNode, filePath, defines, calls, imports, exports, callS
|
|
|
871
1069
|
if (!head) {
|
|
872
1070
|
// Non-symbolic head (keyword-first, map-first, etc.) at top level.
|
|
873
1071
|
// Still walk it as file-level init — unusual but possible.
|
|
874
|
-
cljExtractCalls(child, topLevelCaller,
|
|
1072
|
+
cljExtractCalls(child, topLevelCaller, ctx, calls, callSet);
|
|
875
1073
|
continue;
|
|
876
1074
|
}
|
|
877
1075
|
switch (head.name) {
|
|
@@ -883,7 +1081,7 @@ function walkClojure(rootNode, filePath, defines, calls, imports, exports, callS
|
|
|
883
1081
|
case "deftest": {
|
|
884
1082
|
const name = cljNextSymNameAfter(child, head.symIdx);
|
|
885
1083
|
if (name)
|
|
886
|
-
cljExtractCalls(child, name,
|
|
1084
|
+
cljExtractCalls(child, qualifyCaller(name), ctx, calls, callSet);
|
|
887
1085
|
break;
|
|
888
1086
|
}
|
|
889
1087
|
case "defmulti": {
|
|
@@ -893,7 +1091,7 @@ function walkClojure(rootNode, filePath, defines, calls, imports, exports, callS
|
|
|
893
1091
|
// named. Attribute to the multi name.
|
|
894
1092
|
const name = cljNextSymNameAfter(child, head.symIdx);
|
|
895
1093
|
if (name)
|
|
896
|
-
cljExtractCalls(child, name,
|
|
1094
|
+
cljExtractCalls(child, qualifyCaller(name), ctx, calls, callSet);
|
|
897
1095
|
break;
|
|
898
1096
|
}
|
|
899
1097
|
case "defmethod": {
|
|
@@ -902,14 +1100,14 @@ function walkClojure(rootNode, filePath, defines, calls, imports, exports, callS
|
|
|
902
1100
|
// is recorded as "called by" the multi and won't look dead.
|
|
903
1101
|
const multiName = cljNextSymNameAfter(child, head.symIdx);
|
|
904
1102
|
if (multiName)
|
|
905
|
-
cljExtractCalls(child, multiName,
|
|
1103
|
+
cljExtractCalls(child, qualifyCaller(multiName), ctx, calls, callSet);
|
|
906
1104
|
break;
|
|
907
1105
|
}
|
|
908
1106
|
case "defrecord":
|
|
909
1107
|
case "deftype":
|
|
910
1108
|
case "extend-type":
|
|
911
1109
|
case "extend-protocol": {
|
|
912
|
-
cljWalkDispatchMethods(child,
|
|
1110
|
+
cljWalkDispatchMethods(child, ctx, calls, callSet);
|
|
913
1111
|
break;
|
|
914
1112
|
}
|
|
915
1113
|
default: {
|
|
@@ -917,7 +1115,7 @@ function walkClojure(rootNode, filePath, defines, calls, imports, exports, callS
|
|
|
917
1115
|
// ns name as caller. Catches use-fixtures, (def x (compute)),
|
|
918
1116
|
// (require '[...]), raw println calls, etc.
|
|
919
1117
|
if (!CLJ_RECOGNIZED_TOPLEVEL.has(head.name)) {
|
|
920
|
-
cljExtractCalls(child, topLevelCaller,
|
|
1118
|
+
cljExtractCalls(child, topLevelCaller, ctx, calls, callSet);
|
|
921
1119
|
}
|
|
922
1120
|
break;
|
|
923
1121
|
}
|
|
@@ -1017,12 +1215,20 @@ const CLJ_RECURSE_TYPES = new Set([
|
|
|
1017
1215
|
* site: emit its head as a callee, then apply HOF / threading-macro
|
|
1018
1216
|
* reference rules to its arguments.
|
|
1019
1217
|
*/
|
|
1020
|
-
function cljProcessCallSite(listLike, enclosingFn, calls, callSet) {
|
|
1218
|
+
function cljProcessCallSite(listLike, enclosingFn, ctx, calls, callSet) {
|
|
1021
1219
|
const emit = (calleeRaw) => {
|
|
1022
|
-
|
|
1023
|
-
if (!callee || callee === enclosingFn)
|
|
1220
|
+
if (!calleeRaw)
|
|
1024
1221
|
return;
|
|
1025
|
-
|
|
1222
|
+
// Special-form filtering happens on the bare tail: `let` is a special
|
|
1223
|
+
// form whether written as `let` or `some.ns/let` (the latter is
|
|
1224
|
+
// degenerate but the filter shouldn't care). The aliased test lets
|
|
1225
|
+
// us shed def/let/if/etc. before paying for resolution.
|
|
1226
|
+
const slashIdx = calleeRaw.indexOf("/");
|
|
1227
|
+
const bareTail = slashIdx >= 0 ? calleeRaw.slice(slashIdx + 1) : calleeRaw;
|
|
1228
|
+
if (slashIdx < 0 && CLJ_SPECIAL_FORMS.has(bareTail))
|
|
1229
|
+
return;
|
|
1230
|
+
const callee = qualifyCljName(calleeRaw, ctx);
|
|
1231
|
+
if (!callee || callee === enclosingFn)
|
|
1026
1232
|
return;
|
|
1027
1233
|
const key = `${enclosingFn}->${callee}`;
|
|
1028
1234
|
if (callSet.has(key))
|
|
@@ -1094,33 +1300,33 @@ function cljProcessCallSite(listLike, enclosingFn, calls, callSet) {
|
|
|
1094
1300
|
* as its own call site via the same top-of-function check.
|
|
1095
1301
|
*
|
|
1096
1302
|
* Every sym_lit encountered at any depth is also checked against
|
|
1097
|
-
* `inFileDefns`: if its
|
|
1098
|
-
* an edge is emitted. This covers in-file references that
|
|
1099
|
-
* list-head position — e.g. fns passed to user-defined HOFs,
|
|
1100
|
-
* in map literals, `(def h my-fn)`, and registration-style
|
|
1101
|
-
* `(reg-event-fx :k handler)`.
|
|
1303
|
+
* `ctx.inFileDefns`: if its qualified form matches a definition in the
|
|
1304
|
+
* current file, an edge is emitted. This covers in-file references that
|
|
1305
|
+
* aren't at list-head position — e.g. fns passed to user-defined HOFs,
|
|
1306
|
+
* fn values in map literals, `(def h my-fn)`, and registration-style
|
|
1307
|
+
* calls like `(reg-event-fx :k handler)`.
|
|
1102
1308
|
*/
|
|
1103
|
-
function cljExtractCalls(node, enclosingFn,
|
|
1309
|
+
function cljExtractCalls(node, enclosingFn, ctx, calls, callSet) {
|
|
1104
1310
|
if (node.type === "list_lit" || node.type === "anon_fn_lit") {
|
|
1105
|
-
cljProcessCallSite(node, enclosingFn, calls, callSet);
|
|
1311
|
+
cljProcessCallSite(node, enclosingFn, ctx, calls, callSet);
|
|
1106
1312
|
}
|
|
1107
1313
|
for (let i = 0; i < node.childCount; i++) {
|
|
1108
1314
|
const child = node.child(i);
|
|
1109
1315
|
if (child.type === "sym_lit") {
|
|
1110
1316
|
const name = cljSymName(child);
|
|
1111
1317
|
if (name) {
|
|
1112
|
-
const
|
|
1113
|
-
if (
|
|
1114
|
-
const key = `${enclosingFn}->${
|
|
1318
|
+
const qualified = qualifyCljName(name, ctx);
|
|
1319
|
+
if (qualified && qualified !== enclosingFn && ctx.inFileDefns.has(qualified)) {
|
|
1320
|
+
const key = `${enclosingFn}->${qualified}`;
|
|
1115
1321
|
if (!callSet.has(key)) {
|
|
1116
1322
|
callSet.add(key);
|
|
1117
|
-
calls.push({ caller: enclosingFn, callee:
|
|
1323
|
+
calls.push({ caller: enclosingFn, callee: qualified });
|
|
1118
1324
|
}
|
|
1119
1325
|
}
|
|
1120
1326
|
}
|
|
1121
1327
|
}
|
|
1122
1328
|
if (CLJ_RECURSE_TYPES.has(child.type)) {
|
|
1123
|
-
cljExtractCalls(child, enclosingFn,
|
|
1329
|
+
cljExtractCalls(child, enclosingFn, ctx, calls, callSet);
|
|
1124
1330
|
}
|
|
1125
1331
|
}
|
|
1126
1332
|
}
|