1use std::sync::Arc;
4
5use php_ast::ast::{ExprKind, FunctionCallExpr, MethodCallExpr, StaticMethodCallExpr};
6use php_ast::Span;
7
8use mir_codebase::storage::{FnParam, MethodStorage, Visibility};
9use mir_issues::{IssueKind, Severity};
10use mir_types::{Atomic, Union};
11
12use crate::context::Context;
13use crate::expr::ExpressionAnalyzer;
14use crate::generic::{build_class_bindings, check_template_bounds, infer_template_bindings};
15use crate::symbol::SymbolKind;
16use crate::taint::{classify_sink, is_expr_tainted, SinkKind};
17
18pub struct CallAnalyzer;
23
24impl CallAnalyzer {
25 pub fn analyze_function_call<'a, 'arena, 'src>(
30 ea: &mut ExpressionAnalyzer<'a>,
31 call: &FunctionCallExpr<'arena, 'src>,
32 ctx: &mut Context,
33 span: Span,
34 ) -> Union {
35 let fn_name = match &call.name.kind {
37 ExprKind::Identifier(name) => (*name).to_string(),
38 _ => {
39 ea.analyze(call.name, ctx);
41 for arg in call.args.iter() {
42 ea.analyze(&arg.value, ctx);
43 }
44 return Union::mixed();
45 }
46 };
47
48 if let Some(sink_kind) = classify_sink(&fn_name) {
50 for arg in call.args.iter() {
51 if is_expr_tainted(&arg.value, ctx) {
52 let issue_kind = match sink_kind {
53 SinkKind::Html => IssueKind::TaintedHtml,
54 SinkKind::Sql => IssueKind::TaintedSql,
55 SinkKind::Shell => IssueKind::TaintedShell,
56 };
57 ea.emit(issue_kind, Severity::Error, span);
58 break; }
60 }
61 }
62
63 let fn_name = fn_name
67 .strip_prefix('\\')
68 .map(|s: &str| s.to_string())
69 .unwrap_or(fn_name);
70 let resolved_fn_name: String = {
71 let qualified = ea.codebase.resolve_class_name(&ea.file, &fn_name);
72 if ea.codebase.functions.contains_key(qualified.as_str()) {
73 qualified
74 } else if ea.codebase.functions.contains_key(fn_name.as_str()) {
75 fn_name.clone()
76 } else {
77 qualified
79 }
80 };
81
82 if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
86 for (i, param) in func.params.iter().enumerate() {
87 if param.is_byref {
88 if param.is_variadic {
89 for arg in call.args.iter().skip(i) {
91 if let ExprKind::Variable(name) = &arg.value.kind {
92 let var_name = name.as_str().trim_start_matches('$');
93 if !ctx.var_is_defined(var_name) {
94 ctx.set_var(var_name, Union::mixed());
95 }
96 }
97 }
98 } else if let Some(arg) = call.args.get(i) {
99 if let ExprKind::Variable(name) = &arg.value.kind {
100 let var_name = name.as_str().trim_start_matches('$');
101 if !ctx.var_is_defined(var_name) {
102 ctx.set_var(var_name, Union::mixed());
103 }
104 }
105 }
106 }
107 }
108 }
109
110 let arg_types: Vec<Union> = call
112 .args
113 .iter()
114 .map(|arg| {
115 let ty = ea.analyze(&arg.value, ctx);
116 if arg.unpack {
117 spread_element_type(&ty)
118 } else {
119 ty
120 }
121 })
122 .collect();
123
124 if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
126 ea.codebase.mark_function_referenced(&func.fqn.clone());
127 let is_deprecated = func.is_deprecated;
128 let params = func.params.clone();
129 let template_params = func.template_params.clone();
130 let return_ty_raw = func
131 .effective_return_type()
132 .cloned()
133 .unwrap_or_else(Union::mixed);
134
135 if is_deprecated {
137 ea.emit(
138 IssueKind::DeprecatedCall {
139 name: resolved_fn_name.clone(),
140 },
141 Severity::Info,
142 span,
143 );
144 }
145
146 check_args(
147 ea,
148 CheckArgsParams {
149 fn_name: &fn_name,
150 params: ¶ms,
151 arg_types: &arg_types,
152 arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
153 arg_names: &call
154 .args
155 .iter()
156 .map(|a| a.name.as_ref().map(|n| n.to_string()))
157 .collect::<Vec<_>>(),
158 call_span: span,
159 has_spread: call.args.iter().any(|a| a.unpack),
160 },
161 );
162
163 for (i, param) in params.iter().enumerate() {
165 if param.is_byref {
166 if param.is_variadic {
167 for arg in call.args.iter().skip(i) {
168 if let ExprKind::Variable(name) = &arg.value.kind {
169 let var_name = name.as_str().trim_start_matches('$');
170 ctx.set_var(var_name, Union::mixed());
171 }
172 }
173 } else if let Some(arg) = call.args.get(i) {
174 if let ExprKind::Variable(name) = &arg.value.kind {
175 let var_name = name.as_str().trim_start_matches('$');
176 ctx.set_var(var_name, Union::mixed());
177 }
178 }
179 }
180 }
181
182 let return_ty = if !template_params.is_empty() {
184 let bindings = infer_template_bindings(&template_params, ¶ms, &arg_types);
185 for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
187 ea.emit(
188 IssueKind::InvalidTemplateParam {
189 name: name.to_string(),
190 expected_bound: format!("{}", bound),
191 actual: format!("{}", inferred),
192 },
193 Severity::Error,
194 span,
195 );
196 }
197 return_ty_raw.substitute_templates(&bindings)
198 } else {
199 return_ty_raw
200 };
201
202 ea.record_symbol(
203 span,
204 SymbolKind::FunctionCall(func.fqn.clone()),
205 return_ty.clone(),
206 );
207 return return_ty;
208 }
209
210 ea.emit(
212 IssueKind::UndefinedFunction { name: fn_name },
213 Severity::Error,
214 span,
215 );
216 Union::mixed()
217 }
218
219 pub fn analyze_method_call<'a, 'arena, 'src>(
224 ea: &mut ExpressionAnalyzer<'a>,
225 call: &MethodCallExpr<'arena, 'src>,
226 ctx: &mut Context,
227 span: Span,
228 nullsafe: bool,
229 ) -> Union {
230 let obj_ty = ea.analyze(call.object, ctx);
231
232 let method_name = match &call.method.kind {
233 ExprKind::Identifier(name) => (*name).to_string(),
234 ExprKind::Variable(name) => name.as_str().to_string(),
235 _ => return Union::mixed(),
236 };
237
238 let arg_types: Vec<Union> = call
242 .args
243 .iter()
244 .map(|arg| {
245 let ty = ea.analyze(&arg.value, ctx);
246 if arg.unpack {
247 spread_element_type(&ty)
248 } else {
249 ty
250 }
251 })
252 .collect();
253
254 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
255
256 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) {
258 if nullsafe {
259 } else if obj_ty.is_single() {
261 ea.emit(
262 IssueKind::NullMethodCall {
263 method: method_name.clone(),
264 },
265 Severity::Error,
266 span,
267 );
268 return Union::mixed();
269 } else {
270 ea.emit(
271 IssueKind::PossiblyNullMethodCall {
272 method: method_name.clone(),
273 },
274 Severity::Info,
275 span,
276 );
277 }
278 }
279
280 if obj_ty.is_mixed() {
282 ea.emit(
283 IssueKind::MixedMethodCall {
284 method: method_name.clone(),
285 },
286 Severity::Info,
287 span,
288 );
289 return Union::mixed();
290 }
291
292 let receiver = obj_ty.remove_null();
293 let mut result = Union::empty();
294
295 for atomic in &receiver.types {
296 match atomic {
297 Atomic::TNamedObject {
298 fqcn,
299 type_params: receiver_type_params,
300 } => {
301 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
303 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
304 if let Some(method) = ea.codebase.get_method(fqcn, &method_name) {
305 ea.codebase.mark_method_referenced(fqcn, &method_name);
307 if method.is_deprecated {
309 ea.emit(
310 IssueKind::DeprecatedMethodCall {
311 class: fqcn.to_string(),
312 method: method_name.clone(),
313 },
314 Severity::Info,
315 span,
316 );
317 }
318 check_method_visibility(ea, &method, ctx, span);
320
321 let arg_names: Vec<Option<String>> = call
323 .args
324 .iter()
325 .map(|a| a.name.as_ref().map(|n| n.to_string()))
326 .collect();
327 check_args(
328 ea,
329 CheckArgsParams {
330 fn_name: &method_name,
331 params: &method.params,
332 arg_types: &arg_types,
333 arg_spans: &arg_spans,
334 arg_names: &arg_names,
335 call_span: span,
336 has_spread: call.args.iter().any(|a| a.unpack),
337 },
338 );
339
340 let ret_raw = method
341 .effective_return_type()
342 .cloned()
343 .unwrap_or_else(Union::mixed);
344 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
346
347 let class_tps = ea.codebase.get_class_template_params(fqcn);
349 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
350
351 if !method.template_params.is_empty() {
353 let method_bindings = infer_template_bindings(
354 &method.template_params,
355 &method.params,
356 &arg_types,
357 );
358 for key in method_bindings.keys() {
359 if bindings.contains_key(key) {
360 ea.emit(
361 IssueKind::ShadowedTemplateParam {
362 name: key.to_string(),
363 },
364 Severity::Info,
365 span,
366 );
367 }
368 }
369 bindings.extend(method_bindings);
370 for (name, inferred, bound) in
371 check_template_bounds(&bindings, &method.template_params)
372 {
373 ea.emit(
374 IssueKind::InvalidTemplateParam {
375 name: name.to_string(),
376 expected_bound: format!("{}", bound),
377 actual: format!("{}", inferred),
378 },
379 Severity::Error,
380 span,
381 );
382 }
383 }
384
385 let ret = if !bindings.is_empty() {
386 ret_raw.substitute_templates(&bindings)
387 } else {
388 ret_raw
389 };
390 result = Union::merge(&result, &ret);
391 } else if ea.codebase.type_exists(fqcn)
392 && !ea.codebase.has_unknown_ancestor(fqcn)
393 {
394 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
401 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
402 if is_interface
403 || is_abstract
404 || ea.codebase.get_method(fqcn, "__call").is_some()
405 {
406 result = Union::merge(&result, &Union::mixed());
407 } else {
408 ea.emit(
409 IssueKind::UndefinedMethod {
410 class: fqcn.to_string(),
411 method: method_name.clone(),
412 },
413 Severity::Error,
414 span,
415 );
416 result = Union::merge(&result, &Union::mixed());
417 }
418 } else {
419 result = Union::merge(&result, &Union::mixed());
420 }
421 }
422 Atomic::TSelf { fqcn }
423 | Atomic::TStaticObject { fqcn }
424 | Atomic::TParent { fqcn } => {
425 let receiver_type_params: &[mir_types::Union] = &[];
426 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
428 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
429 if let Some(method) = ea.codebase.get_method(fqcn, &method_name) {
430 ea.codebase.mark_method_referenced(fqcn, &method_name);
432 if method.is_deprecated {
434 ea.emit(
435 IssueKind::DeprecatedMethodCall {
436 class: fqcn.to_string(),
437 method: method_name.clone(),
438 },
439 Severity::Info,
440 span,
441 );
442 }
443 check_method_visibility(ea, &method, ctx, span);
445
446 let arg_names: Vec<Option<String>> = call
448 .args
449 .iter()
450 .map(|a| a.name.as_ref().map(|n| n.to_string()))
451 .collect();
452 check_args(
453 ea,
454 CheckArgsParams {
455 fn_name: &method_name,
456 params: &method.params,
457 arg_types: &arg_types,
458 arg_spans: &arg_spans,
459 arg_names: &arg_names,
460 call_span: span,
461 has_spread: call.args.iter().any(|a| a.unpack),
462 },
463 );
464
465 let ret_raw = method
466 .effective_return_type()
467 .cloned()
468 .unwrap_or_else(Union::mixed);
469 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
471
472 let class_tps = ea.codebase.get_class_template_params(fqcn);
474 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
475
476 if !method.template_params.is_empty() {
478 let method_bindings = infer_template_bindings(
479 &method.template_params,
480 &method.params,
481 &arg_types,
482 );
483 for key in method_bindings.keys() {
484 if bindings.contains_key(key) {
485 ea.emit(
486 IssueKind::ShadowedTemplateParam {
487 name: key.to_string(),
488 },
489 Severity::Info,
490 span,
491 );
492 }
493 }
494 bindings.extend(method_bindings);
495 for (name, inferred, bound) in
496 check_template_bounds(&bindings, &method.template_params)
497 {
498 ea.emit(
499 IssueKind::InvalidTemplateParam {
500 name: name.to_string(),
501 expected_bound: format!("{}", bound),
502 actual: format!("{}", inferred),
503 },
504 Severity::Error,
505 span,
506 );
507 }
508 }
509
510 let ret = if !bindings.is_empty() {
511 ret_raw.substitute_templates(&bindings)
512 } else {
513 ret_raw
514 };
515 result = Union::merge(&result, &ret);
516 } else if ea.codebase.type_exists(fqcn)
517 && !ea.codebase.has_unknown_ancestor(fqcn)
518 {
519 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
526 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
527 if is_interface
528 || is_abstract
529 || ea.codebase.get_method(fqcn, "__call").is_some()
530 {
531 result = Union::merge(&result, &Union::mixed());
532 } else {
533 ea.emit(
534 IssueKind::UndefinedMethod {
535 class: fqcn.to_string(),
536 method: method_name.clone(),
537 },
538 Severity::Error,
539 span,
540 );
541 result = Union::merge(&result, &Union::mixed());
542 }
543 } else {
544 result = Union::merge(&result, &Union::mixed());
545 }
546 }
547 Atomic::TObject => {
548 result = Union::merge(&result, &Union::mixed());
549 }
550 Atomic::TTemplateParam { .. } => {
554 result = Union::merge(&result, &Union::mixed());
555 }
556 _ => {
557 result = Union::merge(&result, &Union::mixed());
558 }
559 }
560 }
561
562 if nullsafe && obj_ty.is_nullable() {
563 result.add_type(Atomic::TNull);
564 }
565
566 let final_ty = if result.is_empty() {
567 Union::mixed()
568 } else {
569 result
570 };
571 for atomic in &obj_ty.types {
573 if let Atomic::TNamedObject { fqcn, .. } = atomic {
574 ea.record_symbol(
575 span,
576 SymbolKind::MethodCall {
577 class: fqcn.clone(),
578 method: Arc::from(method_name.as_str()),
579 },
580 final_ty.clone(),
581 );
582 break;
583 }
584 }
585 final_ty
586 }
587
588 pub fn analyze_static_method_call<'a, 'arena, 'src>(
593 ea: &mut ExpressionAnalyzer<'a>,
594 call: &StaticMethodCallExpr<'arena, 'src>,
595 ctx: &mut Context,
596 span: Span,
597 ) -> Union {
598 let method_name = call.method.as_ref();
599
600 let fqcn = match &call.class.kind {
601 ExprKind::Identifier(name) => ea.codebase.resolve_class_name(&ea.file, name.as_ref()),
602 _ => return Union::mixed(),
603 };
604
605 let fqcn = resolve_static_class(&fqcn, ctx);
606
607 let arg_types: Vec<Union> = call
608 .args
609 .iter()
610 .map(|arg| {
611 let ty = ea.analyze(&arg.value, ctx);
612 if arg.unpack {
613 spread_element_type(&ty)
614 } else {
615 ty
616 }
617 })
618 .collect();
619 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
620
621 if let Some(method) = ea.codebase.get_method(&fqcn, method_name) {
622 ea.codebase.mark_method_referenced(&fqcn, method_name);
623 if method.is_deprecated {
625 ea.emit(
626 IssueKind::DeprecatedMethodCall {
627 class: fqcn.clone(),
628 method: method_name.to_string(),
629 },
630 Severity::Info,
631 span,
632 );
633 }
634 let arg_names: Vec<Option<String>> = call
635 .args
636 .iter()
637 .map(|a| a.name.as_ref().map(|n| n.to_string()))
638 .collect();
639 check_args(
640 ea,
641 CheckArgsParams {
642 fn_name: method_name,
643 params: &method.params,
644 arg_types: &arg_types,
645 arg_spans: &arg_spans,
646 arg_names: &arg_names,
647 call_span: span,
648 has_spread: call.args.iter().any(|a| a.unpack),
649 },
650 );
651 let ret_raw = method
652 .effective_return_type()
653 .cloned()
654 .unwrap_or_else(Union::mixed);
655 let fqcn_arc: std::sync::Arc<str> = Arc::from(fqcn.as_str());
656 let ret = substitute_static_in_return(ret_raw, &fqcn_arc);
657 ea.record_symbol(
658 span,
659 SymbolKind::StaticCall {
660 class: fqcn_arc,
661 method: Arc::from(method_name),
662 },
663 ret.clone(),
664 );
665 ret
666 } else if ea.codebase.type_exists(&fqcn) && !ea.codebase.has_unknown_ancestor(&fqcn) {
667 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_str());
671 let is_abstract = ea.codebase.is_abstract_class(&fqcn);
672 if is_interface || is_abstract || ea.codebase.get_method(&fqcn, "__call").is_some() {
673 Union::mixed()
674 } else {
675 ea.emit(
676 IssueKind::UndefinedMethod {
677 class: fqcn,
678 method: method_name.to_string(),
679 },
680 Severity::Error,
681 span,
682 );
683 Union::mixed()
684 }
685 } else {
686 Union::mixed()
688 }
689 }
690}
691
692pub struct CheckArgsParams<'a> {
697 pub fn_name: &'a str,
698 pub params: &'a [FnParam],
699 pub arg_types: &'a [Union],
700 pub arg_spans: &'a [Span],
701 pub arg_names: &'a [Option<String>],
702 pub call_span: Span,
703 pub has_spread: bool,
704}
705
706pub fn check_constructor_args(
707 ea: &mut ExpressionAnalyzer<'_>,
708 class_name: &str,
709 p: CheckArgsParams<'_>,
710) {
711 let ctor_name = format!("{}::__construct", class_name);
712 check_args(
713 ea,
714 CheckArgsParams {
715 fn_name: &ctor_name,
716 ..p
717 },
718 );
719}
720
721fn check_args(ea: &mut ExpressionAnalyzer<'_>, p: CheckArgsParams<'_>) {
726 let CheckArgsParams {
727 fn_name,
728 params,
729 arg_types,
730 arg_spans,
731 arg_names,
732 call_span,
733 has_spread,
734 } = p;
735 let has_named = arg_names.iter().any(|n| n.is_some());
738
739 let mut param_to_arg: Vec<Option<(Union, Span)>> = vec![None; params.len()];
741
742 if has_named {
743 let mut positional = 0usize;
744 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
745 if let Some(Some(name)) = arg_names.get(i) {
746 if let Some(pi) = params.iter().position(|p| p.name.as_ref() == name.as_str()) {
748 param_to_arg[pi] = Some((ty.clone(), *span));
749 }
750 } else {
751 while positional < params.len() && param_to_arg[positional].is_some() {
753 positional += 1;
754 }
755 if positional < params.len() {
756 param_to_arg[positional] = Some((ty.clone(), *span));
757 positional += 1;
758 }
759 }
760 }
761 } else {
762 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
764 if i < params.len() {
765 param_to_arg[i] = Some((ty.clone(), *span));
766 }
767 }
768 }
769
770 let required_count = params
771 .iter()
772 .filter(|p| !p.is_optional && !p.is_variadic)
773 .count();
774 let provided_count = if params.iter().any(|p| p.is_variadic) {
775 arg_types.len()
776 } else {
777 arg_types.len().min(params.len())
778 };
779
780 if provided_count < required_count && !has_spread {
781 ea.emit(
782 IssueKind::InvalidArgument {
783 param: format!("#{}", provided_count + 1),
784 fn_name: fn_name.to_string(),
785 expected: format!("{} argument(s)", required_count),
786 actual: format!("{} provided", provided_count),
787 },
788 Severity::Error,
789 call_span,
790 );
791 return;
792 }
793
794 for (i, (param, slot)) in params.iter().zip(param_to_arg.iter()).enumerate() {
795 let (arg_ty, arg_span) = match slot {
796 Some(pair) => pair,
797 None => continue, };
799 let arg_span = *arg_span;
800 let _ = i;
801
802 if let Some(raw_param_ty) = ¶m.ty {
803 let param_ty_owned;
805 let param_ty: &Union = if param.is_variadic {
806 if let Some(elem_ty) = raw_param_ty.types.iter().find_map(|a| match a {
807 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
808 Some(*value.clone())
809 }
810 _ => None,
811 }) {
812 param_ty_owned = elem_ty;
813 ¶m_ty_owned
814 } else {
815 raw_param_ty
816 }
817 } else {
818 raw_param_ty
819 };
820 if !param_ty.is_nullable() && arg_ty.is_nullable() {
822 ea.emit(
823 IssueKind::PossiblyNullArgument {
824 param: param.name.to_string(),
825 fn_name: fn_name.to_string(),
826 },
827 Severity::Info,
828 arg_span,
829 );
830 } else if !param_ty.is_nullable()
831 && arg_ty.contains(|t| matches!(t, Atomic::TNull))
832 && arg_ty.is_single()
833 {
834 ea.emit(
835 IssueKind::NullArgument {
836 param: param.name.to_string(),
837 fn_name: fn_name.to_string(),
838 },
839 Severity::Error,
840 arg_span,
841 );
842 }
843
844 if !arg_ty.is_subtype_of_simple(param_ty)
847 && !param_ty.is_mixed()
848 && !arg_ty.is_mixed()
849 && !named_object_subtype(arg_ty, param_ty, ea)
850 && !param_contains_template_or_unknown(param_ty, ea)
851 && !param_contains_template_or_unknown(arg_ty, ea)
852 && !array_list_compatible(arg_ty, param_ty, ea)
853 && !param_ty.is_subtype_of_simple(arg_ty)
856 && !param_ty.remove_null().is_subtype_of_simple(arg_ty)
858 && !param_ty.types.iter().any(|p| Union::single(p.clone()).is_subtype_of_simple(arg_ty))
860 && !arg_ty.remove_null().is_subtype_of_simple(param_ty)
863 && !arg_ty.remove_false().is_subtype_of_simple(param_ty)
864 && !named_object_subtype(&arg_ty.remove_null(), param_ty, ea)
865 && !named_object_subtype(&arg_ty.remove_false(), param_ty, ea)
866 {
867 ea.emit(
868 IssueKind::InvalidArgument {
869 param: param.name.to_string(),
870 fn_name: fn_name.to_string(),
871 expected: format!("{}", param_ty),
872 actual: format!("{}", arg_ty),
873 },
874 Severity::Error,
875 arg_span,
876 );
877 }
878 }
879 }
880}
881
882fn named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
889 use mir_types::Atomic;
890 arg.types.iter().all(|a_atomic| {
892 let arg_fqcn: &Arc<str> = match a_atomic {
894 Atomic::TNamedObject { fqcn, .. } => fqcn,
895 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => {
896 if ea.codebase.traits.contains_key(fqcn.as_ref()) {
898 return true;
899 }
900 fqcn
901 }
902 Atomic::TParent { fqcn } => fqcn,
903 Atomic::TNever => return true,
905 Atomic::TClosure { .. } => {
907 return param.types.iter().any(|p| match p {
908 Atomic::TClosure { .. } | Atomic::TCallable { .. } => true,
909 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
910 _ => false,
911 });
912 }
913 Atomic::TCallable { .. } => {
915 return param.types.iter().any(|p| match p {
916 Atomic::TCallable { .. } | Atomic::TClosure { .. } => true,
917 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
918 _ => false,
919 });
920 }
921 Atomic::TClassString(Some(arg_cls)) => {
923 return param.types.iter().any(|p| match p {
924 Atomic::TClassString(None) | Atomic::TString => true,
925 Atomic::TClassString(Some(param_cls)) => {
926 arg_cls == param_cls
927 || ea
928 .codebase
929 .extends_or_implements(arg_cls.as_ref(), param_cls.as_ref())
930 }
931 _ => false,
932 });
933 }
934 Atomic::TNull => {
936 return param.types.iter().any(|p| matches!(p, Atomic::TNull));
937 }
938 Atomic::TFalse => {
940 return param
941 .types
942 .iter()
943 .any(|p| matches!(p, Atomic::TFalse | Atomic::TBool));
944 }
945 _ => return false, };
947
948 if param
950 .types
951 .iter()
952 .any(|p| matches!(p, Atomic::TCallable { .. }))
953 {
954 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
955 if ea.codebase.get_method(&resolved_arg, "__invoke").is_some()
956 || ea
957 .codebase
958 .get_method(arg_fqcn.as_ref(), "__invoke")
959 .is_some()
960 {
961 return true;
962 }
963 }
964
965 param.types.iter().any(|p_atomic| {
966 let param_fqcn: &Arc<str> = match p_atomic {
967 Atomic::TNamedObject { fqcn, .. } => fqcn,
968 Atomic::TSelf { fqcn } => fqcn,
969 Atomic::TStaticObject { fqcn } => fqcn,
970 Atomic::TParent { fqcn } => fqcn,
971 _ => return false,
972 };
973 let resolved_param = ea
975 .codebase
976 .resolve_class_name(&ea.file, param_fqcn.as_ref());
977 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
978
979 let is_same_class = resolved_param == resolved_arg
981 || arg_fqcn.as_ref() == resolved_param.as_str()
982 || resolved_arg == param_fqcn.as_ref();
983
984 if is_same_class {
985 let arg_type_params = match a_atomic {
986 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
987 _ => &[],
988 };
989 let param_type_params = match p_atomic {
990 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
991 _ => &[],
992 };
993 if !arg_type_params.is_empty() || !param_type_params.is_empty() {
994 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
995 return generic_type_params_compatible(
996 arg_type_params,
997 param_type_params,
998 &class_tps,
999 ea,
1000 );
1001 }
1002 return true;
1003 }
1004
1005 if ea.codebase.extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1006 || ea.codebase.extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1007 || ea.codebase.extends_or_implements(&resolved_arg, &resolved_param)
1008 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), &resolved_arg)
1011 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), arg_fqcn.as_ref())
1012 || ea.codebase.extends_or_implements(&resolved_param, &resolved_arg)
1013 {
1014 return true;
1015 }
1016
1017 if !arg_fqcn.contains('\\') && !ea.codebase.type_exists(&resolved_arg) {
1022 for entry in ea.codebase.classes.iter() {
1023 if entry.value().short_name.as_ref() == arg_fqcn.as_ref() {
1024 let actual_fqcn = entry.key().clone();
1025 if ea
1026 .codebase
1027 .extends_or_implements(actual_fqcn.as_ref(), &resolved_param)
1028 || ea
1029 .codebase
1030 .extends_or_implements(actual_fqcn.as_ref(), param_fqcn.as_ref())
1031 {
1032 return true;
1033 }
1034 }
1035 }
1036 }
1037
1038 let iface_key = if ea.codebase.interfaces.contains_key(arg_fqcn.as_ref()) {
1042 Some(arg_fqcn.as_ref())
1043 } else if ea.codebase.interfaces.contains_key(resolved_arg.as_str()) {
1044 Some(resolved_arg.as_str())
1045 } else {
1046 None
1047 };
1048 if let Some(iface_fqcn) = iface_key {
1049 let compatible = ea.codebase.classes.iter().any(|entry| {
1050 let cls = entry.value();
1051 cls.all_parents.iter().any(|p| p.as_ref() == iface_fqcn)
1052 && (ea
1053 .codebase
1054 .extends_or_implements(entry.key().as_ref(), param_fqcn.as_ref())
1055 || ea
1056 .codebase
1057 .extends_or_implements(entry.key().as_ref(), &resolved_param))
1058 });
1059 if compatible {
1060 return true;
1061 }
1062 }
1063
1064 if arg_fqcn.contains('\\')
1067 && !ea.codebase.type_exists(arg_fqcn.as_ref())
1068 && !ea.codebase.type_exists(&resolved_arg)
1069 {
1070 return true;
1071 }
1072
1073 if param_fqcn.contains('\\')
1076 && !ea.codebase.type_exists(param_fqcn.as_ref())
1077 && !ea.codebase.type_exists(&resolved_param)
1078 {
1079 return true;
1080 }
1081
1082 false
1083 })
1084 })
1085}
1086
1087fn strict_named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1094 use mir_types::Atomic;
1095 arg.types.iter().all(|a_atomic| {
1096 let arg_fqcn: &Arc<str> = match a_atomic {
1097 Atomic::TNamedObject { fqcn, .. } => fqcn,
1098 Atomic::TNever => return true,
1099 _ => return false,
1100 };
1101 param.types.iter().any(|p_atomic| {
1102 let param_fqcn: &Arc<str> = match p_atomic {
1103 Atomic::TNamedObject { fqcn, .. } => fqcn,
1104 _ => return false,
1105 };
1106 let resolved_param = ea
1107 .codebase
1108 .resolve_class_name(&ea.file, param_fqcn.as_ref());
1109 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1110 resolved_param == resolved_arg
1112 || arg_fqcn.as_ref() == resolved_param.as_str()
1113 || resolved_arg == param_fqcn.as_ref()
1114 || ea
1115 .codebase
1116 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1117 || ea
1118 .codebase
1119 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1120 || ea
1121 .codebase
1122 .extends_or_implements(&resolved_arg, &resolved_param)
1123 })
1124 })
1125}
1126
1127fn generic_type_params_compatible(
1134 arg_params: &[Union],
1135 param_params: &[Union],
1136 template_params: &[mir_codebase::storage::TemplateParam],
1137 ea: &ExpressionAnalyzer<'_>,
1138) -> bool {
1139 if arg_params.len() != param_params.len() {
1141 return true;
1142 }
1143 if arg_params.is_empty() {
1145 return true;
1146 }
1147
1148 for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
1149 let variance = template_params
1150 .get(i)
1151 .map(|tp| tp.variance)
1152 .unwrap_or(mir_types::Variance::Invariant);
1153
1154 let compatible = match variance {
1155 mir_types::Variance::Covariant => {
1156 arg_p.is_subtype_of_simple(param_p)
1158 || param_p.is_mixed()
1159 || arg_p.is_mixed()
1160 || strict_named_object_subtype(arg_p, param_p, ea)
1161 }
1162 mir_types::Variance::Contravariant => {
1163 param_p.is_subtype_of_simple(arg_p)
1165 || arg_p.is_mixed()
1166 || param_p.is_mixed()
1167 || strict_named_object_subtype(param_p, arg_p, ea)
1168 }
1169 mir_types::Variance::Invariant => {
1170 arg_p == param_p
1172 || arg_p.is_mixed()
1173 || param_p.is_mixed()
1174 || (arg_p.is_subtype_of_simple(param_p) && param_p.is_subtype_of_simple(arg_p))
1175 }
1176 };
1177
1178 if !compatible {
1179 return false;
1180 }
1181 }
1182
1183 true
1184}
1185
1186fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1190 param_ty.types.iter().any(|atomic| match atomic {
1191 Atomic::TTemplateParam { .. } => true,
1192 Atomic::TNamedObject { fqcn, .. } => {
1193 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1194 }
1195 Atomic::TClassString(Some(inner)) => {
1197 !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
1198 }
1199 Atomic::TArray { key: _, value }
1200 | Atomic::TList { value }
1201 | Atomic::TNonEmptyArray { key: _, value }
1202 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1203 Atomic::TTemplateParam { .. } => true,
1204 Atomic::TNamedObject { fqcn, .. } => {
1205 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1206 }
1207 _ => false,
1208 }),
1209 _ => false,
1210 })
1211}
1212
1213fn substitute_static_in_return(ret: Union, receiver_fqcn: &Arc<str>) -> Union {
1216 use mir_types::Atomic;
1217 let from_docblock = ret.from_docblock;
1218 let types: Vec<Atomic> = ret
1219 .types
1220 .into_iter()
1221 .map(|a| match a {
1222 Atomic::TStaticObject { .. } | Atomic::TSelf { .. } => Atomic::TNamedObject {
1223 fqcn: receiver_fqcn.clone(),
1224 type_params: vec![],
1225 },
1226 other => other,
1227 })
1228 .collect();
1229 let mut result = Union::from_vec(types);
1230 result.from_docblock = from_docblock;
1231 result
1232}
1233
1234pub fn spread_element_type(arr_ty: &Union) -> Union {
1238 use mir_types::Atomic;
1239 let mut result = Union::empty();
1240 for atomic in arr_ty.types.iter() {
1241 match atomic {
1242 Atomic::TArray { value, .. }
1243 | Atomic::TNonEmptyArray { value, .. }
1244 | Atomic::TList { value }
1245 | Atomic::TNonEmptyList { value } => {
1246 for t in value.types.iter() {
1247 result.add_type(t.clone());
1248 }
1249 }
1250 Atomic::TKeyedArray { properties, .. } => {
1251 for (_key, prop) in properties.iter() {
1252 for t in prop.ty.types.iter() {
1253 result.add_type(t.clone());
1254 }
1255 }
1256 }
1257 _ => return Union::mixed(),
1259 }
1260 }
1261 if result.types.is_empty() {
1262 Union::mixed()
1263 } else {
1264 result
1265 }
1266}
1267
1268fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1274 arg_ty.types.iter().all(|av| {
1275 let av_fqcn: &Arc<str> = match av {
1277 Atomic::TNamedObject { fqcn, .. } => fqcn,
1278 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
1279 fqcn
1280 }
1281 Atomic::TArray { value, .. }
1283 | Atomic::TNonEmptyArray { value, .. }
1284 | Atomic::TList { value }
1285 | Atomic::TNonEmptyList { value } => {
1286 return param_ty.types.iter().any(|pv| {
1287 let pv_val: &Union = match pv {
1288 Atomic::TArray { value, .. }
1289 | Atomic::TNonEmptyArray { value, .. }
1290 | Atomic::TList { value }
1291 | Atomic::TNonEmptyList { value } => value,
1292 _ => return false,
1293 };
1294 union_compatible(value, pv_val, ea)
1295 });
1296 }
1297 Atomic::TKeyedArray { .. } => return true,
1298 _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
1299 };
1300
1301 param_ty.types.iter().any(|pv| {
1302 let pv_fqcn: &Arc<str> = match pv {
1303 Atomic::TNamedObject { fqcn, .. } => fqcn,
1304 Atomic::TSelf { fqcn }
1305 | Atomic::TStaticObject { fqcn }
1306 | Atomic::TParent { fqcn } => fqcn,
1307 _ => return false,
1308 };
1309 if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
1311 return true;
1312 }
1313 let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
1314 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
1315 resolved_param == resolved_arg
1316 || ea
1317 .codebase
1318 .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
1319 || ea
1320 .codebase
1321 .extends_or_implements(&resolved_arg, &resolved_param)
1322 || ea
1323 .codebase
1324 .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
1325 || ea
1326 .codebase
1327 .extends_or_implements(&resolved_param, &resolved_arg)
1328 })
1329 })
1330}
1331
1332fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1333 arg_ty.types.iter().all(|a_atomic| {
1334 let arg_value: &Union = match a_atomic {
1335 Atomic::TArray { value, .. }
1336 | Atomic::TNonEmptyArray { value, .. }
1337 | Atomic::TList { value }
1338 | Atomic::TNonEmptyList { value } => value,
1339 Atomic::TKeyedArray { .. } => return true, _ => return false,
1341 };
1342
1343 param_ty.types.iter().any(|p_atomic| {
1344 let param_value: &Union = match p_atomic {
1345 Atomic::TArray { value, .. }
1346 | Atomic::TNonEmptyArray { value, .. }
1347 | Atomic::TList { value }
1348 | Atomic::TNonEmptyList { value } => value,
1349 _ => return false,
1350 };
1351
1352 union_compatible(arg_value, param_value, ea)
1353 })
1354 })
1355}
1356
1357fn check_method_visibility(
1358 ea: &mut ExpressionAnalyzer<'_>,
1359 method: &MethodStorage,
1360 ctx: &Context,
1361 span: Span,
1362) {
1363 match method.visibility {
1364 Visibility::Private => {
1365 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1367 if caller_fqcn != method.fqcn.as_ref() {
1368 ea.emit(
1369 IssueKind::UndefinedMethod {
1370 class: method.fqcn.to_string(),
1371 method: method.name.to_string(),
1372 },
1373 Severity::Error,
1374 span,
1375 );
1376 }
1377 }
1378 Visibility::Protected => {
1379 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1381 if caller_fqcn.is_empty() {
1382 ea.emit(
1384 IssueKind::UndefinedMethod {
1385 class: method.fqcn.to_string(),
1386 method: method.name.to_string(),
1387 },
1388 Severity::Error,
1389 span,
1390 );
1391 } else {
1392 let allowed = caller_fqcn == method.fqcn.as_ref()
1394 || ea
1395 .codebase
1396 .extends_or_implements(caller_fqcn, method.fqcn.as_ref());
1397 if !allowed {
1398 ea.emit(
1399 IssueKind::UndefinedMethod {
1400 class: method.fqcn.to_string(),
1401 method: method.name.to_string(),
1402 },
1403 Severity::Error,
1404 span,
1405 );
1406 }
1407 }
1408 }
1409 Visibility::Public => {}
1410 }
1411}
1412
1413fn resolve_static_class(name: &str, ctx: &Context) -> String {
1414 match name.to_lowercase().as_str() {
1415 "self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
1416 "parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
1417 "static" => ctx
1418 .static_fqcn
1419 .as_deref()
1420 .unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
1421 .to_string(),
1422 _ => name.to_string(),
1423 }
1424}