Skip to main content

mir_analyzer/call/
args.rs

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