1use std::sync::Arc;
4
5use php_ast::ast::{
6 ExprKind, FunctionCallExpr, MethodCallExpr, StaticDynMethodCallExpr, StaticMethodCallExpr,
7};
8use php_ast::Span;
9
10use mir_codebase::storage::{FnParam, MethodStorage, Visibility};
11use mir_issues::{IssueKind, Severity};
12use mir_types::{Atomic, Union};
13
14use crate::context::Context;
15use crate::expr::ExpressionAnalyzer;
16use crate::generic::{build_class_bindings, check_template_bounds, infer_template_bindings};
17use crate::symbol::SymbolKind;
18use crate::taint::{classify_sink, is_expr_tainted, SinkKind};
19
20pub struct CallAnalyzer;
25
26impl CallAnalyzer {
27 pub fn analyze_function_call<'a, 'arena, 'src>(
32 ea: &mut ExpressionAnalyzer<'a>,
33 call: &FunctionCallExpr<'arena, 'src>,
34 ctx: &mut Context,
35 span: Span,
36 ) -> Union {
37 let fn_name = match &call.name.kind {
39 ExprKind::Identifier(name) => (*name).to_string(),
40 _ => {
41 ea.analyze(call.name, ctx);
43 for arg in call.args.iter() {
44 ea.analyze(&arg.value, ctx);
45 }
46 return Union::mixed();
47 }
48 };
49
50 if let Some(sink_kind) = classify_sink(&fn_name) {
52 for arg in call.args.iter() {
53 if is_expr_tainted(&arg.value, ctx) {
54 let issue_kind = match sink_kind {
55 SinkKind::Html => IssueKind::TaintedHtml,
56 SinkKind::Sql => IssueKind::TaintedSql,
57 SinkKind::Shell => IssueKind::TaintedShell,
58 };
59 ea.emit(issue_kind, Severity::Error, span);
60 break; }
62 }
63 }
64
65 let fn_name = fn_name
69 .strip_prefix('\\')
70 .map(|s: &str| s.to_string())
71 .unwrap_or(fn_name);
72 let resolved_fn_name: String = {
73 let qualified = ea.codebase.resolve_class_name(&ea.file, &fn_name);
74 if ea.codebase.functions.contains_key(qualified.as_str()) {
75 qualified
76 } else if ea.codebase.functions.contains_key(fn_name.as_str()) {
77 fn_name.clone()
78 } else {
79 qualified
81 }
82 };
83
84 if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
88 for (i, param) in func.params.iter().enumerate() {
89 if param.is_byref {
90 if param.is_variadic {
91 for arg in call.args.iter().skip(i) {
93 if let ExprKind::Variable(name) = &arg.value.kind {
94 let var_name = name.as_str().trim_start_matches('$');
95 if !ctx.var_is_defined(var_name) {
96 ctx.set_var(var_name, Union::mixed());
97 }
98 }
99 }
100 } else if let Some(arg) = call.args.get(i) {
101 if let ExprKind::Variable(name) = &arg.value.kind {
102 let var_name = name.as_str().trim_start_matches('$');
103 if !ctx.var_is_defined(var_name) {
104 ctx.set_var(var_name, Union::mixed());
105 }
106 }
107 }
108 }
109 }
110 }
111
112 let arg_types: Vec<Union> = call
114 .args
115 .iter()
116 .map(|arg| {
117 let ty = ea.analyze(&arg.value, ctx);
118 if arg.unpack {
119 spread_element_type(&ty)
120 } else {
121 ty
122 }
123 })
124 .collect();
125
126 if let Some(func) = ea.codebase.functions.get(resolved_fn_name.as_str()) {
128 let name_span = call.name.span;
131 ea.codebase.mark_function_referenced_at(
132 &func.fqn,
133 ea.file.clone(),
134 name_span.start,
135 name_span.end,
136 );
137 let deprecated = func.deprecated.clone();
138 let params = func.params.clone();
139 let template_params = func.template_params.clone();
140 let return_ty_raw = func
141 .effective_return_type()
142 .cloned()
143 .unwrap_or_else(Union::mixed);
144
145 if let Some(msg) = deprecated {
147 ea.emit(
148 IssueKind::DeprecatedCall {
149 name: resolved_fn_name.clone(),
150 message: Some(msg).filter(|m| !m.is_empty()),
151 },
152 Severity::Info,
153 span,
154 );
155 }
156
157 check_args(
158 ea,
159 CheckArgsParams {
160 fn_name: &fn_name,
161 params: ¶ms,
162 arg_types: &arg_types,
163 arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
164 arg_names: &call
165 .args
166 .iter()
167 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
168 .collect::<Vec<_>>(),
169 call_span: span,
170 has_spread: call.args.iter().any(|a| a.unpack),
171 },
172 );
173
174 for (i, param) in params.iter().enumerate() {
176 if param.is_byref {
177 if param.is_variadic {
178 for arg in call.args.iter().skip(i) {
179 if let ExprKind::Variable(name) = &arg.value.kind {
180 let var_name = name.as_str().trim_start_matches('$');
181 ctx.set_var(var_name, Union::mixed());
182 }
183 }
184 } else if let Some(arg) = call.args.get(i) {
185 if let ExprKind::Variable(name) = &arg.value.kind {
186 let var_name = name.as_str().trim_start_matches('$');
187 ctx.set_var(var_name, Union::mixed());
188 }
189 }
190 }
191 }
192
193 let return_ty = if !template_params.is_empty() {
195 let bindings = infer_template_bindings(&template_params, ¶ms, &arg_types);
196 for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
198 ea.emit(
199 IssueKind::InvalidTemplateParam {
200 name: name.to_string(),
201 expected_bound: format!("{}", bound),
202 actual: format!("{}", inferred),
203 },
204 Severity::Error,
205 span,
206 );
207 }
208 return_ty_raw.substitute_templates(&bindings)
209 } else {
210 return_ty_raw
211 };
212
213 ea.record_symbol(
214 call.name.span,
215 SymbolKind::FunctionCall(func.fqn.clone()),
216 return_ty.clone(),
217 );
218 return return_ty;
219 }
220
221 ea.emit(
223 IssueKind::UndefinedFunction { name: fn_name },
224 Severity::Error,
225 span,
226 );
227 Union::mixed()
228 }
229
230 pub fn analyze_method_call<'a, 'arena, 'src>(
235 ea: &mut ExpressionAnalyzer<'a>,
236 call: &MethodCallExpr<'arena, 'src>,
237 ctx: &mut Context,
238 span: Span,
239 nullsafe: bool,
240 ) -> Union {
241 let obj_ty = ea.analyze(call.object, ctx);
242
243 let method_name = match &call.method.kind {
244 ExprKind::Identifier(name) | ExprKind::Variable(name) => name.as_str(),
245 _ => return Union::mixed(),
246 };
247
248 let arg_types: Vec<Union> = call
252 .args
253 .iter()
254 .map(|arg| {
255 let ty = ea.analyze(&arg.value, ctx);
256 if arg.unpack {
257 spread_element_type(&ty)
258 } else {
259 ty
260 }
261 })
262 .collect();
263
264 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
265
266 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) {
268 if nullsafe {
269 } else if obj_ty.is_single() {
271 ea.emit(
272 IssueKind::NullMethodCall {
273 method: method_name.to_string(),
274 },
275 Severity::Error,
276 span,
277 );
278 return Union::mixed();
279 } else {
280 ea.emit(
281 IssueKind::PossiblyNullMethodCall {
282 method: method_name.to_string(),
283 },
284 Severity::Info,
285 span,
286 );
287 }
288 }
289
290 if obj_ty.is_mixed() {
292 ea.emit(
293 IssueKind::MixedMethodCall {
294 method: method_name.to_string(),
295 },
296 Severity::Info,
297 span,
298 );
299 return Union::mixed();
300 }
301
302 let receiver = obj_ty.remove_null();
303 let mut result = Union::empty();
304
305 for atomic in &receiver.types {
306 match atomic {
307 Atomic::TNamedObject {
308 fqcn,
309 type_params: receiver_type_params,
310 } => {
311 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
313 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
314 if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
315 ea.codebase.mark_method_referenced_at(
319 fqcn,
320 method_name,
321 ea.file.clone(),
322 call.method.span.start,
323 call.method.span.end,
324 );
325 if let Some(msg) = method.deprecated.clone() {
327 ea.emit(
328 IssueKind::DeprecatedMethodCall {
329 class: fqcn.to_string(),
330 method: method_name.to_string(),
331 message: Some(msg).filter(|m| !m.is_empty()),
332 },
333 Severity::Info,
334 span,
335 );
336 }
337 check_method_visibility(ea, &method, ctx, span);
339
340 let arg_names: Vec<Option<String>> = call
342 .args
343 .iter()
344 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
345 .collect();
346 check_args(
347 ea,
348 CheckArgsParams {
349 fn_name: method_name,
350 params: &method.params,
351 arg_types: &arg_types,
352 arg_spans: &arg_spans,
353 arg_names: &arg_names,
354 call_span: span,
355 has_spread: call.args.iter().any(|a| a.unpack),
356 },
357 );
358
359 let ret_raw = method
360 .effective_return_type()
361 .cloned()
362 .unwrap_or_else(Union::mixed);
363 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
365
366 let class_tps = ea.codebase.get_class_template_params(fqcn);
368 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
369 for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
371 bindings.entry(k).or_insert(v);
372 }
373
374 if !method.template_params.is_empty() {
376 let method_bindings = infer_template_bindings(
377 &method.template_params,
378 &method.params,
379 &arg_types,
380 );
381 for key in method_bindings.keys() {
382 if bindings.contains_key(key) {
383 ea.emit(
384 IssueKind::ShadowedTemplateParam {
385 name: key.to_string(),
386 },
387 Severity::Info,
388 span,
389 );
390 }
391 }
392 bindings.extend(method_bindings);
393 for (name, inferred, bound) in
394 check_template_bounds(&bindings, &method.template_params)
395 {
396 ea.emit(
397 IssueKind::InvalidTemplateParam {
398 name: name.to_string(),
399 expected_bound: format!("{}", bound),
400 actual: format!("{}", inferred),
401 },
402 Severity::Error,
403 span,
404 );
405 }
406 }
407
408 let ret = if !bindings.is_empty() {
409 ret_raw.substitute_templates(&bindings)
410 } else {
411 ret_raw
412 };
413 result = Union::merge(&result, &ret);
414 } else if ea.codebase.type_exists(fqcn)
415 && !ea.codebase.has_unknown_ancestor(fqcn)
416 {
417 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
424 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
425 if is_interface
426 || is_abstract
427 || ea.codebase.get_method(fqcn, "__call").is_some()
428 {
429 result = Union::merge(&result, &Union::mixed());
430 } else {
431 ea.emit(
432 IssueKind::UndefinedMethod {
433 class: fqcn.to_string(),
434 method: method_name.to_string(),
435 },
436 Severity::Error,
437 span,
438 );
439 result = Union::merge(&result, &Union::mixed());
440 }
441 } else {
442 result = Union::merge(&result, &Union::mixed());
443 }
444 }
445 Atomic::TSelf { fqcn }
446 | Atomic::TStaticObject { fqcn }
447 | Atomic::TParent { fqcn } => {
448 let receiver_type_params: &[mir_types::Union] = &[];
449 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
451 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
452 if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
453 ea.codebase.mark_method_referenced_at(
457 fqcn,
458 method_name,
459 ea.file.clone(),
460 call.method.span.start,
461 call.method.span.end,
462 );
463 if let Some(msg) = method.deprecated.clone() {
465 ea.emit(
466 IssueKind::DeprecatedMethodCall {
467 class: fqcn.to_string(),
468 method: method_name.to_string(),
469 message: Some(msg).filter(|m| !m.is_empty()),
470 },
471 Severity::Info,
472 span,
473 );
474 }
475 check_method_visibility(ea, &method, ctx, span);
477
478 let arg_names: Vec<Option<String>> = call
480 .args
481 .iter()
482 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
483 .collect();
484 check_args(
485 ea,
486 CheckArgsParams {
487 fn_name: method_name,
488 params: &method.params,
489 arg_types: &arg_types,
490 arg_spans: &arg_spans,
491 arg_names: &arg_names,
492 call_span: span,
493 has_spread: call.args.iter().any(|a| a.unpack),
494 },
495 );
496
497 let ret_raw = method
498 .effective_return_type()
499 .cloned()
500 .unwrap_or_else(Union::mixed);
501 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
503
504 let class_tps = ea.codebase.get_class_template_params(fqcn);
506 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
507 for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
509 bindings.entry(k).or_insert(v);
510 }
511
512 if !method.template_params.is_empty() {
514 let method_bindings = infer_template_bindings(
515 &method.template_params,
516 &method.params,
517 &arg_types,
518 );
519 for key in method_bindings.keys() {
520 if bindings.contains_key(key) {
521 ea.emit(
522 IssueKind::ShadowedTemplateParam {
523 name: key.to_string(),
524 },
525 Severity::Info,
526 span,
527 );
528 }
529 }
530 bindings.extend(method_bindings);
531 for (name, inferred, bound) in
532 check_template_bounds(&bindings, &method.template_params)
533 {
534 ea.emit(
535 IssueKind::InvalidTemplateParam {
536 name: name.to_string(),
537 expected_bound: format!("{}", bound),
538 actual: format!("{}", inferred),
539 },
540 Severity::Error,
541 span,
542 );
543 }
544 }
545
546 let ret = if !bindings.is_empty() {
547 ret_raw.substitute_templates(&bindings)
548 } else {
549 ret_raw
550 };
551 result = Union::merge(&result, &ret);
552 } else if ea.codebase.type_exists(fqcn)
553 && !ea.codebase.has_unknown_ancestor(fqcn)
554 {
555 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
562 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
563 if is_interface
564 || is_abstract
565 || ea.codebase.get_method(fqcn, "__call").is_some()
566 {
567 result = Union::merge(&result, &Union::mixed());
568 } else {
569 ea.emit(
570 IssueKind::UndefinedMethod {
571 class: fqcn.to_string(),
572 method: method_name.to_string(),
573 },
574 Severity::Error,
575 span,
576 );
577 result = Union::merge(&result, &Union::mixed());
578 }
579 } else {
580 result = Union::merge(&result, &Union::mixed());
581 }
582 }
583 Atomic::TObject => {
584 result = Union::merge(&result, &Union::mixed());
585 }
586 Atomic::TTemplateParam { .. } => {
590 result = Union::merge(&result, &Union::mixed());
591 }
592 _ => {
593 result = Union::merge(&result, &Union::mixed());
594 }
595 }
596 }
597
598 if nullsafe && obj_ty.is_nullable() {
599 result.add_type(Atomic::TNull);
600 }
601
602 let final_ty = if result.is_empty() {
603 Union::mixed()
604 } else {
605 result
606 };
607 for atomic in &obj_ty.types {
611 if let Atomic::TNamedObject { fqcn, .. } = atomic {
612 ea.record_symbol(
613 call.method.span,
614 SymbolKind::MethodCall {
615 class: fqcn.clone(),
616 method: Arc::from(method_name),
617 },
618 final_ty.clone(),
619 );
620 break;
621 }
622 }
623 final_ty
624 }
625
626 pub fn analyze_static_method_call<'a, 'arena, 'src>(
631 ea: &mut ExpressionAnalyzer<'a>,
632 call: &StaticMethodCallExpr<'arena, 'src>,
633 ctx: &mut Context,
634 span: Span,
635 ) -> Union {
636 let method_name = match &call.method.kind {
637 ExprKind::Identifier(name) => name.as_str(),
638 _ => return Union::mixed(),
639 };
640
641 let fqcn = match &call.class.kind {
642 ExprKind::Identifier(name) => ea.codebase.resolve_class_name(&ea.file, name.as_ref()),
643 _ => return Union::mixed(),
644 };
645
646 let fqcn = resolve_static_class(&fqcn, ctx);
647
648 let arg_types: Vec<Union> = call
649 .args
650 .iter()
651 .map(|arg| {
652 let ty = ea.analyze(&arg.value, ctx);
653 if arg.unpack {
654 spread_element_type(&ty)
655 } else {
656 ty
657 }
658 })
659 .collect();
660 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
661
662 if let Some(method) = ea.codebase.get_method(&fqcn, method_name) {
663 let method_span = call.method.span;
664 ea.codebase.mark_method_referenced_at(
665 &fqcn,
666 method_name,
667 ea.file.clone(),
668 method_span.start,
669 method_span.end,
670 );
671 if let Some(msg) = method.deprecated.clone() {
673 ea.emit(
674 IssueKind::DeprecatedMethodCall {
675 class: fqcn.clone(),
676 method: method_name.to_string(),
677 message: Some(msg).filter(|m| !m.is_empty()),
678 },
679 Severity::Info,
680 span,
681 );
682 }
683 let arg_names: Vec<Option<String>> = call
684 .args
685 .iter()
686 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
687 .collect();
688 check_args(
689 ea,
690 CheckArgsParams {
691 fn_name: method_name,
692 params: &method.params,
693 arg_types: &arg_types,
694 arg_spans: &arg_spans,
695 arg_names: &arg_names,
696 call_span: span,
697 has_spread: call.args.iter().any(|a| a.unpack),
698 },
699 );
700 let ret_raw = method
701 .effective_return_type()
702 .cloned()
703 .unwrap_or_else(Union::mixed);
704 let fqcn_arc: std::sync::Arc<str> = Arc::from(fqcn.as_str());
705 let ret = substitute_static_in_return(ret_raw, &fqcn_arc);
706 ea.record_symbol(
707 method_span,
708 SymbolKind::StaticCall {
709 class: fqcn_arc,
710 method: Arc::from(method_name),
711 },
712 ret.clone(),
713 );
714 ret
715 } else if ea.codebase.type_exists(&fqcn) && !ea.codebase.has_unknown_ancestor(&fqcn) {
716 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_str());
720 let is_abstract = ea.codebase.is_abstract_class(&fqcn);
721 if is_interface || is_abstract || ea.codebase.get_method(&fqcn, "__call").is_some() {
722 Union::mixed()
723 } else {
724 ea.emit(
725 IssueKind::UndefinedMethod {
726 class: fqcn,
727 method: method_name.to_string(),
728 },
729 Severity::Error,
730 span,
731 );
732 Union::mixed()
733 }
734 } else if !ea.codebase.type_exists(&fqcn)
735 && !matches!(fqcn.as_str(), "self" | "static" | "parent")
736 {
737 ea.emit(
738 IssueKind::UndefinedClass { name: fqcn },
739 Severity::Error,
740 call.class.span,
741 );
742 Union::mixed()
743 } else {
744 Union::mixed()
746 }
747 }
748
749 pub fn analyze_static_dyn_method_call<'a, 'arena, 'src>(
754 ea: &mut ExpressionAnalyzer<'a>,
755 call: &StaticDynMethodCallExpr<'arena, 'src>,
756 ctx: &mut Context,
757 ) -> Union {
758 for arg in call.args.iter() {
760 ea.analyze(&arg.value, ctx);
761 }
762 Union::mixed()
763 }
764}
765
766pub struct CheckArgsParams<'a> {
771 pub fn_name: &'a str,
772 pub params: &'a [FnParam],
773 pub arg_types: &'a [Union],
774 pub arg_spans: &'a [Span],
775 pub arg_names: &'a [Option<String>],
776 pub call_span: Span,
777 pub has_spread: bool,
778}
779
780pub fn check_constructor_args(
781 ea: &mut ExpressionAnalyzer<'_>,
782 class_name: &str,
783 p: CheckArgsParams<'_>,
784) {
785 let ctor_name = format!("{}::__construct", class_name);
786 check_args(
787 ea,
788 CheckArgsParams {
789 fn_name: &ctor_name,
790 ..p
791 },
792 );
793}
794
795fn check_args(ea: &mut ExpressionAnalyzer<'_>, p: CheckArgsParams<'_>) {
800 let CheckArgsParams {
801 fn_name,
802 params,
803 arg_types,
804 arg_spans,
805 arg_names,
806 call_span,
807 has_spread,
808 } = p;
809 let has_named = arg_names.iter().any(|n| n.is_some());
812
813 let mut param_to_arg: Vec<Option<(Union, Span)>> = vec![None; params.len()];
815
816 if has_named {
817 let mut positional = 0usize;
818 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
819 if let Some(Some(name)) = arg_names.get(i) {
820 if let Some(pi) = params.iter().position(|p| p.name.as_ref() == name.as_str()) {
822 param_to_arg[pi] = Some((ty.clone(), *span));
823 }
824 } else {
825 while positional < params.len() && param_to_arg[positional].is_some() {
827 positional += 1;
828 }
829 if positional < params.len() {
830 param_to_arg[positional] = Some((ty.clone(), *span));
831 positional += 1;
832 }
833 }
834 }
835 } else {
836 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
838 if i < params.len() {
839 param_to_arg[i] = Some((ty.clone(), *span));
840 }
841 }
842 }
843
844 let required_count = params
845 .iter()
846 .filter(|p| !p.is_optional && !p.is_variadic)
847 .count();
848 let provided_count = if params.iter().any(|p| p.is_variadic) {
849 arg_types.len()
850 } else {
851 arg_types.len().min(params.len())
852 };
853
854 if provided_count < required_count && !has_spread {
855 ea.emit(
856 IssueKind::InvalidArgument {
857 param: format!("#{}", provided_count + 1),
858 fn_name: fn_name.to_string(),
859 expected: format!("{} argument(s)", required_count),
860 actual: format!("{} provided", provided_count),
861 },
862 Severity::Error,
863 call_span,
864 );
865 return;
866 }
867
868 for (param, slot) in params.iter().zip(param_to_arg.iter()) {
869 let (arg_ty, arg_span) = match slot {
870 Some(pair) => pair,
871 None => continue, };
873 let arg_span = *arg_span;
874
875 if let Some(raw_param_ty) = ¶m.ty {
876 let param_ty_owned;
878 let param_ty: &Union = if param.is_variadic {
879 if let Some(elem_ty) = raw_param_ty.types.iter().find_map(|a| match a {
880 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
881 Some(*value.clone())
882 }
883 _ => None,
884 }) {
885 param_ty_owned = elem_ty;
886 ¶m_ty_owned
887 } else {
888 raw_param_ty
889 }
890 } else {
891 raw_param_ty
892 };
893 if !param_ty.is_nullable()
897 && !param_ty.is_mixed()
898 && arg_ty.is_single()
899 && arg_ty.contains(|t| matches!(t, Atomic::TNull))
900 {
901 ea.emit(
902 IssueKind::InvalidArgument {
903 param: param.name.to_string(),
904 fn_name: fn_name.to_string(),
905 expected: format!("{}", param_ty),
906 actual: format!("{}", arg_ty),
907 },
908 Severity::Error,
909 arg_span,
910 );
911 } else if !param_ty.is_nullable() && !param_ty.is_mixed() && arg_ty.is_nullable() {
912 ea.emit(
913 IssueKind::PossiblyNullArgument {
914 param: param.name.to_string(),
915 fn_name: fn_name.to_string(),
916 },
917 Severity::Info,
918 arg_span,
919 );
920 }
921
922 if !arg_ty.is_subtype_of_simple(param_ty)
925 && !param_ty.is_mixed()
926 && !arg_ty.is_mixed()
927 && !named_object_subtype(arg_ty, param_ty, ea)
928 && !param_contains_template_or_unknown(param_ty, ea)
929 && !param_contains_template_or_unknown(arg_ty, ea)
930 && !array_list_compatible(arg_ty, param_ty, ea)
931 && !(arg_ty.is_single() && param_ty.is_subtype_of_simple(arg_ty))
936 && !(arg_ty.is_single() && param_ty.remove_null().is_subtype_of_simple(arg_ty))
938 && !(arg_ty.is_single() && param_ty.types.iter().any(|p| Union::single(p.clone()).is_subtype_of_simple(arg_ty)))
940 && !arg_ty.remove_null().is_subtype_of_simple(param_ty)
943 && !arg_ty.remove_false().is_subtype_of_simple(param_ty)
944 && !named_object_subtype(&arg_ty.remove_null(), param_ty, ea)
945 && !named_object_subtype(&arg_ty.remove_false(), param_ty, ea)
946 {
947 ea.emit(
948 IssueKind::InvalidArgument {
949 param: param.name.to_string(),
950 fn_name: fn_name.to_string(),
951 expected: format!("{}", param_ty),
952 actual: format!("{}", arg_ty),
953 },
954 Severity::Error,
955 arg_span,
956 );
957 }
958 }
959 }
960}
961
962fn named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
969 use mir_types::Atomic;
970 arg.types.iter().all(|a_atomic| {
972 let arg_fqcn: &Arc<str> = match a_atomic {
974 Atomic::TNamedObject { fqcn, .. } => fqcn,
975 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => {
976 if ea.codebase.traits.contains_key(fqcn.as_ref()) {
978 return true;
979 }
980 fqcn
981 }
982 Atomic::TParent { fqcn } => fqcn,
983 Atomic::TNever => return true,
985 Atomic::TClosure { .. } => {
987 return param.types.iter().any(|p| match p {
988 Atomic::TClosure { .. } | Atomic::TCallable { .. } => true,
989 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
990 _ => false,
991 });
992 }
993 Atomic::TCallable { .. } => {
995 return param.types.iter().any(|p| match p {
996 Atomic::TCallable { .. } | Atomic::TClosure { .. } => true,
997 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
998 _ => false,
999 });
1000 }
1001 Atomic::TClassString(Some(arg_cls)) => {
1003 return param.types.iter().any(|p| match p {
1004 Atomic::TClassString(None) | Atomic::TString => true,
1005 Atomic::TClassString(Some(param_cls)) => {
1006 arg_cls == param_cls
1007 || ea
1008 .codebase
1009 .extends_or_implements(arg_cls.as_ref(), param_cls.as_ref())
1010 }
1011 _ => false,
1012 });
1013 }
1014 Atomic::TNull => {
1016 return param.types.iter().any(|p| matches!(p, Atomic::TNull));
1017 }
1018 Atomic::TFalse => {
1020 return param
1021 .types
1022 .iter()
1023 .any(|p| matches!(p, Atomic::TFalse | Atomic::TBool));
1024 }
1025 _ => return false, };
1027
1028 if param
1030 .types
1031 .iter()
1032 .any(|p| matches!(p, Atomic::TCallable { .. }))
1033 {
1034 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1035 if ea.codebase.get_method(&resolved_arg, "__invoke").is_some()
1036 || ea
1037 .codebase
1038 .get_method(arg_fqcn.as_ref(), "__invoke")
1039 .is_some()
1040 {
1041 return true;
1042 }
1043 }
1044
1045 param.types.iter().any(|p_atomic| {
1046 let param_fqcn: &Arc<str> = match p_atomic {
1047 Atomic::TNamedObject { fqcn, .. } => fqcn,
1048 Atomic::TSelf { fqcn } => fqcn,
1049 Atomic::TStaticObject { fqcn } => fqcn,
1050 Atomic::TParent { fqcn } => fqcn,
1051 _ => return false,
1052 };
1053 let resolved_param = ea
1055 .codebase
1056 .resolve_class_name(&ea.file, param_fqcn.as_ref());
1057 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1058
1059 let is_same_class = resolved_param == resolved_arg
1061 || arg_fqcn.as_ref() == resolved_param.as_str()
1062 || resolved_arg == param_fqcn.as_ref();
1063
1064 if is_same_class {
1065 let arg_type_params = match a_atomic {
1066 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1067 _ => &[],
1068 };
1069 let param_type_params = match p_atomic {
1070 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1071 _ => &[],
1072 };
1073 if !arg_type_params.is_empty() || !param_type_params.is_empty() {
1074 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
1075 return generic_type_params_compatible(
1076 arg_type_params,
1077 param_type_params,
1078 &class_tps,
1079 ea,
1080 );
1081 }
1082 return true;
1083 }
1084
1085 if ea.codebase.extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1086 || ea.codebase.extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1087 || ea.codebase.extends_or_implements(&resolved_arg, &resolved_param)
1088 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), &resolved_arg)
1091 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), arg_fqcn.as_ref())
1092 || ea.codebase.extends_or_implements(&resolved_param, &resolved_arg)
1093 {
1094 return true;
1095 }
1096
1097 if !arg_fqcn.contains('\\') && !ea.codebase.type_exists(&resolved_arg) {
1102 for entry in ea.codebase.classes.iter() {
1103 if entry.value().short_name.as_ref() == arg_fqcn.as_ref() {
1104 let actual_fqcn = entry.key().clone();
1105 if ea
1106 .codebase
1107 .extends_or_implements(actual_fqcn.as_ref(), &resolved_param)
1108 || ea
1109 .codebase
1110 .extends_or_implements(actual_fqcn.as_ref(), param_fqcn.as_ref())
1111 {
1112 return true;
1113 }
1114 }
1115 }
1116 }
1117
1118 let iface_key = if ea.codebase.interfaces.contains_key(arg_fqcn.as_ref()) {
1122 Some(arg_fqcn.as_ref())
1123 } else if ea.codebase.interfaces.contains_key(resolved_arg.as_str()) {
1124 Some(resolved_arg.as_str())
1125 } else {
1126 None
1127 };
1128 if let Some(iface_fqcn) = iface_key {
1129 let compatible = ea.codebase.classes.iter().any(|entry| {
1130 let cls = entry.value();
1131 cls.all_parents.iter().any(|p| p.as_ref() == iface_fqcn)
1132 && (ea
1133 .codebase
1134 .extends_or_implements(entry.key().as_ref(), param_fqcn.as_ref())
1135 || ea
1136 .codebase
1137 .extends_or_implements(entry.key().as_ref(), &resolved_param))
1138 });
1139 if compatible {
1140 return true;
1141 }
1142 }
1143
1144 if arg_fqcn.contains('\\')
1147 && !ea.codebase.type_exists(arg_fqcn.as_ref())
1148 && !ea.codebase.type_exists(&resolved_arg)
1149 {
1150 return true;
1151 }
1152
1153 if param_fqcn.contains('\\')
1156 && !ea.codebase.type_exists(param_fqcn.as_ref())
1157 && !ea.codebase.type_exists(&resolved_param)
1158 {
1159 return true;
1160 }
1161
1162 false
1163 })
1164 })
1165}
1166
1167fn strict_named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1174 use mir_types::Atomic;
1175 arg.types.iter().all(|a_atomic| {
1176 let arg_fqcn: &Arc<str> = match a_atomic {
1177 Atomic::TNamedObject { fqcn, .. } => fqcn,
1178 Atomic::TNever => return true,
1179 _ => return false,
1180 };
1181 param.types.iter().any(|p_atomic| {
1182 let param_fqcn: &Arc<str> = match p_atomic {
1183 Atomic::TNamedObject { fqcn, .. } => fqcn,
1184 _ => return false,
1185 };
1186 let resolved_param = ea
1187 .codebase
1188 .resolve_class_name(&ea.file, param_fqcn.as_ref());
1189 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1190 resolved_param == resolved_arg
1192 || arg_fqcn.as_ref() == resolved_param.as_str()
1193 || resolved_arg == param_fqcn.as_ref()
1194 || ea
1195 .codebase
1196 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1197 || ea
1198 .codebase
1199 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1200 || ea
1201 .codebase
1202 .extends_or_implements(&resolved_arg, &resolved_param)
1203 })
1204 })
1205}
1206
1207fn generic_type_params_compatible(
1214 arg_params: &[Union],
1215 param_params: &[Union],
1216 template_params: &[mir_codebase::storage::TemplateParam],
1217 ea: &ExpressionAnalyzer<'_>,
1218) -> bool {
1219 if arg_params.len() != param_params.len() {
1221 return true;
1222 }
1223 if arg_params.is_empty() {
1225 return true;
1226 }
1227
1228 for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
1229 let variance = template_params
1230 .get(i)
1231 .map(|tp| tp.variance)
1232 .unwrap_or(mir_types::Variance::Invariant);
1233
1234 let compatible = match variance {
1235 mir_types::Variance::Covariant => {
1236 arg_p.is_subtype_of_simple(param_p)
1238 || param_p.is_mixed()
1239 || arg_p.is_mixed()
1240 || strict_named_object_subtype(arg_p, param_p, ea)
1241 }
1242 mir_types::Variance::Contravariant => {
1243 param_p.is_subtype_of_simple(arg_p)
1245 || arg_p.is_mixed()
1246 || param_p.is_mixed()
1247 || strict_named_object_subtype(param_p, arg_p, ea)
1248 }
1249 mir_types::Variance::Invariant => {
1250 arg_p == param_p
1252 || arg_p.is_mixed()
1253 || param_p.is_mixed()
1254 || (arg_p.is_subtype_of_simple(param_p) && param_p.is_subtype_of_simple(arg_p))
1255 }
1256 };
1257
1258 if !compatible {
1259 return false;
1260 }
1261 }
1262
1263 true
1264}
1265
1266fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1270 param_ty.types.iter().any(|atomic| match atomic {
1271 Atomic::TTemplateParam { .. } => true,
1272 Atomic::TNamedObject { fqcn, .. } => {
1273 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1274 }
1275 Atomic::TClassString(Some(inner)) => {
1277 !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
1278 }
1279 Atomic::TArray { key: _, value }
1280 | Atomic::TList { value }
1281 | Atomic::TNonEmptyArray { key: _, value }
1282 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1283 Atomic::TTemplateParam { .. } => true,
1284 Atomic::TNamedObject { fqcn, .. } => {
1285 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1286 }
1287 _ => false,
1288 }),
1289 _ => false,
1290 })
1291}
1292
1293fn substitute_static_in_return(ret: Union, receiver_fqcn: &Arc<str>) -> Union {
1296 use mir_types::Atomic;
1297 let from_docblock = ret.from_docblock;
1298 let types: Vec<Atomic> = ret
1299 .types
1300 .into_iter()
1301 .map(|a| match a {
1302 Atomic::TStaticObject { .. } | Atomic::TSelf { .. } => Atomic::TNamedObject {
1303 fqcn: receiver_fqcn.clone(),
1304 type_params: vec![],
1305 },
1306 other => other,
1307 })
1308 .collect();
1309 let mut result = Union::from_vec(types);
1310 result.from_docblock = from_docblock;
1311 result
1312}
1313
1314pub fn spread_element_type(arr_ty: &Union) -> Union {
1318 use mir_types::Atomic;
1319 let mut result = Union::empty();
1320 for atomic in arr_ty.types.iter() {
1321 match atomic {
1322 Atomic::TArray { value, .. }
1323 | Atomic::TNonEmptyArray { value, .. }
1324 | Atomic::TList { value }
1325 | Atomic::TNonEmptyList { value } => {
1326 for t in value.types.iter() {
1327 result.add_type(t.clone());
1328 }
1329 }
1330 Atomic::TKeyedArray { properties, .. } => {
1331 for (_key, prop) in properties.iter() {
1332 for t in prop.ty.types.iter() {
1333 result.add_type(t.clone());
1334 }
1335 }
1336 }
1337 _ => return Union::mixed(),
1339 }
1340 }
1341 if result.types.is_empty() {
1342 Union::mixed()
1343 } else {
1344 result
1345 }
1346}
1347
1348fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1354 arg_ty.types.iter().all(|av| {
1355 let av_fqcn: &Arc<str> = match av {
1357 Atomic::TNamedObject { fqcn, .. } => fqcn,
1358 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
1359 fqcn
1360 }
1361 Atomic::TArray { value, .. }
1363 | Atomic::TNonEmptyArray { value, .. }
1364 | Atomic::TList { value }
1365 | Atomic::TNonEmptyList { value } => {
1366 return param_ty.types.iter().any(|pv| {
1367 let pv_val: &Union = match pv {
1368 Atomic::TArray { value, .. }
1369 | Atomic::TNonEmptyArray { value, .. }
1370 | Atomic::TList { value }
1371 | Atomic::TNonEmptyList { value } => value,
1372 _ => return false,
1373 };
1374 union_compatible(value, pv_val, ea)
1375 });
1376 }
1377 Atomic::TKeyedArray { .. } => return true,
1378 _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
1379 };
1380
1381 param_ty.types.iter().any(|pv| {
1382 let pv_fqcn: &Arc<str> = match pv {
1383 Atomic::TNamedObject { fqcn, .. } => fqcn,
1384 Atomic::TSelf { fqcn }
1385 | Atomic::TStaticObject { fqcn }
1386 | Atomic::TParent { fqcn } => fqcn,
1387 _ => return false,
1388 };
1389 if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
1391 return true;
1392 }
1393 let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
1394 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
1395 resolved_param == resolved_arg
1396 || ea
1397 .codebase
1398 .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
1399 || ea
1400 .codebase
1401 .extends_or_implements(&resolved_arg, &resolved_param)
1402 || ea
1403 .codebase
1404 .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
1405 || ea
1406 .codebase
1407 .extends_or_implements(&resolved_param, &resolved_arg)
1408 })
1409 })
1410}
1411
1412fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1413 arg_ty.types.iter().all(|a_atomic| {
1414 let arg_value: &Union = match a_atomic {
1415 Atomic::TArray { value, .. }
1416 | Atomic::TNonEmptyArray { value, .. }
1417 | Atomic::TList { value }
1418 | Atomic::TNonEmptyList { value } => value,
1419 Atomic::TKeyedArray { .. } => return true, _ => return false,
1421 };
1422
1423 param_ty.types.iter().any(|p_atomic| {
1424 let param_value: &Union = match p_atomic {
1425 Atomic::TArray { value, .. }
1426 | Atomic::TNonEmptyArray { value, .. }
1427 | Atomic::TList { value }
1428 | Atomic::TNonEmptyList { value } => value,
1429 _ => return false,
1430 };
1431
1432 union_compatible(arg_value, param_value, ea)
1433 })
1434 })
1435}
1436
1437fn check_method_visibility(
1438 ea: &mut ExpressionAnalyzer<'_>,
1439 method: &MethodStorage,
1440 ctx: &Context,
1441 span: Span,
1442) {
1443 match method.visibility {
1444 Visibility::Private => {
1445 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1447 if caller_fqcn != method.fqcn.as_ref() {
1448 ea.emit(
1449 IssueKind::UndefinedMethod {
1450 class: method.fqcn.to_string(),
1451 method: method.name.to_string(),
1452 },
1453 Severity::Error,
1454 span,
1455 );
1456 }
1457 }
1458 Visibility::Protected => {
1459 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1461 if caller_fqcn.is_empty() {
1462 ea.emit(
1464 IssueKind::UndefinedMethod {
1465 class: method.fqcn.to_string(),
1466 method: method.name.to_string(),
1467 },
1468 Severity::Error,
1469 span,
1470 );
1471 } else {
1472 let allowed = caller_fqcn == method.fqcn.as_ref()
1474 || ea
1475 .codebase
1476 .extends_or_implements(caller_fqcn, method.fqcn.as_ref());
1477 if !allowed {
1478 ea.emit(
1479 IssueKind::UndefinedMethod {
1480 class: method.fqcn.to_string(),
1481 method: method.name.to_string(),
1482 },
1483 Severity::Error,
1484 span,
1485 );
1486 }
1487 }
1488 }
1489 Visibility::Public => {}
1490 }
1491}
1492
1493fn resolve_static_class(name: &str, ctx: &Context) -> String {
1494 match name.to_lowercase().as_str() {
1495 "self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
1496 "parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
1497 "static" => ctx
1498 .static_fqcn
1499 .as_deref()
1500 .unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
1501 .to_string(),
1502 _ => name.to_string(),
1503 }
1504}