1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet, VecDeque};
3use std::fs;
4use std::io;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use sha2::{Digest, Sha256};
10use tree_sitter::{Node, Parser, Tree};
11
12use super::{FileContext, FrontendResult, LanguageBackend, ProjectContext};
13use crate::model::{
14 CallLabel, CallSite, CallSiteId, CallSyntax, CallTarget, DispatchCandidate, DispatchEvidence,
15 DispatchResolution, FileAnalysis, FunctionInfo, LanguageFact, LanguageId, SourceSpan, SymbolId,
16 UnresolvedReason,
17};
18use crate::source::{collect_files_with_extension, collect_source_files, paths_match};
19
20static OCAML_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
21const OCAML_EXTRACTOR_SOURCE: &str = include_str!("../../support/ocaml/extract.ml");
22
23thread_local! {
24 static OCAML_EXPRESSION_PARSER: RefCell<Option<Parser>> = RefCell::new({
28 let mut parser = Parser::new();
29 parser
30 .set_language(&tree_sitter_ocaml::LANGUAGE_OCAML.into())
31 .ok()
32 .map(|_| parser)
33 });
34}
35
36fn parse_ocaml_expression(source: &str) -> Option<Tree> {
37 OCAML_EXPRESSION_PARSER.with(|parser| parser.borrow_mut().as_mut()?.parse(source, None))
38}
39
40#[derive(Default)]
44pub struct OcamlFrontend;
45
46pub static OCAML_BACKEND: OcamlFrontend = OcamlFrontend;
47
48pub fn analyze_semantic_project(root: &Path) -> FrontendResult<FileAnalysis> {
52 let root = root.canonicalize()?;
53 let build = Command::new("dune")
54 .args(["build", "@check", "--display", "quiet"])
55 .current_dir(&root)
56 .output()
57 .map_err(|error| {
58 io::Error::new(
59 error.kind(),
60 format!("failed to run Dune for OCaml semantic analysis: {error}"),
61 )
62 })?;
63 if !build.status.success() {
64 let diagnostic = String::from_utf8_lossy(&build.stderr).trim().to_owned();
65 return Err(io::Error::other(if diagnostic.is_empty() {
66 format!("Dune semantic build exited with {}", build.status)
67 } else {
68 diagnostic
69 })
70 .into());
71 }
72
73 let helper = OcamlExtractor::compile()?;
74 let cmt_files = collect_files_with_extension(&root.join("_build"), "cmt")?;
75 let mut events = Vec::new();
76 for cmt in cmt_files {
77 let output = Command::new(helper.executable()).arg(&cmt).output()?;
78 if !output.status.success() {
79 let diagnostic = String::from_utf8_lossy(&output.stderr).trim().to_owned();
80 return Err(io::Error::other(format!(
81 "compiler-libs could not read {}: {}",
82 cmt.display(),
83 diagnostic
84 ))
85 .into());
86 }
87 events.extend(parse_typed_events(
88 &root,
89 &String::from_utf8(output.stdout)?,
90 )?);
91 }
92
93 let source_files = collect_source_files(&root, &["ml", "mli"])?;
94 let mut analysis = FileAnalysis::default();
95 for file in source_files {
96 let source = fs::read_to_string(&file)?;
97 let module = ocaml_file_module_path(&root, &file);
98 let mut file_analysis = analyze_ocaml_syntax(
99 &FileContext {
100 path: &file,
101 module: &module,
102 },
103 &source,
104 )?;
105 apply_typed_events(&mut file_analysis, &events);
106 analysis.append(file_analysis);
107 }
108 resolve_ocaml_function_values(&mut analysis);
109 Ok(analysis)
110}
111
112pub fn analyze_source_project(root: &Path) -> FrontendResult<FileAnalysis> {
115 let root = root.canonicalize()?;
116 let files = collect_source_files(&root, &["ml", "mli"])?;
117 let mut analysis = FileAnalysis::default();
118 for file in files {
119 let source = fs::read_to_string(&file)?;
120 let module = ocaml_file_module_path(&root, &file);
121 let mut file_analysis = analyze_ocaml_syntax(
122 &FileContext {
123 path: &file,
124 module: &module,
125 },
126 &source,
127 )?;
128 if let Some(events) = standalone_typed_events(&file, &source)? {
129 apply_typed_events(&mut file_analysis, &events);
130 }
131 analysis.append(file_analysis);
132 }
133 resolve_ocaml_function_values(&mut analysis);
134 Ok(analysis)
135}
136
137struct OcamlExtractor {
138 directory: PathBuf,
139}
140
141impl OcamlExtractor {
142 fn compile() -> FrontendResult<Self> {
143 let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
144 let directory = std::env::temp_dir().join(format!(
145 "diffkit-ocaml-extractor-{}-{sequence}",
146 std::process::id()
147 ));
148 fs::create_dir(&directory)?;
149 let extractor = Self { directory };
150 let source = extractor.directory.join("extract.ml");
151 fs::write(&source, OCAML_EXTRACTOR_SOURCE)?;
152 let output = Command::new("ocamlc")
153 .args(["-I", "+compiler-libs", "ocamlcommon.cma"])
154 .arg(&source)
155 .arg("-o")
156 .arg(extractor.executable())
157 .output()
158 .map_err(|error| {
159 io::Error::new(
160 error.kind(),
161 format!("failed to start ocamlc for compiler-libs adapter: {error}"),
162 )
163 })?;
164 if !output.status.success() {
165 let diagnostic = String::from_utf8_lossy(&output.stderr).trim().to_owned();
166 return Err(io::Error::other(format!(
167 "failed to compile compiler-libs adapter: {diagnostic}"
168 ))
169 .into());
170 }
171 Ok(extractor)
172 }
173
174 fn executable(&self) -> PathBuf {
175 self.directory.join("extract")
176 }
177}
178
179impl Drop for OcamlExtractor {
180 fn drop(&mut self) {
181 let _ = fs::remove_dir_all(&self.directory);
182 }
183}
184
185#[derive(Clone, Debug)]
186struct TypedCallEvent {
187 target: Option<String>,
188 signature: Option<String>,
189 span: SourceSpan,
190}
191
192impl LanguageBackend for OcamlFrontend {
193 fn language(&self) -> LanguageId {
194 LanguageId::new("ocaml")
195 }
196
197 fn extensions(&self) -> &'static [&'static str] {
198 &["ml", "mli"]
199 }
200
201 fn analyze_file(
202 &self,
203 context: &FileContext<'_>,
204 source: &str,
205 ) -> FrontendResult<FileAnalysis> {
206 let mut analysis = analyze_ocaml_syntax(context, source)?;
207 if let Some(events) = standalone_typed_events(context.path, source)? {
208 apply_typed_events(&mut analysis, &events);
209 }
210 resolve_ocaml_function_values(&mut analysis);
211 Ok(analysis)
212 }
213
214 fn analyze_project(&self, context: &ProjectContext<'_>) -> FrontendResult<FileAnalysis> {
215 if context.root.join("dune-project").is_file() {
216 analyze_semantic_project(context.root)
217 } else {
218 analyze_source_project(context.root)
219 }
220 }
221}
222
223fn standalone_typed_events(
224 source_path: &Path,
225 source: &str,
226) -> FrontendResult<Option<Vec<TypedCallEvent>>> {
227 if Command::new("ocamlc").arg("-version").output().is_err() {
228 return Ok(None);
229 }
230 let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
231 let directory = std::env::temp_dir().join(format!(
232 "diffkit-ocaml-source-{}-{sequence}",
233 std::process::id()
234 ));
235 fs::create_dir(&directory)?;
236 let capture = StandaloneOcamlCapture { directory };
237 let extension = source_path
238 .extension()
239 .and_then(|extension| extension.to_str())
240 .unwrap_or("ml");
241 let temporary_source = capture.directory.join(format!("input.{extension}"));
242 fs::write(&temporary_source, source)?;
243 let compile = Command::new("ocamlc")
244 .args(["-bin-annot", "-c"])
245 .arg(&temporary_source)
246 .current_dir(&capture.directory)
247 .output()?;
248 if !compile.status.success() {
249 return Ok(None);
253 }
254 let annotation = if extension == "mli" {
255 capture.directory.join("input.cmti")
256 } else {
257 capture.directory.join("input.cmt")
258 };
259 if !annotation.is_file() {
260 return Ok(None);
261 }
262 let helper = OcamlExtractor::compile()?;
263 let output = Command::new(helper.executable()).arg(annotation).output()?;
264 if !output.status.success() {
265 return Ok(None);
266 }
267 let mut events = parse_typed_events(&capture.directory, &String::from_utf8(output.stdout)?)?;
268 for event in &mut events {
269 event.span.file = source_path.to_path_buf();
270 }
271 Ok(Some(events))
272}
273
274struct StandaloneOcamlCapture {
275 directory: PathBuf,
276}
277
278impl Drop for StandaloneOcamlCapture {
279 fn drop(&mut self) {
280 let _ = fs::remove_dir_all(&self.directory);
281 }
282}
283
284fn analyze_ocaml_syntax(context: &FileContext<'_>, source: &str) -> FrontendResult<FileAnalysis> {
285 let mut parser = Parser::new();
286 let language = if context.path.extension().and_then(|value| value.to_str()) == Some("mli") {
287 tree_sitter_ocaml::LANGUAGE_OCAML_INTERFACE
288 } else {
289 tree_sitter_ocaml::LANGUAGE_OCAML
290 };
291 parser
292 .set_language(&language.into())
293 .map_err(|error| io::Error::other(format!("failed to load OCaml grammar: {error}")))?;
294 let tree = parser
295 .parse(source, None)
296 .ok_or_else(|| io::Error::other("OCaml parser returned no syntax tree"))?;
297 if tree.root_node().has_error() {
298 return Err(io::Error::other(format!(
299 "failed to parse OCaml source: {}",
300 context.path.display()
301 ))
302 .into());
303 }
304
305 let mut analysis = FileAnalysis::default();
306 analysis.source_files.insert(context.path.to_path_buf());
307 let module = context
308 .module
309 .iter()
310 .map(|part| ocaml_module_name(part))
311 .collect::<Vec<_>>();
312 analyze_structure(
313 context.path,
314 &module,
315 tree.root_node(),
316 source,
317 &mut analysis,
318 );
319 disambiguate_ocaml_shadowed_bindings(&mut analysis);
320 resolve_ocaml_recursive_groups(&mut analysis);
321 resolve_local_callables(&mut analysis);
322 redact_anonymous_call_arguments(&mut analysis);
323 Ok(analysis)
324}
325
326#[derive(Clone)]
327struct OcamlBindingRemap {
328 old: SymbolId,
329 new: SymbolId,
330 owner_span: SourceSpan,
331}
332
333fn disambiguate_ocaml_shadowed_bindings(analysis: &mut FileAnalysis) {
337 let mut groups = BTreeMap::<(PathBuf, Vec<String>, String), Vec<usize>>::new();
338 for (index, function) in analysis.functions.iter().enumerate() {
339 if function.id.container.is_none() {
340 groups
341 .entry((
342 function.span.file.clone(),
343 function.id.module.clone(),
344 function.id.name.clone(),
345 ))
346 .or_default()
347 .push(index);
348 }
349 }
350
351 let mut remaps = Vec::new();
352 for indices in groups.values_mut().filter(|indices| indices.len() > 1) {
353 indices.sort_by_key(|index| {
354 let function = &analysis.functions[*index];
355 (
356 function.span.start_line,
357 function.span.start_column,
358 function.span.end_line,
359 function.span.end_column,
360 )
361 });
362 for (ordinal, index) in indices.iter().copied().enumerate() {
363 let owner = analysis.functions[index].clone();
364 let old_owner_text = owner.id.to_string();
365 let mut new_owner = owner.id.clone();
366 if ordinal + 1 != indices.len() {
367 new_owner.name = format!("{}#binding{ordinal}", owner.id.name);
368 }
369 let new_owner_text = new_owner.to_string();
370
371 for function in &mut analysis.functions {
372 if function.span.file != owner.span.file || !owner.span.contains(&function.span) {
373 continue;
374 }
375 let old = function.id.clone();
376 if old == owner.id {
377 function.id = new_owner.clone();
378 } else if let Some(container) = &old.container
379 && container.contains(&old_owner_text)
380 {
381 function.id.container =
382 Some(container.replacen(&old_owner_text, &new_owner_text, 1));
383 } else {
384 continue;
385 }
386 remaps.push(OcamlBindingRemap {
387 old,
388 new: function.id.clone(),
389 owner_span: owner.span.clone(),
390 });
391 }
392 }
393 }
394
395 for fact in &mut analysis.facts {
396 if let Some(remap) = remaps.iter().find(|remap| {
397 fact.subject == remap.old
398 && fact.span.file == remap.owner_span.file
399 && remap.owner_span.contains(&fact.span)
400 }) {
401 fact.subject = remap.new.clone();
402 }
403 }
404
405 analysis.roots = analysis
406 .roots
407 .iter()
408 .map(|root| {
409 remaps
410 .iter()
411 .find(|remap| remap.old == *root)
412 .map_or_else(|| root.clone(), |remap| remap.new.clone())
413 })
414 .collect();
415}
416
417fn resolve_ocaml_recursive_groups(analysis: &mut FileAnalysis) {
418 let groups = analysis
419 .facts
420 .iter()
421 .filter(|fact| fact.kind == "recursive-group")
422 .fold(
423 BTreeMap::<(PathBuf, String), Vec<SymbolId>>::new(),
424 |mut groups, fact| {
425 groups
426 .entry((fact.span.file.clone(), fact.value.clone()))
427 .or_default()
428 .push(fact.subject.clone());
429 groups
430 },
431 );
432 let function_index = analysis.functions.clone();
433 for members in groups.values() {
434 let owners = members
435 .iter()
436 .filter_map(|member| {
437 function_index
438 .iter()
439 .find(|function| function.id == *member)
440 })
441 .collect::<Vec<_>>();
442 for function in &mut analysis.functions {
443 if !owners.iter().any(|owner| {
444 owner.span.file == function.span.file && owner.span.contains(&function.span)
445 }) {
446 continue;
447 }
448 for call in &mut function.calls {
449 let CallSyntax::Path(parts) = call.syntax.visible() else {
450 continue;
451 };
452 let Some(name) = parts.last() else {
453 continue;
454 };
455 let mut targets = owners
456 .iter()
457 .filter(|owner| {
458 local_callable_source_name(owner) == name
459 && (parts.len() == 1 || parts[..parts.len() - 1] == owner.id.module)
460 })
461 .map(|owner| owner.id.clone())
462 .collect::<Vec<_>>();
463 targets.sort();
464 targets.dedup();
465 if targets.len() == 1 {
466 call.target = CallTarget::Direct(targets.remove(0));
467 }
468 }
469 }
470 }
471}
472
473fn ocaml_file_module_path(root: &Path, file: &Path) -> Vec<String> {
474 let relative = file.strip_prefix(root).unwrap_or(file);
475 relative
476 .file_stem()
477 .and_then(|stem| stem.to_str())
478 .map(|stem| vec![stem.to_owned()])
479 .unwrap_or_default()
480}
481
482fn parse_typed_events(root: &Path, output: &str) -> FrontendResult<Vec<TypedCallEvent>> {
483 output
484 .lines()
485 .filter(|line| !line.trim().is_empty())
486 .map(|line| {
487 let fields = line.split('\t').collect::<Vec<_>>();
488 if fields.len() != 8 {
489 return Err(
490 io::Error::other(format!("invalid compiler-libs event: {line}")).into(),
491 );
492 }
493 let file = PathBuf::from(fields[2]);
494 let file = if file.is_absolute() {
495 file
496 } else {
497 root.join(file)
498 };
499 let file = dune_source_path(root, &file).unwrap_or(file);
500 Ok(TypedCallEvent {
501 target: (fields[0] == "direct").then(|| fields[1].to_owned()),
502 signature: (!fields[7].is_empty()).then(|| fields[7].to_owned()),
503 span: SourceSpan {
504 file,
505 start_line: fields[3].parse()?,
506 start_column: fields[4].parse()?,
507 start_byte: None,
508 end_line: fields[5].parse()?,
509 end_column: fields[6].parse()?,
510 end_byte: None,
511 },
512 })
513 })
514 .collect()
515}
516
517fn dune_source_path(root: &Path, file: &Path) -> Option<PathBuf> {
518 let relative = file
519 .strip_prefix(root.join("_build"))
520 .ok()
521 .or_else(|| file.strip_prefix("_build").ok())?;
522 let components = relative.components().collect::<Vec<_>>();
523 let skip = if components
524 .first()
525 .is_some_and(|component| component.as_os_str() == ".sandbox")
526 {
527 3
528 } else {
529 1
530 };
531 let candidate = components
532 .get(skip..)?
533 .iter()
534 .fold(root.to_path_buf(), |path, component| {
535 path.join(component.as_os_str())
536 });
537 candidate.is_file().then_some(candidate)
538}
539
540fn apply_typed_events(analysis: &mut FileAnalysis, events: &[TypedCallEvent]) {
541 let parameters = ocaml_parameters(&analysis.functions, &analysis.facts);
542 let local_values = ocaml_local_values(&analysis.facts);
543 let local_symbols = analysis
544 .functions
545 .iter()
546 .map(|function| function.id.clone())
547 .collect::<BTreeSet<_>>();
548 let mut claimed = BTreeSet::new();
549 for function in &mut analysis.functions {
550 for call in &mut function.calls {
551 if matches!(
552 &call.target,
553 CallTarget::Direct(target) if local_symbols.contains(target)
554 ) {
555 continue;
556 }
557 if matches!(call.target, CallTarget::Dynamic { .. }) {
558 continue;
559 }
560 let Some((event_index, event)) = events
561 .iter()
562 .enumerate()
563 .filter(|(index, _)| !claimed.contains(index))
564 .filter(|event| {
565 ocaml_source_files_match(&event.1.span.file, &call.span.file)
566 && event.1.span.overlaps(&call.span)
567 })
568 .min_by_key(|(_, event)| event.span.boundary_distance(&call.span))
569 else {
570 continue;
571 };
572 claimed.insert(event_index);
573 if let Some(signature) = &event.signature {
574 call.label.typed = Some(annotate_ocaml_call(&call.label.default, call, signature));
575 }
576 let is_flow_resolved_callable = match call.syntax.visible() {
577 CallSyntax::Path(parts) => {
578 parameters.get(&function.id).is_some_and(|bindings| {
579 called_ocaml_parameter_index(parts, bindings, Some(&call.span)).is_some()
580 }) || (parts.len() == 1
581 && unique_ocaml_local_value(
582 &local_values,
583 &function.id,
584 &parts[0],
585 Some(&call.span),
586 )
587 .is_some())
588 }
589 _ => false,
590 };
591 call.target = match &event.target {
592 _ if is_flow_resolved_callable => CallTarget::Unresolved,
593 Some(target) => CallTarget::Direct(ocaml_path_symbol(target)),
594 None => CallTarget::Indirect {
595 signature: event.signature.clone(),
596 reason: UnresolvedReason::OpaqueInput,
597 },
598 };
599 }
600 }
601}
602
603fn annotate_ocaml_call(label: &str, call: &CallSite, signature: &str) -> String {
604 let arguments = ocaml_call_arguments(call);
605 if arguments.is_empty() {
606 return label.to_owned();
607 }
608 let mut types = split_ocaml_function_type(signature);
609 if types.len() <= 1 {
610 return label.to_owned();
611 }
612 types.pop(); if types.len() < arguments.len() {
614 return label.to_owned();
615 }
616 let callee = match call.syntax.visible() {
617 CallSyntax::Path(parts) => parts.join("."),
618 CallSyntax::SelfMethod(method) => method.clone(),
619 CallSyntax::Method { receiver, method } => format!("{receiver}#{method}"),
620 CallSyntax::Expression(expression) => expression.clone(),
621 CallSyntax::CompilerConfirmed(syntax) => syntax.key_fragment(),
622 };
623 let types = &types[types.len() - arguments.len()..];
624 let annotated = arguments
625 .iter()
626 .zip(types)
627 .map(|(argument, ty)| {
628 let ty = ty.split_once(':').map_or(ty.as_str(), |(_, ty)| ty).trim();
629 format!("({argument}: {ty})")
630 })
631 .collect::<Vec<_>>()
632 .join(" ");
633 format!("{callee} {annotated}")
634}
635
636fn split_ocaml_function_type(signature: &str) -> Vec<String> {
637 let mut result = Vec::new();
638 let mut start = 0usize;
639 let mut depth = 0usize;
640 let bytes = signature.as_bytes();
641 let mut index = 0usize;
642 while index + 1 < bytes.len() {
643 match bytes[index] {
644 b'(' | b'[' | b'{' => depth += 1,
645 b')' | b']' | b'}' => depth = depth.saturating_sub(1),
646 b'-' if bytes[index + 1] == b'>' && depth == 0 => {
647 result.push(signature[start..index].trim().to_owned());
648 start = index + 2;
649 index += 1;
650 }
651 _ => {}
652 }
653 index += 1;
654 }
655 result.push(signature[start..].trim().to_owned());
656 result
657}
658
659fn ocaml_path_symbol(path: &str) -> SymbolId {
660 let mut parts = path
661 .trim_start_matches("global ")
662 .split('.')
663 .filter(|part| !part.is_empty())
664 .map(|part| part.rsplit("__").next().unwrap_or(part).to_owned())
665 .collect::<Vec<_>>();
666 let name = parts.pop().unwrap_or_else(|| path.to_owned());
667 SymbolId {
668 language: LanguageId::new("ocaml"),
669 module: parts,
670 container: None,
671 name,
672 }
673}
674
675fn ocaml_source_files_match(left: &Path, right: &Path) -> bool {
676 paths_match(left, right)
677}
678
679fn ocaml_module_name(value: &str) -> String {
680 let mut characters = value.chars();
681 let Some(first) = characters.next() else {
682 return String::new();
683 };
684 first.to_uppercase().chain(characters).collect()
685}
686
687fn analyze_structure(
688 file: &Path,
689 module: &[String],
690 structure: Node<'_>,
691 source: &str,
692 analysis: &mut FileAnalysis,
693) {
694 let mut cursor = structure.walk();
695 for child in structure.named_children(&mut cursor) {
696 match child.kind() {
697 "module_definition" => analyze_module_definition(file, module, child, source, analysis),
698 "value_definition" => analyze_value_definition(file, module, child, source, analysis),
699 _ => {}
700 }
701 }
702}
703
704fn analyze_module_definition(
705 file: &Path,
706 module: &[String],
707 definition: Node<'_>,
708 source: &str,
709 analysis: &mut FileAnalysis,
710) {
711 let mut cursor = definition.walk();
712 for binding in definition
713 .named_children(&mut cursor)
714 .filter(|child| child.kind() == "module_binding")
715 {
716 let Some(name_node) = direct_named_child(binding, "module_name") else {
717 continue;
718 };
719 let Some(body) = binding.child_by_field_name("body") else {
720 continue;
721 };
722 if body.kind() == "module_path" {
723 analysis.facts.push(LanguageFact {
724 subject: SymbolId {
725 language: LanguageId::new("ocaml"),
726 module: module.to_vec(),
727 container: None,
728 name: node_text(name_node, source).to_owned(),
729 },
730 namespace: LanguageId::new("ocaml"),
731 kind: "module-alias".to_owned(),
732 key: "target".to_owned(),
733 value: normalize_source(node_text(body, source)),
734 span: tree_sitter_span(file, binding),
735 });
736 continue;
737 }
738 if body.kind() == "module_application" {
739 let Some((functor, arguments)) = ocaml_module_application(body, source) else {
740 continue;
741 };
742 analysis.facts.push(LanguageFact {
743 subject: SymbolId {
744 language: LanguageId::new("ocaml"),
745 module: module.to_vec(),
746 container: None,
747 name: node_text(name_node, source).to_owned(),
748 },
749 namespace: LanguageId::new("ocaml"),
750 kind: "functor-instance".to_owned(),
751 key: functor,
752 value: arguments.join("\t"),
753 span: tree_sitter_span(file, binding),
754 });
755 continue;
756 }
757 for (index, parameter) in direct_named_children(binding, "module_parameter")
758 .into_iter()
759 .enumerate()
760 {
761 let Some(parameter_name) = direct_named_child(parameter, "module_name") else {
762 continue;
763 };
764 analysis.facts.push(LanguageFact {
765 subject: SymbolId {
766 language: LanguageId::new("ocaml"),
767 module: module.to_vec(),
768 container: None,
769 name: node_text(name_node, source).to_owned(),
770 },
771 namespace: LanguageId::new("ocaml"),
772 kind: "functor-parameter".to_owned(),
773 key: index.to_string(),
774 value: node_text(parameter_name, source).to_owned(),
775 span: tree_sitter_span(file, parameter),
776 });
777 }
778 let body = if body.kind() == "structure" {
779 body
780 } else if let Some(structure) = first_descendant_of_kind(body, "structure") {
781 structure
782 } else {
783 continue;
784 };
785
786 let mut nested_module = module.to_vec();
787 nested_module.push(node_text(name_node, source).to_owned());
788 analyze_structure(file, &nested_module, body, source, analysis);
789 }
790}
791
792fn ocaml_module_application(node: Node<'_>, source: &str) -> Option<(String, Vec<String>)> {
793 if node.kind() == "module_path" {
794 return Some((normalize_source(node_text(node, source)), Vec::new()));
795 }
796 if node.kind() != "module_application" {
797 return None;
798 }
799 let functor = node.child_by_field_name("functor")?;
800 let (functor, mut arguments) = ocaml_module_application(functor, source)?;
801 let argument = node.child_by_field_name("argument")?;
802 let argument = if argument.kind() == "module_path" {
803 argument
804 } else {
805 first_descendant_of_kind(argument, "module_path")?
806 };
807 arguments.push(normalize_source(node_text(argument, source)));
808 Some((functor, arguments))
809}
810
811fn first_descendant_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
812 let mut cursor = node.walk();
813 for child in node.named_children(&mut cursor) {
814 if child.kind() == kind {
815 return Some(child);
816 }
817 if let Some(found) = first_descendant_of_kind(child, kind) {
818 return Some(found);
819 }
820 }
821 None
822}
823
824fn analyze_value_definition(
825 file: &Path,
826 module: &[String],
827 definition: Node<'_>,
828 source: &str,
829 analysis: &mut FileAnalysis,
830) {
831 let recursive_group = normalize_source(node_text(definition, source))
832 .starts_with("let rec ")
833 .then(|| {
834 format!(
835 "{}:{}-{}:{}",
836 definition.start_position().row,
837 definition.start_position().column,
838 definition.end_position().row,
839 definition.end_position().column,
840 )
841 });
842 let mut cursor = definition.walk();
843 for binding in definition
844 .named_children(&mut cursor)
845 .filter(|child| child.kind() == "let_binding")
846 {
847 if let Some(function) = function_from_binding(file, module, binding, source, analysis) {
848 if let Some(group) = &recursive_group {
849 analysis.facts.push(LanguageFact {
850 subject: function.id.clone(),
851 namespace: LanguageId::new("ocaml"),
852 kind: "recursive-group".to_owned(),
853 key: local_callable_source_name(&function).to_owned(),
854 value: group.clone(),
855 span: function.span.clone(),
856 });
857 }
858 let owner = function.id.clone();
859 analysis.functions.push(function);
860 collect_nested_callables_from_binding(file, module, &owner, binding, source, analysis);
861 }
862 }
863}
864
865#[allow(clippy::too_many_arguments)]
866fn collect_nested_callables(
867 file: &Path,
868 module: &[String],
869 owner: &SymbolId,
870 node: Node<'_>,
871 source: &str,
872 analysis: &mut FileAnalysis,
873 ordinal: &mut usize,
874) {
875 if node.kind() == "let_binding" {
876 let facts_start = analysis.facts.len();
877 if let Some(mut function) = function_from_binding(file, module, node, source, analysis) {
878 let display_name = function.id.name.clone();
879 let old_id = function.id.clone();
880 let current_ordinal = *ordinal;
881 *ordinal += 1;
882 function.id.container = Some(owner.to_string());
883 function.id.name = format!("{display_name}#closure{current_ordinal}");
884 function.label = local_callable_label(&function.label, &display_name, current_ordinal);
885 for fact in &mut analysis.facts[facts_start..] {
886 if fact.subject == old_id {
887 fact.subject = function.id.clone();
888 }
889 }
890 if let Some(scope) = ocaml_local_callable_scope(node, source) {
891 analysis.facts.push(LanguageFact {
892 subject: function.id.clone(),
893 namespace: LanguageId::new("ocaml"),
894 kind: "local-callable-scope".to_owned(),
895 key: display_name.clone(),
896 value: String::new(),
897 span: tree_sitter_span(file, scope),
898 });
899 }
900 let nested_owner = function.id.clone();
901 analysis.functions.push(function);
902 collect_nested_callables_from_binding(
903 file,
904 module,
905 &nested_owner,
906 node,
907 source,
908 analysis,
909 );
910 return;
911 }
912 }
913
914 if matches!(node.kind(), "fun_expression" | "function_expression") {
915 let current_ordinal = *ordinal;
916 *ordinal += 1;
917 let parameters = if node.kind() == "fun_expression" {
918 direct_named_children(node, "parameter")
919 } else {
920 Vec::new()
921 };
922 let parameter_labels = if parameters.is_empty() {
923 vec!["_".to_owned()]
924 } else {
925 parameters
926 .iter()
927 .map(|parameter| parameter_default_label(*parameter, source))
928 .collect()
929 };
930 let body = node.child_by_field_name("body").unwrap_or(node);
931 let expression = node_text(node, source);
932 let base = anonymous_callable_base(expression)
933 .unwrap_or_else(|| anonymous_callable_identity(expression));
934 let id = SymbolId {
935 language: LanguageId::new("ocaml"),
936 module: module.to_vec(),
937 container: Some(owner.to_string()),
938 name: format!("{base}#closure{current_ordinal}"),
939 };
940 analysis.facts.push(LanguageFact {
941 subject: id.clone(),
942 namespace: LanguageId::new("ocaml"),
943 kind: "closure-expression".to_owned(),
944 key: base,
945 value: normalize_source(node_text(node, source)),
946 span: tree_sitter_span(file, node),
947 });
948 for (index, parameter) in parameters.iter().enumerate() {
949 analysis.facts.push(LanguageFact {
950 subject: id.clone(),
951 namespace: LanguageId::new("ocaml"),
952 kind: "parameter".to_owned(),
953 key: parameter_labels[index].clone(),
954 value: parameter_pattern_and_type(*parameter)
955 .1
956 .map(|node| normalize_source(node_text(node, source)))
957 .unwrap_or_default(),
958 span: tree_sitter_span(file, *parameter),
959 });
960 }
961 collect_ocaml_parameter_refinement_facts(
962 file,
963 &id,
964 body,
965 source,
966 true,
967 &mut analysis.facts,
968 );
969 let mut calls = Vec::new();
970 collect_calls(file, body, source, true, &mut calls);
971 let lambda = format!("λ#{}", current_ordinal + 1);
972 let label = if parameter_labels.is_empty() {
973 lambda
974 } else {
975 format!("{lambda} {}", parameter_labels.join(" "))
976 };
977 let nested_owner = id.clone();
978 analysis.functions.push(FunctionInfo {
979 id,
980 label: CallLabel::new(label),
981 public: false,
982 calls,
983 span: tree_sitter_span(file, node),
984 });
985
986 let mut nested_ordinal = 0usize;
987 let mut cursor = body.walk();
988 for child in body.named_children(&mut cursor) {
989 collect_nested_callables(
990 file,
991 module,
992 &nested_owner,
993 child,
994 source,
995 analysis,
996 &mut nested_ordinal,
997 );
998 }
999 return;
1000 }
1001
1002 let mut cursor = node.walk();
1003 for child in node.named_children(&mut cursor) {
1004 collect_nested_callables(file, module, owner, child, source, analysis, ordinal);
1005 }
1006}
1007
1008fn ocaml_local_callable_scope<'tree>(binding: Node<'tree>, source: &str) -> Option<Node<'tree>> {
1009 let definition = binding
1010 .parent()
1011 .filter(|node| node.kind() == "value_definition")?;
1012 let expression = definition
1013 .parent()
1014 .filter(|node| node.kind() == "let_expression")?;
1015 if normalize_source(node_text(definition, source)).starts_with("let rec ") {
1016 Some(expression)
1017 } else {
1018 expression.child_by_field_name("body")
1019 }
1020}
1021
1022fn collect_nested_callables_from_binding(
1023 file: &Path,
1024 module: &[String],
1025 owner: &SymbolId,
1026 binding: Node<'_>,
1027 source: &str,
1028 analysis: &mut FileAnalysis,
1029) {
1030 let Some(mut body) = binding.child_by_field_name("body") else {
1031 return;
1032 };
1033 while body.kind() == "fun_expression" {
1034 let Some(next) = body.child_by_field_name("body") else {
1035 return;
1036 };
1037 body = next;
1038 }
1039 let mut ordinal = 0usize;
1040 if body.kind() == "function_expression" {
1041 let mut cursor = body.walk();
1042 for child in body.named_children(&mut cursor) {
1043 collect_nested_callables(file, module, owner, child, source, analysis, &mut ordinal);
1044 }
1045 } else {
1046 collect_nested_callables(file, module, owner, body, source, analysis, &mut ordinal);
1047 }
1048}
1049
1050fn anonymous_callable_base(expression: &str) -> Option<String> {
1051 let expression = normalize_ocaml_callable_argument(expression);
1052 if !expression.starts_with("fun ") && expression != "fun" && !expression.starts_with("function")
1053 {
1054 return None;
1055 }
1056 Some(anonymous_callable_identity(expression))
1057}
1058
1059fn anonymous_callable_identity(expression: &str) -> String {
1060 let normalized = expression.split_whitespace().collect::<Vec<_>>().join(" ");
1061 let mut hasher = Sha256::new();
1062 hasher.update(normalized.as_bytes());
1063 let digest = format!("{:x}", hasher.finalize());
1064 format!("{{lambda:{}}}", &digest[..12])
1065}
1066
1067fn local_callable_label(label: &CallLabel, name: &str, ordinal: usize) -> CallLabel {
1068 let rewrite = |text: &str| {
1069 let arguments = text
1070 .find(char::is_whitespace)
1071 .map_or("", |start| &text[start..]);
1072 format!("{name}{arguments} [closure#{ordinal}]")
1073 };
1074 CallLabel {
1075 default: rewrite(&label.default),
1076 typed: label.typed.as_deref().map(rewrite),
1077 }
1078}
1079
1080fn resolve_local_callables(analysis: &mut FileAnalysis) {
1081 let functions = analysis.functions.clone();
1082 let symbols = OcamlSymbols::new(&functions);
1083 let scopes = ocaml_local_callable_scopes(&analysis.facts);
1084 for function in &mut analysis.functions {
1085 for call in &mut function.calls {
1086 if !matches!(call.target, CallTarget::Unresolved) {
1087 continue;
1088 }
1089 if let CallSyntax::Expression(expression) = call.syntax.visible()
1090 && let Some(candidate) = anonymous_callable_symbol_at(
1091 expression,
1092 &function.id,
1093 &symbols,
1094 Some(&call.span),
1095 )
1096 && let Some(target) = symbols.get(&candidate)
1097 {
1098 let arguments = ocaml_call_arguments(call);
1099 call.target = CallTarget::Direct(candidate);
1100 call.label = CallLabel::new(ocaml_candidate_call_label(target, &arguments));
1101 continue;
1102 }
1103 let Some(name) = (match call.syntax.visible() {
1104 CallSyntax::Path(parts) if parts.len() == 1 => parts.first(),
1105 CallSyntax::Path(_)
1106 | CallSyntax::SelfMethod(_)
1107 | CallSyntax::Method { .. }
1108 | CallSyntax::Expression(_)
1109 | CallSyntax::CompilerConfirmed(_) => None,
1110 }) else {
1111 continue;
1112 };
1113 let candidate =
1114 local_callable_at(name, &function.id, &symbols, &scopes, Some(&call.span))
1115 .and_then(|id| symbols.get(&id));
1116 let Some(candidate) = candidate else {
1117 continue;
1118 };
1119 call.target = CallTarget::Direct(candidate.id.clone());
1120 if let Some(ordinal) = candidate
1121 .id
1122 .name
1123 .rsplit_once("#closure")
1124 .map(|(_, ordinal)| ordinal)
1125 {
1126 call.label = call.label.with_suffix(&format!(" [closure#{ordinal}]"));
1127 }
1128 }
1129 }
1130}
1131
1132fn local_callable_at(
1133 name: &str,
1134 caller: &SymbolId,
1135 symbols: &OcamlSymbols<'_>,
1136 scopes: &BTreeMap<SymbolId, SourceSpan>,
1137 position: Option<&SourceSpan>,
1138) -> Option<SymbolId> {
1139 let direct_owner = caller.to_string();
1140 let sibling_owner = caller.container.as_deref();
1141 let mut candidates = symbols
1142 .named(name)
1143 .filter(|candidate| {
1144 candidate.id.module == caller.module
1145 && candidate.id.container.is_some()
1146 && (candidate.id.container.as_deref() == Some(direct_owner.as_str())
1147 || candidate.id.container.as_deref() == sibling_owner
1148 || candidate.id == *caller)
1149 && (candidate.id == *caller
1150 || position.is_none_or(|position| {
1151 scopes
1152 .get(&candidate.id)
1153 .is_some_and(|scope| scope.contains(position))
1154 }))
1155 })
1156 .collect::<Vec<_>>();
1157 candidates.sort_by_key(|candidate| {
1158 let scope = scopes.get(&candidate.id).unwrap_or(&candidate.span);
1159 (
1160 scope.start_line,
1161 scope.start_column,
1162 candidate.span.start_line,
1163 candidate.span.start_column,
1164 candidate.id.clone(),
1165 )
1166 });
1167 candidates.last().map(|candidate| candidate.id.clone())
1168}
1169
1170fn local_callable_source_name(function: &FunctionInfo) -> &str {
1171 ocaml_source_name(&function.id.name)
1172}
1173
1174fn ocaml_source_name(name: &str) -> &str {
1175 let name = name.split_once("#ctx[").map_or(name, |(name, _)| name);
1176 let name = strip_ocaml_numeric_suffix(name, "#closure");
1177 strip_ocaml_numeric_suffix(name, "#binding")
1178}
1179
1180fn strip_ocaml_numeric_suffix<'a>(name: &'a str, marker: &str) -> &'a str {
1181 name.rsplit_once(marker).map_or(name, |(base, suffix)| {
1182 if suffix.chars().all(|character| character.is_ascii_digit()) {
1183 base
1184 } else {
1185 name
1186 }
1187 })
1188}
1189
1190fn redact_anonymous_call_arguments(analysis: &mut FileAnalysis) {
1191 let labels = analysis
1192 .functions
1193 .iter()
1194 .filter(|function| local_callable_source_name(function).starts_with("{lambda:"))
1195 .filter_map(|function| {
1196 let expression = analysis
1197 .facts
1198 .iter()
1199 .find(|fact| fact.subject == function.id && fact.kind == "closure-expression")?;
1200 let lambda = function.label.default.split_whitespace().next()?.to_owned();
1201 Some((expression.value.clone(), lambda))
1202 })
1203 .collect::<Vec<_>>();
1204 for function in &mut analysis.functions {
1205 for call in &mut function.calls {
1206 for (expression, lambda) in &labels {
1207 call.label.default = call.label.default.replace(expression, lambda);
1208 if let Some(typed) = &mut call.label.typed {
1209 *typed = typed.replace(expression, lambda);
1210 }
1211 }
1212 }
1213 }
1214}
1215
1216#[derive(Clone, Debug, Default, Eq, PartialEq)]
1217struct CallableFlow {
1218 candidates: BTreeSet<SymbolId>,
1219 unresolved_reasons: BTreeSet<UnresolvedReason>,
1220}
1221
1222#[derive(Clone, Debug)]
1223struct OcamlCallFlow {
1224 caller: SymbolId,
1225 target: SymbolId,
1226 arguments: Vec<String>,
1227 span: SourceSpan,
1228}
1229
1230struct OcamlSymbols<'a> {
1231 functions: &'a [FunctionInfo],
1232 by_id: BTreeMap<SymbolId, usize>,
1233 by_name: BTreeMap<String, Vec<usize>>,
1234 by_file: BTreeMap<PathBuf, Vec<usize>>,
1235}
1236
1237impl<'a> OcamlSymbols<'a> {
1238 fn new(functions: &'a [FunctionInfo]) -> Self {
1239 let mut by_id = BTreeMap::new();
1240 let mut by_name = BTreeMap::<String, Vec<usize>>::new();
1241 let mut by_file = BTreeMap::<PathBuf, Vec<usize>>::new();
1242 for (index, function) in functions.iter().enumerate() {
1243 by_id.insert(function.id.clone(), index);
1244 by_name
1245 .entry(local_callable_source_name(function).to_owned())
1246 .or_default()
1247 .push(index);
1248 by_file
1249 .entry(function.span.file.clone())
1250 .or_default()
1251 .push(index);
1252 }
1253 let canonical_files = by_file
1254 .iter()
1255 .filter_map(|(file, functions)| {
1256 let canonical = file.canonicalize().ok()?;
1257 (canonical != *file).then(|| (canonical, functions.clone()))
1258 })
1259 .collect::<Vec<_>>();
1260 for (file, functions) in canonical_files {
1261 by_file.entry(file).or_default().extend(functions);
1262 }
1263 for functions in by_file.values_mut() {
1264 functions.sort_unstable();
1265 functions.dedup();
1266 }
1267 Self {
1268 functions,
1269 by_id,
1270 by_name,
1271 by_file,
1272 }
1273 }
1274
1275 fn get(&self, symbol: &SymbolId) -> Option<&'a FunctionInfo> {
1276 self.by_id
1277 .get(symbol)
1278 .and_then(|index| self.functions.get(*index))
1279 }
1280
1281 fn named(&self, name: &str) -> impl Iterator<Item = &'a FunctionInfo> + '_ {
1282 self.by_name
1283 .get(name)
1284 .into_iter()
1285 .flatten()
1286 .filter_map(|index| self.functions.get(*index))
1287 }
1288
1289 fn resolve(
1290 &self,
1291 parts: &[String],
1292 caller: &SymbolId,
1293 position: Option<&SourceSpan>,
1294 ) -> Option<SymbolId> {
1295 let name = parts.last()?;
1296 let indices = self.by_name.get(name)?;
1297 let mut candidates = indices
1298 .iter()
1299 .filter_map(|index| self.functions.get(*index))
1300 .filter(|candidate| {
1301 if parts.len() == 1 {
1302 candidate.id.module == caller.module && candidate.id.container.is_none()
1303 } else {
1304 let requested_module = &parts[..parts.len() - 1];
1305 candidate.id.container.is_none()
1306 && requested_module.len() <= candidate.id.module.len()
1307 && candidate.id.module[candidate.id.module.len() - requested_module.len()..]
1308 == *requested_module
1309 }
1310 })
1311 .collect::<Vec<_>>();
1312 if candidates.len() == 1 {
1313 return Some(candidates[0].id.clone());
1314 }
1315
1316 if let Some(position) = position {
1317 let same_file = self
1318 .functions_in_file(&position.file)
1319 .filter(|candidate| candidates.iter().any(|item| item.id == candidate.id))
1320 .collect::<Vec<_>>();
1321 if !same_file.is_empty() {
1322 candidates = same_file;
1323 let enclosing_binding = self
1324 .functions_in_file(&position.file)
1325 .filter(|function| {
1326 function.id.container.is_none() && function.span.contains(position)
1327 })
1328 .min_by_key(|function| function.span.extent());
1329 let visibility_limit = enclosing_binding
1330 .map_or((position.start_line, position.start_column), |function| {
1331 (function.span.start_line, function.span.start_column)
1332 });
1333 candidates.retain(|candidate| {
1334 (candidate.span.start_line, candidate.span.start_column) < visibility_limit
1335 });
1336 candidates.sort_by_key(|candidate| {
1337 (
1338 candidate.span.start_line,
1339 candidate.span.start_column,
1340 candidate.span.end_line,
1341 candidate.span.end_column,
1342 )
1343 });
1344 if let Some(candidate) = candidates.last() {
1345 return Some(candidate.id.clone());
1346 }
1347 }
1348 }
1349
1350 let mut candidates = candidates
1351 .into_iter()
1352 .map(|candidate| candidate.id.clone())
1353 .collect::<Vec<_>>();
1354 candidates.sort();
1355 candidates.dedup();
1356 (candidates.len() == 1).then(|| candidates.remove(0))
1357 }
1358
1359 fn local_target(&self, target: &CallTarget) -> Option<SymbolId> {
1360 let CallTarget::Direct(target) = target else {
1361 return None;
1362 };
1363 if self.by_id.contains_key(target) {
1364 return Some(target.clone());
1365 }
1366 let target_parts = target.qualified_parts();
1367 let mut matches = self
1368 .functions
1369 .iter()
1370 .filter(|function| {
1371 let parts = function.id.qualified_parts();
1372 target_parts.len() <= parts.len()
1373 && parts[parts.len() - target_parts.len()..] == target_parts
1374 })
1375 .map(|function| function.id.clone())
1376 .collect::<Vec<_>>();
1377 (matches.len() == 1).then(|| matches.remove(0))
1378 }
1379
1380 fn functions_in_file(&self, file: &Path) -> impl Iterator<Item = &'a FunctionInfo> + '_ {
1381 let functions = self.by_file.get(file).or_else(|| {
1382 file.canonicalize()
1383 .ok()
1384 .and_then(|file| self.by_file.get(&file))
1385 });
1386 functions
1387 .into_iter()
1388 .flatten()
1389 .filter_map(|index| self.functions.get(*index))
1390 }
1391}
1392
1393struct OcamlFlowIndex<'a> {
1394 symbols: OcamlSymbols<'a>,
1395 parameters: &'a BTreeMap<SymbolId, Vec<OcamlParameterBinding>>,
1396 return_outcomes: &'a BTreeMap<SymbolId, Vec<String>>,
1397 module_aliases: &'a BTreeMap<Vec<String>, Vec<String>>,
1398 local_values: &'a BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>>,
1399 local_callable_scopes: &'a BTreeMap<SymbolId, SourceSpan>,
1400}
1401
1402#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1403enum OcamlParameterKind {
1404 Value,
1405 Module,
1406}
1407
1408#[derive(Clone, Debug)]
1409struct OcamlParameterBinding {
1410 name: String,
1411 argument_index: usize,
1412 projection: Vec<OcamlProjection>,
1413 kind: OcamlParameterKind,
1414 scope: Option<SourceSpan>,
1415}
1416
1417#[derive(Clone, Debug)]
1418enum OcamlProjection {
1419 Tuple(usize),
1420 Constructor(String),
1421 RecordField(String),
1422}
1423
1424#[derive(Clone, Debug)]
1425struct OcamlLocalValue {
1426 expression: String,
1427 scope: SourceSpan,
1428}
1429
1430fn called_ocaml_parameter_index(
1431 parts: &[String],
1432 bindings: &[OcamlParameterBinding],
1433 position: Option<&SourceSpan>,
1434) -> Option<usize> {
1435 bindings.iter().position(|binding| {
1436 binding
1437 .scope
1438 .as_ref()
1439 .is_none_or(|scope| position.is_some_and(|position| scope.contains(position)))
1440 && match binding.kind {
1441 OcamlParameterKind::Value => parts.len() == 1 && parts[0] == binding.name,
1442 OcamlParameterKind::Module => parts.len() > 1 && parts[0] == binding.name,
1443 }
1444 })
1445}
1446
1447fn ocaml_local_values(
1448 facts: &[LanguageFact],
1449) -> BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>> {
1450 facts.iter().filter(|fact| fact.kind == "local-value").fold(
1451 BTreeMap::new(),
1452 |mut values, fact| {
1453 values
1454 .entry((fact.subject.clone(), fact.key.clone()))
1455 .or_insert_with(Vec::new)
1456 .push(OcamlLocalValue {
1457 expression: fact.value.clone(),
1458 scope: fact.span.clone(),
1459 });
1460 values
1461 },
1462 )
1463}
1464
1465fn ocaml_local_callable_scopes(facts: &[LanguageFact]) -> BTreeMap<SymbolId, SourceSpan> {
1466 facts
1467 .iter()
1468 .filter(|fact| fact.kind == "local-callable-scope")
1469 .map(|fact| (fact.subject.clone(), fact.span.clone()))
1470 .collect()
1471}
1472
1473fn unique_ocaml_local_value<'a>(
1474 values: &'a BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>>,
1475 owner: &SymbolId,
1476 name: &str,
1477 position: Option<&SourceSpan>,
1478) -> Option<&'a str> {
1479 scoped_ocaml_local_value(values, owner, name, position)
1480 .map(|candidate| candidate.expression.as_str())
1481}
1482
1483fn scoped_ocaml_local_value<'a>(
1484 values: &'a BTreeMap<(SymbolId, String), Vec<OcamlLocalValue>>,
1485 owner: &SymbolId,
1486 name: &str,
1487 position: Option<&SourceSpan>,
1488) -> Option<&'a OcamlLocalValue> {
1489 let candidates = values.get(&(owner.clone(), name.to_owned()))?;
1490 let mut candidates = candidates
1491 .iter()
1492 .filter(|candidate| position.is_none_or(|position| candidate.scope.contains(position)))
1493 .collect::<Vec<_>>();
1494 candidates.sort_by_key(|candidate| (candidate.scope.start_line, candidate.scope.start_column));
1495 candidates.last().copied()
1496}
1497
1498fn position_before_ocaml_scope(scope: &SourceSpan) -> SourceSpan {
1499 let mut position = scope.clone();
1500 if position.start_column > 0 {
1501 position.start_column -= 1;
1502 position.end_column = position.start_column;
1503 } else if position.start_line > 1 {
1504 position.start_line -= 1;
1505 position.end_line = position.start_line;
1506 position.start_column = usize::MAX;
1507 position.end_column = usize::MAX;
1508 }
1509 position.start_byte = position.start_byte.map(|byte| byte.saturating_sub(1));
1510 position.end_byte = position.start_byte;
1511 position
1512}
1513
1514fn instantiate_ocaml_functors(analysis: &mut FileAnalysis) {
1515 let base_functions = analysis.functions.clone();
1516 let base_facts = analysis.facts.clone();
1517 let functors = base_facts
1518 .iter()
1519 .filter(|fact| fact.kind == "functor-parameter")
1520 .fold(
1521 BTreeMap::<Vec<String>, Vec<String>>::new(),
1522 |mut map, fact| {
1523 let mut module = fact.subject.module.clone();
1524 module.push(fact.subject.name.clone());
1525 map.entry(module).or_default().push(fact.value.clone());
1526 map
1527 },
1528 );
1529 let instances = base_facts
1530 .iter()
1531 .filter(|fact| fact.kind == "functor-instance")
1532 .cloned()
1533 .collect::<Vec<_>>();
1534
1535 for instance in instances {
1536 let mut instance_module = instance.subject.module.clone();
1537 instance_module.push(instance.subject.name.clone());
1538 let raw_functor = instance
1539 .key
1540 .split('.')
1541 .map(ToOwned::to_owned)
1542 .collect::<Vec<_>>();
1543 let mut relative_functor = instance.subject.module.clone();
1544 relative_functor.extend(raw_functor.iter().cloned());
1545 let functor_module = if functors.contains_key(&relative_functor) {
1546 relative_functor
1547 } else {
1548 raw_functor
1549 };
1550 let Some(parameters) = functors.get(&functor_module) else {
1551 continue;
1552 };
1553 let arguments = instance
1554 .value
1555 .split('\t')
1556 .filter(|argument| !argument.is_empty())
1557 .map(|argument| {
1558 let raw = argument
1559 .split('.')
1560 .map(ToOwned::to_owned)
1561 .collect::<Vec<_>>();
1562 let mut relative = instance.subject.module.clone();
1563 relative.extend(raw.iter().cloned());
1564 if base_functions
1565 .iter()
1566 .any(|function| function.id.module.starts_with(&relative))
1567 {
1568 relative
1569 } else {
1570 raw
1571 }
1572 })
1573 .collect::<Vec<_>>();
1574 if parameters.len() != arguments.len() {
1575 continue;
1576 }
1577 let substitutions = parameters
1578 .iter()
1579 .cloned()
1580 .zip(arguments)
1581 .collect::<Vec<_>>();
1582
1583 let selected = base_functions
1584 .iter()
1585 .filter(|function| function.id.module.starts_with(&functor_module))
1586 .cloned()
1587 .collect::<Vec<_>>();
1588 let ids = selected
1589 .iter()
1590 .map(|function| {
1591 let mut id = function.id.clone();
1592 let suffix = id.module[functor_module.len()..].to_vec();
1593 id.module = instance_module.iter().cloned().chain(suffix).collect();
1594 (function.id.clone(), id)
1595 })
1596 .collect::<BTreeMap<_, _>>();
1597 let containers = ids
1598 .iter()
1599 .map(|(old, new)| (old.to_string(), new.to_string()))
1600 .collect::<BTreeMap<_, _>>();
1601 let mut clones = Vec::new();
1602 for mut function in selected {
1603 let old_id = function.id.clone();
1604 let Some(new_id) = ids.get(&old_id).cloned() else {
1605 continue;
1606 };
1607 function.id = new_id;
1608 if let Some(container) = &function.id.container
1609 && let Some(rewritten) = containers.get(container)
1610 {
1611 function.id.container = Some(rewritten.clone());
1612 }
1613 function.label.default = replace_ocaml_module_prefix(
1614 &function.label.default,
1615 &functor_module,
1616 &instance_module,
1617 );
1618 if let Some(typed) = &mut function.label.typed {
1619 *typed = replace_ocaml_module_prefix(typed, &functor_module, &instance_module);
1620 }
1621 for call in &mut function.calls {
1622 for (parameter, argument_module) in &substitutions {
1623 rewrite_functor_call(call, parameter, argument_module, &containers, &ids);
1624 }
1625 }
1626 clones.push((old_id, function));
1627 }
1628 for (old_id, function) in &clones {
1629 analysis.facts.extend(
1630 base_facts
1631 .iter()
1632 .filter(|fact| fact.subject == *old_id)
1633 .cloned()
1634 .map(|mut fact| {
1635 fact.subject = function.id.clone();
1636 fact
1637 }),
1638 );
1639 }
1640 let new_functions = clones
1641 .into_iter()
1642 .map(|(_, function)| function)
1643 .filter(|function| {
1644 !analysis
1645 .functions
1646 .iter()
1647 .any(|existing| existing.id == function.id)
1648 })
1649 .collect::<Vec<_>>();
1650 analysis.functions.extend(new_functions);
1651 }
1652}
1653
1654fn rewrite_functor_call(
1655 call: &mut CallSite,
1656 parameter: &str,
1657 argument_module: &[String],
1658 containers: &BTreeMap<String, String>,
1659 ids: &BTreeMap<SymbolId, SymbolId>,
1660) {
1661 rewrite_functor_syntax(&mut call.syntax, parameter, argument_module);
1662 let parameter_prefix = format!("{parameter}.");
1663 let argument_prefix = format!("{}.", argument_module.join("."));
1664 call.label.default = call
1665 .label
1666 .default
1667 .replacen(¶meter_prefix, &argument_prefix, 1);
1668 if let Some(typed) = &mut call.label.typed {
1669 *typed = typed.replacen(¶meter_prefix, &argument_prefix, 1);
1670 }
1671 match &mut call.target {
1672 CallTarget::Direct(target) => {
1673 if let Some(rewritten) = ids.get(target) {
1674 *target = rewritten.clone();
1675 } else if let Some(container) = &target.container
1676 && let Some(rewritten) = containers.get(container)
1677 {
1678 target.container = Some(rewritten.clone());
1679 }
1680 }
1681 CallTarget::Dynamic {
1682 dispatch,
1683 candidates,
1684 ..
1685 } => {
1686 if let Some(rewritten) = ids.get(dispatch) {
1687 *dispatch = rewritten.clone();
1688 }
1689 for candidate in candidates {
1690 if let Some(rewritten) = ids.get(&candidate.target) {
1691 candidate.target = rewritten.clone();
1692 }
1693 }
1694 }
1695 CallTarget::Indirect { .. } | CallTarget::Unresolved => {}
1696 }
1697}
1698
1699fn rewrite_functor_syntax(syntax: &mut CallSyntax, parameter: &str, argument_module: &[String]) {
1700 match syntax {
1701 CallSyntax::Path(parts) if parts.first().is_some_and(|part| part == parameter) => {
1702 let mut rewritten = argument_module.to_vec();
1703 rewritten.extend_from_slice(&parts[1..]);
1704 *parts = rewritten;
1705 }
1706 CallSyntax::CompilerConfirmed(syntax) => {
1707 rewrite_functor_syntax(syntax, parameter, argument_module);
1708 }
1709 CallSyntax::Path(_)
1710 | CallSyntax::SelfMethod(_)
1711 | CallSyntax::Method { .. }
1712 | CallSyntax::Expression(_) => {}
1713 }
1714}
1715
1716fn replace_ocaml_module_prefix(label: &str, from: &[String], to: &[String]) -> String {
1717 let from = format!("{}.", from.join("."));
1718 let to = format!("{}.", to.join("."));
1719 label.replacen(&from, &to, 1)
1720}
1721
1722fn resolve_ocaml_function_values(analysis: &mut FileAnalysis) {
1723 instantiate_ocaml_functors(analysis);
1724 let mut functions = analysis.functions.clone();
1725 let parameters = ocaml_parameters(&functions, &analysis.facts);
1726 let return_outcomes = ocaml_return_outcomes(&functions, &analysis.facts);
1727 let module_aliases = ocaml_module_aliases(&functions, &analysis.facts);
1728 let local_values = ocaml_local_values(&analysis.facts);
1729 let local_callable_scopes = ocaml_local_callable_scopes(&analysis.facts);
1730
1731 let symbol_functions = functions.clone();
1734 let symbols = OcamlSymbols::new(&symbol_functions);
1735 for function in &mut functions {
1736 let parameter_names = parameters.get(&function.id).cloned().unwrap_or_default();
1737 for call in &mut function.calls {
1738 if let CallSyntax::Path(parts) = call.syntax.visible()
1739 && (called_ocaml_parameter_index(parts, ¶meter_names, Some(&call.span))
1740 .is_some()
1741 || (parts.len() == 1
1742 && unique_ocaml_local_value(
1743 &local_values,
1744 &function.id,
1745 &parts[0],
1746 Some(&call.span),
1747 )
1748 .is_some()))
1749 {
1750 call.target = CallTarget::Unresolved;
1754 continue;
1755 }
1756 if let CallTarget::Direct(target) = &call.target {
1757 let parts = target
1758 .qualified_parts()
1759 .into_iter()
1760 .map(ToOwned::to_owned)
1761 .collect::<Vec<_>>();
1762 if let Some(expanded) = expand_ocaml_module_alias(&parts, &module_aliases)
1763 && let Some(resolved) =
1764 symbols.resolve(&expanded, &function.id, Some(&call.span))
1765 {
1766 call.target = CallTarget::Direct(resolved);
1767 continue;
1768 }
1769 }
1770 if !matches!(call.target, CallTarget::Unresolved) {
1771 continue;
1772 }
1773 let CallSyntax::Path(parts) = call.syntax.visible() else {
1774 continue;
1775 };
1776 let expanded =
1777 expand_ocaml_module_alias(parts, &module_aliases).unwrap_or_else(|| parts.to_vec());
1778 if let Some(target) = symbols.resolve(&expanded, &function.id, Some(&call.span)) {
1779 call.target = CallTarget::Direct(target);
1780 }
1781 }
1782 }
1783
1784 let mut called_parameters = functions
1785 .iter()
1786 .flat_map(|function| {
1787 let names = parameters.get(&function.id).cloned().unwrap_or_default();
1788 function.calls.iter().filter_map(move |call| {
1789 let CallSyntax::Path(parts) = call.syntax.visible() else {
1790 return None;
1791 };
1792 called_ocaml_parameter_index(parts, &names, Some(&call.span))
1793 .map(|index| (function.id.clone(), index))
1794 })
1795 })
1796 .collect::<BTreeSet<_>>();
1797 let symbols = OcamlSymbols::new(&functions);
1798 let return_called = ocaml_return_called_functions(&symbols);
1799 for (function, outcomes) in &return_outcomes {
1800 if !return_called.contains(function) {
1801 continue;
1802 }
1803 let names = parameters.get(function).cloned().unwrap_or_default();
1804 for outcome in outcomes {
1805 if let Some(index) = names.iter().position(|binding| binding.name == *outcome) {
1806 called_parameters.insert((function.clone(), index));
1807 }
1808 }
1809 }
1810
1811 let mut call_flows = Vec::new();
1812 for function in &functions {
1813 for call in &function.calls {
1814 let Some(target) = symbols.local_target(&call.target) else {
1815 continue;
1816 };
1817 call_flows.push(OcamlCallFlow {
1818 caller: function.id.clone(),
1819 target,
1820 arguments: ocaml_call_arguments(call),
1821 span: call.span.clone(),
1822 });
1823 }
1824 }
1825 loop {
1826 let previous_len = called_parameters.len();
1827 for call in &call_flows {
1828 let caller_parameters = parameters.get(&call.caller).cloned().unwrap_or_default();
1829 let target_parameters = parameters.get(&call.target).cloned().unwrap_or_default();
1830 for (target_parameter, binding) in target_parameters.iter().enumerate() {
1831 if !called_parameters.contains(&(call.target.clone(), target_parameter)) {
1832 continue;
1833 }
1834 let Some(argument) = call
1835 .arguments
1836 .get(binding.argument_index)
1837 .and_then(|argument| project_ocaml_argument(argument, &binding.projection))
1838 else {
1839 continue;
1840 };
1841 let outcomes = ocaml_callable_outcome_names(&argument);
1842 let module_argument = ocaml_module_package_path(&argument);
1843 for (caller_parameter, binding) in caller_parameters.iter().enumerate() {
1844 let passes_module_parameter = binding.kind == OcamlParameterKind::Module
1845 && module_argument
1846 .as_ref()
1847 .is_some_and(|parts| parts.len() == 1 && parts[0] == binding.name);
1848 if outcomes.contains(&binding.name) || passes_module_parameter {
1849 called_parameters.insert((call.caller.clone(), caller_parameter));
1850 }
1851 }
1852 }
1853 }
1854 if called_parameters.len() == previous_len {
1855 break;
1856 }
1857 }
1858 let flow_index = OcamlFlowIndex {
1859 symbols,
1860 parameters: ¶meters,
1861 return_outcomes: &return_outcomes,
1862 module_aliases: &module_aliases,
1863 local_values: &local_values,
1864 local_callable_scopes: &local_callable_scopes,
1865 };
1866 let (functions, facts) = specialize_ocaml_function_values(
1867 &functions,
1868 &analysis.facts,
1869 &called_parameters,
1870 &call_flows,
1871 &flow_index,
1872 );
1873 analysis.functions = functions;
1874 analysis.facts = facts;
1875}
1876
1877fn ocaml_module_aliases(
1878 functions: &[FunctionInfo],
1879 facts: &[LanguageFact],
1880) -> BTreeMap<Vec<String>, Vec<String>> {
1881 facts
1882 .iter()
1883 .filter(|fact| fact.kind == "module-alias")
1884 .map(|fact| {
1885 let mut alias = fact.subject.module.clone();
1886 alias.push(fact.subject.name.clone());
1887 let target = fact
1888 .value
1889 .split('.')
1890 .map(ToOwned::to_owned)
1891 .collect::<Vec<_>>();
1892 let mut relative = fact.subject.module.clone();
1893 relative.extend(target.iter().cloned());
1894 let target = if functions
1895 .iter()
1896 .any(|function| function.id.module.starts_with(&relative))
1897 {
1898 relative
1899 } else {
1900 target
1901 };
1902 (alias, target)
1903 })
1904 .collect()
1905}
1906
1907fn expand_ocaml_module_alias(
1908 parts: &[String],
1909 aliases: &BTreeMap<Vec<String>, Vec<String>>,
1910) -> Option<Vec<String>> {
1911 let mut expanded = parts.to_vec();
1912 let mut changed = false;
1913 let mut seen = BTreeSet::new();
1914 while let Some((alias, target)) = aliases
1915 .iter()
1916 .filter(|(alias, _)| expanded.starts_with(alias))
1917 .max_by_key(|(alias, _)| alias.len())
1918 {
1919 if !seen.insert(alias.clone()) {
1920 break;
1921 }
1922 let mut next = target.clone();
1923 next.extend_from_slice(&expanded[alias.len()..]);
1924 expanded = next;
1925 changed = true;
1926 }
1927 changed.then_some(expanded)
1928}
1929
1930fn ocaml_return_called_functions(symbols: &OcamlSymbols<'_>) -> BTreeSet<SymbolId> {
1931 symbols
1932 .functions
1933 .iter()
1934 .flat_map(|caller| {
1935 caller.calls.iter().filter_map(|call| {
1936 let CallSyntax::Expression(expression) = call.syntax.visible() else {
1937 return None;
1938 };
1939 let parts = ocaml_applied_function_path(expression)?;
1940 symbols.resolve(&parts, &caller.id, Some(&call.span))
1941 })
1942 })
1943 .collect()
1944}
1945
1946fn ocaml_applied_function_path(expression: &str) -> Option<Vec<String>> {
1947 let tree = parse_ocaml_expression(expression)?;
1948 if tree.root_node().has_error() {
1949 return None;
1950 }
1951 let mut node = tree.root_node().named_child(0)?;
1952 while matches!(
1953 node.kind(),
1954 "expression_item"
1955 | "parenthesized_expression"
1956 | "typed_expression"
1957 | "coercion_expression"
1958 | "local_open_expression"
1959 ) {
1960 node = node
1961 .child_by_field_name("body")
1962 .or_else(|| node.child_by_field_name("expression"))
1963 .or_else(|| node.named_child(0))?;
1964 }
1965 (node.kind() == "application_expression")
1966 .then(|| node.child_by_field_name("function"))
1967 .flatten()
1968 .and_then(|function| ocaml_value_path(function, expression))
1969}
1970
1971fn specialize_ocaml_function_values(
1972 functions: &[FunctionInfo],
1973 facts: &[LanguageFact],
1974 called_parameters: &BTreeSet<(SymbolId, usize)>,
1975 call_flows: &[OcamlCallFlow],
1976 flow_index: &OcamlFlowIndex<'_>,
1977) -> (Vec<FunctionInfo>, Vec<LanguageFact>) {
1978 let parameters = flow_index.parameters;
1979 let index = functions
1980 .iter()
1981 .enumerate()
1982 .map(|(index, function)| (function.id.clone(), index))
1983 .collect::<BTreeMap<_, _>>();
1984 let mut incoming = vec![0usize; functions.len()];
1985 for call in call_flows {
1986 if let Some(target) = index.get(&call.target) {
1987 incoming[*target] += 1;
1988 }
1989 for argument in &call.arguments {
1990 if let Some(candidate) = argument_callable_symbol(
1991 argument,
1992 &call.caller,
1993 &flow_index.symbols,
1994 Some(&call.span),
1995 ) && let Some(target) = index.get(&candidate)
1996 {
1997 incoming[*target] += 1;
1998 }
1999 }
2000 }
2001
2002 let mut pending = VecDeque::<(usize, Vec<CallableFlow>)>::new();
2003 for (function, incoming) in incoming.iter().enumerate() {
2004 if *incoming == 0 {
2005 pending.push_back((
2006 function,
2007 root_ocaml_context(&functions[function], parameters, called_parameters),
2008 ));
2009 }
2010 }
2011 let mut visited = BTreeSet::new();
2012 let mut covered = BTreeSet::new();
2013 let mut specialized = Vec::new();
2014 let mut specialized_facts = Vec::new();
2015
2016 loop {
2017 while let Some((raw_index, context)) = pending.pop_front() {
2018 let raw = &functions[raw_index];
2019 let id = specialized_ocaml_id(raw, &context, called_parameters);
2020 covered.insert(raw_index);
2025 if !visited.insert(id.clone()) {
2026 continue;
2027 }
2028 let names = parameters.get(&raw.id).cloned().unwrap_or_default();
2029 let mut function = raw.clone();
2030 function.id = id.clone();
2031
2032 for call in &mut function.calls {
2033 if let CallSyntax::Expression(expression) = call.syntax.visible()
2034 && !matches!(
2035 call.target,
2036 CallTarget::Direct(_) | CallTarget::Dynamic { .. }
2037 )
2038 {
2039 let flow = argument_callable_flow(
2040 expression,
2041 raw,
2042 &context,
2043 flow_index,
2044 Some(&call.span),
2045 );
2046 let arguments = ocaml_call_arguments(call);
2047 let candidates = flow
2048 .candidates
2049 .iter()
2050 .filter_map(|target| {
2051 let target_index = *index.get(target)?;
2052 let target_function = &functions[target_index];
2053 let target_context = ocaml_call_context(
2054 target_function,
2055 &arguments,
2056 &call.span,
2057 raw,
2058 &context,
2059 flow_index,
2060 called_parameters,
2061 );
2062 let target = specialized_ocaml_id(
2063 target_function,
2064 &target_context,
2065 called_parameters,
2066 );
2067 pending.push_back((target_index, target_context));
2068 Some(DispatchCandidate {
2069 target,
2070 label: CallLabel::new(ocaml_candidate_call_label(
2071 target_function,
2072 &arguments,
2073 )),
2074 })
2075 })
2076 .collect::<Vec<_>>();
2077 call.target = CallTarget::dynamic(
2078 SymbolId {
2079 language: LanguageId::new("ocaml"),
2080 module: raw.id.module.clone(),
2081 container: Some(raw.id.name.clone()),
2082 name: expression.clone(),
2083 },
2084 candidates,
2085 DispatchEvidence::ExactFlow,
2086 flow.unresolved_reasons,
2087 );
2088 continue;
2089 }
2090
2091 let CallSyntax::Path(parts) = call.syntax.visible() else {
2092 continue;
2093 };
2094 let local_expression = (parts.len() == 1)
2095 .then(|| {
2096 unique_ocaml_local_value(
2097 flow_index.local_values,
2098 &raw.id,
2099 &parts[0],
2100 Some(&call.span),
2101 )
2102 })
2103 .flatten();
2104 let flow_and_binding =
2105 local_expression
2106 .map(|expression| {
2107 (
2108 argument_callable_flow(
2109 expression,
2110 raw,
2111 &context,
2112 flow_index,
2113 Some(&call.span),
2114 ),
2115 None,
2116 )
2117 })
2118 .or_else(|| {
2119 let parameter_index =
2120 called_ocaml_parameter_index(parts, &names, Some(&call.span))?;
2121 let binding = &names[parameter_index];
2122 let flow = context.get(parameter_index).cloned().unwrap_or_else(|| {
2123 CallableFlow {
2124 candidates: BTreeSet::new(),
2125 unresolved_reasons: [UnresolvedReason::AnalysisLimit]
2126 .into_iter()
2127 .collect(),
2128 }
2129 });
2130 Some((flow, Some(binding)))
2131 });
2132 if let Some((flow, binding)) = flow_and_binding {
2133 let arguments = ocaml_call_arguments(call);
2134 let candidates = flow
2135 .candidates
2136 .iter()
2137 .filter_map(|target| {
2138 let target = match binding.map(|binding| binding.kind) {
2139 None | Some(OcamlParameterKind::Value) => target.clone(),
2140 Some(OcamlParameterKind::Module) => {
2141 let mut target_parts = ocaml_module_flow_path(target)?.to_vec();
2142 target_parts.extend_from_slice(&parts[1..]);
2143 flow_index.symbols.resolve(
2144 &target_parts,
2145 &raw.id,
2146 Some(&call.span),
2147 )?
2148 }
2149 };
2150 let target_index = *index.get(&target)?;
2151 let target_function = &functions[target_index];
2152 let target_context = ocaml_call_context(
2153 target_function,
2154 &arguments,
2155 &call.span,
2156 raw,
2157 &context,
2158 flow_index,
2159 called_parameters,
2160 );
2161 let specialized_target = specialized_ocaml_id(
2162 target_function,
2163 &target_context,
2164 called_parameters,
2165 );
2166 pending.push_back((target_index, target_context));
2167 Some(DispatchCandidate {
2168 target: specialized_target,
2169 label: CallLabel::new(ocaml_candidate_call_label(
2170 target_function,
2171 &arguments,
2172 )),
2173 })
2174 })
2175 .collect::<Vec<_>>();
2176 call.target = CallTarget::dynamic(
2177 SymbolId {
2178 language: LanguageId::new("ocaml"),
2179 module: raw.id.module.clone(),
2180 container: Some(raw.id.name.clone()),
2181 name: parts.join("."),
2182 },
2183 candidates,
2184 DispatchEvidence::ExactFlow,
2185 flow.unresolved_reasons,
2186 );
2187 continue;
2188 }
2189
2190 let Some(target) = flow_index.symbols.local_target(&call.target) else {
2191 continue;
2192 };
2193 let Some(target_index) = index.get(&target).copied() else {
2194 continue;
2195 };
2196 let target_function = &functions[target_index];
2197 let arguments = ocaml_call_arguments(call);
2198 let target_context = ocaml_call_context(
2199 target_function,
2200 &arguments,
2201 &call.span,
2202 raw,
2203 &context,
2204 flow_index,
2205 called_parameters,
2206 );
2207 let target =
2208 specialized_ocaml_id(target_function, &target_context, called_parameters);
2209 pending.push_back((target_index, target_context));
2210 call.target = CallTarget::Direct(target);
2211 }
2212
2213 specialized_facts.extend(
2214 facts
2215 .iter()
2216 .filter(|fact| fact.subject == raw.id)
2217 .cloned()
2218 .map(|mut fact| {
2219 fact.subject = id.clone();
2220 fact
2221 }),
2222 );
2223 specialized.push(function);
2224 }
2225
2226 let Some(uncovered) = (0..functions.len()).find(|index| !covered.contains(index)) else {
2227 break;
2228 };
2229 pending.push_back((
2230 uncovered,
2231 root_ocaml_context(&functions[uncovered], parameters, called_parameters),
2232 ));
2233 }
2234 (specialized, specialized_facts)
2235}
2236
2237fn root_ocaml_context(
2238 function: &FunctionInfo,
2239 parameters: &BTreeMap<SymbolId, Vec<OcamlParameterBinding>>,
2240 called_parameters: &BTreeSet<(SymbolId, usize)>,
2241) -> Vec<CallableFlow> {
2242 parameters
2243 .get(&function.id)
2244 .into_iter()
2245 .flatten()
2246 .enumerate()
2247 .map(|(index, _)| CallableFlow {
2248 candidates: BTreeSet::new(),
2249 unresolved_reasons: called_parameters
2250 .contains(&(function.id.clone(), index))
2251 .then_some(UnresolvedReason::OpaqueInput)
2252 .into_iter()
2253 .collect(),
2254 })
2255 .collect()
2256}
2257
2258#[allow(clippy::too_many_arguments)]
2259fn ocaml_call_context(
2260 target: &FunctionInfo,
2261 arguments: &[String],
2262 call_span: &SourceSpan,
2263 caller: &FunctionInfo,
2264 caller_context: &[CallableFlow],
2265 flow_index: &OcamlFlowIndex<'_>,
2266 called_parameters: &BTreeSet<(SymbolId, usize)>,
2267) -> Vec<CallableFlow> {
2268 flow_index
2269 .parameters
2270 .get(&target.id)
2271 .into_iter()
2272 .flatten()
2273 .enumerate()
2274 .map(|(index, binding)| {
2275 if !called_parameters.contains(&(target.id.clone(), index)) {
2276 return CallableFlow::default();
2277 }
2278 arguments
2279 .get(binding.argument_index)
2280 .and_then(|argument| project_ocaml_argument(argument, &binding.projection))
2281 .map_or_else(
2282 || CallableFlow {
2283 candidates: BTreeSet::new(),
2284 unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
2285 },
2286 |argument| {
2287 argument_callable_flow(
2288 &argument,
2289 caller,
2290 caller_context,
2291 flow_index,
2292 Some(call_span),
2293 )
2294 },
2295 )
2296 })
2297 .collect()
2298}
2299
2300fn specialized_ocaml_id(
2301 function: &FunctionInfo,
2302 context: &[CallableFlow],
2303 called_parameters: &BTreeSet<(SymbolId, usize)>,
2304) -> SymbolId {
2305 let bindings = context
2306 .iter()
2307 .enumerate()
2308 .filter(|(index, _)| called_parameters.contains(&(function.id.clone(), *index)))
2309 .map(|(index, flow)| {
2310 let mut values = flow
2311 .candidates
2312 .iter()
2313 .map(ToString::to_string)
2314 .collect::<Vec<_>>();
2315 for reason in &flow.unresolved_reasons {
2316 values.push(format!("?{reason}"));
2317 }
2318 format!("{index}={}", values.join("|"))
2319 })
2320 .collect::<Vec<_>>();
2321 if bindings.is_empty() {
2322 return function.id.clone();
2323 }
2324 let mut id = function.id.clone();
2325 id.name = format!("{}#ctx[{}]", id.name, bindings.join(","));
2326 id
2327}
2328
2329fn ocaml_parameters(
2330 functions: &[FunctionInfo],
2331 facts: &[LanguageFact],
2332) -> BTreeMap<SymbolId, Vec<OcamlParameterBinding>> {
2333 functions
2334 .iter()
2335 .map(|function| {
2336 let mut parameters = Vec::new();
2337 for (argument_index, fact) in facts
2338 .iter()
2339 .filter(|fact| fact.subject == function.id && fact.kind == "parameter")
2340 .enumerate()
2341 {
2342 collect_ocaml_parameter_bindings(
2343 &fact.key,
2344 argument_index,
2345 &mut Vec::new(),
2346 &mut parameters,
2347 );
2348 }
2349 for fact in facts
2350 .iter()
2351 .filter(|fact| fact.subject == function.id && fact.kind == "parameter-refinement")
2352 {
2353 let Some((source, pattern)) = fact.value.split_once('\n') else {
2354 continue;
2355 };
2356 let Some(base) = parameters
2357 .iter()
2358 .find(|binding| binding.scope.is_none() && binding.name == source)
2359 .cloned()
2360 else {
2361 continue;
2362 };
2363 let mut refined = Vec::new();
2364 let mut projection = base.projection.clone();
2365 collect_ocaml_parameter_bindings(
2366 pattern,
2367 base.argument_index,
2368 &mut projection,
2369 &mut refined,
2370 );
2371 for mut binding in refined {
2372 if binding.name != fact.key {
2373 continue;
2374 }
2375 binding.scope = Some(fact.span.clone());
2376 parameters.push(binding);
2377 }
2378 }
2379 (function.id.clone(), parameters)
2380 })
2381 .collect()
2382}
2383
2384fn ocaml_return_outcomes(
2385 functions: &[FunctionInfo],
2386 facts: &[LanguageFact],
2387) -> BTreeMap<SymbolId, Vec<String>> {
2388 functions
2389 .iter()
2390 .map(|function| {
2391 let outcomes = facts
2392 .iter()
2393 .filter(|fact| fact.subject == function.id && fact.kind == "callable-return")
2394 .map(|fact| fact.value.clone())
2395 .collect();
2396 (function.id.clone(), outcomes)
2397 })
2398 .collect()
2399}
2400
2401fn collect_ocaml_parameter_bindings(
2402 pattern: &str,
2403 argument_index: usize,
2404 projection: &mut Vec<OcamlProjection>,
2405 bindings: &mut Vec<OcamlParameterBinding>,
2406) {
2407 let unwrapped = strip_outer_ocaml_parentheses(pattern).trim();
2408 if let Some(module) = unwrapped.strip_prefix("module ") {
2409 let name = module.split([' ', ':']).next().unwrap_or(module).trim();
2410 if !name.is_empty() {
2411 bindings.push(OcamlParameterBinding {
2412 name: name.to_owned(),
2413 argument_index,
2414 projection: projection.clone(),
2415 kind: OcamlParameterKind::Module,
2416 scope: None,
2417 });
2418 }
2419 return;
2420 }
2421 let pattern = normalize_ocaml_pattern(pattern);
2422 let tuple = split_top_level_ocaml(&pattern, ',');
2423 if tuple.len() > 1 {
2424 for (index, element) in tuple.into_iter().enumerate() {
2425 projection.push(OcamlProjection::Tuple(index));
2426 collect_ocaml_parameter_bindings(element, argument_index, projection, bindings);
2427 projection.pop();
2428 }
2429 return;
2430 }
2431 if let Some(fields) = split_ocaml_record_fields(&pattern) {
2432 for (field, value) in fields {
2433 projection.push(OcamlProjection::RecordField(field));
2434 collect_ocaml_parameter_bindings(&value, argument_index, projection, bindings);
2435 projection.pop();
2436 }
2437 return;
2438 }
2439 if let Some((constructor, payload)) = split_ocaml_constructor_application(&pattern) {
2440 projection.push(OcamlProjection::Constructor(constructor.to_owned()));
2441 collect_ocaml_parameter_bindings(payload, argument_index, projection, bindings);
2442 projection.pop();
2443 return;
2444 }
2445 if is_value_identifier(&pattern) && pattern != "_" {
2446 bindings.push(OcamlParameterBinding {
2447 name: pattern,
2448 argument_index,
2449 projection: projection.clone(),
2450 kind: OcamlParameterKind::Value,
2451 scope: None,
2452 });
2453 }
2454}
2455
2456fn normalize_ocaml_pattern(pattern: &str) -> String {
2457 let mut pattern = pattern.trim();
2458 if pattern.starts_with(['~', '?']) {
2459 let stripped = &pattern[1..];
2460 pattern = stripped.split_once(':').map_or_else(
2461 || stripped.split('=').next().unwrap_or(stripped),
2462 |(_, value)| value,
2463 );
2464 }
2465 strip_outer_ocaml_parentheses(pattern).trim().to_owned()
2466}
2467
2468fn project_ocaml_argument(argument: &str, projection: &[OcamlProjection]) -> Option<String> {
2469 let mut value = argument.trim().to_owned();
2470 if value.starts_with(['~', '?']) {
2471 let stripped = &value[1..];
2472 value = stripped
2473 .split_once(':')
2474 .map_or(stripped, |(_, value)| value)
2475 .to_owned();
2476 }
2477 for step in projection {
2478 let current = strip_outer_ocaml_parentheses(&value).trim();
2479 match step {
2480 OcamlProjection::Tuple(index) => {
2481 let elements = split_top_level_ocaml(current, ',');
2482 value = elements.get(*index)?.trim().to_owned();
2483 }
2484 OcamlProjection::Constructor(expected) => {
2485 let (constructor, payload) = split_ocaml_constructor_application(current)?;
2486 if constructor != expected {
2487 return None;
2488 }
2489 value = payload.to_owned();
2490 }
2491 OcamlProjection::RecordField(expected) => {
2492 let fields = split_ocaml_record_fields(current)?;
2493 value = fields
2494 .into_iter()
2495 .find(|(field, _)| field == expected)
2496 .map(|(_, value)| value)?;
2497 }
2498 }
2499 }
2500 Some(normalize_source(
2501 strip_outer_ocaml_parentheses(&value).trim(),
2502 ))
2503}
2504
2505fn split_ocaml_record_fields(value: &str) -> Option<Vec<(String, String)>> {
2506 let value = strip_outer_ocaml_parentheses(value).trim();
2507 let inner = value.strip_prefix('{')?.strip_suffix('}')?.trim();
2508 if inner.is_empty() || split_top_level_ocaml(inner, ';').contains(&"_") {
2509 return None;
2510 }
2511 let mut fields = Vec::new();
2512 for field in split_top_level_ocaml(inner, ';') {
2513 let field = field.trim();
2514 if field.is_empty() {
2515 continue;
2516 }
2517 let assignment = split_top_level_ocaml(field, '=');
2518 let (path, value) = match assignment.as_slice() {
2519 [path] => (*path, *path),
2520 [path, value] => (*path, *value),
2521 _ => return None,
2522 };
2523 let name = path.rsplit('.').next()?.trim();
2524 if !is_value_identifier(name) {
2525 return None;
2526 }
2527 fields.push((name.to_owned(), normalize_source(value)));
2528 }
2529 (!fields.is_empty()).then_some(fields)
2530}
2531
2532fn split_ocaml_constructor_application(value: &str) -> Option<(&str, &str)> {
2533 let value = strip_outer_ocaml_parentheses(value).trim();
2534 let mut depth = 0usize;
2535 let mut split = None;
2536 for (index, character) in value.char_indices() {
2537 match character {
2538 '(' | '[' | '{' => depth += 1,
2539 ')' | ']' | '}' => depth = depth.saturating_sub(1),
2540 character if character.is_whitespace() && depth == 0 => {
2541 split = Some(index);
2542 break;
2543 }
2544 _ => {}
2545 }
2546 }
2547 let split = split?;
2548 let constructor = value[..split].trim();
2549 let payload = value[split..].trim();
2550 let leaf = constructor.rsplit('.').next().unwrap_or(constructor);
2551 ((leaf.starts_with('`')
2552 || leaf
2553 .chars()
2554 .next()
2555 .is_some_and(|character| character.is_ascii_uppercase()))
2556 && !payload.is_empty())
2557 .then_some((constructor, payload))
2558}
2559
2560fn strip_outer_ocaml_parentheses(mut value: &str) -> &str {
2561 loop {
2562 value = value.trim();
2563 let Some(inner) = value.strip_prefix('(') else {
2564 return value;
2565 };
2566 let mut depth = 1usize;
2567 let mut in_string = false;
2568 let mut escaped = false;
2569 let mut closing = None;
2570 for (index, character) in inner.char_indices() {
2571 if in_string {
2572 if escaped {
2573 escaped = false;
2574 } else if character == '\\' {
2575 escaped = true;
2576 } else if character == '"' {
2577 in_string = false;
2578 }
2579 continue;
2580 }
2581 match character {
2582 '"' => in_string = true,
2583 '(' => depth += 1,
2584 ')' => {
2585 depth = depth.saturating_sub(1);
2586 if depth == 0 {
2587 closing = Some(index);
2588 break;
2589 }
2590 }
2591 _ => {}
2592 }
2593 }
2594 let Some(closing) = closing else {
2595 return value;
2596 };
2597 if closing + ')'.len_utf8() != inner.len() {
2598 return value;
2599 }
2600 value = &inner[..closing];
2601 }
2602}
2603
2604fn split_top_level_ocaml(value: &str, separator: char) -> Vec<&str> {
2605 let mut result = Vec::new();
2606 let mut start = 0usize;
2607 let mut depth = 0usize;
2608 let mut in_string = false;
2609 let mut escaped = false;
2610 for (index, character) in value.char_indices() {
2611 if in_string {
2612 if escaped {
2613 escaped = false;
2614 } else if character == '\\' {
2615 escaped = true;
2616 } else if character == '"' {
2617 in_string = false;
2618 }
2619 continue;
2620 }
2621 match character {
2622 '"' => in_string = true,
2623 '(' | '[' | '{' => depth += 1,
2624 ')' | ']' | '}' => depth = depth.saturating_sub(1),
2625 _ if character == separator && depth == 0 => {
2626 result.push(value[start..index].trim());
2627 start = index + character.len_utf8();
2628 }
2629 _ => {}
2630 }
2631 }
2632 result.push(value[start..].trim());
2633 result
2634}
2635
2636fn argument_callable_flow(
2637 argument: &str,
2638 caller: &FunctionInfo,
2639 caller_context: &[CallableFlow],
2640 flow_index: &OcamlFlowIndex<'_>,
2641 position: Option<&SourceSpan>,
2642) -> CallableFlow {
2643 if let Some(mut module) = ocaml_module_package_path(argument) {
2644 if module.len() == 1
2645 && let Some(index) = flow_index
2646 .parameters
2647 .get(&caller.id)
2648 .and_then(|parameters| {
2649 parameters.iter().position(|parameter| {
2650 parameter.kind == OcamlParameterKind::Module && parameter.name == module[0]
2651 })
2652 })
2653 {
2654 return caller_context
2655 .get(index)
2656 .cloned()
2657 .unwrap_or_else(opaque_callable_flow);
2658 }
2659 if let Some(expanded) = expand_ocaml_module_alias(&module, flow_index.module_aliases) {
2660 module = expanded;
2661 }
2662 return CallableFlow {
2663 candidates: BTreeSet::from([ocaml_module_flow_candidate(module)]),
2664 unresolved_reasons: BTreeSet::new(),
2665 };
2666 }
2667 let argument = normalize_ocaml_callable_argument(argument);
2668 if let Some(flow) =
2669 parse_ocaml_callable_flow(argument, caller, caller_context, flow_index, position)
2670 {
2671 return flow;
2672 }
2673 if let Some(candidate) =
2674 argument_callable_symbol(argument, &caller.id, &flow_index.symbols, position)
2675 {
2676 return CallableFlow {
2677 candidates: BTreeSet::from([candidate]),
2678 unresolved_reasons: BTreeSet::new(),
2679 };
2680 }
2681 CallableFlow {
2682 candidates: BTreeSet::new(),
2683 unresolved_reasons: [UnresolvedReason::UnsupportedConstruct]
2684 .into_iter()
2685 .collect(),
2686 }
2687}
2688
2689const OCAML_MODULE_FLOW_MARKER: &str = "{module}";
2690
2691fn ocaml_module_package_path(argument: &str) -> Option<Vec<String>> {
2692 let argument = strip_outer_ocaml_parentheses(argument).trim();
2693 let module = argument.strip_prefix("module ")?.trim();
2694 let module = split_top_level_ocaml(module, ':')
2695 .into_iter()
2696 .next()?
2697 .trim();
2698 let parts = module
2699 .split('.')
2700 .map(str::trim)
2701 .filter(|part| !part.is_empty() && part.chars().all(is_ocaml_path_character))
2702 .map(ToOwned::to_owned)
2703 .collect::<Vec<_>>();
2704 (!parts.is_empty()).then_some(parts)
2705}
2706
2707fn is_ocaml_path_character(character: char) -> bool {
2708 character == '_' || character == '\'' || character.is_alphanumeric()
2709}
2710
2711fn ocaml_module_flow_candidate(module: Vec<String>) -> SymbolId {
2712 SymbolId {
2713 language: LanguageId::new("ocaml"),
2714 module,
2715 container: None,
2716 name: OCAML_MODULE_FLOW_MARKER.to_owned(),
2717 }
2718}
2719
2720fn ocaml_module_flow_path(candidate: &SymbolId) -> Option<&[String]> {
2721 (candidate.language.0 == "ocaml"
2722 && candidate.container.is_none()
2723 && candidate.name == OCAML_MODULE_FLOW_MARKER)
2724 .then_some(candidate.module.as_slice())
2725}
2726
2727fn parse_ocaml_callable_flow(
2728 expression: &str,
2729 caller: &FunctionInfo,
2730 caller_context: &[CallableFlow],
2731 flow_index: &OcamlFlowIndex<'_>,
2732 position: Option<&SourceSpan>,
2733) -> Option<CallableFlow> {
2734 let tree = parse_ocaml_expression(expression)?;
2735 if tree.root_node().has_error() {
2736 return None;
2737 }
2738 let node = tree.root_node().named_child(0)?;
2739 ocaml_callable_node_flow(
2740 node,
2741 expression,
2742 caller,
2743 caller_context,
2744 flow_index,
2745 position,
2746 )
2747}
2748
2749fn ocaml_callable_outcome_names(expression: &str) -> BTreeSet<String> {
2750 let expression = normalize_ocaml_callable_argument(expression);
2751 let Some(tree) = parse_ocaml_expression(expression) else {
2752 return BTreeSet::new();
2753 };
2754 if tree.root_node().has_error() {
2755 return BTreeSet::new();
2756 }
2757 let Some(node) = tree.root_node().named_child(0) else {
2758 return BTreeSet::new();
2759 };
2760 let mut names = BTreeSet::new();
2761 collect_ocaml_callable_outcomes(node, expression, &mut names);
2762 names
2763}
2764
2765fn collect_ocaml_callable_outcomes(node: Node<'_>, source: &str, names: &mut BTreeSet<String>) {
2766 if matches!(
2767 node.kind(),
2768 "value_path" | "value_name" | "parenthesized_operator"
2769 ) {
2770 names.insert(normalize_source(node_text(node, source)));
2771 return;
2772 }
2773 if matches!(
2774 node.kind(),
2775 "expression_item"
2776 | "parenthesized_expression"
2777 | "typed_expression"
2778 | "coercion_expression"
2779 | "let_expression"
2780 | "local_open_expression"
2781 ) {
2782 if let Some(expression) = node
2783 .child_by_field_name("body")
2784 .or_else(|| node.child_by_field_name("expression"))
2785 .or_else(|| node.named_child(0))
2786 {
2787 collect_ocaml_callable_outcomes(expression, source, names);
2788 }
2789 return;
2790 }
2791 if node.kind() == "sequence_expression" {
2792 let mut cursor = node.walk();
2793 if let Some(last) = node.named_children(&mut cursor).last() {
2794 collect_ocaml_callable_outcomes(last, source, names);
2795 }
2796 return;
2797 }
2798 if node.kind() == "if_expression" {
2799 let mut cursor = node.walk();
2800 for clause in node
2801 .named_children(&mut cursor)
2802 .filter(|child| matches!(child.kind(), "then_clause" | "else_clause"))
2803 {
2804 if let Some(expression) = clause.child_by_field_name("expression") {
2805 collect_ocaml_callable_outcomes(expression, source, names);
2806 }
2807 }
2808 return;
2809 }
2810 if matches!(node.kind(), "match_expression" | "function_expression") {
2811 let mut cursor = node.walk();
2812 for case in node
2813 .named_children(&mut cursor)
2814 .filter(|child| child.kind() == "match_case")
2815 {
2816 if let Some(body) = case.child_by_field_name("body") {
2817 collect_ocaml_callable_outcomes(body, source, names);
2818 }
2819 }
2820 }
2821}
2822
2823fn ocaml_callable_node_flow(
2824 node: Node<'_>,
2825 source: &str,
2826 caller: &FunctionInfo,
2827 caller_context: &[CallableFlow],
2828 flow_index: &OcamlFlowIndex<'_>,
2829 position: Option<&SourceSpan>,
2830) -> Option<CallableFlow> {
2831 if matches!(node.kind(), "fun_expression" | "function_expression") {
2832 let target = anonymous_callable_symbol_at(
2833 node_text(node, source),
2834 &caller.id,
2835 &flow_index.symbols,
2836 position,
2837 )?;
2838 return Some(CallableFlow {
2839 candidates: BTreeSet::from([target]),
2840 unresolved_reasons: BTreeSet::new(),
2841 });
2842 }
2843 if node.kind() == "application_expression" {
2844 let function = node.child_by_field_name("function")?;
2845 let parts = ocaml_value_path(function, source)?;
2846 let arguments = ocaml_application_arguments(node, function, source);
2847 let Some(target) = flow_index.symbols.resolve(&parts, &caller.id, position) else {
2848 let projection = match parts.as_slice() {
2849 [name] if name == "fst" => Some(0),
2850 [name] if name == "snd" => Some(1),
2851 _ => None,
2852 }?;
2853 let argument = arguments.first()?;
2854 return projected_ocaml_callable_flow(
2855 argument,
2856 &[OcamlProjection::Tuple(projection)],
2857 caller,
2858 caller_context,
2859 flow_index,
2860 position,
2861 0,
2862 );
2863 };
2864 let target_function = flow_index.symbols.get(&target)?;
2865 let target_context = flow_index
2866 .parameters
2867 .get(&target)
2868 .into_iter()
2869 .flatten()
2870 .map(|binding| {
2871 arguments
2872 .get(binding.argument_index)
2873 .and_then(|argument| project_ocaml_argument(argument, &binding.projection))
2874 .map_or_else(opaque_callable_flow, |argument| {
2875 argument_callable_flow(
2876 &argument,
2877 caller,
2878 caller_context,
2879 flow_index,
2880 position,
2881 )
2882 })
2883 })
2884 .collect::<Vec<_>>();
2885 let outcomes = flow_index.return_outcomes.get(&target)?;
2886 let mut flow = CallableFlow::default();
2887 for outcome in outcomes {
2888 merge_callable_flow(
2889 &mut flow,
2890 argument_callable_flow(
2891 outcome,
2892 target_function,
2893 &target_context,
2894 flow_index,
2895 position,
2896 ),
2897 );
2898 }
2899 return (!outcomes.is_empty()).then_some(flow);
2900 }
2901 if node.kind() == "field_get_expression" {
2902 let record = node.child_by_field_name("record")?;
2903 let field = node.child_by_field_name("field")?;
2904 return ocaml_record_field_flow(
2905 node_text(record, source),
2906 node_text(field, source),
2907 caller,
2908 caller_context,
2909 flow_index,
2910 position,
2911 0,
2912 );
2913 }
2914 if matches!(
2915 node.kind(),
2916 "value_path" | "value_name" | "parenthesized_operator"
2917 ) {
2918 return ocaml_callable_atom_flow(
2919 node_text(node, source),
2920 caller,
2921 caller_context,
2922 flow_index,
2923 position,
2924 );
2925 }
2926 if matches!(
2927 node.kind(),
2928 "expression_item"
2929 | "parenthesized_expression"
2930 | "typed_expression"
2931 | "coercion_expression"
2932 | "let_expression"
2933 | "local_open_expression"
2934 ) {
2935 let expression = node
2936 .child_by_field_name("body")
2937 .or_else(|| node.child_by_field_name("expression"))
2938 .or_else(|| node.named_child(0))?;
2939 return ocaml_callable_node_flow(
2940 expression,
2941 source,
2942 caller,
2943 caller_context,
2944 flow_index,
2945 position,
2946 );
2947 }
2948 if node.kind() == "sequence_expression" {
2949 let mut cursor = node.walk();
2950 let last = node.named_children(&mut cursor).last()?;
2951 return ocaml_callable_node_flow(
2952 last,
2953 source,
2954 caller,
2955 caller_context,
2956 flow_index,
2957 position,
2958 );
2959 }
2960 if node.kind() == "if_expression" {
2961 let mut flow = CallableFlow::default();
2962 let mut branches = 0usize;
2963 let mut cursor = node.walk();
2964 for clause in node
2965 .named_children(&mut cursor)
2966 .filter(|child| matches!(child.kind(), "then_clause" | "else_clause"))
2967 {
2968 let Some(expression) = clause.child_by_field_name("expression") else {
2969 flow.unresolved_reasons
2970 .insert(UnresolvedReason::UnsupportedConstruct);
2971 continue;
2972 };
2973 branches += 1;
2974 merge_callable_flow(
2975 &mut flow,
2976 ocaml_callable_node_flow(
2977 expression,
2978 source,
2979 caller,
2980 caller_context,
2981 flow_index,
2982 position,
2983 )
2984 .unwrap_or_else(opaque_callable_flow),
2985 );
2986 }
2987 if branches < 2 {
2988 flow.unresolved_reasons
2989 .insert(UnresolvedReason::OpaqueInput);
2990 }
2991 return Some(flow);
2992 }
2993 if matches!(node.kind(), "match_expression" | "function_expression") {
2994 let mut flow = CallableFlow::default();
2995 let mut branches = 0usize;
2996 let mut cursor = node.walk();
2997 for case in node
2998 .named_children(&mut cursor)
2999 .filter(|child| child.kind() == "match_case")
3000 {
3001 let Some(body) = case.child_by_field_name("body") else {
3002 flow.unresolved_reasons
3003 .insert(UnresolvedReason::UnsupportedConstruct);
3004 continue;
3005 };
3006 branches += 1;
3007 merge_callable_flow(
3008 &mut flow,
3009 ocaml_callable_node_flow(
3010 body,
3011 source,
3012 caller,
3013 caller_context,
3014 flow_index,
3015 position,
3016 )
3017 .unwrap_or_else(opaque_callable_flow),
3018 );
3019 }
3020 return (branches > 0).then_some(flow);
3021 }
3022 None
3023}
3024
3025#[allow(clippy::too_many_arguments)]
3026fn projected_ocaml_callable_flow(
3027 expression: &str,
3028 projection: &[OcamlProjection],
3029 caller: &FunctionInfo,
3030 caller_context: &[CallableFlow],
3031 flow_index: &OcamlFlowIndex<'_>,
3032 position: Option<&SourceSpan>,
3033 depth: usize,
3034) -> Option<CallableFlow> {
3035 if depth >= 64 {
3036 return Some(CallableFlow {
3037 candidates: BTreeSet::new(),
3038 unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
3039 });
3040 }
3041
3042 let expression = normalize_ocaml_callable_argument(expression);
3043 if is_value_identifier(expression)
3044 && let Some(local) =
3045 scoped_ocaml_local_value(flow_index.local_values, &caller.id, expression, position)
3046 {
3047 let definition_position = position_before_ocaml_scope(&local.scope);
3048 return projected_ocaml_callable_flow(
3049 &local.expression,
3050 projection,
3051 caller,
3052 caller_context,
3053 flow_index,
3054 Some(&definition_position),
3055 depth + 1,
3056 );
3057 }
3058
3059 let projected = project_ocaml_argument(expression, projection)?;
3060 Some(argument_callable_flow(
3061 &projected,
3062 caller,
3063 caller_context,
3064 flow_index,
3065 position,
3066 ))
3067}
3068
3069fn ocaml_callable_atom_flow(
3070 argument: &str,
3071 caller: &FunctionInfo,
3072 caller_context: &[CallableFlow],
3073 flow_index: &OcamlFlowIndex<'_>,
3074 position: Option<&SourceSpan>,
3075) -> Option<CallableFlow> {
3076 let argument = normalize_ocaml_callable_argument(argument);
3077 if let Some(local) =
3078 scoped_ocaml_local_value(flow_index.local_values, &caller.id, argument, position)
3079 {
3080 let definition_position = position_before_ocaml_scope(&local.scope);
3081 return Some(
3082 parse_ocaml_callable_flow(
3083 &local.expression,
3084 caller,
3085 caller_context,
3086 flow_index,
3087 Some(&definition_position),
3088 )
3089 .unwrap_or_else(opaque_callable_flow),
3090 );
3091 }
3092 if let Some(flow) = flow_index
3093 .parameters
3094 .get(&caller.id)
3095 .and_then(|parameters| {
3096 parameters.iter().position(|parameter| {
3097 parameter.kind == OcamlParameterKind::Value
3098 && parameter.name == argument
3099 && parameter.scope.as_ref().is_none_or(|scope| {
3100 position.is_some_and(|position| scope.contains(position))
3101 })
3102 })
3103 })
3104 .map(|index| {
3105 caller_context
3106 .get(index)
3107 .cloned()
3108 .unwrap_or_else(opaque_callable_flow)
3109 })
3110 {
3111 return Some(flow);
3112 }
3113 if let Some(target) = local_callable_at(
3114 argument,
3115 &caller.id,
3116 &flow_index.symbols,
3117 flow_index.local_callable_scopes,
3118 position,
3119 ) {
3120 return Some(CallableFlow {
3121 candidates: BTreeSet::from([target]),
3122 unresolved_reasons: BTreeSet::new(),
3123 });
3124 }
3125 argument_callable_symbol(argument, &caller.id, &flow_index.symbols, position).map(|target| {
3126 CallableFlow {
3127 candidates: BTreeSet::from([target]),
3128 unresolved_reasons: BTreeSet::new(),
3129 }
3130 })
3131}
3132
3133#[allow(clippy::too_many_arguments)]
3134fn ocaml_record_field_flow(
3135 record_expression: &str,
3136 requested_field: &str,
3137 caller: &FunctionInfo,
3138 caller_context: &[CallableFlow],
3139 flow_index: &OcamlFlowIndex<'_>,
3140 position: Option<&SourceSpan>,
3141 depth: usize,
3142) -> Option<CallableFlow> {
3143 if depth >= 64 {
3144 return Some(CallableFlow {
3145 candidates: BTreeSet::new(),
3146 unresolved_reasons: [UnresolvedReason::AnalysisLimit].into_iter().collect(),
3147 });
3148 }
3149
3150 let record_expression = strip_outer_ocaml_parentheses(record_expression).trim();
3151 if is_value_identifier(record_expression)
3152 && let Some(local) = scoped_ocaml_local_value(
3153 flow_index.local_values,
3154 &caller.id,
3155 record_expression,
3156 position,
3157 )
3158 {
3159 let definition_position = position_before_ocaml_scope(&local.scope);
3160 return ocaml_record_field_flow(
3161 &local.expression,
3162 requested_field,
3163 caller,
3164 caller_context,
3165 flow_index,
3166 Some(&definition_position),
3167 depth + 1,
3168 );
3169 }
3170
3171 let tree = parse_ocaml_expression(record_expression)?;
3172 if tree.root_node().has_error() {
3173 return None;
3174 }
3175 let mut record = tree.root_node().named_child(0)?;
3176 while matches!(
3177 record.kind(),
3178 "expression_item" | "parenthesized_expression"
3179 ) {
3180 record = record
3181 .child_by_field_name("expression")
3182 .or_else(|| record.named_child(0))?;
3183 }
3184 if record.kind() != "record_expression" {
3185 return None;
3186 }
3187
3188 let requested_field = normalize_source(requested_field);
3189 let requested_field = requested_field
3190 .rsplit('.')
3191 .next()
3192 .unwrap_or(&requested_field);
3193 let mut cursor = record.walk();
3194 for field in record
3195 .named_children(&mut cursor)
3196 .filter(|child| child.kind() == "field_expression")
3197 {
3198 let Some(path) = field
3199 .named_children(&mut field.walk())
3200 .find(|child| child.kind() == "field_path")
3201 else {
3202 continue;
3203 };
3204 let field_name = normalize_source(node_text(path, record_expression));
3205 if field_name.rsplit('.').next() != Some(requested_field) {
3206 continue;
3207 }
3208 if let Some(body) = field.child_by_field_name("body") {
3209 return ocaml_callable_node_flow(
3210 body,
3211 record_expression,
3212 caller,
3213 caller_context,
3214 flow_index,
3215 position,
3216 );
3217 }
3218 return ocaml_callable_atom_flow(&field_name, caller, caller_context, flow_index, position);
3219 }
3220
3221 let base = record.child_by_field_name("record")?;
3222 ocaml_record_field_flow(
3223 node_text(base, record_expression),
3224 requested_field,
3225 caller,
3226 caller_context,
3227 flow_index,
3228 position,
3229 depth + 1,
3230 )
3231}
3232
3233fn ocaml_application_arguments(
3234 application: Node<'_>,
3235 function: Node<'_>,
3236 source: &str,
3237) -> Vec<String> {
3238 let application = normalize_source(node_text(application, source));
3239 let function = normalize_source(node_text(function, source));
3240 application
3241 .strip_prefix(&function)
3242 .map(str::trim)
3243 .map(split_ocaml_arguments)
3244 .unwrap_or_default()
3245}
3246
3247fn opaque_callable_flow() -> CallableFlow {
3248 CallableFlow {
3249 candidates: BTreeSet::new(),
3250 unresolved_reasons: [UnresolvedReason::OpaqueInput].into_iter().collect(),
3251 }
3252}
3253
3254fn merge_callable_flow(destination: &mut CallableFlow, source: CallableFlow) {
3255 destination.candidates.extend(source.candidates);
3256 destination
3257 .unresolved_reasons
3258 .extend(source.unresolved_reasons);
3259}
3260
3261fn argument_callable_symbol(
3262 argument: &str,
3263 caller: &SymbolId,
3264 symbols: &OcamlSymbols<'_>,
3265 position: Option<&SourceSpan>,
3266) -> Option<SymbolId> {
3267 let argument = normalize_ocaml_callable_argument(argument);
3268 if let Some(candidate) = anonymous_callable_symbol(argument, caller, symbols) {
3269 return Some(candidate);
3270 }
3271 if let Some(candidate) =
3272 displayed_anonymous_callable_symbol(argument, caller, symbols.functions)
3273 {
3274 return Some(candidate);
3275 }
3276 let parts = argument
3277 .split('.')
3278 .map(ToOwned::to_owned)
3279 .collect::<Vec<_>>();
3280 symbols.resolve(&parts, caller, position)
3281}
3282
3283fn displayed_anonymous_callable_symbol(
3284 expression: &str,
3285 caller: &SymbolId,
3286 functions: &[FunctionInfo],
3287) -> Option<SymbolId> {
3288 let lambda = normalize_ocaml_callable_argument(expression);
3289 if !lambda.starts_with('λ') {
3290 return None;
3291 }
3292 let direct_owner = caller.to_string();
3293 let sibling_owner = caller.container.as_deref();
3294 let mut candidates = functions
3295 .iter()
3296 .filter(|candidate| {
3297 local_callable_source_name(candidate).starts_with("{lambda:")
3298 && candidate.label.default.split_whitespace().next() == Some(lambda)
3299 && (candidate.id.container.as_deref() == Some(direct_owner.as_str())
3300 || candidate.id.container.as_deref() == sibling_owner)
3301 })
3302 .map(|candidate| candidate.id.clone())
3303 .collect::<Vec<_>>();
3304 candidates.sort();
3305 candidates.dedup();
3306 (candidates.len() == 1).then(|| candidates.remove(0))
3307}
3308
3309fn anonymous_callable_symbol(
3310 expression: &str,
3311 caller: &SymbolId,
3312 symbols: &OcamlSymbols<'_>,
3313) -> Option<SymbolId> {
3314 anonymous_callable_symbol_at(expression, caller, symbols, None)
3315}
3316
3317fn anonymous_callable_symbol_at(
3318 expression: &str,
3319 caller: &SymbolId,
3320 symbols: &OcamlSymbols<'_>,
3321 position: Option<&SourceSpan>,
3322) -> Option<SymbolId> {
3323 let base = anonymous_callable_base(expression)?;
3324 let direct_owner = caller.to_string();
3325 let sibling_owner = caller.container.as_deref();
3326 let mut candidates = symbols
3327 .named(&base)
3328 .filter(|candidate| {
3329 candidate.id.container.as_deref() == Some(direct_owner.as_str())
3330 || candidate.id.container.as_deref() == sibling_owner
3331 })
3332 .map(|candidate| candidate.id.clone())
3333 .collect::<Vec<_>>();
3334 if let Some(position) = position {
3335 let positioned = candidates
3336 .iter()
3337 .filter(|candidate| {
3338 symbols
3339 .get(candidate)
3340 .is_some_and(|function| position.contains(&function.span))
3341 })
3342 .cloned()
3343 .collect::<Vec<_>>();
3344 if !positioned.is_empty() {
3345 candidates = positioned;
3346 }
3347 }
3348 candidates.sort();
3349 candidates.dedup();
3350 (candidates.len() == 1).then(|| candidates.remove(0))
3351}
3352
3353fn normalize_ocaml_callable_argument(argument: &str) -> &str {
3354 let argument = argument.trim();
3355 let argument = argument
3356 .strip_prefix(['~', '?'])
3357 .map_or(argument, |argument| {
3358 argument
3359 .split_once(':')
3360 .map_or(argument, |(_, value)| value)
3361 });
3362 strip_outer_ocaml_parentheses(argument).trim()
3363}
3364
3365fn ocaml_call_arguments(call: &CallSite) -> Vec<String> {
3366 let callee = match call.syntax.visible() {
3367 CallSyntax::Path(parts) => parts.join("."),
3368 CallSyntax::SelfMethod(method) => method.clone(),
3369 CallSyntax::Method { receiver, method } => format!("{receiver}#{method}"),
3370 CallSyntax::Expression(expression) => expression.clone(),
3371 CallSyntax::CompilerConfirmed(syntax) => syntax.key_fragment(),
3372 };
3373 let remainder = call
3374 .label
3375 .default
3376 .strip_prefix(&callee)
3377 .unwrap_or(&call.label.default)
3378 .trim();
3379 split_ocaml_arguments(remainder)
3380}
3381
3382fn split_ocaml_arguments(arguments: &str) -> Vec<String> {
3383 let mut result = Vec::new();
3384 let mut start = None;
3385 let mut delimiters = Vec::new();
3386 let mut in_string = false;
3387 let mut escaped = false;
3388 for (index, character) in arguments.char_indices() {
3389 if in_string {
3390 if escaped {
3391 escaped = false;
3392 } else if character == '\\' {
3393 escaped = true;
3394 } else if character == '"' {
3395 in_string = false;
3396 }
3397 continue;
3398 }
3399 match character {
3400 '"' => {
3401 in_string = true;
3402 start.get_or_insert(index);
3403 }
3404 '(' | '[' | '{' => {
3405 delimiters.push(character);
3406 start.get_or_insert(index);
3407 }
3408 ')' | ']' | '}' => {
3409 delimiters.pop();
3410 }
3411 character if character.is_whitespace() && delimiters.is_empty() => {
3412 if let Some(argument_start) = start.take() {
3413 result.push(arguments[argument_start..index].to_owned());
3414 }
3415 }
3416 _ => {
3417 start.get_or_insert(index);
3418 }
3419 }
3420 }
3421 if let Some(argument_start) = start {
3422 result.push(arguments[argument_start..].to_owned());
3423 }
3424 result
3425}
3426
3427fn ocaml_candidate_call_label(function: &FunctionInfo, arguments: &[String]) -> String {
3428 let mut name = local_callable_source_name(function).to_owned();
3429 let anonymous = name.starts_with("{lambda:");
3430 if anonymous {
3431 name = function
3432 .label
3433 .default
3434 .split_whitespace()
3435 .next()
3436 .unwrap_or("λ")
3437 .to_owned();
3438 }
3439 if function.id.container.is_none() && !function.id.module.is_empty() {
3440 name = function
3441 .id
3442 .module
3443 .iter()
3444 .chain(std::iter::once(&name))
3445 .cloned()
3446 .collect::<Vec<_>>()
3447 .join(".");
3448 }
3449 let mut label = if arguments.is_empty() {
3450 name
3451 } else {
3452 format!("{name} {}", arguments.join(" "))
3453 };
3454 if !anonymous
3455 && let Some(ordinal) = function
3456 .id
3457 .name
3458 .rsplit_once("#closure")
3459 .map(|(_, ordinal)| ordinal)
3460 {
3461 label.push_str(&format!(" [closure#{ordinal}]"));
3462 }
3463 label
3464}
3465
3466fn function_from_binding(
3467 file: &Path,
3468 module: &[String],
3469 binding: Node<'_>,
3470 source: &str,
3471 analysis: &mut FileAnalysis,
3472) -> Option<FunctionInfo> {
3473 let pattern = binding.child_by_field_name("pattern")?;
3474 if !matches!(pattern.kind(), "value_name" | "value_pattern") {
3475 return None;
3476 }
3477 let name = node_text(pattern, source).trim();
3478 if !is_value_identifier(name) {
3479 return None;
3480 }
3481
3482 let binding_body = binding.child_by_field_name("body")?;
3483 let mut parameters = direct_named_children(binding, "parameter");
3484 let mut body = binding_body;
3485 let mut function_expression = false;
3486 while body.kind() == "fun_expression" {
3487 function_expression = true;
3488 parameters.extend(direct_named_children(body, "parameter"));
3489 body = body.child_by_field_name("body").unwrap_or(body);
3490 }
3491 if body.kind() == "function_expression" {
3492 function_expression = true;
3493 parameters.push(body);
3494 }
3495 if parameters.is_empty() && !function_expression {
3496 return None;
3497 }
3498
3499 let id = SymbolId {
3500 language: LanguageId::new("ocaml"),
3501 module: module.to_vec(),
3502 container: None,
3503 name: name.to_owned(),
3504 };
3505 let parameter_labels = parameters
3506 .iter()
3507 .map(|parameter| {
3508 if parameter.kind() == "function_expression" {
3509 "_".to_owned()
3510 } else {
3511 parameter_default_label(*parameter, source)
3512 }
3513 })
3514 .collect::<Vec<_>>();
3515 let default_label = ocaml_declaration_label(&id, ¶meter_labels);
3516 let typed_label = typed_declaration_label(&id, ¶meters, source);
3517
3518 for (index, parameter) in parameters.iter().enumerate() {
3519 if parameter.kind() == "function_expression" {
3520 continue;
3521 }
3522 let key = parameter_labels[index].clone();
3523 let value = parameter_pattern_and_type(*parameter)
3524 .1
3525 .map(|node| normalize_source(node_text(node, source)))
3526 .unwrap_or_default();
3527 analysis.facts.push(LanguageFact {
3528 subject: id.clone(),
3529 namespace: LanguageId::new("ocaml"),
3530 kind: "parameter".to_owned(),
3531 key,
3532 value,
3533 span: tree_sitter_span(file, *parameter),
3534 });
3535 }
3536
3537 let mut return_outcomes = BTreeSet::new();
3538 collect_ocaml_callable_outcomes(body, source, &mut return_outcomes);
3539 for (index, outcome) in return_outcomes.into_iter().enumerate() {
3540 analysis.facts.push(LanguageFact {
3541 subject: id.clone(),
3542 namespace: LanguageId::new("ocaml"),
3543 kind: "callable-return".to_owned(),
3544 key: index.to_string(),
3545 value: outcome,
3546 span: tree_sitter_span(file, body),
3547 });
3548 }
3549
3550 collect_ocaml_local_value_facts(file, &id, body, source, true, &mut analysis.facts);
3551 collect_ocaml_parameter_refinement_facts(file, &id, body, source, true, &mut analysis.facts);
3552
3553 let mut calls = Vec::new();
3554 collect_calls(file, body, source, true, &mut calls);
3555
3556 Some(FunctionInfo {
3557 id,
3558 label: match typed_label {
3559 Some(typed) => CallLabel::with_types(default_label, typed),
3560 None => CallLabel::new(default_label),
3561 },
3562 public: true,
3565 calls,
3566 span: tree_sitter_span(file, binding),
3567 })
3568}
3569
3570fn collect_ocaml_local_value_facts(
3571 file: &Path,
3572 owner: &SymbolId,
3573 node: Node<'_>,
3574 source: &str,
3575 is_root: bool,
3576 facts: &mut Vec<LanguageFact>,
3577) {
3578 if !is_root && matches!(node.kind(), "fun_expression" | "function_expression") {
3579 return;
3580 }
3581 if node.kind() == "let_expression" {
3582 let bindings = direct_named_children(node, "value_definition")
3583 .into_iter()
3584 .flat_map(|definition| direct_named_children(definition, "let_binding"));
3585 for binding in bindings {
3586 let Some(body) = binding.child_by_field_name("body") else {
3587 continue;
3588 };
3589 let is_callable_binding = !direct_named_children(binding, "parameter").is_empty()
3590 || matches!(body.kind(), "fun_expression" | "function_expression");
3591 if is_callable_binding {
3592 continue;
3593 }
3594 if let Some(pattern) = binding.child_by_field_name("pattern") {
3595 let mut bindings = Vec::new();
3596 collect_ocaml_parameter_bindings(
3597 node_text(pattern, source),
3598 0,
3599 &mut Vec::new(),
3600 &mut bindings,
3601 );
3602 for local in bindings
3603 .into_iter()
3604 .filter(|binding| binding.kind == OcamlParameterKind::Value)
3605 {
3606 let Some(value) =
3607 project_ocaml_argument(node_text(body, source), &local.projection)
3608 else {
3609 continue;
3610 };
3611 facts.push(LanguageFact {
3612 subject: owner.clone(),
3613 namespace: LanguageId::new("ocaml"),
3614 kind: "local-value".to_owned(),
3615 key: local.name,
3616 value,
3617 span: tree_sitter_span(
3618 file,
3619 node.child_by_field_name("body").unwrap_or(node),
3620 ),
3621 });
3622 }
3623 }
3624 }
3625 }
3626
3627 let mut cursor = node.walk();
3628 for child in node.named_children(&mut cursor) {
3629 collect_ocaml_local_value_facts(file, owner, child, source, false, facts);
3630 }
3631}
3632
3633fn collect_ocaml_parameter_refinement_facts(
3634 file: &Path,
3635 owner: &SymbolId,
3636 node: Node<'_>,
3637 source: &str,
3638 is_root: bool,
3639 facts: &mut Vec<LanguageFact>,
3640) {
3641 if !is_root && matches!(node.kind(), "fun_expression" | "function_expression") {
3642 return;
3643 }
3644 if node.kind() == "let_binding" && !direct_named_children(node, "parameter").is_empty() {
3645 return;
3646 }
3647
3648 if node.kind() == "match_expression"
3649 && let Some(scrutinee) = node.child_by_field_name("expression")
3650 {
3651 let scrutinee = normalize_source(node_text(scrutinee, source));
3652 if is_value_identifier(&scrutinee) {
3653 for case in direct_named_children(node, "match_case") {
3654 let (Some(pattern), Some(body)) = (
3655 case.child_by_field_name("pattern"),
3656 case.child_by_field_name("body"),
3657 ) else {
3658 continue;
3659 };
3660 push_ocaml_parameter_refinements(
3661 file,
3662 owner,
3663 &scrutinee,
3664 node_text(pattern, source),
3665 body,
3666 facts,
3667 );
3668 }
3669 }
3670 }
3671
3672 if node.kind() == "let_expression"
3673 && let Some(scope) = node.child_by_field_name("body")
3674 {
3675 for binding in direct_named_children(node, "value_definition")
3676 .into_iter()
3677 .flat_map(|definition| direct_named_children(definition, "let_binding"))
3678 {
3679 let (Some(pattern), Some(value)) = (
3680 binding.child_by_field_name("pattern"),
3681 binding.child_by_field_name("body"),
3682 ) else {
3683 continue;
3684 };
3685 let value = normalize_source(node_text(value, source));
3686 if is_value_identifier(&value) {
3687 push_ocaml_parameter_refinements(
3688 file,
3689 owner,
3690 &value,
3691 node_text(pattern, source),
3692 scope,
3693 facts,
3694 );
3695 }
3696 }
3697 }
3698
3699 let mut cursor = node.walk();
3700 for child in node.named_children(&mut cursor) {
3701 collect_ocaml_parameter_refinement_facts(file, owner, child, source, false, facts);
3702 }
3703}
3704
3705fn push_ocaml_parameter_refinements(
3706 file: &Path,
3707 owner: &SymbolId,
3708 source_name: &str,
3709 pattern: &str,
3710 scope: Node<'_>,
3711 facts: &mut Vec<LanguageFact>,
3712) {
3713 let mut bindings = Vec::new();
3714 collect_ocaml_parameter_bindings(pattern, 0, &mut Vec::new(), &mut bindings);
3715 for binding in bindings {
3716 if binding.kind != OcamlParameterKind::Value {
3717 continue;
3718 }
3719 facts.push(LanguageFact {
3720 subject: owner.clone(),
3721 namespace: LanguageId::new("ocaml"),
3722 kind: "parameter-refinement".to_owned(),
3723 key: binding.name,
3724 value: format!("{source_name}\n{}", normalize_source(pattern)),
3725 span: tree_sitter_span(file, scope),
3726 });
3727 }
3728}
3729
3730fn collect_calls(
3731 file: &Path,
3732 node: Node<'_>,
3733 source: &str,
3734 is_callable_body: bool,
3735 calls: &mut Vec<CallSite>,
3736) {
3737 if node.kind() == "application_expression" {
3738 let mut cursor = node.walk();
3742 for child in node.named_children(&mut cursor) {
3743 collect_calls(file, child, source, false, calls);
3744 }
3745 if let Some(function) = node.child_by_field_name("function") {
3746 if matches!(
3747 function.kind(),
3748 "constructor_path" | "constructor_name" | "tag"
3749 ) {
3750 return;
3751 }
3752 if let Some(parts) = ocaml_value_path(function, source) {
3753 if ocaml_path_is_constructor(&parts) {
3754 return;
3755 }
3756 let label = ocaml_path_call_label(node, function, &parts, source);
3757 let syntax = CallSyntax::Path(parts);
3758 let span = tree_sitter_span(file, node);
3759 calls.push(CallSite {
3760 id: CallSiteId::source(&syntax, &span),
3761 syntax,
3762 target: CallTarget::Unresolved,
3763 label: CallLabel::new(label),
3764 span,
3765 });
3766 } else if function.kind() == "method_invocation" {
3767 let syntax = CallSyntax::Path(vec![normalize_source(node_text(function, source))]);
3768 let span = tree_sitter_span(file, node);
3769 calls.push(CallSite {
3770 id: CallSiteId::source(&syntax, &span),
3771 syntax,
3772 target: CallTarget::Dynamic {
3773 dispatch: SymbolId {
3774 language: LanguageId::new("ocaml"),
3775 module: Vec::new(),
3776 container: None,
3777 name: format!(
3778 "object-method@{}:{}",
3779 node.start_position().row + 1,
3780 node.start_position().column
3781 ),
3782 },
3783 candidates: Vec::new(),
3784 resolution: DispatchResolution::Unresolved,
3785 evidence: DispatchEvidence::ExactFlow,
3786 unresolved_reasons: [UnresolvedReason::OpaqueInput].into_iter().collect(),
3787 },
3788 label: CallLabel::new(normalize_source(node_text(node, source))),
3789 span,
3790 });
3791 } else {
3792 let syntax = CallSyntax::Expression(normalize_source(node_text(function, source)));
3793 let span = tree_sitter_span(file, node);
3794 calls.push(CallSite {
3795 id: CallSiteId::source(&syntax, &span),
3796 syntax,
3797 target: CallTarget::Unresolved,
3798 label: CallLabel::new(normalize_source(node_text(node, source))),
3799 span,
3800 });
3801 }
3802 }
3803 return;
3804 } else if !is_callable_body && matches!(node.kind(), "fun_expression" | "function_expression") {
3805 return;
3808 } else if node.kind() == "let_binding" && !direct_named_children(node, "parameter").is_empty() {
3809 return;
3812 }
3813
3814 let mut cursor = node.walk();
3815 for child in node.named_children(&mut cursor) {
3816 collect_calls(file, child, source, false, calls);
3817 }
3818}
3819
3820fn ocaml_path_is_constructor(parts: &[String]) -> bool {
3821 parts.last().is_some_and(|name| {
3822 name.starts_with('`')
3823 || name
3824 .chars()
3825 .next()
3826 .is_some_and(|character| character.is_ascii_uppercase())
3827 })
3828}
3829
3830fn ocaml_path_call_label(
3831 application: Node<'_>,
3832 function: Node<'_>,
3833 parts: &[String],
3834 source: &str,
3835) -> String {
3836 let application = normalize_source(node_text(application, source));
3837 let function = normalize_source(node_text(function, source));
3838 let qualified = parts.join(".");
3839 application
3840 .strip_prefix(&function)
3841 .map_or(application.clone(), |arguments| {
3842 format!("{qualified}{arguments}")
3843 })
3844}
3845
3846fn ocaml_value_path(function: Node<'_>, source: &str) -> Option<Vec<String>> {
3847 if !matches!(
3848 function.kind(),
3849 "value_path" | "value_name" | "parenthesized_operator"
3850 ) {
3851 return None;
3852 }
3853 let raw = normalize_source(node_text(function, source));
3854 let mut parts = raw
3855 .split('.')
3856 .map(str::trim)
3857 .filter(|part| !part.is_empty())
3858 .map(ToOwned::to_owned)
3859 .collect::<Vec<_>>();
3860 let mut opened = Vec::new();
3861 let mut ancestor = function.parent();
3862 while let Some(node) = ancestor {
3863 if node.kind() == "local_open_expression"
3864 && let Some(module_path) = direct_named_child(node, "module_path")
3865 {
3866 let mut prefix = normalize_source(node_text(module_path, source))
3867 .split('.')
3868 .map(str::trim)
3869 .filter(|part| !part.is_empty())
3870 .map(ToOwned::to_owned)
3871 .collect::<Vec<_>>();
3872 prefix.append(&mut opened);
3873 opened = prefix;
3874 }
3875 ancestor = node.parent();
3876 }
3877 opened.append(&mut parts);
3878 let parts = opened;
3879 (!parts.is_empty()).then_some(parts)
3880}
3881
3882fn direct_named_child<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
3883 let mut cursor = node.walk();
3884 node.named_children(&mut cursor)
3885 .find(|child| child.kind() == kind)
3886}
3887
3888fn direct_named_children<'tree>(node: Node<'tree>, kind: &str) -> Vec<Node<'tree>> {
3889 let mut cursor = node.walk();
3890 node.named_children(&mut cursor)
3891 .filter(|child| child.kind() == kind)
3892 .collect()
3893}
3894
3895fn node_text<'a>(node: Node<'_>, source: &'a str) -> &'a str {
3896 node.utf8_text(source.as_bytes())
3897 .expect("tree-sitter nodes always point into the parsed UTF-8 source")
3898}
3899
3900fn ocaml_declaration_label(id: &SymbolId, parameters: &[String]) -> String {
3901 let name = id
3902 .module
3903 .iter()
3904 .chain(std::iter::once(&id.name))
3905 .cloned()
3906 .collect::<Vec<_>>()
3907 .join(".");
3908 if parameters.is_empty() {
3909 name
3910 } else {
3911 format!("{name} {}", parameters.join(" "))
3912 }
3913}
3914
3915fn typed_declaration_label(id: &SymbolId, parameters: &[Node<'_>], source: &str) -> Option<String> {
3916 let mut found_type = false;
3917 let labels = parameters
3918 .iter()
3919 .map(|parameter| {
3920 if parameter.kind() == "function_expression" {
3921 return "_".to_owned();
3922 }
3923 let (label, typed) = parameter_typed_label(*parameter, source);
3924 found_type |= typed;
3925 label
3926 })
3927 .collect::<Vec<_>>();
3928 found_type.then(|| ocaml_declaration_label(id, &labels))
3929}
3930
3931fn parameter_default_label(parameter: Node<'_>, source: &str) -> String {
3932 let (pattern_node, ty) = parameter_pattern_and_type(parameter);
3933 if ty.is_none() {
3934 return normalize_source(node_text(parameter, source));
3935 }
3936
3937 let pattern = pattern_node
3938 .map(|pattern| normalize_source(node_text(pattern, source)))
3939 .unwrap_or_else(|| "_".to_owned());
3940 let Some(label) = direct_named_child(parameter, "label_name") else {
3941 return pattern;
3942 };
3943 let marker = node_text(parameter, source)
3944 .trim_start()
3945 .chars()
3946 .next()
3947 .filter(|marker| matches!(marker, '~' | '?'))
3948 .unwrap_or('~');
3949 let label = normalize_source(node_text(label, source));
3950 if pattern == label {
3951 format!("{marker}{label}")
3952 } else {
3953 format!("{marker}{label}:{pattern}")
3954 }
3955}
3956
3957fn parameter_typed_label(parameter: Node<'_>, source: &str) -> (String, bool) {
3958 let (pattern_node, ty) = parameter_pattern_and_type(parameter);
3959 let Some(ty) = ty else {
3960 return (parameter_default_label(parameter, source), false);
3961 };
3962 let pattern = pattern_node
3963 .map(|pattern| normalize_source(node_text(pattern, source)))
3964 .unwrap_or_else(|| "_".to_owned());
3965 let annotated = format!("({pattern} : {})", normalize_source(node_text(ty, source)));
3966 let Some(label) = direct_named_child(parameter, "label_name") else {
3967 return (annotated, true);
3968 };
3969 let marker = node_text(parameter, source)
3970 .trim_start()
3971 .chars()
3972 .next()
3973 .filter(|marker| matches!(marker, '~' | '?'))
3974 .unwrap_or('~');
3975 (
3976 format!(
3977 "{marker}{}:{annotated}",
3978 normalize_source(node_text(label, source))
3979 ),
3980 true,
3981 )
3982}
3983
3984fn parameter_pattern_and_type<'tree>(
3985 parameter: Node<'tree>,
3986) -> (Option<Node<'tree>>, Option<Node<'tree>>) {
3987 let pattern = parameter.child_by_field_name("pattern");
3988 match pattern {
3989 Some(pattern) if pattern.kind() == "typed_pattern" => (
3990 pattern.child_by_field_name("pattern"),
3991 pattern.child_by_field_name("type"),
3992 ),
3993 pattern => (pattern, parameter.child_by_field_name("type")),
3994 }
3995}
3996
3997fn is_value_identifier(value: &str) -> bool {
3998 let mut chars = value.chars();
3999 chars
4000 .next()
4001 .is_some_and(|first| first == '_' || first.is_ascii_lowercase())
4002 && chars.all(|character| {
4003 character == '_' || character == '\'' || character.is_ascii_alphanumeric()
4004 })
4005}
4006
4007fn tree_sitter_span(file: &Path, node: Node<'_>) -> SourceSpan {
4008 SourceSpan {
4009 file: file.to_path_buf(),
4010 start_line: node.start_position().row + 1,
4011 start_column: node.start_position().column,
4012 start_byte: Some(node.start_byte()),
4013 end_line: node.end_position().row + 1,
4014 end_column: node.end_position().column,
4015 end_byte: Some(node.end_byte()),
4016 }
4017}
4018
4019fn normalize_source(source: &str) -> String {
4020 let mut output = String::with_capacity(source.len());
4021 let mut in_string = false;
4022 let mut escaped = false;
4023 let mut pending_space = false;
4024
4025 for character in source.chars() {
4026 if in_string {
4027 output.push(character);
4028 if escaped {
4029 escaped = false;
4030 } else if character == '\\' {
4031 escaped = true;
4032 } else if character == '"' {
4033 in_string = false;
4034 }
4035 continue;
4036 }
4037
4038 if character == '"' {
4039 if pending_space && !output.is_empty() {
4040 output.push(' ');
4041 }
4042 pending_space = false;
4043 in_string = true;
4044 output.push(character);
4045 } else if character.is_whitespace() {
4046 pending_space = true;
4047 } else {
4048 if pending_space && !output.is_empty() {
4049 output.push(' ');
4050 }
4051 pending_space = false;
4052 output.push(character);
4053 }
4054 }
4055 output.trim().to_owned()
4056}
4057
4058#[cfg(test)]
4059mod tests {
4060 use std::path::Path;
4061
4062 use super::*;
4063
4064 #[test]
4065 fn extracts_module_functions_and_native_call_labels() {
4066 let source = include_str!("../../examples/ocaml/after.ml");
4067 let analysis = OcamlFrontend
4068 .analyze_file(
4069 &FileContext {
4070 path: Path::new("after.ml"),
4071 module: &[],
4072 },
4073 source,
4074 )
4075 .unwrap();
4076
4077 let save = analysis
4078 .functions
4079 .iter()
4080 .find(|function| function.id.module == ["Postgres"] && function.id.name == "save")
4081 .unwrap();
4082 assert_eq!(save.label.default, "Postgres.save order");
4083 assert_eq!(
4084 save.calls
4085 .iter()
4086 .map(|call| call.label.default.as_str())
4087 .collect::<Vec<_>>(),
4088 ["Sql.begin_tx order", "Sql.insert order", "Sql.commit order"]
4089 );
4090 }
4091
4092 #[test]
4093 fn keeps_labeled_optional_and_unit_application_syntax() {
4094 let source = r#"
4095 let run order =
4096 charge ~currency:"KRW" order 100;
4097 find ?limit:None ~tenant:"acme" order;
4098 commit ()
4099 "#;
4100 let analysis = OcamlFrontend
4101 .analyze_file(
4102 &FileContext {
4103 path: Path::new("calls.ml"),
4104 module: &[],
4105 },
4106 source,
4107 )
4108 .unwrap();
4109 let calls = &analysis.functions[0].calls;
4110 assert_eq!(calls[0].label.default, "charge ~currency:\"KRW\" order 100");
4111 assert_eq!(
4112 calls[1].label.default,
4113 "find ?limit:None ~tenant:\"acme\" order"
4114 );
4115 assert_eq!(calls[2].label.default, "commit ()");
4116 }
4117
4118 #[test]
4119 fn separates_ocaml_parameter_types_from_the_default_label() {
4120 let source = "let run (order : Order.t) = validate order";
4121 let analysis = OcamlFrontend
4122 .analyze_file(
4123 &FileContext {
4124 path: Path::new("typed.ml"),
4125 module: &[],
4126 },
4127 source,
4128 )
4129 .unwrap();
4130 let label = &analysis.functions[0].label;
4131 assert_eq!(label.default, "run order");
4132 assert_eq!(label.typed.as_deref(), Some("run (order : Order.t)"));
4133 }
4134
4135 #[test]
4136 fn source_project_resolves_calls_across_ocaml_modules() {
4137 let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
4138 let directory = std::env::temp_dir().join(format!(
4139 "diffkit-ocaml-source-project-{}-{sequence}",
4140 std::process::id()
4141 ));
4142 fs::create_dir(&directory).unwrap();
4143 fs::write(directory.join("a.ml"), "let entry value = B.save value\n").unwrap();
4144 fs::write(directory.join("b.ml"), "let save value = value\n").unwrap();
4145
4146 let analysis = analyze_source_project(&directory).unwrap();
4147 let entry = analysis
4148 .functions
4149 .iter()
4150 .find(|function| function.id.module == ["A"] && function.id.name == "entry")
4151 .unwrap();
4152 assert!(matches!(
4153 &entry.calls[0].target,
4154 CallTarget::Direct(target) if target.module == ["B"] && target.name == "save"
4155 ));
4156 fs::remove_dir_all(directory).unwrap();
4157 }
4158
4159 #[test]
4160 fn maps_dune_build_locations_back_to_source_files() {
4161 let sequence = OCAML_TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
4162 let directory = std::env::temp_dir().join(format!(
4163 "diffkit-ocaml-dune-path-{}-{sequence}",
4164 std::process::id()
4165 ));
4166 fs::create_dir_all(directory.join("src")).unwrap();
4167 fs::write(directory.join("src/service.ml"), "let run () = ()\n").unwrap();
4168 let mapped = dune_source_path(&directory, &directory.join("_build/default/src/service.ml"));
4169 assert_eq!(mapped, Some(directory.join("src/service.ml")));
4170 fs::remove_dir_all(directory).unwrap();
4171 }
4172
4173 #[test]
4174 fn compiler_libs_adapter_compiles_when_ocaml_is_installed() {
4175 if Command::new("ocamlc").arg("-version").output().is_err() {
4176 return;
4177 }
4178 let extractor = OcamlExtractor::compile().unwrap();
4179 assert!(extractor.executable().is_file());
4180 }
4181
4182 #[test]
4183 fn splits_curried_inferred_types_without_splitting_nested_arrows() {
4184 assert_eq!(
4185 split_ocaml_function_type("('a -> 'b) -> label:int -> 'a -> 'b"),
4186 ["('a -> 'b)", "label:int", "'a", "'b"]
4187 );
4188 }
4189
4190 #[test]
4191 fn typedtree_identifiers_do_not_turn_callable_parameters_into_static_functions() {
4192 let source = "let apply callback value = callback value";
4193 let path = Path::new("callback.ml");
4194 let mut analysis =
4195 analyze_ocaml_syntax(&FileContext { path, module: &[] }, source).unwrap();
4196 let span = analysis.functions[0].calls[0].span.clone();
4197 apply_typed_events(
4198 &mut analysis,
4199 &[TypedCallEvent {
4200 target: Some("callback".to_owned()),
4201 signature: Some("'a -> 'b".to_owned()),
4202 span,
4203 }],
4204 );
4205
4206 assert!(matches!(
4207 analysis.functions[0].calls[0].target,
4208 CallTarget::Unresolved
4209 ));
4210 }
4211
4212 #[test]
4213 fn one_typedtree_event_cannot_be_reused_by_two_source_calls() {
4214 let source = "let run value = outer (inner value)";
4215 let path = Path::new("nested.ml");
4216 let mut analysis =
4217 analyze_ocaml_syntax(&FileContext { path, module: &[] }, source).unwrap();
4218 let span = analysis.functions[0]
4219 .calls
4220 .iter()
4221 .find(|call| call.label.default == "inner value")
4222 .unwrap()
4223 .span
4224 .clone();
4225 apply_typed_events(
4226 &mut analysis,
4227 &[TypedCallEvent {
4228 target: Some("Inner.target".to_owned()),
4229 signature: None,
4230 span,
4231 }],
4232 );
4233
4234 assert_eq!(
4235 analysis.functions[0]
4236 .calls
4237 .iter()
4238 .filter(|call| matches!(call.target, CallTarget::Direct(_)))
4239 .count(),
4240 1
4241 );
4242 }
4243
4244 #[test]
4245 fn extracts_functions_declared_inside_a_functor_body() {
4246 let source = r#"
4247 module Make (Store : sig val save : int -> unit end) = struct
4248 let run value = Store.save value
4249 end
4250 "#;
4251 let analysis = OcamlFrontend
4252 .analyze_file(
4253 &FileContext {
4254 path: Path::new("functor.ml"),
4255 module: &[],
4256 },
4257 source,
4258 )
4259 .unwrap();
4260 assert!(
4261 analysis
4262 .functions
4263 .iter()
4264 .any(|function| { function.id.module == ["Make"] && function.id.name == "run" })
4265 );
4266 }
4267}