mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
feat: add Rust scope-resolution hooks (RFC #909 Ring 3)
Implement the scope-based resolution pipeline for Rust, following the established pattern from Go and other migrated languages. New files in gitnexus/src/core/ingestion/languages/rust/: - query.ts: tree-sitter scope query covering scopes, declarations, imports, type bindings, and references - cache-stats.ts: parse cache hit/miss counters - import-decomposer.ts: decomposes use declarations into individual import captures (handles grouped, wildcard, renamed, re-exported) - receiver-binding.ts: synthesizes self type bindings for impl methods - interpret.ts: interprets captures into ParsedImport/ParsedTypeBinding - arity.ts: arity compatibility checker (no overloading in Rust) - merge-bindings.ts: local-shadows-import binding merge strategy - simple-hooks.ts: binding scope, import owning scope, receiver binding - import-target.ts: resolves Rust module paths (crate/super/self) - method-owners.ts: bridges impl block methods to struct defs - captures.ts: main emit function with import decomposition and self-binding synthesis - scope-resolver.ts: ScopeResolver implementation - index.ts: barrel re-exports Wiring changes: - rust.ts: add scope hook imports and properties to defineLanguage - registry.ts: register rustScopeResolver in SCOPE_RESOLVERS - registry-primary-flag.ts: add Rust to MIGRATED_LANGUAGES Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
This commit is contained in:
parent
08c12e9a14
commit
ef43b2e452
16 changed files with 1166 additions and 0 deletions
|
|
@ -32,6 +32,15 @@ import { rustVariableConfig } from '../variable-extractors/configs/rust.js';
|
|||
import { createCallExtractor } from '../call-extractors/generic.js';
|
||||
import { rustCallConfig } from '../call-extractors/configs/rust.js';
|
||||
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
|
||||
import {
|
||||
emitRustScopeCaptures,
|
||||
rustArityCompatibility,
|
||||
rustBindingScopeFor,
|
||||
rustImportOwningScope,
|
||||
rustReceiverBinding,
|
||||
interpretRustImport,
|
||||
interpretRustTypeBinding,
|
||||
} from './rust/index.js';
|
||||
|
||||
/** Rust impl_item: find the function_item child and extract its name as a Method. */
|
||||
const rustExtractFunctionName = (
|
||||
|
|
@ -173,4 +182,12 @@ export const rustProvider = defineLanguage({
|
|||
classExtractor: createClassExtractor(rustClassConfig),
|
||||
heritageExtractor: createHeritageExtractor(SupportedLanguages.Rust),
|
||||
builtInNames: BUILT_INS,
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──────────
|
||||
emitScopeCaptures: emitRustScopeCaptures,
|
||||
interpretImport: interpretRustImport,
|
||||
interpretTypeBinding: interpretRustTypeBinding,
|
||||
bindingScopeFor: rustBindingScopeFor,
|
||||
importOwningScope: rustImportOwningScope,
|
||||
receiverBinding: rustReceiverBinding,
|
||||
arityCompatibility: rustArityCompatibility,
|
||||
});
|
||||
|
|
|
|||
15
gitnexus/src/core/ingestion/languages/rust/arity.ts
Normal file
15
gitnexus/src/core/ingestion/languages/rust/arity.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { Callsite, SymbolDefinition } from 'gitnexus-shared';
|
||||
|
||||
export function rustArityCompatibility(
|
||||
def: SymbolDefinition,
|
||||
callsite: Callsite,
|
||||
): 'compatible' | 'unknown' | 'incompatible' {
|
||||
const max = def.parameterCount;
|
||||
const min = def.requiredParameterCount;
|
||||
if (max === undefined && min === undefined) return 'unknown';
|
||||
if (!Number.isFinite(callsite.arity) || callsite.arity < 0) return 'unknown';
|
||||
|
||||
if (min !== undefined && callsite.arity < min) return 'incompatible';
|
||||
if (max !== undefined && callsite.arity > max) return 'incompatible';
|
||||
return 'compatible';
|
||||
}
|
||||
18
gitnexus/src/core/ingestion/languages/rust/cache-stats.ts
Normal file
18
gitnexus/src/core/ingestion/languages/rust/cache-stats.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
let hits = 0;
|
||||
let misses = 0;
|
||||
|
||||
export function recordRustCacheHit(): void {
|
||||
hits++;
|
||||
}
|
||||
export function recordRustCacheMiss(): void {
|
||||
misses++;
|
||||
}
|
||||
|
||||
export function getRustCaptureCacheStats(): { readonly hits: number; readonly misses: number } {
|
||||
return { hits, misses };
|
||||
}
|
||||
|
||||
export function resetRustCaptureCacheStats(): void {
|
||||
hits = 0;
|
||||
misses = 0;
|
||||
}
|
||||
160
gitnexus/src/core/ingestion/languages/rust/captures.ts
Normal file
160
gitnexus/src/core/ingestion/languages/rust/captures.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import {
|
||||
findNodeAtRange,
|
||||
nodeToCapture,
|
||||
syntheticCapture,
|
||||
type SyntaxNode,
|
||||
} from '../../utils/ast-helpers.js';
|
||||
import { getRustParser, getRustScopeQuery } from './query.js';
|
||||
import { recordRustCacheHit, recordRustCacheMiss } from './cache-stats.js';
|
||||
import { splitRustUseDeclaration } from './import-decomposer.js';
|
||||
import { synthesizeRustReceiverBinding } from './receiver-binding.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
|
||||
export function emitRustScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
cachedTree?: unknown,
|
||||
): readonly CaptureMatch[] {
|
||||
let tree = cachedTree as ReturnType<ReturnType<typeof getRustParser>['parse']> | undefined;
|
||||
if (tree === undefined) {
|
||||
tree = parseSourceSafe(getRustParser(), sourceText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(sourceText),
|
||||
});
|
||||
recordRustCacheMiss();
|
||||
} else {
|
||||
recordRustCacheHit();
|
||||
}
|
||||
|
||||
const rawMatches = getRustScopeQuery().matches(tree.rootNode);
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
for (const m of rawMatches) {
|
||||
const grouped: Record<string, Capture> = {};
|
||||
for (const c of m.captures) {
|
||||
const tag = '@' + c.name;
|
||||
if (tag.startsWith('@_')) continue;
|
||||
grouped[tag] = nodeToCapture(tag, c.node);
|
||||
}
|
||||
if (Object.keys(grouped).length === 0) continue;
|
||||
|
||||
// Decompose use declarations into individual import captures
|
||||
if (grouped['@import.statement'] !== undefined) {
|
||||
const anchor = grouped['@import.statement']!;
|
||||
const useNode = findNodeAtRange(tree.rootNode, anchor.range, 'use_declaration');
|
||||
if (useNode !== null) {
|
||||
out.push(...splitRustUseDeclaration(useNode));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Synthesize self receiver bindings for methods inside impl blocks
|
||||
if (grouped['@scope.function'] !== undefined) {
|
||||
const scopeCap = grouped['@scope.function']!;
|
||||
const fnNode = findNodeAtRange(tree.rootNode, scopeCap.range, 'function_item');
|
||||
if (fnNode !== null) {
|
||||
const implNode = findEnclosingImpl(fnNode);
|
||||
const receiver = synthesizeRustReceiverBinding(fnNode, implNode);
|
||||
if (receiver !== null) out.push(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach declaration arity for functions/methods
|
||||
const declAnchor = grouped['@declaration.function'];
|
||||
if (declAnchor !== undefined) {
|
||||
const fnNode = findNodeAtRange(tree.rootNode, declAnchor.range, 'function_item');
|
||||
if (fnNode !== null) {
|
||||
const arity = computeRustDeclarationArity(fnNode);
|
||||
if (arity.parameterCount !== undefined) {
|
||||
grouped['@declaration.parameter-count'] = syntheticCapture(
|
||||
'@declaration.parameter-count',
|
||||
fnNode,
|
||||
String(arity.parameterCount),
|
||||
);
|
||||
}
|
||||
if (arity.requiredParameterCount !== undefined) {
|
||||
grouped['@declaration.required-parameter-count'] = syntheticCapture(
|
||||
'@declaration.required-parameter-count',
|
||||
fnNode,
|
||||
String(arity.requiredParameterCount),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Attach call arity for call expressions
|
||||
const callAnchor =
|
||||
grouped['@reference.call.free'] ??
|
||||
grouped['@reference.call.member'] ??
|
||||
grouped['@reference.call.constructor'];
|
||||
if (callAnchor !== undefined) {
|
||||
const callNode = findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
|
||||
findNodeAtRange(tree.rootNode, callAnchor.range, 'struct_expression');
|
||||
if (callNode !== null) {
|
||||
const arity = computeRustCallArity(callNode);
|
||||
if (arity !== undefined) {
|
||||
grouped['@reference.arity'] = syntheticCapture(
|
||||
'@reference.arity',
|
||||
callNode,
|
||||
String(arity),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function findEnclosingImpl(node: SyntaxNode): SyntaxNode | null {
|
||||
let current: SyntaxNode | null = node.parent;
|
||||
while (current !== null) {
|
||||
if (current.type === 'impl_item') return current;
|
||||
if (current.type === 'source_file' || current.type === 'mod_item') return null;
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function computeRustDeclarationArity(
|
||||
fnNode: SyntaxNode,
|
||||
): { parameterCount?: number; requiredParameterCount?: number } {
|
||||
const params = fnNode.childForFieldName('parameters');
|
||||
if (params === null) return {};
|
||||
|
||||
let count = 0;
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
const child = params.namedChild(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === 'self_parameter') continue;
|
||||
if (child.type === 'parameter') count++;
|
||||
}
|
||||
// Rust has no default parameters or overloading
|
||||
return { parameterCount: count, requiredParameterCount: count };
|
||||
}
|
||||
|
||||
function computeRustCallArity(callNode: SyntaxNode): number | undefined {
|
||||
if (callNode.type === 'struct_expression') {
|
||||
const body = callNode.childForFieldName('body');
|
||||
if (body === null) return 0;
|
||||
let count = 0;
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
if (body.namedChild(i)?.type === 'field_initializer') count++;
|
||||
if (body.namedChild(i)?.type === 'shorthand_field_initializer') count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const args = callNode.childForFieldName('arguments');
|
||||
if (args === null) return 0;
|
||||
|
||||
let count = 0;
|
||||
for (let i = 0; i < args.namedChildCount; i++) {
|
||||
const child = args.namedChild(i);
|
||||
if (child !== null) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
208
gitnexus/src/core/ingestion/languages/rust/import-decomposer.ts
Normal file
208
gitnexus/src/core/ingestion/languages/rust/import-decomposer.ts
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
import type { CaptureMatch } from 'gitnexus-shared';
|
||||
import { syntheticCapture } from '../../utils/ast-helpers.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
|
||||
/**
|
||||
* Decompose a Rust `use_declaration` into individual import captures.
|
||||
* Handles simple paths, grouped imports ({A, B}), wildcards (*),
|
||||
* renames (as), and `pub use` re-exports.
|
||||
*/
|
||||
export function splitRustUseDeclaration(node: SyntaxNode): CaptureMatch[] {
|
||||
if (node.type !== 'use_declaration') return [];
|
||||
|
||||
const isReexport = hasVisibilityModifier(node);
|
||||
const argument = getUseArgument(node);
|
||||
if (argument === null) return [];
|
||||
|
||||
return decomposeUseArgument(argument, '', isReexport, node);
|
||||
}
|
||||
|
||||
function hasVisibilityModifier(node: SyntaxNode): boolean {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
if (node.child(i)?.type === 'visibility_modifier') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getUseArgument(node: SyntaxNode): SyntaxNode | null {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child === null) continue;
|
||||
if (
|
||||
child.type === 'scoped_identifier' ||
|
||||
child.type === 'scoped_use_list' ||
|
||||
child.type === 'use_wildcard' ||
|
||||
child.type === 'use_as_clause' ||
|
||||
child.type === 'identifier' ||
|
||||
child.type === 'use_list'
|
||||
) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function decomposeUseArgument(
|
||||
node: SyntaxNode,
|
||||
prefixPath: string,
|
||||
isReexport: boolean,
|
||||
anchor: SyntaxNode,
|
||||
): CaptureMatch[] {
|
||||
switch (node.type) {
|
||||
case 'scoped_identifier': {
|
||||
const path = buildScopedPath(node);
|
||||
const segments = path.split('::');
|
||||
const name = segments[segments.length - 1];
|
||||
return [
|
||||
makeImportCapture(
|
||||
anchor,
|
||||
isReexport ? 'reexport' : 'named',
|
||||
joinPaths(prefixPath, path),
|
||||
name,
|
||||
undefined,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
case 'scoped_use_list': {
|
||||
const pathNode = node.childForFieldName('path');
|
||||
const listNode = node.childForFieldName('list');
|
||||
const pathStr = pathNode ? buildNodePath(pathNode) : '';
|
||||
const fullPrefix = joinPaths(prefixPath, pathStr);
|
||||
if (listNode === null) return [];
|
||||
return decomposeUseList(listNode, fullPrefix, isReexport, anchor);
|
||||
}
|
||||
|
||||
case 'use_list': {
|
||||
return decomposeUseList(node, prefixPath, isReexport, anchor);
|
||||
}
|
||||
|
||||
case 'use_wildcard': {
|
||||
const wcPath = buildWildcardPath(node);
|
||||
return [makeImportCapture(anchor, 'wildcard', joinPaths(prefixPath, wcPath), '*', undefined)];
|
||||
}
|
||||
|
||||
case 'use_as_clause': {
|
||||
const pathChild = node.childForFieldName('path');
|
||||
const aliasChild = node.childForFieldName('alias');
|
||||
if (pathChild === null || aliasChild === null) return [];
|
||||
const originalName =
|
||||
pathChild.type === 'scoped_identifier'
|
||||
? buildScopedPath(pathChild)
|
||||
: pathChild.text;
|
||||
const aliasName = aliasChild.text;
|
||||
const segments = originalName.split('::');
|
||||
const importedName = segments[segments.length - 1];
|
||||
return [
|
||||
makeImportCapture(
|
||||
anchor,
|
||||
isReexport ? 'reexport' : 'named',
|
||||
joinPaths(prefixPath, originalName),
|
||||
importedName,
|
||||
aliasName,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
case 'identifier': {
|
||||
return [
|
||||
makeImportCapture(
|
||||
anchor,
|
||||
isReexport ? 'reexport' : 'named',
|
||||
joinPaths(prefixPath, node.text),
|
||||
node.text,
|
||||
undefined,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function decomposeUseList(
|
||||
listNode: SyntaxNode,
|
||||
prefix: string,
|
||||
isReexport: boolean,
|
||||
anchor: SyntaxNode,
|
||||
): CaptureMatch[] {
|
||||
const out: CaptureMatch[] = [];
|
||||
for (let i = 0; i < listNode.namedChildCount; i++) {
|
||||
const child = listNode.namedChild(i);
|
||||
if (child === null) continue;
|
||||
|
||||
if (child.type === 'self') {
|
||||
// `use crate::models::{self}` — imports the module itself
|
||||
const segments = prefix.split('::').filter(Boolean);
|
||||
const name = segments[segments.length - 1] ?? 'self';
|
||||
out.push(makeImportCapture(anchor, 'namespace', prefix, name, undefined));
|
||||
} else {
|
||||
out.push(...decomposeUseArgument(child, prefix, isReexport, anchor));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildScopedPath(node: SyntaxNode): string {
|
||||
if (node.type === 'scoped_identifier') {
|
||||
const parts: string[] = [];
|
||||
collectScopedParts(node, parts);
|
||||
return parts.join('::');
|
||||
}
|
||||
return node.text;
|
||||
}
|
||||
|
||||
function collectScopedParts(node: SyntaxNode, parts: string[]): void {
|
||||
if (node.type === 'scoped_identifier') {
|
||||
const pathNode = node.childForFieldName('path');
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (pathNode) collectScopedParts(pathNode, parts);
|
||||
if (nameNode) parts.push(nameNode.text);
|
||||
} else {
|
||||
parts.push(node.text);
|
||||
}
|
||||
}
|
||||
|
||||
function buildNodePath(node: SyntaxNode): string {
|
||||
if (node.type === 'scoped_identifier') {
|
||||
return buildScopedPath(node);
|
||||
}
|
||||
return node.text;
|
||||
}
|
||||
|
||||
function buildWildcardPath(node: SyntaxNode): string {
|
||||
for (let i = 0; i < node.childCount; i++) {
|
||||
const child = node.child(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === 'scoped_identifier') return buildScopedPath(child);
|
||||
if (child.type === 'identifier') return child.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function joinPaths(prefix: string, suffix: string): string {
|
||||
if (!prefix) return suffix;
|
||||
if (!suffix) return prefix;
|
||||
return `${prefix}::${suffix}`;
|
||||
}
|
||||
|
||||
function makeImportCapture(
|
||||
anchor: SyntaxNode,
|
||||
kind: string,
|
||||
source: string,
|
||||
name: string,
|
||||
alias: string | undefined,
|
||||
): CaptureMatch {
|
||||
const result: CaptureMatch = {
|
||||
'@import.statement': syntheticCapture('@import.statement', anchor, anchor.text),
|
||||
'@import.kind': syntheticCapture('@import.kind', anchor, kind),
|
||||
'@import.source': syntheticCapture('@import.source', anchor, source),
|
||||
'@import.name': syntheticCapture('@import.name', anchor, alias ?? name),
|
||||
};
|
||||
if (alias !== undefined) {
|
||||
result['@import.alias'] = syntheticCapture('@import.alias', anchor, alias);
|
||||
result['@import.original-name'] = syntheticCapture('@import.original-name', anchor, name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
128
gitnexus/src/core/ingestion/languages/rust/import-target.ts
Normal file
128
gitnexus/src/core/ingestion/languages/rust/import-target.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/**
|
||||
* Resolve a Rust `use` import path to a repo-relative file path.
|
||||
*
|
||||
* Rust module resolution rules:
|
||||
* - `crate::foo::bar` → `src/foo/bar.rs` or `src/foo/bar/mod.rs`
|
||||
* - `super::foo` → parent directory's `foo.rs` or `foo/mod.rs`
|
||||
* - `self::foo` → same directory's `foo.rs` or `foo/mod.rs`
|
||||
* - External crate imports (no `crate::`/`super::`/`self::`) → null
|
||||
*/
|
||||
export function resolveRustImportTarget(
|
||||
targetRaw: string,
|
||||
fromFile: string,
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
_resolutionConfig?: unknown,
|
||||
): string | readonly string[] | null {
|
||||
if (!targetRaw) return null;
|
||||
|
||||
const segments = targetRaw.split('::').filter(Boolean);
|
||||
if (segments.length === 0) return null;
|
||||
|
||||
const fromNormalized = fromFile.replace(/\\/g, '/');
|
||||
const fromDir = fromNormalized.includes('/')
|
||||
? fromNormalized.slice(0, fromNormalized.lastIndexOf('/'))
|
||||
: '';
|
||||
|
||||
if (segments[0] === 'crate') {
|
||||
const cratePath = segments.slice(1);
|
||||
return resolveModulePath(cratePath, findSrcRoot(fromNormalized), allFilePaths);
|
||||
}
|
||||
|
||||
if (segments[0] === 'super') {
|
||||
const parentDir = fromDir.includes('/')
|
||||
? fromDir.slice(0, fromDir.lastIndexOf('/'))
|
||||
: '';
|
||||
const restPath = segments.slice(1);
|
||||
return resolveModulePath(restPath, parentDir, allFilePaths);
|
||||
}
|
||||
|
||||
if (segments[0] === 'self') {
|
||||
const restPath = segments.slice(1);
|
||||
return resolveModulePath(restPath, fromDir, allFilePaths);
|
||||
}
|
||||
|
||||
// External crate — try workspace-level resolution
|
||||
return resolveWorkspaceCrate(segments, allFilePaths);
|
||||
}
|
||||
|
||||
function findSrcRoot(filePath: string): string {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
const srcIdx = normalized.lastIndexOf('/src/');
|
||||
if (srcIdx !== -1) return normalized.slice(0, srcIdx + 4); // includes trailing /src
|
||||
if (normalized.startsWith('src/')) return 'src';
|
||||
return '';
|
||||
}
|
||||
|
||||
function resolveModulePath(
|
||||
pathSegments: string[],
|
||||
baseDir: string,
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
): string | readonly string[] | null {
|
||||
if (pathSegments.length === 0) {
|
||||
const modPath = baseDir ? `${baseDir}/mod.rs` : 'mod.rs';
|
||||
if (allFilePaths.has(modPath)) return modPath;
|
||||
return null;
|
||||
}
|
||||
|
||||
const modulePath = pathSegments.join('/');
|
||||
|
||||
// Try direct file
|
||||
const directFile = baseDir ? `${baseDir}/${modulePath}.rs` : `${modulePath}.rs`;
|
||||
if (allFilePaths.has(directFile)) return directFile;
|
||||
|
||||
// Try mod.rs inside directory
|
||||
const modFile = baseDir ? `${baseDir}/${modulePath}/mod.rs` : `${modulePath}/mod.rs`;
|
||||
if (allFilePaths.has(modFile)) return modFile;
|
||||
|
||||
// Try partial path resolution: for `use crate::models::User` where
|
||||
// User is a type inside models.rs, resolve to `src/models.rs`
|
||||
if (pathSegments.length >= 2) {
|
||||
const parentPath = pathSegments.slice(0, -1).join('/');
|
||||
const parentFile = baseDir ? `${baseDir}/${parentPath}.rs` : `${parentPath}.rs`;
|
||||
if (allFilePaths.has(parentFile)) return parentFile;
|
||||
|
||||
const parentModFile = baseDir
|
||||
? `${baseDir}/${parentPath}/mod.rs`
|
||||
: `${parentPath}/mod.rs`;
|
||||
if (allFilePaths.has(parentModFile)) return parentModFile;
|
||||
}
|
||||
|
||||
// Fallback: try increasingly shorter path prefixes
|
||||
for (let i = pathSegments.length - 2; i >= 1; i--) {
|
||||
const prefix = pathSegments.slice(0, i).join('/');
|
||||
const prefixFile = baseDir ? `${baseDir}/${prefix}.rs` : `${prefix}.rs`;
|
||||
if (allFilePaths.has(prefixFile)) return prefixFile;
|
||||
const prefixModFile = baseDir ? `${baseDir}/${prefix}/mod.rs` : `${prefix}/mod.rs`;
|
||||
if (allFilePaths.has(prefixModFile)) return prefixModFile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveWorkspaceCrate(
|
||||
segments: string[],
|
||||
allFilePaths: ReadonlySet<string>,
|
||||
): string | null {
|
||||
const crateName = segments[0];
|
||||
const restSegments = segments.slice(1);
|
||||
|
||||
const candidates = [
|
||||
restSegments.length > 0
|
||||
? `${crateName}/src/${restSegments.join('/')}.rs`
|
||||
: `${crateName}/src/lib.rs`,
|
||||
restSegments.length > 0
|
||||
? `${crateName}/src/${restSegments.join('/')}/mod.rs`
|
||||
: `${crateName}/src/lib.rs`,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (allFilePaths.has(candidate)) return candidate;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface RustResolveContext {
|
||||
readonly fromFile: string;
|
||||
readonly allFilePaths: ReadonlySet<string>;
|
||||
}
|
||||
12
gitnexus/src/core/ingestion/languages/rust/index.ts
Normal file
12
gitnexus/src/core/ingestion/languages/rust/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Rust scope-resolution hooks (RFC #909 Ring 3).
|
||||
*/
|
||||
export { emitRustScopeCaptures } from './captures.js';
|
||||
export { getRustCaptureCacheStats, resetRustCaptureCacheStats } from './cache-stats.js';
|
||||
export { interpretRustImport, interpretRustTypeBinding, normalizeRustTypeName } from './interpret.js';
|
||||
export { splitRustUseDeclaration } from './import-decomposer.js';
|
||||
export { synthesizeRustReceiverBinding } from './receiver-binding.js';
|
||||
export { rustArityCompatibility } from './arity.js';
|
||||
export { rustMergeBindings } from './merge-bindings.js';
|
||||
export { rustBindingScopeFor, rustImportOwningScope, rustReceiverBinding } from './simple-hooks.js';
|
||||
export { resolveRustImportTarget, type RustResolveContext } from './import-target.js';
|
||||
162
gitnexus/src/core/ingestion/languages/rust/interpret.ts
Normal file
162
gitnexus/src/core/ingestion/languages/rust/interpret.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared';
|
||||
|
||||
// ─── interpretImport ──────────────────────────────────────────────────────
|
||||
|
||||
export function interpretRustImport(captures: CaptureMatch): ParsedImport | null {
|
||||
const kind = captures['@import.kind']?.text;
|
||||
const source = captures['@import.source']?.text;
|
||||
const name = captures['@import.name']?.text;
|
||||
const alias = captures['@import.alias']?.text;
|
||||
if (kind === undefined || source === undefined) return null;
|
||||
|
||||
if (kind === 'wildcard') return { kind: 'wildcard', targetRaw: source };
|
||||
if (kind === 'namespace') {
|
||||
if (name === undefined) return null;
|
||||
return { kind: 'namespace', localName: name, importedName: name, targetRaw: source };
|
||||
}
|
||||
if (kind === 'reexport') {
|
||||
if (name === undefined) return null;
|
||||
const originalName = captures['@import.original-name']?.text;
|
||||
return {
|
||||
kind: 'named',
|
||||
localName: alias ?? name,
|
||||
importedName: originalName ?? name,
|
||||
targetRaw: source,
|
||||
isReexport: true,
|
||||
};
|
||||
}
|
||||
// kind === 'named'
|
||||
if (name === undefined) return null;
|
||||
const originalName = captures['@import.original-name']?.text;
|
||||
return {
|
||||
kind: 'named',
|
||||
localName: alias ?? name,
|
||||
importedName: originalName ?? name,
|
||||
targetRaw: source,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── interpretTypeBinding ─────────────────────────────────────────────────
|
||||
|
||||
export function interpretRustTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null {
|
||||
const name = captures['@type-binding.name']?.text;
|
||||
const type = captures['@type-binding.type']?.text;
|
||||
if (name === undefined || type === undefined) return null;
|
||||
|
||||
let source: TypeRef['source'] = 'annotation';
|
||||
let normalizedType: string;
|
||||
|
||||
if (captures['@type-binding.self'] !== undefined) {
|
||||
source = 'self';
|
||||
normalizedType = normalizeRustTypeName(type);
|
||||
} else if (captures['@type-binding.constructor'] !== undefined) {
|
||||
source = 'constructor-inferred';
|
||||
normalizedType = normalizeRustTypeName(type);
|
||||
} else if (captures['@type-binding.call-return'] !== undefined) {
|
||||
source = 'constructor-inferred';
|
||||
normalizedType = normalizeRustCallReturnType(type);
|
||||
} else if (captures['@type-binding.return'] !== undefined) {
|
||||
source = 'return-annotation';
|
||||
normalizedType = normalizeRustReturnType(type);
|
||||
} else if (captures['@type-binding.assignment'] !== undefined) {
|
||||
source = 'assignment-inferred';
|
||||
normalizedType = normalizeRustTypeName(type);
|
||||
} else if (captures['@type-binding.alias'] !== undefined) {
|
||||
source = 'assignment-inferred';
|
||||
normalizedType = normalizeRustTypeName(type);
|
||||
} else if (captures['@type-binding.parameter'] !== undefined) {
|
||||
source = 'parameter-annotation';
|
||||
normalizedType = normalizeRustTypeName(type);
|
||||
} else {
|
||||
normalizedType = normalizeRustTypeName(type);
|
||||
}
|
||||
|
||||
return { boundName: name, rawTypeName: normalizedType, source };
|
||||
}
|
||||
|
||||
export function normalizeRustTypeName(text: string): string {
|
||||
let t = text.trim();
|
||||
// Strip reference prefixes (&, &mut, *const, *mut)
|
||||
while (t.startsWith('&')) t = t.replace(/^&\s*(mut\s+)?/, '');
|
||||
while (t.startsWith('*')) t = t.replace(/^\*\s*(const|mut)?\s*/, '');
|
||||
// Unwrap common smart-pointer/container wrappers to their inner type
|
||||
const wrappers = ['Box', 'Option', 'Arc', 'Rc', 'Mutex', 'RwLock', 'RefCell', 'Cell'];
|
||||
for (const w of wrappers) {
|
||||
if (t.startsWith(`${w}<`)) {
|
||||
const inner = extractFirstGenericArg(t);
|
||||
if (inner !== null) {
|
||||
t = inner;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t.startsWith('Vec<')) {
|
||||
const inner = extractFirstGenericArg(t);
|
||||
if (inner !== null) t = inner;
|
||||
}
|
||||
const bracket = t.indexOf('<');
|
||||
if (bracket !== -1) t = t.slice(0, bracket);
|
||||
// Take last segment of qualified paths (crate::foo::Bar → Bar)
|
||||
const lastColon = t.lastIndexOf('::');
|
||||
if (lastColon !== -1) t = t.slice(lastColon + 2);
|
||||
return t.trim();
|
||||
}
|
||||
|
||||
function extractFirstGenericArg(text: string): string | null {
|
||||
const open = text.indexOf('<');
|
||||
if (open === -1) return null;
|
||||
let depth = 0;
|
||||
for (let i = open; i < text.length; i++) {
|
||||
if (text[i] === '<') depth++;
|
||||
else if (text[i] === '>') {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
const inner = text.slice(open + 1, i).trim();
|
||||
const comma = findTopLevelComma(inner);
|
||||
return comma === -1 ? inner : inner.slice(0, comma).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findTopLevelComma(text: string): number {
|
||||
let depth = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i] === '<') depth++;
|
||||
else if (text[i] === '>') depth--;
|
||||
else if (text[i] === ',' && depth === 0) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function normalizeRustCallReturnType(text: string): string {
|
||||
let t = text.trim();
|
||||
// For scoped calls like `Foo::new()`, extract the type part before `::`
|
||||
const scopeIdx = t.indexOf('::');
|
||||
if (scopeIdx !== -1) {
|
||||
t = t.slice(0, scopeIdx);
|
||||
}
|
||||
return normalizeRustTypeName(t);
|
||||
}
|
||||
|
||||
function normalizeRustReturnType(text: string): string {
|
||||
let t = text.trim();
|
||||
while (t.startsWith('&')) t = t.replace(/^&\s*(mut\s+)?/, '');
|
||||
// Unwrap Result<T, E>, Option<T> for return types
|
||||
const wrappers = ['Result', 'Option'];
|
||||
for (const w of wrappers) {
|
||||
if (t.startsWith(`${w}<`)) {
|
||||
const inner = extractFirstGenericArg(t);
|
||||
if (inner !== null) {
|
||||
t = inner;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bracket = t.indexOf('<');
|
||||
if (bracket !== -1) t = t.slice(0, bracket);
|
||||
const lastColon = t.lastIndexOf('::');
|
||||
if (lastColon !== -1) t = t.slice(lastColon + 2);
|
||||
return t.trim();
|
||||
}
|
||||
27
gitnexus/src/core/ingestion/languages/rust/merge-bindings.ts
Normal file
27
gitnexus/src/core/ingestion/languages/rust/merge-bindings.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { BindingRef } from 'gitnexus-shared';
|
||||
|
||||
const TIER: Record<BindingRef['origin'], number> = {
|
||||
local: 0,
|
||||
namespace: 1,
|
||||
import: 2,
|
||||
reexport: 3,
|
||||
wildcard: 4,
|
||||
};
|
||||
|
||||
export function rustMergeBindings(
|
||||
existing: readonly BindingRef[],
|
||||
incoming: readonly BindingRef[],
|
||||
_scopeId: string,
|
||||
): BindingRef[] {
|
||||
const seen = new Set<string>();
|
||||
return [...existing, ...incoming]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(TIER[a.origin] ?? 99) - (TIER[b.origin] ?? 99) || a.def.nodeId.localeCompare(b.def.nodeId),
|
||||
)
|
||||
.filter((binding) => {
|
||||
if (seen.has(binding.def.nodeId)) return false;
|
||||
seen.add(binding.def.nodeId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
63
gitnexus/src/core/ingestion/languages/rust/method-owners.ts
Normal file
63
gitnexus/src/core/ingestion/languages/rust/method-owners.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { isClassLike, populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
|
||||
/**
|
||||
* Populate `ownerId` on Rust method defs.
|
||||
*
|
||||
* Rust methods are declared inside `impl TypeName { ... }` blocks, not
|
||||
* directly inside struct bodies. The tree-sitter query creates Class scopes
|
||||
* for impl blocks, and the generic `populateClassOwnedMembers` handles methods
|
||||
* that are structurally nested inside those Class scopes. But we also need to
|
||||
* bridge the impl block's methods to the actual struct def, since the impl
|
||||
* block is semantically "owned by" the struct.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Run the generic `populateClassOwnedMembers` (handles property fields in
|
||||
* structs and methods in impl blocks).
|
||||
* 2. For each method in an impl block's Class scope whose ownerId points to
|
||||
* the impl block, re-point ownerId to the struct def (if found in the
|
||||
* same module).
|
||||
*/
|
||||
export function populateRustOwners(parsed: ParsedFile): void {
|
||||
populateClassOwnedMembers(parsed);
|
||||
populateRustImplOwners(parsed);
|
||||
}
|
||||
|
||||
function populateRustImplOwners(parsed: ParsedFile): void {
|
||||
// Build a map of struct name → def nodeId from all scopes.
|
||||
const structByName = new Map<string, string>();
|
||||
for (const scope of parsed.scopes) {
|
||||
for (const def of scope.ownedDefs) {
|
||||
if (isClassLike(def.type) && (def.type === 'Struct' || def.type === 'Trait')) {
|
||||
structByName.set(def.name, def.nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (structByName.size === 0) return;
|
||||
|
||||
// For each method whose ownerId is unset, try to remap to the struct def
|
||||
// using the self typeBinding's receiver type.
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Function') continue;
|
||||
const methodDefs = scope.ownedDefs.filter(
|
||||
(d) => d.type === 'Method' && d.ownerId === undefined,
|
||||
);
|
||||
if (methodDefs.length === 0) continue;
|
||||
|
||||
let receiverType: string | undefined;
|
||||
for (const [, tb] of scope.typeBindings) {
|
||||
if (tb.source === 'self') {
|
||||
receiverType = tb.rawName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (receiverType === undefined) continue;
|
||||
|
||||
const ownerId = structByName.get(receiverType);
|
||||
if (ownerId !== undefined) {
|
||||
for (const def of methodDefs) {
|
||||
(def as { ownerId?: string }).ownerId = ownerId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
gitnexus/src/core/ingestion/languages/rust/query.ts
Normal file
136
gitnexus/src/core/ingestion/languages/rust/query.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import Parser from 'tree-sitter';
|
||||
import Rust from 'tree-sitter-rust';
|
||||
|
||||
const RUST_SCOPE_QUERY = `
|
||||
;; Scopes
|
||||
(source_file) @scope.module
|
||||
(struct_item) @scope.class
|
||||
(trait_item) @scope.class
|
||||
(impl_item) @scope.class
|
||||
(enum_item) @scope.class
|
||||
(function_item) @scope.function
|
||||
(closure_expression) @scope.function
|
||||
(block) @scope.block
|
||||
(if_expression) @scope.block
|
||||
(match_expression) @scope.block
|
||||
(for_expression) @scope.block
|
||||
(while_expression) @scope.block
|
||||
(loop_expression) @scope.block
|
||||
(mod_item) @scope.namespace
|
||||
|
||||
;; Declarations — struct
|
||||
(struct_item
|
||||
name: (type_identifier) @declaration.name) @declaration.struct
|
||||
|
||||
;; Declarations — trait
|
||||
(trait_item
|
||||
name: (type_identifier) @declaration.name) @declaration.trait
|
||||
|
||||
;; Declarations — enum
|
||||
(enum_item
|
||||
name: (type_identifier) @declaration.name) @declaration.enum
|
||||
|
||||
;; Declarations — function (top-level or inside mod)
|
||||
(function_item
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
;; Declarations — struct fields
|
||||
(field_declaration
|
||||
name: (field_identifier) @declaration.name
|
||||
type: (_) @declaration.field-type) @declaration.field
|
||||
|
||||
;; Declarations — variables (let bindings)
|
||||
(let_declaration
|
||||
pattern: (identifier) @declaration.name) @declaration.variable
|
||||
|
||||
;; Declarations — const
|
||||
(const_item
|
||||
name: (identifier) @declaration.name) @declaration.const
|
||||
|
||||
;; Declarations — static
|
||||
(static_item
|
||||
name: (identifier) @declaration.name) @declaration.const
|
||||
|
||||
;; Imports
|
||||
(use_declaration) @import.statement
|
||||
|
||||
;; Type bindings — parameter annotations
|
||||
(parameter
|
||||
pattern: (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.parameter
|
||||
|
||||
;; Type bindings — let with type annotation
|
||||
(let_declaration
|
||||
pattern: (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.assignment
|
||||
|
||||
;; Type bindings — struct literal constructor inference
|
||||
(let_declaration
|
||||
pattern: (identifier) @type-binding.name
|
||||
value: (struct_expression
|
||||
name: (_) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings — call-return inference (let x = Foo::new())
|
||||
(let_declaration
|
||||
pattern: (identifier) @type-binding.name
|
||||
value: (call_expression
|
||||
function: (_) @type-binding.type)) @type-binding.call-return
|
||||
|
||||
;; Type bindings — variable alias (let x = y)
|
||||
(let_declaration
|
||||
pattern: (identifier) @type-binding.name
|
||||
value: (identifier) @type-binding.type) @type-binding.alias
|
||||
|
||||
;; Type bindings — return type annotation
|
||||
(function_item
|
||||
name: (identifier) @type-binding.name
|
||||
return_type: (_) @type-binding.type) @type-binding.return
|
||||
|
||||
;; References — free calls
|
||||
(call_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; References — member calls (obj.method())
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
value: (_) @reference.receiver
|
||||
field: (field_identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; References — scoped calls (Foo::bar())
|
||||
(call_expression
|
||||
function: (scoped_identifier
|
||||
name: (identifier) @reference.name)) @reference.call.free
|
||||
|
||||
;; References — constructor calls (struct literal)
|
||||
(struct_expression
|
||||
name: (_) @reference.name) @reference.call.constructor
|
||||
|
||||
;; References — field reads
|
||||
(field_expression
|
||||
value: (_) @reference.receiver
|
||||
field: (field_identifier) @reference.name) @reference.read
|
||||
|
||||
;; References — field writes (assignment)
|
||||
(assignment_expression
|
||||
left: (field_expression
|
||||
value: (_) @reference.receiver
|
||||
field: (field_identifier) @reference.name)) @reference.write
|
||||
`;
|
||||
|
||||
let _parser: Parser | null = null;
|
||||
let _query: Parser.Query | null = null;
|
||||
|
||||
export function getRustParser(): Parser {
|
||||
if (_parser === null) {
|
||||
_parser = new Parser();
|
||||
_parser.setLanguage(Rust as Parameters<Parser['setLanguage']>[0]);
|
||||
}
|
||||
return _parser;
|
||||
}
|
||||
|
||||
export function getRustScopeQuery(): Parser.Query {
|
||||
if (_query === null) {
|
||||
_query = new Parser.Query(Rust as Parameters<Parser['setLanguage']>[0], RUST_SCOPE_QUERY);
|
||||
}
|
||||
return _query;
|
||||
}
|
||||
136
gitnexus/src/core/ingestion/languages/rust/receiver-binding.ts
Normal file
136
gitnexus/src/core/ingestion/languages/rust/receiver-binding.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import type { CaptureMatch } from 'gitnexus-shared';
|
||||
import { syntheticCapture } from '../../utils/ast-helpers.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
|
||||
/**
|
||||
* Given a function_item node that is inside an impl_item, synthesize a
|
||||
* self-type-binding capture if the function has a `self_parameter`.
|
||||
*
|
||||
* The impl_item structure:
|
||||
* impl [TraitName for] TypeName { fn method(&self) { ... } }
|
||||
*/
|
||||
export function synthesizeRustReceiverBinding(
|
||||
fnNode: SyntaxNode,
|
||||
implNode: SyntaxNode | null,
|
||||
): CaptureMatch | null {
|
||||
if (fnNode.type !== 'function_item') return null;
|
||||
if (implNode === null) return null;
|
||||
|
||||
const params = fnNode.childForFieldName('parameters');
|
||||
if (params === null) return null;
|
||||
|
||||
let hasSelf = false;
|
||||
for (let i = 0; i < params.namedChildCount; i++) {
|
||||
if (params.namedChild(i)?.type === 'self_parameter') {
|
||||
hasSelf = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasSelf) return null;
|
||||
|
||||
const implType = getImplTargetType(implNode);
|
||||
if (implType === null) return null;
|
||||
|
||||
return {
|
||||
'@type-binding.self': syntheticCapture('@type-binding.self', fnNode, 'self'),
|
||||
'@type-binding.name': syntheticCapture('@type-binding.name', fnNode, 'self'),
|
||||
'@type-binding.type': syntheticCapture('@type-binding.type', fnNode, implType),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the target type from an impl_item.
|
||||
* `impl TypeName { ... }` → "TypeName"
|
||||
* `impl TraitName for TypeName { ... }` → "TypeName"
|
||||
*/
|
||||
export function getImplTargetType(implNode: SyntaxNode): string | null {
|
||||
if (implNode.type !== 'impl_item') return null;
|
||||
|
||||
// Look for `for` keyword — if present, impl is `impl Trait for Type`
|
||||
let hasFor = false;
|
||||
let typeAfterFor: SyntaxNode | null = null;
|
||||
for (let i = 0; i < implNode.childCount; i++) {
|
||||
const child = implNode.child(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === 'for') {
|
||||
hasFor = true;
|
||||
continue;
|
||||
}
|
||||
if (hasFor && child.type === 'type_identifier') {
|
||||
typeAfterFor = child;
|
||||
break;
|
||||
}
|
||||
if (hasFor && child.type === 'scoped_type_identifier') {
|
||||
typeAfterFor = child;
|
||||
break;
|
||||
}
|
||||
if (hasFor && child.type === 'generic_type') {
|
||||
typeAfterFor = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasFor && typeAfterFor !== null) {
|
||||
return normalizeRustTypeName(typeAfterFor.text);
|
||||
}
|
||||
|
||||
// No `for` keyword: impl TypeName { ... }
|
||||
const typeField = implNode.childForFieldName('type');
|
||||
if (typeField !== null) {
|
||||
return normalizeRustTypeName(typeField.text);
|
||||
}
|
||||
|
||||
// Fallback: find first type_identifier after `impl`
|
||||
let afterImpl = false;
|
||||
for (let i = 0; i < implNode.childCount; i++) {
|
||||
const child = implNode.child(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === 'impl') {
|
||||
afterImpl = true;
|
||||
continue;
|
||||
}
|
||||
if (afterImpl && (child.type === 'type_identifier' || child.type === 'generic_type')) {
|
||||
return normalizeRustTypeName(child.text);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the trait name from an impl_item when it's `impl Trait for Type`.
|
||||
*/
|
||||
export function getImplTraitName(implNode: SyntaxNode): string | null {
|
||||
if (implNode.type !== 'impl_item') return null;
|
||||
|
||||
let afterImpl = false;
|
||||
for (let i = 0; i < implNode.childCount; i++) {
|
||||
const child = implNode.child(i);
|
||||
if (child === null) continue;
|
||||
if (child.type === 'impl') {
|
||||
afterImpl = true;
|
||||
continue;
|
||||
}
|
||||
if (child.type === 'for') {
|
||||
break;
|
||||
}
|
||||
if (afterImpl && (child.type === 'type_identifier' || child.type === 'scoped_type_identifier')) {
|
||||
for (let j = i + 1; j < implNode.childCount; j++) {
|
||||
const next = implNode.child(j);
|
||||
if (next === null) continue;
|
||||
if (next.type === 'for') {
|
||||
return normalizeRustTypeName(child.text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeRustTypeName(text: string): string {
|
||||
let t = text.trim();
|
||||
while (t.startsWith('&')) t = t.replace(/^&\s*(mut\s+)?/, '');
|
||||
while (t.startsWith('*')) t = t.slice(1).trim();
|
||||
const bracket = t.indexOf('<');
|
||||
if (bracket !== -1) t = t.slice(0, bracket);
|
||||
return t.trim();
|
||||
}
|
||||
37
gitnexus/src/core/ingestion/languages/rust/scope-resolver.ts
Normal file
37
gitnexus/src/core/ingestion/languages/rust/scope-resolver.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { rustProvider } from '../rust.js';
|
||||
import {
|
||||
rustArityCompatibility,
|
||||
rustMergeBindings,
|
||||
resolveRustImportTarget,
|
||||
} from './index.js';
|
||||
import { populateRustOwners } from './method-owners.js';
|
||||
|
||||
export const rustScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Rust,
|
||||
languageProvider: rustProvider,
|
||||
importEdgeReason: 'rust-scope: use',
|
||||
|
||||
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) =>
|
||||
resolveRustImportTarget(targetRaw, fromFile, allFilePaths, resolutionConfig),
|
||||
|
||||
mergeBindings: (existing, incoming, scopeId) =>
|
||||
rustMergeBindings(existing, incoming, scopeId),
|
||||
|
||||
arityCompatibility: (callsite, def) => rustArityCompatibility(def, callsite),
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
|
||||
populateOwners: (parsed: ParsedFile) => populateRustOwners(parsed),
|
||||
|
||||
isSuperReceiver: () => false,
|
||||
|
||||
fieldFallbackOnMethodLookup: false,
|
||||
hoistTypeBindingsToModule: true,
|
||||
propagatesReturnTypesAcrossImports: true,
|
||||
allowGlobalFreeCallFallback: true,
|
||||
};
|
||||
44
gitnexus/src/core/ingestion/languages/rust/simple-hooks.ts
Normal file
44
gitnexus/src/core/ingestion/languages/rust/simple-hooks.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type {
|
||||
CaptureMatch,
|
||||
ParsedImport,
|
||||
Scope,
|
||||
ScopeId,
|
||||
ScopeTree,
|
||||
TypeRef,
|
||||
} from 'gitnexus-shared';
|
||||
|
||||
export function rustBindingScopeFor(
|
||||
decl: CaptureMatch,
|
||||
innermost: Scope,
|
||||
_tree: ScopeTree,
|
||||
): ScopeId | null {
|
||||
// Keep self typeBindings in the method's Function scope so
|
||||
// populateRustOwners can match Method defs to their receiver types.
|
||||
if (decl['@type-binding.self'] !== undefined) {
|
||||
return innermost.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rust `use` statements inside a function body should attach at function scope,
|
||||
* not module scope. If the innermost scope is a Function, attach there.
|
||||
*/
|
||||
export function rustImportOwningScope(
|
||||
_imp: ParsedImport,
|
||||
innermost: Scope,
|
||||
_tree: ScopeTree,
|
||||
): ScopeId | null {
|
||||
if (innermost.kind === 'Function') {
|
||||
return innermost.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function rustReceiverBinding(functionScope: Scope): TypeRef | null {
|
||||
if (functionScope.kind !== 'Function') return null;
|
||||
for (const binding of functionScope.typeBindings.values()) {
|
||||
if (binding.source === 'self') return binding;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ export const MIGRATED_LANGUAGES: ReadonlySet<SupportedLanguages> = new Set<Suppo
|
|||
SupportedLanguages.C,
|
||||
SupportedLanguages.CPlusPlus,
|
||||
SupportedLanguages.PHP,
|
||||
SupportedLanguages.Rust,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { javaScopeResolver } from '../../languages/java/scope-resolver.js';
|
|||
import { cScopeResolver } from '../../languages/c/scope-resolver.js';
|
||||
import { cppScopeResolver } from '../../languages/cpp/scope-resolver.js';
|
||||
import { phpScopeResolver } from '../../languages/php/scope-resolver.js';
|
||||
import { rustScopeResolver } from '../../languages/rust/scope-resolver.js';
|
||||
|
||||
/** Map of `SupportedLanguages` → `ScopeResolver`. The phase iterates
|
||||
* this map intersected with `MIGRATED_LANGUAGES` (the per-language
|
||||
|
|
@ -36,4 +37,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
|
|||
[SupportedLanguages.C, cScopeResolver],
|
||||
[SupportedLanguages.CPlusPlus, cppScopeResolver],
|
||||
[SupportedLanguages.PHP, phpScopeResolver],
|
||||
[SupportedLanguages.Rust, rustScopeResolver],
|
||||
]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue