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_ref().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_ref().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_ref().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_ref().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_ref().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 if resolved_param == resolved_arg
980 || arg_fqcn.as_ref() == resolved_param.as_str()
981 || resolved_arg == param_fqcn.as_ref()
982 || ea.codebase.extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
983 || ea.codebase.extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
984 || ea.codebase.extends_or_implements(&resolved_arg, &resolved_param)
985 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), &resolved_arg)
988 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), arg_fqcn.as_ref())
989 || ea.codebase.extends_or_implements(&resolved_param, &resolved_arg)
990 {
991 return true;
992 }
993
994 if !arg_fqcn.contains('\\') && !ea.codebase.type_exists(&resolved_arg) {
999 for entry in ea.codebase.classes.iter() {
1000 if entry.value().short_name.as_ref() == arg_fqcn.as_ref() {
1001 let actual_fqcn = entry.key().clone();
1002 if ea
1003 .codebase
1004 .extends_or_implements(actual_fqcn.as_ref(), &resolved_param)
1005 || ea
1006 .codebase
1007 .extends_or_implements(actual_fqcn.as_ref(), param_fqcn.as_ref())
1008 {
1009 return true;
1010 }
1011 }
1012 }
1013 }
1014
1015 let iface_key = if ea.codebase.interfaces.contains_key(arg_fqcn.as_ref()) {
1019 Some(arg_fqcn.as_ref())
1020 } else if ea.codebase.interfaces.contains_key(resolved_arg.as_str()) {
1021 Some(resolved_arg.as_str())
1022 } else {
1023 None
1024 };
1025 if let Some(iface_fqcn) = iface_key {
1026 let compatible = ea.codebase.classes.iter().any(|entry| {
1027 let cls = entry.value();
1028 cls.all_parents.iter().any(|p| p.as_ref() == iface_fqcn)
1029 && (ea
1030 .codebase
1031 .extends_or_implements(entry.key().as_ref(), param_fqcn.as_ref())
1032 || ea
1033 .codebase
1034 .extends_or_implements(entry.key().as_ref(), &resolved_param))
1035 });
1036 if compatible {
1037 return true;
1038 }
1039 }
1040
1041 if arg_fqcn.contains('\\')
1044 && !ea.codebase.type_exists(arg_fqcn.as_ref())
1045 && !ea.codebase.type_exists(&resolved_arg)
1046 {
1047 return true;
1048 }
1049
1050 if param_fqcn.contains('\\')
1053 && !ea.codebase.type_exists(param_fqcn.as_ref())
1054 && !ea.codebase.type_exists(&resolved_param)
1055 {
1056 return true;
1057 }
1058
1059 false
1060 })
1061 })
1062}
1063
1064fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1068 param_ty.types.iter().any(|atomic| match atomic {
1069 Atomic::TTemplateParam { .. } => true,
1070 Atomic::TNamedObject { fqcn, .. } => {
1071 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1072 }
1073 Atomic::TClassString(Some(inner)) => {
1075 !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
1076 }
1077 Atomic::TArray { key: _, value }
1078 | Atomic::TList { value }
1079 | Atomic::TNonEmptyArray { key: _, value }
1080 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1081 Atomic::TTemplateParam { .. } => true,
1082 Atomic::TNamedObject { fqcn, .. } => {
1083 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1084 }
1085 _ => false,
1086 }),
1087 _ => false,
1088 })
1089}
1090
1091fn substitute_static_in_return(ret: Union, receiver_fqcn: &Arc<str>) -> Union {
1094 use mir_types::Atomic;
1095 let from_docblock = ret.from_docblock;
1096 let types: Vec<Atomic> = ret
1097 .types
1098 .into_iter()
1099 .map(|a| match a {
1100 Atomic::TStaticObject { .. } | Atomic::TSelf { .. } => Atomic::TNamedObject {
1101 fqcn: receiver_fqcn.clone(),
1102 type_params: vec![],
1103 },
1104 other => other,
1105 })
1106 .collect();
1107 let mut result = Union::from_vec(types);
1108 result.from_docblock = from_docblock;
1109 result
1110}
1111
1112pub fn spread_element_type(arr_ty: &Union) -> Union {
1116 use mir_types::Atomic;
1117 let mut result = Union::empty();
1118 for atomic in arr_ty.types.iter() {
1119 match atomic {
1120 Atomic::TArray { value, .. }
1121 | Atomic::TNonEmptyArray { value, .. }
1122 | Atomic::TList { value }
1123 | Atomic::TNonEmptyList { value } => {
1124 for t in value.types.iter() {
1125 result.add_type(t.clone());
1126 }
1127 }
1128 Atomic::TKeyedArray { properties, .. } => {
1129 for (_key, prop) in properties.iter() {
1130 for t in prop.ty.types.iter() {
1131 result.add_type(t.clone());
1132 }
1133 }
1134 }
1135 _ => return Union::mixed(),
1137 }
1138 }
1139 if result.types.is_empty() {
1140 Union::mixed()
1141 } else {
1142 result
1143 }
1144}
1145
1146fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1152 arg_ty.types.iter().all(|av| {
1153 let av_fqcn: &Arc<str> = match av {
1155 Atomic::TNamedObject { fqcn, .. } => fqcn,
1156 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
1157 fqcn
1158 }
1159 Atomic::TArray { value, .. }
1161 | Atomic::TNonEmptyArray { value, .. }
1162 | Atomic::TList { value }
1163 | Atomic::TNonEmptyList { value } => {
1164 return param_ty.types.iter().any(|pv| {
1165 let pv_val: &Union = match pv {
1166 Atomic::TArray { value, .. }
1167 | Atomic::TNonEmptyArray { value, .. }
1168 | Atomic::TList { value }
1169 | Atomic::TNonEmptyList { value } => value,
1170 _ => return false,
1171 };
1172 union_compatible(value, pv_val, ea)
1173 });
1174 }
1175 Atomic::TKeyedArray { .. } => return true,
1176 _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
1177 };
1178
1179 param_ty.types.iter().any(|pv| {
1180 let pv_fqcn: &Arc<str> = match pv {
1181 Atomic::TNamedObject { fqcn, .. } => fqcn,
1182 Atomic::TSelf { fqcn }
1183 | Atomic::TStaticObject { fqcn }
1184 | Atomic::TParent { fqcn } => fqcn,
1185 _ => return false,
1186 };
1187 if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
1189 return true;
1190 }
1191 let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
1192 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
1193 resolved_param == resolved_arg
1194 || ea
1195 .codebase
1196 .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
1197 || ea
1198 .codebase
1199 .extends_or_implements(&resolved_arg, &resolved_param)
1200 || ea
1201 .codebase
1202 .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
1203 || ea
1204 .codebase
1205 .extends_or_implements(&resolved_param, &resolved_arg)
1206 })
1207 })
1208}
1209
1210fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1211 arg_ty.types.iter().all(|a_atomic| {
1212 let arg_value: &Union = match a_atomic {
1213 Atomic::TArray { value, .. }
1214 | Atomic::TNonEmptyArray { value, .. }
1215 | Atomic::TList { value }
1216 | Atomic::TNonEmptyList { value } => value,
1217 Atomic::TKeyedArray { .. } => return true, _ => return false,
1219 };
1220
1221 param_ty.types.iter().any(|p_atomic| {
1222 let param_value: &Union = match p_atomic {
1223 Atomic::TArray { value, .. }
1224 | Atomic::TNonEmptyArray { value, .. }
1225 | Atomic::TList { value }
1226 | Atomic::TNonEmptyList { value } => value,
1227 _ => return false,
1228 };
1229
1230 union_compatible(arg_value, param_value, ea)
1231 })
1232 })
1233}
1234
1235fn check_method_visibility(
1236 ea: &mut ExpressionAnalyzer<'_>,
1237 method: &MethodStorage,
1238 ctx: &Context,
1239 span: Span,
1240) {
1241 match method.visibility {
1242 Visibility::Private => {
1243 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1245 if caller_fqcn != method.fqcn.as_ref() {
1246 ea.emit(
1247 IssueKind::UndefinedMethod {
1248 class: method.fqcn.to_string(),
1249 method: method.name.to_string(),
1250 },
1251 Severity::Error,
1252 span,
1253 );
1254 }
1255 }
1256 Visibility::Protected => {
1257 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1259 if caller_fqcn.is_empty() {
1260 ea.emit(
1262 IssueKind::UndefinedMethod {
1263 class: method.fqcn.to_string(),
1264 method: method.name.to_string(),
1265 },
1266 Severity::Error,
1267 span,
1268 );
1269 } else {
1270 let allowed = caller_fqcn == method.fqcn.as_ref()
1272 || ea
1273 .codebase
1274 .extends_or_implements(caller_fqcn, method.fqcn.as_ref());
1275 if !allowed {
1276 ea.emit(
1277 IssueKind::UndefinedMethod {
1278 class: method.fqcn.to_string(),
1279 method: method.name.to_string(),
1280 },
1281 Severity::Error,
1282 span,
1283 );
1284 }
1285 }
1286 }
1287 Visibility::Public => {}
1288 }
1289}
1290
1291fn resolve_static_class(name: &str, ctx: &Context) -> String {
1292 match name.to_lowercase().as_str() {
1293 "self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
1294 "parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
1295 "static" => ctx
1296 .static_fqcn
1297 .as_deref()
1298 .unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
1299 .to_string(),
1300 _ => name.to_string(),
1301 }
1302}