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