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