Skip to content

Rust: rest of #2614 — derive(Default), outside builder and outside writer calls link to a same-named project method #3252

Open

Description

@Egrn

Environment

codegraph 1.6.2 from npm, and current main (f72c9d2) built from source and run on Node 22.23.4. Linux, rustc 1.99.1, fresh codegraph init. The repro crate passes cargo check.

Problem

This is the rest of #2614. Thanks for the fixes there: all four shapes from its repro crate are fine on current main. But some other shapes still link a method call to a project method that only has the same name. In shapes 2–5 the receiver is not a project type at all; in shape 1 the path names a project type that has no hand-written default. In our crate (42 files) this gives 14 false call edges on current main (15 on 1.6.2).

  1. Rules::default() where Rules has #[derive(Default)] goes to Tokens::default, the only hand-written impl Default in the crate. The type is written right in the path.
  2. ext::Builder::new().enable().build() (a builder from another crate) goes to Graph::build, which is an associated fn without self.
  3. std::fs::OpenOptions::new() … .write(true) written on several lines inside a closure goes to Log::write from impl Write for Log. On one line outside a closure it is fine.
  4. ext::GzBuilder::new().mtime(0).write(data, 6) and then enc.finish()?.flush()? go to Log::write and Log::flush.
  5. let mut stdout = ext::stdout(); stdout.flush() goes to Log::flush.

Minimal repro

Two crates side by side; only app is indexed (codegraph init app), so ext stays outside the project.

ext/Cargo.toml:

[package]
name = "ext"
version = "0.1.0"
edition = "2021"

ext/src/lib.rs:

pub struct Builder {
    on: bool,
}

impl Builder {
    pub fn new() -> Self {
        Builder { on: false }
    }

    pub fn enable(mut self) -> Self {
        self.on = true;
        self
    }

    pub fn build(self) -> bool {
        self.on
    }
}

pub struct GzBuilder {
    m: u32,
}

impl GzBuilder {
    pub fn new() -> Self {
        GzBuilder { m: 0 }
    }

    pub fn mtime(mut self, m: u32) -> Self {
        self.m = m;
        self
    }

    pub fn write(self, sink: Vec<u8>, level: u32) -> Encoder {
        Encoder { sink, level: level + self.m }
    }
}

pub struct Encoder {
    sink: Vec<u8>,
    level: u32,
}

impl Encoder {
    pub fn finish(self) -> Result<Sink, ()> {
        Ok(Sink { data: self.sink, level: self.level })
    }
}

pub struct Sink {
    data: Vec<u8>,
    level: u32,
}

impl Sink {
    pub fn flush(&mut self) -> Result<(), ()> {
        self.data.clear();
        Ok(())
    }
}

pub fn stdout() -> Sink {
    Sink { data: vec![], level: 0 }
}

app/Cargo.toml:

[package]
name = "repro_receiver"
version = "0.1.0"
edition = "2021"

[dependencies]
ext = { path = "../ext" }

app/src/main.rs:

use std::io::Write;

// 1. derive(Default) type called through its own path
#[derive(Default)]
pub struct Rules {
    n: u32,
}

pub struct Tokens {
    n: u32,
}

impl Default for Tokens {
    fn default() -> Self {
        Tokens { n: 1 }
    }
}

fn rules() -> Rules {
    Rules::default()
}

// 2. builder chain from an outside crate; the project has an associated fn `build` without self
pub struct Graph {
    n: u32,
}

impl Graph {
    pub fn build(n: u32) -> Graph {
        Graph { n }
    }
}

fn runtime() -> bool {
    ext::Builder::new().enable().build()
}

// the project implements io::Write for its own type
pub struct Log {
    buf: Vec<u8>,
}

impl Write for Log {
    fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
        self.buf.extend_from_slice(b);
        Ok(b.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

// control: std `OpenOptions::write` on one line and std `flush` — no false edge
fn open(p: &str) -> std::io::Result<std::fs::File> {
    std::fs::OpenOptions::new().write(true).create(true).open(p)
}

fn out() -> std::io::Result<()> {
    std::io::stdout().flush()
}

// 3. multi-line std builder chain inside a closure (`.write(true)` is OpenOptions::write)
fn take_lock(p: &str) -> std::io::Result<std::fs::File> {
    let open = move || -> std::io::Result<std::fs::File> {
        std::fs::OpenOptions::new()
            .create(true)
            .truncate(false)
            .write(true)
            .open(p)
    };
    open()
}

// 4. builder method `write` of an outside crate and `flush` on what its chain returns
fn write_single(data: Vec<u8>) -> Result<(), ()> {
    let enc = ext::GzBuilder::new().mtime(0).write(data, 6);
    enc.finish()?.flush()?;
    Ok(())
}

// 5. local of an outside type, then `flush`
fn to_stdout() -> Result<(), ()> {
    let mut stdout = ext::stdout();
    stdout.flush()
}

fn main() {
    let r = rules();
    let t = Tokens::default();
    let g = Graph::build(r.n + t.n);
    let mut l = Log { buf: vec![] };
    let _ = l.write(b"x");
    let _ = l.flush();
    let _ = (runtime(), open("/dev/null"), out(), g.n, take_lock("/dev/null").is_ok(), write_single(vec![]), to_stdout());
}

Actual

Call edges in app/.codegraph/codegraph.db on current main (1.6.2 gives the same edges):

main         -> Graph::build      resolvedBy qualified-name,  confidence 0.95, refName Graph::build      (ok)
main         -> Log::flush        resolvedBy instance-method, confidence 0.9,  refName l.flush           (ok)
main         -> Log::write        resolvedBy instance-method, confidence 0.9,  refName l.write           (ok)
main         -> Tokens::default   resolvedBy qualified-name,  confidence 0.95, refName Tokens::default   (ok)
rules        -> Tokens::default   resolvedBy instance-method, confidence 0.7,  refName Rules::default    (wrong)
runtime      -> Graph::build      resolvedBy exact-match,     confidence 0.9,  refName build             (wrong)
take_lock    -> Log::write        resolvedBy exact-match,     confidence 0.9,  refName write             (wrong)
to_stdout    -> Log::flush        resolvedBy instance-method, confidence 0.7,  refName stdout.flush      (wrong)
write_single -> Log::flush        resolvedBy exact-match,     confidence 0.9,  refName flush             (wrong)
write_single -> Log::write        resolvedBy exact-match,     confidence 0.9,  refName write             (wrong)

open (OpenOptions on one line) and out (std::io::stdout().flush()) get no false edge.

Expected

  • rules: no project edge. The derived default is not in the source, and the path names Rules, never Tokens.
  • runtime, take_lock, write_single, to_stdout: no project edge. The receivers are ext::Builder, OpenOptions, ext::GzBuilder and ext::Sink — all outside the project. Unresolved is fine.

Notes

Shapes 2–5 lose the receiver in a chain or in a local of an outside type and then fall back to the bare name (exact-match on build, write, flush). Graph::build has no self, so a method call x.build() can never reach it — maybe a small check before the name fallback would help. Shape 1 is close to what #3203 (open) does for a path that names a type.

Related, I checked before posting: #2614 (closed, other shapes), #3203 (open PR), #2697 and #2715 (closed).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions