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::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                // Strip null too: handles int|null|false → int (alongside PossiblyNullArgument)
357                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
416// ---------------------------------------------------------------------------
417// Subtype helpers (private to this module)
418// ---------------------------------------------------------------------------
419
420fn 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 &param_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
480/// Returns true if every atomic in `arg` can be assigned to some atomic in `param`
481/// using codebase-aware class hierarchy checks.
482fn 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 class_fqcns: Vec<std::sync::Arc<str>> = ea
675                    .codebase
676                    .classes
677                    .iter()
678                    .map(|e| e.key().clone())
679                    .collect();
680                let compatible = class_fqcns.iter().any(|cls_fqcn| {
681                    ea.codebase
682                        .extends_or_implements(cls_fqcn.as_ref(), iface_fqcn)
683                        && (ea
684                            .codebase
685                            .extends_or_implements(cls_fqcn.as_ref(), param_fqcn.as_ref())
686                            || ea
687                                .codebase
688                                .extends_or_implements(cls_fqcn.as_ref(), &resolved_param))
689                });
690                if compatible {
691                    return true;
692                }
693            }
694
695            if arg_fqcn.contains('\\')
696                && !ea.codebase.type_exists(arg_fqcn.as_ref())
697                && !ea.codebase.type_exists(&resolved_arg)
698            {
699                return true;
700            }
701
702            if param_fqcn.contains('\\')
703                && !ea.codebase.type_exists(param_fqcn.as_ref())
704                && !ea.codebase.type_exists(&resolved_param)
705            {
706                return true;
707            }
708
709            false
710        })
711    })
712}
713
714/// Strict subtype check for generic type parameter positions (no coercion direction).
715fn strict_named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
716    arg.types.iter().all(|a_atomic| {
717        let arg_fqcn: &Arc<str> = match a_atomic {
718            Atomic::TNamedObject { fqcn, .. } => fqcn,
719            Atomic::TNever => return true,
720            _ => return false,
721        };
722        param.types.iter().any(|p_atomic| {
723            let param_fqcn: &Arc<str> = match p_atomic {
724                Atomic::TNamedObject { fqcn, .. } => fqcn,
725                _ => return false,
726            };
727            let resolved_param = ea
728                .codebase
729                .resolve_class_name(&ea.file, param_fqcn.as_ref());
730            let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
731            resolved_param == resolved_arg
732                || arg_fqcn.as_ref() == resolved_param.as_str()
733                || resolved_arg == param_fqcn.as_ref()
734                || ea
735                    .codebase
736                    .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
737                || ea
738                    .codebase
739                    .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
740                || ea
741                    .codebase
742                    .extends_or_implements(&resolved_arg, &resolved_param)
743        })
744    })
745}
746
747/// Check generic type parameter compatibility according to declared variance.
748fn generic_type_params_compatible(
749    arg_params: &[Union],
750    param_params: &[Union],
751    template_params: &[mir_codebase::storage::TemplateParam],
752    ea: &ExpressionAnalyzer<'_>,
753) -> bool {
754    if arg_params.len() != param_params.len() {
755        return true;
756    }
757    if arg_params.is_empty() {
758        return true;
759    }
760
761    for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
762        let variance = template_params
763            .get(i)
764            .map(|tp| tp.variance)
765            .unwrap_or(mir_types::Variance::Invariant);
766
767        let compatible = match variance {
768            mir_types::Variance::Covariant => {
769                arg_p.is_subtype_of_simple(param_p)
770                    || param_p.is_mixed()
771                    || arg_p.is_mixed()
772                    || strict_named_object_subtype(arg_p, param_p, ea)
773            }
774            mir_types::Variance::Contravariant => {
775                param_p.is_subtype_of_simple(arg_p)
776                    || arg_p.is_mixed()
777                    || param_p.is_mixed()
778                    || strict_named_object_subtype(param_p, arg_p, ea)
779            }
780            mir_types::Variance::Invariant => {
781                arg_p == param_p
782                    || arg_p.is_mixed()
783                    || param_p.is_mixed()
784                    || (arg_p.is_subtype_of_simple(param_p) && param_p.is_subtype_of_simple(arg_p))
785            }
786        };
787
788        if !compatible {
789            return false;
790        }
791    }
792
793    true
794}
795
796fn generic_ancestor_type_args(
797    child: &str,
798    ancestor: &str,
799    ea: &ExpressionAnalyzer<'_>,
800) -> Option<Vec<Union>> {
801    let mut seen = std::collections::HashSet::new();
802    generic_ancestor_type_args_inner(child, ancestor, ea, &mut seen)
803}
804
805fn generic_ancestor_type_args_inner(
806    child: &str,
807    ancestor: &str,
808    ea: &ExpressionAnalyzer<'_>,
809    seen: &mut std::collections::HashSet<String>,
810) -> Option<Vec<Union>> {
811    if child == ancestor {
812        return Some(vec![]);
813    }
814    if !seen.insert(child.to_string()) {
815        return None;
816    }
817
818    let cls = ea.codebase.classes.get(child)?;
819    let parent = cls.parent.clone();
820    let extends_type_args = cls.extends_type_args.clone();
821    let implements_type_args = cls.implements_type_args.clone();
822    drop(cls);
823
824    for (iface, args) in implements_type_args {
825        if iface.as_ref() == ancestor {
826            return Some(args);
827        }
828    }
829
830    let parent = parent?;
831    if parent.as_ref() == ancestor {
832        return Some(extends_type_args);
833    }
834
835    let parent_args = generic_ancestor_type_args_inner(parent.as_ref(), ancestor, ea, seen)?;
836    if parent_args.is_empty() {
837        return Some(parent_args);
838    }
839
840    let parent_template_params = ea.codebase.get_class_template_params(parent.as_ref());
841    let bindings: std::collections::HashMap<Arc<str>, Union> = parent_template_params
842        .iter()
843        .zip(extends_type_args.iter())
844        .map(|(tp, ty)| (tp.name.clone(), ty.clone()))
845        .collect();
846
847    Some(
848        parent_args
849            .into_iter()
850            .map(|ty| ty.substitute_templates(&bindings))
851            .collect(),
852    )
853}
854
855fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
856    param_ty.types.iter().any(|atomic| match atomic {
857        Atomic::TTemplateParam { .. } => true,
858        Atomic::TNamedObject { fqcn, .. } => {
859            !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
860        }
861        Atomic::TClassString(Some(inner)) => {
862            !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
863        }
864        Atomic::TArray { key: _, value }
865        | Atomic::TList { value }
866        | Atomic::TNonEmptyArray { key: _, value }
867        | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
868            Atomic::TTemplateParam { .. } => true,
869            Atomic::TNamedObject { fqcn, .. } => {
870                !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
871            }
872            _ => false,
873        }),
874        _ => false,
875    })
876}
877
878fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
879    arg_ty.types.iter().all(|av| {
880        let av_fqcn: &Arc<str> = match av {
881            Atomic::TNamedObject { fqcn, .. } => fqcn,
882            Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
883                fqcn
884            }
885            Atomic::TArray { value, .. }
886            | Atomic::TNonEmptyArray { value, .. }
887            | Atomic::TList { value }
888            | Atomic::TNonEmptyList { value } => {
889                return param_ty.types.iter().any(|pv| {
890                    let pv_val: &Union = match pv {
891                        Atomic::TArray { value, .. }
892                        | Atomic::TNonEmptyArray { value, .. }
893                        | Atomic::TList { value }
894                        | Atomic::TNonEmptyList { value } => value,
895                        _ => return false,
896                    };
897                    union_compatible(value, pv_val, ea)
898                });
899            }
900            Atomic::TKeyedArray { .. } => return true,
901            _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
902        };
903
904        param_ty.types.iter().any(|pv| {
905            let pv_fqcn: &Arc<str> = match pv {
906                Atomic::TNamedObject { fqcn, .. } => fqcn,
907                Atomic::TSelf { fqcn }
908                | Atomic::TStaticObject { fqcn }
909                | Atomic::TParent { fqcn } => fqcn,
910                _ => return false,
911            };
912            if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
913                return true;
914            }
915            let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
916            let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
917            resolved_param == resolved_arg
918                || ea
919                    .codebase
920                    .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
921                || ea
922                    .codebase
923                    .extends_or_implements(&resolved_arg, &resolved_param)
924                || ea
925                    .codebase
926                    .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
927                || ea
928                    .codebase
929                    .extends_or_implements(&resolved_param, &resolved_arg)
930        })
931    })
932}
933
934fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
935    arg_ty.types.iter().all(|a_atomic| {
936        let arg_value: &Union = match a_atomic {
937            Atomic::TArray { value, .. }
938            | Atomic::TNonEmptyArray { value, .. }
939            | Atomic::TList { value }
940            | Atomic::TNonEmptyList { value } => value,
941            Atomic::TKeyedArray { .. } => return true,
942            _ => return false,
943        };
944
945        param_ty.types.iter().any(|p_atomic| {
946            let param_value: &Union = match p_atomic {
947                Atomic::TArray { value, .. }
948                | Atomic::TNonEmptyArray { value, .. }
949                | Atomic::TList { value }
950                | Atomic::TNonEmptyList { value } => value,
951                _ => return false,
952            };
953
954            union_compatible(arg_value, param_value, ea)
955        })
956    })
957}