mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-04 07:27:20 +00:00
* 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>
154 lines
4.8 KiB
TypeScript
154 lines
4.8 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {
|
|
getRelationships,
|
|
runPipelineFromRepo,
|
|
writeFixtureRepo,
|
|
type PipelineResult,
|
|
} from './resolvers/helpers.js';
|
|
|
|
const roots: string[] = [];
|
|
afterAll(() => {
|
|
for (const root of roots) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
|
|
async function analyze(files: Record<string, string>): Promise<PipelineResult> {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-rust-pointer-dispatch-'));
|
|
roots.push(root);
|
|
writeFixtureRepo(root, files);
|
|
return runPipelineFromRepo(root, () => {}, {});
|
|
}
|
|
|
|
describe('pointer and pointee member ownership', () => {
|
|
let result: PipelineResult;
|
|
beforeAll(async () => {
|
|
result = await analyze({
|
|
'main.rs': `use std::rc::Rc;
|
|
use std::sync::Arc;
|
|
struct User;
|
|
impl User { fn as_ref(&self) {} fn save(&self) {} }
|
|
struct SafeUser;
|
|
impl SafeUser { fn save(&self) {} }
|
|
struct Holder {
|
|
boxed: Box<User>, rc: Rc<User>, arc: Arc<User>,
|
|
safe_boxed: Box<SafeUser>, safe_rc: Rc<SafeUser>, safe_arc: Arc<SafeUser>
|
|
}
|
|
fn boxed_builtin(h: &Holder) { h.boxed.as_ref(); }
|
|
fn rc_builtin(h: &Holder) { h.rc.as_ref(); }
|
|
fn arc_builtin(h: &Holder) { h.arc.as_ref(); }
|
|
fn boxed_target(h: &Holder) { h.boxed.save(); }
|
|
fn rc_target(h: &Holder) { h.rc.save(); }
|
|
fn arc_target(h: &Holder) { h.arc.save(); }
|
|
fn safe_boxed(h: &Holder) { h.safe_boxed.save(); }
|
|
fn safe_rc(h: &Holder) { h.safe_rc.save(); }
|
|
fn safe_arc(h: &Holder) { h.safe_arc.save(); }
|
|
`,
|
|
});
|
|
}, 120000);
|
|
|
|
it.each(['boxed_builtin', 'rc_builtin', 'arc_builtin'])(
|
|
'%s never substitutes User.as_ref for a pointer method',
|
|
(name) => {
|
|
expect(getRelationships(result, 'CALLS').filter((e) => e.source === name)).toEqual([]);
|
|
},
|
|
);
|
|
it.each(['boxed_target', 'rc_target', 'arc_target'])(
|
|
'%s conservatively leaves other methods unresolved on a conflicted field',
|
|
(name) => {
|
|
// save() is legal Rust. Whole-field refusal deliberately loses this edge
|
|
// until dispatch can represent ordered autoderef for individual members.
|
|
expect(getRelationships(result, 'CALLS').filter((e) => e.source === name)).toEqual([]);
|
|
},
|
|
);
|
|
it.each(['safe_boxed', 'safe_rc', 'safe_arc'])(
|
|
'%s still resolves an unconflicted pointee method',
|
|
(name) => {
|
|
expect(
|
|
getRelationships(result, 'CALLS')
|
|
.filter((e) => e.source === name && e.target === 'save')
|
|
.map((e) => e.rel.targetId),
|
|
).toEqual(['Function:main.rs:SafeUser.save#0']);
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('pointer declaration ownership', () => {
|
|
it('does not replace a qualified pointee with a local namesake', async () => {
|
|
const result = await analyze({
|
|
'src/lib.rs': `mod remote;
|
|
struct User;
|
|
impl User { fn save(&self) {} }
|
|
struct Holder { repo: Box<remote::User> }
|
|
impl Holder {
|
|
fn direct(&self) { self.repo.save(); }
|
|
fn assigned(&self) { let value = self.repo; value.save(); }
|
|
}
|
|
fn destructured(h: Holder) { let Holder { repo } = h; repo.save(); }
|
|
`,
|
|
'src/remote.rs': 'pub struct User; impl User { pub fn save(&self) {} }',
|
|
});
|
|
for (const name of ['direct', 'assigned', 'destructured']) {
|
|
expect(
|
|
getRelationships(result, 'CALLS').filter(
|
|
(e) => e.source === name && e.target === 'save' && e.targetFilePath === 'src/lib.rs',
|
|
),
|
|
).toEqual([]);
|
|
}
|
|
}, 120000);
|
|
|
|
it.each([
|
|
['standard crate', '', true],
|
|
[
|
|
'crate alias',
|
|
`#![no_std]
|
|
extern crate self as std;
|
|
pub mod boxed {
|
|
pub struct Box<T> { pub value: T }
|
|
impl<T> Box<T> { pub fn save(&self) {} }
|
|
}
|
|
`,
|
|
false,
|
|
],
|
|
] as const)(
|
|
'%s respects absolute wrapper ownership',
|
|
async (_name, prefix, standard) => {
|
|
const result = await analyze({
|
|
'src/lib.rs': `${prefix}
|
|
struct User;
|
|
impl User { fn save(&self) {} }
|
|
struct Holder { repo: ::std::boxed::Box<User> }
|
|
impl Holder { fn direct(&self) { self.repo.save(); } }
|
|
`,
|
|
});
|
|
const calls = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.source === 'direct' && e.rel.targetId === 'Function:src/lib.rs:User.save#0',
|
|
);
|
|
expect(calls).toHaveLength(standard ? 1 : 0);
|
|
},
|
|
120000,
|
|
);
|
|
|
|
it.each(['Rc<User>', '&Rc<User>'])(
|
|
'does not bypass a local trait implementation on %s',
|
|
async (receiver) => {
|
|
const result = await analyze({
|
|
'main.rs': `use std::rc::Rc;
|
|
trait Save { fn save(&self); }
|
|
struct User;
|
|
impl User { fn save(&self) {} }
|
|
impl Save for ${receiver} { fn save(&self) {} }
|
|
struct Holder { repo: Rc<User> }
|
|
fn probe(h: &Holder) { h.repo.save(); }
|
|
`,
|
|
});
|
|
expect(
|
|
getRelationships(result, 'CALLS').filter(
|
|
(e) => e.source === 'probe' && e.rel.targetId === 'Function:main.rs:User.save#0',
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
120000,
|
|
);
|
|
});
|