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
51pub 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
113pub fn rustc_wrapper_requested() -> bool {
116 env::var_os(RUSTC_CAPTURE_DIRECTORY).is_some()
117}
118
119pub 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
170pub(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 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 .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
549pub(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 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 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
1744fn 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 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 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 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(¶meter.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(¶meter.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(¶meter.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 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 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}