Python fp and docs updtes (#58)

* refactor: Update comments for clarity and add expectations.json files for performance metrics

* feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks

* feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks

* refactor: Simplify code formatting for better readability in multiple files

* refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration

* feat: Update Java and Python patterns to include new security rules

* refactor: Improve comment clarity and consistency across multiple Rust files

* refactor: Simplify code formatting for improved readability in integration tests and module files

* refactor: Improve comment formatting and enhance clarity in assertions across multiple files
This commit is contained in:
Eli Peter 2026-04-29 19:53:34 -04:00 committed by GitHub
parent 4db0805de6
commit a438886217
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
291 changed files with 9485 additions and 3851 deletions

View file

@ -16,7 +16,7 @@ use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct CallEdge {
/// The raw callee string as it appeared in source (e.g. `"env::var"`).
/// Preserved for diagnostics **not** the normalized form used for resolution.
/// Preserved for diagnostics, **not** the normalized form used for resolution.
#[allow(dead_code)] // used for future diagnostics and path display
pub call_site: String,
}
@ -28,7 +28,7 @@ pub struct UnresolvedCallee {
pub callee_name: String,
}
/// A callee that matched multiple function definitions ambiguous.
/// A callee that matched multiple function definitions, ambiguous.
#[derive(Debug, Clone)]
pub struct AmbiguousCallee {
pub caller: FuncKey,
@ -168,14 +168,14 @@ pub(crate) fn callee_container_hint(raw: &str) -> &str {
///
/// Key design notes:
///
/// * Keys are **language-scoped** a Java `findById` and a Python
/// * Keys are **language-scoped**, a Java `findById` and a Python
/// `findById` never alias. Every other index in this module is also
/// language-scoped (`by_lang_name`, `by_lang_qualified`); keeping the
/// same partition here means devirtualisation's "subset of today's
/// targets" invariant is structurally preserved.
/// * The container key carries the [`FuncKey::container`] verbatim
/// (e.g. `"Repository"` or nested `"Outer::Inner"`). Empty containers
/// are not indexed in `by_container` free top-level functions live
/// are not indexed in `by_container`, free top-level functions live
/// only in `by_name` and are looked up via the `None` container path.
/// * `SmallVec` inline capacity is sized for the common case (≤ 2 same-
/// container overloads, ≤ 4 same-name candidates across containers);
@ -199,7 +199,7 @@ impl ClassMethodIndex {
/// Iteration is over every `FuncKey` in the map; each key is
/// inserted into `by_name` and (when its container is non-empty)
/// into `by_container`. No ordering guarantees on the candidate
/// vectors call sites that need determinism should sort downstream.
/// vectors, call sites that need determinism should sort downstream.
pub fn build(summaries: &GlobalSummaries) -> Self {
let mut by_container: HashMap<(Lang, String, String), SmallVec<[FuncKey; 2]>> =
HashMap::new();
@ -223,11 +223,11 @@ impl ClassMethodIndex {
/// Resolve `(container, method)` to its candidate target set.
///
/// * `container = Some(c)` return only candidates whose defining
/// * `container = Some(c)`, return only candidates whose defining
/// container equals `c`. Empty slice when no such target exists,
/// even if a same-name function lives in another container.
/// This is the **devirtualised** path: a hard subset of `by_name`.
/// * `container = None` return every same-name candidate in the
/// * `container = None`, return every same-name candidate in the
/// language. This is the **fallback** path used when the receiver
/// type is unknown; matches today's name-only behaviour.
///
@ -264,48 +264,19 @@ impl ClassMethodIndex {
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Type hierarchy index — Phase 6 (subtype awareness)
// ─────────────────────────────────────────────────────────────────────────────
// ── Type hierarchy index ────────────────────────────────────────────────
/// Per-language `(super_type) → SmallVec<[sub_type]>` index built once
/// per call-graph construction from every merged
/// [`crate::summary::FuncSummary::hierarchy_edges`]. When a method
/// call's receiver is statically typed as a super-class / trait /
/// interface, the call-graph wedge fans out the edge to every concrete
/// implementer's matching method — recovering the dispatch precision
/// that would otherwise be lost to today's name-only resolution.
/// Per-language `(super_type) → sub-types` index built from every merged
/// [`crate::summary::FuncSummary::hierarchy_edges`]. Lets virtual
/// dispatch fan out to every concrete implementer's matching method.
///
/// Subtype semantics covered:
/// * Java `class X extends Y` / `class X implements I` / `interface
/// I extends J`
/// * Rust `impl Trait for Type`
/// * TypeScript `class X extends Y implements I` /
/// `interface I extends J`
/// * Python `class X(Base)` (excludes `object`)
/// * PHP, Ruby, C++ — see [`crate::cfg::hierarchy`] for the
/// per-language extraction rules.
/// Covers Java `extends`/`implements`, Rust `impl Trait for Type`, TS
/// `extends`/`implements`, Python `class X(Base)`, plus PHP/Ruby/C++
/// (see [`crate::cfg::hierarchy`]). Go's structural interfaces are
/// intentionally omitted, name-only resolution is used instead.
///
/// Go's structural / implicit interface satisfaction is intractable to
/// enumerate from per-file information and is **deliberately omitted**
/// — Go callers fall back to today's name-only resolution, so
/// precision is unchanged from the pre-Phase-6 baseline.
///
/// Key design notes
/// ────────────────
///
/// * **Language-scoped.** Mirrors [`ClassMethodIndex`]: a Java
/// `Repository` and a Python `Repository` never alias.
/// * **Bare container names.** No namespace qualification. When
/// container names alias across unrelated namespaces (rare in
/// practice, common in mono-repos) the resolver may over-fan-out;
/// that is conservative for *correctness* (a subset of dispatch
/// targets is unsafe — virtual dispatch may genuinely reach any
/// implementer) and may need namespace-qualified keying as a
/// Phase 6.5 follow-up if benchmark precision regresses.
/// * **`SmallVec` inline capacity.** 4 implementers per super-type
/// covers most real-world hierarchies without spillover; spillover
/// allocates but keeps lookups O(1) amortised.
/// Container names are bare (no namespace), so cross-namespace aliases
/// may over-fan-out. That is conservative for correctness.
#[derive(Debug, Default, Clone)]
pub struct TypeHierarchyIndex {
/// `(lang, super_type)` → distinct sub-type / impl container names.
@ -438,15 +409,11 @@ impl TypeHierarchyIndex {
/// 3. On ambiguity: use two-segment qualified name to narrow candidates
/// 4. Interop edges (explicit cross-language bridges)
///
/// **Phase 3 (typed call-graph devirtualisation):** when an SSA
/// summary on the caller carries a `(call_ordinal, container_name)`
/// entry in [`crate::summary::ssa_summary::SsaFuncSummary::typed_call_receivers`],
/// the matching call site is first resolved via [`ClassMethodIndex`]
/// restricted to the receiver-typed container. An exact match (after
/// arity filter) becomes the edge; a multi-candidate hit is fed back
/// into the standard resolver via `CalleeQuery.receiver_type`; a
/// zero-candidate hit falls through to today's name-only resolution
/// so receiver-type misclassifications never silently drop edges.
/// Typed-call devirtualisation: when the caller's SSA summary carries
/// a typed container for a call ordinal, that site is first resolved
/// via [`ClassMethodIndex`] restricted to the receiver type. Exact
/// match → edge; multi-candidate → fed back through
/// `CalleeQuery.receiver_type`; zero match → name-only fallback.
///
/// Unresolved and ambiguous callees are recorded for diagnostics but
/// do **not** create edges.
@ -460,7 +427,7 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
index.insert(key.clone(), idx);
}
// Phase 3: build a single `(lang, container, name) → candidates`
// build a single `(lang, container, name) → candidates`
// index from the merged summaries. Used below to devirtualise
// every method-call edge whose receiver has a recoverable type
// fact. Cost is one allocation per FuncKey across the program;
@ -468,7 +435,7 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
// win on codebases with many same-name methods.
let method_index = ClassMethodIndex::build(summaries);
// Phase 6: build a sibling `(lang, super_type) → sub_types` index
// build a sibling `(lang, super_type) → sub_types` index
// from every merged summary's `hierarchy_edges`. Consumed below
// to fan out method-call edges to all known concrete
// implementers when a receiver's static type is a super-class /
@ -497,7 +464,7 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
None
};
// Phase 3: per-caller `(call_ordinal → container_name)` map
// per-caller `(call_ordinal → container_name)` map
// pulled from the caller's SSA summary, when one exists.
// Empty when the caller has no SSA summary (zero-param trivial
// bodies skip extraction unless they had typed receivers) or
@ -520,23 +487,15 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
let leaf = callee_leaf_name(raw_callee);
// Two-segment form for diagnostics / fallback disambiguation.
let qualified = normalize_callee_name(raw_callee);
// Structured arity carried per call site used to disambiguate
// Structured arity carried per call site, used to disambiguate
// same-name/different-arity overloads during resolution.
let arity_hint: Option<usize> = site.arity;
// Phase 3 devirtualisation entry point. Only fires for
// method calls (sites carrying a structured receiver) when
// the caller's SSA summary recorded a typed container for
// this ordinal. When `Some(container)` resolves to a
// single arity-matching target, we add the edge and skip
// the standard resolver. When it resolves to multiple,
// we fall through with the container hinted as
// `receiver_type` so `resolve_callee`'s authoritative
// step-1 picks the right one. When it resolves to zero,
// we fall through entirely so today's name-only path can
// still find the edge — preserving the
// "subset of today's targets, never a superset" rule
// even under type-fact misclassification.
// Devirtualisation: for method calls whose SSA summary
// recorded a typed container, resolve via ClassMethodIndex
// first. Single match → direct edge; multi → fall through
// with `receiver_type` set; zero → name-only fallback so
// misclassified receivers never silently drop edges.
let typed_container: Option<&str> = if site.receiver.is_some() {
typed_receivers.get(&site.ordinal).copied()
} else {
@ -544,12 +503,10 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
};
if let Some(container) = typed_container {
// Phase 6: resolve the typed container *plus* every
// known sub-type / impl in the hierarchy index, so a
// receiver typed as a super-class / trait / interface
// fans out to every concrete implementer. When the
// hierarchy has no matching super-type entry, this
// collapses to the Phase 3 direct-container lookup.
// Resolve the typed container plus every known
// sub-type / impl, so a super-class / trait / interface
// receiver fans out to every concrete implementer.
// No hierarchy entry → direct-container lookup.
let widened: Vec<FuncKey> = hierarchy.resolve_with_hierarchy(
&method_index,
caller_key.lang,
@ -575,8 +532,8 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
}
continue;
}
// Phase 6: multiple arity-filtered candidates means
// genuine virtual dispatch through a super-type fan
// multiple arity-filtered candidates means
// genuine virtual dispatch through a super-type, fan
// out to *every* implementer. This widens edges
// (correctly: the call genuinely may target any
// implementer at runtime) so SCC sizes may grow on
@ -614,7 +571,7 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
continue;
}
// Either zero matches (fall through to legacy path) or
// multiple matches on the direct container let
// multiple matches on the direct container, let
// `resolve_callee` apply its authoritative
// receiver_type filter + tie-breakers.
if !arity_filtered.is_empty() {
@ -652,8 +609,8 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
// Rust callers with a module-qualified call (no receiver) go
// through the `use`-map aware resolver first. When the call has
// a structured receiver it is a method call the qualifier is
// an impl/trait name, not a module path so we fall back to the
// a structured receiver it is a method call, the qualifier is
// an impl/trait name, not a module path, so we fall back to the
// structured resolver. All other languages skip the use-map
// branch entirely.
let use_rust_path = caller_key.lang == Lang::Rust && site.receiver.is_none();
@ -671,11 +628,11 @@ pub fn build_call_graph(summaries: &GlobalSummaries, interop_edges: &[InteropEdg
// categorize each hint so the resolver can apply the right
// policy:
//
// * `namespace_qualifier` structured module/namespace
// * `namespace_qualifier`, structured module/namespace
// prefix (`env` in `env::var`, `http` in `http.Get`).
// * `receiver_var` syntactic receiver variable (e.g.
// * `receiver_var`, syntactic receiver variable (e.g.
// `obj` in `obj.method`); used only as a last tie-break.
// * `caller_container` caller's own class/impl, so bare
// * `caller_container`, caller's own class/impl, so bare
// `foo()` inside a method resolves to the same class.
//
// The raw text-parsed container (legacy
@ -815,7 +772,7 @@ fn resolve_via_interop(
/// Compute SCC decomposition and topological ordering of the call graph.
///
/// `petgraph::algo::tarjan_scc` returns SCCs in *reverse* topological order
/// of the condensation DAG i.e. leaf SCCs (no outgoing cross-SCC edges)
/// of the condensation DAG, i.e. leaf SCCs (no outgoing cross-SCC edges)
/// come **first**. That is exactly the **callee-first** order suitable for
/// bottom-up taint propagation.
pub fn analyse(cg: &CallGraph) -> CallGraphAnalysis {
@ -850,7 +807,7 @@ pub fn analyse(cg: &CallGraph) -> CallGraphAnalysis {
/// [`crate::commands::scan::run_topo_batches`]. `cross_file` is a tighter
/// signal used by joint fixed-point convergence: it implies the
/// recursion involves at least one cross-file call edge, so the inline
/// cache and per-iteration findings need joint convergence not just
/// cache and per-iteration findings need joint convergence, not just
/// summary convergence.
pub struct FileBatch<'a> {
pub files: Vec<&'a PathBuf>,
@ -901,7 +858,7 @@ pub fn callers_of(cg: &CallGraph, callee: &FuncKey) -> Vec<FuncKey> {
/// result is a `HashSet<String>` suitable for membership checks while
/// filtering the batch's file list.
///
/// A changed callee's *own* namespace is also included if the
/// A changed callee's *own* namespace is also included, if the
/// callee's summary was refined, the file it lives in may itself
/// have been a caller (intra-file recursion) or may carry sibling
/// functions whose analysis should be re-run alongside the callee
@ -958,7 +915,7 @@ pub fn scc_file_batches_with_metadata<'a>(
// 2. Build file relative-path → (min topo index, has_mutual_recursion, cross_file).
// `cross_file` is set whenever the file participates in an SCC whose
// nodes span more than one namespace the cross-file signal.
// nodes span more than one namespace, the cross-file signal.
let mut file_topo: HashMap<&str, (usize, bool, bool)> = HashMap::new();
for (topo_pos, &scc_idx) in analysis.topo_scc_callee_first.iter().enumerate() {
let scc_recursive = analysis.sccs[scc_idx].len() > 1;
@ -1015,7 +972,7 @@ pub fn scc_file_batches_with_metadata<'a>(
/// of its functions appear. This ensures leaf callees are available as early
/// as possible for files that depend on them. Caller functions in the same
/// file that happen to be in a later SCC are no worse off than the current
/// fully-parallel approach they simply don't yet benefit from ordering,
/// fully-parallel approach, they simply don't yet benefit from ordering,
/// but nothing is lost.
///
/// Returns `(ordered_batches, orphan_files)` where orphan_files are paths
@ -1188,7 +1145,7 @@ mod tests {
fn same_name_python_and_rust() {
let py_foo = make_summary("foo", "handler.py", "python", 0, vec![]);
let rs_foo = make_summary("foo", "handler.rs", "rust", 0, vec![]);
// Python caller calls "foo" should only see the Python one
// Python caller calls "foo", should only see the Python one
let py_caller = make_summary("main", "app.py", "python", 0, vec!["foo"]);
let gs = merge_summaries(vec![py_foo, rs_foo, py_caller], None);
@ -1315,7 +1272,7 @@ mod tests {
let gs = merge_summaries(vec![helper_a, helper_b, caller], None);
let cg = build_call_graph(&gs, &[]);
assert_eq!(cg.graph.edge_count(), 0); // no edge ambiguous
assert_eq!(cg.graph.edge_count(), 0); // no edge, ambiguous
assert!(cg.unresolved_not_found.is_empty());
assert_eq!(cg.unresolved_ambiguous.len(), 1);
assert_eq!(cg.unresolved_ambiguous[0].callee_name, "helper");
@ -1728,7 +1685,7 @@ mod tests {
// Two "send" functions in different namespaces.
let send_http = make_summary("send", "src/http.rs", "rust", 0, vec![]);
let send_mail = make_summary("send", "src/mail.rs", "rust", 0, vec![]);
// Caller is in a third namespace, calling "http::send" leaf "send"
// Caller is in a third namespace, calling "http::send", leaf "send"
// is ambiguous, but "http" qualifier should match "src/http.rs".
let caller = make_summary("caller", "src/main.rs", "rust", 0, vec!["http::send"]);
@ -1766,7 +1723,7 @@ mod tests {
#[test]
fn unqualified_callee_stays_ambiguous() {
// Same setup but caller uses unqualified "send" no disambiguation
// Same setup but caller uses unqualified "send", no disambiguation
let send_http = make_summary("send", "src/http.rs", "rust", 0, vec![]);
let send_mail = make_summary("send", "src/mail.rs", "rust", 0, vec![]);
let caller = make_summary("caller", "src/main.rs", "rust", 0, vec!["send"]);
@ -1806,7 +1763,7 @@ mod tests {
// ── structured-metadata disambiguation (callee metadata) ─────────────
/// Helper: build a summary whose callees carry structured CalleeSite
/// metadata used by the tests below to exercise arity / receiver /
/// metadata, used by the tests below to exercise arity / receiver /
/// qualifier propagation into resolution.
fn summary_with_sites(
name: &str,
@ -1840,7 +1797,7 @@ mod tests {
// Two `encode` functions in the same file, different arities.
let encode1 = make_summary("encode", "src/codec.rs", "rust", 1, vec![]);
let encode2 = make_summary("encode", "src/codec.rs", "rust", 2, vec![]);
// Caller lives in *another* file so namespace does not disambiguate
// Caller lives in *another* file so namespace does not disambiguate ,
// the only signal is the per-call-site arity.
let caller = summary_with_sites(
"driver",
@ -2007,7 +1964,7 @@ mod tests {
#[test]
fn legacy_string_callees_still_resolve() {
let helper = make_summary("helper", "src/lib.rs", "rust", 0, vec![]);
// make_summary already returns CalleeSite::bare entries i.e. the
// make_summary already returns CalleeSite::bare entries, i.e. the
// "lifted legacy" form with no arity or receiver metadata.
let caller = make_summary("main", "src/lib.rs", "rust", 0, vec!["helper"]);
let gs = merge_summaries(vec![helper, caller], None);
@ -2017,7 +1974,7 @@ mod tests {
assert!(cg.unresolved_ambiguous.is_empty());
}
// ── ClassMethodIndex (Phase 1: structural index, no behaviour wiring) ──
// ── ClassMethodIndex ────────────────────────────────────────────────
/// Helper: `(name, container)` pairs in the same file. Builds two
/// summaries with the same leaf name on different containers so the
@ -2058,7 +2015,7 @@ mod tests {
assert_eq!(cache_hits.len(), 1);
assert_eq!(cache_hits[0].container, "Cache");
// Bare-name lookup keeps both candidates fallback behaviour.
// Bare-name lookup keeps both candidates, fallback behaviour.
let bare_hits = idx.resolve(Lang::Rust, None, "findById");
assert_eq!(
bare_hits.len(),
@ -2070,7 +2027,7 @@ mod tests {
#[test]
fn class_method_index_falls_back_to_name_when_container_unknown() {
// `None` container or empty-string container both route to
// the bare-name index equivalent to today's name-only edge
// the bare-name index, equivalent to today's name-only edge
// insertion.
let svc = make_method_summary("process", "OrderService", "src/svc.rs", "rust", 1);
let helper = make_summary("process", "src/util.rs", "rust", 1, vec![]);
@ -2082,7 +2039,7 @@ mod tests {
let none_hits = idx.resolve(Lang::Rust, None, "process");
assert_eq!(none_hits.len(), 2);
// Empty string container behaves identically to None it is
// Empty string container behaves identically to None, it is
// not stored under any container key.
let empty_hits = idx.resolve(Lang::Rust, Some(""), "process");
assert_eq!(empty_hits.len(), 2);
@ -2107,7 +2064,7 @@ mod tests {
.is_empty()
);
// Right method, wrong container → empty (no fallback to bare-name
// when a container is supplied that's the whole devirtualisation
// when a container is supplied, that's the whole devirtualisation
// promise).
assert!(
idx.resolve(Lang::Rust, Some("OtherClass"), "findById")
@ -2140,7 +2097,7 @@ mod tests {
#[test]
fn class_method_index_handles_arity_overloads() {
// Two arity overloads on the same container are both kept under
// the same `(container, name)` key arity narrowing is the
// the same `(container, name)` key, arity narrowing is the
// caller's responsibility (today's resolver also does this).
let one = make_method_summary("encode", "Codec", "src/codec.rs", "rust", 1);
let two = make_method_summary("encode", "Codec", "src/codec.rs", "rust", 2);
@ -2156,7 +2113,7 @@ mod tests {
);
}
// ── Phase 3: devirtualised edge insertion via typed_call_receivers ──
// ── devirtualised edge insertion via typed_call_receivers ──
/// Two `findById` definitions live on different containers in
/// different files. A caller whose SSA summary records the
@ -2241,7 +2198,7 @@ mod tests {
use crate::summary::ssa_summary::SsaFuncSummary;
// Single `process` on `Worker`. No `process` exists on
// `Other` that's the receiver type the caller's SSA
// `Other`, that's the receiver type the caller's SSA
// summary will (incorrectly) record.
let worker = make_method_summary("process", "Worker", "src/worker.rs", "rust", 1);
let caller = summary_with_sites(
@ -2270,7 +2227,7 @@ mod tests {
gs.insert_ssa(
caller_key.clone(),
SsaFuncSummary {
// Wrong receiver type `Other::process` does not exist.
// Wrong receiver type, `Other::process` does not exist.
typed_call_receivers: vec![(0, "Other".to_string())],
..Default::default()
},
@ -2292,7 +2249,7 @@ mod tests {
);
}
// ── Phase 6: TypeHierarchyIndex ───────────────────────────────────
// ── TypeHierarchyIndex ───────────────────────────────────
/// Helper: build a hierarchy index from a list of
/// `(lang, sub, super)` edges by injecting them onto a single
@ -2334,7 +2291,7 @@ mod tests {
TypeHierarchyIndex::build(&gs)
}
/// B-1: Round-trip a hierarchy built from a small set of edges
/// B-1: Round-trip, a hierarchy built from a small set of edges
/// answers `subs_of` correctly and `super_keys_len` matches the
/// distinct super count.
#[test]
@ -2356,7 +2313,7 @@ mod tests {
assert_eq!(h.super_keys_len(), 2);
}
/// B-2: Java interface dispatch `Repository r; r.findById(...)`
/// B-2: Java interface dispatch, `Repository r; r.findById(...)`
/// fans out to every concrete implementer's `findById`.
#[test]
fn b2_java_interface_dispatch_fans_out_to_all_impls() {
@ -2421,7 +2378,7 @@ mod tests {
assert_eq!(targets.len(), 2, "B-2: exactly two fan-out edges expected");
}
/// B-3: Java extends `Base b; b.foo()` reaches Base AND Derived
/// B-3: Java extends, `Base b; b.foo()` reaches Base AND Derived
/// when Derived extends Base. Pins inheritance fan-out separately
/// from interface implements.
#[test]
@ -2479,7 +2436,7 @@ mod tests {
);
}
/// B-4: Rust trait dispatch `Box<dyn Repo>; r.find(...)` reaches
/// B-4: Rust trait dispatch, `Box<dyn Repo>; r.find(...)` reaches
/// every `impl Repo for X` `find`.
#[test]
fn b4_rust_trait_dispatch_fans_out_to_impls() {
@ -2536,10 +2493,9 @@ mod tests {
);
}
/// B-7: Empty hierarchy when the typed container has no recorded
/// B-7: Empty hierarchy, when the typed container has no recorded
/// sub-types, `resolve_with_hierarchy` collapses to the direct
/// `ClassMethodIndex::resolve` lookup. Pin: Phase 6 is a no-op
/// when no inheritance was extracted.
/// `ClassMethodIndex::resolve` lookup.
#[test]
fn b7_empty_hierarchy_falls_back_to_single_container() {
use crate::summary::ssa_summary::SsaFuncSummary;
@ -2561,7 +2517,7 @@ mod tests {
);
let mut gs = merge_summaries(vec![repo, cache, caller], None);
// No hierarchy_edges set anywhere Repository has no
// No hierarchy_edges set anywhere, Repository has no
// sub-types, so devirtualisation collapses to direct match.
let caller_key = FuncKey {
lang: Lang::Rust,
@ -2589,10 +2545,9 @@ mod tests {
assert_eq!(targets[0].container, "Repository");
}
/// B-8: Concrete sub-type when the receiver is typed as the
/// B-8: Concrete sub-type, when the receiver is typed as the
/// concrete sub-class (not the super-type), no hierarchy
/// expansion fires. Pin: Phase 6 narrows on concrete types
/// exactly like Phase 3.
/// expansion fires.
#[test]
fn b8_concrete_subtype_does_not_widen() {
use crate::summary::ssa_summary::SsaFuncSummary;
@ -2654,7 +2609,7 @@ mod tests {
assert_eq!(targets[0].container, "UserRepo");
}
/// B-9: Diamond multiple impls sharing a super-type, dedup
/// B-9: Diamond, multiple impls sharing a super-type, dedup
/// applied per call site so each FuncKey is edged at most once.
#[test]
fn b9_diamond_dedup_one_edge_per_funckey() {
@ -2662,7 +2617,7 @@ mod tests {
let a = make_method_summary("doIt", "A", "src/A.java", "java", 0);
let b = make_method_summary("doIt", "B", "src/B.java", "java", 0);
// A and B both extend Iface in two separate file emissions
// A and B both extend Iface in two separate file emissions ,
// hierarchy_edges duplicates across files; dedup expected.
let mut h1 = make_method_summary("__h", "Iface", "src/I1.java", "java", 0);
h1.hierarchy_edges = vec![
@ -2722,7 +2677,7 @@ mod tests {
assert!(containers.contains("A") && containers.contains("B"));
}
/// B-13: Stale hierarchy edge sub-type referenced by an edge
/// B-13: Stale hierarchy edge, sub-type referenced by an edge
/// no longer has a matching FuncKey. Resolver must not panic
/// and must still resolve to whatever IS present.
#[test]
@ -2730,7 +2685,7 @@ mod tests {
use crate::summary::ssa_summary::SsaFuncSummary;
// `Base` exists; `Derived` referenced by hierarchy_edges but
// its `foo` is never defined. Phase 6 must not panic and
// its `foo` is never defined. Resolver must not panic and
// must still emit the Base::foo edge.
let base = make_method_summary("foo", "Base", "src/Base.java", "java", 0);
let mut h = make_method_summary("__h", "X", "src/X.java", "java", 0);
@ -2815,7 +2770,7 @@ mod tests {
arity: Some(0),
..Default::default()
};
// A typed_call_receivers entry with ordinal=0 but since the
// A typed_call_receivers entry with ordinal=0, but since the
// site has receiver=None, this MUST be ignored.
gs.insert_ssa(
caller_key.clone(),