Skip to content

fix(rust): bind Type::fn() calls only to the named type's methods - #2595

Merged
vitali87 merged 8 commits into
mainfrom
claude/rust-std-assoc-calls-2543
Oct 4, 2026
Merged

vitali87 merged 8 commits into
mainfrom
claude/rust-std-assoc-calls-2543

Conversation

@vitali87

@vitali87 vitali87 commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • A Rust Type::f() call whose Type is a std/prelude type (String::new(), Vec::new(), String::from(..)), a prelude trait (Default::default(), Clone::clone(&x)), a std path (std::string::String::new(), fmt::Debug::fmt(..) under use std::fmt) or a manifest-proven external crate (tokio::sync::Mutex::new(..)) now resolves only to a first-party impl block on that very type (impl From<Foo> for u8), and otherwise records no edge. On main the qualifier was dropped and the bare name went to the trie, which bound whichever project type's same-named method sat closest (Frame::new, Frame::from, ...). A new step, _resolve_rust_external_path, runs ahead of the name-based probes and makes that decision.
  • A Type::f() / Self::f() on a first-party type whose method the graph cannot see, because a derive generates it (Config::default(), clap's Cli::parse()), now binds only to that type's own methods. Those are an impl block in another module or a trait default the type implements. If none exists, the call gets no edge. The same restriction covers a method call on a literal ("42".parse(), which has type str). This is the _resolve_rust_named_owner step, the last one before the trie. A type alias, a generic parameter and an unplaced external type are unknown rather than known, so they keep the old fallback.
  • Rust two-part calls no longer fall to the same-module free-function guess, because Type::f() and recv.f() never name the caller module's own fn f. On main, String::new(), HashMap::new() (explicitly imported from std) and "42".parse() bound the file's own fn new / fn parse.

Side effect: Self::f() on the caller's own type now resolves exact (was heuristic).

The parser fingerprint changes (parsers/ and constants/ are touched), so an existing graph re-parses once on its next sync.

Demo

Rust std and derived Type::fn() calls, main vs this branch

Animated runs

Rust std and derived Type::fn() calls, main vs this branch, animated

The screenshot and GIF show a real session on the issue's repro crate (cargo check passes), indexed into a fresh Memgraph 3.3. On main, cgr graph callees …src.app.build lists five heuristic edges: String::new() and Vec::new() go to Frame.new, String::from("x") to Frame.from, the derived Config::default() to Frame.default and "42".parse() to Frame.parse. On this branch the same sync re-parses once and build() has no CALLS edges. In both runs the genuine Frame::parse("ok") in real() stays exact.

Type of Change

  • Bug fix
  • New feature
  • Performance improvement
  • Refactoring (no functional changes)
  • Documentation
  • CI/CD or tooling
  • Dependencies

Related Issues

Fixes #2543

Test Plan

  • Unit tests pass (make test-parallel or uv run pytest -n auto -m "not integration"): not run locally in full (shared machine); CI runs the full suite on every push.
  • New tests added: codebase_rag/tests/test_rust_std_assoc_calls.py
  • Integration tests pass (make test-integration, requires Docker)
  • Manual testing (the demo above: the issue's repro indexed into Memgraph with main vs this branch)

Red (fail on main's resolver, pass here):

  • test_prelude_and_derived_assoc_calls_do_not_bind_project_methods (the issue's repro verbatim)
  • test_derived_parser_parse_does_not_bind_the_frame_parser (mini-redis Cli::parse())
  • test_prelude_trait_qualified_calls_do_not_bind_project_methods
  • test_self_qualified_derived_call_does_not_bind_another_type
  • test_std_type_calls_do_not_bind_a_same_module_free_function
  • test_std_path_qualified_calls_do_not_bind_project_methods
  • test_external_crate_path_does_not_bind_project_methods
  • test_prelude_type_does_not_bind_an_out_of_scope_same_named_type

Negative (behaviour that must not change; pass on main and here):

  • test_first_party_assoc_calls_still_resolve_exact: Frame::new(), use … as F; F::parse() and Self::new().
  • test_first_party_type_shadowing_a_prelude_name_resolves_locally: a crate struct String, defined in the module or imported, stays exact.
  • test_impl_elsewhere_trait_default_and_type_alias_keep_their_edges: an impl block in another module, a trait default method and a type Alias = Frame all keep their edges.
  • test_std_type_method_from_a_first_party_impl_still_binds: u8::from(..) still reaches impl From<Foo> for u8.
  • test_literal_receiver_types / test_non_literal_receiver_has_no_literal_type: a raw identifier (r#type) is not a string literal.

Targeted scope run locally (all green):

  • The new file (27 tests).
  • Every codebase_rag/tests/test_rust*.py (613 passed, 3 skipped).
  • The call-resolution neighbours, 599 passed: test_call_resolver, test_call_processor, test_call_processor_integration, test_edge_resolution, test_edge_site_properties, test_value_receiver_class_resolution, test_typed_receiver_lacks_method, test_interface_dispatch_single_impl, test_implements_overrides, test_method_calls_caller_attribution, test_enum_variant_nodes, test_type_inference_iterative, test_polyglot_eval, test_dead_code_eval, test_graph_determinism, test_rs_macro_definition_nodes, test_dynamic_trace_rust, test_cross_file_inherits_implements, test_watch_created_file_calls, test_type_facts, test_same_line_duplicate_qns, test_reingest_new_file_importers, test_structural_relationships, test_generated_docs_are_current, test_parser_fingerprint*.

Also: ruff check, ruff format and ty check pass. The one remaining ruff check finding on main is PLW0108 in flow_verdict.py, which PR #2537 fixes.

Review follow-up (ec27207)

  • A prelude call bound a project method (Greptile 4171473236): fixed. When a call has no first-party owner, a name match on an owner whose impl lives elsewhere now also requires that impl to be on a foreign type. The impl module's own binding decides. If nothing there binds the name, the crate that defines a Rust type of that name wins.
  • Red test: test_prelude_call_does_not_bind_an_impl_elsewhere_of_the_crates_own_type, covering both the use and the path-qualified impl forms.
  • Negative tests: test_imported_own_type_still_reaches_its_impl_elsewhere and test_trait_impl_on_the_prelude_type_still_binds.
  • Results: all test_rust_*.py pass (631). ruff and ty are clean.

Review follow-up (f4f6691)

  • An external trait method lost its edge (Greptile 4171829665): fixed. Whether an impl block is on a foreign type is now decided by its self type as written, read in the impl's own module. A crate type that only shares the name no longer decides it. Impl blocks not parsed in the current run (unchanged files on an incremental run) still use the name-based rule.
  • Red test: test_trait_impl_on_a_std_path_reaches_it_beside_the_crates_own_type.
  • Negative tests: the earlier prelude, imported-own-type, trait-on-prelude and u8::from tests still pass.
  • Results: 232 Rust tests pass on the re-review subset. ruff and ty are clean.

Sonar follow-up (e853eb1)

  • Sonar S3776 (cognitive complexity) on _resolve_function_call: 16 → 14. The three caller-scoped stages (receiver shadow, Rust block scope, caller scope) now run in order through one helper, _resolve_caller_scoped_stages, ahead of the caller-independent resolution cache. Behaviour is unchanged.
  • Checks: ruff, ty, and 859 passed / 3 skipped on the call-resolution tests.

Checklist

  • PR title follows Conventional Commits format
  • All pre-commit checks pass (make pre-commit)
  • No hardcoded strings in non-config/non-constants files
  • No # type: ignore, cast(), Any, or object type hints
  • New comments and docstrings explain why, not what (see Comment Policy)

Review follow-up (cddfe4c)

  • Distinct String impls collided (Greptile 4172155393): fixed. One module can hold an impl of the crate's own String and a trait impl for std::string::String, and both were judged by the same shared record. Each impl method now also records the self type its own block was written for, and the foreign-type check reads that first. It falls back to the shared record only for trait implementers, and for files that an incremental run does not re-parse.
  • Red test: test_std_path_call_skips_the_crates_impl_in_a_module_with_both.
  • Negative test: test_std_path_call_still_reaches_the_std_impl_in_a_module_with_both. The earlier std-path, prelude and impl-elsewhere tests still pass.
  • Results: 234 passed and 2 skipped across 14 Rust test files. ruff and ty are clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QjpNsgSNyL5gsmAFqfuoD6


Generated by Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved Rust call resolution for associated functions and methods on standard-library, external, and first-party types, including trait methods and methods implemented in other modules.
    • Calls involving strings, characters, booleans, and numeric literals now resolve against applicable methods on their recognized types.
    • Prevented calls from incorrectly matching unrelated methods or same-named free functions. Resolution for aliases, in-scope types, and applicable trait implementations is preserved.

Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: vitali87/code-graph-rag/.coderabbit.yaml
  • Review profile: QUIET
  • Plan: Advanced
  • Run ID: 1260ea14-afe6-4696-918b-551a7e24bf56
📥 Commits

Reviewing files that changed from the base of the PR and between cddfe4c and e853eb1.

📒 Files selected for processing (5)
  • codebase_rag/constants/ast_rust.py
  • codebase_rag/logs.py
  • codebase_rag/parsers/call_resolver.py
  • codebase_rag/parsers/class_ingest/mixin.py
  • codebase_rag/parsers/import_processor.py

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Rust call resolution now uses associated-path qualifiers and recognized literal receiver types to match methods. It avoids falling through to unrelated project methods or same-named module functions. Regression tests cover these cases and preserve first-party resolution paths.

Changes

Rust associated-call resolution

Layer / File(s) Summary
Capture Rust owner context
codebase_rag/constants/ast_rust.py, codebase_rag/parsers/rs/utils.py, codebase_rag/parsers/class_ingest/mixin.py, codebase_rag/parsers/import_processor.py
Adds Rust prelude, primitive, and literal constants. Extracts written impl target paths and records them for impls and their methods, with module-owned state cleanup.
Resolve calls by owner
codebase_rag/parsers/call_resolver.py, codebase_rag/logs.py
Parses associated paths and classifies path heads. Checks external paths, registered owners, applicable trait methods, and literal receiver types before fallback. Rust two-part calls no longer fall through to same-named caller-module functions.
Validate associated-call resolution
codebase_rag/tests/test_rust_std_assoc_calls.py
Adds regression tests for unrelated standard, prelude, derived, and external-call candidates, and for preserved first-party method and trait resolutions.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~50 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CallResolver
  participant ImportProcessor
  participant OwnerRegistry
  Caller->>CallResolver: Submit Rust associated call
  CallResolver->>ImportProcessor: Read recorded impl self paths
  CallResolver->>OwnerRegistry: Find matching owner and trait methods
  OwnerRegistry-->>CallResolver: Return matching methods or no match
  CallResolver-->>Caller: Resolve matching method or leave call unresolved
Loading

Suggested reviewers: claude

Merge Risk: ⚪ Minimal · up to e853e

This change stops Rust calls on standard, prelude, derived, or external types from linking to unrelated project methods, while first-party resolution is preserved. No merge-blocking issue was found in the supplied material. The full unit suite was not run locally and CI covers it.

Security Architecture Review

Security architecture risk: 🔵 Low · up to e853e

Owner-aware lookup reduces incorrect connections to unrelated methods. No introduced execution or privilege-escalation path was established. Risk remains low rather than minimal because failure recovery and concurrent reuse are not fully established.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated attacker-controlled scope is Rust call attribution within an ingestion processor's graph registry. ProcessorFactory constructs ImportProcessor for a specific repository and project; inspected code does not establish new tenant, credential, environment, or service authority.

Trust Boundaries and Controls

  • observed — The inspected new stages constrain graph-target selection rather than execute repository-controlled methods or construct database queries. Registered method labels and owner matching are attribution controls, not authorization controls for downstream consumers.

Resilience and Maintainability Implications

  • observed — Set-based recording makes repeated identical metadata writes idempotent, while declaring-module key tracking supports scoped cleanup. These controls cover normal repetition, reparse, and deletion; they do not establish atomic failure recovery or safe simultaneous reuse of one processor.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The merge-base-to-head changes include substantial unrelated work. The incremental diff adds CLI export and report-table changes for issues #2440 and #2561, MCP server error handling for #2518, Java m… Remove the unrelated changes from this PR or separate them into their own PRs. Keep this PR limited to the Rust call-resolution fix and its supporting tests.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Issue #2543 requires Rust calls on known external, prelude, derived, or literal types to avoid unrelated first-party methods. The resolver handles external paths and known owners before trie fallback,…
Title check ✅ Passed The title follows Conventional Commits format and clearly identifies the Rust call-resolution fix.
Description check ✅ Passed The description covers the summary, demo, change type, related issue, test plan, and checklist. It also states which tests were not run and reports the targeted test results.
Full details: Out of Scope Changes check

Explanation

The merge-base-to-head changes include substantial unrelated work. The incremental diff adds CLI export and report-table changes for issues #2440 and #2561, MCP server error handling for #2518, Java method-reference support for #2546, and graph-updater cleanup for #2463. These changes do not support Rust associated-call resolution in issue #2543.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

vitali87 commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

@greptileai please review the latest commit (cc5a1ec).


Generated by Claude Code

@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium risk] Fixes Rust method resolution for standard library type calls.

No outstanding findings block merging.

Summary

The latest commit moves three caller-scoped resolution stages into a helper while preserving their order and early-return behavior. No new findings.

Reviews (6) · Last reviewed commit: "refactor(calls): run the caller-scoped r..."

Comment thread codebase_rag/parsers/call_resolver.py Resolved

vitali87 commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

@greptileai please review the latest commit (ec27207).


Generated by Claude Code

Comment thread codebase_rag/parsers/call_resolver.py Resolved

vitali87 commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

@greptileai please review the latest commit (f4f6691).


Generated by Claude Code

Comment thread codebase_rag/parsers/call_resolver.py Resolved
…ne module holds impls of two same-named types

vitali87 commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

@greptileai please review the latest commit (cddfe4c).


Generated by Claude Code

vitali87 commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

@greptileai please review the latest commit (1224a84).


Generated by Claude Code

…helper to keep _resolve_function_call under the complexity limit (#2543)

vitali87 commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Sonar raised one issue at 1224a84: python:S3776, call_resolver.py:1371, where _resolve_function_call had cognitive complexity 16 (the limit is 15).

Fixed in e853eb1. The three stages that run before the caller-independent cache (_resolve_receiver_shadow, _resolve_rust_block_scope and _resolve_caller_scope) now run in the same order through one helper, _resolve_caller_scoped_stages. Behaviour is unchanged. Complexity is now 14 for _resolve_function_call and 3 for the helper.

Results: this PR's tests plus the call-resolver, call-processor, Rust and Python-local tests gave 859 passed, 3 skipped. ruff and ty are clean.

@greptileai please review the latest commit (e853eb1).


Generated by Claude Code

@greptile-apps

greptile-apps Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

This PR is set for a Plus review. TREX isn't supported with Plus or Apex reviews yet, so Greptile reviews this PR at Base while TREX is on.

@sonarqubecloud

sonarqubecloud Bot commented Oct 3, 2026

Copy link
Copy Markdown

@vitali87
vitali87 merged commit 03ce7a1 into main Oct 4, 2026
34 checks passed
@vitali87
vitali87 deleted the claude/rust-std-assoc-calls-2543 branch October 4, 2026 16:24
vitali87 added a commit that referenced this pull request Oct 5, 2026
Main's #2595 decides a Type::fn() whose path head binds an external crate
before any probe, reading the manifest's pathless entries as external.
A pathless entry renamed onto a workspace member (`base-alias = { version
= "0.1", package = "my-base" }`) fetches the crate the repo holds, so
`use base_alias::Meter; Meter::new()` lost its edge after the merge. The
check now asks rust_head_is_member_dep first, as this branch's other
external-head check does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EbsCWQcSTGQXv3g5sk418m
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

1 participant