fix(rust): framework name heuristics never reach a function-local item (#2224)

The Rust resolver's handler/service/struct heuristics took the first
same-named item and then checked scope; they now drop items declared inside
a function body and out-of-scope names before choosing, and take the file's
own item first.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-01 00:34:57 +00:00
committed by GitHub
parent ca1eba98bd
commit 7365427d4f
4 changed files with 105 additions and 8 deletions
+20 -7
View File
@@ -8,7 +8,7 @@ import { Node } from '../../types';
import { FrameworkResolver, UnresolvedRef, ResolvedRef, ResolutionContext } from '../types';
import { stripCommentsForRegex } from '../strip-comments';
import { getCargoWorkspaceCrateMap } from './cargo-workspace';
import { isRustNameInScope } from '../name-matcher';
import { isRustNameInScope, isLexicallyReachable } from '../name-matcher';
/**
* Whether the item a name heuristic found is one the reference can name:
@@ -42,7 +42,7 @@ export const rustResolver: FrameworkResolver = {
resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
// Pattern 1: Handler references
if (ref.referenceName.endsWith('_handler') || ref.referenceName.startsWith('handle_')) {
const result = resolveByNameAndKind(ref.referenceName, FUNCTION_KINDS, HANDLER_DIRS, context);
const result = resolveByNameAndKind(ref, FUNCTION_KINDS, HANDLER_DIRS, context);
if (result && inRustScope(result, ref, context)) {
return {
original: ref,
@@ -55,7 +55,7 @@ export const rustResolver: FrameworkResolver = {
// Pattern 2: Service/Repository trait implementations
if (ref.referenceName.endsWith('Service') || ref.referenceName.endsWith('Repository')) {
const result = resolveByNameAndKind(ref.referenceName, SERVICE_KINDS, SERVICE_DIRS, context);
const result = resolveByNameAndKind(ref, SERVICE_KINDS, SERVICE_DIRS, context);
if (result && inRustScope(result, ref, context)) {
return {
original: ref,
@@ -68,7 +68,7 @@ export const rustResolver: FrameworkResolver = {
// Pattern 3: Struct references (PascalCase)
if (/^[A-Z][a-zA-Z]+$/.test(ref.referenceName)) {
const result = resolveByNameAndKind(ref.referenceName, STRUCT_KINDS, MODEL_DIRS, context);
const result = resolveByNameAndKind(ref, STRUCT_KINDS, MODEL_DIRS, context);
if (result && inRustScope(result, ref, context)) {
return {
original: ref,
@@ -291,18 +291,31 @@ function findMatchingParen(s: string, openIdx: number): number {
/**
* Resolve a symbol by name using indexed queries instead of scanning all files.
*/
/**
* The same-named item of these kinds that the reference can see — in scope
* by its `use`s and module paths, and not declared inside some function body
* (axum's examples' `Uri`, imported from `http`, went to a `struct Uri` a
* routing test declares inside itself) — its own file's first, then a
* framework-conventional directory's.
*/
function resolveByNameAndKind(
name: string,
ref: UnresolvedRef,
kinds: Set<string>,
preferredDirPatterns: string[],
context: ResolutionContext,
): string | null {
const candidates = context.getNodesByName(name);
const candidates = context.getNodesByName(ref.referenceName);
if (candidates.length === 0) return null;
const kindFiltered = candidates.filter((n) => kinds.has(n.kind));
const kindFiltered = candidates.filter((n) => kinds.has(n.kind) &&
isLexicallyReachable(n, ref, context) && isRustNameInScope(n, ref, context));
if (kindFiltered.length === 0) return null;
// The file is a module: its own items are in scope. A sibling file is
// another module, reached only through a `use` — no nearer than any other.
const sameFile = kindFiltered.find((n) => n.filePath === ref.filePath);
if (sameFile) return sameFile.id;
// Prefer candidates in framework-conventional directories
const preferred = kindFiltered.filter((n) =>
preferredDirPatterns.some((d) => n.filePath.includes(d))
+1 -1
View File
@@ -636,7 +636,7 @@ const LOCAL_TYPE_KINDS = new Set<string>(['class', 'struct', 'enum', 'interface'
* unaffected (their parent resolves to a class-like node), as are top-level
* symbols and C++ namespace-prefixed names (the prefix has no node).
*/
function isLexicallyReachable(
export function isLexicallyReachable(
candidate: Node,
ref: UnresolvedRef,
context: ResolutionContext