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
12fn 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
28pub 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
58pub 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
89pub(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
185pub(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 = ¶ms[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) = ¶m.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 ¶m_ty_owned
339 } else {
340 raw_param_ty
341 }
342 } else {
343 raw_param_ty
344 };
345
346 for param_atomic in ¶m_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 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 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
469fn 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 ¶m_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
531fn 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 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
781fn 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
815fn 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}