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