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::NullArgument {
330 param: param.name.to_string(),
331 fn_name: fn_name.to_string(),
332 },
333 Severity::Warning,
334 arg_span,
335 );
336 } else if !param_ty.is_nullable() && !param_ty.is_mixed() && arg_ty.is_nullable() {
337 ea.emit(
338 IssueKind::PossiblyNullArgument {
339 param: param.name.to_string(),
340 fn_name: fn_name.to_string(),
341 },
342 Severity::Info,
343 arg_span,
344 );
345 }
346
347 let param_accepts_false =
348 param_ty.contains(|t| matches!(t, Atomic::TFalse | Atomic::TBool));
349 if !param_accepts_false
350 && !param_ty.is_mixed()
351 && !arg_ty.is_mixed()
352 && !arg_ty.is_single()
353 && arg_ty.contains(|t| matches!(t, Atomic::TFalse | Atomic::TBool))
354 {
355 let arg_without_false = arg_ty.remove_false();
356 let arg_core = arg_without_false.remove_null();
358 if !arg_core.types.is_empty()
359 && (arg_without_false.is_subtype_of_simple(param_ty)
360 || arg_core.is_subtype_of_simple(param_ty)
361 || named_object_subtype(&arg_without_false, param_ty, ea)
362 || named_object_subtype(&arg_core, param_ty, ea))
363 {
364 ea.emit(
365 IssueKind::PossiblyInvalidArgument {
366 param: param.name.to_string(),
367 fn_name: fn_name.to_string(),
368 expected: format!("{param_ty}"),
369 actual: format!("{arg_ty}"),
370 },
371 Severity::Info,
372 arg_span,
373 );
374 }
375 }
376
377 let arg_core = arg_ty.remove_null().remove_false();
378 if !arg_ty.is_subtype_of_simple(param_ty)
379 && !param_ty.is_mixed()
380 && !arg_ty.is_mixed()
381 && !named_object_subtype(&arg_ty, param_ty, ea)
382 && !param_contains_template_or_unknown(param_ty, ea)
383 && !param_contains_template_or_unknown(&arg_ty, ea)
384 && !array_list_compatible(&arg_ty, param_ty, ea)
385 && !(arg_ty.is_single() && param_ty.is_subtype_of_simple(&arg_ty))
386 && !(arg_ty.is_single() && param_ty.remove_null().is_subtype_of_simple(&arg_ty))
387 && !(arg_ty.is_single()
388 && param_ty
389 .types
390 .iter()
391 .any(|p| Union::single(p.clone()).is_subtype_of_simple(&arg_ty)))
392 && !arg_ty.remove_null().is_subtype_of_simple(param_ty)
393 && (arg_ty.remove_false().types.is_empty()
394 || !arg_ty.remove_false().is_subtype_of_simple(param_ty))
395 && (arg_core.types.is_empty() || !arg_core.is_subtype_of_simple(param_ty))
396 && !named_object_subtype(&arg_ty.remove_null(), param_ty, ea)
397 && (arg_ty.remove_false().types.is_empty()
398 || !named_object_subtype(&arg_ty.remove_false(), param_ty, ea))
399 && (arg_core.types.is_empty() || !named_object_subtype(&arg_core, param_ty, ea))
400 {
401 ea.emit(
402 IssueKind::InvalidArgument {
403 param: param.name.to_string(),
404 fn_name: fn_name.to_string(),
405 expected: format!("{param_ty}"),
406 actual: invalid_argument_actual_type(&arg_ty, param_ty, ea),
407 },
408 Severity::Error,
409 arg_span,
410 );
411 }
412 }
413 }
414}
415
416fn invalid_argument_actual_type(
421 arg_ty: &Union,
422 param_ty: &Union,
423 ea: &ExpressionAnalyzer<'_>,
424) -> String {
425 if let Some(projected) = project_generic_ancestor_type(arg_ty, param_ty, ea) {
426 return format!("{projected}");
427 }
428 format!("{arg_ty}")
429}
430
431fn project_generic_ancestor_type(
432 arg_ty: &Union,
433 param_ty: &Union,
434 ea: &ExpressionAnalyzer<'_>,
435) -> Option<Union> {
436 if !arg_ty.is_single() {
437 return None;
438 }
439 let arg_fqcn = match arg_ty.types.first()? {
440 Atomic::TNamedObject { fqcn, type_params } => {
441 if !type_params.is_empty() {
442 return None;
443 }
444 fqcn
445 }
446 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => fqcn,
447 _ => return None,
448 };
449 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
450
451 for param_atomic in ¶m_ty.types {
452 let (param_fqcn, param_type_params) = match param_atomic {
453 Atomic::TNamedObject { fqcn, type_params } => (fqcn, type_params),
454 _ => continue,
455 };
456 if param_type_params.is_empty() {
457 continue;
458 }
459
460 let resolved_param = ea
461 .codebase
462 .resolve_class_name(&ea.file, param_fqcn.as_ref());
463 let ancestor_args = generic_ancestor_type_args(arg_fqcn.as_ref(), &resolved_param, ea)
464 .or_else(|| generic_ancestor_type_args(&resolved_arg, &resolved_param, ea))
465 .or_else(|| generic_ancestor_type_args(arg_fqcn.as_ref(), param_fqcn.as_ref(), ea))
466 .or_else(|| generic_ancestor_type_args(&resolved_arg, param_fqcn.as_ref(), ea))?;
467 if ancestor_args.is_empty() {
468 continue;
469 }
470
471 return Some(Union::single(Atomic::TNamedObject {
472 fqcn: param_fqcn.clone(),
473 type_params: ancestor_args,
474 }));
475 }
476
477 None
478}
479
480fn named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
483 arg.types.iter().all(|a_atomic| {
484 let arg_fqcn: &Arc<str> = match a_atomic {
485 Atomic::TNamedObject { fqcn, .. } => fqcn,
486 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => {
487 if ea.codebase.traits.contains_key(fqcn.as_ref()) {
488 return true;
489 }
490 fqcn
491 }
492 Atomic::TParent { fqcn } => fqcn,
493 Atomic::TNever => return true,
494 Atomic::TClosure { .. } => {
495 return param.types.iter().any(|p| match p {
496 Atomic::TClosure { .. } | Atomic::TCallable { .. } => true,
497 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
498 _ => false,
499 });
500 }
501 Atomic::TCallable { .. } => {
502 return param.types.iter().any(|p| match p {
503 Atomic::TCallable { .. } | Atomic::TClosure { .. } => true,
504 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
505 _ => false,
506 });
507 }
508 Atomic::TClassString(Some(arg_cls)) => {
509 return param.types.iter().any(|p| match p {
510 Atomic::TClassString(None) | Atomic::TString => true,
511 Atomic::TClassString(Some(param_cls)) => {
512 arg_cls == param_cls
513 || ea
514 .codebase
515 .extends_or_implements(arg_cls.as_ref(), param_cls.as_ref())
516 }
517 _ => false,
518 });
519 }
520 Atomic::TNull => {
521 return param.types.iter().any(|p| matches!(p, Atomic::TNull));
522 }
523 Atomic::TFalse => {
524 return param
525 .types
526 .iter()
527 .any(|p| matches!(p, Atomic::TFalse | Atomic::TBool));
528 }
529 _ => return false,
530 };
531
532 if param
533 .types
534 .iter()
535 .any(|p| matches!(p, Atomic::TCallable { .. }))
536 {
537 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
538 if ea.codebase.get_method(&resolved_arg, "__invoke").is_some()
539 || ea
540 .codebase
541 .get_method(arg_fqcn.as_ref(), "__invoke")
542 .is_some()
543 {
544 return true;
545 }
546 }
547
548 param.types.iter().any(|p_atomic| {
549 let param_fqcn: &Arc<str> = match p_atomic {
550 Atomic::TNamedObject { fqcn, .. } => fqcn,
551 Atomic::TSelf { fqcn } => fqcn,
552 Atomic::TStaticObject { fqcn } => fqcn,
553 Atomic::TParent { fqcn } => fqcn,
554 _ => return false,
555 };
556 let resolved_param = ea
557 .codebase
558 .resolve_class_name(&ea.file, param_fqcn.as_ref());
559 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
560
561 let is_same_class = resolved_param == resolved_arg
562 || arg_fqcn.as_ref() == resolved_param.as_str()
563 || resolved_arg == param_fqcn.as_ref();
564
565 if is_same_class {
566 let arg_type_params = match a_atomic {
567 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
568 _ => &[],
569 };
570 let param_type_params = match p_atomic {
571 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
572 _ => &[],
573 };
574 if !arg_type_params.is_empty() || !param_type_params.is_empty() {
575 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
576 return generic_type_params_compatible(
577 arg_type_params,
578 param_type_params,
579 &class_tps,
580 ea,
581 );
582 }
583 return true;
584 }
585
586 let arg_extends_param = ea
587 .codebase
588 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
589 || ea
590 .codebase
591 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
592 || ea
593 .codebase
594 .extends_or_implements(&resolved_arg, &resolved_param);
595
596 if arg_extends_param {
597 let param_type_params = match p_atomic {
598 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
599 _ => &[],
600 };
601 if !param_type_params.is_empty() {
602 let ancestor_args =
603 generic_ancestor_type_args(arg_fqcn.as_ref(), &resolved_param, ea)
604 .or_else(|| {
605 generic_ancestor_type_args(&resolved_arg, &resolved_param, ea)
606 })
607 .or_else(|| {
608 generic_ancestor_type_args(
609 arg_fqcn.as_ref(),
610 param_fqcn.as_ref(),
611 ea,
612 )
613 })
614 .or_else(|| {
615 generic_ancestor_type_args(&resolved_arg, param_fqcn.as_ref(), ea)
616 });
617 if let Some(arg_as_param_params) = ancestor_args {
618 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
619 return generic_type_params_compatible(
620 &arg_as_param_params,
621 param_type_params,
622 &class_tps,
623 ea,
624 );
625 }
626 }
627 return true;
628 }
629
630 if ea
631 .codebase
632 .extends_or_implements(param_fqcn.as_ref(), &resolved_arg)
633 || ea
634 .codebase
635 .extends_or_implements(param_fqcn.as_ref(), arg_fqcn.as_ref())
636 || ea
637 .codebase
638 .extends_or_implements(&resolved_param, &resolved_arg)
639 {
640 let param_type_params = match p_atomic {
641 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
642 _ => &[],
643 };
644 if param_type_params.is_empty() {
645 return true;
646 }
647 }
648
649 if !arg_fqcn.contains('\\') && !ea.codebase.type_exists(&resolved_arg) {
650 for entry in ea.codebase.classes.iter() {
651 if entry.value().short_name.as_ref() == arg_fqcn.as_ref() {
652 let actual_fqcn = entry.key().clone();
653 if ea
654 .codebase
655 .extends_or_implements(actual_fqcn.as_ref(), &resolved_param)
656 || ea
657 .codebase
658 .extends_or_implements(actual_fqcn.as_ref(), param_fqcn.as_ref())
659 {
660 return true;
661 }
662 }
663 }
664 }
665
666 let iface_key = if ea.codebase.interfaces.contains_key(arg_fqcn.as_ref()) {
667 Some(arg_fqcn.as_ref())
668 } else if ea.codebase.interfaces.contains_key(resolved_arg.as_str()) {
669 Some(resolved_arg.as_str())
670 } else {
671 None
672 };
673 if let Some(iface_fqcn) = iface_key {
674 let compatible = ea.codebase.classes.iter().any(|entry| {
675 let cls = entry.value();
676 cls.all_parents.iter().any(|p| p.as_ref() == iface_fqcn)
677 && (ea
678 .codebase
679 .extends_or_implements(entry.key().as_ref(), param_fqcn.as_ref())
680 || ea
681 .codebase
682 .extends_or_implements(entry.key().as_ref(), &resolved_param))
683 });
684 if compatible {
685 return true;
686 }
687 }
688
689 if arg_fqcn.contains('\\')
690 && !ea.codebase.type_exists(arg_fqcn.as_ref())
691 && !ea.codebase.type_exists(&resolved_arg)
692 {
693 return true;
694 }
695
696 if param_fqcn.contains('\\')
697 && !ea.codebase.type_exists(param_fqcn.as_ref())
698 && !ea.codebase.type_exists(&resolved_param)
699 {
700 return true;
701 }
702
703 false
704 })
705 })
706}
707
708fn strict_named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
710 arg.types.iter().all(|a_atomic| {
711 let arg_fqcn: &Arc<str> = match a_atomic {
712 Atomic::TNamedObject { fqcn, .. } => fqcn,
713 Atomic::TNever => return true,
714 _ => return false,
715 };
716 param.types.iter().any(|p_atomic| {
717 let param_fqcn: &Arc<str> = match p_atomic {
718 Atomic::TNamedObject { fqcn, .. } => fqcn,
719 _ => return false,
720 };
721 let resolved_param = ea
722 .codebase
723 .resolve_class_name(&ea.file, param_fqcn.as_ref());
724 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
725 resolved_param == resolved_arg
726 || arg_fqcn.as_ref() == resolved_param.as_str()
727 || resolved_arg == param_fqcn.as_ref()
728 || ea
729 .codebase
730 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
731 || ea
732 .codebase
733 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
734 || ea
735 .codebase
736 .extends_or_implements(&resolved_arg, &resolved_param)
737 })
738 })
739}
740
741fn generic_type_params_compatible(
743 arg_params: &[Union],
744 param_params: &[Union],
745 template_params: &[mir_codebase::storage::TemplateParam],
746 ea: &ExpressionAnalyzer<'_>,
747) -> bool {
748 if arg_params.len() != param_params.len() {
749 return true;
750 }
751 if arg_params.is_empty() {
752 return true;
753 }
754
755 for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
756 let variance = template_params
757 .get(i)
758 .map(|tp| tp.variance)
759 .unwrap_or(mir_types::Variance::Invariant);
760
761 let compatible = match variance {
762 mir_types::Variance::Covariant => {
763 arg_p.is_subtype_of_simple(param_p)
764 || param_p.is_mixed()
765 || arg_p.is_mixed()
766 || strict_named_object_subtype(arg_p, param_p, ea)
767 }
768 mir_types::Variance::Contravariant => {
769 param_p.is_subtype_of_simple(arg_p)
770 || arg_p.is_mixed()
771 || param_p.is_mixed()
772 || strict_named_object_subtype(param_p, arg_p, ea)
773 }
774 mir_types::Variance::Invariant => {
775 arg_p == param_p
776 || arg_p.is_mixed()
777 || param_p.is_mixed()
778 || (arg_p.is_subtype_of_simple(param_p) && param_p.is_subtype_of_simple(arg_p))
779 }
780 };
781
782 if !compatible {
783 return false;
784 }
785 }
786
787 true
788}
789
790fn generic_ancestor_type_args(
791 child: &str,
792 ancestor: &str,
793 ea: &ExpressionAnalyzer<'_>,
794) -> Option<Vec<Union>> {
795 let mut seen = std::collections::HashSet::new();
796 generic_ancestor_type_args_inner(child, ancestor, ea, &mut seen)
797}
798
799fn generic_ancestor_type_args_inner(
800 child: &str,
801 ancestor: &str,
802 ea: &ExpressionAnalyzer<'_>,
803 seen: &mut std::collections::HashSet<String>,
804) -> Option<Vec<Union>> {
805 if child == ancestor {
806 return Some(vec![]);
807 }
808 if !seen.insert(child.to_string()) {
809 return None;
810 }
811
812 let cls = ea.codebase.classes.get(child)?;
813 let parent = cls.parent.clone();
814 let extends_type_args = cls.extends_type_args.clone();
815 let implements_type_args = cls.implements_type_args.clone();
816 drop(cls);
817
818 for (iface, args) in implements_type_args {
819 if iface.as_ref() == ancestor {
820 return Some(args);
821 }
822 }
823
824 let parent = parent?;
825 if parent.as_ref() == ancestor {
826 return Some(extends_type_args);
827 }
828
829 let parent_args = generic_ancestor_type_args_inner(parent.as_ref(), ancestor, ea, seen)?;
830 if parent_args.is_empty() {
831 return Some(parent_args);
832 }
833
834 let parent_template_params = ea.codebase.get_class_template_params(parent.as_ref());
835 let bindings: std::collections::HashMap<Arc<str>, Union> = parent_template_params
836 .iter()
837 .zip(extends_type_args.iter())
838 .map(|(tp, ty)| (tp.name.clone(), ty.clone()))
839 .collect();
840
841 Some(
842 parent_args
843 .into_iter()
844 .map(|ty| ty.substitute_templates(&bindings))
845 .collect(),
846 )
847}
848
849fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
850 param_ty.types.iter().any(|atomic| match atomic {
851 Atomic::TTemplateParam { .. } => true,
852 Atomic::TNamedObject { fqcn, .. } => {
853 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
854 }
855 Atomic::TClassString(Some(inner)) => {
856 !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
857 }
858 Atomic::TArray { key: _, value }
859 | Atomic::TList { value }
860 | Atomic::TNonEmptyArray { key: _, value }
861 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
862 Atomic::TTemplateParam { .. } => true,
863 Atomic::TNamedObject { fqcn, .. } => {
864 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
865 }
866 _ => false,
867 }),
868 _ => false,
869 })
870}
871
872fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
873 arg_ty.types.iter().all(|av| {
874 let av_fqcn: &Arc<str> = match av {
875 Atomic::TNamedObject { fqcn, .. } => fqcn,
876 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
877 fqcn
878 }
879 Atomic::TArray { value, .. }
880 | Atomic::TNonEmptyArray { value, .. }
881 | Atomic::TList { value }
882 | Atomic::TNonEmptyList { value } => {
883 return param_ty.types.iter().any(|pv| {
884 let pv_val: &Union = match pv {
885 Atomic::TArray { value, .. }
886 | Atomic::TNonEmptyArray { value, .. }
887 | Atomic::TList { value }
888 | Atomic::TNonEmptyList { value } => value,
889 _ => return false,
890 };
891 union_compatible(value, pv_val, ea)
892 });
893 }
894 Atomic::TKeyedArray { .. } => return true,
895 _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
896 };
897
898 param_ty.types.iter().any(|pv| {
899 let pv_fqcn: &Arc<str> = match pv {
900 Atomic::TNamedObject { fqcn, .. } => fqcn,
901 Atomic::TSelf { fqcn }
902 | Atomic::TStaticObject { fqcn }
903 | Atomic::TParent { fqcn } => fqcn,
904 _ => return false,
905 };
906 if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
907 return true;
908 }
909 let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
910 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
911 resolved_param == resolved_arg
912 || ea
913 .codebase
914 .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
915 || ea
916 .codebase
917 .extends_or_implements(&resolved_arg, &resolved_param)
918 || ea
919 .codebase
920 .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
921 || ea
922 .codebase
923 .extends_or_implements(&resolved_param, &resolved_arg)
924 })
925 })
926}
927
928fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
929 arg_ty.types.iter().all(|a_atomic| {
930 let arg_value: &Union = match a_atomic {
931 Atomic::TArray { value, .. }
932 | Atomic::TNonEmptyArray { value, .. }
933 | Atomic::TList { value }
934 | Atomic::TNonEmptyList { value } => value,
935 Atomic::TKeyedArray { .. } => return true,
936 _ => return false,
937 };
938
939 param_ty.types.iter().any(|p_atomic| {
940 let param_value: &Union = match p_atomic {
941 Atomic::TArray { value, .. }
942 | Atomic::TNonEmptyArray { value, .. }
943 | Atomic::TList { value }
944 | Atomic::TNonEmptyList { value } => value,
945 _ => return false,
946 };
947
948 union_compatible(arg_value, param_value, ea)
949 })
950 })
951}