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