1use std::sync::Arc;
2
3use php_ast::ast::{Expr, ExprKind};
4use php_ast::Span;
5
6use mir_codebase::storage::{FnParam, MethodStorage, Visibility};
7use mir_issues::{IssueKind, Severity};
8use mir_types::{Atomic, Union};
9
10use crate::expr::ExpressionAnalyzer;
11
12pub struct CheckArgsParams<'a> {
17 pub fn_name: &'a str,
18 pub params: &'a [FnParam],
19 pub arg_types: &'a [Union],
20 pub arg_spans: &'a [Span],
21 pub arg_names: &'a [Option<String>],
22 pub arg_can_be_byref: &'a [bool],
23 pub call_span: Span,
24 pub has_spread: bool,
25}
26
27pub fn check_constructor_args(
28 ea: &mut ExpressionAnalyzer<'_>,
29 class_name: &str,
30 p: CheckArgsParams<'_>,
31) {
32 let ctor_name = format!("{class_name}::__construct");
33 check_args(
34 ea,
35 CheckArgsParams {
36 fn_name: &ctor_name,
37 ..p
38 },
39 );
40}
41
42pub fn spread_element_type(arr_ty: &Union) -> Union {
45 let mut result = Union::empty();
46 for atomic in arr_ty.types.iter() {
47 match atomic {
48 Atomic::TArray { value, .. }
49 | Atomic::TNonEmptyArray { value, .. }
50 | Atomic::TList { value }
51 | Atomic::TNonEmptyList { value } => {
52 for t in value.types.iter() {
53 result.add_type(t.clone());
54 }
55 }
56 Atomic::TKeyedArray { properties, .. } => {
57 for (_key, prop) in properties.iter() {
58 for t in prop.ty.types.iter() {
59 result.add_type(t.clone());
60 }
61 }
62 }
63 _ => return Union::mixed(),
64 }
65 }
66 if result.types.is_empty() {
67 Union::mixed()
68 } else {
69 result
70 }
71}
72
73pub(crate) fn substitute_static_in_return(ret: Union, receiver_fqcn: &Arc<str>) -> Union {
75 let from_docblock = ret.from_docblock;
76 let types: Vec<Atomic> = ret
77 .types
78 .into_iter()
79 .map(|a| match a {
80 Atomic::TStaticObject { .. } | Atomic::TSelf { .. } => Atomic::TNamedObject {
81 fqcn: receiver_fqcn.clone(),
82 type_params: vec![],
83 },
84 other => other,
85 })
86 .collect();
87 let mut result = Union::from_vec(types);
88 result.from_docblock = from_docblock;
89 result
90}
91
92pub(crate) fn check_method_visibility(
93 ea: &mut ExpressionAnalyzer<'_>,
94 method: &MethodStorage,
95 ctx: &crate::context::Context,
96 span: Span,
97) {
98 match method.visibility {
99 Visibility::Private => {
100 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
101 let from_trait = ea.codebase.traits.contains_key(method.fqcn.as_ref());
102 let allowed = caller_fqcn == method.fqcn.as_ref()
103 || (from_trait
104 && ea
105 .codebase
106 .extends_or_implements(caller_fqcn, method.fqcn.as_ref()));
107 if !allowed {
108 ea.emit(
109 IssueKind::UndefinedMethod {
110 class: method.fqcn.to_string(),
111 method: method.name.to_string(),
112 },
113 Severity::Error,
114 span,
115 );
116 }
117 }
118 Visibility::Protected => {
119 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
120 if caller_fqcn.is_empty() {
121 ea.emit(
122 IssueKind::UndefinedMethod {
123 class: method.fqcn.to_string(),
124 method: method.name.to_string(),
125 },
126 Severity::Error,
127 span,
128 );
129 } else {
130 let allowed = caller_fqcn == method.fqcn.as_ref()
131 || ea
132 .codebase
133 .extends_or_implements(caller_fqcn, method.fqcn.as_ref());
134 if !allowed {
135 ea.emit(
136 IssueKind::UndefinedMethod {
137 class: method.fqcn.to_string(),
138 method: method.name.to_string(),
139 },
140 Severity::Error,
141 span,
142 );
143 }
144 }
145 }
146 Visibility::Public => {}
147 }
148}
149
150pub(crate) fn expr_can_be_passed_by_reference(expr: &Expr<'_, '_>) -> bool {
151 matches!(
152 expr.kind,
153 ExprKind::Variable(_)
154 | ExprKind::ArrayAccess(_)
155 | ExprKind::PropertyAccess(_)
156 | ExprKind::NullsafePropertyAccess(_)
157 | ExprKind::StaticPropertyAccess(_)
158 | ExprKind::StaticPropertyAccessDynamic { .. }
159 )
160}
161
162pub(crate) fn check_args(ea: &mut ExpressionAnalyzer<'_>, p: CheckArgsParams<'_>) {
167 let CheckArgsParams {
168 fn_name,
169 params,
170 arg_types,
171 arg_spans,
172 arg_names,
173 arg_can_be_byref,
174 call_span,
175 has_spread,
176 } = p;
177
178 let variadic_index = params.iter().position(|p| p.is_variadic);
179 let max_positional = variadic_index.unwrap_or(params.len());
180 let mut param_to_arg: Vec<Option<(Union, Span, usize)>> = vec![None; params.len()];
181 let mut arg_bindings: Vec<(usize, Union, Span, usize)> = Vec::new();
182 let mut positional = 0usize;
183 let mut seen_named = false;
184 let mut has_shape_error = false;
185
186 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
187 if has_spread && i > 0 {
188 break;
189 }
190
191 if let Some(Some(name)) = arg_names.get(i) {
192 seen_named = true;
193 if let Some(pi) = params.iter().position(|p| p.name.as_ref() == name.as_str()) {
194 if param_to_arg[pi].is_some() {
195 has_shape_error = true;
196 ea.emit(
197 IssueKind::InvalidNamedArgument {
198 fn_name: fn_name.to_string(),
199 name: name.to_string(),
200 },
201 Severity::Error,
202 *span,
203 );
204 continue;
205 }
206 param_to_arg[pi] = Some((ty.clone(), *span, i));
207 arg_bindings.push((pi, ty.clone(), *span, i));
208 } else if let Some(vi) = variadic_index {
209 arg_bindings.push((vi, ty.clone(), *span, i));
210 } else {
211 has_shape_error = true;
212 ea.emit(
213 IssueKind::InvalidNamedArgument {
214 fn_name: fn_name.to_string(),
215 name: name.to_string(),
216 },
217 Severity::Error,
218 *span,
219 );
220 }
221 continue;
222 }
223
224 if seen_named && !has_spread {
225 has_shape_error = true;
226 ea.emit(
227 IssueKind::InvalidNamedArgument {
228 fn_name: fn_name.to_string(),
229 name: format!("#{}", i + 1),
230 },
231 Severity::Error,
232 *span,
233 );
234 continue;
235 }
236
237 while positional < max_positional && param_to_arg[positional].is_some() {
238 positional += 1;
239 }
240
241 let Some(pi) = (if positional < max_positional {
242 Some(positional)
243 } else {
244 variadic_index
245 }) else {
246 continue;
247 };
248
249 if pi < max_positional {
250 param_to_arg[pi] = Some((ty.clone(), *span, i));
251 positional += 1;
252 }
253 arg_bindings.push((pi, ty.clone(), *span, i));
254 }
255
256 let required_count = params
257 .iter()
258 .filter(|p| !p.is_optional && !p.is_variadic)
259 .count();
260 let provided_count = param_to_arg
261 .iter()
262 .take(required_count)
263 .filter(|slot| slot.is_some())
264 .count();
265
266 if provided_count < required_count && !has_spread && !has_shape_error {
267 ea.emit(
268 IssueKind::TooFewArguments {
269 fn_name: fn_name.to_string(),
270 expected: required_count,
271 actual: arg_types.len(),
272 },
273 Severity::Error,
274 call_span,
275 );
276 }
277
278 if variadic_index.is_none() && arg_types.len() > params.len() && !has_spread && !has_shape_error
279 {
280 ea.emit(
281 IssueKind::TooManyArguments {
282 fn_name: fn_name.to_string(),
283 expected: params.len(),
284 actual: arg_types.len(),
285 },
286 Severity::Error,
287 arg_spans.get(params.len()).copied().unwrap_or(call_span),
288 );
289 }
290
291 for (param_idx, arg_ty, arg_span, arg_idx) in arg_bindings {
292 let param = ¶ms[param_idx];
293
294 if param.is_byref && !arg_can_be_byref.get(arg_idx).copied().unwrap_or(false) {
295 ea.emit(
296 IssueKind::InvalidPassByReference {
297 fn_name: fn_name.to_string(),
298 param: param.name.to_string(),
299 },
300 Severity::Error,
301 arg_span,
302 );
303 }
304
305 if let Some(raw_param_ty) = ¶m.ty {
306 let param_ty_owned;
307 let param_ty: &Union = if param.is_variadic {
308 if let Some(elem_ty) = raw_param_ty.types.iter().find_map(|a| match a {
309 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
310 Some(*value.clone())
311 }
312 _ => None,
313 }) {
314 param_ty_owned = elem_ty;
315 ¶m_ty_owned
316 } else {
317 raw_param_ty
318 }
319 } else {
320 raw_param_ty
321 };
322
323 if !param_ty.is_nullable()
324 && !param_ty.is_mixed()
325 && arg_ty.is_single()
326 && arg_ty.contains(|t| matches!(t, Atomic::TNull))
327 {
328 ea.emit(
329 IssueKind::InvalidArgument {
330 param: param.name.to_string(),
331 fn_name: fn_name.to_string(),
332 expected: format!("{param_ty}"),
333 actual: format!("{arg_ty}"),
334 },
335 Severity::Error,
336 arg_span,
337 );
338 } else if !param_ty.is_nullable() && !param_ty.is_mixed() && arg_ty.is_nullable() {
339 ea.emit(
340 IssueKind::PossiblyNullArgument {
341 param: param.name.to_string(),
342 fn_name: fn_name.to_string(),
343 },
344 Severity::Info,
345 arg_span,
346 );
347 }
348
349 if !arg_ty.is_subtype_of_simple(param_ty)
350 && !param_ty.is_mixed()
351 && !arg_ty.is_mixed()
352 && !named_object_subtype(&arg_ty, param_ty, ea)
353 && !param_contains_template_or_unknown(param_ty, ea)
354 && !param_contains_template_or_unknown(&arg_ty, ea)
355 && !array_list_compatible(&arg_ty, param_ty, ea)
356 && !(arg_ty.is_single() && param_ty.is_subtype_of_simple(&arg_ty))
357 && !(arg_ty.is_single() && param_ty.remove_null().is_subtype_of_simple(&arg_ty))
358 && !(arg_ty.is_single()
359 && param_ty
360 .types
361 .iter()
362 .any(|p| Union::single(p.clone()).is_subtype_of_simple(&arg_ty)))
363 && !arg_ty.remove_null().is_subtype_of_simple(param_ty)
364 && !arg_ty.remove_false().is_subtype_of_simple(param_ty)
365 && !named_object_subtype(&arg_ty.remove_null(), param_ty, ea)
366 && !named_object_subtype(&arg_ty.remove_false(), param_ty, ea)
367 {
368 ea.emit(
369 IssueKind::InvalidArgument {
370 param: param.name.to_string(),
371 fn_name: fn_name.to_string(),
372 expected: format!("{param_ty}"),
373 actual: invalid_argument_actual_type(&arg_ty, param_ty, ea),
374 },
375 Severity::Error,
376 arg_span,
377 );
378 }
379 }
380 }
381}
382
383fn invalid_argument_actual_type(
388 arg_ty: &Union,
389 param_ty: &Union,
390 ea: &ExpressionAnalyzer<'_>,
391) -> String {
392 if let Some(projected) = project_generic_ancestor_type(arg_ty, param_ty, ea) {
393 return format!("{projected}");
394 }
395 format!("{arg_ty}")
396}
397
398fn project_generic_ancestor_type(
399 arg_ty: &Union,
400 param_ty: &Union,
401 ea: &ExpressionAnalyzer<'_>,
402) -> Option<Union> {
403 if !arg_ty.is_single() {
404 return None;
405 }
406 let arg_fqcn = match arg_ty.types.first()? {
407 Atomic::TNamedObject { fqcn, type_params } => {
408 if !type_params.is_empty() {
409 return None;
410 }
411 fqcn
412 }
413 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => fqcn,
414 _ => return None,
415 };
416 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
417
418 for param_atomic in ¶m_ty.types {
419 let (param_fqcn, param_type_params) = match param_atomic {
420 Atomic::TNamedObject { fqcn, type_params } => (fqcn, type_params),
421 _ => continue,
422 };
423 if param_type_params.is_empty() {
424 continue;
425 }
426
427 let resolved_param = ea
428 .codebase
429 .resolve_class_name(&ea.file, param_fqcn.as_ref());
430 let ancestor_args = generic_ancestor_type_args(arg_fqcn.as_ref(), &resolved_param, ea)
431 .or_else(|| generic_ancestor_type_args(&resolved_arg, &resolved_param, ea))
432 .or_else(|| generic_ancestor_type_args(arg_fqcn.as_ref(), param_fqcn.as_ref(), ea))
433 .or_else(|| generic_ancestor_type_args(&resolved_arg, param_fqcn.as_ref(), ea))?;
434 if ancestor_args.is_empty() {
435 continue;
436 }
437
438 return Some(Union::single(Atomic::TNamedObject {
439 fqcn: param_fqcn.clone(),
440 type_params: ancestor_args,
441 }));
442 }
443
444 None
445}
446
447fn named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
450 arg.types.iter().all(|a_atomic| {
451 let arg_fqcn: &Arc<str> = match a_atomic {
452 Atomic::TNamedObject { fqcn, .. } => fqcn,
453 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => {
454 if ea.codebase.traits.contains_key(fqcn.as_ref()) {
455 return true;
456 }
457 fqcn
458 }
459 Atomic::TParent { fqcn } => fqcn,
460 Atomic::TNever => return true,
461 Atomic::TClosure { .. } => {
462 return param.types.iter().any(|p| match p {
463 Atomic::TClosure { .. } | Atomic::TCallable { .. } => true,
464 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
465 _ => false,
466 });
467 }
468 Atomic::TCallable { .. } => {
469 return param.types.iter().any(|p| match p {
470 Atomic::TCallable { .. } | Atomic::TClosure { .. } => true,
471 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
472 _ => false,
473 });
474 }
475 Atomic::TClassString(Some(arg_cls)) => {
476 return param.types.iter().any(|p| match p {
477 Atomic::TClassString(None) | Atomic::TString => true,
478 Atomic::TClassString(Some(param_cls)) => {
479 arg_cls == param_cls
480 || ea
481 .codebase
482 .extends_or_implements(arg_cls.as_ref(), param_cls.as_ref())
483 }
484 _ => false,
485 });
486 }
487 Atomic::TNull => {
488 return param.types.iter().any(|p| matches!(p, Atomic::TNull));
489 }
490 Atomic::TFalse => {
491 return param
492 .types
493 .iter()
494 .any(|p| matches!(p, Atomic::TFalse | Atomic::TBool));
495 }
496 _ => return false,
497 };
498
499 if param
500 .types
501 .iter()
502 .any(|p| matches!(p, Atomic::TCallable { .. }))
503 {
504 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
505 if ea.codebase.get_method(&resolved_arg, "__invoke").is_some()
506 || ea
507 .codebase
508 .get_method(arg_fqcn.as_ref(), "__invoke")
509 .is_some()
510 {
511 return true;
512 }
513 }
514
515 param.types.iter().any(|p_atomic| {
516 let param_fqcn: &Arc<str> = match p_atomic {
517 Atomic::TNamedObject { fqcn, .. } => fqcn,
518 Atomic::TSelf { fqcn } => fqcn,
519 Atomic::TStaticObject { fqcn } => fqcn,
520 Atomic::TParent { fqcn } => fqcn,
521 _ => return false,
522 };
523 let resolved_param = ea
524 .codebase
525 .resolve_class_name(&ea.file, param_fqcn.as_ref());
526 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
527
528 let is_same_class = resolved_param == resolved_arg
529 || arg_fqcn.as_ref() == resolved_param.as_str()
530 || resolved_arg == param_fqcn.as_ref();
531
532 if is_same_class {
533 let arg_type_params = match a_atomic {
534 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
535 _ => &[],
536 };
537 let param_type_params = match p_atomic {
538 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
539 _ => &[],
540 };
541 if !arg_type_params.is_empty() || !param_type_params.is_empty() {
542 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
543 return generic_type_params_compatible(
544 arg_type_params,
545 param_type_params,
546 &class_tps,
547 ea,
548 );
549 }
550 return true;
551 }
552
553 let arg_extends_param = ea
554 .codebase
555 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
556 || ea
557 .codebase
558 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
559 || ea
560 .codebase
561 .extends_or_implements(&resolved_arg, &resolved_param);
562
563 if arg_extends_param {
564 let param_type_params = match p_atomic {
565 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
566 _ => &[],
567 };
568 if !param_type_params.is_empty() {
569 let ancestor_args =
570 generic_ancestor_type_args(arg_fqcn.as_ref(), &resolved_param, ea)
571 .or_else(|| {
572 generic_ancestor_type_args(&resolved_arg, &resolved_param, ea)
573 })
574 .or_else(|| {
575 generic_ancestor_type_args(
576 arg_fqcn.as_ref(),
577 param_fqcn.as_ref(),
578 ea,
579 )
580 })
581 .or_else(|| {
582 generic_ancestor_type_args(&resolved_arg, param_fqcn.as_ref(), ea)
583 });
584 if let Some(arg_as_param_params) = ancestor_args {
585 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
586 return generic_type_params_compatible(
587 &arg_as_param_params,
588 param_type_params,
589 &class_tps,
590 ea,
591 );
592 }
593 }
594 return true;
595 }
596
597 if ea
598 .codebase
599 .extends_or_implements(param_fqcn.as_ref(), &resolved_arg)
600 || ea
601 .codebase
602 .extends_or_implements(param_fqcn.as_ref(), arg_fqcn.as_ref())
603 || ea
604 .codebase
605 .extends_or_implements(&resolved_param, &resolved_arg)
606 {
607 let param_type_params = match p_atomic {
608 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
609 _ => &[],
610 };
611 if param_type_params.is_empty() {
612 return true;
613 }
614 }
615
616 if !arg_fqcn.contains('\\') && !ea.codebase.type_exists(&resolved_arg) {
617 for entry in ea.codebase.classes.iter() {
618 if entry.value().short_name.as_ref() == arg_fqcn.as_ref() {
619 let actual_fqcn = entry.key().clone();
620 if ea
621 .codebase
622 .extends_or_implements(actual_fqcn.as_ref(), &resolved_param)
623 || ea
624 .codebase
625 .extends_or_implements(actual_fqcn.as_ref(), param_fqcn.as_ref())
626 {
627 return true;
628 }
629 }
630 }
631 }
632
633 let iface_key = if ea.codebase.interfaces.contains_key(arg_fqcn.as_ref()) {
634 Some(arg_fqcn.as_ref())
635 } else if ea.codebase.interfaces.contains_key(resolved_arg.as_str()) {
636 Some(resolved_arg.as_str())
637 } else {
638 None
639 };
640 if let Some(iface_fqcn) = iface_key {
641 let compatible = ea.codebase.classes.iter().any(|entry| {
642 let cls = entry.value();
643 cls.all_parents.iter().any(|p| p.as_ref() == iface_fqcn)
644 && (ea
645 .codebase
646 .extends_or_implements(entry.key().as_ref(), param_fqcn.as_ref())
647 || ea
648 .codebase
649 .extends_or_implements(entry.key().as_ref(), &resolved_param))
650 });
651 if compatible {
652 return true;
653 }
654 }
655
656 if arg_fqcn.contains('\\')
657 && !ea.codebase.type_exists(arg_fqcn.as_ref())
658 && !ea.codebase.type_exists(&resolved_arg)
659 {
660 return true;
661 }
662
663 if param_fqcn.contains('\\')
664 && !ea.codebase.type_exists(param_fqcn.as_ref())
665 && !ea.codebase.type_exists(&resolved_param)
666 {
667 return true;
668 }
669
670 false
671 })
672 })
673}
674
675fn strict_named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
677 arg.types.iter().all(|a_atomic| {
678 let arg_fqcn: &Arc<str> = match a_atomic {
679 Atomic::TNamedObject { fqcn, .. } => fqcn,
680 Atomic::TNever => return true,
681 _ => return false,
682 };
683 param.types.iter().any(|p_atomic| {
684 let param_fqcn: &Arc<str> = match p_atomic {
685 Atomic::TNamedObject { fqcn, .. } => fqcn,
686 _ => return false,
687 };
688 let resolved_param = ea
689 .codebase
690 .resolve_class_name(&ea.file, param_fqcn.as_ref());
691 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
692 resolved_param == resolved_arg
693 || arg_fqcn.as_ref() == resolved_param.as_str()
694 || resolved_arg == param_fqcn.as_ref()
695 || ea
696 .codebase
697 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
698 || ea
699 .codebase
700 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
701 || ea
702 .codebase
703 .extends_or_implements(&resolved_arg, &resolved_param)
704 })
705 })
706}
707
708fn generic_type_params_compatible(
710 arg_params: &[Union],
711 param_params: &[Union],
712 template_params: &[mir_codebase::storage::TemplateParam],
713 ea: &ExpressionAnalyzer<'_>,
714) -> bool {
715 if arg_params.len() != param_params.len() {
716 return true;
717 }
718 if arg_params.is_empty() {
719 return true;
720 }
721
722 for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
723 let variance = template_params
724 .get(i)
725 .map(|tp| tp.variance)
726 .unwrap_or(mir_types::Variance::Invariant);
727
728 let compatible = match variance {
729 mir_types::Variance::Covariant => {
730 arg_p.is_subtype_of_simple(param_p)
731 || param_p.is_mixed()
732 || arg_p.is_mixed()
733 || strict_named_object_subtype(arg_p, param_p, ea)
734 }
735 mir_types::Variance::Contravariant => {
736 param_p.is_subtype_of_simple(arg_p)
737 || arg_p.is_mixed()
738 || param_p.is_mixed()
739 || strict_named_object_subtype(param_p, arg_p, ea)
740 }
741 mir_types::Variance::Invariant => {
742 arg_p == param_p
743 || arg_p.is_mixed()
744 || param_p.is_mixed()
745 || (arg_p.is_subtype_of_simple(param_p) && param_p.is_subtype_of_simple(arg_p))
746 }
747 };
748
749 if !compatible {
750 return false;
751 }
752 }
753
754 true
755}
756
757fn generic_ancestor_type_args(
758 child: &str,
759 ancestor: &str,
760 ea: &ExpressionAnalyzer<'_>,
761) -> Option<Vec<Union>> {
762 let mut seen = std::collections::HashSet::new();
763 generic_ancestor_type_args_inner(child, ancestor, ea, &mut seen)
764}
765
766fn generic_ancestor_type_args_inner(
767 child: &str,
768 ancestor: &str,
769 ea: &ExpressionAnalyzer<'_>,
770 seen: &mut std::collections::HashSet<String>,
771) -> Option<Vec<Union>> {
772 if child == ancestor {
773 return Some(vec![]);
774 }
775 if !seen.insert(child.to_string()) {
776 return None;
777 }
778
779 let cls = ea.codebase.classes.get(child)?;
780 let parent = cls.parent.clone();
781 let extends_type_args = cls.extends_type_args.clone();
782 let implements_type_args = cls.implements_type_args.clone();
783 drop(cls);
784
785 for (iface, args) in implements_type_args {
786 if iface.as_ref() == ancestor {
787 return Some(args);
788 }
789 }
790
791 let parent = parent?;
792 if parent.as_ref() == ancestor {
793 return Some(extends_type_args);
794 }
795
796 let parent_args = generic_ancestor_type_args_inner(parent.as_ref(), ancestor, ea, seen)?;
797 if parent_args.is_empty() {
798 return Some(parent_args);
799 }
800
801 let parent_template_params = ea.codebase.get_class_template_params(parent.as_ref());
802 let bindings: std::collections::HashMap<Arc<str>, Union> = parent_template_params
803 .iter()
804 .zip(extends_type_args.iter())
805 .map(|(tp, ty)| (tp.name.clone(), ty.clone()))
806 .collect();
807
808 Some(
809 parent_args
810 .into_iter()
811 .map(|ty| ty.substitute_templates(&bindings))
812 .collect(),
813 )
814}
815
816fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
817 param_ty.types.iter().any(|atomic| match atomic {
818 Atomic::TTemplateParam { .. } => true,
819 Atomic::TNamedObject { fqcn, .. } => {
820 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
821 }
822 Atomic::TClassString(Some(inner)) => {
823 !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
824 }
825 Atomic::TArray { key: _, value }
826 | Atomic::TList { value }
827 | Atomic::TNonEmptyArray { key: _, value }
828 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
829 Atomic::TTemplateParam { .. } => true,
830 Atomic::TNamedObject { fqcn, .. } => {
831 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
832 }
833 _ => false,
834 }),
835 _ => false,
836 })
837}
838
839fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
840 arg_ty.types.iter().all(|av| {
841 let av_fqcn: &Arc<str> = match av {
842 Atomic::TNamedObject { fqcn, .. } => fqcn,
843 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
844 fqcn
845 }
846 Atomic::TArray { value, .. }
847 | Atomic::TNonEmptyArray { value, .. }
848 | Atomic::TList { value }
849 | Atomic::TNonEmptyList { value } => {
850 return param_ty.types.iter().any(|pv| {
851 let pv_val: &Union = match pv {
852 Atomic::TArray { value, .. }
853 | Atomic::TNonEmptyArray { value, .. }
854 | Atomic::TList { value }
855 | Atomic::TNonEmptyList { value } => value,
856 _ => return false,
857 };
858 union_compatible(value, pv_val, ea)
859 });
860 }
861 Atomic::TKeyedArray { .. } => return true,
862 _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
863 };
864
865 param_ty.types.iter().any(|pv| {
866 let pv_fqcn: &Arc<str> = match pv {
867 Atomic::TNamedObject { fqcn, .. } => fqcn,
868 Atomic::TSelf { fqcn }
869 | Atomic::TStaticObject { fqcn }
870 | Atomic::TParent { fqcn } => fqcn,
871 _ => return false,
872 };
873 if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
874 return true;
875 }
876 let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
877 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
878 resolved_param == resolved_arg
879 || ea
880 .codebase
881 .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
882 || ea
883 .codebase
884 .extends_or_implements(&resolved_arg, &resolved_param)
885 || ea
886 .codebase
887 .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
888 || ea
889 .codebase
890 .extends_or_implements(&resolved_param, &resolved_arg)
891 })
892 })
893}
894
895fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
896 arg_ty.types.iter().all(|a_atomic| {
897 let arg_value: &Union = match a_atomic {
898 Atomic::TArray { value, .. }
899 | Atomic::TNonEmptyArray { value, .. }
900 | Atomic::TList { value }
901 | Atomic::TNonEmptyList { value } => value,
902 Atomic::TKeyedArray { .. } => return true,
903 _ => return false,
904 };
905
906 param_ty.types.iter().any(|p_atomic| {
907 let param_value: &Union = match p_atomic {
908 Atomic::TArray { value, .. }
909 | Atomic::TNonEmptyArray { value, .. }
910 | Atomic::TList { value }
911 | Atomic::TNonEmptyList { value } => value,
912 _ => return false,
913 };
914
915 union_compatible(arg_value, param_value, ea)
916 })
917 })
918}