weavatrix 0.2.13 → 0.2.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/README.md +99 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.15.md +37 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +14 -71
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/conventions.js +16 -0
- package/src/analysis/dependency/scoped-dependencies.js +45 -0
- package/src/analysis/dependency/source-references.js +21 -0
- package/src/analysis/duplicates.tokenize.js +12 -2
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.reach.js +20 -0
- package/src/analysis/internal-audit.run.js +114 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/analysis/task-retrieval.js +3 -14
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/duplicate-member-order.mjs +8 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +3 -7
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/git-output.mjs +10 -0
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +197 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/reverse-reach.mjs +42 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +97 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp/tools-impact-change.mjs +2 -38
- package/src/mcp/tools-impact.mjs +2 -43
- package/src/mcp-server.mjs +35 -146
- package/src/path-classification.js +14 -0
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/security/malware-heuristics.sweep.js +1 -1
- package/src/security/registry-sig.classify.js +2 -1
- package/src/security/registry-sig.rules.js +3 -3
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -8,11 +8,17 @@ import { specToPkg } from "./builder/spec-pkg.js";
|
|
|
8
8
|
import { analyzeSyntaxComplexity } from "../analysis/source-complexity.js";
|
|
9
9
|
import { Parser, Query, GRAMMARS, LANGS, EXT_LANG, FAMILY, isDataFile, isDocFile, MAX_PARSE_BYTES, ensureParser, walk } from "./internal-builder.langs.js";
|
|
10
10
|
import { buildResolvers } from "./internal-builder.resolvers.js";
|
|
11
|
-
import { addJavaReferences } from "./internal-builder.java.js";
|
|
12
11
|
import { assignDeterministicCommunities } from "./community.js";
|
|
13
|
-
import { resolveJsBarrels } from "./internal-builder.barrels.js";
|
|
14
12
|
import { snapshotRepository } from "./incremental-refresh.js";
|
|
15
13
|
import { EDGE_PROVENANCE_V, stampEdgeProvenance } from "./edge-provenance.js";
|
|
14
|
+
import {runInternalGraphPass2} from './internal-builder.pass2.js'
|
|
15
|
+
|
|
16
|
+
function physicalLineCount(text) {
|
|
17
|
+
if (!text.length) return 0;
|
|
18
|
+
let lines = 1;
|
|
19
|
+
for (let i = 0; i < text.length; i++) if (text.charCodeAt(i) === 10) lines++;
|
|
20
|
+
return text.endsWith("\n") ? lines - 1 : lines;
|
|
21
|
+
}
|
|
16
22
|
|
|
17
23
|
// Parse a repo directory into a graph-builder-compatible { nodes, links } graph.
|
|
18
24
|
export async function buildInternalGraph(repoDir, opts = {}) {
|
|
@@ -78,10 +84,13 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
78
84
|
let _parsed = 0;
|
|
79
85
|
for (const abs of files) {
|
|
80
86
|
const fileRel = rel(abs); const ext = extname(abs);
|
|
81
|
-
|
|
82
|
-
if (isDataFile(fileRel) || isDocFile(fileRel)) {
|
|
87
|
+
const fileNode = { id: fileRel, label: fileRel.split("/").pop(), file_type: "code", source_file: fileRel, source_location: "L1" };
|
|
88
|
+
if (isDataFile(fileRel) || isDocFile(fileRel)) {
|
|
89
|
+
addNode(fileNode); perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue;
|
|
90
|
+
} // config/infra/docs file-only node
|
|
91
|
+
let code; try { code = readFileSync(abs, "utf8"); } catch { addNode(fileNode); continue; }
|
|
92
|
+
addNode({ ...fileNode, physical_loc: physicalLineCount(code) });
|
|
83
93
|
const grammar = EXT_LANG[ext]; const lang = LANGS[FAMILY[grammar]]; if (!lang || !langs[grammar]) continue;
|
|
84
|
-
let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
|
|
85
94
|
// giant / generated / minified file → keep the file NODE but don't parse symbols (avoids a wedged parse)
|
|
86
95
|
if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; }
|
|
87
96
|
if ((++_parsed % 24) === 0) await new Promise((r) => setImmediate(r)); // breathe: let the UI paint between chunks
|
|
@@ -144,7 +153,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
144
153
|
...(extra && extra.symbolSpace ? { symbol_space: extra.symbolSpace } : {}),
|
|
145
154
|
...(extra && extra.memberOf ? { member_of: extra.memberOf } : {}),
|
|
146
155
|
...(extra && extra.visibility ? { visibility: extra.visibility } : {}),
|
|
147
|
-
...(Number.isInteger(extra?.parameterCount) ? { parameter_count: extra.parameterCount } : {})
|
|
156
|
+
...(Number.isInteger(extra?.parameterCount) ? { parameter_count: extra.parameterCount } : {}),
|
|
157
|
+
...(extra?.receiverType ? { receiver_type: extra.receiverType } : {}),
|
|
158
|
+
...(extra?.returnType ? { return_type: extra.returnType } : {}),
|
|
159
|
+
...(extra?.fieldTypes && Object.keys(extra.fieldTypes).length ? { field_types: extra.fieldTypes } : {})
|
|
148
160
|
});
|
|
149
161
|
links.push({ source: fileRel, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
150
162
|
syms.push({
|
|
@@ -216,184 +228,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
216
228
|
// ---- pass 2: scope-aware calls + inheritance (Go package = whole dir → same-dir symbols share scope;
|
|
217
229
|
// C# gets the same treatment: one folder ≈ one namespace by convention, and `using` names namespaces,
|
|
218
230
|
// not files, so the folder map is the only reliable cross-file resolver) ----
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (!sharesDirScope(fr)) continue;
|
|
223
|
-
const d = fr.includes("/") ? fr.slice(0, fr.lastIndexOf("/")) : "";
|
|
224
|
-
let dm = goDirSymbols.get(d); if (!dm) goDirSymbols.set(d, (dm = new Map()));
|
|
225
|
-
for (const [n, id] of m) if (!dm.has(n)) dm.set(n, id);
|
|
226
|
-
}
|
|
227
|
-
const inferredSymbolSpace = (node) => {
|
|
228
|
-
const explicit = String(node?.symbol_space || "");
|
|
229
|
-
if (["value", "type", "both"].includes(explicit)) return explicit;
|
|
230
|
-
const kind = String(node?.symbol_kind || "").toLowerCase();
|
|
231
|
-
if (["interface", "type"].includes(kind)) return "type";
|
|
232
|
-
if (["class", "enum"].includes(kind)) return "both";
|
|
233
|
-
return "value";
|
|
234
|
-
};
|
|
235
|
-
const resolveNamedSymbol = (file, name, space = "value") => {
|
|
236
|
-
const ids = symIdsByFileName.get(file)?.get(name) || [];
|
|
237
|
-
const accepts = (id) => {
|
|
238
|
-
const symbolSpace = inferredSymbolSpace(nodeById.get(id));
|
|
239
|
-
return symbolSpace === "both" || symbolSpace === space;
|
|
240
|
-
};
|
|
241
|
-
const exact = ids.find((id) => inferredSymbolSpace(nodeById.get(id)) === space);
|
|
242
|
-
return exact || ids.find(accepts) || null;
|
|
243
|
-
};
|
|
244
|
-
const { resolveNamespaceMember, reExportOccurrences } = resolveJsBarrels({
|
|
245
|
-
jsExports, importedLocals, links,
|
|
246
|
-
resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? "type" : "value"),
|
|
231
|
+
const {reExportOccurrences} = runInternalGraphPass2({
|
|
232
|
+
files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
|
|
233
|
+
symIdsByFileName, importedLocals, jsExports,
|
|
247
234
|
});
|
|
248
|
-
// Exact source ranges can overlap (a named function nested inside another function). Attribute a call
|
|
249
|
-
// to the innermost matching symbol, not whichever outer declaration happened to be added first.
|
|
250
|
-
const enclosing = (fileRel, line) => {
|
|
251
|
-
let best = null;
|
|
252
|
-
for (const s of perFileSymbols.get(fileRel) || []) {
|
|
253
|
-
if (line < s.start || line > s.end) continue;
|
|
254
|
-
if (!best || s.start > best.start || (s.start === best.start && s.end < best.end)) best = s;
|
|
255
|
-
}
|
|
256
|
-
return best;
|
|
257
|
-
};
|
|
258
|
-
const resolveCall = (name, fileRel) => {
|
|
259
|
-
const local = resolveNamedSymbol(fileRel, name, "value"); if (local) return local;
|
|
260
|
-
if (sharesDirScope(fileRel)) { const d = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : ""; const dm = goDirSymbols.get(d); if (dm && dm.has(name)) return dm.get(name); }
|
|
261
|
-
const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(name);
|
|
262
|
-
if (imp && imp.targetFile) {
|
|
263
|
-
const targetFile = imp.originFile || imp.targetFile;
|
|
264
|
-
const importedName = imp.originName || imp.imported;
|
|
265
|
-
return resolveNamedSymbol(targetFile, importedName, "value");
|
|
266
|
-
}
|
|
267
|
-
return null;
|
|
268
|
-
};
|
|
269
|
-
const javaTypeKinds = new Set(["class", "interface", "enum", "record", "annotation"]);
|
|
270
|
-
const resolveJavaType = (name, fileRel) => {
|
|
271
|
-
const imp = importedLocals.get(fileRel)?.get(name);
|
|
272
|
-
if (imp?.targetFile) {
|
|
273
|
-
const symbols = symByFileName.get(imp.targetFile);
|
|
274
|
-
const target = symbols?.get(imp.imported) || symbols?.get(name);
|
|
275
|
-
if (target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind)) return target;
|
|
276
|
-
}
|
|
277
|
-
const target = symByFileName.get(fileRel)?.get(name);
|
|
278
|
-
return target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind) ? target : null;
|
|
279
|
-
};
|
|
280
|
-
for (const abs of files) {
|
|
281
|
-
const fileRel = rel(abs); const grammar = EXT_LANG[extname(abs)]; if (!grammar) continue;
|
|
282
|
-
const lang = LANGS[FAMILY[grammar]]; if (!lang || lang.isWeb || !langs[grammar]) continue;
|
|
283
|
-
let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
|
|
284
|
-
const parser = new Parser(); parser.setLanguage(langs[grammar]);
|
|
285
|
-
let tree; try { tree = parser.parse(code); } catch { continue; }
|
|
286
|
-
if (!lang.customCalls) for (const cap of caps(grammar, lang.calls, tree.rootNode)) {
|
|
287
|
-
const caller = enclosing(fileRel, cap.node.startPosition.row + 1); if (!caller) continue;
|
|
288
|
-
const target = resolveCall(cap.node.text, fileRel); if (!target || target === caller.id) continue;
|
|
289
|
-
links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
|
|
290
|
-
}
|
|
291
|
-
if (typeof lang.pass2 === "function") {
|
|
292
|
-
try {
|
|
293
|
-
lang.pass2({
|
|
294
|
-
grammar, tree, fileRel, code, caps, field, enclosing, links, nodeById,
|
|
295
|
-
perFileSymbols, symByFileName, symIdsByFileName, importedLocals, resolveCall, resolveJavaType,
|
|
296
|
-
});
|
|
297
|
-
} catch (e) { /* one language-specific resolver never sinks the graph */ void e; }
|
|
298
|
-
}
|
|
299
|
-
// qualified/selector calls (Go): `pkg.Func()` → the imported package's dir; else `receiver.Method()` → the
|
|
300
|
-
// SAME package (heuristic by method name — connects lifecycle methods like peer.Enable() that need type info).
|
|
301
|
-
if (lang.selectorCall) for (const cap of caps(grammar, lang.selectorCall, tree.rootNode)) {
|
|
302
|
-
const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
|
|
303
|
-
if (!operand || operand.type !== "identifier" || !fld) continue;
|
|
304
|
-
const caller = enclosing(fileRel, sel.startPosition.row + 1); if (!caller) continue;
|
|
305
|
-
const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text);
|
|
306
|
-
const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
|
|
307
|
-
const dm = goDirSymbols.get(imp && imp.targetDir ? imp.targetDir : dir);
|
|
308
|
-
const target = dm && dm.get(fld.text);
|
|
309
|
-
if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
|
|
310
|
-
}
|
|
311
|
-
for (const heritageSpec of lang.heritage || []) {
|
|
312
|
-
const query = typeof heritageSpec === "string" ? heritageSpec : heritageSpec.query;
|
|
313
|
-
const relation = typeof heritageSpec === "string" ? "inherits" : (heritageSpec.relation || "inherits");
|
|
314
|
-
for (const cap of caps(grammar, query, tree.rootNode)) {
|
|
315
|
-
const cls = enclosing(fileRel, cap.node.startPosition.row + 1); if (!cls) continue;
|
|
316
|
-
const target = FAMILY[grammar] === "java" ? resolveJavaType(cap.node.text, fileRel) : resolveCall(cap.node.text, fileRel);
|
|
317
|
-
if (target && target !== cls.id) links.push({ source: cls.id, target, relation, confidence: "INFERRED" });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
// JSX is a real symbol use even though it is not a call_expression. Resolve imported components to their
|
|
321
|
-
// declaration so component fan-in and unused-export checks do not claim `<SettingsView />` is unreferenced.
|
|
322
|
-
if (FAMILY[grammar] === "js") {
|
|
323
|
-
// Namespace calls (`ui.run()`) need the member name before an export-star facade can be resolved.
|
|
324
|
-
for (const cap of caps(grammar, `(call_expression function: (member_expression) @memberCall)`, tree.rootNode)) {
|
|
325
|
-
const object = field(cap.node, "object"), property = field(cap.node, "property");
|
|
326
|
-
if (!object || object.type !== "identifier" || !property) continue;
|
|
327
|
-
const imp = importedLocals.get(fileRel)?.get(object.text);
|
|
328
|
-
if (!imp || !["*", "default"].includes(imp.imported) || imp.typeOnly) continue;
|
|
329
|
-
const origin = resolveNamespaceMember(fileRel, imp, property.text, "call");
|
|
330
|
-
if (origin.status !== "resolved") continue;
|
|
331
|
-
const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, "value");
|
|
332
|
-
const caller = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
333
|
-
if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
|
|
334
|
-
}
|
|
335
|
-
for (const cap of caps(grammar, `[
|
|
336
|
-
(jsx_opening_element name: (_) @jsx)
|
|
337
|
-
(jsx_self_closing_element name: (_) @jsx)
|
|
338
|
-
]`, tree.rootNode)) {
|
|
339
|
-
const jsxName = cap.node.text;
|
|
340
|
-
const parts = jsxName.split(".");
|
|
341
|
-
const localName = parts[0];
|
|
342
|
-
if (parts.length === 1 && !/^[A-Z_$]/.test(localName)) continue; // undotted lowercase tags are platform/intrinsic elements
|
|
343
|
-
const imp = importedLocals.get(fileRel)?.get(localName);
|
|
344
|
-
if (!imp || !imp.targetFile || imp.typeOnly) continue;
|
|
345
|
-
let targetFile = imp.originFile || imp.targetFile;
|
|
346
|
-
let importedName = imp.originName || imp.imported;
|
|
347
|
-
if (imp.imported === "*" && parts.length > 1) {
|
|
348
|
-
const origin = resolveNamespaceMember(fileRel, imp, parts[parts.length - 1], "jsx");
|
|
349
|
-
if (origin.status === "resolved") { targetFile = origin.origin.file; importedName = origin.origin.name; }
|
|
350
|
-
}
|
|
351
|
-
const target = resolveNamedSymbol(targetFile, importedName, "value") || resolveNamedSymbol(targetFile, localName, "value");
|
|
352
|
-
if (!target) continue;
|
|
353
|
-
const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
354
|
-
links.push({ source: owner?.id || fileRel, target, relation: "references", confidence: "INFERRED", usage: "jsx", line: cap.node.startPosition.row + 1 });
|
|
355
|
-
}
|
|
356
|
-
// Type identifiers are a separate namespace in TypeScript. Resolve them to type/both-space
|
|
357
|
-
// declarations without creating runtime calls or keeping a same-named value symbol alive.
|
|
358
|
-
if (grammar !== "javascript") for (const cap of caps(grammar, `(type_identifier) @typeRef`, tree.rootNode)) {
|
|
359
|
-
const name = cap.node.text;
|
|
360
|
-
const imp = importedLocals.get(fileRel)?.get(name);
|
|
361
|
-
const targetFile = imp?.originFile || imp?.targetFile || fileRel;
|
|
362
|
-
const targetName = imp?.originName || imp?.imported || name;
|
|
363
|
-
const target = resolveNamedSymbol(targetFile, targetName, "type");
|
|
364
|
-
if (!target || String(target).startsWith(`${fileRel}#${name}@${cap.node.startPosition.row + 1}`)) continue;
|
|
365
|
-
const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
366
|
-
const source = owner?.id || fileRel;
|
|
367
|
-
if (source === target) continue;
|
|
368
|
-
links.push({source, target, relation: "references", confidence: "EXTRACTED", provenance: "RESOLVED", typeOnly: true, line: cap.node.startPosition.row + 1, usage: "type"});
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
// Go value references: a top-level const/var/type/func used BY NAME (bare `X`, or cross-package `pkg.X`) in
|
|
372
|
-
// another file/scope → a `references` edge, so used-but-never-called symbols (message-type consts, etc.) are
|
|
373
|
-
// not falsely DEAD. (Same-file usage is already covered by graph-builder-analysis localRefs.) Go-only for now.
|
|
374
|
-
if (FAMILY[grammar] === "go") {
|
|
375
|
-
const refSeen = new Set();
|
|
376
|
-
const emitRef = (src, target) => { if (!target || target === src) return; const k = src + ">" + target; if (refSeen.has(k)) return; refSeen.add(k); links.push({ source: src, target, relation: "references", confidence: "INFERRED" }); };
|
|
377
|
-
const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
|
|
378
|
-
const dm = goDirSymbols.get(dir);
|
|
379
|
-
for (const cap of caps(grammar, `[(identifier) (type_identifier)] @id`, tree.rootNode)) { // type_identifier → struct/type usage
|
|
380
|
-
const target = dm && dm.get(cap.node.text); if (!target || target.slice(0, target.indexOf("#")) === fileRel) continue;
|
|
381
|
-
const caller = enclosing(fileRel, cap.node.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
|
|
382
|
-
}
|
|
383
|
-
for (const cap of caps(grammar, `(selector_expression) @sel`, tree.rootNode)) {
|
|
384
|
-
const sel = cap.node; const operand = field(sel, "operand"), fld = field(sel, "field");
|
|
385
|
-
if (!operand || operand.type !== "identifier" || !fld) continue;
|
|
386
|
-
const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(operand.text); if (!imp || !imp.targetDir) continue;
|
|
387
|
-
const tdm = goDirSymbols.get(imp.targetDir); const target = tdm && tdm.get(fld.text); if (!target) continue;
|
|
388
|
-
const caller = enclosing(fileRel, sel.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
if (FAMILY[grammar] === "java") {
|
|
392
|
-
addJavaReferences({ grammar, tree, fileRel, caps, resolveJavaType, enclosing, links });
|
|
393
|
-
}
|
|
394
|
-
tree.delete();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
235
|
// HTML class/id usage → the CSS file(s) defining that selector: file-level reference edges (deduped per pair).
|
|
398
236
|
const htmlRefSeen = new Set();
|
|
399
237
|
for (const u of htmlUsages) {
|
|
@@ -422,6 +260,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
422
260
|
edgeTypesV: 2,
|
|
423
261
|
edgeProvenanceV: EDGE_PROVENANCE_V,
|
|
424
262
|
complexityV: 2,
|
|
263
|
+
physicalFileLocV: 1,
|
|
425
264
|
repoBoundaryV: 1,
|
|
426
265
|
barrelResolutionV: 1,
|
|
427
266
|
reExportOccurrencesV: 1,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs'
|
|
2
|
+
import {extname} from 'node:path'
|
|
3
|
+
import {Parser, EXT_LANG, FAMILY, LANGS} from './internal-builder.langs.js'
|
|
4
|
+
import {addJavaReferences} from './internal-builder.java.js'
|
|
5
|
+
import {resolveJsBarrels} from './internal-builder.barrels.js'
|
|
6
|
+
import {createPass2Resolution} from './builder/pass2-resolution.js'
|
|
7
|
+
import {createGoReceiverResolution} from './builder/go-receiver-resolution.js'
|
|
8
|
+
|
|
9
|
+
export function runInternalGraphPass2({
|
|
10
|
+
files, rel, langs, caps, field, links, nodeById, perFileSymbols, symByFileName,
|
|
11
|
+
symIdsByFileName, importedLocals, jsExports,
|
|
12
|
+
}) {
|
|
13
|
+
const resolution = createPass2Resolution({symIdsByFileName, nodeById, importedLocals, symByFileName})
|
|
14
|
+
const {dirSymbols, resolveNamedSymbol, resolveCall, resolveJavaType} = resolution
|
|
15
|
+
const {resolveNamespaceMember, reExportOccurrences} = resolveJsBarrels({
|
|
16
|
+
jsExports, importedLocals, links,
|
|
17
|
+
resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? 'type' : 'value'),
|
|
18
|
+
})
|
|
19
|
+
const enclosing = (file, line) => {
|
|
20
|
+
let best = null
|
|
21
|
+
for (const symbol of perFileSymbols.get(file) || []) {
|
|
22
|
+
if (line < symbol.start || line > symbol.end) continue
|
|
23
|
+
if (!best || symbol.start > best.start || (symbol.start === best.start && symbol.end < best.end)) best = symbol
|
|
24
|
+
}
|
|
25
|
+
return best
|
|
26
|
+
}
|
|
27
|
+
const goReceivers = createGoReceiverResolution({resolution, nodeById, importedLocals})
|
|
28
|
+
for (const absolute of files) {
|
|
29
|
+
const file = rel(absolute), grammar = EXT_LANG[extname(absolute)]
|
|
30
|
+
if (!grammar) continue
|
|
31
|
+
const lang = LANGS[FAMILY[grammar]]
|
|
32
|
+
if (!lang || lang.isWeb || !langs[grammar]) continue
|
|
33
|
+
let code
|
|
34
|
+
try { code = readFileSync(absolute, 'utf8') } catch { continue }
|
|
35
|
+
const parser = new Parser()
|
|
36
|
+
parser.setLanguage(langs[grammar])
|
|
37
|
+
let tree
|
|
38
|
+
try { tree = parser.parse(code) } catch { continue }
|
|
39
|
+
if (!lang.customCalls) for (const capture of caps(grammar, lang.calls, tree.rootNode)) {
|
|
40
|
+
const caller = enclosing(file, capture.node.startPosition.row + 1)
|
|
41
|
+
if (!caller) continue
|
|
42
|
+
const target = resolveCall(capture.node.text, file)
|
|
43
|
+
if (target && target !== caller.id) links.push({source: caller.id, target, relation: 'calls', confidence: 'INFERRED', line: capture.node.startPosition.row + 1})
|
|
44
|
+
}
|
|
45
|
+
if (typeof lang.pass2 === 'function') try {
|
|
46
|
+
lang.pass2({
|
|
47
|
+
grammar, tree, fileRel: file, code, caps, field, enclosing, links, nodeById,
|
|
48
|
+
perFileSymbols, symByFileName, symIdsByFileName, importedLocals, resolveCall, resolveJavaType,
|
|
49
|
+
})
|
|
50
|
+
} catch { /* one language-specific resolver never sinks the graph */ }
|
|
51
|
+
if (lang.selectorCall) for (const capture of caps(grammar, lang.selectorCall, tree.rootNode)) {
|
|
52
|
+
const selector = capture.node, operand = field(selector, 'operand'), member = field(selector, 'field')
|
|
53
|
+
if (!operand || !member) continue
|
|
54
|
+
const caller = enclosing(file, selector.startPosition.row + 1)
|
|
55
|
+
if (!caller) continue
|
|
56
|
+
const bindings = goReceivers.receiverBindings(selector, file)
|
|
57
|
+
const imported = operand.type === 'identifier' && !bindings.has(operand.text)
|
|
58
|
+
? importedLocals.get(file)?.get(operand.text) : null
|
|
59
|
+
let target = imported?.targetDir ? dirSymbols.get(imported.targetDir)?.get(member.text) : null
|
|
60
|
+
let resolved = !!target
|
|
61
|
+
if (!target) {
|
|
62
|
+
let receiver = null
|
|
63
|
+
if (operand.type === 'identifier') receiver = bindings.get(operand.text) || null
|
|
64
|
+
else if (operand.type === 'selector_expression') {
|
|
65
|
+
const base = field(operand, 'operand')
|
|
66
|
+
const baseType = base?.type === 'identifier' ? bindings.get(base.text) : null
|
|
67
|
+
receiver = goReceivers.fieldType(baseType, field(operand, 'field')?.text)
|
|
68
|
+
}
|
|
69
|
+
target = goReceivers.exactMethod(receiver, member.text)
|
|
70
|
+
resolved = !!target
|
|
71
|
+
if (!target && !receiver) target = goReceivers.uniqueMethod(goReceivers.dirOf(file), member.text)
|
|
72
|
+
}
|
|
73
|
+
if (target && target !== caller.id) links.push({
|
|
74
|
+
source: caller.id, target, relation: 'calls', confidence: 'INFERRED',
|
|
75
|
+
...(resolved ? {provenance: 'RESOLVED'} : {}), line: selector.startPosition.row + 1,
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
for (const heritageSpec of lang.heritage || []) {
|
|
79
|
+
const query = typeof heritageSpec === 'string' ? heritageSpec : heritageSpec.query
|
|
80
|
+
const relation = typeof heritageSpec === 'string' ? 'inherits' : (heritageSpec.relation || 'inherits')
|
|
81
|
+
for (const capture of caps(grammar, query, tree.rootNode)) {
|
|
82
|
+
const owner = enclosing(file, capture.node.startPosition.row + 1)
|
|
83
|
+
if (!owner) continue
|
|
84
|
+
const target = FAMILY[grammar] === 'java' ? resolveJavaType(capture.node.text, file) : resolveCall(capture.node.text, file)
|
|
85
|
+
if (target && target !== owner.id) links.push({source: owner.id, target, relation, confidence: 'INFERRED'})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (FAMILY[grammar] === 'js') addJavaScriptReferences({
|
|
89
|
+
grammar, tree, file, caps, field, importedLocals, resolveNamespaceMember,
|
|
90
|
+
resolveNamedSymbol, enclosing, links,
|
|
91
|
+
})
|
|
92
|
+
if (FAMILY[grammar] === 'go') addGoReferences({
|
|
93
|
+
grammar, tree, file, caps, field, importedLocals, dirSymbols, enclosing, links,
|
|
94
|
+
})
|
|
95
|
+
if (FAMILY[grammar] === 'java') addJavaReferences({grammar, tree, fileRel: file, caps, resolveJavaType, enclosing, links})
|
|
96
|
+
tree.delete()
|
|
97
|
+
}
|
|
98
|
+
return {reExportOccurrences}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function addJavaScriptReferences({grammar, tree, file, caps, field, importedLocals, resolveNamespaceMember, resolveNamedSymbol, enclosing, links}) {
|
|
102
|
+
for (const capture of caps(grammar, `(call_expression function: (member_expression) @memberCall)`, tree.rootNode)) {
|
|
103
|
+
const object = field(capture.node, 'object'), property = field(capture.node, 'property')
|
|
104
|
+
if (!object || object.type !== 'identifier' || !property) continue
|
|
105
|
+
const imported = importedLocals.get(file)?.get(object.text)
|
|
106
|
+
if (!imported || !['*', 'default'].includes(imported.imported) || imported.typeOnly) continue
|
|
107
|
+
const origin = resolveNamespaceMember(file, imported, property.text, 'call')
|
|
108
|
+
if (origin.status !== 'resolved') continue
|
|
109
|
+
const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, 'value')
|
|
110
|
+
const caller = enclosing(file, capture.node.startPosition.row + 1)
|
|
111
|
+
if (target && caller && target !== caller.id) links.push({source: caller.id, target, relation: 'calls', confidence: 'INFERRED', line: capture.node.startPosition.row + 1})
|
|
112
|
+
}
|
|
113
|
+
for (const capture of caps(grammar, `[(jsx_opening_element name: (_) @jsx) (jsx_self_closing_element name: (_) @jsx)]`, tree.rootNode)) {
|
|
114
|
+
const parts = capture.node.text.split('.'), localName = parts[0]
|
|
115
|
+
if (parts.length === 1 && !/^[A-Z_$]/.test(localName)) continue
|
|
116
|
+
const imported = importedLocals.get(file)?.get(localName)
|
|
117
|
+
if (!imported?.targetFile || imported.typeOnly) continue
|
|
118
|
+
let targetFile = imported.originFile || imported.targetFile
|
|
119
|
+
let importedName = imported.originName || imported.imported
|
|
120
|
+
if (imported.imported === '*' && parts.length > 1) {
|
|
121
|
+
const origin = resolveNamespaceMember(file, imported, parts.at(-1), 'jsx')
|
|
122
|
+
if (origin.status === 'resolved') { targetFile = origin.origin.file; importedName = origin.origin.name }
|
|
123
|
+
}
|
|
124
|
+
const target = resolveNamedSymbol(targetFile, importedName, 'value') || resolveNamedSymbol(targetFile, localName, 'value')
|
|
125
|
+
if (!target) continue
|
|
126
|
+
const owner = enclosing(file, capture.node.startPosition.row + 1)
|
|
127
|
+
links.push({source: owner?.id || file, target, relation: 'references', confidence: 'INFERRED', usage: 'jsx', line: capture.node.startPosition.row + 1})
|
|
128
|
+
}
|
|
129
|
+
if (grammar === 'javascript') return
|
|
130
|
+
for (const capture of caps(grammar, `(type_identifier) @typeRef`, tree.rootNode)) {
|
|
131
|
+
const name = capture.node.text, imported = importedLocals.get(file)?.get(name)
|
|
132
|
+
const targetFile = imported?.originFile || imported?.targetFile || file
|
|
133
|
+
const targetName = imported?.originName || imported?.imported || name
|
|
134
|
+
const target = resolveNamedSymbol(targetFile, targetName, 'type')
|
|
135
|
+
if (!target || String(target).startsWith(`${file}#${name}@${capture.node.startPosition.row + 1}`)) continue
|
|
136
|
+
const source = enclosing(file, capture.node.startPosition.row + 1)?.id || file
|
|
137
|
+
if (source !== target) links.push({source, target, relation: 'references', confidence: 'EXTRACTED', provenance: 'RESOLVED', typeOnly: true, line: capture.node.startPosition.row + 1, usage: 'type'})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function addGoReferences({grammar, tree, file, caps, field, importedLocals, dirSymbols, enclosing, links}) {
|
|
142
|
+
const seen = new Set()
|
|
143
|
+
const emit = (source, target) => {
|
|
144
|
+
const key = `${source}>${target}`
|
|
145
|
+
if (!target || target === source || seen.has(key)) return
|
|
146
|
+
seen.add(key); links.push({source, target, relation: 'references', confidence: 'INFERRED'})
|
|
147
|
+
}
|
|
148
|
+
const dir = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : '', symbols = dirSymbols.get(dir)
|
|
149
|
+
for (const capture of caps(grammar, `[(identifier) (type_identifier)] @id`, tree.rootNode)) {
|
|
150
|
+
const target = symbols?.get(capture.node.text)
|
|
151
|
+
if (!target || target.slice(0, target.indexOf('#')) === file) continue
|
|
152
|
+
emit(enclosing(file, capture.node.startPosition.row + 1)?.id || file, target)
|
|
153
|
+
}
|
|
154
|
+
for (const capture of caps(grammar, `(selector_expression) @sel`, tree.rootNode)) {
|
|
155
|
+
const selector = capture.node, operand = field(selector, 'operand'), member = field(selector, 'field')
|
|
156
|
+
if (!operand || operand.type !== 'identifier' || !member) continue
|
|
157
|
+
const imported = importedLocals.get(file)?.get(operand.text)
|
|
158
|
+
const target = imported?.targetDir && dirSymbols.get(imported.targetDir)?.get(member.text)
|
|
159
|
+
if (target) emit(enclosing(file, selector.startPosition.row + 1)?.id || file, target)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -6,6 +6,7 @@ import { readFileSync } from "node:fs";
|
|
|
6
6
|
import { join, dirname } from "node:path";
|
|
7
7
|
import { parseGoMod } from "../analysis/manifests.js";
|
|
8
8
|
import { createRepoBoundary } from "../repo-path.js";
|
|
9
|
+
import { createRustResolvers } from "./resolvers/rust.js";
|
|
9
10
|
|
|
10
11
|
export function buildResolvers(repoDir, fileSet) {
|
|
11
12
|
const boundary = createRepoBoundary(repoDir);
|
|
@@ -48,111 +49,7 @@ export function buildResolvers(repoDir, fileSet) {
|
|
|
48
49
|
return cands.find((f) => f === full || f.endsWith("/" + full)) || cands[0] || null;
|
|
49
50
|
};
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
// `foo.rs` owns children below `foo/`, while lib.rs/main.rs/mod.rs own siblings. Keep the resolver
|
|
53
|
-
// filesystem-only and crate-local: Cargo/external dependencies belong to dependency analysis, not to
|
|
54
|
-
// the internal module graph.
|
|
55
|
-
const cleanRustRel = (p) => String(p || "").replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "").replace(/^\.$/, "");
|
|
56
|
-
const rustDir = (p) => { p = cleanRustRel(p); const i = p.lastIndexOf("/"); return i < 0 ? "" : p.slice(0, i); };
|
|
57
|
-
const rustBase = (p) => { p = cleanRustRel(p); const i = p.lastIndexOf("/"); return i < 0 ? p : p.slice(i + 1); };
|
|
58
|
-
const rustJoin = (...parts) => cleanRustRel(join(...parts.filter((x) => x != null && x !== "")));
|
|
59
|
-
const rustFiles = new Set([...fileSet].filter((fr) => fr.endsWith(".rs")));
|
|
60
|
-
const rustRoots = new Map();
|
|
61
|
-
for (const fr of rustFiles) {
|
|
62
|
-
const base = rustBase(fr);
|
|
63
|
-
if (base !== "lib.rs" && base !== "main.rs") continue;
|
|
64
|
-
const dir = rustDir(fr);
|
|
65
|
-
let root = rustRoots.get(dir);
|
|
66
|
-
if (!root) rustRoots.set(dir, (root = { base: dir, lib: null, main: null }));
|
|
67
|
-
root[base === "lib.rs" ? "lib" : "main"] = fr;
|
|
68
|
-
}
|
|
69
|
-
const rustRootList = [...rustRoots.values()].sort((a, b) => b.base.length - a.base.length);
|
|
70
|
-
const rustContext = (fromRel) => {
|
|
71
|
-
fromRel = cleanRustRel(fromRel);
|
|
72
|
-
const base = rustBase(fromRel);
|
|
73
|
-
const dir = rustDir(fromRel);
|
|
74
|
-
if (base === "lib.rs" || base === "main.rs") return { base: dir, rootFile: fromRel };
|
|
75
|
-
|
|
76
|
-
// Cargo treats direct files in these conventional folders as independent crate roots. This only
|
|
77
|
-
// affects paths originating in the root file itself; nested module ownership still comes from mod/use.
|
|
78
|
-
if (/^(?:bin|examples|tests|benches)$/.test(rustBase(dir))) return { base: dir, rootFile: fromRel };
|
|
79
|
-
for (const root of rustRootList) {
|
|
80
|
-
if (!root.base || fromRel.startsWith(root.base + "/")) return { base: root.base, rootFile: root.lib || root.main };
|
|
81
|
-
}
|
|
82
|
-
return { base: dir, rootFile: fromRel };
|
|
83
|
-
};
|
|
84
|
-
const rustModuleBase = (fromRel) => {
|
|
85
|
-
const ctx = rustContext(fromRel);
|
|
86
|
-
if (ctx.rootFile === cleanRustRel(fromRel)) return rustDir(fromRel);
|
|
87
|
-
const name = rustBase(fromRel);
|
|
88
|
-
if (name === "lib.rs" || name === "main.rs" || name === "mod.rs") return rustDir(fromRel);
|
|
89
|
-
return rustJoin(rustDir(fromRel), name.replace(/\.rs$/, ""));
|
|
90
|
-
};
|
|
91
|
-
const rustInlineBase = (fromRel, inlineModules = []) => {
|
|
92
|
-
let base = rustModuleBase(fromRel);
|
|
93
|
-
for (let i = 0; i < inlineModules.length; i++) {
|
|
94
|
-
const mod = inlineModules[i] || {};
|
|
95
|
-
if (mod.path) {
|
|
96
|
-
// Rust Reference: a path attribute on the first inline module is relative to the source file's
|
|
97
|
-
// directory; nested attributes are relative to their containing module's search directory.
|
|
98
|
-
const parent = i === 0 ? rustDir(fromRel) : base;
|
|
99
|
-
base = rustJoin(parent, mod.path);
|
|
100
|
-
} else base = rustJoin(base, mod.name);
|
|
101
|
-
}
|
|
102
|
-
return base;
|
|
103
|
-
};
|
|
104
|
-
const rustModuleFile = (moduleBase, ctx) => {
|
|
105
|
-
moduleBase = cleanRustRel(moduleBase);
|
|
106
|
-
if (moduleBase === cleanRustRel(ctx.base) && ctx.rootFile && rustFiles.has(ctx.rootFile)) return ctx.rootFile;
|
|
107
|
-
const flat = moduleBase + ".rs";
|
|
108
|
-
if (rustFiles.has(flat)) return flat;
|
|
109
|
-
const legacy = rustJoin(moduleBase, "mod.rs");
|
|
110
|
-
return rustFiles.has(legacy) ? legacy : null;
|
|
111
|
-
};
|
|
112
|
-
const resolveRustMod = (fromRel, name, { inlineModules = [], explicitPath = "" } = {}) => {
|
|
113
|
-
fromRel = cleanRustRel(fromRel);
|
|
114
|
-
if (explicitPath) {
|
|
115
|
-
const parent = inlineModules.length ? rustInlineBase(fromRel, inlineModules) : rustDir(fromRel);
|
|
116
|
-
const target = rustJoin(parent, explicitPath);
|
|
117
|
-
return rustFiles.has(target) ? target : null;
|
|
118
|
-
}
|
|
119
|
-
const targetBase = rustJoin(rustInlineBase(fromRel, inlineModules), String(name || "").replace(/^r#/, ""));
|
|
120
|
-
return rustModuleFile(targetBase, rustContext(fromRel));
|
|
121
|
-
};
|
|
122
|
-
const resolveRustPath = (fromRel, rawSegments, { inlineModules = [], unqualified = true } = {}) => {
|
|
123
|
-
fromRel = cleanRustRel(fromRel);
|
|
124
|
-
const segments = (Array.isArray(rawSegments) ? rawSegments : String(rawSegments || "").split("::"))
|
|
125
|
-
.map((s) => String(s).trim().replace(/^r#/, "")).filter(Boolean);
|
|
126
|
-
if (!segments.length) return null;
|
|
127
|
-
const ctx = rustContext(fromRel);
|
|
128
|
-
const current = rustInlineBase(fromRel, inlineModules);
|
|
129
|
-
let rest = [...segments];
|
|
130
|
-
const starts = [];
|
|
131
|
-
let anchored = false;
|
|
132
|
-
if (rest[0] === "crate") { anchored = true; rest.shift(); starts.push(ctx.base); }
|
|
133
|
-
else if (rest[0] === "self") { anchored = true; rest.shift(); starts.push(current); }
|
|
134
|
-
else if (rest[0] === "super") {
|
|
135
|
-
anchored = true;
|
|
136
|
-
let base = current;
|
|
137
|
-
while (rest[0] === "super") { rest.shift(); base = rustDir(base); }
|
|
138
|
-
if (ctx.base && base !== ctx.base && !base.startsWith(ctx.base + "/")) return null;
|
|
139
|
-
starts.push(base);
|
|
140
|
-
} else if (unqualified) {
|
|
141
|
-
// Rust 2018 resolves a bare use path from the crate root/external prelude. Prefer an internal root
|
|
142
|
-
// module, then allow a lexically-local module for qualified expressions in nested modules.
|
|
143
|
-
starts.push(ctx.base);
|
|
144
|
-
if (current !== ctx.base) starts.push(current);
|
|
145
|
-
} else return null;
|
|
146
|
-
|
|
147
|
-
for (const start of starts) {
|
|
148
|
-
const min = anchored ? 0 : 1; // never reinterpret an unresolved external `serde::X` as the crate root
|
|
149
|
-
for (let used = rest.length; used >= min; used--) {
|
|
150
|
-
const target = rustModuleFile(rustJoin(start, ...rest.slice(0, used)), ctx);
|
|
151
|
-
if (target) return { targetFile: target, consumed: segments.length - rest.length + used, remaining: rest.slice(used), anchored };
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return null;
|
|
155
|
-
};
|
|
52
|
+
const { resolveRustMod, resolveRustPath } = createRustResolvers(fileSet);
|
|
156
53
|
|
|
157
54
|
// Path aliases (tsconfig compilerOptions.paths + vite/webpack alias) are scoped to their config folder.
|
|
158
55
|
// Without nearest-config resolution, a monorepo's root `@/*` can hijack the same alias in web/.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {join} from 'node:path'
|
|
2
|
+
|
|
3
|
+
const cleanRustRel = (path) => String(path || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/').replace(/\/$/, '').replace(/^\.$/, '')
|
|
4
|
+
const rustDir = (path) => {
|
|
5
|
+
const clean = cleanRustRel(path)
|
|
6
|
+
const index = clean.lastIndexOf('/')
|
|
7
|
+
return index < 0 ? '' : clean.slice(0, index)
|
|
8
|
+
}
|
|
9
|
+
const rustBase = (path) => {
|
|
10
|
+
const clean = cleanRustRel(path)
|
|
11
|
+
const index = clean.lastIndexOf('/')
|
|
12
|
+
return index < 0 ? clean : clean.slice(index + 1)
|
|
13
|
+
}
|
|
14
|
+
const rustJoin = (...parts) => cleanRustRel(join(...parts.filter((part) => part != null && part !== '')))
|
|
15
|
+
|
|
16
|
+
export function createRustResolvers(fileSet) {
|
|
17
|
+
const rustFiles = new Set([...fileSet].filter((file) => file.endsWith('.rs')))
|
|
18
|
+
const rustRoots = new Map()
|
|
19
|
+
for (const file of rustFiles) {
|
|
20
|
+
const base = rustBase(file)
|
|
21
|
+
if (base !== 'lib.rs' && base !== 'main.rs') continue
|
|
22
|
+
const dir = rustDir(file)
|
|
23
|
+
let root = rustRoots.get(dir)
|
|
24
|
+
if (!root) rustRoots.set(dir, (root = {base: dir, lib: null, main: null}))
|
|
25
|
+
root[base === 'lib.rs' ? 'lib' : 'main'] = file
|
|
26
|
+
}
|
|
27
|
+
const rustRootList = [...rustRoots.values()].sort((a, b) => b.base.length - a.base.length)
|
|
28
|
+
|
|
29
|
+
const rustContext = (fromRel) => {
|
|
30
|
+
const clean = cleanRustRel(fromRel)
|
|
31
|
+
const base = rustBase(clean)
|
|
32
|
+
const dir = rustDir(clean)
|
|
33
|
+
if (base === 'lib.rs' || base === 'main.rs') return {base: dir, rootFile: clean}
|
|
34
|
+
if (/^(?:bin|examples|tests|benches)$/.test(rustBase(dir))) return {base: dir, rootFile: clean}
|
|
35
|
+
for (const root of rustRootList) {
|
|
36
|
+
if (!root.base || clean.startsWith(root.base + '/')) return {base: root.base, rootFile: root.lib || root.main}
|
|
37
|
+
}
|
|
38
|
+
return {base: dir, rootFile: clean}
|
|
39
|
+
}
|
|
40
|
+
const rustModuleBase = (fromRel) => {
|
|
41
|
+
const context = rustContext(fromRel)
|
|
42
|
+
if (context.rootFile === cleanRustRel(fromRel)) return rustDir(fromRel)
|
|
43
|
+
const name = rustBase(fromRel)
|
|
44
|
+
if (name === 'lib.rs' || name === 'main.rs' || name === 'mod.rs') return rustDir(fromRel)
|
|
45
|
+
return rustJoin(rustDir(fromRel), name.replace(/\.rs$/, ''))
|
|
46
|
+
}
|
|
47
|
+
const rustInlineBase = (fromRel, inlineModules = []) => {
|
|
48
|
+
let base = rustModuleBase(fromRel)
|
|
49
|
+
for (let index = 0; index < inlineModules.length; index++) {
|
|
50
|
+
const module = inlineModules[index] || {}
|
|
51
|
+
if (module.path) {
|
|
52
|
+
const parent = index === 0 ? rustDir(fromRel) : base
|
|
53
|
+
base = rustJoin(parent, module.path)
|
|
54
|
+
} else base = rustJoin(base, module.name)
|
|
55
|
+
}
|
|
56
|
+
return base
|
|
57
|
+
}
|
|
58
|
+
const rustModuleFile = (moduleBase, context) => {
|
|
59
|
+
const clean = cleanRustRel(moduleBase)
|
|
60
|
+
if (clean === cleanRustRel(context.base) && context.rootFile && rustFiles.has(context.rootFile)) return context.rootFile
|
|
61
|
+
const flat = clean + '.rs'
|
|
62
|
+
if (rustFiles.has(flat)) return flat
|
|
63
|
+
const legacy = rustJoin(clean, 'mod.rs')
|
|
64
|
+
return rustFiles.has(legacy) ? legacy : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const resolveRustMod = (fromRel, name, {inlineModules = [], explicitPath = ''} = {}) => {
|
|
68
|
+
const clean = cleanRustRel(fromRel)
|
|
69
|
+
if (explicitPath) {
|
|
70
|
+
const parent = inlineModules.length ? rustInlineBase(clean, inlineModules) : rustDir(clean)
|
|
71
|
+
const target = rustJoin(parent, explicitPath)
|
|
72
|
+
return rustFiles.has(target) ? target : null
|
|
73
|
+
}
|
|
74
|
+
const targetBase = rustJoin(rustInlineBase(clean, inlineModules), String(name || '').replace(/^r#/, ''))
|
|
75
|
+
return rustModuleFile(targetBase, rustContext(clean))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const resolveRustPath = (fromRel, rawSegments, {inlineModules = [], unqualified = true} = {}) => {
|
|
79
|
+
const clean = cleanRustRel(fromRel)
|
|
80
|
+
const segments = (Array.isArray(rawSegments) ? rawSegments : String(rawSegments || '').split('::'))
|
|
81
|
+
.map((segment) => String(segment).trim().replace(/^r#/, '')).filter(Boolean)
|
|
82
|
+
if (!segments.length) return null
|
|
83
|
+
const context = rustContext(clean)
|
|
84
|
+
const current = rustInlineBase(clean, inlineModules)
|
|
85
|
+
const rest = [...segments]
|
|
86
|
+
const starts = []
|
|
87
|
+
let anchored = false
|
|
88
|
+
if (rest[0] === 'crate') { anchored = true; rest.shift(); starts.push(context.base) }
|
|
89
|
+
else if (rest[0] === 'self') { anchored = true; rest.shift(); starts.push(current) }
|
|
90
|
+
else if (rest[0] === 'super') {
|
|
91
|
+
anchored = true
|
|
92
|
+
let base = current
|
|
93
|
+
while (rest[0] === 'super') { rest.shift(); base = rustDir(base) }
|
|
94
|
+
if (context.base && base !== context.base && !base.startsWith(context.base + '/')) return null
|
|
95
|
+
starts.push(base)
|
|
96
|
+
} else if (unqualified) {
|
|
97
|
+
starts.push(context.base)
|
|
98
|
+
if (current !== context.base) starts.push(current)
|
|
99
|
+
} else return null
|
|
100
|
+
|
|
101
|
+
for (const start of starts) {
|
|
102
|
+
const minimum = anchored ? 0 : 1
|
|
103
|
+
for (let used = rest.length; used >= minimum; used--) {
|
|
104
|
+
const target = rustModuleFile(rustJoin(start, ...rest.slice(0, used)), context)
|
|
105
|
+
if (target) return {
|
|
106
|
+
targetFile: target,
|
|
107
|
+
consumed: segments.length - rest.length + used,
|
|
108
|
+
remaining: rest.slice(used),
|
|
109
|
+
anchored,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {resolveRustMod, resolveRustPath}
|
|
117
|
+
}
|