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 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 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 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
453fn 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 ¶m_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
515fn 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 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
765fn 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
799fn 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}