1use super::type_facts::{DynamicBoundary, ShapeFact, TypeEvidence, TypeFact};
10use super::type_inference::{PerlType, TypeEnvironment};
11use crate::ast::{Node, NodeKind};
12use perl_semantic_facts::Confidence;
13
14#[derive(Debug, Clone, Copy, Default)]
16#[non_exhaustive]
17pub struct ReceiverFactContext<'a> {
18 pub type_environment: Option<&'a TypeEnvironment>,
20 pub source: Option<&'a str>,
23}
24
25impl<'a> ReceiverFactContext<'a> {
26 pub fn new(type_environment: Option<&'a TypeEnvironment>) -> Self {
28 Self { type_environment, source: None }
29 }
30
31 pub fn with_source(mut self, source: &'a str) -> Self {
33 self.source = Some(source);
34 self
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub enum ReceiverKind {
42 SelfReceiver,
44 ObjectVariable,
46 StaticPackage,
48 HashSlot,
50 HashRefSlot,
52 ArrayIndex,
54 DynamicKey,
56 Unknown,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum ReceiverFactFreshness {
64 Fresh,
66 Unknown,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum ReceiverFallbackState {
74 Exact,
76 Fallback,
78 Blocked,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84#[non_exhaustive]
85pub struct ReceiverFact {
86 pub kind: ReceiverKind,
88 pub package: Option<String>,
90 pub shape: Option<ShapeFact>,
92 pub confidence: Confidence,
94 pub evidence: Vec<TypeEvidence>,
96 pub freshness: ReceiverFactFreshness,
98 pub dynamic_boundary: Option<DynamicBoundary>,
100 pub source_range: Option<(usize, usize)>,
102 pub fallback_state: ReceiverFallbackState,
104}
105
106impl ReceiverFact {
107 fn unknown(receiver: &Node, reason: impl Into<String>) -> Self {
108 Self {
109 kind: ReceiverKind::Unknown,
110 package: None,
111 shape: None,
112 confidence: Confidence::Low,
113 evidence: vec![TypeEvidence::Heuristic { reason: reason.into() }],
114 freshness: ReceiverFactFreshness::Unknown,
115 dynamic_boundary: Some(DynamicBoundary::UnknownReceiver),
116 source_range: Some((receiver.location.start, receiver.location.end)),
117 fallback_state: ReceiverFallbackState::Fallback,
118 }
119 }
120
121 fn dynamic_key(receiver: &Node, evidence: TypeEvidence) -> Self {
122 Self {
123 kind: ReceiverKind::DynamicKey,
124 package: None,
125 shape: None,
126 confidence: Confidence::Low,
127 evidence: vec![evidence],
128 freshness: ReceiverFactFreshness::Unknown,
129 dynamic_boundary: Some(DynamicBoundary::DynamicHashKey),
130 source_range: Some((receiver.location.start, receiver.location.end)),
131 fallback_state: ReceiverFallbackState::Fallback,
132 }
133 }
134
135 fn from_type_fact(kind: ReceiverKind, fact: TypeFact, receiver: &Node) -> Self {
136 let package = package_from_type_fact(&fact);
137 let fallback_state = fallback_state_for_fact(package.as_deref(), &fact);
138 Self {
139 kind,
140 package,
141 shape: fact.shape,
142 confidence: fact.confidence,
143 evidence: fact.evidence,
144 freshness: ReceiverFactFreshness::Fresh,
145 dynamic_boundary: fact.dynamic_boundary,
146 source_range: Some((receiver.location.start, receiver.location.end)),
147 fallback_state,
148 }
149 }
150}
151
152pub fn receiver_fact_for_method_call(
154 call: &Node,
155 context: ReceiverFactContext<'_>,
156) -> ReceiverFact {
157 let NodeKind::MethodCall { object, method, .. } = &call.kind else {
158 return ReceiverFact::unknown(call, "node is not a method call");
159 };
160
161 infer_receiver_fact(object, Some(method.as_str()), context)
162}
163
164pub fn infer_receiver_fact(
166 receiver: &Node,
167 method_name: Option<&str>,
168 context: ReceiverFactContext<'_>,
169) -> ReceiverFact {
170 match &receiver.kind {
171 NodeKind::Variable { sigil, name } if sigil == "$" => {
172 variable_receiver_fact(receiver, name, context)
173 }
174 NodeKind::VariableWithAttributes { variable, .. } => {
175 infer_receiver_fact(variable, method_name, context)
176 }
177 NodeKind::Identifier { name } => static_package_receiver(receiver, name, method_name),
178 NodeKind::String { value, .. } => {
179 let normalized = normalize_package_string(value);
180 match normalized {
181 Some(package) => static_package_receiver(receiver, &package, method_name),
182 None => ReceiverFact::unknown(receiver, "empty package string receiver"),
183 }
184 }
185 NodeKind::Binary { op, left, right } if op == "{}" || op == "->{}" => {
186 hash_receiver_fact(receiver, left, right, context)
187 }
188 NodeKind::Binary { op, left, right } if op == "[]" || op == "->[]" => {
189 array_receiver_fact(receiver, left, right, context)
190 }
191 NodeKind::MethodCall { .. } => ReceiverFact::unknown(
192 receiver,
193 "receiver is itself a method call and requires completion-chain evidence",
194 ),
195 _ => ReceiverFact::unknown(receiver, "receiver expression has no source-backed fact"),
196 }
197}
198
199fn variable_receiver_fact(
200 receiver: &Node,
201 name: &str,
202 context: ReceiverFactContext<'_>,
203) -> ReceiverFact {
204 let kind = if is_self_like_name(name) {
205 ReceiverKind::SelfReceiver
206 } else {
207 ReceiverKind::ObjectVariable
208 };
209
210 if let Some(fact) = context.type_environment.and_then(|env| env.get_fact_at(name)) {
211 return ReceiverFact::from_type_fact(kind, fact, receiver);
212 }
213
214 if is_self_like_name(name) {
215 return ReceiverFact {
216 kind,
217 package: None,
218 shape: None,
219 confidence: Confidence::Medium,
220 evidence: vec![TypeEvidence::Heuristic {
221 reason: "self-like receiver without package fact".to_string(),
222 }],
223 freshness: ReceiverFactFreshness::Unknown,
224 dynamic_boundary: None,
225 source_range: Some((receiver.location.start, receiver.location.end)),
226 fallback_state: ReceiverFallbackState::Fallback,
227 };
228 }
229
230 ReceiverFact::unknown(receiver, "object variable has no type fact")
231}
232
233fn static_package_receiver(
234 receiver: &Node,
235 package: &str,
236 method_name: Option<&str>,
237) -> ReceiverFact {
238 let evidence = if method_name == Some("new") {
239 TypeEvidence::ConstructorCall { package: package.to_string() }
240 } else {
241 TypeEvidence::Heuristic { reason: "static package receiver".to_string() }
242 };
243
244 ReceiverFact {
245 kind: ReceiverKind::StaticPackage,
246 package: Some(package.to_string()),
247 shape: None,
248 confidence: Confidence::High,
249 evidence: vec![evidence],
250 freshness: ReceiverFactFreshness::Fresh,
251 dynamic_boundary: None,
252 source_range: Some((receiver.location.start, receiver.location.end)),
253 fallback_state: ReceiverFallbackState::Exact,
254 }
255}
256
257fn hash_receiver_fact(
258 receiver: &Node,
259 left: &Node,
260 right: &Node,
261 context: ReceiverFactContext<'_>,
262) -> ReceiverFact {
263 let Some(key) = static_slot_key(right) else {
264 return ReceiverFact::dynamic_key(
265 receiver,
266 TypeEvidence::Heuristic { reason: "hash receiver key is dynamic".to_string() },
267 );
268 };
269
270 let base = receiver_base_label(left);
271 let kind = if matches!(&receiver.kind, NodeKind::Binary { op, .. } if op == "->{}")
272 || receiver_text(receiver, context.source).is_some_and(|text| text.contains("->{"))
273 {
274 ReceiverKind::HashRefSlot
275 } else {
276 ReceiverKind::HashSlot
277 };
278 let evidence = match kind {
279 ReceiverKind::HashRefSlot => TypeEvidence::HashRefSlot { base: base.clone(), key },
280 _ => TypeEvidence::HashSlot { hash: base.clone(), key },
281 };
282
283 let Some(container_fact) = receiver_container_fact(left, context) else {
284 return ReceiverFact {
285 kind,
286 package: None,
287 shape: None,
288 confidence: Confidence::Low,
289 evidence: vec![evidence],
290 freshness: ReceiverFactFreshness::Unknown,
291 dynamic_boundary: None,
292 source_range: Some((receiver.location.start, receiver.location.end)),
293 fallback_state: ReceiverFallbackState::Fallback,
294 };
295 };
296
297 if let Some(slot_fact) = hash_slot_type_fact(&container_fact, &evidence) {
298 return ReceiverFact::from_type_fact(
299 kind,
300 with_extra_evidence(slot_fact, evidence),
301 receiver,
302 );
303 }
304
305 ReceiverFact {
306 kind,
307 package: None,
308 shape: container_fact.shape,
309 confidence: Confidence::Low,
310 evidence: vec![evidence],
311 freshness: ReceiverFactFreshness::Fresh,
312 dynamic_boundary: container_fact.dynamic_boundary,
313 source_range: Some((receiver.location.start, receiver.location.end)),
314 fallback_state: ReceiverFallbackState::Fallback,
315 }
316}
317
318fn array_receiver_fact(
319 receiver: &Node,
320 left: &Node,
321 right: &Node,
322 context: ReceiverFactContext<'_>,
323) -> ReceiverFact {
324 let evidence = TypeEvidence::Heuristic { reason: "array index receiver".to_string() };
325 let Some(container_fact) = receiver_container_fact(left, context) else {
326 return ReceiverFact {
327 kind: ReceiverKind::ArrayIndex,
328 package: None,
329 shape: None,
330 confidence: Confidence::Low,
331 evidence: vec![evidence],
332 freshness: ReceiverFactFreshness::Unknown,
333 dynamic_boundary: None,
334 source_range: Some((receiver.location.start, receiver.location.end)),
335 fallback_state: ReceiverFallbackState::Fallback,
336 };
337 };
338
339 let Some(index) = static_array_index(right) else {
340 return ReceiverFact {
341 kind: ReceiverKind::ArrayIndex,
342 package: None,
343 shape: container_fact.shape,
344 confidence: Confidence::Low,
345 evidence: vec![evidence],
346 freshness: ReceiverFactFreshness::Unknown,
347 dynamic_boundary: Some(DynamicBoundary::UnknownReceiver),
348 source_range: Some((receiver.location.start, receiver.location.end)),
349 fallback_state: ReceiverFallbackState::Fallback,
350 };
351 };
352
353 if let Some(index_fact) = array_index_type_fact(&container_fact, index) {
354 return ReceiverFact::from_type_fact(
355 ReceiverKind::ArrayIndex,
356 with_extra_evidence(index_fact, evidence),
357 receiver,
358 );
359 }
360
361 ReceiverFact {
362 kind: ReceiverKind::ArrayIndex,
363 package: None,
364 shape: container_fact.shape,
365 confidence: Confidence::Low,
366 evidence: vec![evidence],
367 freshness: ReceiverFactFreshness::Fresh,
368 dynamic_boundary: container_fact.dynamic_boundary,
369 source_range: Some((receiver.location.start, receiver.location.end)),
370 fallback_state: ReceiverFallbackState::Fallback,
371 }
372}
373
374fn receiver_container_fact(left: &Node, context: ReceiverFactContext<'_>) -> Option<TypeFact> {
375 let (_, name) = variable_identity(left)?;
376 context.type_environment.and_then(|env| env.get_fact_at(name))
377}
378
379fn hash_slot_type_fact(container_fact: &TypeFact, evidence: &TypeEvidence) -> Option<TypeFact> {
380 let key = match evidence {
381 TypeEvidence::HashSlot { key, .. } | TypeEvidence::HashRefSlot { key, .. } => key,
382 _ => return None,
383 };
384 match &container_fact.shape {
385 Some(ShapeFact::Hash(shape)) => shape
386 .slots
387 .get(key)
388 .cloned()
389 .or_else(|| shape.fallback_value.as_ref().map(|fact| fact.as_ref().clone())),
390 Some(ShapeFact::Object(shape)) => shape.fields.get(key).cloned(),
391 _ => None,
392 }
393}
394
395fn array_index_type_fact(container_fact: &TypeFact, index: usize) -> Option<TypeFact> {
396 match &container_fact.shape {
397 Some(ShapeFact::Array(shape)) => shape
398 .indexed
399 .get(&index)
400 .cloned()
401 .or_else(|| shape.element.as_ref().map(|fact| fact.as_ref().clone())),
402 _ => None,
403 }
404}
405
406fn with_extra_evidence(mut fact: TypeFact, evidence: TypeEvidence) -> TypeFact {
407 fact.evidence.push(evidence);
408 fact
409}
410
411fn fallback_state_for_fact(package: Option<&str>, fact: &TypeFact) -> ReceiverFallbackState {
412 if package.is_some_and(|package| type_fact_has_exact_package(fact, package))
413 && fact.confidence == Confidence::High
414 && fact.dynamic_boundary.is_none()
415 {
416 ReceiverFallbackState::Exact
417 } else {
418 ReceiverFallbackState::Fallback
419 }
420}
421
422fn type_fact_has_exact_package(fact: &TypeFact, package: &str) -> bool {
423 if type_has_exact_package(&fact.ty, package) {
424 return true;
425 }
426
427 matches!(
428 (&fact.ty, &fact.shape),
429 (PerlType::Any, Some(ShapeFact::Object(shape))) if shape.package == package
430 )
431}
432
433fn type_has_exact_package(ty: &PerlType, package: &str) -> bool {
434 match ty {
435 PerlType::Object(candidate) => candidate == package,
436 PerlType::Reference(inner) => type_has_exact_package(inner, package),
437 PerlType::Union(types) => {
438 !types.is_empty() && types.iter().all(|ty| type_has_exact_package(ty, package))
439 }
440 _ => false,
441 }
442}
443
444fn variable_identity(node: &Node) -> Option<(&str, &str)> {
445 match &node.kind {
446 NodeKind::Variable { sigil, name } => Some((sigil.as_str(), name.as_str())),
447 NodeKind::VariableWithAttributes { variable, .. } => variable_identity(variable),
448 _ => None,
449 }
450}
451
452fn receiver_base_label(node: &Node) -> String {
453 match variable_identity(node) {
454 Some((sigil, name)) => format!("{sigil}{name}"),
455 None => node.kind.kind_name().to_string(),
456 }
457}
458
459fn static_slot_key(node: &Node) -> Option<String> {
460 match &node.kind {
461 NodeKind::String { value, .. } => Some(normalize_literal(value)),
462 NodeKind::Identifier { name } => Some(name.clone()),
463 NodeKind::Number { value } => Some(value.clone()),
464 _ => None,
465 }
466}
467
468fn static_array_index(node: &Node) -> Option<usize> {
469 match &node.kind {
470 NodeKind::Number { value } => value.parse().ok(),
471 _ => None,
472 }
473}
474
475fn receiver_text<'a>(receiver: &Node, source: Option<&'a str>) -> Option<&'a str> {
476 source?.get(receiver.location.start..receiver.location.end)
477}
478
479fn package_from_type_fact(fact: &TypeFact) -> Option<String> {
480 package_from_type(&fact.ty).or_else(|| match &fact.shape {
481 Some(ShapeFact::Object(shape)) => Some(shape.package.clone()),
482 _ => None,
483 })
484}
485
486fn package_from_type(ty: &PerlType) -> Option<String> {
487 match ty {
488 PerlType::Object(package) => Some(package.clone()),
489 PerlType::Reference(inner) => package_from_type(inner),
490 PerlType::Union(types) => types.iter().find_map(package_from_type),
491 _ => None,
492 }
493}
494
495fn normalize_package_string(value: &str) -> Option<String> {
496 let normalized = normalize_literal(value);
497 if normalized.is_empty() { None } else { Some(normalized) }
498}
499
500fn normalize_literal(value: &str) -> String {
501 value.trim().trim_matches('\'').trim_matches('"').trim().to_string()
502}
503
504fn is_self_like_name(name: &str) -> bool {
505 matches!(name, "self" | "this")
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use crate::Parser;
512 use std::collections::BTreeMap;
513
514 fn parse_ast(code: &str) -> Result<Node, String> {
515 let mut parser = Parser::new(code);
516 parser.parse().map_err(|err| format!("parse failed: {err:?}"))
517 }
518
519 fn method_call_named<'a>(node: &'a Node, name: &str) -> Option<&'a Node> {
520 if let NodeKind::MethodCall { method, .. } = &node.kind {
521 if method == name {
522 return Some(node);
523 }
524 }
525
526 match &node.kind {
527 NodeKind::Program { statements } => {
528 statements.iter().find_map(|child| method_call_named(child, name))
529 }
530 NodeKind::ExpressionStatement { expression } => method_call_named(expression, name),
531 NodeKind::VariableDeclaration { initializer, .. } => {
532 initializer.as_deref().and_then(|child| method_call_named(child, name))
533 }
534 NodeKind::Assignment { lhs, rhs, .. } => {
535 method_call_named(lhs, name).or_else(|| method_call_named(rhs, name))
536 }
537 NodeKind::MethodCall { object, args, .. } => method_call_named(object, name)
538 .or_else(|| args.iter().find_map(|child| method_call_named(child, name))),
539 NodeKind::Binary { left, right, .. } => {
540 method_call_named(left, name).or_else(|| method_call_named(right, name))
541 }
542 _ => None,
543 }
544 }
545
546 fn object_fact(package: &str, confidence: Confidence) -> TypeFact {
547 TypeFact {
548 ty: PerlType::Object(package.to_string()),
549 confidence,
550 evidence: vec![TypeEvidence::WorkspaceSymbol { package: package.to_string() }],
551 dynamic_boundary: None,
552 shape: Some(ShapeFact::Object(super::super::type_facts::ObjectShape::new(
553 package.to_string(),
554 BTreeMap::new(),
555 ))),
556 }
557 }
558
559 fn hash_shape_fact(slot: &str, package: &str) -> TypeFact {
560 let mut slots = BTreeMap::new();
561 slots.insert(slot.to_string(), object_fact(package, Confidence::High));
562 TypeFact {
563 ty: PerlType::Hash { key: Box::new(PerlType::Any), value: Box::new(PerlType::Any) },
564 confidence: Confidence::High,
565 evidence: vec![TypeEvidence::Literal],
566 dynamic_boundary: None,
567 shape: Some(ShapeFact::Hash(super::super::type_facts::HashShape::new(slots, None))),
568 }
569 }
570
571 fn object_field_shape_fact(field: &str, field_package: &str) -> TypeFact {
572 let mut fields = BTreeMap::new();
573 fields.insert(field.to_string(), object_fact(field_package, Confidence::Medium));
574 TypeFact {
575 ty: PerlType::Object("My::Controller".to_string()),
576 confidence: Confidence::Medium,
577 evidence: vec![TypeEvidence::BlessLiteral { package: "My::Controller".to_string() }],
578 dynamic_boundary: None,
579 shape: Some(ShapeFact::Object(super::super::type_facts::ObjectShape::new(
580 "My::Controller".to_string(),
581 fields,
582 ))),
583 }
584 }
585
586 fn array_shape_fact(index: usize, package: &str) -> TypeFact {
587 let mut indexed = BTreeMap::new();
588 indexed.insert(index, object_fact(package, Confidence::High));
589 TypeFact {
590 ty: PerlType::Array(Box::new(PerlType::Any)),
591 confidence: Confidence::High,
592 evidence: vec![TypeEvidence::Literal],
593 dynamic_boundary: None,
594 shape: Some(ShapeFact::Array(super::super::type_facts::ArrayShape::new(indexed, None))),
595 }
596 }
597
598 fn union_object_fact(first: &str, second: &str) -> TypeFact {
599 TypeFact {
600 ty: PerlType::Union(vec![
601 PerlType::Object(first.to_string()),
602 PerlType::Object(second.to_string()),
603 ]),
604 confidence: Confidence::High,
605 evidence: vec![TypeEvidence::WorkspaceSymbol { package: first.to_string() }],
606 dynamic_boundary: None,
607 shape: None,
608 }
609 }
610
611 fn receiver_fact_for(
612 code: &str,
613 method: &str,
614 env: &TypeEnvironment,
615 ) -> Result<ReceiverFact, String> {
616 let ast = parse_ast(code)?;
617 let call = method_call_named(&ast, method).ok_or("expected method call")?;
618 Ok(receiver_fact_for_method_call(
619 call,
620 ReceiverFactContext::new(Some(env)).with_source(code),
621 ))
622 }
623
624 #[test]
625 fn static_constructor_receiver_records_package() -> Result<(), String> {
626 let env = TypeEnvironment::new();
627 let fact = receiver_fact_for("Foo::Bar->new();", "new", &env)?;
628
629 assert_eq!(fact.kind, ReceiverKind::StaticPackage);
630 assert_eq!(fact.package.as_deref(), Some("Foo::Bar"));
631 assert_eq!(fact.confidence, Confidence::High);
632 assert_eq!(fact.freshness, ReceiverFactFreshness::Fresh);
633 assert_eq!(fact.fallback_state, ReceiverFallbackState::Exact);
634 assert!(matches!(
635 fact.evidence.first(),
636 Some(TypeEvidence::ConstructorCall { package }) if package == "Foo::Bar"
637 ));
638 Ok(())
639 }
640
641 #[test]
642 fn self_receiver_uses_type_environment_fact() -> Result<(), String> {
643 let mut env = TypeEnvironment::new();
644 env.set_variable_fact("self".to_string(), object_fact("My::Controller", Confidence::High));
645
646 let fact = receiver_fact_for("$self->render();", "render", &env)?;
647
648 assert_eq!(fact.kind, ReceiverKind::SelfReceiver);
649 assert_eq!(fact.package.as_deref(), Some("My::Controller"));
650 assert_eq!(fact.confidence, Confidence::High);
651 assert!(matches!(fact.shape, Some(ShapeFact::Object(_))));
652 assert_eq!(fact.dynamic_boundary, None);
653 assert_eq!(fact.fallback_state, ReceiverFallbackState::Exact);
654 Ok(())
655 }
656
657 #[test]
658 fn object_receiver_uses_type_environment_fact() -> Result<(), String> {
659 let mut env = TypeEnvironment::new();
660 env.set_variable_fact("object".to_string(), object_fact("My::Service", Confidence::High));
661
662 let fact = receiver_fact_for("$object->run();", "run", &env)?;
663
664 assert_eq!(fact.kind, ReceiverKind::ObjectVariable);
665 assert_eq!(fact.package.as_deref(), Some("My::Service"));
666 assert_eq!(fact.freshness, ReceiverFactFreshness::Fresh);
667 assert_eq!(fact.fallback_state, ReceiverFallbackState::Exact);
668 Ok(())
669 }
670
671 #[test]
672 fn medium_confidence_object_receiver_preserves_fallback() -> Result<(), String> {
673 let mut env = TypeEnvironment::new();
674 env.set_variable_fact("object".to_string(), object_fact("My::Service", Confidence::Medium));
675
676 let fact = receiver_fact_for("$object->run();", "run", &env)?;
677
678 assert_eq!(fact.kind, ReceiverKind::ObjectVariable);
679 assert_eq!(fact.package.as_deref(), Some("My::Service"));
680 assert_eq!(fact.confidence, Confidence::Medium);
681 assert_eq!(fact.fallback_state, ReceiverFallbackState::Fallback);
682 Ok(())
683 }
684
685 #[test]
686 fn union_object_receiver_preserves_fallback() -> Result<(), String> {
687 let mut env = TypeEnvironment::new();
688 env.set_variable_fact("object".to_string(), union_object_fact("My::Service", "Other"));
689
690 let fact = receiver_fact_for("$object->run();", "run", &env)?;
691
692 assert_eq!(fact.kind, ReceiverKind::ObjectVariable);
693 assert_eq!(fact.package.as_deref(), Some("My::Service"));
694 assert_eq!(fact.confidence, Confidence::High);
695 assert_eq!(fact.fallback_state, ReceiverFallbackState::Fallback);
696 Ok(())
697 }
698
699 #[test]
700 fn hash_slot_receiver_uses_known_slot_fact() -> Result<(), String> {
701 let mut env = TypeEnvironment::new();
702 env.set_variable_fact("services".to_string(), hash_shape_fact("mailer", "My::Mailer"));
703
704 let fact = receiver_fact_for("$services{mailer}->send();", "send", &env)?;
705
706 assert_eq!(fact.kind, ReceiverKind::HashSlot);
707 assert_eq!(fact.package.as_deref(), Some("My::Mailer"));
708 assert_eq!(fact.fallback_state, ReceiverFallbackState::Exact);
709 assert!(fact.evidence.iter().any(|evidence| {
710 matches!(evidence, TypeEvidence::HashSlot { hash, key } if hash == "$services" && key == "mailer")
711 }));
712 Ok(())
713 }
714
715 #[test]
716 fn hashref_slot_receiver_preserves_hashref_kind() -> Result<(), String> {
717 let mut env = TypeEnvironment::new();
718 env.set_variable_fact("services".to_string(), hash_shape_fact("mailer", "My::Mailer"));
719
720 let fact = receiver_fact_for("$services->{mailer}->send();", "send", &env)?;
721
722 assert_eq!(fact.kind, ReceiverKind::HashRefSlot);
723 assert_eq!(fact.package.as_deref(), Some("My::Mailer"));
724 assert_eq!(fact.fallback_state, ReceiverFallbackState::Exact);
725 assert!(fact.evidence.iter().any(|evidence| {
726 matches!(evidence, TypeEvidence::HashRefSlot { base, key } if base == "$services" && key == "mailer")
727 }));
728 Ok(())
729 }
730
731 #[test]
732 fn object_field_receiver_preserves_fallback() -> Result<(), String> {
733 let mut env = TypeEnvironment::new();
734 env.set_variable_fact("self".to_string(), object_field_shape_fact("db", "My::DB"));
735
736 let fact = receiver_fact_for("$self->{db}->connect();", "connect", &env)?;
737
738 assert_eq!(fact.kind, ReceiverKind::HashRefSlot);
739 assert_eq!(fact.package.as_deref(), Some("My::DB"));
740 assert_eq!(fact.confidence, Confidence::Medium);
741 assert_eq!(fact.fallback_state, ReceiverFallbackState::Fallback);
742 assert!(fact.evidence.iter().any(|evidence| {
743 matches!(evidence, TypeEvidence::HashRefSlot { base, key } if base == "$self" && key == "db")
744 }));
745 Ok(())
746 }
747
748 #[test]
749 fn dynamic_hash_key_marks_dynamic_boundary() -> Result<(), String> {
750 let mut env = TypeEnvironment::new();
751 env.set_variable_fact("services".to_string(), hash_shape_fact("mailer", "My::Mailer"));
752
753 let fact = receiver_fact_for("$services{$name}->send();", "send", &env)?;
754
755 assert_eq!(fact.kind, ReceiverKind::DynamicKey);
756 assert_eq!(fact.confidence, Confidence::Low);
757 assert_eq!(fact.dynamic_boundary, Some(DynamicBoundary::DynamicHashKey));
758 assert_eq!(fact.package, None);
759 assert_eq!(fact.fallback_state, ReceiverFallbackState::Fallback);
760 Ok(())
761 }
762
763 #[test]
764 fn array_index_receiver_uses_known_index_fact() -> Result<(), String> {
765 let mut env = TypeEnvironment::new();
766 env.set_variable_fact("items".to_string(), array_shape_fact(0, "My::Item"));
767
768 let fact = receiver_fact_for("$items[0]->render();", "render", &env)?;
769
770 assert_eq!(fact.kind, ReceiverKind::ArrayIndex);
771 assert_eq!(fact.package.as_deref(), Some("My::Item"));
772 assert_eq!(fact.confidence, Confidence::High);
773 assert_eq!(fact.fallback_state, ReceiverFallbackState::Exact);
774 Ok(())
775 }
776
777 #[test]
778 fn dynamic_array_index_stays_low_confidence() -> Result<(), String> {
779 let mut env = TypeEnvironment::new();
780 env.set_variable_fact("items".to_string(), array_shape_fact(0, "My::Item"));
781
782 let fact = receiver_fact_for("$items[$i]->render();", "render", &env)?;
783
784 assert_eq!(fact.kind, ReceiverKind::ArrayIndex);
785 assert_eq!(fact.package, None);
786 assert_eq!(fact.confidence, Confidence::Low);
787 assert_eq!(fact.dynamic_boundary, Some(DynamicBoundary::UnknownReceiver));
788 assert_eq!(fact.fallback_state, ReceiverFallbackState::Fallback);
789 Ok(())
790 }
791
792 #[test]
793 fn unknown_receiver_stays_low_confidence() -> Result<(), String> {
794 let env = TypeEnvironment::new();
795
796 let fact = receiver_fact_for("$unknown->run();", "run", &env)?;
797
798 assert_eq!(fact.kind, ReceiverKind::Unknown);
799 assert_eq!(fact.confidence, Confidence::Low);
800 assert_eq!(fact.dynamic_boundary, Some(DynamicBoundary::UnknownReceiver));
801 assert_eq!(fact.package, None);
802 assert_eq!(fact.fallback_state, ReceiverFallbackState::Fallback);
803 Ok(())
804 }
805}