diff --git a/gitnexus-shared/src/index.ts b/gitnexus-shared/src/index.ts index fc591fdb01..d732d2633f 100644 --- a/gitnexus-shared/src/index.ts +++ b/gitnexus-shared/src/index.ts @@ -116,6 +116,8 @@ export type { FieldRegistry, FieldLookupOptions, } from './scope-resolution/registries/field-registry.js'; +export { buildMacroRegistry } from './scope-resolution/registries/macro-registry.js'; +export type { MacroRegistry } from './scope-resolution/registries/macro-registry.js'; export { lookupCore } from './scope-resolution/registries/lookup-core.js'; export type { CoreLookupParams } from './scope-resolution/registries/lookup-core.js'; export { lookupQualified } from './scope-resolution/registries/lookup-qualified.js'; @@ -127,7 +129,12 @@ export { CONFIDENCE_EPSILON, } from './scope-resolution/registries/tie-breaks.js'; export type { TieBreakKey } from './scope-resolution/registries/tie-breaks.js'; -export { CLASS_KINDS, METHOD_KINDS, FIELD_KINDS } from './scope-resolution/registries/context.js'; +export { + CLASS_KINDS, + METHOD_KINDS, + FIELD_KINDS, + MACRO_KINDS, +} from './scope-resolution/registries/context.js'; export type { RegistryContext, RegistryProviders, diff --git a/gitnexus-shared/src/scope-resolution/reference-site.ts b/gitnexus-shared/src/scope-resolution/reference-site.ts index 58fabd4277..7b7b8be8e5 100644 --- a/gitnexus-shared/src/scope-resolution/reference-site.ts +++ b/gitnexus-shared/src/scope-resolution/reference-site.ts @@ -37,7 +37,12 @@ export type ReferenceKind = | 'write' | 'type-reference' | 'inherits' - | 'import-use'; + | 'import-use' + // A macro invocation (`log!(...)` / `vec![...]`). Resolved against + // `Macro`-labeled definitions ONLY (see `MacroRegistry`) so a macro + // never aliases a same-named free function — macros and functions are + // disjoint namespaces. Emitted as a `USES` edge, not `CALLS`. + | 'macro'; /** * How a call site binds its target. Informs `Registry.lookup` Step 2 diff --git a/gitnexus-shared/src/scope-resolution/registries/context.ts b/gitnexus-shared/src/scope-resolution/registries/context.ts index 657856f3e2..cd37a3e95d 100644 --- a/gitnexus-shared/src/scope-resolution/registries/context.ts +++ b/gitnexus-shared/src/scope-resolution/registries/context.ts @@ -162,3 +162,10 @@ export const FIELD_KINDS: readonly NodeLabel[] = Object.freeze([ 'Const', 'Static', ]); + +// Macros occupy a namespace disjoint from functions/methods: a `log!` +// invocation must resolve ONLY to a `macro_rules! log` definition, never +// to a same-named `fn log`. `MACRO_KINDS` is therefore a singleton +// (`['Macro']`) and is NOT merged into METHOD_KINDS — keeping the two +// keyspaces separate is what prevents the cross-namespace false-edge. +export const MACRO_KINDS: readonly NodeLabel[] = Object.freeze(['Macro']); diff --git a/gitnexus-shared/src/scope-resolution/registries/macro-registry.ts b/gitnexus-shared/src/scope-resolution/registries/macro-registry.ts new file mode 100644 index 0000000000..0eaf0e96af --- /dev/null +++ b/gitnexus-shared/src/scope-resolution/registries/macro-registry.ts @@ -0,0 +1,43 @@ +/** + * `MacroRegistry` — scope-aware lookup for macro definitions + * (`macro_rules!` in Rust; `#define` in C/C++) referenced from a macro + * invocation site. + * + * Thin wrapper over `lookupCore`, specialized for the macro namespace: + * + * - `acceptedKinds` = `MACRO_KINDS` (`['Macro']` only). Crucially this + * does NOT include `Function`/`Method`, so a `log!(…)` invocation can + * never resolve to a same-named free function `fn log` — macros and + * functions are disjoint namespaces (the false-`CALLS`-edge class the + * #1934 review flagged). + * - `useReceiverTypeBinding` is **false** — a macro invocation has no + * receiver; resolution is name-through-the-lexical-chain + the global + * qualified fallback, exactly like `ClassRegistry`. + * - Arity is not applied — macros are variadic by nature. + */ + +import type { Resolution, ScopeId } from '../types.js'; +import { lookupCore, type CoreLookupParams } from './lookup-core.js'; +import { MACRO_KINDS, type RegistryContext } from './context.js'; + +export interface MacroRegistry { + /** + * Look up a macro definition by simple or scoped name anchored at + * `scope`. Returns a confidence-ranked `Resolution[]`; consume `[0]` + * for the best answer. + */ + lookup(name: string, scope: ScopeId): readonly Resolution[]; +} + +export function buildMacroRegistry(ctx: RegistryContext): MacroRegistry { + const params: CoreLookupParams = { + acceptedKinds: MACRO_KINDS, + useReceiverTypeBinding: false, + ownerScopedContributor: null, + }; + return { + lookup(name: string, scope: ScopeId) { + return lookupCore(name, scope, params, ctx); + }, + }; +} diff --git a/gitnexus-shared/src/scope-resolution/types.ts b/gitnexus-shared/src/scope-resolution/types.ts index 828874a7f1..c73543d053 100644 --- a/gitnexus-shared/src/scope-resolution/types.ts +++ b/gitnexus-shared/src/scope-resolution/types.ts @@ -439,7 +439,7 @@ export interface Reference { readonly toDef: DefId; /** Location of the reference in source. */ readonly atRange: Range; - readonly kind: 'call' | 'read' | 'write' | 'type-reference' | 'inherits' | 'import-use'; + readonly kind: 'call' | 'read' | 'write' | 'type-reference' | 'inherits' | 'import-use' | 'macro'; readonly confidence: number; readonly evidence: readonly ResolutionEvidence[]; } diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 23dd6986e0..96fa1430d1 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -28,10 +28,10 @@ "scaling_budget": 1.5 }, "rust": { - "fingerprint": "3c4b8e0a707299cc5db0af2528c72a99457859104589a7ef3cd1f377da01793e", + "fingerprint": "a5fdff2cf427504e33e66d0221b3ad62739c64bd0898e1dafedc15dbbe347b4d", "scaling_budget": 1.5, - "_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04).", - "_note": "#1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls). Pure fixture-corpus drift — the fix is the legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target, NOT the rust scope-extractor; existing fixtures' captures byte-identical. fixture_count 120->121." + "_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) — legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical.", + "_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126)." }, "php": { "fingerprint": "f9c8eaf6d1084f9b95a9fb97ccce5e618a24d936c85fb8af4b96c73a560f7a7f", diff --git a/gitnexus/src/core/ingestion/emit-references.ts b/gitnexus/src/core/ingestion/emit-references.ts index 1d2c4db5b3..8c1145874c 100644 --- a/gitnexus/src/core/ingestion/emit-references.ts +++ b/gitnexus/src/core/ingestion/emit-references.ts @@ -267,9 +267,14 @@ function buildRelationship( /** * Map a `Reference.kind` to an existing `RelationshipType`. Read/write - * both fold into `ACCESSES`; `type-reference` + `import-use` both fold - * into `USES`. This keeps the graph schema additive — no new + * both fold into `ACCESSES`; `type-reference`, `import-use`, and `macro` + * all fold into `USES`. This keeps the graph schema additive — no new * RelationshipType values are introduced by this module. + * + * `macro` folds into `USES` (not `CALLS`) deliberately: a macro + * invocation targets a `Macro` node, not a callable function, so keeping + * it out of the `CALLS` keyspace preserves the invariant that `CALLS` + * edges denote function/method dispatch. */ function mapKindToType(kind: Reference['kind']): RelationshipType { switch (kind) { @@ -282,6 +287,7 @@ function mapKindToType(kind: Reference['kind']): RelationshipType { return 'INHERITS'; case 'type-reference': case 'import-use': + case 'macro': return 'USES'; } } diff --git a/gitnexus/src/core/ingestion/languages/rust/query.ts b/gitnexus/src/core/ingestion/languages/rust/query.ts index 8e4671ba3e..d85660981d 100644 --- a/gitnexus/src/core/ingestion/languages/rust/query.ts +++ b/gitnexus/src/core/ingestion/languages/rust/query.ts @@ -8,6 +8,7 @@ const RUST_SCOPE_QUERY = ` (trait_item) @scope.class (impl_item) @scope.class (enum_item) @scope.class +(union_item) @scope.class (function_item) @scope.function (closure_expression) @scope.function (block) @scope.block @@ -30,6 +31,26 @@ const RUST_SCOPE_QUERY = ` (enum_item name: (type_identifier) @declaration.name) @declaration.enum +;; Declarations — union +;; Deliberately tagged @declaration.struct (→ Struct label), NOT a +;; @declaration.union: every registry-primary resolution gate — +;; isLinkableLabel (node-lookup.ts), CALLABLE_OR_TYPE_LIKE +;; (finalize-algorithm.ts), ClassLikeNodeLabel (class-types.ts) — includes +;; Struct but EXCLUDES Union, so a Union-labeled node would be an +;; unresolvable orphan. A Rust union is a type whose literal is a real +;; constructor, so Struct is both the resolvable and the semantically +;; honest label here. #1934 F71. +(union_item + name: (type_identifier) @declaration.name) @declaration.struct + +;; Declarations — macro (macro_rules! foo { ... }) +;; Captured as @declaration.macro → Macro label. A macro invocation +;; (@reference.macro, below) resolves to this definition via MacroRegistry, +;; whose acceptedKinds is ['Macro'] ONLY — so an invoked macro never binds +;; to a same-named free function (log!() is not fn log). #1934 F72. +(macro_definition + name: (identifier) @declaration.name) @declaration.macro + ;; Declarations — function (top-level or inside mod) (function_item name: (identifier) @declaration.name) @declaration.function @@ -40,6 +61,10 @@ const RUST_SCOPE_QUERY = ` type: (_) @declaration.field-type) @declaration.field ;; Declarations — variables (let bindings) +;; Uses pattern:(identifier) — works for let x and let mut x (mutable_specifier +;; is a sibling, not a wrapper). Destructuring patterns like let (a, b) use +;; tuple_pattern etc. which pattern:(identifier) intentionally does not match; +;; capturing them with (_) would produce "(a, b)" as the name, which is useless. (let_declaration pattern: (identifier) @declaration.name) @declaration.variable @@ -109,9 +134,24 @@ const RUST_SCOPE_QUERY = ` name: (identifier) @reference.name)) @reference.call.free ;; References — constructor calls (struct literal) +;; Covers bare names (Foo {}), scoped (foo::bar::Baz {}), and turbofish +;; (Foo:: {}) — the name: field resolves to the trailing identifier +;; in all cases through tree-sitter-rust's grammar. (struct_expression name: (_) @reference.name) @reference.call.constructor +;; References — macro invocations (disjoint namespace from functions) +;; Resolved via MacroRegistry → Macro defs only (never fn of the same name). +(macro_invocation + macro: (identifier) @reference.name) @reference.macro + +;; Scoped macro invocation (log::info!(…)) — capture the tail identifier, +;; mirroring the scoped free-call pattern above, so the resolved name is +;; the tail (info), not the full path (log::info). +(macro_invocation + macro: (scoped_identifier + name: (identifier) @reference.name)) @reference.macro + ;; References — field reads (field_expression value: (_) @reference.receiver diff --git a/gitnexus/src/core/ingestion/resolve-references.ts b/gitnexus/src/core/ingestion/resolve-references.ts index 837635422d..e4413c119a 100644 --- a/gitnexus/src/core/ingestion/resolve-references.ts +++ b/gitnexus/src/core/ingestion/resolve-references.ts @@ -41,12 +41,14 @@ import { buildClassRegistry, buildFieldRegistry, + buildMacroRegistry, buildMethodRegistry, CLASS_KINDS, FIELD_KINDS, METHOD_KINDS, type ClassRegistry, type FieldRegistry, + type MacroRegistry, type MethodRegistry, type Reference, type ReferenceIndex, @@ -102,6 +104,7 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef const classRegistry = buildClassRegistry(ctx); const methodRegistry = buildMethodRegistry(ctx); const fieldRegistry = buildFieldRegistry(ctx); + const macroRegistry = buildMacroRegistry(ctx); // bySourceScope is the canonical index; byTargetDef is derived from it. const bySourceScope = new Map(); @@ -114,7 +117,13 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef for (const site of scopes.referenceSites) { sitesProcessed++; - const resolutions = lookupForSite(site, classRegistry, methodRegistry, fieldRegistry); + const resolutions = lookupForSite( + site, + classRegistry, + methodRegistry, + fieldRegistry, + macroRegistry, + ); if (resolutions.length === 0) { unresolved++; continue; @@ -165,6 +174,12 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef * | `type-reference` | ClassRegistry | CLASS_KINDS | * | `read`/`write` | FieldRegistry | FIELD_KINDS | * | `import-use` | tiered fallback | METHOD ∪ CLASS ∪ FIELD | + * | `macro` | MacroRegistry | MACRO_KINDS (`Macro` only) | + * + * `macro` has its own single-kind registry so a macro invocation + * (`log!(…)`) resolves ONLY to a `macro_rules! log` definition and never + * to a same-named free function — macros and functions are disjoint + * namespaces (the false-`CALLS`-edge class flagged in the #1934 review). * * `import-use` doesn't have a single registry — the imported name might * be a class, a function, or a constant. Try each in priority order and @@ -177,6 +192,7 @@ function lookupForSite( classRegistry: ClassRegistry, methodRegistry: MethodRegistry, fieldRegistry: FieldRegistry, + macroRegistry: MacroRegistry, ): readonly Resolution[] { switch (site.kind) { case 'call': { @@ -213,6 +229,11 @@ function lookupForSite( if (methodHits.length > 0) return methodHits; return fieldRegistry.lookup(site.name, site.inScope); } + case 'macro': { + // Macro-only namespace: resolves against `Macro`-labeled defs, never + // functions. No receiver, no arity — see `MacroRegistry`. + return macroRegistry.lookup(site.name, site.inScope); + } } } diff --git a/gitnexus/src/core/ingestion/scope-extractor.ts b/gitnexus/src/core/ingestion/scope-extractor.ts index 31a59d2f5c..446ab75fec 100644 --- a/gitnexus/src/core/ingestion/scope-extractor.ts +++ b/gitnexus/src/core/ingestion/scope-extractor.ts @@ -745,6 +745,8 @@ function normalizeNodeLabel(kindStr: string): SymbolDefinition['type'] | undefin return 'Annotation'; case 'namespace': return 'Namespace'; + case 'macro': + return 'Macro'; default: return undefined; } @@ -1044,6 +1046,8 @@ function referenceKindFromAnchor(name: string): ReferenceKind | undefined { case 'import_use': case 'import-use': return 'import-use'; + case 'macro': + return 'macro'; default: return undefined; } diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts index 19b6bf0f16..5c7120495c 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/edges.ts @@ -39,6 +39,11 @@ export function mapReferenceKindToEdgeType( return 'EXTENDS'; case 'type-reference': return 'USES'; + // Macro invocations resolve to a `Macro` node (never a function), so + // they emit `USES` — kept out of the `CALLS` keyspace which denotes + // function/method dispatch (#1934 review). + case 'macro': + return 'USES'; case 'import-use': return undefined; default: diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts index 229c83f450..80bc5dedd0 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts @@ -186,6 +186,11 @@ export function isLinkableLabel(label: NodeLabel): boolean { // `Variable` def for `export const fooService = {...}` to the canonical // `Const:filePath:name` graph node id, against which object-literal // method symbols register their `ownerId` (PR #1718 / issue #1358). - label === 'Const' + label === 'Const' || + // Macro nodes are linkable so a macro invocation (`log!(…)`) resolved + // via `MacroRegistry` can bridge its scope-resolution `Macro` def to + // the legacy `@definition.macro` graph node and emit the `USES` edge + // (Rust #1934 F72; also covers C/C++ `#define` macro defs). + label === 'Macro' ); } diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 91881f4af4..1be596a69e 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -1215,6 +1215,11 @@ export const RUST_QUERIES = ` (function_item name: (identifier) @name) @definition.function (function_signature_item name: (identifier) @name) @definition.function (struct_item name: (type_identifier) @name) @definition.struct +; A union is materialized as a Struct node (same rationale as the +; scope-resolution @declaration.struct in languages/rust/query.ts: every +; registry-primary resolution gate includes Struct but excludes Union, so a +; Union-labeled node would be an unresolvable orphan). #1934 F71. +(union_item name: (type_identifier) @name) @definition.struct (enum_item name: (type_identifier) @name) @definition.enum (trait_item name: (type_identifier) @name) @definition.trait (impl_item type: (type_identifier) @name !trait) @definition.impl diff --git a/gitnexus/test/fixtures/lang-resolution/rust-coverage/macros.rs b/gitnexus/test/fixtures/lang-resolution/rust-coverage/macros.rs new file mode 100644 index 0000000000..08af370e52 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-coverage/macros.rs @@ -0,0 +1,5 @@ +// F72 — macro invocations +fn use_macros() { + println!("hello"); + let v = vec![1, 2, 3]; +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-coverage/patterns.rs b/gitnexus/test/fixtures/lang-resolution/rust-coverage/patterns.rs new file mode 100644 index 0000000000..4aceece6e7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-coverage/patterns.rs @@ -0,0 +1,10 @@ +// F66/F68 — let binding with various pattern shapes +fn pattern_shapes() { + let x = 1; // bare identifier + let mut y = 2; // identifier with mut + let (a, b) = (1, 2); // tuple pattern + let Some(val) = Some(3); // tuple struct pattern + let Foo { field } = Foo { field: 1 }; // struct pattern + let ref z = 4; // ref pattern + let n @ 1..=10 = 5; // captured pattern +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-coverage/union.rs b/gitnexus/test/fixtures/lang-resolution/rust-coverage/union.rs new file mode 100644 index 0000000000..3bb32cc2cb --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-coverage/union.rs @@ -0,0 +1,5 @@ +// F71 — union declaration +union MyUnion { + x: i32, + y: f64, +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-macro/lib.rs b/gitnexus/test/fixtures/lang-resolution/rust-macro/lib.rs new file mode 100644 index 0000000000..d3ba24ea84 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-macro/lib.rs @@ -0,0 +1,21 @@ +// F72 — a macro invocation resolves to its `macro_rules!` definition (a +// Macro node) via a USES edge, and NEVER to a same-named free function. +// Macros and functions are disjoint namespaces. + +macro_rules! greet { + ($name:expr) => { + let _ = $name; + }; +} + +// Same simple name as the macro, on purpose: proves the macro invocation +// does not bind to this function (no false CALLS edge) and the function +// call does not bind to the macro. +fn greet() -> u32 { + 0 +} + +fn run() { + greet!("world"); // macro invocation -> USES edge to Macro greet + let _ = greet(); // function call -> CALLS edge to Function greet +} diff --git a/gitnexus/test/fixtures/lang-resolution/rust-union/lib.rs b/gitnexus/test/fixtures/lang-resolution/rust-union/lib.rs new file mode 100644 index 0000000000..d6c19ce009 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/rust-union/lib.rs @@ -0,0 +1,12 @@ +// F71 — a `union` is captured as a Struct-labeled node (every +// registry-primary resolution gate includes Struct but excludes Union), +// and it is resolvable: the union literal is a real type constructor. + +union MyUnion { + int_val: i32, + float_val: f64, +} + +fn make() -> MyUnion { + MyUnion { int_val: 5 } // constructor -> CALLS edge to the Struct MyUnion +} diff --git a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json index 23aefaf5a7..9a89f154ce 100644 --- a/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/rust-captures-golden/expected-captures.json @@ -1,7 +1,7 @@ { "rust-abstract-dispatch/src/lib.rs": { - "captureGroups": 27, - "digest": "e7a8f5bca32037a547093095eae32d68560b9ef2bbb29c51bef9f2e7b3e57614" + "captureGroups": 28, + "digest": "f1fad77028347c5102a39dbe6522786fa7863e5d4fcaf675294dffc23555d23b" }, "rust-abstract-dispatch/src/main.rs": { "captureGroups": 19, @@ -123,6 +123,18 @@ "captureGroups": 15, "digest": "043cd8b9341ab299750f8d07f1d1ca34b714c4ecf69acba80ec827e93e3852c0" }, + "rust-coverage/macros.rs": { + "captureGroups": 7, + "digest": "ff4606a898325894a9ef71fa5d16446fba7f24976bb88d7fcf5e6e62f76f99d3" + }, + "rust-coverage/patterns.rs": { + "captureGroups": 8, + "digest": "107045c7d648c89b825214f56bdaffee80fccbc063b69a126b95d5aca27618b6" + }, + "rust-coverage/union.rs": { + "captureGroups": 5, + "digest": "886081c47a5a9d84b00024e09843753e3dc1731f5e1c520e73b60ede8b9701ae" + }, "rust-cross-module-collision/src/a.rs": { "captureGroups": 11, "digest": "27e5b3770416d99fa69fcd492adf565c296780181e11fd5ac6bd26f3c57a9ab0" @@ -188,12 +200,12 @@ "digest": "292eba3d86490a2ee600ebe4d9e19434204b43706af459c25ae82c2b646da5f9" }, "rust-for-call-expr/src/repo.rs": { - "captureGroups": 13, - "digest": "2b7e531ed136a976228b8073066f1fd9ba30e9409c10d5708c544a617ad2f31c" + "captureGroups": 14, + "digest": "6c6c53ab458997365f7ee4a9675ec64809aaebd2dd101b6418928b0d9313bdec" }, "rust-for-call-expr/src/user.rs": { - "captureGroups": 13, - "digest": "9e5ee9a073c988caace8946b7008e56e7a1cfa852972a28387f8bf0c52b73555" + "captureGroups": 14, + "digest": "7331591b986aa2a9b0ee57020c1f0285ad0da10d5044c326b4beaaeb26019aa8" }, "rust-for-loop/src/main.rs": { "captureGroups": 24, @@ -208,12 +220,12 @@ "digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e" }, "rust-grouped-imports/src/helpers/mod.rs": { - "captureGroups": 13, - "digest": "04ce0efd7458e7ff624e5611d34bbb0c01e77259108e0477895bba61e9568249" + "captureGroups": 14, + "digest": "32bc4f57a1dc0ffe8e9cbf0f0d6ce2ac5b1f2f93e2e3de3f217c92636480de63" }, "rust-grouped-imports/src/main.rs": { - "captureGroups": 13, - "digest": "17fff90e2627d094f24ffb7cd06b258d3f69b026b52f892c66132d061d031b1e" + "captureGroups": 14, + "digest": "d0997acf33c23b27864423aebaa1b1f4341d57bf5374c6f40c7efa6a166b39e7" }, "rust-if-let-unwrap/models/mod.rs": { "captureGroups": 1, @@ -260,12 +272,16 @@ "digest": "a8562a331eb945b4c099a7a9ec6c9b5eed8ff603c9359897b0445ce316a1424e" }, "rust-local-shadow/src/main.rs": { - "captureGroups": 15, - "digest": "a9903b31883988b89bf634ea2bda7de522f256d06b0d80a65050a3fb4fae4a80" + "captureGroups": 16, + "digest": "66d3a2678fb33301bbaa419684d208a0dc5c914d6e3bcf20e1c99e9666ed6fa2" }, "rust-local-shadow/src/utils.rs": { - "captureGroups": 5, - "digest": "8007a597a21f60c078ceaaa96c660d157d4f1c27401345145be0b5b8b1fb4c0d" + "captureGroups": 6, + "digest": "493dbeab554e28bed4a03cb62b951cad4f43bce5b5b522a192cd04159b3b2d9c" + }, + "rust-macro/lib.rs": { + "captureGroups": 11, + "digest": "19b650be99256aa211356edc5a3dde83aff21e5d763e2c079322a24045bf69c9" }, "rust-match-unwrap/src/main.rs": { "captureGroups": 24, @@ -296,8 +312,8 @@ "digest": "cd836a2a9c15ab240961d2e15f192f7e33d65eb5ebf2e1a8af2f620a47fe66ae" }, "rust-method-enrichment/src/lib.rs": { - "captureGroups": 38, - "digest": "014c09ab82a5a348c2a6225e07da0773981dd6f282492b4517e33071873dcda6" + "captureGroups": 39, + "digest": "8e60a44f5e18d1e26eea4bb21129494d2e3be697fec373dd4b6044f989176d9e" }, "rust-method-enrichment/src/main.rs": { "captureGroups": 18, @@ -348,8 +364,8 @@ "digest": "15be069f28f1400e4beb0b0860acb59979f78549960486f36a92f56578f05a06" }, "rust-qualified-trait/src/widget.rs": { - "captureGroups": 22, - "digest": "3e1d4c6167338e410d9d93bf5f80f289ffb2f05611813e59c7a53402bf6a101d" + "captureGroups": 23, + "digest": "ee34385539f7e9398123c056738c6a662a80dac41fc038db96fab0da5c84c8ac" }, "rust-receiver-resolution/src/main.rs": { "captureGroups": 21, @@ -404,20 +420,20 @@ "digest": "7346b2cf62e4946b261ed0eb46f2623fb1882f46d832abdc2068012917c7b16a" }, "rust-scoped-multi-file/src/models/repo.rs": { - "captureGroups": 18, - "digest": "5fdd3d9d0fa35089cfda53a3e84ac4b788c5dca97b7268c3f634d3bca5ba40d8" + "captureGroups": 19, + "digest": "25a3d44bc451ccaf8c6875b226fbda22ed1a18dcc884d4c6d9914ed83fc555b0" }, "rust-scoped-multi-file/src/models/user.rs": { - "captureGroups": 18, - "digest": "cc1404f69ca2264fd6ae12aebbd5cc6844eccd68b11595ebe9c1e861d168a86b" + "captureGroups": 19, + "digest": "77dc9858229322d8e1abbf209230bfbf6fca00627a5864888e91f1841b1f0cbb" }, "rust-self-struct-literal/main.rs": { "captureGroups": 11, "digest": "3c0beb60f1487a63c60329e0843c1913b6a3854bb1ed3df84e4a07bc16dbd82d" }, "rust-self-struct-literal/models.rs": { - "captureGroups": 31, - "digest": "91e3ba35c7dcf31ab8914f052bbec884d0b9f4f0ab991878b084fbf9e4fac192" + "captureGroups": 32, + "digest": "e02d230c1215b3fd87fedbdaf98649a407fa1f2bfc61beb66f49d7ba070de7de" }, "rust-self-this-resolution/src/repo.rs": { "captureGroups": 10, @@ -444,8 +460,8 @@ "digest": "e71269ff626cd0b0655591ee08e48d90e9a1421f3be8d9e8ac23262388a3f7ad" }, "rust-struct-literal-inference/models.rs": { - "captureGroups": 26, - "digest": "363f8d0d3948d88b2b656a80db15d6c0c0c43e0f8ecc3e6a504cf8d8d7e6a020" + "captureGroups": 27, + "digest": "dedd93d6214ff00ee0ee267140918403b7f59e0ab010cb9058af40cb3b08bb81" }, "rust-struct-literals/app.rs": { "captureGroups": 12, @@ -456,8 +472,8 @@ "digest": "60fc4ac44f58ae67d462e243655b0571392a18de85b9e200e9391b50c941a6c9" }, "rust-traits/src/impls/button.rs": { - "captureGroups": 32, - "digest": "ba93629d0e5a008a5ea84b6a93ea93b1ae4901cc24d8c4c7ee685f51e2712f12" + "captureGroups": 34, + "digest": "80fc76cdf20e3e0594a1ed933ce25682519151632157d18ef011dba60a2245d1" }, "rust-traits/src/main.rs": { "captureGroups": 11, @@ -471,6 +487,10 @@ "captureGroups": 5, "digest": "1dca39bbc7c1b1b66f1a34730b9a5b4dba04c54ee9d2688255e0fd4e6bc48499" }, + "rust-union/lib.rs": { + "captureGroups": 10, + "digest": "e2a6fb9eab259b8c7104f1530b96b8c1f42ab32fe1d71d6bdca04d68263507f2" + }, "rust-write-access/models.rs": { "captureGroups": 9, "digest": "660f755fd70cd1796f9da02ad7d65f599dea8029665ee45ecd18cd27919741f3" diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index 391f5733eb..fe0410572b 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -181,6 +181,17 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly([ // #1756 companion-vs-instance dispatch: the registry-primary path // suppresses `instance.companionMethod()` via `ScopeResolver. diff --git a/gitnexus/test/integration/resolvers/rust-coverage.test.ts b/gitnexus/test/integration/resolvers/rust-coverage.test.ts new file mode 100644 index 0000000000..17a8714d2d --- /dev/null +++ b/gitnexus/test/integration/resolvers/rust-coverage.test.ts @@ -0,0 +1,91 @@ +/** + * Regression tests for Rust scope-resolution coverage gaps (issue #1934). + */ +import { describe, it, expect } from 'vitest'; +import { emitRustScopeCaptures } from '../../../src/core/ingestion/languages/rust/index.js'; +import type { CaptureMatch } from 'gitnexus-shared'; + +// --------------------------------------------------------------------------- +// F66/F68 — let binding patterns (identifier-only, works with let mut x) +// --------------------------------------------------------------------------- + +describe('F66/F68 — let binding pattern shapes', () => { + it('bare identifier let binding emits @declaration.variable', () => { + const src = `fn f() { let x = 1; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const vars = matches.filter((m) => m['@declaration.variable']); + expect(vars.length).toBe(1); + expect(vars[0]['@declaration.name'].text).toBe('x'); + }); + + it('let mut x emits @declaration.variable', () => { + const src = `fn f() { let mut x = 1; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const vars = matches.filter((m) => m['@declaration.variable']); + expect(vars.length).toBe(1); + expect(vars[0]['@declaration.name'].text).toBe('x'); + }); +}); + +// --------------------------------------------------------------------------- +// F71 — union declarations +// --------------------------------------------------------------------------- + +describe('F71 — union declaration', () => { + it('union item emits @scope.class and @declaration.struct', () => { + const src = `union MyUnion { x: i32, y: f64 }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const scopes = matches.filter((m) => m['@scope.class']); + expect(scopes.length).toBe(1); + const decls = matches.filter((m) => m['@declaration.struct']); + expect(decls.length).toBe(1); + expect(decls[0]['@declaration.name'].text).toBe('MyUnion'); + }); +}); + +// --------------------------------------------------------------------------- +// F72 — macro invocations (capture layer) +// +// These pin the tree-sitter CAPTURE shape only. End-to-end macro RESOLUTION +// (the @reference.macro → MacroRegistry → USES-edge-to-a-Macro-node path, and +// the guarantee that a macro never binds to a same-named function) is asserted +// at the pipeline level — and under the legacy-vs-registry-primary scope-parity +// gate — in `rust.test.ts` › "Rust macro resolution (issue #1934 F72)". +// --------------------------------------------------------------------------- + +describe('F72 — macro invocations (capture layer)', () => { + it('macro_invocation with bare identifier emits @reference.macro', () => { + const src = `fn f() { println!("hi"); }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const macroRefs = matches.filter((m) => m['@reference.macro']); + const macroNames = macroRefs.map((m) => m['@reference.name']?.text); + expect(macroNames).toContain('println'); + }); + + it('vec! macro emits @reference.macro', () => { + const src = `fn f() { let v = vec![1, 2, 3]; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const macroRefs = matches.filter((m) => m['@reference.macro']); + const macroNames = macroRefs.map((m) => m['@reference.name']?.text); + expect(macroNames).toContain('vec'); + }); + + it('scoped macro invocation captures the TAIL identifier, not the full path', () => { + const src = `fn f() { log::info!("hi"); }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const macroRefs = matches.filter((m) => m['@reference.macro']); + const macroNames = macroRefs.map((m) => m['@reference.name']?.text); + // Must be the tail `info`, not the whole path `log::info` — mirrors the + // scoped free-call pattern. Guards the P3 fix. + expect(macroNames).toContain('info'); + expect(macroNames).not.toContain('log::info'); + }); + + it('macro_rules! definition emits a @declaration.macro capture', () => { + const src = `macro_rules! greet { () => {}; }\n`; + const matches = emitRustScopeCaptures(src, 'test.rs') as CaptureMatch[]; + const macroDecls = matches.filter((m) => m['@declaration.macro']); + expect(macroDecls.length).toBe(1); + expect(macroDecls[0]['@declaration.name'].text).toBe('greet'); + }); +}); diff --git a/gitnexus/test/integration/resolvers/rust.test.ts b/gitnexus/test/integration/resolvers/rust.test.ts index bcfa4200d9..91af9af7a1 100644 --- a/gitnexus/test/integration/resolvers/rust.test.ts +++ b/gitnexus/test/integration/resolvers/rust.test.ts @@ -12,9 +12,15 @@ import { findDanglingEdges, edgeSet, runPipelineFromRepo, + createResolverParityIt, type PipelineResult, } from './helpers.js'; +// Registry-primary-only assertions (e.g. macro resolution, which the legacy +// DAG does not implement) use this parity-aware `it` so they are skipped — +// not failed — under the legacy half of the scope-parity gate. +const rustParityIt = createResolverParityIt('rust'); + // --------------------------------------------------------------------------- // Heritage: trait implementations // --------------------------------------------------------------------------- @@ -2047,3 +2053,77 @@ describe('Rust scoped inherent impl — ownership + collision (issue #1975)', () expect(fromA!.source).not.toBe(fromB!.source); }); }); + +// --------------------------------------------------------------------------- +// F71 — union declarations resolve as Struct nodes (issue #1934) +// +// A `union` is deliberately captured as a Struct-labeled node (see the +// rationale in languages/rust/query.ts): every registry-primary resolution +// gate includes Struct but excludes Union, so a Union-labeled node would be +// an unresolvable orphan. These pipeline-level assertions pin BOTH that the +// node is labeled Struct AND that it is genuinely resolvable (the union +// literal is a real constructor) — works on the legacy + registry-primary +// paths, so it runs under both halves of the scope-parity gate. +// --------------------------------------------------------------------------- + +describe('Rust union resolution (issue #1934 F71)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-union'), () => {}); + }, 60000); + + it('captures the union as a Struct node named MyUnion (not Union)', () => { + expect(getNodesByLabel(result, 'Struct')).toContain('MyUnion'); + expect(getNodesByLabel(result, 'Union')).toEqual([]); + }); + + it('resolves the union literal MyUnion { .. } as a CALLS edge to the Struct', () => { + const calls = getRelationships(result, 'CALLS'); + const ctor = calls.find((e) => e.source === 'make' && e.target === 'MyUnion'); + expect(ctor).toBeDefined(); + expect(ctor!.targetLabel).toBe('Struct'); + }); +}); + +// --------------------------------------------------------------------------- +// F72 — macro invocations resolve to their definition (issue #1934) +// +// A `macro_rules! greet` invocation (`greet!(...)`) resolves via the +// MacroRegistry to the Macro node, emitting a USES edge — NEVER a CALLS +// edge, and NEVER binding to a same-named free function `fn greet`. This is +// a registry-primary-only capability (the legacy DAG does not resolve +// macros), so the resolution assertions use `rustParityIt` and are listed +// in helpers' LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES. +// --------------------------------------------------------------------------- + +describe('Rust macro resolution (issue #1934 F72)', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'rust-macro'), () => {}); + }, 60000); + + it('materializes both a Macro and a same-named Function node', () => { + expect(getNodesByLabel(result, 'Macro')).toContain('greet'); + expect(getNodesByLabel(result, 'Function')).toContain('greet'); + }); + + rustParityIt('resolves greet!(..) as a USES edge to the Macro (not the Function)', () => { + const uses = getRelationships(result, 'USES'); + const macroUse = uses.find((e) => e.source === 'run' && e.target === 'greet'); + expect(macroUse).toBeDefined(); + expect(macroUse!.targetLabel).toBe('Macro'); + }); + + rustParityIt('does NOT emit a CALLS edge from the macro invocation to fn greet', () => { + const calls = getRelationships(result, 'CALLS'); + // The only run -> greet CALLS edge is the genuine fn call; it must target + // the Function, and there must be exactly one (the macro adds no CALLS). + const greetCalls = calls.filter((e) => e.source === 'run' && e.target === 'greet'); + expect(greetCalls.length).toBe(1); + expect(greetCalls[0].targetLabel).toBe('Function'); + // And no CALLS edge anywhere targets the Macro node. + expect(calls.every((e) => e.targetLabel !== 'Macro')).toBe(true); + }); +});