weavatrix 0.1.3 → 0.1.4

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.
@@ -1,29 +1,192 @@
1
- // Java extractor. Symbols: class/interface/enum + method/constructor + fields.
2
- // Imports: `import a.b.C;` the C.java file (package path = dir; suffix-matched). Calls: `x.method()` /
3
- // `method()` (name only). Heritage: `extends` + `implements`.
4
- const SYMS = `
1
+ // Java extractor. Keeps file-level imports/calls while modelling the Java ownership and type system:
2
+ // classes/interfaces/enums/records own their methods, heritage distinguishes extends from implements,
3
+ // and project-local type references resolve to the declaration node (never to synthetic type-name nodes).
4
+ const TYPE_CORE = `
5
5
  (class_declaration name: (identifier) @class)
6
- (interface_declaration name: (identifier) @class)
7
- (enum_declaration name: (identifier) @class)
6
+ (interface_declaration name: (identifier) @interface)
7
+ (enum_declaration name: (identifier) @enum)`;
8
+ // Keep grammar-version-dependent declarations separate: one unknown node type invalidates a whole query.
9
+ const TYPE_OPTIONAL = [
10
+ `(record_declaration name: (identifier) @record)`,
11
+ `(annotation_type_declaration name: (identifier) @annotation)`,
12
+ ];
13
+ const MEMBERS = `
8
14
  (method_declaration name: (identifier) @method)
9
- (constructor_declaration name: (identifier) @method)
15
+ (constructor_declaration name: (identifier) @constructor)
10
16
  (field_declaration declarator: (variable_declarator name: (identifier) @field))`;
11
17
 
18
+ const TYPE_DECLARATIONS = new Set([
19
+ "class_declaration", "interface_declaration", "enum_declaration",
20
+ "record_declaration", "annotation_type_declaration",
21
+ ]);
22
+ const FIELD_DECLARATIONS = new Set(["field_declaration"]);
23
+
24
+ const ancestor = (node, accepted) => {
25
+ let current = node?.parent;
26
+ for (let hops = 0; current && hops < 12; hops++, current = current.parent) {
27
+ if (accepted.has(current.type)) return current;
28
+ }
29
+ return null;
30
+ };
31
+
32
+ const visibilityOf = (declaration, owner) => {
33
+ const modifiers = declaration?.namedChildren?.find((node) => node.type === "modifiers")?.text || "";
34
+ if (/\bprivate\b/.test(modifiers)) return "private";
35
+ if (/\bprotected\b/.test(modifiers)) return "protected";
36
+ if (/\bpublic\b/.test(modifiers)) return "public";
37
+ if (["interface_declaration", "annotation_type_declaration"].includes(owner?.type)) return "public";
38
+ return "package";
39
+ };
40
+
41
+ const lineOf = (node) => node.startPosition.row + 1;
42
+ const symbolBaseId = (fileRel, nameNode) => `${fileRel}#${nameNode.text}@${lineOf(nameNode)}`;
43
+ const declarationKey = (node) => `${node?.startIndex ?? -1}:${node?.endIndex ?? -1}`;
44
+ const exactJavaTarget = (resolveJavaImport, parts) => {
45
+ const target = resolveJavaImport(parts);
46
+ const suffix = parts.join("/") + ".java";
47
+ return target && (target === suffix || target.endsWith("/" + suffix)) ? target : null;
48
+ };
49
+
12
50
  export default {
13
51
  family: "java",
14
52
  grammars: ["java"],
15
53
  exts: { ".java": "java" },
16
54
  isWeb: false,
17
55
  calls: `(method_invocation name: (identifier) @callee)`,
18
- heritage: [`(superclass (type_identifier) @super)`, `(super_interfaces (type_list (type_identifier) @super))`],
56
+ // Capturing the base type_identifier (rather than the whole generic/scoped type) gives the shared
57
+ // resolver the imported/local declaration name. Each query is deliberately non-overlapping.
58
+ heritage: [
59
+ { relation: "inherits", query: `(superclass (type_identifier) @super)` },
60
+ { relation: "inherits", query: `(superclass (generic_type (type_identifier) @super))` },
61
+ { relation: "inherits", query: `(superclass (scoped_type_identifier name: (type_identifier) @super))` },
62
+ { relation: "inherits", query: `(superclass (generic_type (scoped_type_identifier name: (type_identifier) @super)))` },
63
+ { relation: "inherits", query: `(extends_interfaces (type_list (type_identifier) @super))` },
64
+ { relation: "inherits", query: `(extends_interfaces (type_list (generic_type (type_identifier) @super)))` },
65
+ { relation: "inherits", query: `(extends_interfaces (type_list (scoped_type_identifier name: (type_identifier) @super)))` },
66
+ { relation: "inherits", query: `(extends_interfaces (type_list (generic_type (scoped_type_identifier name: (type_identifier) @super))))` },
67
+ { relation: "implements", query: `(super_interfaces (type_list (type_identifier) @super))` },
68
+ { relation: "implements", query: `(super_interfaces (type_list (generic_type (type_identifier) @super)))` },
69
+ { relation: "implements", query: `(super_interfaces (type_list (scoped_type_identifier name: (type_identifier) @super)))` },
70
+ { relation: "implements", query: `(super_interfaces (type_list (generic_type (scoped_type_identifier name: (type_identifier) @super))))` },
71
+ ],
19
72
 
20
73
  pass1(ctx) {
21
- const { grammar, tree, caps, addSym, addImportEdge, imports, resolveJavaImport } = ctx;
22
- for (const cap of caps(grammar, SYMS, tree.rootNode)) addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
23
- for (const cap of caps(grammar, `(import_declaration (scoped_identifier) @imp)`, tree.rootNode)) {
24
- const parts = cap.node.text.split("."); const cls = parts[parts.length - 1];
25
- const tgt = resolveJavaImport(parts); if (!tgt) continue;
26
- addImportEdge(tgt); imports.set(cls, { imported: "*", targetFile: tgt });
74
+ const {
75
+ grammar, tree, fileRel, caps, field, addSym, addImportEdge, imports,
76
+ resolveJavaImport, fileSet, links, nodeIds,
77
+ } = ctx;
78
+ const ownerIds = new Map();
79
+ const addJavaSym = (nameNode, callable, extra) => {
80
+ const base = symbolBaseId(fileRel, nameNode);
81
+ // Compact/generated Java can put an owner, constructor and overloads with the same name on one
82
+ // line. Preserve historical IDs normally; disambiguate only an actual collision by source column.
83
+ const id = nodeIds.has(base) ? `${base}:c${nameNode.startPosition.column + 1}` : base;
84
+ addSym(nameNode.text, lineOf(nameNode), callable, {
85
+ ...extra,
86
+ ...(id === base ? {} : { idSuffix: id.slice(base.length) }),
87
+ });
88
+ return nodeIds.has(id) ? id : null;
89
+ };
90
+
91
+ // Add owners first even though tree-sitter captures are source-ordered. This makes ownership edges
92
+ // deterministic for nested types and methods declared before/after other nested declarations.
93
+ for (const src of [TYPE_CORE, ...TYPE_OPTIONAL]) {
94
+ for (const cap of caps(grammar, src, tree.rootNode)) {
95
+ const declaration = cap.node.parent;
96
+ const id = addJavaSym(cap.node, false, {
97
+ sourceNode: cap.node.parent,
98
+ symbolKind: cap.name,
99
+ });
100
+ if (id) ownerIds.set(declarationKey(declaration), id);
101
+ }
102
+ }
103
+
104
+ for (const cap of caps(grammar, MEMBERS, tree.rootNode)) {
105
+ const declaration = cap.name === "field" ? ancestor(cap.node, FIELD_DECLARATIONS) : cap.node.parent;
106
+ const owner = ancestor(cap.node, TYPE_DECLARATIONS);
107
+ const ownerNameNode = owner && field(owner, "name");
108
+ const memberKind = cap.name === "constructor" ? "constructor" : cap.name;
109
+ const memberId = addJavaSym(cap.node, cap.name !== "field", {
110
+ sourceNode: declaration,
111
+ symbolKind: memberKind,
112
+ ...(ownerNameNode ? { memberOf: ownerNameNode.text } : {}),
113
+ visibility: visibilityOf(declaration, owner),
114
+ });
115
+ if (!ownerNameNode || cap.name === "field") continue;
116
+ const ownerId = ownerIds.get(declarationKey(owner));
117
+ if (ownerId && memberId && ownerId !== memberId) {
118
+ links.push({ source: ownerId, target: memberId, relation: "method", confidence: "EXTRACTED" });
119
+ }
120
+ }
121
+
122
+ const wildcardPackages = [];
123
+ const staticWildcardTargets = [];
124
+ const importStatements = caps(grammar, `(import_declaration) @imp`, tree.rootNode);
125
+ for (const cap of importStatements) {
126
+ const match = cap.node.text.match(/^\s*import\s+(static\s+)?([\w.]+?)(\.\*)?\s*;?\s*$/);
127
+ if (!match) continue;
128
+ const isStatic = !!match[1];
129
+ const parts = match[2].split(".").filter(Boolean);
130
+ const wildcard = !!match[3];
131
+ const line = lineOf(cap.node);
132
+ if (!isStatic && wildcard) {
133
+ wildcardPackages.push(parts);
134
+ continue;
135
+ }
136
+ if (!isStatic) {
137
+ const target = exactJavaTarget(resolveJavaImport, parts);
138
+ if (!target) continue;
139
+ const local = parts[parts.length - 1];
140
+ addImportEdge(target, { line, specifier: parts.join("."), compileOnly: true });
141
+ imports.set(local, { imported: local, targetFile: target });
142
+ continue;
143
+ }
144
+
145
+ // Static imports end in a member name. Walk prefixes until the declaring project class resolves.
146
+ let target = null; let classParts = null;
147
+ const max = wildcard ? parts.length : parts.length - 1;
148
+ for (let take = max; take > 0 && !target; take--) {
149
+ const candidate = parts.slice(0, take);
150
+ target = exactJavaTarget(resolveJavaImport, candidate);
151
+ if (target) classParts = candidate;
152
+ }
153
+ if (!target) continue;
154
+ addImportEdge(target, { line, specifier: `${isStatic ? "static " : ""}${parts.join(".")}${wildcard ? ".*" : ""}`, compileOnly: true });
155
+ if (wildcard) staticWildcardTargets.push(target);
156
+ else {
157
+ const member = parts[classParts.length];
158
+ if (member) imports.set(member, { imported: member, targetFile: target });
159
+ }
160
+ }
161
+
162
+ // Seed the shared pass-2 resolver with Java's implicit same-package types and wildcard imports.
163
+ // Exact-path validation is important: the general basename fallback is useful for explicit imports,
164
+ // but must not bind an unimported Foo to an unrelated package's Foo.java.
165
+ const slash = fileRel.lastIndexOf("/");
166
+ const ownDir = slash < 0 ? "" : fileRel.slice(0, slash);
167
+ const bindType = (name) => {
168
+ if (!name || imports.has(name)) return;
169
+ const samePackage = `${ownDir ? ownDir + "/" : ""}${name}.java`;
170
+ if (fileSet.has(samePackage)) {
171
+ imports.set(name, { imported: name, targetFile: samePackage });
172
+ return;
173
+ }
174
+ for (const pkg of wildcardPackages) {
175
+ const target = exactJavaTarget(resolveJavaImport, [...pkg, name]);
176
+ if (target) { imports.set(name, { imported: name, targetFile: target }); return; }
177
+ }
178
+ };
179
+ for (const cap of caps(grammar, `(type_identifier) @type`, tree.rootNode)) bindType(cap.node.text);
180
+ for (const cap of caps(grammar, `(scoped_type_identifier) @type`, tree.rootNode)) {
181
+ const parts = cap.node.text.split(".").filter(Boolean);
182
+ const target = exactJavaTarget(resolveJavaImport, parts);
183
+ const name = parts[parts.length - 1];
184
+ if (target && name && !imports.has(name)) imports.set(name, { imported: name, targetFile: target });
185
+ }
186
+ if (staticWildcardTargets.length === 1) {
187
+ for (const cap of caps(grammar, `(method_invocation name: (identifier) @callee)`, tree.rootNode)) {
188
+ if (!imports.has(cap.node.text)) imports.set(cap.node.text, { imported: cap.node.text, targetFile: staticWildcardTargets[0] });
189
+ }
27
190
  }
28
191
  },
29
192
  };
@@ -1,10 +1,8 @@
1
1
  // Rust extractor. Symbols: fn (incl. impl methods) + struct/enum/trait/type/mod + const/static
2
2
  // (+ macro_rules!/union via optional queries). Calls: `foo()` / `x.foo()` / `path::foo()` (name only).
3
- // Heritage: `impl Trait for Type` resolves only when the impl block sits inside a tracked symbol
4
- // (e.g. a mod) a top-level impl has no enclosing symbol to anchor the edge, so v1 usually skips it.
5
- // `use` paths map to files only through the crate's module tree, which needs Cargo layout resolution —
6
- // no import edges in v1; cross-file connectivity comes from the name-based calls pass plus the
7
- // same-folder scope (mod.rs convention).
3
+ // File dependencies follow Rust's module tree: outlined `mod`, `use` trees, public re-exports, and anchored
4
+ // `crate/self/super` paths resolve to repo-local .rs or */mod.rs files. External crates deliberately stay out
5
+ // of this adapter; Cargo dependency analysis owns them.
8
6
  const SYMS_CORE = `
9
7
  (function_item name: (identifier) @method)
10
8
  (struct_item name: (type_identifier) @class)
@@ -20,6 +18,66 @@ const SYMS_OPTIONAL = [
20
18
  `(union_item name: (type_identifier) @class)`,
21
19
  ];
22
20
 
21
+ const cleanSegment = (part) => String(part || "").trim().replace(/^r#/, "");
22
+ const pathParts = (node) => String(node?.text || "").split("::").map(cleanSegment).filter(Boolean);
23
+ const under = (node, type) => { for (let p = node?.parent; p; p = p.parent) if (p.type === type) return true; return false; };
24
+
25
+ function pathAttribute(modNode) {
26
+ for (let prev = modNode?.previousNamedSibling; prev?.type === "attribute_item"; prev = prev.previousNamedSibling) {
27
+ const hashedRaw = prev.text.match(/\bpath\s*=\s*r(#+)"([^"]+)"\1/);
28
+ if (hashedRaw) return hashedRaw[2] || "";
29
+ const plain = prev.text.match(/\bpath\s*=\s*(?:r)?"([^"]+)"/);
30
+ if (plain) return plain[1] || "";
31
+ }
32
+ return "";
33
+ }
34
+
35
+ function inlineAncestors(node) {
36
+ const result = [];
37
+ for (let p = node?.parent; p; p = p.parent) {
38
+ if (p.type !== "declaration_list" || p.parent?.type !== "mod_item") continue;
39
+ const mod = p.parent;
40
+ const name = mod.namedChildren.find((child) => child.type === "identifier")?.text;
41
+ if (name) result.unshift({ name: cleanSegment(name), path: pathAttribute(mod) });
42
+ }
43
+ return result;
44
+ }
45
+
46
+ // Expand a Rust use tree into its leaf paths while retaining aliases. Examples:
47
+ // `crate::api::{self, Client as C, types::*}` -> api, api::Client, api::types.
48
+ function useLeaves(node, prefix = []) {
49
+ if (!node) return [];
50
+ if (node.type === "use_declaration") {
51
+ const body = node.namedChildren.find((child) => child.type !== "visibility_modifier");
52
+ return useLeaves(body, prefix);
53
+ }
54
+ if (node.type === "use_list") return node.namedChildren.flatMap((child) => useLeaves(child, prefix));
55
+ if (node.type === "scoped_use_list") {
56
+ const list = node.namedChildren.find((child) => child.type === "use_list");
57
+ const head = node.namedChildren.find((child) => child !== list);
58
+ return useLeaves(list, [...prefix, ...pathParts(head)]);
59
+ }
60
+ if (node.type === "use_as_clause") {
61
+ const named = node.namedChildren;
62
+ const alias = cleanSegment(named[named.length - 1]?.text);
63
+ return useLeaves(named[0], prefix).map((leaf) => ({ ...leaf, local: alias }));
64
+ }
65
+ if (node.type === "use_wildcard") {
66
+ return [{ segments: [...prefix, ...pathParts(node.namedChildren[0])], wildcard: true, local: null }];
67
+ }
68
+ if (node.type === "self") {
69
+ const local = [...prefix].reverse().find((part) => !["crate", "self", "super"].includes(part)) || null;
70
+ return [{ segments: prefix.length ? [...prefix] : ["self"], moduleSelf: true, local }];
71
+ }
72
+ if (["identifier", "scoped_identifier", "scoped_type_identifier", "crate", "super"].includes(node.type)) {
73
+ const own = pathParts(node);
74
+ const segments = [...prefix, ...own];
75
+ const local = [...own].reverse().find((part) => !["crate", "self", "super"].includes(part)) || null;
76
+ return [{ segments, local }];
77
+ }
78
+ return [];
79
+ }
80
+
23
81
  export default {
24
82
  family: "rust",
25
83
  grammars: ["rust"],
@@ -35,11 +93,76 @@ export default {
35
93
  ],
36
94
 
37
95
  pass1(ctx) {
38
- const { grammar, tree, caps, addSym } = ctx;
96
+ const { grammar, tree, fileRel, caps, addSym, addImportEdge, imports, resolveRustMod, resolveRustPath } = ctx;
39
97
  for (const src of [SYMS_CORE, ...SYMS_OPTIONAL]) {
40
98
  for (const cap of caps(grammar, src, tree.rootNode)) {
41
99
  addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
42
100
  }
43
101
  }
102
+
103
+ // File dependency edges are intentionally unique per source/target/relation. A qualified path can be
104
+ // nested in another qualified path and often repeats an existing `mod`/`use`; counting occurrences would
105
+ // inflate module coupling without adding structure.
106
+ const emitted = new Set();
107
+ const emit = (target, meta = {}) => {
108
+ if (!target || target === fileRel) return;
109
+ const relation = meta.relation || "imports";
110
+ const key = relation + ">" + target;
111
+ if (emitted.has(key)) return;
112
+ emitted.add(key);
113
+ // Rust `mod`/`use`/`pub use` relationships are resolved by the compiler. They describe real
114
+ // architectural coupling, but they are not runtime module loading and therefore must not create
115
+ // JavaScript-style initialization cycles or runtime boundary/blast-radius findings.
116
+ addImportEdge(target, { ...meta, relation, compileOnly: true });
117
+ };
118
+
119
+ // `mod foo;` loads foo.rs or foo/mod.rs. Inline `mod foo { ... }` stays in the current file, but any
120
+ // outlined modules declared inside it are captured separately with their inline ancestor path.
121
+ for (const cap of caps(grammar, `(mod_item) @mod`, tree.rootNode)) {
122
+ const mod = cap.node;
123
+ if (mod.namedChildren.some((child) => child.type === "declaration_list")) continue;
124
+ const name = mod.namedChildren.find((child) => child.type === "identifier")?.text;
125
+ if (!name) continue;
126
+ const target = resolveRustMod(fileRel, cleanSegment(name), {
127
+ inlineModules: inlineAncestors(mod),
128
+ explicitPath: pathAttribute(mod),
129
+ });
130
+ emit(target, { line: mod.startPosition.row + 1, specifier: `mod ${name}` });
131
+ }
132
+
133
+ // Imports/re-exports may be nested trees and aliases. Besides the file edge, retain direct item aliases
134
+ // so the existing pass-2 call resolver can connect `use crate::worker::run; run()` across folders.
135
+ for (const cap of caps(grammar, `(use_declaration) @use`, tree.rootNode)) {
136
+ const use = cap.node;
137
+ const relation = use.namedChildren.some((child) => child.type === "visibility_modifier") ? "re_exports" : "imports";
138
+ const ancestors = inlineAncestors(use);
139
+ for (const leaf of useLeaves(use)) {
140
+ const resolved = resolveRustPath(fileRel, leaf.segments, { inlineModules: ancestors, unqualified: true });
141
+ if (!resolved) continue;
142
+ emit(resolved.targetFile, { relation, line: use.startPosition.row + 1, specifier: use.text.replace(/;\s*$/, "") });
143
+ if (!leaf.wildcard && leaf.local && leaf.local !== "_") {
144
+ const imported = resolved.remaining.length ? cleanSegment(resolved.remaining.at(-1)) : "*";
145
+ imports.set(cleanSegment(leaf.local), { imported, targetFile: resolved.targetFile, rustModule: imported === "*" });
146
+ }
147
+ }
148
+ }
149
+
150
+ // Fully-qualified paths need no `use` declaration but still prove a dependency. Restrict this pass to
151
+ // explicit crate/self/super anchors so associated paths such as `Type::method` cannot be mistaken for a
152
+ // same-named module file. Inner prefixes are skipped; only the maximal path emits an edge.
153
+ for (const cap of caps(grammar, `[(scoped_identifier) (scoped_type_identifier)] @path`, tree.rootNode)) {
154
+ const node = cap.node;
155
+ if (under(node, "use_declaration")) continue;
156
+ if (["scoped_identifier", "scoped_type_identifier"].includes(node.parent?.type)) continue;
157
+ const segments = pathParts(node);
158
+ if (!["crate", "self", "super"].includes(segments[0])) continue;
159
+ const resolved = resolveRustPath(fileRel, segments, { inlineModules: inlineAncestors(node), unqualified: false });
160
+ if (!resolved) continue;
161
+ emit(resolved.targetFile, { line: node.startPosition.row + 1, specifier: node.text });
162
+ const finalName = cleanSegment(segments.at(-1));
163
+ if (resolved.remaining.length && finalName && !imports.has(finalName)) {
164
+ imports.set(finalName, { imported: finalName, targetFile: resolved.targetFile, rustQualified: true });
165
+ }
166
+ }
44
167
  },
45
168
  };
@@ -0,0 +1,17 @@
1
+ // Deterministic community territories. Most languages keep the historical top-two-folder bucket. Java
2
+ // Maven/Gradle trees need package depth: otherwise every class under application/src becomes one giant blob.
3
+ export function communityTerritoryOf(file) {
4
+ const normalized = String(file || "").replace(/\\/g, "/");
5
+ if (/\.java$/i.test(normalized)) {
6
+ const marker = /(?:^|\/)src\/(?:main|test)\/java\//.exec(normalized);
7
+ if (marker) {
8
+ const end = marker.index + marker[0].length;
9
+ const prefix = normalized.slice(0, end).replace(/\/$/, "");
10
+ const packageDirs = normalized.slice(end).split("/").filter(Boolean).slice(0, -1);
11
+ if (packageDirs.length) return `${prefix}/${packageDirs.slice(0, 5).join("/")}`;
12
+ return prefix;
13
+ }
14
+ }
15
+ const dirs = normalized.split("/").filter(Boolean).slice(0, -1);
16
+ return dirs.length ? dirs.slice(0, 2).join("/") : "(root)";
17
+ }
@@ -8,6 +8,8 @@ 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
+ import { communityTerritoryOf } from "./community.js";
11
13
 
12
14
  // Parse a repo directory into a graph-builder-compatible { nodes, links } graph.
13
15
  export async function buildInternalGraph(repoDir, opts = {}) {
@@ -56,7 +58,8 @@ export async function buildInternalGraph(repoDir, opts = {}) {
56
58
  const syms = []; const nameToId = new Map(); const moduleNameToId = new Map();
57
59
  const addSym = (name, line, callable, extra) => {
58
60
  if (!name || !/^[A-Za-z_$][\w$]*$/.test(name)) return;
59
- const id = `${fileRel}#${name}@${line}`; if (nodeIds.has(id)) return;
61
+ const suffix = /^:[A-Za-z0-9_-]+$/.test(extra?.idSuffix || "") ? extra.idSuffix : "";
62
+ const id = `${fileRel}#${name}@${line}${suffix}`; if (nodeIds.has(id)) return;
60
63
  const sourceNode = extra && extra.sourceNode;
61
64
  const endLine = sourceNode?.endPosition ? sourceNode.endPosition.row + 1 : 0;
62
65
  let complexity = null;
@@ -82,6 +85,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
82
85
  syms.push({ id, name, start: line, end: endLine >= line ? endLine : 0 });
83
86
  if (!nameToId.has(name)) nameToId.set(name, id);
84
87
  if (extra?.moduleDeclaration && !moduleNameToId.has(name)) moduleNameToId.set(name, id);
88
+ return id;
85
89
  };
86
90
  const imports = new Map(); importedLocals.set(fileRel, imports);
87
91
  const addImportEdge = (tgt, meta = {}) => {
@@ -92,6 +96,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
92
96
  relation: meta.relation || "imports",
93
97
  confidence: "EXTRACTED",
94
98
  ...(typeof meta.typeOnly === "boolean" ? { typeOnly: meta.typeOnly } : {}),
99
+ ...(meta.compileOnly === true ? { compileOnly: true } : {}),
95
100
  ...(meta.line ? { line: meta.line } : {}),
96
101
  ...(meta.specifier ? { specifier: meta.specifier } : {}),
97
102
  });
@@ -152,6 +157,17 @@ export async function buildInternalGraph(repoDir, opts = {}) {
152
157
  if (imp && imp.targetFile) { const tf = symByFileName.get(imp.targetFile); if (tf && tf.has(imp.imported)) return tf.get(imp.imported); }
153
158
  return null;
154
159
  };
160
+ const javaTypeKinds = new Set(["class", "interface", "enum", "record", "annotation"]);
161
+ const resolveJavaType = (name, fileRel) => {
162
+ const imp = importedLocals.get(fileRel)?.get(name);
163
+ if (imp?.targetFile) {
164
+ const symbols = symByFileName.get(imp.targetFile);
165
+ const target = symbols?.get(imp.imported) || symbols?.get(name);
166
+ if (target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind)) return target;
167
+ }
168
+ const target = symByFileName.get(fileRel)?.get(name);
169
+ return target && javaTypeKinds.has(nodeById.get(target)?.symbol_kind) ? target : null;
170
+ };
155
171
  for (const abs of files) {
156
172
  const fileRel = rel(abs); const grammar = EXT_LANG[extname(abs)]; if (!grammar) continue;
157
173
  const lang = LANGS[FAMILY[grammar]]; if (!lang || lang.isWeb || !langs[grammar]) continue;
@@ -175,10 +191,14 @@ export async function buildInternalGraph(repoDir, opts = {}) {
175
191
  const target = dm && dm.get(fld.text);
176
192
  if (target && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED" });
177
193
  }
178
- for (const heritageSrc of lang.heritage || []) for (const cap of caps(grammar, heritageSrc, tree.rootNode)) {
179
- const cls = enclosing(fileRel, cap.node.startPosition.row + 1); if (!cls) continue;
180
- const target = resolveCall(cap.node.text, fileRel);
181
- if (target && target !== cls.id) links.push({ source: cls.id, target, relation: "inherits", confidence: "INFERRED" });
194
+ for (const heritageSpec of lang.heritage || []) {
195
+ const query = typeof heritageSpec === "string" ? heritageSpec : heritageSpec.query;
196
+ const relation = typeof heritageSpec === "string" ? "inherits" : (heritageSpec.relation || "inherits");
197
+ for (const cap of caps(grammar, query, tree.rootNode)) {
198
+ const cls = enclosing(fileRel, cap.node.startPosition.row + 1); if (!cls) continue;
199
+ const target = FAMILY[grammar] === "java" ? resolveJavaType(cap.node.text, fileRel) : resolveCall(cap.node.text, fileRel);
200
+ if (target && target !== cls.id) links.push({ source: cls.id, target, relation, confidence: "INFERRED" });
201
+ }
182
202
  }
183
203
  // JSX is a real symbol use even though it is not a call_expression. Resolve imported components to their
184
204
  // declaration so component fan-in and unused-export checks do not claim `<SettingsView />` is unreferenced.
@@ -222,6 +242,9 @@ export async function buildInternalGraph(repoDir, opts = {}) {
222
242
  const caller = enclosing(fileRel, sel.startPosition.row + 1); emitRef(caller ? caller.id : fileRel, target);
223
243
  }
224
244
  }
245
+ if (FAMILY[grammar] === "java") {
246
+ addJavaReferences({ grammar, tree, fileRel, caps, resolveJavaType, enclosing, links });
247
+ }
225
248
  tree.delete();
226
249
  }
227
250
 
@@ -238,13 +261,14 @@ export async function buildInternalGraph(repoDir, opts = {}) {
238
261
 
239
262
  // community = folder bucket (top 2 path parts) — deterministic, mirrors the folder-based module grouping the
240
263
  // app already uses (graph-builder-analysis.js). Populates Modules/community cards without a heavy clustering pass.
241
- const folderOf = (f) => { const d = String(f || "").split("/").filter(Boolean).slice(0, -1); return d.length ? d.slice(0, 2).join("/") : "(root)"; };
242
264
  const commOf = new Map(); let commSeq = 0;
243
- for (const n of nodes) { const fo = folderOf(n.source_file); if (!commOf.has(fo)) commOf.set(fo, commSeq++); n.community = commOf.get(fo); }
265
+ for (const n of nodes) { const territory = communityTerritoryOf(n.source_file); if (!commOf.has(territory)) commOf.set(territory, commSeq++); n.community = commOf.get(territory); }
244
266
 
245
267
  // extImportsV: bump when the externalImports schema/coverage changes (v2 = go/python ecosystems) —
246
268
  // deps-engine rebuilds in memory when a saved graph is older than this.
247
- return { nodes, links, externalImports, extImportsV: 2, edgeTypesV: 1, complexityV: 1, repoBoundaryV: 1 };
269
+ // edgeTypesV 2 adds language-neutral compile-only edges (currently Rust mod/use/re-export) on top
270
+ // of v1's TypeScript typeOnly classification.
271
+ return { nodes, links, externalImports, extImportsV: 2, edgeTypesV: 2, complexityV: 1, repoBoundaryV: 1 };
248
272
  }
249
273
 
250
274
  // Build + write graph.json to outPath (creating the dir). Returns { ok, nodes, links, graphJson }.
@@ -0,0 +1,33 @@
1
+ // Resolve Java type usages only when they land on declarations that exist in the graph. Synthetic nodes for
2
+ // String/List/annotations inflate metrics without adding navigation value, so unresolved/external types vanish.
3
+ export function addJavaReferences({ grammar, tree, fileRel, caps, resolveJavaType, enclosing, links }) {
4
+ const refSeen = new Set();
5
+ const isHeritage = (node) => {
6
+ let current = node?.parent;
7
+ for (let hops = 0; current && hops < 8; hops++, current = current.parent) {
8
+ if (["superclass", "super_interfaces", "extends_interfaces"].includes(current.type)) return true;
9
+ if (["class_declaration", "interface_declaration", "enum_declaration", "record_declaration"].includes(current.type)) return false;
10
+ }
11
+ return false;
12
+ };
13
+ const emitTypeRef = (name, node) => {
14
+ if (!name || isHeritage(node)) return;
15
+ const target = resolveJavaType(name, fileRel);
16
+ if (!target) return;
17
+ const owner = enclosing(fileRel, node.startPosition.row + 1);
18
+ const source = owner?.id || fileRel;
19
+ if (source === target) return;
20
+ const key = `${source}>${target}`;
21
+ if (refSeen.has(key)) return;
22
+ refSeen.add(key);
23
+ links.push({ source, target, relation: "references", confidence: "INFERRED", usage: "type", line: node.startPosition.row + 1 });
24
+ };
25
+ for (const cap of caps(grammar, `(type_identifier) @type`, tree.rootNode)) {
26
+ // A qualified type is handled once by its outer scoped_type_identifier. Inner package segments could
27
+ // otherwise bind to unrelated same-named project types.
28
+ if (cap.node.parent?.type !== "scoped_type_identifier") emitTypeRef(cap.node.text, cap.node);
29
+ }
30
+ for (const cap of caps(grammar, `(scoped_type_identifier) @type`, tree.rootNode)) {
31
+ if (cap.node.parent?.type !== "scoped_type_identifier") emitTypeRef(cap.node.text.split(".").pop(), cap.node);
32
+ }
33
+ }
@@ -1,5 +1,6 @@
1
1
  // Per-repo resolution context shared by the language modules: JS/TS path-aliases + relative imports, Python
2
- // dotted/relative modules, Go package dirs, Java class files, and web hrefs / the CSS selector index.
2
+ // dotted/relative modules, Go package dirs, Rust crate/module paths, Java class files, and web hrefs /
3
+ // the CSS selector index.
3
4
  // (Split from internal-builder.js — see its doc comment for the overall architecture.)
4
5
  import { readFileSync } from "node:fs";
5
6
  import { join, dirname } from "node:path";
@@ -47,6 +48,112 @@ export function buildResolvers(repoDir, fileSet) {
47
48
  return cands.find((f) => f === full || f.endsWith("/" + full)) || cands[0] || null;
48
49
  };
49
50
 
51
+ // Rust modules are files, but their paths are logical rather than simple source-relative imports:
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
+ };
156
+
50
157
  // Path aliases (tsconfig compilerOptions.paths + vite/webpack alias) are scoped to their config folder.
51
158
  // Without nearest-config resolution, a monorepo's root `@/*` can hijack the same alias in web/.
52
159
  const aliasContexts = new Map();
@@ -188,5 +295,5 @@ export function buildResolvers(repoDir, fileSet) {
188
295
  return fileSet.has(cand) ? cand : null;
189
296
  };
190
297
 
191
- return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveJavaImport, resolveHref, selectorIndex, htmlUsages, goModule, goRequires };
298
+ return { resolveJsImport, resolveAlias, resolvePyPath, pyBaseDir, pyTopDirs, resolveGoImport, dirFiles, resolveRustMod, resolveRustPath, resolveJavaImport, resolveHref, selectorIndex, htmlUsages, goModule, goRequires };
192
299
  }
@@ -0,0 +1,4 @@
1
+ // Ownership/nesting edges are useful for navigation, but are not evidence of code usage.
2
+ const STRUCTURAL_RELATIONS = new Set(["contains", "method"]);
3
+
4
+ export const isStructuralRelation = (relation) => STRUCTURAL_RELATIONS.has(String(relation || ""));