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