Skip to main content

diffkit/language/
rust.rs

1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
2use std::env;
3use std::fs;
4use std::io::Read;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::Mutex;
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use proc_macro2::Span;
11use quote::ToTokens;
12use rustc_public::CompilerError;
13use rustc_public::crate_def::CrateDef;
14use rustc_public::mir::mono::{Instance, InstanceKind};
15use rustc_public::mir::{
16    AggregateKind, Body, CastKind, Operand, Place, PointerCoercion, ProjectionElem, Rvalue,
17    StatementKind, TerminatorKind,
18};
19use rustc_public::ty::{
20    AssocContainer, ExistentialTraitRef, RigidTy, TraitRef, Ty, TyKind, VtblEntry,
21};
22use serde::{Deserialize, Serialize};
23use sha2::{Digest, Sha256};
24use syn::spanned::Spanned;
25use syn::visit::{self, Visit};
26use syn::visit_mut::{self, VisitMut};
27use syn::{
28    Expr, ExprCall, ExprClosure, ExprMethodCall, FnArg, ImplItem, Item, ItemFn, ItemImpl,
29    ItemTrait, Pat, ReturnType, Signature, TraitItem, Visibility,
30};
31
32use super::{FileContext, FrontendResult, LanguageBackend, ProjectContext};
33use crate::model::{
34    CallLabel, CallSite, CallSiteId, CallSyntax, CallTarget, DispatchCandidate, DispatchEvidence,
35    DispatchResolution, FileAnalysis, FunctionInfo, LanguageFact, LanguageId, SourceSpan, SymbolId,
36    UnresolvedReason,
37};
38
39#[derive(Default)]
40pub struct RustBackend;
41
42pub static RUST_BACKEND: RustBackend = RustBackend;
43
44static RUSTC_DRIVER_LOCK: Mutex<()> = Mutex::new(());
45static TEMP_SOURCE_SEQUENCE: AtomicU64 = AtomicU64::new(0);
46const RUSTC_CAPTURE_DIRECTORY: &str = "DIFFKIT_RUSTC_CAPTURE_DIRECTORY";
47const RUSTC_ANALYSIS_ROOT: &str = "DIFFKIT_RUSTC_ANALYSIS_ROOT";
48const SNAPSHOT_FINGERPRINT_FILE: &str = ".diffkit-snapshot-key";
49const RUST_SEMANTIC_CACHE_SCHEMA: u32 = 2;
50
51/// Analyze a standalone, compilable Rust source file with rustc's typed MIR.
52///
53/// Analyze Rust by combining source-shaped labels and spans with rustc's typed
54/// MIR. The source extractor is an internal stage, not a separate analysis
55/// mode.
56pub fn analyze_semantic_file(path: &Path) -> FrontendResult<FileAnalysis> {
57    analyze_semantic_file_with_entries(path, &[])
58}
59
60pub fn analyze_semantic_file_with_entries(
61    path: &Path,
62    entries: &[String],
63) -> FrontendResult<FileAnalysis> {
64    if !path.is_file() {
65        return Err(std::io::Error::other(format!(
66            "Rust analysis currently requires a standalone .rs file: {}",
67            path.display()
68        ))
69        .into());
70    }
71
72    let source = fs::read_to_string(path)?;
73    let syntax = RustBackend.analyze_file(&FileContext { path, module: &[] }, &source)?;
74    let semantic = collect_rustc_program(path)?;
75    let analysis = merge_semantic_program(syntax.clone(), semantic);
76    let missing_entries = entries
77        .iter()
78        .filter(|entry| entry.contains('<') && !analysis_has_entry(&analysis, entry))
79        .cloned()
80        .collect::<Vec<_>>();
81    if missing_entries.is_empty() {
82        return Ok(analysis);
83    }
84
85    let Some(seeded_source) = append_entry_seeds(&source, &missing_entries)? else {
86        return Ok(analysis);
87    };
88    let temporary = TemporaryRustSource::create(&seeded_source)?;
89    let temporary_path = temporary.path();
90    let mut semantic = collect_rustc_program(&temporary_path)?;
91    remap_semantic_file(&mut semantic, &temporary_path, path);
92    Ok(merge_semantic_program(syntax, semantic))
93}
94
95fn remap_semantic_file(program: &mut SemanticProgram, from: &Path, to: &Path) {
96    for function in &mut program.functions {
97        if source_files_match(&function.body_span.file, from) {
98            function.body_span.file = to.to_path_buf();
99        }
100        for call in &mut function.calls {
101            if source_files_match(&call.span.file, from) {
102                call.span.file = to.to_path_buf();
103            }
104        }
105    }
106}
107
108pub fn analyze_semantic_source(source: &str, entries: &[String]) -> FrontendResult<FileAnalysis> {
109    let temporary = TemporaryRustSource::create(source)?;
110    analyze_semantic_file_with_entries(&temporary.path(), entries)
111}
112
113/// Whether this process was started by Cargo as DiffKit's rustc workspace
114/// wrapper. The binary checks this before parsing user-facing CLI arguments.
115pub fn rustc_wrapper_requested() -> bool {
116    env::var_os(RUSTC_CAPTURE_DIRECTORY).is_some()
117}
118
119/// Run one rustc invocation and persist its semantic result for the parent
120/// DiffKit process. Dependency crates are not intercepted because the parent
121/// uses `RUSTC_WORKSPACE_WRAPPER`, not the broader `RUSTC_WRAPPER`.
122pub fn run_rustc_wrapper() -> FrontendResult<()> {
123    let mut arguments = env::args_os().skip(1);
124    let rustc = arguments
125        .next()
126        .ok_or_else(|| std::io::Error::other("rustc wrapper did not receive a compiler path"))?;
127    let arguments = arguments.collect::<Vec<_>>();
128    let mut driver_arguments = vec![rustc.to_string_lossy().into_owned()];
129    driver_arguments.extend(
130        arguments
131            .iter()
132            .cloned()
133            .map(|argument| {
134                argument
135                    .into_string()
136                    .map_err(|_| std::io::Error::other("rustc argument is not valid UTF-8"))
137            })
138            .collect::<Result<Vec<_>, _>>()?,
139    );
140
141    let program = match rustc_public::run!(&driver_arguments, || {
142        std::ops::ControlFlow::<(), SemanticProgram>::Continue(collect_instances())
143    }) {
144        Ok(program) => program,
145        Err(CompilerError::Failed) => {
146            return Err(std::io::Error::other("rustc failed during semantic capture").into());
147        }
148        Err(CompilerError::Skipped) => return Ok(()),
149        Err(CompilerError::Interrupted(())) => {
150            return Err(std::io::Error::other("rustc semantic capture was interrupted").into());
151        }
152    };
153
154    persist_semantic_program(&program)
155}
156
157fn persist_semantic_program(program: &SemanticProgram) -> FrontendResult<()> {
158    let directory = PathBuf::from(env::var_os(RUSTC_CAPTURE_DIRECTORY).ok_or_else(|| {
159        std::io::Error::other("semantic capture directory disappeared from the environment")
160    })?);
161    fs::create_dir_all(&directory)?;
162    let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
163    let destination = directory.join(format!("{}-{sequence}.json", std::process::id()));
164    let temporary = destination.with_extension("json.tmp");
165    fs::write(&temporary, serde_json::to_vec(program)?)?;
166    fs::rename(temporary, destination)?;
167    Ok(())
168}
169
170/// Analyze every Rust target selected by Cargo under `project_root` using the
171/// exact rustc invocations Cargo produced. The returned graph fragment is the
172/// only representation that crosses into DiffKit's common engine.
173pub(crate) fn analyze_semantic_project(
174    project_root: &Path,
175    wrapper_executable: &Path,
176    entries: &[String],
177    session: &RustProjectSession,
178) -> FrontendResult<FileAnalysis> {
179    let root = project_root.canonicalize()?;
180    if !root.join("Cargo.toml").is_file() {
181        return Err(std::io::Error::other(format!(
182            "no Cargo.toml at Rust project root {}",
183            root.display()
184        ))
185        .into());
186    }
187    let generic_entries = entries
188        .iter()
189        .filter(|entry| entry.contains('<'))
190        .cloned()
191        .collect::<Vec<_>>();
192    if generic_entries.is_empty() {
193        return capture_semantic_project(&root, wrapper_executable, session);
194    }
195
196    let seeded = SeededRustProject::create(&root)?;
197    seed_project_entries(seeded.root(), &generic_entries)?;
198    let mut analysis = capture_semantic_project(seeded.root(), wrapper_executable, session)?;
199    remap_analysis_root(&mut analysis, seeded.root(), &root);
200    remove_seed_functions(&mut analysis);
201    Ok(analysis)
202}
203
204fn capture_semantic_project(
205    root: &Path,
206    wrapper_executable: &Path,
207    session: &RustProjectSession,
208) -> FrontendResult<FileAnalysis> {
209    let started = std::time::Instant::now();
210    let fingerprint_started = std::time::Instant::now();
211    let fingerprint = session
212        .cache_path()
213        .map(|_| semantic_cache_fingerprint(root, wrapper_executable))
214        .transpose()?;
215    let fingerprint_elapsed = fingerprint_started.elapsed();
216    // `cargo clean --workspace` and the following check form one operation.
217    // Cargo's own target lock covers each command separately, so another
218    // DiffKit process could otherwise clean between them and then observe a
219    // fresh build without receiving any semantic wrapper output.
220    let cache_lock = fs::OpenOptions::new()
221        .create(true)
222        .read(true)
223        .write(true)
224        .truncate(false)
225        .open(session.target().join(".diffkit.lock"))?;
226    cache_lock.lock()?;
227    if let (Some(cache_path), Some(fingerprint)) = (session.cache_path(), fingerprint.as_deref())
228        && let Some(analysis) = read_analysis_cache(cache_path, fingerprint, root)
229    {
230        session.report_cache(true, fingerprint, fingerprint_elapsed, started.elapsed());
231        return Ok(analysis);
232    }
233    let capture = RustProjectCapture::create()?;
234    let wrapper = wrapper_executable.canonicalize()?;
235    let clean = Command::new("cargo")
236        .args(["clean", "--workspace", "--quiet"])
237        .current_dir(root)
238        .env("CARGO_TARGET_DIR", session.target())
239        .output()?;
240    if !clean.status.success() {
241        let diagnostic = String::from_utf8_lossy(&clean.stderr).trim().to_owned();
242        return Err(std::io::Error::other(if diagnostic.is_empty() {
243            format!("Cargo cache preparation exited with {}", clean.status)
244        } else {
245            diagnostic
246        })
247        .into());
248    }
249    let output = Command::new("cargo")
250        .args(["check", "--workspace", "--all-targets", "--quiet"])
251        .current_dir(root)
252        .env("RUSTC_WORKSPACE_WRAPPER", &wrapper)
253        .env(RUSTC_CAPTURE_DIRECTORY, capture.results())
254        .env(RUSTC_ANALYSIS_ROOT, root)
255        .env("CARGO_TARGET_DIR", session.target())
256        // Before and after snapshots have different source roots. Reusing
257        // rustc incremental state across those roots can mix stale module
258        // paths even though Cargo correctly reruns the workspace target.
259        .env("CARGO_INCREMENTAL", "0")
260        .output()?;
261    if !output.status.success() {
262        let diagnostic = String::from_utf8_lossy(&output.stderr).trim().to_owned();
263        return Err(std::io::Error::other(if diagnostic.is_empty() {
264            format!("Cargo semantic analysis exited with {}", output.status)
265        } else {
266            diagnostic
267        })
268        .into());
269    }
270
271    let mut result_files = fs::read_dir(capture.results())?.collect::<Result<Vec<_>, _>>()?;
272    result_files.sort_by_key(std::fs::DirEntry::path);
273    let mut semantic = SemanticProgram {
274        crate_name: String::new(),
275        functions: Vec::new(),
276    };
277    for result_file in result_files {
278        if result_file
279            .path()
280            .extension()
281            .and_then(|value| value.to_str())
282            != Some("json")
283        {
284            continue;
285        }
286        let mut program: SemanticProgram = serde_json::from_slice(&fs::read(result_file.path())?)?;
287        semantic.functions.append(&mut program.functions);
288    }
289    if semantic.functions.is_empty() {
290        return Err(std::io::Error::other(format!(
291            "Cargo produced no Rust semantic targets under {}",
292            root.display()
293        ))
294        .into());
295    }
296    semantic.functions.sort_by(|left, right| {
297        (&left.key, &left.body_span.file, left.body_span.start_line).cmp(&(
298            &right.key,
299            &right.body_span.file,
300            right.body_span.start_line,
301        ))
302    });
303    semantic.functions.dedup_by(|left, right| {
304        left.key == right.key
305            && left.body_span.file == right.body_span.file
306            && left.body_span.start_line == right.body_span.start_line
307            && left.body_span.start_column == right.body_span.start_column
308    });
309    respecialize_merged_semantic_program(&mut semantic);
310
311    let syntax = analyze_project_sources(root)?;
312    let analysis = deduplicate_analysis(merge_semantic_program(syntax, semantic));
313    if let (Some(cache_path), Some(fingerprint)) = (session.cache_path(), fingerprint) {
314        let cache = CachedRustAnalysis {
315            schema: RUST_SEMANTIC_CACHE_SCHEMA,
316            fingerprint,
317            root: root.to_path_buf(),
318            analysis,
319        };
320        let _ = write_analysis_cache(cache_path, &cache);
321        session.report_cache(
322            false,
323            &cache.fingerprint,
324            fingerprint_elapsed,
325            started.elapsed(),
326        );
327        return Ok(cache.analysis);
328    }
329    Ok(analysis)
330}
331
332fn semantic_cache_fingerprint(root: &Path, wrapper_executable: &Path) -> std::io::Result<String> {
333    let mut hasher = Sha256::new();
334    hash_cache_bytes(&mut hasher, &RUST_SEMANTIC_CACHE_SCHEMA.to_le_bytes());
335    hash_cache_bytes(&mut hasher, &analyzer_identity(wrapper_executable)?);
336    hash_command_identity(&mut hasher, root, "rustc", &["-vV"])?;
337    hash_command_identity(&mut hasher, root, "cargo", &["-V"])?;
338
339    let mut environment = env::vars_os()
340        .filter(|(key, _)| rust_build_environment_key(key))
341        .collect::<Vec<_>>();
342    environment.sort_by(|left, right| {
343        left.0
344            .as_encoded_bytes()
345            .cmp(right.0.as_encoded_bytes())
346            .then_with(|| left.1.as_encoded_bytes().cmp(right.1.as_encoded_bytes()))
347    });
348    for (key, value) in environment {
349        hash_cache_bytes(&mut hasher, key.as_encoded_bytes());
350        hash_cache_bytes(&mut hasher, value.as_encoded_bytes());
351    }
352    if let Ok(snapshot_key) = fs::read(root.join(SNAPSHOT_FINGERPRINT_FILE)) {
353        hash_cache_bytes(&mut hasher, b"git-snapshot");
354        hash_cache_bytes(&mut hasher, &snapshot_key);
355    } else {
356        hash_project_inputs(root, root, &mut hasher)?;
357    }
358    Ok(format!("{:x}", hasher.finalize()))
359}
360
361fn rust_build_environment_key(key: &std::ffi::OsStr) -> bool {
362    let Some(key) = key.to_str() else {
363        return true;
364    };
365    matches!(
366        key,
367        "PATH"
368            | "HOME"
369            | "RUSTFLAGS"
370            | "RUSTDOCFLAGS"
371            | "RUSTC"
372            | "RUSTUP_HOME"
373            | "RUSTUP_TOOLCHAIN"
374            | "CARGO_HOME"
375            | "CARGO_BUILD_TARGET"
376            | "CARGO_ENCODED_RUSTFLAGS"
377            | "CC"
378            | "CXX"
379            | "AR"
380            | "CFLAGS"
381            | "CXXFLAGS"
382            | "LDFLAGS"
383            | "MACOSX_DEPLOYMENT_TARGET"
384            | "SDKROOT"
385            | "PKG_CONFIG_PATH"
386    ) || [
387        "CARGO_PROFILE_",
388        "CARGO_TARGET_",
389        "RUSTC_",
390        "CC_",
391        "CXX_",
392        "AR_",
393        "CFLAGS_",
394        "CXXFLAGS_",
395        "PKG_CONFIG_",
396    ]
397    .iter()
398    .any(|prefix| key.starts_with(prefix))
399}
400
401fn analyzer_identity(wrapper_executable: &Path) -> std::io::Result<Vec<u8>> {
402    let metadata = wrapper_executable.metadata()?;
403    let modified = metadata
404        .modified()?
405        .duration_since(std::time::UNIX_EPOCH)
406        .unwrap_or_default();
407    Ok(format!(
408        "{}:{}:{}:{}",
409        env!("CARGO_PKG_VERSION"),
410        metadata.len(),
411        modified.as_secs(),
412        modified.subsec_nanos()
413    )
414    .into_bytes())
415}
416
417fn hash_command_identity(
418    hasher: &mut Sha256,
419    root: &Path,
420    program: &str,
421    arguments: &[&str],
422) -> std::io::Result<()> {
423    let output = Command::new(program)
424        .args(arguments)
425        .current_dir(root)
426        .output()?;
427    hash_cache_bytes(hasher, program.as_bytes());
428    hash_cache_bytes(hasher, &output.stdout);
429    hash_cache_bytes(hasher, &output.stderr);
430    hash_cache_bytes(hasher, &output.status.code().unwrap_or(-1).to_le_bytes());
431    Ok(())
432}
433
434fn hash_project_inputs(root: &Path, directory: &Path, hasher: &mut Sha256) -> std::io::Result<()> {
435    let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
436    entries.sort_by_key(std::fs::DirEntry::path);
437    for entry in entries {
438        let path = entry.path();
439        let file_type = entry.file_type()?;
440        if file_type.is_dir() {
441            if !is_generated_directory(&path) {
442                hash_project_inputs(root, &path, hasher)?;
443            }
444            continue;
445        }
446
447        let relative = path.strip_prefix(root).unwrap_or(&path);
448        if file_type.is_file() {
449            hash_cache_bytes(hasher, b"file");
450            hash_cache_bytes(hasher, relative.as_os_str().as_encoded_bytes());
451            hash_file_contents(&path, hasher)?;
452        } else if file_type.is_symlink() {
453            hash_cache_bytes(hasher, b"symlink");
454            hash_cache_bytes(hasher, relative.as_os_str().as_encoded_bytes());
455            let target = fs::read_link(&path)?;
456            hash_cache_bytes(hasher, target.as_os_str().as_encoded_bytes());
457            if let Ok(resolved) = path.canonicalize()
458                && resolved.is_file()
459            {
460                hash_file_contents(&resolved, hasher)?;
461            }
462        }
463    }
464    Ok(())
465}
466
467fn is_generated_directory(path: &Path) -> bool {
468    matches!(
469        path.file_name().and_then(|name| name.to_str()),
470        Some(".git" | "target" | "node_modules" | ".zig-cache" | "zig-out" | "_build")
471    )
472}
473
474fn hash_file_contents(path: &Path, hasher: &mut Sha256) -> std::io::Result<()> {
475    let mut file = fs::File::open(path)?;
476    let length = file.metadata()?.len();
477    hasher.update(length.to_le_bytes());
478    let mut buffer = [0u8; 64 * 1024];
479    loop {
480        let read = file.read(&mut buffer)?;
481        if read == 0 {
482            break;
483        }
484        hasher.update(&buffer[..read]);
485    }
486    Ok(())
487}
488
489fn hash_cache_bytes(hasher: &mut Sha256, bytes: &[u8]) {
490    hasher.update((bytes.len() as u64).to_le_bytes());
491    hasher.update(bytes);
492}
493
494fn read_analysis_cache(path: PathBuf, fingerprint: &str, root: &Path) -> Option<FileAnalysis> {
495    let mut cached = serde_json::from_slice::<CachedRustAnalysis>(&fs::read(path).ok()?).ok()?;
496    if cached.schema != RUST_SEMANTIC_CACHE_SCHEMA || cached.fingerprint != fingerprint {
497        return None;
498    }
499    remap_analysis_root(&mut cached.analysis, &cached.root, root);
500    Some(cached.analysis)
501}
502
503fn write_analysis_cache(path: PathBuf, cached: &CachedRustAnalysis) -> std::io::Result<()> {
504    if let Some(parent) = path.parent() {
505        fs::create_dir_all(parent)?;
506    }
507    let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
508    let temporary = path.with_extension(format!("json.tmp-{}-{sequence}", std::process::id()));
509    fs::write(&temporary, serde_json::to_vec(cached)?)?;
510    if let Err(error) = fs::rename(&temporary, &path) {
511        if path.is_file() {
512            fs::remove_file(&path)?;
513            fs::rename(&temporary, &path)?;
514        } else {
515            let _ = fs::remove_file(temporary);
516            return Err(error);
517        }
518    }
519    Ok(())
520}
521
522struct RustProjectCapture {
523    directory: PathBuf,
524}
525
526impl RustProjectCapture {
527    fn create() -> std::io::Result<Self> {
528        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
529        let directory = env::temp_dir().join(format!(
530            "diffkit-rust-capture-{}-{sequence}",
531            std::process::id()
532        ));
533        fs::create_dir(&directory)?;
534        fs::create_dir(directory.join("results"))?;
535        Ok(Self { directory })
536    }
537
538    fn results(&self) -> PathBuf {
539        self.directory.join("results")
540    }
541}
542
543impl Drop for RustProjectCapture {
544    fn drop(&mut self) {
545        let _ = fs::remove_dir_all(&self.directory);
546    }
547}
548
549/// Owns one Cargo target directory for one semantic-analysis endpoint.
550///
551/// A before/after comparison must not share a persistent target directory:
552/// Cargo identifies workspace packages independently of the temporary Git
553/// snapshot path, so a binary from one endpoint can otherwise be checked
554/// against stale library metadata from the other endpoint.
555pub(crate) struct RustProjectSession {
556    target: PathBuf,
557    cleanup: Option<PathBuf>,
558    endpoint: Option<String>,
559    verbose: bool,
560}
561
562impl RustProjectSession {
563    pub(crate) fn create() -> std::io::Result<Self> {
564        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
565        let directory = env::temp_dir().join(format!(
566            "diffkit-rust-session-{}-{sequence}",
567            std::process::id()
568        ));
569        fs::create_dir(&directory)?;
570        Ok(Self {
571            target: directory.join("target"),
572            cleanup: Some(directory),
573            endpoint: None,
574            verbose: false,
575        })
576    }
577
578    pub(crate) fn create_cached(
579        project_root: &Path,
580        endpoint: &str,
581        verbose: bool,
582    ) -> std::io::Result<Self> {
583        let mut components = Path::new(endpoint).components();
584        if !matches!(components.next(), Some(std::path::Component::Normal(_)))
585            || components.next().is_some()
586        {
587            return Err(std::io::Error::new(
588                std::io::ErrorKind::InvalidInput,
589                "Rust semantic cache endpoint must be one path component",
590            ));
591        }
592        let target = project_root
593            .join("target")
594            .join("diffkit-semantic")
595            .join(endpoint);
596        fs::create_dir_all(&target)?;
597        Ok(Self {
598            target,
599            cleanup: None,
600            endpoint: Some(endpoint.to_owned()),
601            verbose,
602        })
603    }
604
605    fn target(&self) -> &Path {
606        &self.target
607    }
608
609    fn cache_path(&self) -> Option<PathBuf> {
610        self.cleanup
611            .is_none()
612            .then(|| self.target.join(".diffkit").join("analysis-v2.json"))
613    }
614
615    fn report_cache(
616        &self,
617        hit: bool,
618        fingerprint_key: &str,
619        fingerprint: std::time::Duration,
620        total: std::time::Duration,
621    ) {
622        if self.verbose {
623            let status = if hit { "hit" } else { "miss" };
624            eprintln!(
625                "Rust semantic cache {status}: {} [{}] ({} ms; fingerprint {} ms)",
626                self.endpoint.as_deref().unwrap_or("temporary"),
627                &fingerprint_key[..fingerprint_key.len().min(12)],
628                total.as_millis(),
629                fingerprint.as_millis(),
630            );
631        }
632    }
633}
634
635impl Drop for RustProjectSession {
636    fn drop(&mut self) {
637        if let Some(directory) = &self.cleanup {
638            let _ = fs::remove_dir_all(directory);
639        }
640    }
641}
642
643struct SeededRustProject {
644    directory: PathBuf,
645    root: PathBuf,
646}
647
648impl SeededRustProject {
649    fn create(source: &Path) -> std::io::Result<Self> {
650        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
651        let directory = env::temp_dir().join(format!(
652            "diffkit-rust-seeded-{}-{sequence}",
653            std::process::id()
654        ));
655        let root = directory.join("project");
656        fs::create_dir_all(&root)?;
657        copy_rust_project(source, source, &root)?;
658        Ok(Self { directory, root })
659    }
660
661    fn root(&self) -> &Path {
662        &self.root
663    }
664}
665
666impl Drop for SeededRustProject {
667    fn drop(&mut self) {
668        let _ = fs::remove_dir_all(&self.directory);
669    }
670}
671
672fn copy_rust_project(
673    source_root: &Path,
674    directory: &Path,
675    destination: &Path,
676) -> std::io::Result<()> {
677    let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
678    entries.sort_by_key(std::fs::DirEntry::path);
679    for entry in entries {
680        let path = entry.path();
681        let relative = path.strip_prefix(source_root).unwrap_or(&path);
682        if relative.components().any(|component| {
683            matches!(
684                component.as_os_str().to_str(),
685                Some(".git" | "target" | "node_modules" | "_build" | ".zig-cache" | "zig-out")
686            )
687        }) {
688            continue;
689        }
690        let target = destination.join(relative);
691        let file_type = entry.file_type()?;
692        if file_type.is_dir() {
693            fs::create_dir_all(&target)?;
694            copy_rust_project(source_root, &path, destination)?;
695        } else if file_type.is_file() {
696            if let Some(parent) = target.parent() {
697                fs::create_dir_all(parent)?;
698            }
699            fs::copy(path, target)?;
700        } else if file_type.is_symlink() {
701            let resolved = fs::canonicalize(path)?;
702            if resolved.starts_with(source_root) && resolved.is_file() {
703                if let Some(parent) = target.parent() {
704                    fs::create_dir_all(parent)?;
705                }
706                fs::copy(resolved, target)?;
707            }
708        }
709    }
710    Ok(())
711}
712
713fn seed_project_entries(root: &Path, entries: &[String]) -> FrontendResult<()> {
714    let mut files = Vec::new();
715    collect_rust_sources(root, &mut files)?;
716    files.sort();
717    for file in files {
718        let source = fs::read_to_string(&file)?;
719        if let Some(seeded) = append_entry_seeds(&source, entries)? {
720            fs::write(file, seeded)?;
721        }
722    }
723    Ok(())
724}
725
726fn remap_analysis_root(analysis: &mut FileAnalysis, from: &Path, to: &Path) {
727    let remap = |path: &mut PathBuf| {
728        if let Ok(relative) = path.strip_prefix(from) {
729            *path = to.join(relative);
730        }
731    };
732    for function in &mut analysis.functions {
733        remap(&mut function.span.file);
734        for call in &mut function.calls {
735            remap(&mut call.span.file);
736        }
737    }
738    for fact in &mut analysis.facts {
739        remap(&mut fact.span.file);
740    }
741    analysis.source_files = std::mem::take(&mut analysis.source_files)
742        .into_iter()
743        .map(|mut file| {
744            remap(&mut file);
745            file
746        })
747        .collect();
748}
749
750fn remove_seed_functions(analysis: &mut FileAnalysis) {
751    let seeds = analysis
752        .functions
753        .iter()
754        .filter(|function| function.label.default.contains("__diffkit_seed_"))
755        .map(|function| function.id.clone())
756        .collect::<HashSet<_>>();
757    analysis
758        .functions
759        .retain(|function| !seeds.contains(&function.id));
760    analysis.facts.retain(|fact| !seeds.contains(&fact.subject));
761}
762
763fn analyze_project_sources(root: &Path) -> FrontendResult<FileAnalysis> {
764    let mut files = Vec::new();
765    collect_rust_sources(root, &mut files)?;
766    files.sort();
767    let mut combined = FileAnalysis::default();
768    for file in files {
769        let source = fs::read_to_string(&file)?;
770        let module = rust_module_path(root, &file);
771        let mut analysis = RustBackend.analyze_file(
772            &FileContext {
773                path: &file,
774                module: &module,
775            },
776            &source,
777        )?;
778        combined.functions.append(&mut analysis.functions);
779        combined.facts.append(&mut analysis.facts);
780        combined.source_files.append(&mut analysis.source_files);
781    }
782    Ok(combined)
783}
784
785fn collect_rust_sources(directory: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<()> {
786    let mut entries = fs::read_dir(directory)?.collect::<Result<Vec<_>, _>>()?;
787    entries.sort_by_key(std::fs::DirEntry::path);
788    for entry in entries {
789        let path = entry.path();
790        let file_type = entry.file_type()?;
791        if file_type.is_dir() {
792            if !matches!(
793                path.file_name().and_then(|name| name.to_str()),
794                Some(".git" | "target" | "node_modules" | ".zig-cache" | "zig-out" | "_build")
795            ) {
796                collect_rust_sources(&path, files)?;
797            }
798        } else if file_type.is_file()
799            && path.extension().and_then(|extension| extension.to_str()) == Some("rs")
800        {
801            files.push(path);
802        }
803    }
804    Ok(())
805}
806
807fn rust_module_path(root: &Path, file: &Path) -> Vec<String> {
808    let source_root = root.join("src");
809    let relative = file
810        .strip_prefix(&source_root)
811        .or_else(|_| file.strip_prefix(root))
812        .unwrap_or(file);
813    let mut parts = relative
814        .parent()
815        .into_iter()
816        .flat_map(Path::components)
817        .filter_map(|component| component.as_os_str().to_str().map(ToOwned::to_owned))
818        .collect::<Vec<_>>();
819    let stem = file
820        .file_stem()
821        .and_then(|stem| stem.to_str())
822        .unwrap_or_default();
823    if !matches!(stem, "lib" | "main" | "mod") {
824        parts.push(stem.to_owned());
825    }
826    parts
827}
828
829fn deduplicate_analysis(analysis: FileAnalysis) -> FileAnalysis {
830    let mut functions = BTreeMap::<SymbolId, FunctionInfo>::new();
831    for function in analysis.functions {
832        functions
833            .entry(function.id.clone())
834            .and_modify(|existing| {
835                for call in &function.calls {
836                    if !existing.calls.contains(call) {
837                        existing.calls.push(call.clone());
838                    }
839                }
840                existing.calls.sort_by_key(|call| {
841                    (
842                        call.span.file.clone(),
843                        call.span.start_line,
844                        call.span.start_column,
845                    )
846                });
847            })
848            .or_insert(function);
849    }
850    FileAnalysis {
851        functions: functions.into_values().collect(),
852        facts: analysis.facts,
853        source_files: analysis.source_files,
854        roots: analysis.roots,
855    }
856}
857
858fn analysis_has_entry(analysis: &FileAnalysis, entry: &str) -> bool {
859    let expected = entry.replace('.', "::").replace("::<", "<");
860    analysis.functions.iter().any(|function| {
861        let label = &function.label.default;
862        let callable =
863            outer_call_arguments_start(label).map_or(label.as_str(), |index| &label[..index]);
864        callable == expected || callable.ends_with(&format!("::{expected}"))
865    })
866}
867
868fn append_entry_seeds(source: &str, entries: &[String]) -> FrontendResult<Option<String>> {
869    let file = syn::parse_file(source)?;
870    let mut functions = Vec::new();
871    collect_seedable_functions(&file.items, &[], &mut functions);
872    let mut seeds = Vec::new();
873
874    for entry in entries {
875        let Some(generic_start) = entry.find('<') else {
876            continue;
877        };
878        if !entry.ends_with('>') || entry.starts_with('<') {
879            continue;
880        }
881        let base = entry[..generic_start]
882            .trim_end_matches("::")
883            .replace('.', "::");
884        let generic_arguments = &entry[generic_start + 1..entry.len() - 1];
885        let lookup = base.strip_prefix("crate::").unwrap_or(&base);
886        let mut matches = functions
887            .iter()
888            .filter(|function| {
889                lookup == function.qualified_name
890                    || (!lookup.contains("::") && lookup == function.name)
891            })
892            .collect::<Vec<_>>();
893        if matches.len() != 1 {
894            continue;
895        }
896        let function = matches.pop().expect("one seedable function matched");
897        let arguments = (0..function.argument_count)
898            .map(|_| "::core::mem::MaybeUninit::uninit().assume_init()")
899            .collect::<Vec<_>>()
900            .join(", ");
901        seeds.push(format!(
902            "\n#[doc(hidden)]\n#[allow(dead_code, invalid_value, unused_unsafe)]\nfn __diffkit_seed_{}() {{\n    unsafe {{ let _ = {}::<{}>({}); }}\n}}\n",
903            seeds.len(), function.qualified_name, generic_arguments, arguments
904        ));
905    }
906
907    if seeds.is_empty() {
908        Ok(None)
909    } else {
910        let mut seeded = source.to_owned();
911        seeded.extend(seeds);
912        Ok(Some(seeded))
913    }
914}
915
916struct SeedableFunction {
917    name: String,
918    qualified_name: String,
919    argument_count: usize,
920}
921
922fn collect_seedable_functions(
923    items: &[Item],
924    module: &[String],
925    functions: &mut Vec<SeedableFunction>,
926) {
927    for item in items {
928        match item {
929            Item::Fn(function) => {
930                let name = function.sig.ident.to_string();
931                let qualified_name = module
932                    .iter()
933                    .map(String::as_str)
934                    .chain(std::iter::once(name.as_str()))
935                    .collect::<Vec<_>>()
936                    .join("::");
937                functions.push(SeedableFunction {
938                    name,
939                    qualified_name,
940                    argument_count: function.sig.inputs.len(),
941                });
942            }
943            Item::Mod(item_mod) => {
944                if let Some((_, nested)) = &item_mod.content {
945                    let mut nested_module = module.to_vec();
946                    nested_module.push(item_mod.ident.to_string());
947                    collect_seedable_functions(nested, &nested_module, functions);
948                }
949            }
950            _ => {}
951        }
952    }
953}
954
955impl TemporaryRustSource {
956    fn create(source: &str) -> std::io::Result<Self> {
957        let sequence = TEMP_SOURCE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
958        let directory = std::env::temp_dir().join(format!(
959            "diffkit-rust-source-{}-{sequence}",
960            std::process::id()
961        ));
962        fs::create_dir(&directory)?;
963        let temporary = TemporaryRustSource { directory };
964        let path = temporary.directory.join("input.rs");
965        fs::write(&path, source)?;
966        Ok(temporary)
967    }
968
969    fn path(&self) -> PathBuf {
970        self.directory.join("input.rs")
971    }
972}
973
974struct TemporaryRustSource {
975    directory: PathBuf,
976}
977
978impl Drop for TemporaryRustSource {
979    fn drop(&mut self) {
980        let _ = fs::remove_file(self.directory.join("input.rs"));
981        let _ = fs::remove_dir(&self.directory);
982    }
983}
984
985#[derive(Clone, Debug, Deserialize, Serialize)]
986struct SemanticProgram {
987    crate_name: String,
988    functions: Vec<SemanticFunction>,
989}
990
991#[derive(Debug, Deserialize, Serialize)]
992struct CachedRustAnalysis {
993    schema: u32,
994    fingerprint: String,
995    root: PathBuf,
996    analysis: FileAnalysis,
997}
998
999#[derive(Clone, Debug, Deserialize, Serialize)]
1000struct SemanticFunction {
1001    base_key: String,
1002    key: String,
1003    display: String,
1004    body_span: SourceSpan,
1005    calls: Vec<SemanticCall>,
1006    constructor_spans: Vec<SourceSpan>,
1007    constructor_names: Vec<String>,
1008    parameter_types: Vec<String>,
1009}
1010
1011#[derive(Clone, Debug, Deserialize, Serialize)]
1012struct SemanticCall {
1013    target: SemanticCallTarget,
1014    definition_name: String,
1015    span: SourceSpan,
1016    argument_types: Vec<String>,
1017    argument_flows: Vec<SemanticDynFlow>,
1018}
1019
1020#[derive(Clone, Debug, Default, Deserialize, Serialize)]
1021struct SemanticDynFlow {
1022    concrete_types: Vec<String>,
1023    parameters: Vec<usize>,
1024    unresolved_reasons: BTreeSet<UnresolvedReason>,
1025}
1026
1027#[derive(Clone, Debug, Deserialize, Serialize)]
1028enum SemanticCallTarget {
1029    Direct {
1030        key: String,
1031        display: String,
1032    },
1033    Dynamic {
1034        key: String,
1035        display: String,
1036        candidates: Vec<SemanticDispatchCandidate>,
1037        open: bool,
1038        unresolved_reasons: BTreeSet<UnresolvedReason>,
1039        receiver_flow: SemanticDynFlow,
1040    },
1041    Indirect {
1042        signature: String,
1043    },
1044    Unresolved,
1045}
1046
1047#[derive(Clone, Debug, Deserialize, Serialize)]
1048struct SemanticDispatchCandidate {
1049    key: String,
1050    display: String,
1051}
1052
1053#[derive(Clone, Debug, Default, Eq, PartialEq)]
1054struct DynFlow {
1055    concrete: Vec<TraitRef>,
1056    parameters: Vec<usize>,
1057    unresolved_reasons: BTreeSet<UnresolvedReason>,
1058}
1059
1060impl DynFlow {
1061    fn parameter(index: usize) -> Self {
1062        Self {
1063            concrete: Vec::new(),
1064            parameters: vec![index],
1065            unresolved_reasons: BTreeSet::new(),
1066        }
1067    }
1068
1069    fn concrete(trait_ref: TraitRef) -> Self {
1070        Self {
1071            concrete: vec![trait_ref],
1072            parameters: Vec::new(),
1073            unresolved_reasons: BTreeSet::new(),
1074        }
1075    }
1076
1077    fn unresolved(reason: UnresolvedReason) -> Self {
1078        Self {
1079            concrete: Vec::new(),
1080            parameters: Vec::new(),
1081            unresolved_reasons: [reason].into_iter().collect(),
1082        }
1083    }
1084
1085    fn is_empty(&self) -> bool {
1086        self.concrete.is_empty() && self.parameters.is_empty() && self.unresolved_reasons.is_empty()
1087    }
1088
1089    fn merge(&mut self, other: &Self) -> bool {
1090        let mut changed = false;
1091        for candidate in &other.concrete {
1092            if !self.concrete.contains(candidate) {
1093                self.concrete.push(candidate.clone());
1094                changed = true;
1095            }
1096        }
1097        for parameter in &other.parameters {
1098            if !self.parameters.contains(parameter) {
1099                self.parameters.push(*parameter);
1100                changed = true;
1101            }
1102        }
1103        for reason in &other.unresolved_reasons {
1104            changed |= self.unresolved_reasons.insert(reason.clone());
1105        }
1106        self.parameters.sort_unstable();
1107        changed
1108    }
1109}
1110
1111#[derive(Clone, Debug, Default)]
1112struct ResolvedDynFlow {
1113    concrete: Vec<TraitRef>,
1114    unresolved_reasons: BTreeSet<UnresolvedReason>,
1115}
1116
1117impl ResolvedDynFlow {
1118    fn merge(&mut self, other: &Self) {
1119        for candidate in &other.concrete {
1120            if !self.concrete.contains(candidate) {
1121                self.concrete.push(candidate.clone());
1122            }
1123        }
1124        self.unresolved_reasons
1125            .extend(other.unresolved_reasons.iter().cloned());
1126    }
1127}
1128
1129#[derive(Clone)]
1130struct RawSemanticFunction {
1131    key: String,
1132    display: String,
1133    body_span: SourceSpan,
1134    calls: Vec<RawSemanticCall>,
1135    constructor_spans: Vec<SourceSpan>,
1136    constructor_names: Vec<String>,
1137    parameter_types: Vec<String>,
1138}
1139
1140#[derive(Clone)]
1141struct RawSemanticCall {
1142    target: RawSemanticCallTarget,
1143    definition_name: String,
1144    span: SourceSpan,
1145    argument_types: Vec<String>,
1146    argument_flows: Vec<DynFlow>,
1147}
1148
1149#[derive(Clone)]
1150enum RawSemanticCallTarget {
1151    Direct {
1152        key: String,
1153        display: String,
1154    },
1155    Dynamic {
1156        dispatch: Instance,
1157        key: String,
1158        display: String,
1159        receiver: DynFlow,
1160    },
1161    Indirect {
1162        signature: String,
1163    },
1164    Unresolved,
1165}
1166
1167#[derive(Clone)]
1168struct InstanceBody {
1169    instance: Instance,
1170    body: Body,
1171}
1172
1173#[derive(Clone, Default)]
1174struct MirCallFlow {
1175    arguments: Vec<DynFlow>,
1176}
1177
1178#[derive(Default)]
1179struct BodyDynAnalysis {
1180    calls: Vec<Option<MirCallFlow>>,
1181    return_flow: DynFlow,
1182}
1183
1184fn collect_rustc_program(path: &Path) -> FrontendResult<SemanticProgram> {
1185    let _driver_guard = RUSTC_DRIVER_LOCK
1186        .lock()
1187        .map_err(|_| std::io::Error::other("rustc semantic driver lock was poisoned"))?;
1188    let arguments = vec![
1189        "rustc".to_owned(),
1190        path.display().to_string(),
1191        "--crate-name=diffkit_input".to_owned(),
1192        "--crate-type=lib".to_owned(),
1193        "--edition=2024".to_owned(),
1194        "--cap-lints=allow".to_owned(),
1195    ];
1196
1197    match rustc_public::run!(&arguments, || {
1198        std::ops::ControlFlow::<SemanticProgram, ()>::Break(collect_instances())
1199    }) {
1200        Err(CompilerError::Interrupted(program)) => Ok(program),
1201        Err(CompilerError::Failed) => Err(std::io::Error::other(format!(
1202            "rustc failed to compile {} for semantic analysis",
1203            path.display()
1204        ))
1205        .into()),
1206        Err(CompilerError::Skipped) => Err(std::io::Error::other(
1207            "rustc skipped semantic analysis before its callback ran",
1208        )
1209        .into()),
1210        Ok(()) => Err(std::io::Error::other(
1211            "rustc semantic callback unexpectedly completed without analysis",
1212        )
1213        .into()),
1214    }
1215}
1216
1217fn collect_instances() -> SemanticProgram {
1218    let crate_name = rustc_public::local_crate().name.to_string();
1219    let trait_method_implementations = trait_method_implementations();
1220    let (instance_bodies, observed_vtables) =
1221        collect_instance_bodies(&trait_method_implementations);
1222    let local_instances = instance_bodies
1223        .iter()
1224        .map(|item| item.instance)
1225        .collect::<HashSet<_>>();
1226    let mut return_summaries = HashMap::<Instance, DynFlow>::new();
1227
1228    // Return-value summaries remain symbolic in terms of the callee's formal
1229    // parameters. This lets a trait object cross ordinary helper functions
1230    // without losing its concrete provenance. The lattice is finite, so the
1231    // monotone iteration also handles recursive helpers.
1232    let max_iterations = instance_bodies
1233        .iter()
1234        .map(|item| item.body.locals().len())
1235        .sum::<usize>()
1236        .saturating_add(instance_bodies.len())
1237        .max(1);
1238    let mut summaries_converged = false;
1239    for _ in 0..max_iterations {
1240        let mut changed = false;
1241        for item in &instance_bodies {
1242            let analysis = analyze_body_dyn_flows(&item.body, &return_summaries, &local_instances);
1243            changed |= return_summaries
1244                .entry(item.instance)
1245                .or_default()
1246                .merge(&analysis.return_flow);
1247        }
1248        if !changed {
1249            summaries_converged = true;
1250            break;
1251        }
1252    }
1253    if !summaries_converged {
1254        for item in &instance_bodies {
1255            if type_contains_dyn(item.body.ret_local().ty) {
1256                return_summaries
1257                    .entry(item.instance)
1258                    .or_default()
1259                    .unresolved_reasons
1260                    .insert(UnresolvedReason::AnalysisLimit);
1261            }
1262        }
1263    }
1264
1265    let mut raw_functions = Vec::new();
1266    for item in &instance_bodies {
1267        let instance = item.instance;
1268        let body = &item.body;
1269        let instance_name = instance.name();
1270        let analysis = analyze_body_dyn_flows(body, &return_summaries, &local_instances);
1271        let mut calls = Vec::new();
1272        for (block_index, block) in body.blocks.iter().enumerate() {
1273            let TerminatorKind::Call { func, args, .. } = &block.terminator.kind else {
1274                continue;
1275            };
1276            let resolved_target = resolve_called_instance(body, func);
1277            let definition_name = resolved_target
1278                .map(|target| target.def.name())
1279                .unwrap_or_default();
1280            let call_flow = analysis
1281                .calls
1282                .get(block_index)
1283                .and_then(Option::as_ref)
1284                .cloned()
1285                .unwrap_or_default();
1286            let target = match resolved_target {
1287                Some(target) => {
1288                    let name = target.name();
1289                    match target.kind {
1290                        InstanceKind::Virtual { .. } => RawSemanticCallTarget::Dynamic {
1291                            dispatch: target,
1292                            key: normalize_instance_key(&name),
1293                            display: normalize_instance_display(&name, &crate_name),
1294                            receiver: call_flow.arguments.first().cloned().unwrap_or_else(|| {
1295                                DynFlow::unresolved(UnresolvedReason::AnalysisLimit)
1296                            }),
1297                        },
1298                        _ => RawSemanticCallTarget::Direct {
1299                            key: normalize_instance_key(&name),
1300                            display: normalize_instance_display(&name, &crate_name),
1301                        },
1302                    }
1303                }
1304                None => func
1305                    .ty(body.locals())
1306                    .ok()
1307                    .filter(|ty| matches!(ty.kind(), TyKind::RigidTy(RigidTy::FnPtr(_))))
1308                    .map(|ty| RawSemanticCallTarget::Indirect {
1309                        signature: normalize_type_display(&ty.to_string(), &crate_name),
1310                    })
1311                    .unwrap_or(RawSemanticCallTarget::Unresolved),
1312            };
1313            calls.push(RawSemanticCall {
1314                target,
1315                definition_name,
1316                span: rustc_source_span(block.terminator.span),
1317                argument_types: args
1318                    .iter()
1319                    .filter_map(|argument| argument.ty(body.locals()).ok())
1320                    .map(|ty| normalize_type_display(&ty.to_string(), &crate_name))
1321                    .collect(),
1322                argument_flows: call_flow.arguments,
1323            });
1324        }
1325        calls.sort_by_key(|call| {
1326            (
1327                call.span.start_line,
1328                call.span.start_column,
1329                call.span.end_line,
1330                call.span.end_column,
1331            )
1332        });
1333        let mut constructor_spans = body
1334            .blocks
1335            .iter()
1336            .flat_map(|block| &block.statements)
1337            .filter_map(|statement| {
1338                let StatementKind::Assign(_, value) = &statement.kind else {
1339                    return None;
1340                };
1341                let ty = value.ty(body.locals()).ok()?;
1342                matches!(ty.kind(), TyKind::RigidTy(RigidTy::Adt(..)))
1343                    .then(|| rustc_source_span(statement.span))
1344            })
1345            .collect::<Vec<_>>();
1346        constructor_spans.sort_by_key(|span| {
1347            (
1348                span.start_line,
1349                span.start_column,
1350                span.end_line,
1351                span.end_column,
1352            )
1353        });
1354        constructor_spans.dedup();
1355        let mut constructor_names = HashSet::new();
1356        for local in body.locals() {
1357            collect_adt_constructor_names(local.ty, &mut constructor_names);
1358        }
1359        let mut constructor_names = constructor_names.into_iter().collect::<Vec<_>>();
1360        constructor_names.sort();
1361        raw_functions.push(RawSemanticFunction {
1362            key: normalize_instance_key(&instance_name),
1363            display: normalize_instance_display(&instance_name, &crate_name),
1364            body_span: rustc_source_span(body.span),
1365            calls,
1366            constructor_spans,
1367            constructor_names,
1368            parameter_types: body
1369                .arg_locals()
1370                .iter()
1371                .map(|argument| normalize_type_display(&argument.ty.to_string(), &crate_name))
1372                .collect(),
1373        });
1374    }
1375
1376    let mut functions = specialize_semantic_functions(
1377        &raw_functions,
1378        &observed_vtables,
1379        &trait_method_implementations,
1380        &crate_name,
1381    );
1382    functions.sort_by(|left, right| left.key.cmp(&right.key));
1383    SemanticProgram {
1384        crate_name,
1385        functions,
1386    }
1387}
1388
1389fn collect_instance_bodies(
1390    trait_method_implementations: &HashMap<rustc_public::DefId, rustc_public::DefId>,
1391) -> (Vec<InstanceBody>, Vec<TraitRef>) {
1392    let mut queue = rustc_public::all_local_items()
1393        .into_iter()
1394        .filter(|item| matches!(item.kind(), rustc_public::ItemKind::Fn))
1395        .filter_map(|item| Instance::try_from(item).ok())
1396        .collect::<VecDeque<_>>();
1397    let mut visited = HashSet::new();
1398    let mut bodies = Vec::new();
1399    let mut observed_vtables = Vec::<TraitRef>::new();
1400
1401    loop {
1402        while let Some(instance) = queue.pop_front() {
1403            if !visited.insert(instance) {
1404                continue;
1405            }
1406            let Some(body) = instance.body() else {
1407                continue;
1408            };
1409
1410            let mut local_vtables = Vec::new();
1411            collect_observed_vtables(&body, &mut local_vtables);
1412            for vtable in local_vtables {
1413                if !observed_vtables.contains(&vtable) {
1414                    observed_vtables.push(vtable);
1415                }
1416            }
1417
1418            for block in &body.blocks {
1419                let TerminatorKind::Call {
1420                    func,
1421                    args,
1422                    destination,
1423                    ..
1424                } = &block.terminator.kind
1425                else {
1426                    continue;
1427                };
1428                let Some(target) = resolve_called_instance(&body, func) else {
1429                    continue;
1430                };
1431                if !matches!(target.kind, InstanceKind::Virtual { .. })
1432                    && target.has_body()
1433                    && (target.def.krate().is_local
1434                        || instance_body_is_in_analysis_root(target)
1435                        || call_boundary_contains_dyn(&body, args, destination))
1436                {
1437                    queue.push_back(target);
1438                }
1439            }
1440            bodies.push(InstanceBody { instance, body });
1441        }
1442
1443        for item in &bodies {
1444            for block in &item.body.blocks {
1445                let TerminatorKind::Call { func, .. } = &block.terminator.kind else {
1446                    continue;
1447                };
1448                let Some(dispatch) = resolve_called_instance(&item.body, func) else {
1449                    continue;
1450                };
1451                if !matches!(dispatch.kind, InstanceKind::Virtual { .. }) {
1452                    continue;
1453                }
1454                for trait_ref in &observed_vtables {
1455                    let Some(candidate) = resolve_dispatch_candidate(
1456                        dispatch,
1457                        trait_ref,
1458                        trait_method_implementations,
1459                    ) else {
1460                        continue;
1461                    };
1462                    if candidate.has_body() && !visited.contains(&candidate) {
1463                        queue.push_back(candidate);
1464                    }
1465                }
1466            }
1467        }
1468        if queue.is_empty() {
1469            break;
1470        }
1471    }
1472    (bodies, observed_vtables)
1473}
1474
1475fn instance_body_is_in_analysis_root(instance: Instance) -> bool {
1476    let Some(root) = env::var_os(RUSTC_ANALYSIS_ROOT).map(PathBuf::from) else {
1477        return false;
1478    };
1479    let Some(body) = instance.body() else {
1480        return false;
1481    };
1482    let file = rustc_source_span(body.span).file;
1483    file.starts_with(&root)
1484        || file
1485            .canonicalize()
1486            .ok()
1487            .zip(root.canonicalize().ok())
1488            .is_some_and(|(file, root)| file.starts_with(root))
1489}
1490
1491fn call_boundary_contains_dyn(body: &Body, arguments: &[Operand], destination: &Place) -> bool {
1492    arguments
1493        .iter()
1494        .filter_map(|argument| argument.ty(body.locals()).ok())
1495        .any(type_contains_dyn)
1496        || destination.ty(body.locals()).is_ok_and(type_contains_dyn)
1497}
1498
1499fn collect_observed_vtables(body: &rustc_public::mir::Body, observed: &mut Vec<TraitRef>) {
1500    for block in &body.blocks {
1501        for statement in &block.statements {
1502            let StatementKind::Assign(
1503                _,
1504                Rvalue::Cast(
1505                    CastKind::PointerCoercion(PointerCoercion::Unsize),
1506                    operand,
1507                    target_ty,
1508                ),
1509            ) = &statement.kind
1510            else {
1511                continue;
1512            };
1513            let Ok(source_ty) = operand.ty(body.locals()) else {
1514                continue;
1515            };
1516            let Some((concrete_ty, principal)) = dyn_coercion(source_ty, *target_ty) else {
1517                continue;
1518            };
1519            let trait_ref = TraitRef::new(principal.def_id, concrete_ty, &principal.generic_args);
1520            if !observed.contains(&trait_ref) {
1521                observed.push(trait_ref);
1522            }
1523        }
1524    }
1525}
1526
1527fn dyn_coercion(source: Ty, target: Ty) -> Option<(Ty, ExistentialTraitRef)> {
1528    let target_kind = target.kind();
1529    if let Some(principal) = target_kind.trait_principal() {
1530        // A trait upcast (`dyn Child` -> `dyn Parent`) does not reveal a
1531        // concrete implementation. Only thin-to-wide coercions contribute an
1532        // concrete receiver-flow candidate here.
1533        return (!source.kind().is_trait()).then_some((source, principal.value));
1534    }
1535
1536    match (source.kind(), target_kind) {
1537        (
1538            TyKind::RigidTy(RigidTy::Ref(_, source, _)),
1539            TyKind::RigidTy(RigidTy::Ref(_, target, _)),
1540        )
1541        | (
1542            TyKind::RigidTy(RigidTy::RawPtr(source, _)),
1543            TyKind::RigidTy(RigidTy::RawPtr(target, _)),
1544        ) => dyn_coercion(source, target),
1545        (
1546            TyKind::RigidTy(RigidTy::Adt(source_def, source_args)),
1547            TyKind::RigidTy(RigidTy::Adt(target_def, target_args)),
1548        ) if source_def == target_def => source_args
1549            .0
1550            .iter()
1551            .zip(&target_args.0)
1552            .filter_map(|(source, target)| Some((*source.ty()?, *target.ty()?)))
1553            .find_map(|(source, target)| dyn_coercion(source, target)),
1554        _ => None,
1555    }
1556}
1557
1558fn trait_method_implementations() -> HashMap<rustc_public::DefId, rustc_public::DefId> {
1559    rustc_public::all_trait_impls()
1560        .into_iter()
1561        .flat_map(|implementation| implementation.associated_items())
1562        .filter_map(|item| match item.container {
1563            AssocContainer::TraitImpl(trait_item) => {
1564                Some((item.def_id.def_id(), trait_item.def_id()))
1565            }
1566            AssocContainer::InherentImpl | AssocContainer::Trait => None,
1567        })
1568        .collect()
1569}
1570
1571fn resolve_called_instance(body: &Body, operand: &Operand) -> Option<Instance> {
1572    let function_type = operand.ty(body.locals()).ok()?;
1573    let function_kind = function_type.kind();
1574    let (definition, arguments) = function_kind.fn_def()?;
1575    Instance::resolve(definition, arguments).ok()
1576}
1577
1578fn resolve_dispatch_candidate(
1579    dispatch: Instance,
1580    trait_ref: &TraitRef,
1581    trait_method_implementations: &HashMap<rustc_public::DefId, rustc_public::DefId>,
1582) -> Option<Instance> {
1583    let InstanceKind::Virtual { idx } = dispatch.kind else {
1584        return None;
1585    };
1586    let VtblEntry::Method(candidate) = trait_ref.vtable_entry(idx)? else {
1587        return None;
1588    };
1589    let trait_method = dispatch.def.def_id();
1590    let candidate_method = candidate.def.def_id();
1591    (candidate_method == trait_method
1592        || trait_method_implementations.get(&candidate_method) == Some(&trait_method))
1593    .then_some(candidate)
1594}
1595
1596#[derive(Clone, Default)]
1597struct DynState {
1598    flows: HashMap<Place, DynFlow>,
1599    aliases: HashMap<Place, Vec<Place>>,
1600}
1601
1602fn analyze_body_dyn_flows(
1603    body: &Body,
1604    return_summaries: &HashMap<Instance, DynFlow>,
1605    local_instances: &HashSet<Instance>,
1606) -> BodyDynAnalysis {
1607    let mut result = BodyDynAnalysis {
1608        calls: vec![None; body.blocks.len()],
1609        return_flow: DynFlow::default(),
1610    };
1611    if body.blocks.is_empty() {
1612        return result;
1613    }
1614
1615    let mut entry = DynState::default();
1616    for (index, argument) in body.arg_locals().iter().enumerate() {
1617        if type_contains_dyn(argument.ty) {
1618            entry
1619                .flows
1620                .insert(Place::from(index + 1), DynFlow::parameter(index));
1621        }
1622    }
1623    let mut incoming = vec![None::<DynState>; body.blocks.len()];
1624    incoming[0] = Some(entry);
1625    let mut pending = VecDeque::from([0usize]);
1626
1627    let mut iterations = 0usize;
1628    let max_iterations = body
1629        .blocks
1630        .len()
1631        .saturating_mul(body.locals().len().saturating_add(1))
1632        .saturating_mul(16)
1633        .max(1);
1634    while let Some(block_index) = pending.pop_front() {
1635        iterations += 1;
1636        if iterations > max_iterations {
1637            degrade_dyn_analysis_to_unknown(body, &mut result);
1638            break;
1639        }
1640        let Some(mut state) = incoming[block_index].clone() else {
1641            continue;
1642        };
1643        let block = &body.blocks[block_index];
1644        for statement in &block.statements {
1645            transfer_dyn_statement(body, &mut state, statement);
1646        }
1647
1648        match &block.terminator.kind {
1649            TerminatorKind::Call {
1650                func,
1651                args,
1652                destination,
1653                target,
1654                ..
1655            } => {
1656                let argument_flows = args
1657                    .iter()
1658                    .map(|argument| dyn_flow_for_operand(body, &state, argument))
1659                    .collect::<Vec<_>>();
1660                merge_call_flow(
1661                    &mut result.calls[block_index],
1662                    &MirCallFlow {
1663                        arguments: argument_flows.clone(),
1664                    },
1665                );
1666
1667                let destination_flow = resolve_called_instance(body, func).map_or_else(
1668                    || unknown_if_dyn_place(body, destination, UnresolvedReason::ExternalCode),
1669                    |callee| {
1670                        if matches!(callee.kind, InstanceKind::Virtual { .. }) {
1671                            unknown_if_dyn_place(body, destination, UnresolvedReason::ExternalCode)
1672                        } else if let Some(summary) = return_summaries.get(&callee) {
1673                            let mut flow = resolve_symbolic_flow(summary, &argument_flows);
1674                            if !callee.def.krate().is_local {
1675                                flow.merge(&external_return_flow(
1676                                    body,
1677                                    args,
1678                                    destination,
1679                                    &argument_flows,
1680                                ));
1681                            }
1682                            flow
1683                        } else if local_instances.contains(&callee) {
1684                            DynFlow::default()
1685                        } else {
1686                            external_return_flow(body, args, destination, &argument_flows)
1687                        }
1688                    },
1689                );
1690
1691                apply_external_memory_effects(
1692                    body,
1693                    &mut state,
1694                    func,
1695                    args,
1696                    resolve_called_instance(body, func),
1697                    &argument_flows,
1698                );
1699
1700                for successor in block.terminator.successors() {
1701                    let mut outgoing = state.clone();
1702                    if Some(successor) == *target {
1703                        write_dyn_place(&mut outgoing, destination, destination_flow.clone());
1704                    }
1705                    if merge_dyn_state(&mut incoming[successor], &outgoing) {
1706                        pending.push_back(successor);
1707                    }
1708                }
1709            }
1710            TerminatorKind::Return => {
1711                let flow = read_dyn_place(&state, &Place::from(0));
1712                result.return_flow.merge(&flow);
1713            }
1714            _ => {
1715                for successor in block.terminator.successors() {
1716                    if merge_dyn_state(&mut incoming[successor], &state) {
1717                        pending.push_back(successor);
1718                    }
1719                }
1720            }
1721        }
1722    }
1723    result
1724}
1725
1726fn external_return_flow(
1727    body: &Body,
1728    arguments: &[Operand],
1729    destination: &Place,
1730    argument_flows: &[DynFlow],
1731) -> DynFlow {
1732    if !destination.ty(body.locals()).is_ok_and(type_contains_dyn) {
1733        return DynFlow::default();
1734    }
1735    let mut flow = DynFlow::unresolved(UnresolvedReason::ExternalCode);
1736    for (argument, argument_flow) in arguments.iter().zip(argument_flows) {
1737        if argument.ty(body.locals()).is_ok_and(type_contains_dyn) {
1738            flow.merge(argument_flow);
1739        }
1740    }
1741    flow
1742}
1743
1744/// Model memory that crosses a call boundary as an abstract region attached
1745/// to the referenced MIR place. This is type-directed: it does not name Vec,
1746/// Arc, HashMap, or individual methods. Opaque external behavior remains in
1747/// the unresolved-reason set, while concrete values that cross that same
1748/// boundary remain available as partial evidence instead of being lost.
1749fn apply_external_memory_effects(
1750    body: &Body,
1751    state: &mut DynState,
1752    _function: &Operand,
1753    arguments: &[Operand],
1754    resolved: Option<Instance>,
1755    argument_flows: &[DynFlow],
1756) {
1757    if resolved.is_some_and(|callee| callee.def.krate().is_local) {
1758        return;
1759    }
1760    let mut stored = DynFlow::default();
1761    for (argument, flow) in arguments.iter().zip(argument_flows) {
1762        let Ok(ty) = argument.ty(body.locals()) else {
1763            continue;
1764        };
1765        if !is_mutable_reference(ty) && type_contains_dyn(ty) {
1766            stored.merge(flow);
1767        }
1768    }
1769    if stored.is_empty() {
1770        return;
1771    }
1772    stored
1773        .unresolved_reasons
1774        .insert(UnresolvedReason::ExternalCode);
1775    for argument in arguments {
1776        let Ok(ty) = argument.ty(body.locals()) else {
1777            continue;
1778        };
1779        if !is_mutable_reference(ty) || !type_contains_dyn(ty) {
1780            continue;
1781        }
1782        let Some(place) = operand_place(argument) else {
1783            continue;
1784        };
1785        merge_dyn_place(state, place, &stored);
1786    }
1787}
1788
1789fn operand_place(operand: &Operand) -> Option<&Place> {
1790    match operand {
1791        Operand::Copy(place) | Operand::Move(place) => Some(place),
1792        Operand::Constant(_) | Operand::RuntimeChecks(_) => None,
1793    }
1794}
1795
1796fn is_mutable_reference(ty: Ty) -> bool {
1797    matches!(
1798        ty.kind(),
1799        TyKind::RigidTy(RigidTy::Ref(_, _, rustc_public::mir::Mutability::Mut))
1800            | TyKind::RigidTy(RigidTy::RawPtr(_, rustc_public::mir::Mutability::Mut))
1801    )
1802}
1803
1804fn degrade_dyn_analysis_to_unknown(body: &Body, analysis: &mut BodyDynAnalysis) {
1805    if type_contains_dyn(body.ret_local().ty) {
1806        analysis
1807            .return_flow
1808            .unresolved_reasons
1809            .insert(UnresolvedReason::AnalysisLimit);
1810    }
1811    for (block_index, block) in body.blocks.iter().enumerate() {
1812        let TerminatorKind::Call { args, .. } = &block.terminator.kind else {
1813            continue;
1814        };
1815        let call = analysis.calls[block_index].get_or_insert_with(|| MirCallFlow {
1816            arguments: vec![DynFlow::default(); args.len()],
1817        });
1818        call.arguments.resize_with(args.len(), DynFlow::default);
1819        for (argument, flow) in args.iter().zip(&mut call.arguments) {
1820            if argument.ty(body.locals()).is_ok_and(type_contains_dyn) {
1821                flow.unresolved_reasons
1822                    .insert(UnresolvedReason::AnalysisLimit);
1823            }
1824        }
1825    }
1826}
1827
1828fn transfer_dyn_statement(
1829    body: &Body,
1830    state: &mut DynState,
1831    statement: &rustc_public::mir::Statement,
1832) {
1833    let StatementKind::Assign(destination, value) = &statement.kind else {
1834        return;
1835    };
1836    match value {
1837        Rvalue::Ref(_, _, source) | Rvalue::AddressOf(_, source) | Rvalue::CopyForDeref(source) => {
1838            let targets = resolve_alias_places(state, source);
1839            state.aliases.insert(destination.clone(), targets);
1840        }
1841        Rvalue::Use(Operand::Copy(source) | Operand::Move(source), _)
1842        | Rvalue::Cast(_, Operand::Copy(source) | Operand::Move(source), _) => {
1843            if let Some(targets) = state.aliases.get(source).cloned() {
1844                state.aliases.insert(destination.clone(), targets);
1845            } else {
1846                state.aliases.remove(destination);
1847            }
1848        }
1849        _ => {
1850            state.aliases.remove(destination);
1851        }
1852    }
1853    let flow = dyn_flow_for_rvalue(body, state, value);
1854    write_dyn_place(state, destination, flow);
1855
1856    let Rvalue::Aggregate(kind, operands) = value else {
1857        return;
1858    };
1859    for (index, operand) in operands.iter().enumerate() {
1860        let Ok(operand_ty) = operand.ty(body.locals()) else {
1861            continue;
1862        };
1863        let projection = match kind {
1864            AggregateKind::Tuple | AggregateKind::Adt(..) => {
1865                Some(ProjectionElem::Field(index, operand_ty))
1866            }
1867            AggregateKind::Array(_) => Some(ProjectionElem::ConstantIndex {
1868                offset: index as u64,
1869                min_length: operands.len() as u64,
1870                from_end: false,
1871            }),
1872            AggregateKind::Closure(..)
1873            | AggregateKind::Coroutine(..)
1874            | AggregateKind::CoroutineClosure(..)
1875            | AggregateKind::RawPtr(..) => None,
1876        };
1877        let Some(projection) = projection else {
1878            continue;
1879        };
1880        let mut field = destination.clone();
1881        field.projection.push(projection);
1882        write_dyn_place(state, &field, dyn_flow_for_operand(body, state, operand));
1883    }
1884}
1885
1886fn dyn_flow_for_rvalue(body: &Body, state: &DynState, value: &Rvalue) -> DynFlow {
1887    let mut flow = match value {
1888        Rvalue::Use(operand, _) | Rvalue::Repeat(operand, _) => {
1889            dyn_flow_for_operand(body, state, operand)
1890        }
1891        Rvalue::Cast(CastKind::PointerCoercion(PointerCoercion::Unsize), operand, target_ty) => {
1892            operand
1893                .ty(body.locals())
1894                .ok()
1895                .and_then(|source_ty| dyn_coercion(source_ty, *target_ty))
1896                .map(|(concrete_ty, principal)| {
1897                    DynFlow::concrete(TraitRef::new(
1898                        principal.def_id,
1899                        concrete_ty,
1900                        &principal.generic_args,
1901                    ))
1902                })
1903                .unwrap_or_else(|| dyn_flow_for_operand(body, state, operand))
1904        }
1905        Rvalue::Cast(_, operand, _) => dyn_flow_for_operand(body, state, operand),
1906        Rvalue::Ref(_, _, place) | Rvalue::AddressOf(_, place) | Rvalue::CopyForDeref(place) => {
1907            read_dyn_place(state, place)
1908        }
1909        Rvalue::Aggregate(_, operands) => {
1910            let mut aggregate = DynFlow::default();
1911            for operand in operands {
1912                aggregate.merge(&dyn_flow_for_operand(body, state, operand));
1913            }
1914            aggregate
1915        }
1916        Rvalue::BinaryOp(_, left, right) | Rvalue::CheckedBinaryOp(_, left, right) => {
1917            let mut combined = dyn_flow_for_operand(body, state, left);
1918            combined.merge(&dyn_flow_for_operand(body, state, right));
1919            combined
1920        }
1921        Rvalue::UnaryOp(_, operand) => dyn_flow_for_operand(body, state, operand),
1922        Rvalue::ThreadLocalRef(_) | Rvalue::Discriminant(_) | Rvalue::Len(_) => DynFlow::default(),
1923    };
1924    if flow.is_empty() && value.ty(body.locals()).is_ok_and(type_contains_dyn) {
1925        flow.unresolved_reasons
1926            .insert(UnresolvedReason::AnalysisLimit);
1927    }
1928    flow
1929}
1930
1931fn dyn_flow_for_operand(body: &Body, state: &DynState, operand: &Operand) -> DynFlow {
1932    let mut flow = match operand {
1933        Operand::Copy(place) | Operand::Move(place) => read_dyn_place(state, place),
1934        Operand::Constant(_) | Operand::RuntimeChecks(_) => DynFlow::default(),
1935    };
1936    if flow.is_empty() && operand.ty(body.locals()).is_ok_and(type_contains_dyn) {
1937        flow.unresolved_reasons
1938            .insert(UnresolvedReason::ExternalMemory);
1939    }
1940    flow
1941}
1942
1943fn unknown_if_dyn_place(body: &Body, place: &Place, reason: UnresolvedReason) -> DynFlow {
1944    place
1945        .ty(body.locals())
1946        .ok()
1947        .filter(|ty| type_contains_dyn(*ty))
1948        .map_or_else(DynFlow::default, |_| DynFlow::unresolved(reason))
1949}
1950
1951fn type_contains_dyn(ty: Ty) -> bool {
1952    ty.to_string().contains("dyn ")
1953}
1954
1955fn collect_adt_constructor_names(ty: Ty, names: &mut HashSet<String>) {
1956    match ty.kind() {
1957        TyKind::RigidTy(RigidTy::Adt(definition, arguments)) => {
1958            names.extend(
1959                definition
1960                    .variants()
1961                    .into_iter()
1962                    .map(|variant| variant.name().to_string()),
1963            );
1964            for argument in arguments.0 {
1965                if let Some(ty) = argument.ty() {
1966                    collect_adt_constructor_names(*ty, names);
1967                }
1968            }
1969        }
1970        TyKind::RigidTy(
1971            RigidTy::FnDef(_, arguments)
1972            | RigidTy::Closure(_, arguments)
1973            | RigidTy::Coroutine(_, arguments)
1974            | RigidTy::CoroutineClosure(_, arguments)
1975            | RigidTy::CoroutineWitness(_, arguments),
1976        ) => {
1977            for argument in arguments.0 {
1978                if let Some(ty) = argument.ty() {
1979                    collect_adt_constructor_names(*ty, names);
1980                }
1981            }
1982        }
1983        TyKind::RigidTy(
1984            RigidTy::Array(ty, _)
1985            | RigidTy::Slice(ty)
1986            | RigidTy::RawPtr(ty, _)
1987            | RigidTy::Ref(_, ty, _),
1988        ) => collect_adt_constructor_names(ty, names),
1989        TyKind::RigidTy(RigidTy::Tuple(types)) => {
1990            for ty in types {
1991                collect_adt_constructor_names(ty, names);
1992            }
1993        }
1994        _ => {}
1995    }
1996}
1997
1998fn read_dyn_place(state: &DynState, place: &Place) -> DynFlow {
1999    if let Some(flow) = state.flows.get(place) {
2000        return flow.clone();
2001    }
2002    let alias_targets = resolve_alias_places(state, place);
2003    if alias_targets.len() != 1 || alias_targets.first() != Some(place) {
2004        let mut aliased = DynFlow::default();
2005        for target in alias_targets {
2006            if target != *place {
2007                aliased.merge(&read_dyn_place_without_alias(state, &target));
2008            }
2009        }
2010        if !aliased.is_empty() {
2011            return aliased;
2012        }
2013    }
2014    read_dyn_place_without_alias(state, place)
2015}
2016
2017fn read_dyn_place_without_alias(state: &DynState, place: &Place) -> DynFlow {
2018    if let Some(flow) = state.flows.get(place) {
2019        return flow.clone();
2020    }
2021    let mut ancestor = place.clone();
2022    while ancestor.projection.pop().is_some() {
2023        if let Some(flow) = state.flows.get(&ancestor) {
2024            let suffix = &place.projection[ancestor.projection.len()..];
2025            return if suffix
2026                .iter()
2027                .all(|projection| matches!(projection, ProjectionElem::Deref))
2028            {
2029                flow.clone()
2030            } else {
2031                // Locally built aggregates have explicit field entries above.
2032                // A whole-value-only region came from opaque memory (for
2033                // example an allocator-backed container), so retain its
2034                // candidates but mark the projection as partial rather than
2035                // assigning them as exact field provenance.
2036                let mut projected = flow.clone();
2037                projected
2038                    .unresolved_reasons
2039                    .insert(UnresolvedReason::ExternalMemory);
2040                projected
2041            };
2042        }
2043    }
2044    let mut result = DynFlow::default();
2045    for (candidate, flow) in &state.flows {
2046        if place_is_prefix(place, candidate) {
2047            result.merge(flow);
2048        }
2049    }
2050    result
2051}
2052
2053fn write_dyn_place(state: &mut DynState, place: &Place, flow: DynFlow) {
2054    let targets = resolve_alias_places(state, place);
2055    let targets = if targets.len() == 1 && targets[0] == *place {
2056        vec![place.clone()]
2057    } else {
2058        targets
2059    };
2060    for target in targets {
2061        state
2062            .flows
2063            .retain(|candidate, _| !place_is_prefix(&target, candidate));
2064        if !flow.is_empty() {
2065            state.flows.insert(target, flow.clone());
2066        }
2067    }
2068}
2069
2070fn merge_dyn_place(state: &mut DynState, place: &Place, flow: &DynFlow) {
2071    if flow.is_empty() {
2072        return;
2073    }
2074    let targets = resolve_alias_places(state, place);
2075    for target in targets {
2076        state.flows.entry(target).or_default().merge(flow);
2077    }
2078}
2079
2080fn resolve_alias_places(state: &DynState, place: &Place) -> Vec<Place> {
2081    if let Some(targets) = state.aliases.get(place) {
2082        return targets.clone();
2083    }
2084    let base = Place::from(place.local);
2085    let Some(targets) = state.aliases.get(&base) else {
2086        return vec![place.clone()];
2087    };
2088    let mut suffix = place.projection.as_slice();
2089    if suffix
2090        .first()
2091        .is_some_and(|projection| matches!(projection, ProjectionElem::Deref))
2092    {
2093        suffix = &suffix[1..];
2094    }
2095    targets
2096        .iter()
2097        .cloned()
2098        .map(|mut target| {
2099            target.projection.extend_from_slice(suffix);
2100            target
2101        })
2102        .collect()
2103}
2104
2105fn place_is_prefix(prefix: &Place, place: &Place) -> bool {
2106    prefix.local == place.local
2107        && prefix.projection.len() <= place.projection.len()
2108        && place.projection.starts_with(&prefix.projection)
2109}
2110
2111fn merge_dyn_state(destination: &mut Option<DynState>, source: &DynState) -> bool {
2112    let Some(destination) = destination else {
2113        *destination = Some(source.clone());
2114        return true;
2115    };
2116    let mut changed = false;
2117    for (place, flow) in &source.flows {
2118        changed |= destination
2119            .flows
2120            .entry(place.clone())
2121            .or_default()
2122            .merge(flow);
2123    }
2124    for (place, targets) in &source.aliases {
2125        let destination_targets = destination.aliases.entry(place.clone()).or_default();
2126        for target in targets {
2127            if !destination_targets.contains(target) {
2128                destination_targets.push(target.clone());
2129                changed = true;
2130            }
2131        }
2132    }
2133    changed
2134}
2135
2136fn merge_call_flow(destination: &mut Option<MirCallFlow>, source: &MirCallFlow) {
2137    let Some(destination) = destination else {
2138        *destination = Some(source.clone());
2139        return;
2140    };
2141    if destination.arguments.len() < source.arguments.len() {
2142        destination
2143            .arguments
2144            .resize_with(source.arguments.len(), DynFlow::default);
2145    }
2146    for (destination, source) in destination.arguments.iter_mut().zip(&source.arguments) {
2147        destination.merge(source);
2148    }
2149}
2150
2151fn resolve_symbolic_flow(flow: &DynFlow, arguments: &[DynFlow]) -> DynFlow {
2152    let mut resolved = DynFlow {
2153        concrete: flow.concrete.clone(),
2154        parameters: Vec::new(),
2155        unresolved_reasons: flow.unresolved_reasons.clone(),
2156    };
2157    for parameter in &flow.parameters {
2158        if let Some(argument) = arguments.get(*parameter) {
2159            resolved.merge(argument);
2160        } else {
2161            resolved
2162                .unresolved_reasons
2163                .insert(UnresolvedReason::AnalysisLimit);
2164        }
2165    }
2166    resolved
2167}
2168
2169fn specialize_semantic_functions(
2170    raw_functions: &[RawSemanticFunction],
2171    observed_vtables: &[TraitRef],
2172    trait_method_implementations: &HashMap<rustc_public::DefId, rustc_public::DefId>,
2173    crate_name: &str,
2174) -> Vec<SemanticFunction> {
2175    let index = raw_functions
2176        .iter()
2177        .enumerate()
2178        .map(|(index, function)| (function.key.clone(), index))
2179        .collect::<HashMap<_, _>>();
2180    let mut incoming = vec![0usize; raw_functions.len()];
2181    for function in raw_functions {
2182        for call in &function.calls {
2183            match &call.target {
2184                RawSemanticCallTarget::Direct { key, .. } => {
2185                    if let Some(target) = index.get(key) {
2186                        incoming[*target] += 1;
2187                    }
2188                }
2189                RawSemanticCallTarget::Dynamic { dispatch, .. } => {
2190                    for trait_ref in observed_vtables {
2191                        let Some(candidate) = resolve_dispatch_candidate(
2192                            *dispatch,
2193                            trait_ref,
2194                            trait_method_implementations,
2195                        ) else {
2196                            continue;
2197                        };
2198                        if let Some(target) = index.get(&normalize_instance_key(&candidate.name()))
2199                        {
2200                            incoming[*target] += 1;
2201                        }
2202                    }
2203                }
2204                RawSemanticCallTarget::Indirect { .. } | RawSemanticCallTarget::Unresolved => {}
2205            }
2206        }
2207    }
2208
2209    let mut pending = VecDeque::<(usize, Vec<ResolvedDynFlow>)>::new();
2210    for (function, count) in incoming.iter().enumerate() {
2211        if *count == 0 {
2212            pending.push_back((function, root_dyn_context(&raw_functions[function])));
2213        }
2214    }
2215    let mut visited = HashSet::new();
2216    let mut covered_raw = HashSet::new();
2217    let mut functions = Vec::new();
2218
2219    loop {
2220        while let Some((raw_index, context)) = pending.pop_front() {
2221            let raw = &raw_functions[raw_index];
2222            let function_key = specialized_function_key(raw, &context);
2223            // Multiple compiler instances can normalize to the same display
2224            // key. Even when the emitted semantic function was already seen,
2225            // this raw instance is consumed; otherwise the disconnected-root
2226            // fallback below selects it forever.
2227            covered_raw.insert(raw_index);
2228            if !visited.insert(function_key.clone()) {
2229                continue;
2230            }
2231            let mut calls = Vec::new();
2232
2233            for raw_call in &raw.calls {
2234                let target = match &raw_call.target {
2235                    RawSemanticCallTarget::Direct { key, display } => {
2236                        let specialized = index.get(key).map_or_else(
2237                            || key.clone(),
2238                            |callee_index| {
2239                                let callee = &raw_functions[*callee_index];
2240                                let callee_context =
2241                                    call_dyn_context(callee, &raw_call.argument_flows, &context);
2242                                let key = specialized_function_key(callee, &callee_context);
2243                                pending.push_back((*callee_index, callee_context));
2244                                key
2245                            },
2246                        );
2247                        SemanticCallTarget::Direct {
2248                            key: specialized,
2249                            display: display.clone(),
2250                        }
2251                    }
2252                    RawSemanticCallTarget::Dynamic {
2253                        dispatch,
2254                        key,
2255                        display,
2256                        receiver,
2257                    } => {
2258                        let receiver_flow = semantic_dyn_flow(receiver, crate_name);
2259                        let receiver = resolve_context_flow(receiver, &context);
2260                        let mut candidates = Vec::new();
2261                        for trait_ref in &receiver.concrete {
2262                            let Some(candidate) = resolve_dispatch_candidate(
2263                                *dispatch,
2264                                trait_ref,
2265                                trait_method_implementations,
2266                            ) else {
2267                                continue;
2268                            };
2269                            let candidate_name = candidate.name();
2270                            let raw_candidate_key = normalize_instance_key(&candidate_name);
2271                            let candidate_key = index.get(&raw_candidate_key).map_or_else(
2272                                || raw_candidate_key.clone(),
2273                                |candidate_index| {
2274                                    let candidate_function = &raw_functions[*candidate_index];
2275                                    let candidate_context = call_dyn_context(
2276                                        candidate_function,
2277                                        &raw_call.argument_flows,
2278                                        &context,
2279                                    );
2280                                    let key = specialized_function_key(
2281                                        candidate_function,
2282                                        &candidate_context,
2283                                    );
2284                                    pending.push_back((*candidate_index, candidate_context));
2285                                    key
2286                                },
2287                            );
2288                            if !candidates
2289                                .iter()
2290                                .any(|existing: &SemanticDispatchCandidate| {
2291                                    existing.key == candidate_key
2292                                })
2293                            {
2294                                candidates.push(SemanticDispatchCandidate {
2295                                    key: candidate_key,
2296                                    display: normalize_instance_display(
2297                                        &candidate_name,
2298                                        crate_name,
2299                                    ),
2300                                });
2301                            }
2302                        }
2303                        candidates.sort_by(|left, right| left.key.cmp(&right.key));
2304                        SemanticCallTarget::Dynamic {
2305                            key: key.clone(),
2306                            display: display.clone(),
2307                            candidates,
2308                            open: !receiver.unresolved_reasons.is_empty(),
2309                            unresolved_reasons: receiver.unresolved_reasons,
2310                            receiver_flow,
2311                        }
2312                    }
2313                    RawSemanticCallTarget::Indirect { signature } => SemanticCallTarget::Indirect {
2314                        signature: signature.clone(),
2315                    },
2316                    RawSemanticCallTarget::Unresolved => SemanticCallTarget::Unresolved,
2317                };
2318                calls.push(SemanticCall {
2319                    target,
2320                    definition_name: raw_call.definition_name.clone(),
2321                    span: raw_call.span.clone(),
2322                    argument_types: raw_call.argument_types.clone(),
2323                    argument_flows: raw_call
2324                        .argument_flows
2325                        .iter()
2326                        .map(|flow| semantic_dyn_flow(flow, crate_name))
2327                        .collect(),
2328                });
2329            }
2330
2331            functions.push(SemanticFunction {
2332                base_key: raw.key.clone(),
2333                key: function_key,
2334                display: raw.display.clone(),
2335                body_span: raw.body_span.clone(),
2336                calls,
2337                constructor_spans: raw.constructor_spans.clone(),
2338                constructor_names: raw.constructor_names.clone(),
2339                parameter_types: raw.parameter_types.clone(),
2340            });
2341        }
2342
2343        let Some(uncovered) = (0..raw_functions.len()).find(|index| !covered_raw.contains(index))
2344        else {
2345            break;
2346        };
2347        pending.push_back((uncovered, root_dyn_context(&raw_functions[uncovered])));
2348    }
2349    functions
2350}
2351
2352fn root_dyn_context(function: &RawSemanticFunction) -> Vec<ResolvedDynFlow> {
2353    function
2354        .parameter_types
2355        .iter()
2356        .map(|parameter| ResolvedDynFlow {
2357            concrete: Vec::new(),
2358            unresolved_reasons: parameter
2359                .contains("dyn ")
2360                .then_some(UnresolvedReason::OpaqueInput)
2361                .into_iter()
2362                .collect(),
2363        })
2364        .collect()
2365}
2366
2367fn call_dyn_context(
2368    callee: &RawSemanticFunction,
2369    arguments: &[DynFlow],
2370    caller_context: &[ResolvedDynFlow],
2371) -> Vec<ResolvedDynFlow> {
2372    callee
2373        .parameter_types
2374        .iter()
2375        .enumerate()
2376        .map(|(index, parameter)| {
2377            if !parameter.contains("dyn ") {
2378                return ResolvedDynFlow::default();
2379            }
2380            arguments.get(index).map_or_else(
2381                || ResolvedDynFlow {
2382                    concrete: Vec::new(),
2383                    unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
2384                },
2385                |flow| {
2386                    let mut resolved = resolve_context_flow(flow, caller_context);
2387                    if resolved.concrete.is_empty() && resolved.unresolved_reasons.is_empty() {
2388                        resolved
2389                            .unresolved_reasons
2390                            .insert(UnresolvedReason::AnalysisLimit);
2391                    }
2392                    resolved
2393                },
2394            )
2395        })
2396        .collect()
2397}
2398
2399fn resolve_context_flow(flow: &DynFlow, context: &[ResolvedDynFlow]) -> ResolvedDynFlow {
2400    let mut resolved = ResolvedDynFlow {
2401        concrete: flow.concrete.clone(),
2402        unresolved_reasons: flow.unresolved_reasons.clone(),
2403    };
2404    for parameter in &flow.parameters {
2405        if let Some(argument) = context.get(*parameter) {
2406            resolved.merge(argument);
2407        } else {
2408            resolved
2409                .unresolved_reasons
2410                .insert(UnresolvedReason::AnalysisLimit);
2411        }
2412    }
2413    resolved
2414}
2415
2416fn semantic_dyn_flow(flow: &DynFlow, crate_name: &str) -> SemanticDynFlow {
2417    let mut concrete_types = flow
2418        .concrete
2419        .iter()
2420        .map(|trait_ref| normalize_type_display(&trait_ref.self_ty().to_string(), crate_name))
2421        .collect::<Vec<_>>();
2422    concrete_types.sort();
2423    concrete_types.dedup();
2424    SemanticDynFlow {
2425        concrete_types,
2426        parameters: flow.parameters.clone(),
2427        unresolved_reasons: flow.unresolved_reasons.clone(),
2428    }
2429}
2430
2431fn specialized_function_key(function: &RawSemanticFunction, context: &[ResolvedDynFlow]) -> String {
2432    let bindings = context
2433        .iter()
2434        .enumerate()
2435        .filter(|(index, value)| {
2436            function
2437                .parameter_types
2438                .get(*index)
2439                .is_some_and(|parameter| parameter.contains("dyn "))
2440                || !value.concrete.is_empty()
2441                || !value.unresolved_reasons.is_empty()
2442        })
2443        .map(|(index, value)| {
2444            let mut candidates = value
2445                .concrete
2446                .iter()
2447                .map(|trait_ref| trait_ref.self_ty().to_string())
2448                .collect::<Vec<_>>();
2449            candidates.sort();
2450            for reason in &value.unresolved_reasons {
2451                candidates.push(format!("?{reason}"));
2452            }
2453            format!("{index}={}", candidates.join("|"))
2454        })
2455        .collect::<Vec<_>>();
2456    if bindings.is_empty() {
2457        function.key.clone()
2458    } else {
2459        format!("{}#ctx[dyn:{}]", function.key, bindings.join(","))
2460    }
2461}
2462
2463#[derive(Clone, Debug, Default, Eq, PartialEq)]
2464struct NamedDynFlow {
2465    concrete_types: BTreeSet<String>,
2466    unresolved_reasons: BTreeSet<UnresolvedReason>,
2467}
2468
2469fn respecialize_merged_semantic_program(program: &mut SemanticProgram) {
2470    if program.functions.is_empty() {
2471        return;
2472    }
2473    let mut templates = BTreeMap::<String, SemanticFunction>::new();
2474    for function in &program.functions {
2475        templates
2476            .entry(function.base_key.clone())
2477            .and_modify(|existing| {
2478                if semantic_template_score(function) > semantic_template_score(existing) {
2479                    *existing = function.clone();
2480                }
2481            })
2482            .or_insert_with(|| function.clone());
2483    }
2484    let bases = templates.keys().cloned().collect::<HashSet<_>>();
2485    let mut incoming = bases
2486        .iter()
2487        .cloned()
2488        .map(|key| (key, 0usize))
2489        .collect::<HashMap<_, _>>();
2490    for function in templates.values() {
2491        for call in &function.calls {
2492            let SemanticCallTarget::Direct { key, .. } = &call.target else {
2493                continue;
2494            };
2495            if let Some(base) = semantic_target_base(key, &bases) {
2496                *incoming.entry(base).or_default() += 1;
2497            }
2498        }
2499    }
2500
2501    let mut pending = incoming
2502        .iter()
2503        .filter(|(_, count)| **count == 0)
2504        .filter_map(|(base, _)| {
2505            templates
2506                .get(base)
2507                .map(|function| (base.clone(), root_named_context(function)))
2508        })
2509        .collect::<VecDeque<_>>();
2510    let mut visited = HashSet::new();
2511    let mut covered = HashSet::new();
2512    let mut specialized = Vec::new();
2513
2514    loop {
2515        while let Some((base, context)) = pending.pop_front() {
2516            let Some(template) = templates.get(&base) else {
2517                continue;
2518            };
2519            covered.insert(base.clone());
2520            let key = named_specialized_key(template, &context);
2521            if !visited.insert(key.clone()) {
2522                continue;
2523            }
2524            let mut function = template.clone();
2525            function.key = key;
2526            for call in &mut function.calls {
2527                match &mut call.target {
2528                    SemanticCallTarget::Direct { key, .. } => {
2529                        let Some(target_base) = semantic_target_base(key, &bases) else {
2530                            continue;
2531                        };
2532                        let Some(target) = templates.get(&target_base) else {
2533                            continue;
2534                        };
2535                        let target_context =
2536                            named_call_context(target, &call.argument_flows, &context);
2537                        *key = named_specialized_key(target, &target_context);
2538                        pending.push_back((target_base, target_context));
2539                    }
2540                    SemanticCallTarget::Dynamic {
2541                        key,
2542                        candidates,
2543                        open,
2544                        unresolved_reasons,
2545                        receiver_flow,
2546                        ..
2547                    } => {
2548                        let mut receiver = resolve_named_flow(receiver_flow, &context);
2549                        if let Some(argument_receiver) = call.argument_flows.first() {
2550                            merge_named_flow(
2551                                &mut receiver,
2552                                &resolve_named_flow(argument_receiver, &context),
2553                            );
2554                        }
2555                        *candidates = receiver
2556                            .concrete_types
2557                            .iter()
2558                            .filter_map(|concrete| {
2559                                let candidate =
2560                                    named_dispatch_candidate(concrete, key, templates.values())?;
2561                                let candidate_context =
2562                                    named_call_context(candidate, &call.argument_flows, &context);
2563                                let candidate_key =
2564                                    named_specialized_key(candidate, &candidate_context);
2565                                pending.push_back((candidate.base_key.clone(), candidate_context));
2566                                Some(SemanticDispatchCandidate {
2567                                    key: candidate_key,
2568                                    display: candidate.display.clone(),
2569                                })
2570                            })
2571                            .collect();
2572                        candidates.sort_by(|left, right| left.key.cmp(&right.key));
2573                        candidates.dedup_by(|left, right| left.key == right.key);
2574                        *unresolved_reasons = receiver.unresolved_reasons;
2575                        *open = !unresolved_reasons.is_empty();
2576                    }
2577                    SemanticCallTarget::Indirect { .. } | SemanticCallTarget::Unresolved => {}
2578                }
2579            }
2580            specialized.push(function);
2581        }
2582
2583        let Some(uncovered) = templates
2584            .keys()
2585            .find(|base| !covered.contains(*base))
2586            .cloned()
2587        else {
2588            break;
2589        };
2590        let context = root_named_context(&templates[&uncovered]);
2591        pending.push_back((uncovered, context));
2592    }
2593    specialized.sort_by(|left, right| left.key.cmp(&right.key));
2594    program.functions = specialized;
2595}
2596
2597fn merge_named_flow(destination: &mut NamedDynFlow, source: &NamedDynFlow) {
2598    destination
2599        .concrete_types
2600        .extend(source.concrete_types.iter().cloned());
2601    destination
2602        .unresolved_reasons
2603        .extend(source.unresolved_reasons.iter().cloned());
2604}
2605
2606fn semantic_template_score(function: &SemanticFunction) -> usize {
2607    let symbolic = function
2608        .calls
2609        .iter()
2610        .map(|call| {
2611            let arguments = call
2612                .argument_flows
2613                .iter()
2614                .map(|flow| flow.parameters.len())
2615                .sum::<usize>();
2616            let receiver = match &call.target {
2617                SemanticCallTarget::Dynamic { receiver_flow, .. } => receiver_flow.parameters.len(),
2618                _ => 0,
2619            };
2620            arguments + receiver
2621        })
2622        .sum::<usize>();
2623    let exact_base = usize::from(function.key == function.base_key);
2624    symbolic.saturating_mul(10) + exact_base
2625}
2626
2627fn semantic_target_base(key: &str, bases: &HashSet<String>) -> Option<String> {
2628    if bases.contains(key) {
2629        return Some(key.to_owned());
2630    }
2631    bases
2632        .iter()
2633        .filter(|base| key.starts_with(base.as_str()))
2634        .max_by_key(|base| base.len())
2635        .cloned()
2636}
2637
2638fn root_named_context(function: &SemanticFunction) -> Vec<NamedDynFlow> {
2639    function
2640        .parameter_types
2641        .iter()
2642        .map(|parameter| NamedDynFlow {
2643            concrete_types: BTreeSet::new(),
2644            unresolved_reasons: parameter
2645                .contains("dyn ")
2646                .then_some(UnresolvedReason::OpaqueInput)
2647                .into_iter()
2648                .collect(),
2649        })
2650        .collect()
2651}
2652
2653fn named_call_context(
2654    callee: &SemanticFunction,
2655    arguments: &[SemanticDynFlow],
2656    caller_context: &[NamedDynFlow],
2657) -> Vec<NamedDynFlow> {
2658    callee
2659        .parameter_types
2660        .iter()
2661        .enumerate()
2662        .map(|(index, parameter)| {
2663            if !parameter.contains("dyn ") {
2664                return NamedDynFlow::default();
2665            }
2666            arguments.get(index).map_or_else(
2667                || NamedDynFlow {
2668                    concrete_types: BTreeSet::new(),
2669                    unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
2670                },
2671                |flow| resolve_named_flow(flow, caller_context),
2672            )
2673        })
2674        .collect()
2675}
2676
2677fn resolve_named_flow(flow: &SemanticDynFlow, context: &[NamedDynFlow]) -> NamedDynFlow {
2678    let mut resolved = NamedDynFlow {
2679        concrete_types: flow.concrete_types.iter().cloned().collect(),
2680        unresolved_reasons: flow.unresolved_reasons.clone(),
2681    };
2682    for parameter in &flow.parameters {
2683        if let Some(value) = context.get(*parameter) {
2684            resolved
2685                .concrete_types
2686                .extend(value.concrete_types.iter().cloned());
2687            resolved
2688                .unresolved_reasons
2689                .extend(value.unresolved_reasons.iter().cloned());
2690        } else {
2691            resolved
2692                .unresolved_reasons
2693                .insert(UnresolvedReason::AnalysisLimit);
2694        }
2695    }
2696    resolved
2697}
2698
2699fn named_specialized_key(function: &SemanticFunction, context: &[NamedDynFlow]) -> String {
2700    let bindings = context
2701        .iter()
2702        .enumerate()
2703        .filter(|(index, flow)| {
2704            function
2705                .parameter_types
2706                .get(*index)
2707                .is_some_and(|parameter| parameter.contains("dyn "))
2708                || !flow.concrete_types.is_empty()
2709                || !flow.unresolved_reasons.is_empty()
2710        })
2711        .map(|(index, flow)| {
2712            let mut values = flow.concrete_types.iter().cloned().collect::<Vec<_>>();
2713            values.extend(
2714                flow.unresolved_reasons
2715                    .iter()
2716                    .map(|reason| format!("?{reason}")),
2717            );
2718            format!("{index}={}", values.join("|"))
2719        })
2720        .collect::<Vec<_>>();
2721    if bindings.is_empty() {
2722        function.base_key.clone()
2723    } else {
2724        format!("{}#ctx[dyn:{}]", function.base_key, bindings.join(","))
2725    }
2726}
2727
2728fn named_dispatch_candidate<'a>(
2729    concrete: &str,
2730    dispatch: &str,
2731    functions: impl Iterator<Item = &'a SemanticFunction>,
2732) -> Option<&'a SemanticFunction> {
2733    let method = dispatch
2734        .rsplit("::")
2735        .next()
2736        .unwrap_or(dispatch)
2737        .trim_end_matches('>');
2738    let concrete = concrete
2739        .rsplit("::")
2740        .next()
2741        .unwrap_or(concrete)
2742        .trim_matches(['&', ' ', '<', '>']);
2743    functions
2744        .filter(|function| {
2745            function.display.ends_with(&format!("{concrete}::{method}"))
2746                || (function.display.ends_with(&format!("::{method}"))
2747                    && function.base_key.contains(concrete)
2748                    && function.base_key.contains(" as "))
2749        })
2750        .min_by_key(|function| function.base_key.clone())
2751}
2752
2753fn merge_semantic_program(syntax: FileAnalysis, mut semantic: SemanticProgram) -> FileAnalysis {
2754    let mut analysis = FileAnalysis {
2755        source_files: syntax.source_files.clone(),
2756        ..FileAnalysis::default()
2757    };
2758    let file_identities = source_file_identities(&syntax, &semantic);
2759    canonicalize_semantic_closures(&syntax, &mut semantic, &file_identities);
2760    let local_function_keys = semantic
2761        .functions
2762        .iter()
2763        .map(|function| function.key.clone())
2764        .collect::<HashSet<_>>();
2765    let semantic_closure_keys = semantic_closure_keys(&syntax, &semantic, &file_identities);
2766    let mut required_syntax_closures = HashSet::<SymbolId>::new();
2767    let mut matched_syntax_functions = HashSet::<SymbolId>::new();
2768    let generic_syntax_functions = syntax
2769        .facts
2770        .iter()
2771        .filter(|fact| fact.kind == "generic-parameter")
2772        .map(|fact| fact.subject.clone())
2773        .collect::<HashSet<_>>();
2774    let mut syntax_functions_by_file = HashMap::<PathBuf, Vec<usize>>::new();
2775    for (index, function) in syntax.functions.iter().enumerate() {
2776        syntax_functions_by_file
2777            .entry(source_file_identity(&file_identities, &function.span.file).to_path_buf())
2778            .or_default()
2779            .push(index);
2780    }
2781    let mut facts_by_subject = HashMap::<&SymbolId, Vec<&LanguageFact>>::new();
2782    for fact in &syntax.facts {
2783        facts_by_subject
2784            .entry(&fact.subject)
2785            .or_default()
2786            .push(fact);
2787    }
2788
2789    for semantic_function in semantic.functions {
2790        let function_file =
2791            source_file_identity(&file_identities, &semantic_function.body_span.file);
2792        let Some(candidate_indices) = syntax_functions_by_file.get(function_file) else {
2793            continue;
2794        };
2795        let Some(template) = best_function_template(
2796            &syntax.functions,
2797            candidate_indices,
2798            &semantic_function.body_span,
2799        ) else {
2800            // Compiler-generated functions (drop glue, coroutine shims, etc.)
2801            // are intentionally omitted until they have explicit source syntax.
2802            continue;
2803        };
2804        matched_syntax_functions.insert(template.id.clone());
2805        let id = semantic_symbol(semantic_function.key);
2806        let mut claimed_calls = HashSet::new();
2807        let calls = template
2808            .calls
2809            .iter()
2810            .filter_map(|call| {
2811                let Some(match_index) = best_semantic_call(
2812                    call,
2813                    &semantic_function.calls,
2814                    &claimed_calls,
2815                    &file_identities,
2816                ) else {
2817                    let is_constructor = semantic_function
2818                        .constructor_spans
2819                        .iter()
2820                        .any(|span| span_coordinates_equal(&call.span, span))
2821                        || call_syntax_name(call).is_some_and(|name| {
2822                            semantic_function
2823                                .constructor_names
2824                                .iter()
2825                                .any(|constructor| constructor == name)
2826                        });
2827                    if is_constructor {
2828                        return None;
2829                    }
2830                    let mut unresolved = call.clone();
2831                    if let Some(closure) =
2832                        source_closure_for_call(template, call, &syntax.functions)
2833                    {
2834                        unresolved.target = semantic_closure_keys
2835                            .get(&closure.id)
2836                            .cloned()
2837                            .map(semantic_symbol)
2838                            .map_or_else(
2839                                || {
2840                                    required_syntax_closures.insert(closure.id.clone());
2841                                    CallTarget::Direct(closure.id.clone())
2842                                },
2843                                CallTarget::Direct,
2844                            );
2845                        unresolved.label =
2846                            replace_label_callee(&unresolved.label, closure_label_callee(closure));
2847                    }
2848                    return Some(unresolved);
2849                };
2850                let mut resolved = call.clone();
2851                claimed_calls.insert(match_index);
2852                let semantic_call = &semantic_function.calls[match_index];
2853                match &semantic_call.target {
2854                    SemanticCallTarget::Direct { key, display } => {
2855                        resolved.target = CallTarget::Direct(semantic_symbol(key.clone()));
2856                        if local_function_keys.contains(key) {
2857                            resolved.label = replace_label_callee(&resolved.label, display);
2858                        }
2859                    }
2860                    SemanticCallTarget::Dynamic {
2861                        key,
2862                        display,
2863                        candidates,
2864                        open,
2865                        unresolved_reasons,
2866                        ..
2867                    } => {
2868                        resolved.target = CallTarget::Dynamic {
2869                            dispatch: semantic_symbol(key.clone()),
2870                            candidates: candidates
2871                                .iter()
2872                                .map(|candidate| DispatchCandidate {
2873                                    target: semantic_symbol(candidate.key.clone()),
2874                                    label: replace_label_callee(
2875                                        &resolved.label,
2876                                        &candidate.display,
2877                                    ),
2878                                })
2879                                .collect(),
2880                            resolution: if candidates.is_empty() {
2881                                DispatchResolution::Unresolved
2882                            } else if *open {
2883                                DispatchResolution::Partial
2884                            } else {
2885                                DispatchResolution::Complete
2886                            },
2887                            evidence: DispatchEvidence::ExactFlow,
2888                            unresolved_reasons: if unresolved_reasons.is_empty()
2889                                && candidates.is_empty()
2890                            {
2891                                [UnresolvedReason::AnalysisLimit].into_iter().collect()
2892                            } else {
2893                                unresolved_reasons.clone()
2894                            },
2895                        };
2896                        resolved.label = replace_label_callee(&resolved.label, display);
2897                    }
2898                    SemanticCallTarget::Indirect { signature } => {
2899                        resolved.target = CallTarget::Indirect {
2900                            signature: Some(signature.clone()),
2901                            reason: UnresolvedReason::FunctionPointer,
2902                        };
2903                    }
2904                    SemanticCallTarget::Unresolved => {
2905                        resolved.target = CallTarget::Unresolved;
2906                    }
2907                }
2908                let argument_types = semantic_call_argument_types(semantic_call);
2909                resolved.label.typed = Some(annotate_rust_label(
2910                    &resolved.label.default,
2911                    &argument_types,
2912                ));
2913                Some(resolved)
2914            })
2915            .collect();
2916
2917        let mut function_label = replace_label_callee(&template.label, &semantic_function.display);
2918        function_label.typed = Some(annotate_rust_label(
2919            &function_label.default,
2920            &semantic_function.parameter_types,
2921        ));
2922        analysis.functions.push(FunctionInfo {
2923            id: id.clone(),
2924            label: function_label,
2925            public: template.public,
2926            calls,
2927            span: template.span.clone(),
2928        });
2929        if let Some(facts) = facts_by_subject.get(&template.id) {
2930            analysis.facts.extend(facts.iter().map(|fact| {
2931                let mut fact = (*fact).clone();
2932                fact.subject = id.clone();
2933                fact
2934            }));
2935        }
2936    }
2937
2938    for function in &syntax.functions {
2939        let preserve_closure = required_syntax_closures.contains(&function.id);
2940        let preserve_open_generic = generic_syntax_functions.contains(&function.id)
2941            && !matched_syntax_functions.contains(&function.id);
2942        if !preserve_closure && !preserve_open_generic {
2943            continue;
2944        }
2945        analysis.functions.push(function.clone());
2946        if preserve_open_generic && let Some(facts) = facts_by_subject.get(&function.id) {
2947            analysis.facts.extend(facts.iter().copied().cloned());
2948        }
2949    }
2950
2951    synthesize_referenced_generic_instances(&mut analysis, &syntax);
2952
2953    analysis
2954}
2955
2956fn synthesize_referenced_generic_instances(analysis: &mut FileAnalysis, syntax: &FileAnalysis) {
2957    let generic_parameters = syntax
2958        .facts
2959        .iter()
2960        .filter(|fact| fact.kind == "generic-parameter")
2961        .fold(BTreeMap::<SymbolId, Vec<String>>::new(), |mut map, fact| {
2962            map.entry(fact.subject.clone())
2963                .or_default()
2964                .push(fact.key.clone());
2965            map
2966        });
2967    if generic_parameters.is_empty() {
2968        return;
2969    }
2970
2971    let mut used_templates = HashSet::new();
2972    loop {
2973        let known = analysis
2974            .functions
2975            .iter()
2976            .map(|function| function.id.clone())
2977            .collect::<HashSet<_>>();
2978        let missing = analysis
2979            .functions
2980            .iter()
2981            .flat_map(|function| &function.calls)
2982            .filter_map(|call| match &call.target {
2983                CallTarget::Direct(target)
2984                    if !known.contains(target) && target.language.0 == "rust" =>
2985                {
2986                    Some(target.clone())
2987                }
2988                _ => None,
2989            })
2990            .collect::<BTreeSet<_>>();
2991        let mut added = Vec::new();
2992        for target in missing {
2993            let Some((base, arguments)) = generic_instance_parts(&target.name) else {
2994                continue;
2995            };
2996            let Some(template) = syntax.functions.iter().find(|function| {
2997                function.id.name == base
2998                    && generic_parameters
2999                        .get(&function.id)
3000                        .is_some_and(|parameters| parameters.len() == arguments.len())
3001            }) else {
3002                continue;
3003            };
3004            let parameters = &generic_parameters[&template.id];
3005            let bindings = parameters
3006                .iter()
3007                .cloned()
3008                .zip(arguments.iter().cloned())
3009                .collect::<HashMap<_, _>>();
3010            let mut function = template.clone();
3011            function.id = target.clone();
3012            let display = normalize_instance_display(&target.name, "");
3013            function.label = replace_label_callee(&function.label, &display);
3014            for call in &mut function.calls {
3015                substitute_generic_call(call, &bindings, &analysis.functions);
3016            }
3017            used_templates.insert(template.id.clone());
3018            added.push(function);
3019        }
3020        if added.is_empty() {
3021            break;
3022        }
3023        analysis.functions.extend(added);
3024    }
3025
3026    let displays = analysis
3027        .functions
3028        .iter()
3029        .map(|function| {
3030            (
3031                function.id.clone(),
3032                outer_call_arguments_start(&function.label.default)
3033                    .map_or(function.label.default.clone(), |start| {
3034                        function.label.default[..start].to_owned()
3035                    }),
3036            )
3037        })
3038        .collect::<HashMap<_, _>>();
3039    for function in &mut analysis.functions {
3040        for call in &mut function.calls {
3041            let CallTarget::Direct(target) = &call.target else {
3042                continue;
3043            };
3044            if let Some(display) = displays.get(target) {
3045                call.label = replace_label_callee(&call.label, display);
3046            }
3047        }
3048    }
3049    analysis
3050        .functions
3051        .retain(|function| !used_templates.contains(&function.id));
3052    analysis
3053        .facts
3054        .retain(|fact| !used_templates.contains(&fact.subject));
3055}
3056
3057fn generic_instance_parts(instance: &str) -> Option<(String, Vec<String>)> {
3058    let start = instance.find('<')?;
3059    let end = instance.rfind('>')?;
3060    if end <= start {
3061        return None;
3062    }
3063    let base = instance[..start]
3064        .rsplit("::")
3065        .next()
3066        .unwrap_or(&instance[..start])
3067        .to_owned();
3068    let arguments = split_rust_arguments(&instance[start + 1..end])
3069        .into_iter()
3070        .map(simplify_type_paths)
3071        .collect();
3072    Some((base, arguments))
3073}
3074
3075fn substitute_generic_call(
3076    call: &mut CallSite,
3077    bindings: &HashMap<String, String>,
3078    functions: &[FunctionInfo],
3079) {
3080    let CallTarget::Direct(target) = &call.target else {
3081        return;
3082    };
3083    let Some(container) = target.container.as_deref() else {
3084        return;
3085    };
3086    let parameter = container
3087        .split_once(" as ")
3088        .map_or(container, |(parameter, _)| parameter);
3089    let Some(concrete) = bindings.get(parameter) else {
3090        return;
3091    };
3092    let callee = format!("{concrete}::{}", target.name);
3093    call.label = replace_label_callee(&call.label, &callee);
3094    call.target = functions
3095        .iter()
3096        .find(|function| {
3097            outer_call_arguments_start(&function.label.default)
3098                .is_some_and(|start| function.label.default[..start] == callee)
3099        })
3100        .map(|function| function.id.clone())
3101        .map_or_else(
3102            || {
3103                CallTarget::Direct(SymbolId {
3104                    language: LanguageId::new("rust"),
3105                    module: target.module.clone(),
3106                    container: Some(concrete.clone()),
3107                    name: target.name.clone(),
3108                })
3109            },
3110            CallTarget::Direct,
3111        );
3112}
3113
3114fn semantic_closure_keys(
3115    syntax: &FileAnalysis,
3116    semantic: &SemanticProgram,
3117    file_identities: &HashMap<PathBuf, PathBuf>,
3118) -> HashMap<SymbolId, String> {
3119    syntax
3120        .functions
3121        .iter()
3122        .filter(|function| is_source_closure(function))
3123        .filter_map(|closure| {
3124            semantic
3125                .functions
3126                .iter()
3127                .filter(|function| {
3128                    source_file_identity(file_identities, &closure.span.file)
3129                        == source_file_identity(file_identities, &function.body_span.file)
3130                        && span_contains(&closure.span, &function.body_span)
3131                })
3132                .min_by_key(|function| span_size(&function.body_span))
3133                .map(|function| (closure.id.clone(), function.key.clone()))
3134        })
3135        .collect()
3136}
3137
3138fn source_closure_for_call<'a>(
3139    owner: &FunctionInfo,
3140    call: &CallSite,
3141    functions: &'a [FunctionInfo],
3142) -> Option<&'a FunctionInfo> {
3143    let name = call_syntax_name(call)?;
3144    let expected_container = owner.id.qualified_parts().join("::");
3145    let expected_lambda = format!("λ{name}");
3146    functions.iter().find(|function| {
3147        is_source_closure(function)
3148            && function.id.container.as_deref() == Some(expected_container.as_str())
3149            && closure_label_callee(function) == expected_lambda
3150    })
3151}
3152
3153fn closure_label_callee(function: &FunctionInfo) -> &str {
3154    outer_call_arguments_start(&function.label.default)
3155        .map_or(function.label.default.as_str(), |start| {
3156            &function.label.default[..start]
3157        })
3158}
3159
3160#[derive(Clone)]
3161struct SemanticClosureSource {
3162    span: SourceSpan,
3163    identity: String,
3164    lambda: String,
3165}
3166
3167#[derive(Clone, Copy)]
3168enum ClosureRewrite {
3169    Identity,
3170    Display,
3171}
3172
3173fn canonicalize_semantic_closures(
3174    syntax: &FileAnalysis,
3175    semantic: &mut SemanticProgram,
3176    file_identities: &HashMap<PathBuf, PathBuf>,
3177) {
3178    let closures = syntax
3179        .functions
3180        .iter()
3181        .filter(|function| is_source_closure(function))
3182        .map(|function| SemanticClosureSource {
3183            span: function.span.clone(),
3184            identity: format!("{{lambda-id:{}}}", function.id),
3185            lambda: outer_call_arguments_start(&function.label.default).map_or_else(
3186                || function.label.default.clone(),
3187                |start| function.label.default[..start].to_owned(),
3188            ),
3189        })
3190        .collect::<Vec<_>>();
3191    if closures.is_empty() {
3192        return;
3193    }
3194
3195    let mut direct_closures = HashMap::<String, (String, String)>::new();
3196    for function in &semantic.functions {
3197        let Some(source) = closure_for_body_span(&function.body_span, &closures, file_identities)
3198        else {
3199            continue;
3200        };
3201        let located_key = rewrite_located_closures(
3202            &function.key,
3203            &closures,
3204            file_identities,
3205            ClosureRewrite::Identity,
3206        );
3207        let key = replace_closure_definition(&located_key, &source.identity);
3208        let display = closure_instance_display(&source.lambda, &function.display);
3209        direct_closures.insert(function.key.clone(), (key, display));
3210    }
3211
3212    for function in &mut semantic.functions {
3213        let original_key = function.key.clone();
3214        if let Some((key, display)) = direct_closures.get(&original_key) {
3215            function.key = key.clone();
3216            function.display = display.clone();
3217        } else {
3218            function.key = rewrite_located_closures(
3219                &function.key,
3220                &closures,
3221                file_identities,
3222                ClosureRewrite::Identity,
3223            );
3224            function.display = rewrite_located_closures(
3225                &function.display,
3226                &closures,
3227                file_identities,
3228                ClosureRewrite::Display,
3229            );
3230        }
3231        for parameter in &mut function.parameter_types {
3232            *parameter = rewrite_located_closures(
3233                parameter,
3234                &closures,
3235                file_identities,
3236                ClosureRewrite::Display,
3237            );
3238        }
3239        for call in &mut function.calls {
3240            for argument in &mut call.argument_types {
3241                *argument = rewrite_located_closures(
3242                    argument,
3243                    &closures,
3244                    file_identities,
3245                    ClosureRewrite::Display,
3246                );
3247            }
3248            match &mut call.target {
3249                SemanticCallTarget::Direct { key, display } => {
3250                    rewrite_closure_target(
3251                        key,
3252                        display,
3253                        &direct_closures,
3254                        &closures,
3255                        file_identities,
3256                    );
3257                }
3258                SemanticCallTarget::Dynamic {
3259                    key,
3260                    display,
3261                    candidates,
3262                    ..
3263                } => {
3264                    rewrite_closure_target(
3265                        key,
3266                        display,
3267                        &direct_closures,
3268                        &closures,
3269                        file_identities,
3270                    );
3271                    for candidate in candidates {
3272                        rewrite_closure_target(
3273                            &mut candidate.key,
3274                            &mut candidate.display,
3275                            &direct_closures,
3276                            &closures,
3277                            file_identities,
3278                        );
3279                    }
3280                }
3281                SemanticCallTarget::Indirect { .. } => {}
3282                SemanticCallTarget::Unresolved => {}
3283            }
3284        }
3285    }
3286}
3287
3288fn is_source_closure(function: &FunctionInfo) -> bool {
3289    function.id.name.starts_with("{lambda:")
3290}
3291
3292fn rewrite_closure_target(
3293    key: &mut String,
3294    display: &mut String,
3295    direct_closures: &HashMap<String, (String, String)>,
3296    closures: &[SemanticClosureSource],
3297    file_identities: &HashMap<PathBuf, PathBuf>,
3298) {
3299    if let Some((canonical_key, canonical_display)) = direct_closures.get(key) {
3300        *key = canonical_key.clone();
3301        *display = canonical_display.clone();
3302        return;
3303    }
3304    *key = rewrite_located_closures(key, closures, file_identities, ClosureRewrite::Identity);
3305    *display =
3306        rewrite_located_closures(display, closures, file_identities, ClosureRewrite::Display);
3307}
3308
3309fn closure_for_body_span<'a>(
3310    body: &SourceSpan,
3311    closures: &'a [SemanticClosureSource],
3312    file_identities: &HashMap<PathBuf, PathBuf>,
3313) -> Option<&'a SemanticClosureSource> {
3314    closures
3315        .iter()
3316        .filter(|closure| {
3317            source_file_identity(file_identities, &closure.span.file)
3318                == source_file_identity(file_identities, &body.file)
3319                && span_contains(&closure.span, body)
3320        })
3321        .min_by_key(|closure| span_size(&closure.span))
3322}
3323
3324fn rewrite_located_closures(
3325    value: &str,
3326    closures: &[SemanticClosureSource],
3327    file_identities: &HashMap<PathBuf, PathBuf>,
3328    rewrite: ClosureRewrite,
3329) -> String {
3330    let mut output = String::with_capacity(value.len());
3331    let mut remainder = value;
3332    loop {
3333        let located = ["{closure@", "{async closure@"]
3334            .into_iter()
3335            .filter_map(|prefix| remainder.find(prefix).map(|start| (start, prefix)))
3336            .min_by_key(|(start, _)| *start);
3337        let Some((start, prefix)) = located else {
3338            output.push_str(remainder);
3339            break;
3340        };
3341        output.push_str(&remainder[..start]);
3342        let located = &remainder[start..];
3343        let Some(end) = located.find('}') else {
3344            output.push_str(located);
3345            break;
3346        };
3347        let location = &located[prefix.len()..end];
3348        let replacement = parse_anonymous_location(location).and_then(|(file, line, column)| {
3349            closure_for_location(&file, line, column, closures, file_identities)
3350        });
3351        if let Some(closure) = replacement {
3352            output.push_str(match rewrite {
3353                ClosureRewrite::Identity => &closure.identity,
3354                ClosureRewrite::Display => &closure.lambda,
3355            });
3356        } else {
3357            output.push_str(&located[..=end]);
3358        }
3359        remainder = &located[end + 1..];
3360    }
3361    output
3362}
3363
3364fn parse_anonymous_location(location: &str) -> Option<(PathBuf, usize, usize)> {
3365    let mut fields = location.rsplitn(5, ':');
3366    let _end_column = fields.next()?.trim().parse::<usize>().ok()?;
3367    let _end_line = fields.next()?.trim().parse::<usize>().ok()?;
3368    let start_column = fields.next()?.trim().parse::<usize>().ok()?;
3369    let start_line = fields.next()?.trim().parse::<usize>().ok()?;
3370    let file = PathBuf::from(fields.next()?.trim());
3371    Some((file, start_line, start_column))
3372}
3373
3374fn closure_for_location<'a>(
3375    file: &Path,
3376    line: usize,
3377    column: usize,
3378    closures: &'a [SemanticClosureSource],
3379    file_identities: &HashMap<PathBuf, PathBuf>,
3380) -> Option<&'a SemanticClosureSource> {
3381    closures
3382        .iter()
3383        .filter(|closure| {
3384            source_file_identity(file_identities, &closure.span.file)
3385                == source_file_identity(file_identities, file)
3386                && closure.span.start_line == line
3387        })
3388        .min_by_key(|closure| closure.span.start_column.saturating_add(1).abs_diff(column))
3389}
3390
3391fn replace_closure_definition(value: &str, replacement: &str) -> String {
3392    let Some(start) = ["{closure#", "{async closure#"]
3393        .into_iter()
3394        .filter_map(|prefix| value.rfind(prefix))
3395        .max()
3396    else {
3397        return value.to_owned();
3398    };
3399    let Some(end) = value[start..].find('}').map(|end| start + end) else {
3400        return value.to_owned();
3401    };
3402    format!("{}{replacement}{}", &value[..start], &value[end + 1..])
3403}
3404
3405fn closure_instance_display(lambda: &str, semantic: &str) -> String {
3406    let Some(start) = ["::{closure#", "::{async closure#"]
3407        .into_iter()
3408        .filter_map(|prefix| semantic.rfind(prefix))
3409        .max()
3410    else {
3411        return lambda.to_owned();
3412    };
3413    let parent = &semantic[..start];
3414    let Some(generic_start) = trailing_generic_arguments_start(parent) else {
3415        return lambda.to_owned();
3416    };
3417    format!("{}{}", lambda, &parent[generic_start..])
3418}
3419
3420fn source_file_identities(
3421    syntax: &FileAnalysis,
3422    semantic: &SemanticProgram,
3423) -> HashMap<PathBuf, PathBuf> {
3424    let mut paths = HashSet::<PathBuf>::new();
3425    for function in &syntax.functions {
3426        paths.insert(function.span.file.clone());
3427        paths.extend(function.calls.iter().map(|call| call.span.file.clone()));
3428    }
3429    for function in &semantic.functions {
3430        paths.insert(function.body_span.file.clone());
3431        paths.extend(function.calls.iter().map(|call| call.span.file.clone()));
3432    }
3433
3434    paths
3435        .into_iter()
3436        .map(|path| {
3437            let identity = path.canonicalize().unwrap_or_else(|_| {
3438                if path.is_absolute() {
3439                    path.clone()
3440                } else {
3441                    env::current_dir().map_or_else(|_| path.clone(), |root| root.join(&path))
3442                }
3443            });
3444            (path, identity)
3445        })
3446        .collect()
3447}
3448
3449fn source_file_identity<'a>(identities: &'a HashMap<PathBuf, PathBuf>, path: &'a Path) -> &'a Path {
3450    identities.get(path).map_or(path, PathBuf::as_path)
3451}
3452
3453fn best_function_template<'a>(
3454    functions: &'a [FunctionInfo],
3455    candidate_indices: &[usize],
3456    body_span: &SourceSpan,
3457) -> Option<&'a FunctionInfo> {
3458    candidate_indices
3459        .iter()
3460        .filter_map(|index| functions.get(*index))
3461        .filter(|function| span_contains(&function.span, body_span))
3462        .min_by_key(|function| span_size(&function.span))
3463}
3464
3465fn best_semantic_call(
3466    syntax_call: &CallSite,
3467    semantic_calls: &[SemanticCall],
3468    claimed: &HashSet<usize>,
3469    file_identities: &HashMap<PathBuf, PathBuf>,
3470) -> Option<usize> {
3471    let syntax_name = call_syntax_name(syntax_call)?;
3472
3473    semantic_calls
3474        .iter()
3475        .enumerate()
3476        .filter(|(index, call)| {
3477            if claimed.contains(index)
3478                || source_file_identity(file_identities, &syntax_call.span.file)
3479                    != source_file_identity(file_identities, &call.span.file)
3480                || !spans_overlap(&syntax_call.span, &call.span)
3481            {
3482                return false;
3483            }
3484            let definition_leaf = call
3485                .definition_name
3486                .rsplit("::")
3487                .next()
3488                .unwrap_or(&call.definition_name);
3489            definition_leaf == syntax_name || span_coordinates_equal(&syntax_call.span, &call.span)
3490        })
3491        .min_by_key(|(_, call)| {
3492            let definition_leaf = call
3493                .definition_name
3494                .rsplit("::")
3495                .next()
3496                .unwrap_or(&call.definition_name);
3497            let name_penalty = usize::from(definition_leaf != syntax_name) * 1_000_000;
3498            name_penalty + span_distance(&syntax_call.span, &call.span)
3499        })
3500        .map(|(index, _)| index)
3501}
3502
3503fn call_syntax_name(call: &CallSite) -> Option<&str> {
3504    match &call.syntax {
3505        CallSyntax::Path(parts) => parts.last().map(String::as_str),
3506        CallSyntax::SelfMethod(method) | CallSyntax::Method { method, .. } => Some(method.as_str()),
3507    }
3508}
3509
3510fn span_coordinates_equal(left: &SourceSpan, right: &SourceSpan) -> bool {
3511    left.start_line == right.start_line
3512        && left.start_column == right.start_column
3513        && left.end_line == right.end_line
3514        && left.end_column == right.end_column
3515}
3516
3517fn semantic_symbol(name: String) -> SymbolId {
3518    SymbolId {
3519        language: LanguageId::new("rust"),
3520        module: Vec::new(),
3521        container: None,
3522        name,
3523    }
3524}
3525
3526fn normalize_instance_key(name: &str) -> String {
3527    normalize_anonymous_type_locations(&name.replace("::<", "<"))
3528}
3529
3530fn normalize_instance_display(name: &str, crate_name: &str) -> String {
3531    let without_crate = if crate_name.is_empty() {
3532        name.to_owned()
3533    } else {
3534        name.replace(&format!("{crate_name}::"), "")
3535    };
3536    let compact = normalize_anonymous_type_locations(&without_crate.replace("::<", "<"));
3537    collapse_trait_qualification(&compact)
3538}
3539
3540fn normalize_anonymous_type_locations(value: &str) -> String {
3541    [
3542        ("{async block@", "{async block}"),
3543        ("{coroutine@", "{coroutine}"),
3544        ("{generator@", "{generator}"),
3545    ]
3546    .into_iter()
3547    .fold(value.to_owned(), |value, (prefix, replacement)| {
3548        replace_braced_location(&value, prefix, replacement)
3549    })
3550}
3551
3552fn replace_braced_location(value: &str, prefix: &str, replacement: &str) -> String {
3553    let mut output = String::with_capacity(value.len());
3554    let mut remainder = value;
3555    while let Some(start) = remainder.find(prefix) {
3556        output.push_str(&remainder[..start]);
3557        let located = &remainder[start..];
3558        let Some(end) = located.find('}') else {
3559            output.push_str(located);
3560            return output;
3561        };
3562        output.push_str(replacement);
3563        remainder = &located[end + 1..];
3564    }
3565    output.push_str(remainder);
3566    output
3567}
3568
3569fn collapse_trait_qualification(name: &str) -> String {
3570    let Some(rest) = name.strip_prefix('<') else {
3571        return name.to_owned();
3572    };
3573    let Some((qualification, method)) = rest.rsplit_once(">::") else {
3574        return name.to_owned();
3575    };
3576    let Some((self_type, _trait_name)) = qualification.rsplit_once(" as ") else {
3577        return name.to_owned();
3578    };
3579    format!("{self_type}::{method}")
3580}
3581
3582fn replace_label_callee(label: &CallLabel, callee: &str) -> CallLabel {
3583    CallLabel {
3584        default: replace_callee_text(&label.default, callee),
3585        typed: label
3586            .typed
3587            .as_ref()
3588            .map(|typed| replace_callee_text(typed, callee)),
3589    }
3590}
3591
3592fn replace_callee_text(label: &str, callee: &str) -> String {
3593    if let Some(arguments_start) = outer_call_arguments_start(label) {
3594        format!(
3595            "{}{}",
3596            semantic_callee(&label[..arguments_start], callee),
3597            &label[arguments_start..]
3598        )
3599    } else {
3600        semantic_callee(label, callee)
3601    }
3602}
3603
3604fn semantic_callee(source: &str, semantic: &str) -> String {
3605    if semantic.starts_with('λ') {
3606        return semantic.to_owned();
3607    }
3608    if semantic.starts_with("dyn ") {
3609        return semantic.rsplit_once("::").map_or_else(
3610            || semantic.to_owned(),
3611            |(dispatch, method)| format!("{}::{method}", simplify_type_paths(dispatch)),
3612        );
3613    }
3614
3615    let generic_arguments = semantic
3616        .find('<')
3617        .zip(semantic.rfind('>'))
3618        .filter(|(start, end)| start < end)
3619        .map(|(start, end)| simplify_type_paths(&semantic[start..=end]));
3620    if let Some(generic_arguments) = generic_arguments {
3621        let source_base = source
3622            .find('<')
3623            .map_or(source, |generic_start| &source[..generic_start]);
3624        return format!("{source_base}{generic_arguments}");
3625    }
3626
3627    let parts = semantic.split("::").collect::<Vec<_>>();
3628    if parts.len() >= 2
3629        && parts[parts.len() - 2]
3630            .chars()
3631            .next()
3632            .is_some_and(char::is_uppercase)
3633    {
3634        return format!("{}::{}", parts[parts.len() - 2], parts[parts.len() - 1]);
3635    }
3636    source.to_owned()
3637}
3638
3639fn simplify_type_paths(value: &str) -> String {
3640    let mut output = String::with_capacity(value.len());
3641    let mut token = String::new();
3642    let flush = |token: &mut String, output: &mut String| {
3643        if token.is_empty() {
3644            return;
3645        }
3646        output.push_str(token.rsplit("::").next().unwrap_or(token));
3647        token.clear();
3648    };
3649    for character in value.chars() {
3650        if character.is_ascii_alphanumeric() || matches!(character, '_' | ':' | '\'') {
3651            token.push(character);
3652        } else {
3653            flush(&mut token, &mut output);
3654            output.push(character);
3655        }
3656    }
3657    flush(&mut token, &mut output);
3658    output
3659}
3660
3661fn normalize_type_display(value: &str, crate_name: &str) -> String {
3662    simplify_type_paths(&normalize_anonymous_type_locations(
3663        &value.replace(&format!("{crate_name}::"), ""),
3664    ))
3665}
3666
3667fn semantic_call_argument_types(call: &SemanticCall) -> Vec<String> {
3668    let is_closure = matches!(
3669        &call.target,
3670        SemanticCallTarget::Direct { display, .. } if display.contains("{closure#")
3671    );
3672    if !is_closure {
3673        return call.argument_types.clone();
3674    }
3675    let Some(tuple) = call.argument_types.last() else {
3676        return Vec::new();
3677    };
3678    let Some(tuple) = tuple
3679        .strip_prefix('(')
3680        .and_then(|tuple| tuple.strip_suffix(')'))
3681    else {
3682        return call.argument_types.clone();
3683    };
3684    split_rust_arguments(tuple)
3685        .into_iter()
3686        .filter(|argument| !argument.is_empty())
3687        .map(ToOwned::to_owned)
3688        .collect()
3689}
3690
3691fn annotate_rust_label(label: &str, semantic_types: &[String]) -> String {
3692    let Some(arguments_start) = outer_call_arguments_start(label) else {
3693        return label.to_owned();
3694    };
3695    let Some(arguments_end) = matching_parenthesis(label, arguments_start) else {
3696        return label.to_owned();
3697    };
3698    let arguments = split_rust_arguments(&label[arguments_start + 1..arguments_end]);
3699    if arguments.is_empty() || semantic_types.is_empty() {
3700        return label.to_owned();
3701    }
3702    let types = if semantic_types.len() >= arguments.len() {
3703        &semantic_types[semantic_types.len() - arguments.len()..]
3704    } else {
3705        semantic_types
3706    };
3707    let leading_untyped = arguments.len().saturating_sub(types.len());
3708    let annotated = arguments
3709        .iter()
3710        .enumerate()
3711        .map(|(index, argument)| {
3712            if index < leading_untyped {
3713                argument.to_string()
3714            } else {
3715                format!("{}: {}", argument, types[index - leading_untyped])
3716            }
3717        })
3718        .collect::<Vec<_>>()
3719        .join(", ");
3720    format!(
3721        "{}({annotated}){}",
3722        &label[..arguments_start],
3723        &label[arguments_end + 1..]
3724    )
3725}
3726
3727fn matching_parenthesis(label: &str, start: usize) -> Option<usize> {
3728    let mut depth = 0usize;
3729    let mut in_string = false;
3730    let mut escaped = false;
3731    for (offset, character) in label[start..].char_indices() {
3732        if in_string {
3733            if escaped {
3734                escaped = false;
3735            } else if character == '\\' {
3736                escaped = true;
3737            } else if character == '"' {
3738                in_string = false;
3739            }
3740            continue;
3741        }
3742        match character {
3743            '"' => in_string = true,
3744            '(' => depth += 1,
3745            ')' => {
3746                depth = depth.checked_sub(1)?;
3747                if depth == 0 {
3748                    return Some(start + offset);
3749                }
3750            }
3751            _ => {}
3752        }
3753    }
3754    None
3755}
3756
3757fn split_rust_arguments(arguments: &str) -> Vec<&str> {
3758    if arguments.trim().is_empty() {
3759        return Vec::new();
3760    }
3761    let mut result = Vec::new();
3762    let mut start = 0usize;
3763    let mut delimiters = Vec::new();
3764    let mut in_string = false;
3765    let mut escaped = false;
3766    for (index, character) in arguments.char_indices() {
3767        if in_string {
3768            if escaped {
3769                escaped = false;
3770            } else if character == '\\' {
3771                escaped = true;
3772            } else if character == '"' {
3773                in_string = false;
3774            }
3775            continue;
3776        }
3777        match character {
3778            '"' => in_string = true,
3779            '(' | '[' | '{' | '<' => delimiters.push(character),
3780            ')' | ']' | '}' | '>' => {
3781                delimiters.pop();
3782            }
3783            ',' if delimiters.is_empty() => {
3784                result.push(arguments[start..index].trim());
3785                start = index + character.len_utf8();
3786            }
3787            _ => {}
3788        }
3789    }
3790    result.push(arguments[start..].trim());
3791    result
3792}
3793
3794fn outer_call_arguments_start(label: &str) -> Option<usize> {
3795    let mut depth = 0usize;
3796    for (index, character) in label.char_indices().rev() {
3797        match character {
3798            ')' => depth += 1,
3799            '(' => {
3800                depth = depth.checked_sub(1)?;
3801                if depth == 0 {
3802                    return Some(index);
3803                }
3804            }
3805            _ => {}
3806        }
3807    }
3808    None
3809}
3810
3811fn rustc_source_span(span: rustc_public::ty::Span) -> SourceSpan {
3812    let lines = span.get_lines();
3813    let file = PathBuf::from(span.get_filename());
3814    let file = if file.is_absolute() {
3815        file
3816    } else {
3817        env::current_dir().map_or(file.clone(), |directory| directory.join(file))
3818    };
3819    SourceSpan {
3820        file,
3821        start_line: lines.start_line,
3822        start_column: lines.start_col.saturating_sub(1),
3823        start_byte: None,
3824        end_line: lines.end_line.max(lines.start_line),
3825        end_column: lines.end_col.saturating_sub(1),
3826        end_byte: None,
3827    }
3828}
3829
3830fn source_files_match(left: &Path, right: &Path) -> bool {
3831    left == right
3832        || left
3833            .canonicalize()
3834            .ok()
3835            .zip(right.canonicalize().ok())
3836            .is_some_and(|(left, right)| left == right)
3837}
3838
3839fn span_contains(outer: &SourceSpan, inner: &SourceSpan) -> bool {
3840    position_le(
3841        outer.start_line,
3842        outer.start_column,
3843        inner.start_line,
3844        inner.start_column,
3845    ) && position_le(
3846        inner.end_line,
3847        inner.end_column,
3848        outer.end_line,
3849        outer.end_column,
3850    )
3851}
3852
3853fn spans_overlap(left: &SourceSpan, right: &SourceSpan) -> bool {
3854    position_le(
3855        left.start_line,
3856        left.start_column,
3857        right.end_line,
3858        right.end_column,
3859    ) && position_le(
3860        right.start_line,
3861        right.start_column,
3862        left.end_line,
3863        left.end_column,
3864    )
3865}
3866
3867fn position_le(
3868    left_line: usize,
3869    left_column: usize,
3870    right_line: usize,
3871    right_column: usize,
3872) -> bool {
3873    (left_line, left_column) <= (right_line, right_column)
3874}
3875
3876fn span_size(span: &SourceSpan) -> usize {
3877    (span.end_line.saturating_sub(span.start_line) * 10_000)
3878        + span.end_column.saturating_sub(span.start_column)
3879}
3880
3881fn span_distance(left: &SourceSpan, right: &SourceSpan) -> usize {
3882    left.start_line.abs_diff(right.start_line) * 10_000
3883        + left.start_column.abs_diff(right.start_column)
3884}
3885
3886impl LanguageBackend for RustBackend {
3887    fn language(&self) -> LanguageId {
3888        LanguageId::new("rust")
3889    }
3890
3891    fn extensions(&self) -> &'static [&'static str] {
3892        &["rs"]
3893    }
3894
3895    fn analyze_file(
3896        &self,
3897        context: &FileContext<'_>,
3898        source: &str,
3899    ) -> FrontendResult<FileAnalysis> {
3900        let syntax = syn::parse_file(source)?;
3901        let mut analysis = FileAnalysis::default();
3902        analysis.source_files.insert(context.path.to_path_buf());
3903        analyze_items(context.path, context.module, &syntax.items, &mut analysis);
3904        Ok(analysis)
3905    }
3906
3907    fn analyze_project(&self, context: &ProjectContext<'_>) -> FrontendResult<FileAnalysis> {
3908        let wrapper = context.driver_executable.ok_or_else(|| {
3909            std::io::Error::other("Rust project analysis requires a driver executable")
3910        })?;
3911        let session = match context.cache {
3912            Some(cache) => RustProjectSession::create_cached(
3913                cache.project_root,
3914                cache.endpoint,
3915                cache.verbose,
3916            )?,
3917            None => RustProjectSession::create()?,
3918        };
3919        analyze_semantic_project(context.root, wrapper, context.entries, &session)
3920    }
3921}
3922
3923fn analyze_items(file: &Path, module: &[String], items: &[Item], analysis: &mut FileAnalysis) {
3924    for item in items {
3925        match item {
3926            Item::Fn(function) => {
3927                let function_info = function_from_item(file, module, None, function);
3928                let owner = function_info.id.clone();
3929                add_signature_facts(file, &function_info, &function.sig, analysis);
3930                analysis.functions.push(function_info);
3931                add_closure_functions(file, module, &owner, &function.block, analysis);
3932            }
3933            Item::Impl(item_impl) => analyze_impl(file, module, item_impl, analysis),
3934            Item::Trait(item_trait) => analyze_trait(file, module, item_trait, analysis),
3935            Item::Mod(item_mod) => {
3936                if let Some((_, nested)) = &item_mod.content {
3937                    let mut nested_module = module.to_vec();
3938                    nested_module.push(item_mod.ident.to_string());
3939                    analyze_items(file, &nested_module, nested, analysis);
3940                }
3941            }
3942            _ => {}
3943        }
3944    }
3945}
3946
3947fn function_from_item(
3948    file: &Path,
3949    module: &[String],
3950    container: Option<String>,
3951    function: &ItemFn,
3952) -> FunctionInfo {
3953    function_from_parts(
3954        file,
3955        module,
3956        container,
3957        &function.sig,
3958        &function.block,
3959        is_public(&function.vis),
3960        function.span(),
3961    )
3962}
3963
3964fn analyze_impl(file: &Path, module: &[String], item_impl: &ItemImpl, analysis: &mut FileAnalysis) {
3965    let self_ty = compact_tokens(&item_impl.self_ty);
3966    let trait_name = item_impl
3967        .trait_
3968        .as_ref()
3969        .map(|(_, path, _)| compact_tokens(path));
3970    let container = trait_name
3971        .as_ref()
3972        .map(|trait_name| format!("{self_ty} as {trait_name}"))
3973        .unwrap_or_else(|| self_ty.clone());
3974
3975    for item in &item_impl.items {
3976        let ImplItem::Fn(method) = item else {
3977            continue;
3978        };
3979        let function = function_from_parts(
3980            file,
3981            module,
3982            Some(container.clone()),
3983            &method.sig,
3984            &method.block,
3985            is_public(&method.vis) || trait_name.is_some(),
3986            method.span(),
3987        );
3988        let owner = function.id.clone();
3989
3990        add_signature_facts(file, &function, &method.sig, analysis);
3991        if let Some(trait_name) = &trait_name {
3992            analysis.facts.push(LanguageFact {
3993                subject: function.id.clone(),
3994                namespace: LanguageId::new("rust"),
3995                kind: "impl-trait".to_owned(),
3996                key: trait_name.clone(),
3997                value: self_ty.clone(),
3998                span: function.span.clone(),
3999            });
4000        }
4001        analysis.functions.push(function);
4002        add_closure_functions(file, module, &owner, &method.block, analysis);
4003    }
4004}
4005
4006fn analyze_trait(
4007    file: &Path,
4008    module: &[String],
4009    item_trait: &ItemTrait,
4010    analysis: &mut FileAnalysis,
4011) {
4012    let trait_name = item_trait.ident.to_string();
4013    for item in &item_trait.items {
4014        let TraitItem::Fn(method) = item else {
4015            continue;
4016        };
4017        let Some(block) = &method.default else {
4018            continue;
4019        };
4020        let function = function_from_parts(
4021            file,
4022            module,
4023            Some(trait_name.clone()),
4024            &method.sig,
4025            block,
4026            is_public(&item_trait.vis),
4027            method.span(),
4028        );
4029        let owner = function.id.clone();
4030        add_signature_facts(file, &function, &method.sig, analysis);
4031        analysis.functions.push(function);
4032        add_closure_functions(file, module, &owner, block, analysis);
4033    }
4034}
4035
4036fn add_closure_functions(
4037    file: &Path,
4038    module: &[String],
4039    owner: &SymbolId,
4040    block: &syn::Block,
4041    analysis: &mut FileAnalysis,
4042) {
4043    let closure_sources = closure_sources(block);
4044    let mut extractor = ClosureExtractor {
4045        file,
4046        module,
4047        owner,
4048        closure_sources: &closure_sources,
4049        functions: Vec::new(),
4050    };
4051    extractor.visit_block(block);
4052    analysis.functions.extend(extractor.functions);
4053}
4054
4055struct ClosureExtractor<'a> {
4056    file: &'a Path,
4057    module: &'a [String],
4058    owner: &'a SymbolId,
4059    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4060    functions: Vec<FunctionInfo>,
4061}
4062
4063impl<'ast> Visit<'ast> for ClosureExtractor<'_> {
4064    fn visit_expr_closure(&mut self, node: &'ast ExprClosure) {
4065        let source = self
4066            .closure_sources
4067            .get(&closure_span(node.span()))
4068            .cloned()
4069            .unwrap_or_else(|| ClosureSource::anonymous(self.functions.len()));
4070        let closure_name = format!("{{lambda:{}}}", source.identity);
4071        let lambda = source.lambda_name();
4072        let id = SymbolId {
4073            language: LanguageId::new("rust"),
4074            module: self.module.to_vec(),
4075            container: Some(self.owner.qualified_parts().join("::")),
4076            name: closure_name.clone(),
4077        };
4078        let mut calls = CallCollector {
4079            file: self.file,
4080            closure_sources: self.closure_sources,
4081            calls: Vec::new(),
4082        };
4083        calls.visit_expr(&node.body);
4084        let parameters = node.inputs.iter().map(pattern_name).collect::<Vec<_>>();
4085        let typed_parameters = node
4086            .inputs
4087            .iter()
4088            .map(|parameter| match parameter {
4089                Pat::Type(typed) => format!(
4090                    "{}: {}",
4091                    pattern_name(&typed.pat),
4092                    compact_tokens(&typed.ty)
4093                ),
4094                parameter => pattern_name(parameter),
4095            })
4096            .collect::<Vec<_>>();
4097        self.functions.push(FunctionInfo {
4098            id,
4099            label: CallLabel::with_types(
4100                format!("{lambda}({})", parameters.join(", ")),
4101                format!("{lambda}({})", typed_parameters.join(", ")),
4102            ),
4103            public: false,
4104            calls: calls.calls,
4105            span: source_span(self.file, node.span()),
4106        });
4107        visit::visit_expr_closure(self, node);
4108    }
4109
4110    fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
4111}
4112
4113fn function_from_parts(
4114    file: &Path,
4115    module: &[String],
4116    container: Option<String>,
4117    signature: &Signature,
4118    block: &syn::Block,
4119    public: bool,
4120    span: Span,
4121) -> FunctionInfo {
4122    let id = SymbolId {
4123        language: LanguageId::new("rust"),
4124        module: module.to_vec(),
4125        container,
4126        name: signature.ident.to_string(),
4127    };
4128    let closure_sources = closure_sources(block);
4129    let mut collector = CallCollector {
4130        file,
4131        closure_sources: &closure_sources,
4132        calls: Vec::new(),
4133    };
4134    collector.visit_block(block);
4135    rewrite_symbolic_generic_calls(module, signature, &mut collector.calls);
4136
4137    FunctionInfo {
4138        label: function_label(&id, signature),
4139        id,
4140        public,
4141        calls: collector.calls,
4142        span: source_span(file, span),
4143    }
4144}
4145
4146fn add_signature_facts(
4147    file: &Path,
4148    function: &FunctionInfo,
4149    signature: &Signature,
4150    analysis: &mut FileAnalysis,
4151) {
4152    for parameter in &signature.generics.params {
4153        let (key, value) = match parameter {
4154            syn::GenericParam::Lifetime(parameter) => (
4155                parameter.lifetime.to_string(),
4156                parameter
4157                    .bounds
4158                    .iter()
4159                    .map(ToString::to_string)
4160                    .collect::<Vec<_>>()
4161                    .join(" + "),
4162            ),
4163            syn::GenericParam::Type(parameter) => (
4164                parameter.ident.to_string(),
4165                parameter
4166                    .bounds
4167                    .iter()
4168                    .map(compact_tokens)
4169                    .collect::<Vec<_>>()
4170                    .join(" + "),
4171            ),
4172            syn::GenericParam::Const(parameter) => {
4173                (parameter.ident.to_string(), compact_tokens(&parameter.ty))
4174            }
4175        };
4176        analysis.facts.push(LanguageFact {
4177            subject: function.id.clone(),
4178            namespace: LanguageId::new("rust"),
4179            kind: "generic-parameter".to_owned(),
4180            key,
4181            value,
4182            span: source_span(file, parameter.span()),
4183        });
4184    }
4185    for input in &signature.inputs {
4186        match input {
4187            FnArg::Receiver(receiver) => {
4188                let mode = match (&receiver.reference, receiver.mutability.is_some()) {
4189                    (Some(_), true) => "&mut self",
4190                    (Some(_), false) => "&self",
4191                    (None, true) => "mut self",
4192                    (None, false) => "self",
4193                };
4194                analysis.facts.push(LanguageFact {
4195                    subject: function.id.clone(),
4196                    namespace: LanguageId::new("rust"),
4197                    kind: "receiver".to_owned(),
4198                    key: "self".to_owned(),
4199                    value: mode.to_owned(),
4200                    span: source_span(file, receiver.span()),
4201                });
4202            }
4203            FnArg::Typed(argument) => {
4204                analysis.facts.push(LanguageFact {
4205                    subject: function.id.clone(),
4206                    namespace: LanguageId::new("rust"),
4207                    kind: "parameter".to_owned(),
4208                    key: compact_tokens(&argument.pat),
4209                    value: compact_tokens(&argument.ty),
4210                    span: source_span(file, argument.span()),
4211                });
4212            }
4213        }
4214    }
4215
4216    if signature.asyncness.is_some() {
4217        push_modifier_fact(function, "async", file, signature.span(), analysis);
4218    }
4219    if signature.unsafety.is_some() {
4220        push_modifier_fact(function, "unsafe", file, signature.span(), analysis);
4221    }
4222    if signature.constness.is_some() {
4223        push_modifier_fact(function, "const", file, signature.span(), analysis);
4224    }
4225    if let ReturnType::Type(_, ty) = &signature.output {
4226        analysis.facts.push(LanguageFact {
4227            subject: function.id.clone(),
4228            namespace: LanguageId::new("rust"),
4229            kind: "return-type".to_owned(),
4230            key: "return".to_owned(),
4231            value: compact_tokens(ty),
4232            span: source_span(file, ty.span()),
4233        });
4234    }
4235}
4236
4237fn push_modifier_fact(
4238    function: &FunctionInfo,
4239    modifier: &str,
4240    file: &Path,
4241    span: Span,
4242    analysis: &mut FileAnalysis,
4243) {
4244    analysis.facts.push(LanguageFact {
4245        subject: function.id.clone(),
4246        namespace: LanguageId::new("rust"),
4247        kind: "modifier".to_owned(),
4248        key: modifier.to_owned(),
4249        value: "true".to_owned(),
4250        span: source_span(file, span),
4251    });
4252}
4253
4254fn parameter_names(signature: &Signature) -> String {
4255    signature
4256        .inputs
4257        .iter()
4258        .map(|input| match input {
4259            FnArg::Receiver(receiver) => match (&receiver.reference, receiver.mutability.is_some())
4260            {
4261                (Some(_), true) => "&mut self".to_owned(),
4262                (Some(_), false) => "&self".to_owned(),
4263                (None, true) => "mut self".to_owned(),
4264                (None, false) => "self".to_owned(),
4265            },
4266            FnArg::Typed(argument) => pattern_name(&argument.pat),
4267        })
4268        .collect::<Vec<_>>()
4269        .join(", ")
4270}
4271
4272fn function_label(id: &SymbolId, signature: &Signature) -> CallLabel {
4273    let generic_names = generic_parameter_labels(signature);
4274    let generics = if generic_names.is_empty() {
4275        String::new()
4276    } else {
4277        format!("<{}>", generic_names.join(", "))
4278    };
4279    let default = format!(
4280        "{}{generics}({})",
4281        id.short_name(),
4282        parameter_names(signature)
4283    );
4284    let typed = format!(
4285        "{}{generics}({})",
4286        id.short_name(),
4287        typed_parameters(signature)
4288    );
4289    CallLabel::with_types(default, typed)
4290}
4291
4292fn generic_parameter_labels(signature: &Signature) -> Vec<String> {
4293    let mut where_bounds = HashMap::<String, Vec<String>>::new();
4294    if let Some(where_clause) = &signature.generics.where_clause {
4295        for predicate in &where_clause.predicates {
4296            let syn::WherePredicate::Type(predicate) = predicate else {
4297                continue;
4298            };
4299            let key = compact_tokens(&predicate.bounded_ty);
4300            where_bounds
4301                .entry(key)
4302                .or_default()
4303                .extend(predicate.bounds.iter().map(compact_tokens));
4304        }
4305    }
4306
4307    signature
4308        .generics
4309        .params
4310        .iter()
4311        .map(|parameter| match parameter {
4312            syn::GenericParam::Lifetime(parameter) => {
4313                let mut bounds = parameter
4314                    .bounds
4315                    .iter()
4316                    .map(ToString::to_string)
4317                    .collect::<Vec<_>>();
4318                bounds.extend(
4319                    where_bounds
4320                        .get(&parameter.lifetime.to_string())
4321                        .cloned()
4322                        .unwrap_or_default(),
4323                );
4324                format_generic_parameter(parameter.lifetime.to_string(), bounds)
4325            }
4326            syn::GenericParam::Type(parameter) => {
4327                let name = parameter.ident.to_string();
4328                let mut bounds = parameter
4329                    .bounds
4330                    .iter()
4331                    .map(compact_tokens)
4332                    .collect::<Vec<_>>();
4333                bounds.extend(where_bounds.get(&name).cloned().unwrap_or_default());
4334                format_generic_parameter(name, bounds)
4335            }
4336            syn::GenericParam::Const(parameter) => {
4337                format!(
4338                    "const {}: {}",
4339                    parameter.ident,
4340                    compact_tokens(&parameter.ty)
4341                )
4342            }
4343        })
4344        .collect()
4345}
4346
4347fn format_generic_parameter(name: String, mut bounds: Vec<String>) -> String {
4348    bounds.retain(|bound| !bound.is_empty());
4349    bounds.sort();
4350    bounds.dedup();
4351    if bounds.is_empty() {
4352        name
4353    } else {
4354        format!("{name}: {}", bounds.join(" + "))
4355    }
4356}
4357
4358fn rewrite_symbolic_generic_calls(
4359    module: &[String],
4360    signature: &Signature,
4361    calls: &mut [CallSite],
4362) {
4363    let generic_bounds = generic_type_bounds(signature);
4364    if generic_bounds.is_empty() {
4365        return;
4366    }
4367    let receivers = signature
4368        .inputs
4369        .iter()
4370        .filter_map(|input| {
4371            let FnArg::Typed(argument) = input else {
4372                return None;
4373            };
4374            let parameter = simple_pattern_binding(&argument.pat)?;
4375            let generic = simple_generic_type(&argument.ty)?;
4376            generic_bounds
4377                .contains_key(&generic)
4378                .then_some((parameter, generic))
4379        })
4380        .collect::<HashMap<_, _>>();
4381
4382    for call in calls {
4383        let CallSyntax::Method { receiver, method } = &call.syntax else {
4384            continue;
4385        };
4386        let Some(generic) = receivers.get(receiver) else {
4387            continue;
4388        };
4389        let bound = generic_bounds
4390            .get(generic)
4391            .and_then(|bounds| (bounds.len() == 1).then(|| bounds[0].clone()));
4392        call.target = CallTarget::Direct(SymbolId {
4393            language: LanguageId::new("rust"),
4394            module: module.to_vec(),
4395            container: Some(
4396                bound.map_or_else(|| generic.clone(), |bound| format!("{generic} as {bound}")),
4397            ),
4398            name: method.clone(),
4399        });
4400        call.label = replace_label_callee(&call.label, &format!("{generic}::{method}"));
4401    }
4402}
4403
4404fn generic_type_bounds(signature: &Signature) -> HashMap<String, Vec<String>> {
4405    let mut result = HashMap::<String, Vec<String>>::new();
4406    for parameter in &signature.generics.params {
4407        let syn::GenericParam::Type(parameter) = parameter else {
4408            continue;
4409        };
4410        result.insert(
4411            parameter.ident.to_string(),
4412            parameter.bounds.iter().map(compact_tokens).collect(),
4413        );
4414    }
4415    if let Some(where_clause) = &signature.generics.where_clause {
4416        for predicate in &where_clause.predicates {
4417            let syn::WherePredicate::Type(predicate) = predicate else {
4418                continue;
4419            };
4420            let name = compact_tokens(&predicate.bounded_ty);
4421            if let Some(bounds) = result.get_mut(&name) {
4422                bounds.extend(predicate.bounds.iter().map(compact_tokens));
4423            }
4424        }
4425    }
4426    for bounds in result.values_mut() {
4427        bounds.sort();
4428        bounds.dedup();
4429    }
4430    result
4431}
4432
4433fn simple_generic_type(ty: &syn::Type) -> Option<String> {
4434    match ty {
4435        syn::Type::Path(path) if path.qself.is_none() && path.path.segments.len() == 1 => {
4436            Some(path.path.segments[0].ident.to_string())
4437        }
4438        syn::Type::Reference(reference) => simple_generic_type(&reference.elem),
4439        syn::Type::Group(group) => simple_generic_type(&group.elem),
4440        syn::Type::Paren(paren) => simple_generic_type(&paren.elem),
4441        _ => None,
4442    }
4443}
4444
4445fn typed_parameters(signature: &Signature) -> String {
4446    signature
4447        .inputs
4448        .iter()
4449        .map(|input| match input {
4450            FnArg::Receiver(_) => pattern_name_from_argument(input),
4451            FnArg::Typed(argument) => {
4452                format!(
4453                    "{}: {}",
4454                    pattern_name(&argument.pat),
4455                    compact_tokens(&argument.ty)
4456                )
4457            }
4458        })
4459        .collect::<Vec<_>>()
4460        .join(", ")
4461}
4462
4463fn pattern_name_from_argument(argument: &FnArg) -> String {
4464    match argument {
4465        FnArg::Receiver(receiver) => match (&receiver.reference, receiver.mutability.is_some()) {
4466            (Some(_), true) => "&mut self".to_owned(),
4467            (Some(_), false) => "&self".to_owned(),
4468            (None, true) => "mut self".to_owned(),
4469            (None, false) => "self".to_owned(),
4470        },
4471        FnArg::Typed(argument) => pattern_name(&argument.pat),
4472    }
4473}
4474
4475fn pattern_name(pattern: &Pat) -> String {
4476    match pattern {
4477        Pat::Ident(ident) => ident.ident.to_string(),
4478        Pat::Reference(reference) => pattern_name(&reference.pat),
4479        Pat::Wild(_) => "_".to_owned(),
4480        _ => compact_tokens(pattern),
4481    }
4482}
4483
4484fn is_public(visibility: &Visibility) -> bool {
4485    matches!(visibility, Visibility::Public(_))
4486}
4487
4488fn compact_tokens(tokens: &impl ToTokens) -> String {
4489    tokens.to_token_stream().to_string().replace(' ', "")
4490}
4491
4492fn source_span(file: &Path, span: Span) -> SourceSpan {
4493    let start = span.start();
4494    let end = span.end();
4495    SourceSpan {
4496        file: file.to_path_buf(),
4497        start_line: start.line,
4498        start_column: start.column,
4499        start_byte: None,
4500        end_line: end.line.max(start.line),
4501        end_column: end.column,
4502        end_byte: None,
4503    }
4504}
4505
4506struct CallCollector<'a> {
4507    file: &'a Path,
4508    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4509    calls: Vec<CallSite>,
4510}
4511
4512impl<'ast> Visit<'ast> for CallCollector<'_> {
4513    fn visit_expr_call(&mut self, node: &'ast ExprCall) {
4514        // MIR evaluates nested calls before their enclosing expression. Keep
4515        // the same order so an aggregate constructor such as `Some(f())`
4516        // cannot claim `f`'s semantic call span.
4517        visit::visit_expr_call(self, node);
4518        if let Some(parts) = callable_path(&node.func) {
4519            let syntax = CallSyntax::Path(parts);
4520            let span = source_span(self.file, node.span());
4521            self.calls.push(CallSite {
4522                id: CallSiteId::source(&syntax, &span),
4523                syntax,
4524                target: CallTarget::Unresolved,
4525                label: CallLabel::new(call_expression_label(node, self.closure_sources)),
4526                span,
4527            });
4528        }
4529    }
4530
4531    fn visit_expr_method_call(&mut self, node: &'ast ExprMethodCall) {
4532        visit::visit_expr_method_call(self, node);
4533        let method = node.method.to_string();
4534        let syntax = if is_self_expr(&node.receiver) {
4535            CallSyntax::SelfMethod(method)
4536        } else {
4537            CallSyntax::Method {
4538                receiver: receiver_name(&node.receiver),
4539                method,
4540            }
4541        };
4542        let span = source_span(self.file, node.span());
4543        self.calls.push(CallSite {
4544            id: CallSiteId::source(&syntax, &span),
4545            syntax,
4546            target: CallTarget::Unresolved,
4547            label: CallLabel::new(method_call_label(node, self.closure_sources)),
4548            span,
4549        });
4550    }
4551
4552    // Nested closures and local functions own their calls; do not attribute them
4553    // to the enclosing function.
4554    fn visit_expr_closure(&mut self, _node: &'ast ExprClosure) {}
4555
4556    fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
4557}
4558
4559fn call_expression_label(
4560    node: &ExprCall,
4561    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4562) -> String {
4563    let function = compact_tokens(&node.func).replace("::<", "<");
4564    let (closure_types, arguments) = call_arguments(&node.args, closure_sources);
4565    let function = append_generic_arguments(&function, &closure_types);
4566    format!("{function}({})", arguments.join(", "))
4567}
4568
4569fn method_call_label(
4570    node: &ExprMethodCall,
4571    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4572) -> String {
4573    let receiver = simple_receiver(&node.receiver)
4574        .then(|| compact_expression(&node.receiver, closure_sources));
4575    let generics = node
4576        .turbofish
4577        .as_ref()
4578        .map(compact_tokens)
4579        .unwrap_or_default()
4580        .replace("::<", "<");
4581    let (closure_types, arguments) = call_arguments(&node.args, closure_sources);
4582    let method = format!("{}{generics}", node.method);
4583    let callee = receiver.map_or(method.clone(), |receiver| format!("{receiver}.{method}"));
4584    let callee = append_generic_arguments(&callee, &closure_types);
4585    format!("{callee}({})", arguments.join(", "))
4586}
4587
4588type ClosureSpan = (usize, usize, usize, usize);
4589
4590#[derive(Clone, Debug)]
4591struct ClosureSource {
4592    ordinal: usize,
4593    binding: Option<String>,
4594    identity: String,
4595}
4596
4597impl ClosureSource {
4598    fn anonymous(ordinal: usize) -> Self {
4599        Self {
4600            ordinal,
4601            binding: None,
4602            identity: format!("fallback-{ordinal}"),
4603        }
4604    }
4605
4606    fn lambda_name(&self) -> String {
4607        self.binding.as_ref().map_or_else(
4608            || format!("λ#{}", self.ordinal + 1),
4609            |name| format!("λ{name}"),
4610        )
4611    }
4612}
4613
4614fn closure_span(span: Span) -> ClosureSpan {
4615    let start = span.start();
4616    let end = span.end();
4617    (start.line, start.column, end.line, end.column)
4618}
4619
4620fn closure_sources(block: &syn::Block) -> HashMap<ClosureSpan, ClosureSource> {
4621    let mut collector = ClosureOrdinalCollector::default();
4622    collector.visit_block(block);
4623    collector.sources
4624}
4625
4626#[derive(Default)]
4627struct ClosureOrdinalCollector {
4628    sources: HashMap<ClosureSpan, ClosureSource>,
4629    bindings: HashMap<ClosureSpan, String>,
4630    identity_counts: HashMap<String, usize>,
4631}
4632
4633impl<'ast> Visit<'ast> for ClosureOrdinalCollector {
4634    fn visit_local(&mut self, node: &'ast syn::Local) {
4635        if let Some(initializer) = &node.init
4636            && let Some(closure) = direct_closure_expression(&initializer.expr)
4637            && let Some(binding) = simple_pattern_binding(&node.pat)
4638        {
4639            self.bindings.insert(closure_span(closure.span()), binding);
4640        }
4641        visit::visit_local(self, node);
4642    }
4643
4644    fn visit_expr_closure(&mut self, node: &'ast ExprClosure) {
4645        let span = closure_span(node.span());
4646        let ordinal = self.sources.len();
4647        let base_identity = closure_structural_identity(node);
4648        let occurrence = self
4649            .identity_counts
4650            .entry(base_identity.clone())
4651            .or_default();
4652        let identity = if *occurrence == 0 {
4653            base_identity
4654        } else {
4655            format!("{base_identity}-{}", *occurrence + 1)
4656        };
4657        *occurrence += 1;
4658        self.sources.insert(
4659            span,
4660            ClosureSource {
4661                ordinal,
4662                binding: self.bindings.get(&span).cloned(),
4663                identity,
4664            },
4665        );
4666        visit::visit_expr_closure(self, node);
4667    }
4668
4669    fn visit_item_fn(&mut self, _node: &'ast ItemFn) {}
4670}
4671
4672fn closure_structural_identity(node: &ExprClosure) -> String {
4673    let signature = format!(
4674        "{}:{}:{}:{}",
4675        node.asyncness.is_some(),
4676        node.movability.is_some(),
4677        node.capture.is_some(),
4678        node.inputs
4679            .iter()
4680            .map(compact_tokens)
4681            .collect::<Vec<_>>()
4682            .join(",")
4683    );
4684    let mut hasher = Sha256::new();
4685    hasher.update(signature.as_bytes());
4686    format!("{:x}", hasher.finalize())[..12].to_owned()
4687}
4688
4689struct ClosureLabelRedactor<'a> {
4690    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4691    used: Vec<(String, String)>,
4692}
4693
4694impl VisitMut for ClosureLabelRedactor<'_> {
4695    fn visit_expr_mut(&mut self, node: &mut Expr) {
4696        let Expr::Closure(closure) = node else {
4697            visit_mut::visit_expr_mut(self, node);
4698            return;
4699        };
4700        let Some(source) = self
4701            .closure_sources
4702            .get(&closure_span(closure.span()))
4703            .cloned()
4704        else {
4705            return;
4706        };
4707        let marker = format!("__diffkit_closure_placeholder_{}", source.ordinal);
4708        self.used.push((marker.clone(), source.lambda_name()));
4709        let ident = syn::Ident::new(&marker, closure.span());
4710        *node = Expr::Path(syn::ExprPath {
4711            attrs: Vec::new(),
4712            qself: None,
4713            path: syn::Path::from(ident),
4714        });
4715    }
4716}
4717
4718fn compact_expression(
4719    expression: &Expr,
4720    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4721) -> String {
4722    let mut expression = expression.clone();
4723    let mut redactor = ClosureLabelRedactor {
4724        closure_sources,
4725        used: Vec::new(),
4726    };
4727    redactor.visit_expr_mut(&mut expression);
4728    let mut rendered = compact_tokens(&expression);
4729    for (marker, lambda) in redactor.used {
4730        rendered = rendered.replace(&marker, &lambda);
4731    }
4732    rendered
4733}
4734
4735fn call_argument_label(
4736    expression: &Expr,
4737    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4738) -> String {
4739    let rendered = compact_expression(expression, closure_sources);
4740    if rendered.chars().count() <= 96 {
4741        rendered
4742    } else {
4743        "…".to_owned()
4744    }
4745}
4746
4747fn call_arguments(
4748    arguments: &syn::punctuated::Punctuated<Expr, syn::Token![,]>,
4749    closure_sources: &HashMap<ClosureSpan, ClosureSource>,
4750) -> (Vec<String>, Vec<String>) {
4751    let mut closure_types = Vec::new();
4752    let mut values = Vec::new();
4753    for argument in arguments {
4754        if let Some(source) = closure_source_for_expression(argument, closure_sources) {
4755            closure_types.push(source.lambda_name());
4756        } else {
4757            values.push(call_argument_label(argument, closure_sources));
4758        }
4759    }
4760    (closure_types, values)
4761}
4762
4763fn closure_source_for_expression<'a>(
4764    expression: &Expr,
4765    closure_sources: &'a HashMap<ClosureSpan, ClosureSource>,
4766) -> Option<&'a ClosureSource> {
4767    if let Some(closure) = direct_closure_expression(expression) {
4768        return closure_sources.get(&closure_span(closure.span()));
4769    }
4770    let Expr::Path(path) = expression else {
4771        return None;
4772    };
4773    let binding = path.path.get_ident()?.to_string();
4774    closure_sources
4775        .values()
4776        .find(|source| source.binding.as_deref() == Some(binding.as_str()))
4777}
4778
4779fn direct_closure_expression(expression: &Expr) -> Option<&ExprClosure> {
4780    match expression {
4781        Expr::Closure(closure) => Some(closure),
4782        Expr::Group(group) => direct_closure_expression(&group.expr),
4783        Expr::Paren(paren) => direct_closure_expression(&paren.expr),
4784        _ => None,
4785    }
4786}
4787
4788fn simple_pattern_binding(pattern: &Pat) -> Option<String> {
4789    match pattern {
4790        Pat::Ident(identifier) => Some(identifier.ident.to_string()),
4791        Pat::Type(typed) => simple_pattern_binding(&typed.pat),
4792        _ => None,
4793    }
4794}
4795
4796fn append_generic_arguments(callee: &str, additions: &[String]) -> String {
4797    if additions.is_empty() {
4798        return callee.to_owned();
4799    }
4800    let additions = additions.join(", ");
4801    let Some(start) = trailing_generic_arguments_start(callee) else {
4802        return format!("{callee}<{additions}>");
4803    };
4804    let existing = &callee[start + 1..callee.len() - 1];
4805    let separator = if existing.is_empty() { "" } else { ", " };
4806    format!("{}<{existing}{separator}{additions}>", &callee[..start])
4807}
4808
4809fn trailing_generic_arguments_start(value: &str) -> Option<usize> {
4810    if !value.ends_with('>') {
4811        return None;
4812    }
4813    let mut depth = 0usize;
4814    for (index, character) in value.char_indices().rev() {
4815        match character {
4816            '>' => depth += 1,
4817            '<' => {
4818                depth = depth.checked_sub(1)?;
4819                if depth == 0 {
4820                    return Some(index);
4821                }
4822            }
4823            _ => {}
4824        }
4825    }
4826    None
4827}
4828
4829fn simple_receiver(expression: &Expr) -> bool {
4830    match expression {
4831        Expr::Path(_) => true,
4832        Expr::Field(field) => simple_receiver(&field.base),
4833        Expr::Index(index) => simple_receiver(&index.expr),
4834        Expr::Paren(paren) => simple_receiver(&paren.expr),
4835        Expr::Group(group) => simple_receiver(&group.expr),
4836        Expr::Reference(reference) => simple_receiver(&reference.expr),
4837        Expr::Unary(unary) => simple_receiver(&unary.expr),
4838        _ => false,
4839    }
4840}
4841
4842fn callable_path(expression: &Expr) -> Option<Vec<String>> {
4843    match expression {
4844        Expr::Path(path) => Some(
4845            path.path
4846                .segments
4847                .iter()
4848                .map(|segment| segment.ident.to_string())
4849                .collect(),
4850        ),
4851        Expr::Group(group) => callable_path(&group.expr),
4852        Expr::Paren(paren) => callable_path(&paren.expr),
4853        _ => None,
4854    }
4855}
4856
4857fn is_self_expr(expression: &Expr) -> bool {
4858    matches!(expression, Expr::Path(path) if path.path.is_ident("self"))
4859}
4860
4861fn receiver_name(expression: &Expr) -> String {
4862    match expression {
4863        Expr::Path(path) => path
4864            .path
4865            .segments
4866            .iter()
4867            .map(|segment| segment.ident.to_string())
4868            .collect::<Vec<_>>()
4869            .join("::"),
4870        _ => "<expr>".to_owned(),
4871    }
4872}
4873
4874#[cfg(test)]
4875mod tests {
4876    use std::fs;
4877    use std::path::Path;
4878
4879    use super::*;
4880
4881    #[test]
4882    fn extracts_functions_methods_calls_and_signature_facts() {
4883        let source = r#"
4884            pub trait Store {
4885                fn save(&mut self, value: Item) {
4886                    validate(value);
4887                }
4888            }
4889
4890            struct Database;
4891
4892            impl Store for Database {
4893                fn save(&mut self, value: Item) {
4894                    self.prepare();
4895                    persist(value);
4896                }
4897            }
4898        "#;
4899        let frontend = RustBackend;
4900        let analysis = frontend
4901            .analyze_file(
4902                &FileContext {
4903                    path: Path::new("src/lib.rs"),
4904                    module: &[],
4905                },
4906                source,
4907            )
4908            .unwrap();
4909
4910        assert_eq!(analysis.functions.len(), 2);
4911        let implementation = analysis
4912            .functions
4913            .iter()
4914            .find(|function| function.id.container.as_deref() == Some("Database as Store"))
4915            .unwrap();
4916        assert_eq!(implementation.calls.len(), 2);
4917        assert!(analysis.facts.iter().any(|fact| {
4918            fact.subject == implementation.id
4919                && fact.kind == "receiver"
4920                && fact.value == "&mut self"
4921        }));
4922        assert!(analysis.facts.iter().any(|fact| {
4923            fact.subject == implementation.id && fact.kind == "impl-trait" && fact.key == "Store"
4924        }));
4925    }
4926
4927    #[test]
4928    fn closure_arguments_are_labels_not_serialized_bodies() {
4929        let source = r#"
4930            fn run(values: &[String]) {
4931                values
4932                    .iter()
4933                    .filter_map(|value| {
4934                        parse(value);
4935                        Some(value)
4936                    })
4937                    .collect::<Vec<_>>();
4938            }
4939            fn parse(_: &String) {}
4940        "#;
4941        let frontend = RustBackend;
4942        let analysis = frontend
4943            .analyze_file(
4944                &FileContext {
4945                    path: Path::new("src/lib.rs"),
4946                    module: &[],
4947                },
4948                source,
4949            )
4950            .unwrap();
4951
4952        let run = analysis
4953            .functions
4954            .iter()
4955            .find(|function| function.label.default.starts_with("run("))
4956            .unwrap();
4957        let labels = run
4958            .calls
4959            .iter()
4960            .map(|call| call.label.default.as_str())
4961            .collect::<Vec<_>>();
4962
4963        assert_eq!(
4964            labels,
4965            ["values.iter()", "filter_map<λ#1>()", "collect<Vec<_>>()",]
4966        );
4967        assert!(labels.iter().all(|label| !label.contains("parse(value)")));
4968
4969        let closure = analysis
4970            .functions
4971            .iter()
4972            .find(|function| function.label.default.starts_with("λ#1("))
4973            .unwrap();
4974        assert!(
4975            closure
4976                .calls
4977                .iter()
4978                .any(|call| call.label.default == "parse(value)")
4979        );
4980    }
4981
4982    #[test]
4983    fn rustc_public_can_analyze_a_compilable_file() {
4984        let directory =
4985            std::env::temp_dir().join(format!("diffkit-rustc-public-{}", std::process::id()));
4986        fs::create_dir_all(&directory).unwrap();
4987        let path = directory.join("input.rs");
4988        fs::write(&path, "pub fn entry() { helper(); } fn helper() {}\n").unwrap();
4989
4990        let analysis = analyze_semantic_file(&path).unwrap();
4991
4992        assert!(
4993            analysis
4994                .functions
4995                .iter()
4996                .any(|function| function.id.name.ends_with("::entry"))
4997        );
4998        assert!(
4999            analysis
5000                .functions
5001                .iter()
5002                .any(|function| function.id.name.ends_with("::helper"))
5003        );
5004        fs::remove_dir_all(directory).unwrap();
5005    }
5006
5007    #[test]
5008    fn semantic_analysis_omits_enum_variant_constructors() {
5009        let directory = std::env::temp_dir().join(format!(
5010            "diffkit-rustc-public-constructors-{}",
5011            std::process::id()
5012        ));
5013        fs::create_dir_all(&directory).unwrap();
5014        let path = directory.join("input.rs");
5015        let source = r#"
5016            pub struct Node;
5017            pub fn root(before: Option<&Node>, after: Option<&Node>) -> Option<()> {
5018                match (before, after) {
5019                    (Some(before), Some(after)) => Some(inner(Some(before), Some(after))),
5020                    _ => None,
5021                }
5022            }
5023            fn inner(_: Option<&Node>, _: Option<&Node>) {}
5024            pub fn is_json(value: Option<&str>) -> bool { value == Some("json") }
5025        "#;
5026        fs::write(&path, source).unwrap();
5027
5028        let analysis = analyze_semantic_file(&path).unwrap();
5029        let root = analysis
5030            .functions
5031            .iter()
5032            .find(|function| function.label.default.starts_with("root("))
5033            .unwrap();
5034        let calls = root
5035            .calls
5036            .iter()
5037            .map(|call| call.label.default.as_str())
5038            .collect::<Vec<_>>();
5039
5040        assert_eq!(calls, ["inner(Some(before), Some(after))"]);
5041        let is_json = analysis
5042            .functions
5043            .iter()
5044            .find(|function| function.label.default.starts_with("is_json("))
5045            .unwrap();
5046        assert!(is_json.calls.is_empty(), "{:#?}", is_json.calls);
5047        fs::remove_dir_all(directory).unwrap();
5048    }
5049
5050    #[test]
5051    fn semantic_labels_do_not_expose_external_inferred_generics_or_closure_bodies() {
5052        let directory = std::env::temp_dir().join(format!(
5053            "diffkit-rustc-public-call-labels-{}",
5054            std::process::id()
5055        ));
5056        fs::create_dir_all(&directory).unwrap();
5057        let path = directory.join("input.rs");
5058        fs::write(
5059            &path,
5060            r#"
5061                pub fn run(values: &[String]) -> Vec<usize> {
5062                    values
5063                        .iter()
5064                        .filter_map(|value| value.parse::<usize>().ok())
5065                        .collect()
5066                }
5067            "#,
5068        )
5069        .unwrap();
5070
5071        let analysis = analyze_semantic_file(&path).unwrap();
5072        let run = analysis
5073            .functions
5074            .iter()
5075            .find(|function| function.label.default.starts_with("run("))
5076            .unwrap();
5077        let labels = run
5078            .calls
5079            .iter()
5080            .map(|call| call.label.default.as_str())
5081            .collect::<Vec<_>>();
5082
5083        assert_eq!(labels, ["values.iter()", "filter_map<λ#1>()", "collect()"]);
5084        assert!(
5085            labels
5086                .iter()
5087                .all(|label| !label.contains("impl ") && !label.contains("|value|"))
5088        );
5089        fs::remove_dir_all(directory).unwrap();
5090    }
5091
5092    #[test]
5093    fn rustc_public_resolves_generic_trait_dispatch() {
5094        let directory = std::env::temp_dir().join(format!(
5095            "diffkit-rustc-public-generics-{}",
5096            std::process::id()
5097        ));
5098        fs::create_dir_all(&directory).unwrap();
5099        let path = directory.join("input.rs");
5100        fs::write(
5101            &path,
5102            r#"
5103                #[derive(Clone, Copy)]
5104                pub struct Order;
5105                trait Store { fn save(&self, order: Order); }
5106                struct Postgres;
5107                impl Store for Postgres {
5108                    fn save(&self, order: Order) {
5109                        sql::begin();
5110                        sql::insert(order);
5111                        sql::commit();
5112                    }
5113                }
5114                struct S3;
5115                impl Store for S3 {
5116                    fn save(&self, order: Order) {
5117                        aws::sign(order);
5118                        aws::put_object(order);
5119                    }
5120                }
5121                fn run<S: Store>(storage: &S, order: Order) {
5122                    validate(order);
5123                    storage.save(order);
5124                    finalize(order);
5125                }
5126                pub fn entry(order: Order) {
5127                    run(&Postgres, order);
5128                    run(&S3, order);
5129                }
5130                fn validate(_: Order) {}
5131                fn finalize(_: Order) {}
5132                mod sql {
5133                    use super::Order;
5134                    pub fn begin() {}
5135                    pub fn insert(_: Order) {}
5136                    pub fn commit() {}
5137                }
5138                mod aws {
5139                    use super::Order;
5140                    pub fn sign(_: Order) {}
5141                    pub fn put_object(_: Order) {}
5142                }
5143            "#,
5144        )
5145        .unwrap();
5146
5147        let analysis = analyze_semantic_file(&path).unwrap();
5148        let labels = analysis
5149            .functions
5150            .iter()
5151            .map(|function| {
5152                (
5153                    function.label.default.clone(),
5154                    function
5155                        .calls
5156                        .iter()
5157                        .map(|call| call.label.default.clone())
5158                        .collect::<Vec<_>>(),
5159                )
5160            })
5161            .collect::<Vec<_>>();
5162        assert!(labels.iter().any(|(label, calls)| {
5163            label == "run<Postgres>(storage, order)"
5164                && calls.iter().any(|call| call == "Postgres::save(order)")
5165        }));
5166        assert!(labels.iter().any(|(label, calls)| {
5167            label == "run<S3>(storage, order)" && calls.iter().any(|call| call == "S3::save(order)")
5168        }));
5169        fs::remove_dir_all(directory).unwrap();
5170    }
5171
5172    #[test]
5173    fn specialization_consumes_duplicate_normalized_instances() {
5174        let span = SourceSpan {
5175            file: PathBuf::from("src/lib.rs"),
5176            start_line: 1,
5177            start_column: 1,
5178            start_byte: None,
5179            end_line: 1,
5180            end_column: 1,
5181            end_byte: None,
5182        };
5183        let function = || RawSemanticFunction {
5184            key: "crate::duplicate".to_owned(),
5185            display: "duplicate".to_owned(),
5186            body_span: span.clone(),
5187            calls: Vec::new(),
5188            constructor_spans: Vec::new(),
5189            constructor_names: Vec::new(),
5190            parameter_types: Vec::new(),
5191        };
5192
5193        let specialized =
5194            specialize_semantic_functions(&[function(), function()], &[], &HashMap::new(), "crate");
5195
5196        assert_eq!(specialized.len(), 1);
5197        assert_eq!(specialized[0].key, "crate::duplicate");
5198    }
5199
5200    #[test]
5201    fn closure_locations_survive_until_the_source_lambda_mapping() {
5202        assert_eq!(
5203            normalize_instance_key("core::result::Result::map::<{closure@src/lib.rs:10:4: 10:12}>"),
5204            "core::result::Result::map<{closure@src/lib.rs:10:4: 10:12}>"
5205        );
5206        assert_eq!(
5207            normalize_type_display(
5208                "diffkit::Runner<{async block@src/lib.rs:20:8: 24:9}>",
5209                "diffkit"
5210            ),
5211            "Runner<{async block}>"
5212        );
5213    }
5214}