GitNexus/gitnexus/test/integration/rust-pointer-opaque-dispatch.test.ts
Gergő Magyar 673dc6968e
fix(rust): see through Box, Rc and Arc on struct field types (#3498) (#3665)
* fix(rust): see through Box, Rc and Arc on struct field types (#3498)

The struct-field normalizer in rust/range-binding.ts cut a type at its
first `<`. A `repo: Box<User>` field was typed `Box`, so `self.repo.save()`
bound nothing. It also stripped a leading `&` but not a lifetime, so a
borrowed field, which always carries one (`&'a User`), came out as
`'a User`.

Box, Rc and Arc auto-deref on a method call, so the field now takes their
first type argument: bare or path-qualified (`std::rc::Rc<User>`), and
nested (`Rc<Box<User>>`). Borrows with a lifetime or `mut` are stripped,
and `Box<dyn Trait>` reduces to the trait through the stripDynBound helper
that parameters already use (#2604).

RefCell, Cell, Mutex, RwLock, Option and Vec are deliberately left alone.
A method on the inner type needs `.borrow()`, `.get()`, `.lock()`,
`.read()`, a match or an index first, so the receiver really is the
wrapper, and a direct call through one must not bind.

Shape arm: rust `decoratedFieldType` moves INVISIBLE-GAP -> RESOLVES, with
the edge on `User::save`. The corpus has no smart-pointer or borrowed
struct field, so the count arm and every other cell are unchanged, and the
corpus edge (2838) and drop (192) sets match main exactly. Scope-capture
fingerprints are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(rust): keep opaque pointer fields wrapped

* fix(rust): refuse opaque macro expansions

* test(rust): pin pointer wrapper targets and opacity limits (#3665)

- Assert a same-file Box keeps its own save target while User.save stays
  unbound (review thread on custom same-named wrappers).
- Pin the documented workspace-wide recall limit: an unrelated unresolved
  import or macro_rules! keeps an otherwise valid pointer field wrapped.
- Drop per-case logging and group the opaque/expansion cases in describe
  blocks.
- Collapse the stacked pointer-field receipts in BASELINE.md into one
  current section listing every workspace-wide refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: preserve Rust integration notes

* test(rust): assert complete pointer targets

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:19:27 +00:00

48 lines
2.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { writeFixtureRepo, runPipelineFromRepo, getRelationships } from './resolvers/helpers.js';
const cases = [
[
'super inherent',
'use std::rc::Rc;\nstruct User; impl User { fn save(&self) {} }\nmod ext { impl super::User { pub fn as_ref(&self) {} } }\nstruct Holder { repo: Rc<User> }\nimpl Holder { fn probe(&self) { self.repo.as_ref(); } }',
],
[
'crate inherent',
'use std::rc::Rc;\nstruct User; impl User { fn save(&self) {} }\nmod ext { impl crate::User { pub fn as_ref(&self) {} } }\nstruct Holder { repo: Rc<User> }\nimpl Holder { fn probe(&self) { self.repo.as_ref(); } }',
],
[
'declined nested chain',
'use std::rc::Rc;\nstruct User;impl User { fn as_ref(&self) {} }\nstruct Holder { repo: Rc<User> }\nstruct Outer { holder: Holder }\nfn probe(o: &Outer) { o.holder.repo.as_ref(); }',
],
[
'unresolved external trait',
'use std::rc::Rc;\nuse external::Describe;\nstruct User; impl User { fn describe(&self) {} }\nstruct Holder { repo: Rc<User> }\nimpl Holder { fn probe(&self) { self.repo.describe(); } }',
],
[
'macro blanket trait',
'use std::rc::Rc;\nmacro_rules! declare_describe { () => { trait Describe { fn describe(&self); } impl<T> Describe for T { fn describe(&self) {} } } }\ndeclare_describe!();\nstruct User; impl User { fn describe(&self) {} }\nstruct Holder { repo: Rc<User> }\nimpl Holder { fn probe(&self) { self.repo.describe(); } }',
],
] as const;
describe('Rust pointer fields with opaque dispatch evidence', () => {
it.each(cases)(
'%s does not invent pointee dispatch',
async (_name, source) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pointer-synthesis-'));
try {
writeFixtureRepo(root, {
'src/lib.rs':
source +
'\nstruct Unrelated; impl Unrelated { fn as_ref(&self) {} fn describe(&self) {} }',
});
const r = await runPipelineFromRepo(root, () => {});
const edges = getRelationships(r, 'CALLS').filter((e) => e.source === 'probe');
expect(edges.map((e) => e.rel.targetId)).toEqual([]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
},
180000,
);
});