@snevins/repo-mapper 1.5.2 → 1.5.3
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.d.ts +9 -0
- package/dist/graph.js +49 -2
- package/dist/output.js +5 -22
- package/dist/parser.js +43 -3
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/dist/graph.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export declare function computeFileDegrees(graph: FileGraph): Map<string, FileDe
|
|
|
9
9
|
/**
|
|
10
10
|
* Build file reference graph from parsed tags.
|
|
11
11
|
* Nodes are files, edges are symbol references from one file to another.
|
|
12
|
+
*
|
|
13
|
+
* Scoped definitions: When a definition has a parent scope (e.g., a function
|
|
14
|
+
* inside a contract/class), edges are only created if the referencing file
|
|
15
|
+
* also references the parent scope. This prevents spurious edges from name
|
|
16
|
+
* collisions (e.g., multiple contracts defining `bid()` won't all get edges
|
|
17
|
+
* from every file that references any `bid()`).
|
|
12
18
|
*/
|
|
13
19
|
export declare function buildFileGraph(tags: readonly Tag[], options?: GraphBuildOptions): FileGraph;
|
|
14
20
|
/**
|
|
@@ -18,5 +24,8 @@ export declare function buildFileGraph(tags: readonly Tag[], options?: GraphBuil
|
|
|
18
24
|
* Unlike buildFileGraph which uses ref counts, this assigns weight 1
|
|
19
25
|
* to each unique file-to-file import relationship regardless of how
|
|
20
26
|
* many times symbols are referenced.
|
|
27
|
+
*
|
|
28
|
+
* Uses same scoped definition logic as buildFileGraph to prevent spurious
|
|
29
|
+
* edges from name collisions.
|
|
21
30
|
*/
|
|
22
31
|
export declare function buildImportGraph(tags: readonly Tag[], options?: GraphBuildOptions): FileGraph;
|
package/dist/graph.js
CHANGED
|
@@ -47,12 +47,20 @@ export function computeFileDegrees(graph) {
|
|
|
47
47
|
/**
|
|
48
48
|
* Build file reference graph from parsed tags.
|
|
49
49
|
* Nodes are files, edges are symbol references from one file to another.
|
|
50
|
+
*
|
|
51
|
+
* Scoped definitions: When a definition has a parent scope (e.g., a function
|
|
52
|
+
* inside a contract/class), edges are only created if the referencing file
|
|
53
|
+
* also references the parent scope. This prevents spurious edges from name
|
|
54
|
+
* collisions (e.g., multiple contracts defining `bid()` won't all get edges
|
|
55
|
+
* from every file that references any `bid()`).
|
|
50
56
|
*/
|
|
51
57
|
export function buildFileGraph(tags, options) {
|
|
52
58
|
const getMultiplier = options?.edgeWeightMultiplier ?? (() => 1.0);
|
|
53
|
-
// First pass: collect nodes and
|
|
59
|
+
// First pass: collect nodes, index defs, and track refs per file
|
|
54
60
|
const nodeSet = new Set();
|
|
55
61
|
const defsByName = new Map();
|
|
62
|
+
// Track all symbol names referenced by each file (for parent scope checking)
|
|
63
|
+
const refsByFile = new Map();
|
|
56
64
|
for (const tag of tags) {
|
|
57
65
|
nodeSet.add(tag.relPath);
|
|
58
66
|
if (tag.kind === "def") {
|
|
@@ -64,6 +72,15 @@ export function buildFileGraph(tags, options) {
|
|
|
64
72
|
defsByName.set(tag.name, [tag]);
|
|
65
73
|
}
|
|
66
74
|
}
|
|
75
|
+
else {
|
|
76
|
+
// kind === "ref": Track all refs per file for parent scope checking
|
|
77
|
+
let fileRefs = refsByFile.get(tag.relPath);
|
|
78
|
+
if (!fileRefs) {
|
|
79
|
+
fileRefs = new Set();
|
|
80
|
+
refsByFile.set(tag.relPath, fileRefs);
|
|
81
|
+
}
|
|
82
|
+
fileRefs.add(tag.name);
|
|
83
|
+
}
|
|
67
84
|
}
|
|
68
85
|
// Sort nodes for determinism
|
|
69
86
|
const nodes = [...nodeSet].sort();
|
|
@@ -79,10 +96,19 @@ export function buildFileGraph(tags, options) {
|
|
|
79
96
|
const defs = defsByName.get(tag.name);
|
|
80
97
|
if (!defs)
|
|
81
98
|
continue;
|
|
99
|
+
// Get refs for this file (for parent scope checking)
|
|
100
|
+
const fileRefs = refsByFile.get(tag.relPath);
|
|
82
101
|
for (const def of defs) {
|
|
83
102
|
// Skip self-edges
|
|
84
103
|
if (def.relPath === tag.relPath)
|
|
85
104
|
continue;
|
|
105
|
+
// Scoped definition check: if the definition has a parent scope,
|
|
106
|
+
// only create edge if the referencing file also references the parent.
|
|
107
|
+
// This prevents spurious edges from name collisions across unrelated
|
|
108
|
+
// contracts/classes (e.g., multiple contracts with `bid()` function).
|
|
109
|
+
if (def.parent && (!fileRefs || !fileRefs.has(def.parent))) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
86
112
|
const from = tag.relPath;
|
|
87
113
|
const to = def.relPath;
|
|
88
114
|
const symbol = tag.name;
|
|
@@ -167,12 +193,17 @@ export function buildFileGraph(tags, options) {
|
|
|
167
193
|
* Unlike buildFileGraph which uses ref counts, this assigns weight 1
|
|
168
194
|
* to each unique file-to-file import relationship regardless of how
|
|
169
195
|
* many times symbols are referenced.
|
|
196
|
+
*
|
|
197
|
+
* Uses same scoped definition logic as buildFileGraph to prevent spurious
|
|
198
|
+
* edges from name collisions.
|
|
170
199
|
*/
|
|
171
200
|
export function buildImportGraph(tags, options) {
|
|
172
201
|
const getMultiplier = options?.edgeWeightMultiplier ?? (() => 1.0);
|
|
173
|
-
// First pass: collect nodes and
|
|
202
|
+
// First pass: collect nodes, index defs, and track refs per file
|
|
174
203
|
const nodeSet = new Set();
|
|
175
204
|
const defsByName = new Map();
|
|
205
|
+
// Track all symbol names referenced by each file (for parent scope checking)
|
|
206
|
+
const refsByFile = new Map();
|
|
176
207
|
for (const tag of tags) {
|
|
177
208
|
nodeSet.add(tag.relPath);
|
|
178
209
|
if (tag.kind === "def") {
|
|
@@ -184,6 +215,15 @@ export function buildImportGraph(tags, options) {
|
|
|
184
215
|
defsByName.set(tag.name, [tag]);
|
|
185
216
|
}
|
|
186
217
|
}
|
|
218
|
+
else {
|
|
219
|
+
// kind === "ref": Track all refs per file for parent scope checking
|
|
220
|
+
let fileRefs = refsByFile.get(tag.relPath);
|
|
221
|
+
if (!fileRefs) {
|
|
222
|
+
fileRefs = new Set();
|
|
223
|
+
refsByFile.set(tag.relPath, fileRefs);
|
|
224
|
+
}
|
|
225
|
+
fileRefs.add(tag.name);
|
|
226
|
+
}
|
|
187
227
|
}
|
|
188
228
|
const nodes = [...nodeSet].sort();
|
|
189
229
|
// Second pass: build binary edges (1 per unique file→file connection)
|
|
@@ -197,9 +237,16 @@ export function buildImportGraph(tags, options) {
|
|
|
197
237
|
const defs = defsByName.get(tag.name);
|
|
198
238
|
if (!defs)
|
|
199
239
|
continue;
|
|
240
|
+
// Get refs for this file (for parent scope checking)
|
|
241
|
+
const fileRefs = refsByFile.get(tag.relPath);
|
|
200
242
|
for (const def of defs) {
|
|
201
243
|
if (def.relPath === tag.relPath)
|
|
202
244
|
continue;
|
|
245
|
+
// Scoped definition check: if the definition has a parent scope,
|
|
246
|
+
// only create edge if the referencing file also references the parent.
|
|
247
|
+
if (def.parent && (!fileRefs || !fileRefs.has(def.parent))) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
203
250
|
const from = tag.relPath;
|
|
204
251
|
const to = def.relPath;
|
|
205
252
|
// Check if we already have this edge (binary: only count once)
|
package/dist/output.js
CHANGED
|
@@ -83,22 +83,6 @@ function formatRankDisplay(rank, sortedRanks) {
|
|
|
83
83
|
const percentile = computePercentile(rank, sortedRanks);
|
|
84
84
|
return (percentile / 100).toFixed(1);
|
|
85
85
|
}
|
|
86
|
-
/**
|
|
87
|
-
* Get all definition tags for a specific file from the graph.
|
|
88
|
-
* Used to show all definitions for files that are included in output,
|
|
89
|
-
* not just the ones that were cross-file referenced.
|
|
90
|
-
*/
|
|
91
|
-
function getAllDefsForFile(file, graph) {
|
|
92
|
-
const result = [];
|
|
93
|
-
for (const tags of graph.defsByName.values()) {
|
|
94
|
-
for (const tag of tags) {
|
|
95
|
-
if (tag.relPath === file) {
|
|
96
|
-
result.push(tag);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
86
|
/**
|
|
103
87
|
* Resolve RankedDefinitions to their Tags by looking up in graph.defsByName.
|
|
104
88
|
* Returns only definitions that can be found.
|
|
@@ -246,18 +230,17 @@ export function formatOutput(defs, graph, fileRanks, focusFiles, maxFilesPerModu
|
|
|
246
230
|
lines.push(`## ${mod}${suffix} (${String(modFiles.length)} file${modFiles.length > 1 ? "s" : ""})`);
|
|
247
231
|
// Files in this module (already sorted by rank)
|
|
248
232
|
for (const file of modFiles) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (allFileTags.length === 0)
|
|
233
|
+
const fileDefs = fileGroups.get(file);
|
|
234
|
+
if (!fileDefs || fileDefs.length === 0)
|
|
252
235
|
continue;
|
|
253
236
|
const fileRank = fileRanks.get(file) ?? 0;
|
|
254
237
|
const deps = getFileDependents(file, graph);
|
|
255
238
|
const depsStr = deps.length > 0 ? ` (used-by: ${deps.slice(0, 10).join(", ")}${deps.length > 10 ? ", ..." : ""})` : "";
|
|
256
239
|
lines.push(`${file}: ${formatRankDisplay(fileRank, allRanks)}${depsStr}`);
|
|
257
240
|
// Sort definitions by line ascending
|
|
258
|
-
const sorted = [...
|
|
259
|
-
for (const
|
|
260
|
-
lines.push(` ${tag.signature ?? tag.name}`);
|
|
241
|
+
const sorted = [...fileDefs].sort((a, b) => a.tag.line - b.tag.line);
|
|
242
|
+
for (const def of sorted) {
|
|
243
|
+
lines.push(` ${def.tag.signature ?? def.tag.name}`);
|
|
261
244
|
}
|
|
262
245
|
}
|
|
263
246
|
// Omission annotation if files were limited
|
package/dist/parser.js
CHANGED
|
@@ -75,6 +75,36 @@ const EXPORT_BLOCKERS = new Set([
|
|
|
75
75
|
"class",
|
|
76
76
|
"class_expression",
|
|
77
77
|
]);
|
|
78
|
+
/**
|
|
79
|
+
* Node types that establish a parent scope for member definitions.
|
|
80
|
+
* Used to track which contract/class/interface a function/struct belongs to.
|
|
81
|
+
* This prevents spurious edges from name collisions across unrelated scopes.
|
|
82
|
+
*/
|
|
83
|
+
const PARENT_SCOPE_TYPES = {
|
|
84
|
+
solidity: new Set(["contract_declaration", "interface_declaration", "library_declaration"]),
|
|
85
|
+
typescript: new Set(["class_declaration", "abstract_class_declaration"]),
|
|
86
|
+
tsx: new Set(["class_declaration", "abstract_class_declaration"]),
|
|
87
|
+
javascript: new Set(["class_declaration"]),
|
|
88
|
+
jsx: new Set(["class_declaration"]),
|
|
89
|
+
python: new Set(["class_definition"]),
|
|
90
|
+
rust: new Set(["impl_item", "trait_item"]),
|
|
91
|
+
go: new Set(), // Go uses package-level scope, no class-like containers
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Get the name of a parent scope node (contract/class/interface name).
|
|
95
|
+
*/
|
|
96
|
+
function getParentScopeName(node) {
|
|
97
|
+
const nameNode = node.childForFieldName("name");
|
|
98
|
+
if (nameNode)
|
|
99
|
+
return nameNode.text;
|
|
100
|
+
// For Rust impl blocks: impl Foo { ... } or impl Trait for Foo { ... }
|
|
101
|
+
if (node.type === "impl_item") {
|
|
102
|
+
const typeNode = node.childForFieldName("type");
|
|
103
|
+
if (typeNode?.type === "type_identifier")
|
|
104
|
+
return typeNode.text;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
78
108
|
/**
|
|
79
109
|
* Check if a variable_declarator is directly exported (not inside a function/class).
|
|
80
110
|
* Stops at function/class boundaries to prevent local variables from being captured.
|
|
@@ -266,13 +296,18 @@ export async function parseFile(absPath, relPath) {
|
|
|
266
296
|
// Walk tree to extract definitions and references
|
|
267
297
|
const cursor = tree.walk();
|
|
268
298
|
const visitedNodes = new Set();
|
|
269
|
-
|
|
299
|
+
// Get parent scope types for this language
|
|
300
|
+
const parentScopeTypes = PARENT_SCOPE_TYPES[langConfig.grammarName] ?? new Set();
|
|
301
|
+
function walk(currentParent) {
|
|
270
302
|
const node = cursor.currentNode;
|
|
271
303
|
const nodeId = node.id;
|
|
272
304
|
// Avoid visiting same node twice
|
|
273
305
|
if (visitedNodes.has(nodeId))
|
|
274
306
|
return;
|
|
275
307
|
visitedNodes.add(nodeId);
|
|
308
|
+
// Check if this node establishes a new parent scope
|
|
309
|
+
const isParentScope = parentScopeTypes.has(node.type);
|
|
310
|
+
const newParent = isParentScope ? getParentScopeName(node) : currentParent;
|
|
276
311
|
// Check for definition nodes using config's definition types
|
|
277
312
|
if (langConfig.definitionTypes.has(node.type)) {
|
|
278
313
|
if (shouldIncludeDefinition(node, langConfig)) {
|
|
@@ -294,6 +329,9 @@ export async function parseFile(absPath, relPath) {
|
|
|
294
329
|
// For Python/Go/Rust/Solidity: all captured defs are "exported" (public API)
|
|
295
330
|
nodeIsExported = true;
|
|
296
331
|
}
|
|
332
|
+
// For parent scope definitions (contract/class), don't set parent
|
|
333
|
+
// (they ARE the parent, not children of another scope)
|
|
334
|
+
const defParent = isParentScope ? undefined : currentParent;
|
|
297
335
|
tags.push({
|
|
298
336
|
relPath,
|
|
299
337
|
absPath,
|
|
@@ -302,6 +340,7 @@ export async function parseFile(absPath, relPath) {
|
|
|
302
340
|
kind: "def",
|
|
303
341
|
signature: getSignature(node, sourceLines),
|
|
304
342
|
isExported: nodeIsExported,
|
|
343
|
+
parent: defParent,
|
|
305
344
|
});
|
|
306
345
|
}
|
|
307
346
|
}
|
|
@@ -318,6 +357,7 @@ export async function parseFile(absPath, relPath) {
|
|
|
318
357
|
kind: "def",
|
|
319
358
|
signature: getSignature(node, sourceLines),
|
|
320
359
|
isExported: true, // isDirectlyExportedVariable already checks this
|
|
360
|
+
parent: currentParent,
|
|
321
361
|
});
|
|
322
362
|
}
|
|
323
363
|
}
|
|
@@ -334,10 +374,10 @@ export async function parseFile(absPath, relPath) {
|
|
|
334
374
|
});
|
|
335
375
|
}
|
|
336
376
|
}
|
|
337
|
-
// Recurse into children
|
|
377
|
+
// Recurse into children with updated parent scope
|
|
338
378
|
if (cursor.gotoFirstChild()) {
|
|
339
379
|
do {
|
|
340
|
-
walk();
|
|
380
|
+
walk(newParent);
|
|
341
381
|
} while (cursor.gotoNextSibling());
|
|
342
382
|
cursor.gotoParent();
|
|
343
383
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export interface Tag {
|
|
|
10
10
|
readonly signature?: string;
|
|
11
11
|
/** Whether this definition is exported (public API). Only set for "def" kind. */
|
|
12
12
|
readonly isExported?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Parent scope for member definitions (e.g., "ReserveAuction" for a function inside that contract).
|
|
15
|
+
* Used to prevent spurious edges from name collisions across unrelated contracts/classes.
|
|
16
|
+
*/
|
|
17
|
+
readonly parent?: string;
|
|
13
18
|
}
|
|
14
19
|
/**
|
|
15
20
|
* CLI options parsed from command line arguments.
|