devlensio 1.0.4 → 1.1.1

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.
Files changed (73) hide show
  1. package/dist/extractors/runner.js +1 -0
  2. package/dist/extractors/types.d.ts +2 -1
  3. package/dist/graph/edges/callEdges.js +97 -5
  4. package/dist/graph/edges/callEdges.test.js +154 -0
  5. package/dist/parser/exportsMap.d.ts +9 -0
  6. package/dist/parser/exportsMap.js +349 -0
  7. package/dist/parser/exportsMap.test.d.ts +1 -0
  8. package/dist/parser/exportsMap.test.js +324 -0
  9. package/dist/parser/extractors/classes.js +23 -4
  10. package/dist/parser/extractors/functions.d.ts +10 -3
  11. package/dist/parser/extractors/functions.js +135 -65
  12. package/dist/parser/extractors/hooks.js +5 -29
  13. package/dist/parser/extractors/objectMethods.js +3 -4
  14. package/dist/parser/index.d.ts +2 -1
  15. package/dist/parser/index.js +44 -6
  16. package/dist/parser/overloads.d.ts +2 -0
  17. package/dist/parser/overloads.js +96 -0
  18. package/dist/parser/overloads.test.d.ts +1 -0
  19. package/dist/parser/overloads.test.js +175 -0
  20. package/dist/parser/typeUtils.d.ts +2 -0
  21. package/dist/parser/typeUtils.js +2 -0
  22. package/dist/pipeline/index.d.ts +2 -1
  23. package/dist/pipeline/index.js +1 -0
  24. package/dist/storage/fileStorage.d.ts +2 -1
  25. package/dist/storage/fileStorage.js +5 -1
  26. package/dist/storage/fileStorage.test.d.ts +1 -0
  27. package/dist/storage/fileStorage.test.js +112 -0
  28. package/dist/types.d.ts +10 -0
  29. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  30. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  31. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  32. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  33. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  34. package/extractors/go/calls.go +134 -2
  35. package/extractors/go/contract.go +1 -0
  36. package/extractors/go/exports.go +107 -0
  37. package/extractors/go/exports_test.go +64 -0
  38. package/extractors/go/extractor.go +1 -0
  39. package/extractors/go/lookup.go +63 -7
  40. package/extractors/go/nodes.go +28 -0
  41. package/extractors/go/parser.go +51 -8
  42. package/extractors/java/devlens_java_extractor.jar +0 -0
  43. package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
  44. package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
  45. package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
  46. package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
  47. package/extractors/java/src/devlens/extractor/Parser.java +39 -0
  48. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
  49. package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
  50. package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
  51. package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
  52. package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
  53. package/extractors/python/devlens_extractors_python/extractor.py +7 -1
  54. package/extractors/python/devlens_extractors_python/lookup.py +6 -0
  55. package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
  56. package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
  57. package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -0
  58. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  59. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  60. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  61. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  62. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  63. package/extractors/rust/src/calls.rs +27 -16
  64. package/extractors/rust/src/contract.rs +2 -0
  65. package/extractors/rust/src/exports.rs +387 -0
  66. package/extractors/rust/src/exports_test.rs +105 -0
  67. package/extractors/rust/src/extractor.rs +2 -0
  68. package/extractors/rust/src/lookup.rs +60 -0
  69. package/extractors/rust/src/main.rs +4 -0
  70. package/extractors/rust/src/nodes.rs +19 -0
  71. package/extractors/rust/src/parser.rs +66 -1
  72. package/extractors/rust/src/routes.rs +2 -1
  73. package/package.json +1 -1
@@ -167,6 +167,7 @@ export async function runInlineExtractor(input, onStep) {
167
167
  nodes: allNodes,
168
168
  edges: allEdges,
169
169
  routes,
170
+ exports: parserResult.exports,
170
171
  stats,
171
172
  errors: [],
172
173
  };
@@ -1,4 +1,4 @@
1
- import { Language, BackendRouteNode, CodeEdge, CodeNode, ProjectFingerprint, RouteNode } from "../types.js";
1
+ import { Language, BackendRouteNode, CodeEdge, CodeNode, ProjectFingerprint, RouteNode, GraphExports } from "../types.js";
2
2
  export interface ExtractorOptions {
3
3
  includeThirdPartyLibs?: string[];
4
4
  }
@@ -20,6 +20,7 @@ export interface ExtractorResult {
20
20
  nodes: CodeNode[];
21
21
  edges: CodeEdge[];
22
22
  routes?: (RouteNode | BackendRouteNode)[];
23
+ exports?: GraphExports;
23
24
  stats: ExtractorStats;
24
25
  errors: ExtractorError[];
25
26
  }
@@ -1,8 +1,89 @@
1
1
  import { closestByPath } from "./utils.js";
2
+ // Arity compatibility: a candidate matches when the call provides at least all
3
+ // required (non-optional, non-rest) params, and does not exceed the declared
4
+ // count unless the candidate declares a rest parameter. Candidates without
5
+ // parameters metadata are permissive (unknown shape).
6
+ function arityMatches(candidate, argCount) {
7
+ const parameters = candidate.metadata?.parameters;
8
+ const params = candidate.metadata?.params;
9
+ if (!params && !parameters)
10
+ return true;
11
+ const total = parameters?.length ?? params?.length ?? 0;
12
+ const hasRest = parameters?.some((p) => p.isRest) ?? false;
13
+ const required = parameters
14
+ ? parameters.filter((p) => !p.isOptional && !p.isRest).length
15
+ : total;
16
+ return argCount >= required && (hasRest || argCount <= total);
17
+ }
18
+ // Loose type agreement between a call-site arg type tag and a declared param
19
+ // type text. Both sides are cheap/heuristic (no type checker), so this scores
20
+ // rather than gates: unknown tags never match, text containment counts.
21
+ function typeAgrees(argType, paramType) {
22
+ if (!argType || argType === "unknown" || !paramType)
23
+ return false;
24
+ const p = paramType.trim();
25
+ return p === argType || p.includes(argType);
26
+ }
27
+ function typeScore(candidate, argTypes) {
28
+ const parameters = candidate.metadata?.parameters;
29
+ if (!parameters)
30
+ return 0;
31
+ let score = 0;
32
+ for (let i = 0; i < argTypes.length; i++) {
33
+ if (parameters[i] && typeAgrees(argTypes[i], parameters[i].type))
34
+ score++;
35
+ }
36
+ return score;
37
+ }
38
+ // Picks the right overload among same-name candidates using the call site's
39
+ // argument shape. Ladder:
40
+ // 1. no usable call shape (USES edges, spread calls, missing callSites)
41
+ // → legacy closestByPath behavior
42
+ // 2. arity filter — survivors whose declared params accept argCount
43
+ // 3. type tiebreak — among arity survivors, prefer the candidate with the
44
+ // highest arg-type agreement (handles same arity, different types)
45
+ // 4. still ambiguous → closestByPath among survivors
46
+ function resolveOverloadTarget(candidates, caller, callSite) {
47
+ if (candidates.length === 1)
48
+ return { target: candidates[0], matchedBy: "name" };
49
+ const usableShape = callSite
50
+ && typeof callSite.argCount === "number"
51
+ && !callSite.hasSpread;
52
+ if (!usableShape)
53
+ return { target: closestByPath(candidates, caller.filePath), matchedBy: "name" };
54
+ let pool = candidates.filter((c) => arityMatches(c, callSite.argCount));
55
+ if (pool.length === 0)
56
+ pool = candidates; // nothing accepts this arity — keep all
57
+ if (pool.length === 1)
58
+ return { target: pool[0], matchedBy: "arity" };
59
+ const argTypes = callSite.argTypes ?? [];
60
+ if (argTypes.some((t) => t && t !== "unknown")) {
61
+ let best = pool[0];
62
+ let bestScore = typeScore(pool[0], argTypes);
63
+ let tieAtBest = 1;
64
+ for (let i = 1; i < pool.length; i++) {
65
+ const s = typeScore(pool[i], argTypes);
66
+ if (s > bestScore) {
67
+ best = pool[i];
68
+ bestScore = s;
69
+ tieAtBest = 1;
70
+ }
71
+ else if (s === bestScore)
72
+ tieAtBest++;
73
+ }
74
+ if (bestScore > 0 && tieAtBest === 1) {
75
+ return { target: best, matchedBy: "signature" };
76
+ }
77
+ }
78
+ return { target: closestByPath(pool, caller.filePath), matchedBy: "name" };
79
+ }
2
80
  export function detectCallEdges(nodes, lookupMp) {
3
81
  const edges = [];
4
82
  // Dedup for THIRD_PARTY CALLS edges — one per (caller, target) pair
5
83
  const createdThirdPartyEdges = new Set();
84
+ // Dedup for local CALLS/USES edges — overload call sites can resolve to
85
+ // the same target through different (name, arity) pairs
86
+ const createdLocalEdges = new Set();
6
87
  // Accumulate resolved calls per node — written back to metadata after edge loop
7
88
  const resolvedCallsMap = new Map();
8
89
  // Lazily-created method nodes for default/namespace import member-access calls
@@ -34,7 +115,16 @@ export function detectCallEdges(nodes, lookupMp) {
34
115
  if (!hasPrimary && !hasHooks)
35
116
  continue;
36
117
  // ── Primary names: both third-party and local edges ──────────────
37
- for (const calledName of (primaryNames ?? [])) {
118
+ // Structured callSites carry the argument shape for overload-aware
119
+ // resolution; fall back to bare calls/uses strings when absent.
120
+ const callSites = node.metadata.callSites;
121
+ // Each (name, arity) call site iterates separately — a caller that
122
+ // invokes two overloads of one name gets two edges.
123
+ const primaryCallSites = (callSites && callSites.length > 0)
124
+ ? callSites
125
+ : (primaryNames ?? []).map((name) => ({ name, argCount: -1 }));
126
+ for (const callSite of primaryCallSites) {
127
+ const calledName = callSite.name;
38
128
  // ── Third-party guard ─────────────────────────────────────────
39
129
  // The alias map is keyed by node.filePath (relative) and populated
40
130
  // by importEdges.ts (which runs first).
@@ -120,16 +210,18 @@ export function detectCallEdges(nodes, lookupMp) {
120
210
  }
121
211
  if (!targets || targets.length === 0)
122
212
  continue;
123
- const target = targets.length === 1
124
- ? targets[0]
125
- : closestByPath(targets, node.filePath);
213
+ const { target, matchedBy } = resolveOverloadTarget(targets, node, callSite.argCount >= 0 ? callSite : undefined);
126
214
  if (target.id === node.id)
127
215
  continue; // skip self-reference
216
+ const localEdgeKey = `${node.id}→${target.id}:${edgeType}`;
217
+ if (createdLocalEdges.has(localEdgeKey))
218
+ continue;
219
+ createdLocalEdges.add(localEdgeKey);
128
220
  edges.push({
129
221
  from: node.id,
130
222
  to: target.id,
131
223
  type: edgeType,
132
- metadata: { calledName: resolvedName },
224
+ metadata: { calledName: resolvedName, matchedBy },
133
225
  });
134
226
  if (!resolvedCallsMap.has(node.id))
135
227
  resolvedCallsMap.set(node.id, []);
@@ -198,3 +198,157 @@ describe("detectCallEdges with class methods", () => {
198
198
  deleteFakeRepo(repo);
199
199
  });
200
200
  });
201
+ // ─── Overload-aware resolution (same-name candidates in one file) ─────────────
202
+ describe("detectCallEdges with overloads", () => {
203
+ // Arity disambiguates same-name functions in one file
204
+ it("routes each call to the overload matching its argument count", () => {
205
+ const repo = createFakeRepo({
206
+ "src/fmt.ts": `
207
+ export function formatValue(v: string) { return v.trim(); }
208
+ export function formatValue(v: string, n: number) { return v.repeat(n); }
209
+ export function caller() {
210
+ formatValue("a");
211
+ formatValue("a", 3);
212
+ }
213
+ `,
214
+ });
215
+ const { edges } = detectCalls(repo);
216
+ const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "formatValue");
217
+ const targets = callEdges.map((e) => e.to).sort();
218
+ expect(targets).toEqual([
219
+ "src/fmt.ts::formatValue#1",
220
+ "src/fmt.ts::formatValue#2",
221
+ ]);
222
+ // resolution confidence recorded on the edge
223
+ expect(callEdges.every((e) => e.metadata?.matchedBy === "arity")).toBe(true);
224
+ deleteFakeRepo(repo);
225
+ });
226
+ // Same arity, different param types — literal arg types break the tie
227
+ it("resolves same-arity overloads via argument type tags", () => {
228
+ const repo = createFakeRepo({
229
+ "src/dec.ts": `
230
+ export function decode(input: string, mode: string) { return input; }
231
+ export function decode(input: number, mode: string) { return String(input); }
232
+ export function caller() {
233
+ decode("abc", "utf8");
234
+ decode(42, "utf8");
235
+ }
236
+ `,
237
+ });
238
+ const { edges } = detectCalls(repo);
239
+ const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "decode");
240
+ // first call (string literal) → string overload; second (number literal) → number overload
241
+ const stringTargets = callEdges.filter((e) => e.metadata?.matchedBy === "signature").map((e) => e.to).sort();
242
+ expect(stringTargets.length).toBe(2);
243
+ expect(stringTargets[0]).not.toBe(stringTargets[1]);
244
+ deleteFakeRepo(repo);
245
+ });
246
+ // A caller can invoke two overloads of one name — two distinct edges
247
+ it("emits one edge per overload call site, not one per name", () => {
248
+ const repo = createFakeRepo({
249
+ "src/multi.ts": `
250
+ export function send(x: string) { return x; }
251
+ export function send(x: string, y: string) { return x + y; }
252
+ export function client() {
253
+ send("a");
254
+ send("a", "b");
255
+ }
256
+ `,
257
+ });
258
+ const { edges } = detectCalls(repo);
259
+ const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "send");
260
+ expect(callEdges.length).toBe(2);
261
+ expect(new Set(callEdges.map((e) => e.to)).size).toBe(2);
262
+ deleteFakeRepo(repo);
263
+ });
264
+ // Spread calls have unknowable arity — must NOT confidently pick an overload
265
+ it("falls back to path proximity for spread calls", () => {
266
+ const repo = createFakeRepo({
267
+ "src/spread.ts": `
268
+ export function combine(a: string) { return a; }
269
+ export function combine(a: string, b: string) { return a + b; }
270
+ export function caller(...args: string[]) {
271
+ return combine(...args);
272
+ }
273
+ `,
274
+ });
275
+ const { edges } = detectCalls(repo);
276
+ const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "combine");
277
+ expect(callEdges.length).toBe(1);
278
+ expect(callEdges[0].metadata?.matchedBy).toBe("name");
279
+ deleteFakeRepo(repo);
280
+ });
281
+ // Optional params: a 1-arg call may hit a 2-param overload with optional 2nd
282
+ it("lets optional parameters accept shorter calls via arity fallback", () => {
283
+ const repo = createFakeRepo({
284
+ "src/opt.ts": `
285
+ export function render(template: string, opts?: object) { return template; }
286
+ export function render(template: string, opts: object, mode: string) { return template; }
287
+ export function caller() {
288
+ render("t");
289
+ }
290
+ `,
291
+ });
292
+ const { edges } = detectCalls(repo);
293
+ const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "render");
294
+ // 1-arg call: renderStrict requires 3 → excluded; render (optional) → match
295
+ expect(callEdges.length).toBe(1);
296
+ expect(callEdges[0].to).toBe("src/opt.ts::render#2");
297
+ deleteFakeRepo(repo);
298
+ });
299
+ // Rest params accept any extra arguments
300
+ it("lets rest parameters accept longer calls", () => {
301
+ const repo = createFakeRepo({
302
+ "src/rest.ts": `
303
+ export function sum(first: number, ...rest: number[]) { return first; }
304
+ export function pair(a: number, b: number) { return a; }
305
+ export function caller() {
306
+ sum(1, 2, 3, 4);
307
+ }
308
+ `,
309
+ });
310
+ const { edges } = detectCalls(repo);
311
+ const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "sum");
312
+ expect(callEdges.length).toBe(1);
313
+ expect(callEdges[0].to).toContain("sum");
314
+ deleteFakeRepo(repo);
315
+ });
316
+ // Legacy behavior preserved when no overload shape exists
317
+ it("still resolves plain single-candidate calls without callSites metadata", () => {
318
+ const repo = createFakeRepo({
319
+ "src/plain.ts": `
320
+ export function helper(x: number) { return x; }
321
+ export function caller() { return helper(1); }
322
+ `,
323
+ });
324
+ const { edges } = detectCalls(repo);
325
+ expect(edges).toContainEqual(expect.objectContaining({
326
+ from: "src/plain.ts::caller",
327
+ to: "src/plain.ts::helper",
328
+ type: "CALLS",
329
+ }));
330
+ deleteFakeRepo(repo);
331
+ });
332
+ // Class method overloads resolve through dotted names
333
+ it("resolves class method overloads by arity through dotted names", () => {
334
+ const repo = createFakeRepo({
335
+ "src/svc.ts": `
336
+ export class Repo {
337
+ find(id: string) { return id; }
338
+ find(id: string, depth: number) { return id; }
339
+ }
340
+ export function caller() {
341
+ Repo.find("a");
342
+ Repo.find("a", 2);
343
+ }
344
+ `,
345
+ });
346
+ const { edges } = detectCalls(repo);
347
+ const callEdges = edges.filter((e) => e.type === "CALLS" && e.metadata?.calledName === "Repo.find");
348
+ expect(callEdges.map((e) => e.to).sort()).toEqual([
349
+ "src/svc.ts::Repo.find#1",
350
+ "src/svc.ts::Repo.find#2",
351
+ ]);
352
+ deleteFakeRepo(repo);
353
+ });
354
+ });
@@ -0,0 +1,9 @@
1
+ import type { Project } from "ts-morph";
2
+ import type { CodeNode, GraphExports } from "../types.js";
3
+ export declare function buildExportsMap(project: Project, repoPath: string, nodes: CodeNode[]): GraphExports | undefined;
4
+ export declare function validateExportsMap(map: GraphExports, nodeIds: Set<string>): string[];
5
+ export declare function exportsMapStats(map: GraphExports): {
6
+ subpaths: number;
7
+ names: number;
8
+ ambiguous: number;
9
+ };
@@ -0,0 +1,349 @@
1
+ // Exports Map builder — the flattened export surface of the repo, shipped in
2
+ // the graph artifact for future cross-repo linking (see docs/EXPORTS_MAP.md).
3
+ //
4
+ // Architecture notes:
5
+ // - Must live in the parser because binding resolution needs the ts-morph
6
+ // Project created inside parseRepo(); a second Project would double parse
7
+ // cost. parseRepo() calls buildExportsMap() after the node loop, so the
8
+ // map is validated against the FINAL node list (only surviving, deduped,
9
+ // overload-suffixed ids can appear as values).
10
+ // - Values are ALWAYS arrays of proper nodeIds — never bare names. The
11
+ // cloud joins against them in one hash lookup; no second resolution step.
12
+ // - Overloads: multiple nodes sharing one base id (path::name) are an
13
+ // overload group — emitted as a plain array. Distinct base ids under one
14
+ // exported name are a TRUE ambiguity — also flagged in ambiguousNames.
15
+ // - TS resolves `export *` conflicts by EXCLUDING the name from the module's
16
+ // exports (same as the JS spec), so star re-exports need no special code.
17
+ // - Anything without a node representation (plain consts, interfaces,
18
+ // type aliases, anonymous defaults, namespace re-exports) is OMITTED —
19
+ // never fabricate an id that won't exist in the nodes array.
20
+ // - package.json "exports" conditions: v1 resolves one condition per subpath
21
+ // (import → default → require); "types"-only subpaths are skipped; wildcard
22
+ // patterns are skipped. Entry targets pointing into dist/build are mapped
23
+ // back to src/ so ids stay in the same coordinate system as the nodes.
24
+ // - Failures never break analysis: any error yields an absent map.
25
+ import path from "path";
26
+ import fs from "fs";
27
+ const ENTRY_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
28
+ const INDEX_NAMES = ["index.ts", "index.tsx", "index.js", "index.jsx"];
29
+ export function buildExportsMap(project, repoPath, nodes) {
30
+ try {
31
+ const pkgPath = path.join(repoPath, "package.json");
32
+ if (!fs.existsSync(pkgPath))
33
+ return undefined;
34
+ let pkg;
35
+ try {
36
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ const entries = resolveEntryPoints(pkg, repoPath, project);
42
+ if (entries.length === 0)
43
+ return undefined;
44
+ const nodeIndex = buildNodeIndex(nodes);
45
+ const starCache = new Map();
46
+ const exports = {};
47
+ const ambiguousNames = {};
48
+ for (const entry of entries) {
49
+ let decls;
50
+ try {
51
+ decls = entry.file.getExportedDeclarations();
52
+ }
53
+ catch {
54
+ continue;
55
+ }
56
+ const explicitNames = explicitExportNames(entry.file);
57
+ const entryPath = entry.file.getFilePath();
58
+ const stars = starContributors(entry.file, starCache);
59
+ const perPath = {};
60
+ const ambiguous = [];
61
+ for (const [name, declList] of decls) {
62
+ const isExplicit = explicitNames.has(name) || declList.some((d) => d.getSourceFile().getFilePath() === entryPath);
63
+ if (!isExplicit) {
64
+ const contributors = stars.get(name);
65
+ if (contributors && contributors.size > 1) {
66
+ ambiguous.push(name);
67
+ continue;
68
+ }
69
+ }
70
+ const resolved = [];
71
+ for (const decl of declList) {
72
+ const hit = resolveDeclarationToNode(decl, repoPath, nodeIndex);
73
+ if (hit)
74
+ resolved.push(hit);
75
+ }
76
+ const ids = [...new Set(resolved.map((r) => r.id))].sort();
77
+ if (ids.length === 0)
78
+ continue;
79
+ const bases = [...new Set(resolved.map((r) => r.base))];
80
+ if (bases.length > 1)
81
+ ambiguous.push(name);
82
+ perPath[name] = ids;
83
+ }
84
+ if (Object.keys(perPath).length > 0)
85
+ exports[entry.subpath] = perPath;
86
+ if (ambiguous.length > 0)
87
+ ambiguousNames[entry.subpath] = ambiguous;
88
+ }
89
+ if (Object.keys(exports).length === 0)
90
+ return undefined;
91
+ return { exports, ambiguousNames };
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ }
97
+ function explicitExportNames(sf) {
98
+ const names = new Set();
99
+ for (const ed of sf.getExportDeclarations()) {
100
+ for (const spec of ed.getNamedExports())
101
+ names.add(spec.getName());
102
+ }
103
+ return names;
104
+ }
105
+ function starContributors(sf, cache) {
106
+ const key = sf.getFilePath();
107
+ const cached = cache.get(key);
108
+ if (cached)
109
+ return cached;
110
+ const result = new Map();
111
+ const seen = new Set([key]);
112
+ const stack = [sf];
113
+ while (stack.length > 0) {
114
+ const current = stack.pop();
115
+ for (const ed of current.getExportDeclarations()) {
116
+ if (ed.getNamedExports().length > 0)
117
+ continue;
118
+ const target = ed.getModuleSpecifierSourceFile();
119
+ if (!target || seen.has(target.getFilePath()))
120
+ continue;
121
+ seen.add(target.getFilePath());
122
+ const targetPath = target.getFilePath();
123
+ let targetDecls;
124
+ try {
125
+ targetDecls = target.getExportedDeclarations();
126
+ }
127
+ catch {
128
+ continue;
129
+ }
130
+ for (const [name, declList] of targetDecls) {
131
+ const isLocal = declList.some((d) => d.getSourceFile().getFilePath() === targetPath);
132
+ if (isLocal) {
133
+ if (!result.has(name))
134
+ result.set(name, new Set());
135
+ result.get(name).add(targetPath);
136
+ }
137
+ else {
138
+ const deeper = starContributors(target, cache);
139
+ const contributors = deeper.get(name);
140
+ if (contributors) {
141
+ if (!result.has(name))
142
+ result.set(name, new Set());
143
+ for (const f of contributors)
144
+ result.get(name).add(f);
145
+ }
146
+ }
147
+ }
148
+ stack.push(target);
149
+ }
150
+ }
151
+ cache.set(key, result);
152
+ return result;
153
+ }
154
+ function buildNodeIndex(nodes) {
155
+ const index = new Map();
156
+ for (const node of nodes) {
157
+ const base = `${node.filePath}::${node.name}`;
158
+ if (!index.has(base))
159
+ index.set(base, []);
160
+ index.get(base).push(node);
161
+ }
162
+ return index;
163
+ }
164
+ function resolveEntryPoints(pkg, repoPath, project) {
165
+ const entries = [];
166
+ if (pkg.exports !== undefined) {
167
+ const normalized = normalizeExportsField(pkg.exports);
168
+ for (const [subpath, target] of normalized) {
169
+ const file = resolveTargetFile(repoPath, target, project);
170
+ if (file)
171
+ entries.push({ subpath, file });
172
+ }
173
+ return entries;
174
+ }
175
+ const fallback = pkg.main ?? pkg.module;
176
+ if (fallback) {
177
+ const file = resolveTargetFile(repoPath, fallback, project);
178
+ if (file)
179
+ entries.push({ subpath: ".", file });
180
+ }
181
+ else {
182
+ for (const candidate of INDEX_NAMES.map((n) => path.join("src", n)).concat(INDEX_NAMES)) {
183
+ const file = resolveTargetFile(repoPath, candidate, project);
184
+ if (file) {
185
+ entries.push({ subpath: ".", file });
186
+ break;
187
+ }
188
+ }
189
+ }
190
+ return entries;
191
+ }
192
+ function normalizeExportsField(field) {
193
+ const out = [];
194
+ if (typeof field === "string") {
195
+ out.push([".", field]);
196
+ return out;
197
+ }
198
+ if (typeof field !== "object" || field === null)
199
+ return out;
200
+ for (const [subpath, value] of Object.entries(field)) {
201
+ if (subpath.includes("*"))
202
+ continue;
203
+ if (typeof value === "string") {
204
+ out.push([subpath, value]);
205
+ continue;
206
+ }
207
+ if (typeof value !== "object" || value === null)
208
+ continue;
209
+ const conditions = value;
210
+ const chosen = (typeof conditions.import === "string" && conditions.import) ||
211
+ (typeof conditions.default === "string" && conditions.default) ||
212
+ (typeof conditions.require === "string" && conditions.require);
213
+ if (typeof chosen === "string" && chosen)
214
+ out.push([subpath, chosen]);
215
+ }
216
+ return out;
217
+ }
218
+ function resolveTargetFile(repoPath, target, project) {
219
+ if (typeof target !== "string" || !target)
220
+ return undefined;
221
+ let rel = target.replace(/^\.\//, "").split(/[?#]/)[0];
222
+ if (rel.endsWith(".d.ts"))
223
+ return undefined;
224
+ const candidates = [rel];
225
+ const distMatch = rel.match(/^(dist|build)(\/|$)/);
226
+ if (distMatch) {
227
+ candidates.push("src" + rel.slice(distMatch[1].length));
228
+ }
229
+ for (const candidate of candidates) {
230
+ const stem = candidate.replace(/\.[^.]+$/, "");
231
+ const tries = new Set([candidate]);
232
+ for (const ext of ENTRY_EXTENSIONS)
233
+ tries.add(stem + ext);
234
+ for (const idx of INDEX_NAMES)
235
+ tries.add(path.posix.join(candidate, idx));
236
+ for (const tryPath of tries) {
237
+ if (tryPath.endsWith(".d.ts"))
238
+ continue;
239
+ const abs = path.resolve(repoPath, tryPath);
240
+ if (!fs.existsSync(abs) || fs.statSync(abs).isDirectory())
241
+ continue;
242
+ const sf = project.getSourceFile(abs);
243
+ if (sf)
244
+ return sf;
245
+ }
246
+ }
247
+ return undefined;
248
+ }
249
+ function resolveDeclarationToNode(decl, repoPath, nodeIndex) {
250
+ const name = declarationName(decl);
251
+ if (!name)
252
+ return undefined;
253
+ const relPath = path.relative(repoPath, decl.getSourceFile().getFilePath()).replace(/\\/g, "/");
254
+ const base = `${relPath}::${name}`;
255
+ const candidates = nodeIndex.get(base);
256
+ if (!candidates || candidates.length === 0)
257
+ return undefined;
258
+ if (candidates.length === 1)
259
+ return { id: candidates[0].id, base };
260
+ const match = matchOverloadedNode(decl, candidates);
261
+ return match ? { id: match.id, base } : undefined;
262
+ }
263
+ function declarationName(decl) {
264
+ const getName = decl.getName;
265
+ if (typeof getName === "function") {
266
+ try {
267
+ const own = getName.call(decl);
268
+ if (typeof own === "string" && own.length > 0)
269
+ return own;
270
+ }
271
+ catch {
272
+ // fall through to symbol
273
+ }
274
+ }
275
+ const sym = decl.getSymbol()?.getName();
276
+ if (sym && sym !== "default" && !sym.startsWith("__"))
277
+ return sym;
278
+ return undefined;
279
+ }
280
+ function matchOverloadedNode(decl, candidates) {
281
+ const paramTypes = declarationParamTypes(decl);
282
+ if (paramTypes === undefined)
283
+ return undefined;
284
+ const declKey = signatureKeyFromTypes(paramTypes);
285
+ const matches = candidates.filter((node) => {
286
+ const params = node.metadata?.parameters;
287
+ if (!Array.isArray(params))
288
+ return false;
289
+ return signatureKeyFromNodeParams(params) === declKey;
290
+ });
291
+ return matches.length >= 1 ? matches[0] : undefined;
292
+ }
293
+ function declarationParamTypes(decl) {
294
+ const anyDecl = decl;
295
+ if (typeof anyDecl.getParameters !== "function")
296
+ return undefined;
297
+ try {
298
+ const params = anyDecl.getParameters();
299
+ if (!Array.isArray(params))
300
+ return undefined;
301
+ return params.map((p) => {
302
+ const t = p.getTypeNode?.()?.getText() ?? "unknown";
303
+ if (p.isRest?.())
304
+ return `...${t}`;
305
+ if (p.isOptional?.())
306
+ return `${t}?`;
307
+ return t;
308
+ });
309
+ }
310
+ catch {
311
+ return undefined;
312
+ }
313
+ }
314
+ function signatureKeyFromTypes(types) {
315
+ return `(${types.join(",")})`;
316
+ }
317
+ function signatureKeyFromNodeParams(params) {
318
+ return `(${params
319
+ .map((p) => {
320
+ const t = typeof p?.type === "string" && p.type.length > 0 ? p.type : "unknown";
321
+ if (p?.isRest)
322
+ return `...${t}`;
323
+ if (p?.isOptional)
324
+ return `${t}?`;
325
+ return t;
326
+ })
327
+ .join(",")})`;
328
+ }
329
+ export function validateExportsMap(map, nodeIds) {
330
+ const dangling = [];
331
+ for (const perPath of Object.values(map.exports)) {
332
+ for (const ids of Object.values(perPath)) {
333
+ for (const id of ids) {
334
+ if (!nodeIds.has(id))
335
+ dangling.push(id);
336
+ }
337
+ }
338
+ }
339
+ return [...new Set(dangling)];
340
+ }
341
+ export function exportsMapStats(map) {
342
+ let names = 0;
343
+ let ambiguous = 0;
344
+ for (const perPath of Object.values(map.exports))
345
+ names += Object.keys(perPath).length;
346
+ for (const list of Object.values(map.ambiguousNames))
347
+ ambiguous += list.length;
348
+ return { subpaths: Object.keys(map.exports).length, names, ambiguous };
349
+ }
@@ -0,0 +1 @@
1
+ export {};