Skip to main content

mir_analyzer/call/
args.rs

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
12// ---------------------------------------------------------------------------
13// Public types and helpers
14// ---------------------------------------------------------------------------
15
16pub 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
42/// For a spread (`...`) argument, return the union of value types across all array atomics.
43/// E.g. `array<int, int>` → `int`, `list<string>` → `string`, `mixed` → `mixed`.
44pub 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
73/// Replace `TStaticObject` / `TSelf` in a method's return type with the actual receiver FQCN.
74pub(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
162// ---------------------------------------------------------------------------
163// Argument type checking
164// ---------------------------------------------------------------------------
165
166pub(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 = &params[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) = &param.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                    &param_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
383// ---------------------------------------------------------------------------
384// Subtype helpers (private to this module)
385// ---------------------------------------------------------------------------
386
387fn 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 &param_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
447/// Returns true if every atomic in `arg` can be assigned to some atomic in `param`
448/// using codebase-aware class hierarchy checks.
449fn 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
675/// Strict subtype check for generic type parameter positions (no coercion direction).
676fn 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
708/// Check generic type parameter compatibility according to declared variance.
709fn 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}