test+feat: add cargo workspace crate resolution for rust resolver (#151)
* test+feat: add cargo workspace crate resolution for rust resolver Agent-Logs-Url: https://github.com/miketheman/codegraph/sessions/0101633b-8b63-4951-a6ca-03efe7fafe0b Co-authored-by: miketheman <529516+miketheman@users.noreply.github.com> * perf: cache cargo workspace map during rust resolution Agent-Logs-Url: https://github.com/miketheman/codegraph/sessions/0101633b-8b63-4951-a6ca-03efe7fafe0b Co-authored-by: miketheman <529516+miketheman@users.noreply.github.com> * feat(rust): expand cargo workspace member globs and trust workspace hits - Parse glob entries in `[workspace].members` (e.g. `crates/*`, `helix-*`) via picomatch against a new optional `ResolutionContext.listDirectories` so workspaces that don't enumerate every member are covered. Implementation walks the static-prefix subtree with a depth cap and skips `target`, `node_modules`, `.git`, etc. - Bump Pattern 4's confidence to 0.95 when the workspace map produces a hit. The cargo manifest gives an unambiguous crate-name -> crate-root mapping, so workspace-driven module resolution should beat name-matcher's self-file matches (otherwise every file with `use foo::...` self-resolves at 0.7 and the cross-crate edge never materializes). - Validated against astral-sh/uv (`members = ["crates/*"]`, 67 crates, 567 .rs files): 1,969 cross-crate `imports` edges reaching 60 distinct member lib.rs files, up from 0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Colby McHenry <me@colbymchenry.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Colby McHenry
parent
1cbd5a8123
commit
6ac2066a7a
@@ -7,6 +7,17 @@
|
||||
import { Node } from '../../types';
|
||||
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
|
||||
import { stripCommentsForRegex } from '../strip-comments';
|
||||
import { getCargoWorkspaceCrateMap } from './cargo-workspace';
|
||||
|
||||
const cargoWorkspaceMapCache = new WeakMap<ResolutionContext, Map<string, string>>();
|
||||
|
||||
function getCachedCargoWorkspaceCrateMap(context: ResolutionContext): Map<string, string> {
|
||||
const cached = cargoWorkspaceMapCache.get(context);
|
||||
if (cached) return cached;
|
||||
const map = getCargoWorkspaceCrateMap(context);
|
||||
cargoWorkspaceMapCache.set(context, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
export const rustResolver: FrameworkResolver = {
|
||||
name: 'rust',
|
||||
@@ -61,10 +72,15 @@ export const rustResolver: FrameworkResolver = {
|
||||
if (/^[a-z_]+$/.test(ref.referenceName)) {
|
||||
const result = resolveModule(ref.referenceName, context);
|
||||
if (result) {
|
||||
// Workspace-manifest hits are an exact crate-name -> crate-root
|
||||
// mapping straight from Cargo.toml, so we trust them above
|
||||
// name-matcher self-file matches (which otherwise win at 0.7
|
||||
// because every file containing `use foo::...` has its own
|
||||
// import node named `foo`).
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: result,
|
||||
confidence: 0.6,
|
||||
targetNodeId: result.targetId,
|
||||
confidence: result.fromWorkspace ? 0.95 : 0.6,
|
||||
resolvedBy: 'framework',
|
||||
};
|
||||
}
|
||||
@@ -191,25 +207,32 @@ function resolveByNameAndKind(
|
||||
return kindFiltered[0]!.id;
|
||||
}
|
||||
|
||||
function resolveModule(name: string, context: ResolutionContext): string | null {
|
||||
interface ModuleResolution {
|
||||
targetId: string;
|
||||
fromWorkspace: boolean;
|
||||
}
|
||||
|
||||
function resolveModule(name: string, context: ResolutionContext): ModuleResolution | null {
|
||||
// Rust modules can be either mod.rs in a directory or name.rs
|
||||
const possiblePaths = [
|
||||
`src/${name}.rs`,
|
||||
`src/${name}/mod.rs`,
|
||||
const localPaths = [`src/${name}.rs`, `src/${name}/mod.rs`];
|
||||
|
||||
const workspaceCrates = getCachedCargoWorkspaceCrateMap(context);
|
||||
const cratePath = workspaceCrates.get(name);
|
||||
const workspacePaths = cratePath
|
||||
? [`${cratePath}/src/lib.rs`, `${cratePath}/src/main.rs`]
|
||||
: [];
|
||||
|
||||
const candidates: Array<{ path: string; fromWorkspace: boolean }> = [
|
||||
...localPaths.map((path) => ({ path, fromWorkspace: false })),
|
||||
...workspacePaths.map((path) => ({ path, fromWorkspace: true })),
|
||||
];
|
||||
|
||||
for (const modPath of possiblePaths) {
|
||||
if (context.fileExists(modPath)) {
|
||||
const nodes = context.getNodesInFile(modPath);
|
||||
const modNode = nodes.find((n) => n.kind === 'module');
|
||||
if (modNode) {
|
||||
return modNode.id;
|
||||
}
|
||||
// If no explicit module node, return the first node in the file
|
||||
if (nodes.length > 0) {
|
||||
return nodes[0]!.id;
|
||||
}
|
||||
}
|
||||
for (const { path: modPath, fromWorkspace } of candidates) {
|
||||
if (!context.fileExists(modPath)) continue;
|
||||
const nodes = context.getNodesInFile(modPath);
|
||||
const modNode = nodes.find((n) => n.kind === 'module');
|
||||
if (modNode) return { targetId: modNode.id, fromWorkspace };
|
||||
if (nodes.length > 0) return { targetId: nodes[0]!.id, fromWorkspace };
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user