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 is_deprecated = func.is_deprecated;
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 is_deprecated {
147 ea.emit(
148 IssueKind::DeprecatedCall {
149 name: resolved_fn_name.clone(),
150 },
151 Severity::Info,
152 span,
153 );
154 }
155
156 check_args(
157 ea,
158 CheckArgsParams {
159 fn_name: &fn_name,
160 params: ¶ms,
161 arg_types: &arg_types,
162 arg_spans: &call.args.iter().map(|a| a.span).collect::<Vec<_>>(),
163 arg_names: &call
164 .args
165 .iter()
166 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
167 .collect::<Vec<_>>(),
168 call_span: span,
169 has_spread: call.args.iter().any(|a| a.unpack),
170 },
171 );
172
173 for (i, param) in params.iter().enumerate() {
175 if param.is_byref {
176 if param.is_variadic {
177 for arg in call.args.iter().skip(i) {
178 if let ExprKind::Variable(name) = &arg.value.kind {
179 let var_name = name.as_str().trim_start_matches('$');
180 ctx.set_var(var_name, Union::mixed());
181 }
182 }
183 } else if let Some(arg) = call.args.get(i) {
184 if let ExprKind::Variable(name) = &arg.value.kind {
185 let var_name = name.as_str().trim_start_matches('$');
186 ctx.set_var(var_name, Union::mixed());
187 }
188 }
189 }
190 }
191
192 let return_ty = if !template_params.is_empty() {
194 let bindings = infer_template_bindings(&template_params, ¶ms, &arg_types);
195 for (name, inferred, bound) in check_template_bounds(&bindings, &template_params) {
197 ea.emit(
198 IssueKind::InvalidTemplateParam {
199 name: name.to_string(),
200 expected_bound: format!("{}", bound),
201 actual: format!("{}", inferred),
202 },
203 Severity::Error,
204 span,
205 );
206 }
207 return_ty_raw.substitute_templates(&bindings)
208 } else {
209 return_ty_raw
210 };
211
212 ea.record_symbol(
213 call.name.span,
214 SymbolKind::FunctionCall(func.fqn.clone()),
215 return_ty.clone(),
216 );
217 return return_ty;
218 }
219
220 ea.emit(
222 IssueKind::UndefinedFunction { name: fn_name },
223 Severity::Error,
224 span,
225 );
226 Union::mixed()
227 }
228
229 pub fn analyze_method_call<'a, 'arena, 'src>(
234 ea: &mut ExpressionAnalyzer<'a>,
235 call: &MethodCallExpr<'arena, 'src>,
236 ctx: &mut Context,
237 span: Span,
238 nullsafe: bool,
239 ) -> Union {
240 let obj_ty = ea.analyze(call.object, ctx);
241
242 let method_name = match &call.method.kind {
243 ExprKind::Identifier(name) | ExprKind::Variable(name) => name.as_str(),
244 _ => return Union::mixed(),
245 };
246
247 let arg_types: Vec<Union> = call
251 .args
252 .iter()
253 .map(|arg| {
254 let ty = ea.analyze(&arg.value, ctx);
255 if arg.unpack {
256 spread_element_type(&ty)
257 } else {
258 ty
259 }
260 })
261 .collect();
262
263 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
264
265 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) {
267 if nullsafe {
268 } else if obj_ty.is_single() {
270 ea.emit(
271 IssueKind::NullMethodCall {
272 method: method_name.to_string(),
273 },
274 Severity::Error,
275 span,
276 );
277 return Union::mixed();
278 } else {
279 ea.emit(
280 IssueKind::PossiblyNullMethodCall {
281 method: method_name.to_string(),
282 },
283 Severity::Info,
284 span,
285 );
286 }
287 }
288
289 if obj_ty.is_mixed() {
291 ea.emit(
292 IssueKind::MixedMethodCall {
293 method: method_name.to_string(),
294 },
295 Severity::Info,
296 span,
297 );
298 return Union::mixed();
299 }
300
301 let receiver = obj_ty.remove_null();
302 let mut result = Union::empty();
303
304 for atomic in &receiver.types {
305 match atomic {
306 Atomic::TNamedObject {
307 fqcn,
308 type_params: receiver_type_params,
309 } => {
310 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
312 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
313 if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
314 ea.codebase.mark_method_referenced_at(
318 fqcn,
319 method_name,
320 ea.file.clone(),
321 call.method.span.start,
322 call.method.span.end,
323 );
324 if method.is_deprecated {
326 ea.emit(
327 IssueKind::DeprecatedMethodCall {
328 class: fqcn.to_string(),
329 method: method_name.to_string(),
330 },
331 Severity::Info,
332 span,
333 );
334 }
335 check_method_visibility(ea, &method, ctx, span);
337
338 let arg_names: Vec<Option<String>> = call
340 .args
341 .iter()
342 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
343 .collect();
344 check_args(
345 ea,
346 CheckArgsParams {
347 fn_name: method_name,
348 params: &method.params,
349 arg_types: &arg_types,
350 arg_spans: &arg_spans,
351 arg_names: &arg_names,
352 call_span: span,
353 has_spread: call.args.iter().any(|a| a.unpack),
354 },
355 );
356
357 let ret_raw = method
358 .effective_return_type()
359 .cloned()
360 .unwrap_or_else(Union::mixed);
361 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
363
364 let class_tps = ea.codebase.get_class_template_params(fqcn);
366 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
367
368 if !method.template_params.is_empty() {
370 let method_bindings = infer_template_bindings(
371 &method.template_params,
372 &method.params,
373 &arg_types,
374 );
375 for key in method_bindings.keys() {
376 if bindings.contains_key(key) {
377 ea.emit(
378 IssueKind::ShadowedTemplateParam {
379 name: key.to_string(),
380 },
381 Severity::Info,
382 span,
383 );
384 }
385 }
386 bindings.extend(method_bindings);
387 for (name, inferred, bound) in
388 check_template_bounds(&bindings, &method.template_params)
389 {
390 ea.emit(
391 IssueKind::InvalidTemplateParam {
392 name: name.to_string(),
393 expected_bound: format!("{}", bound),
394 actual: format!("{}", inferred),
395 },
396 Severity::Error,
397 span,
398 );
399 }
400 }
401
402 let ret = if !bindings.is_empty() {
403 ret_raw.substitute_templates(&bindings)
404 } else {
405 ret_raw
406 };
407 result = Union::merge(&result, &ret);
408 } else if ea.codebase.type_exists(fqcn)
409 && !ea.codebase.has_unknown_ancestor(fqcn)
410 {
411 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
418 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
419 if is_interface
420 || is_abstract
421 || ea.codebase.get_method(fqcn, "__call").is_some()
422 {
423 result = Union::merge(&result, &Union::mixed());
424 } else {
425 ea.emit(
426 IssueKind::UndefinedMethod {
427 class: fqcn.to_string(),
428 method: method_name.to_string(),
429 },
430 Severity::Error,
431 span,
432 );
433 result = Union::merge(&result, &Union::mixed());
434 }
435 } else {
436 result = Union::merge(&result, &Union::mixed());
437 }
438 }
439 Atomic::TSelf { fqcn }
440 | Atomic::TStaticObject { fqcn }
441 | Atomic::TParent { fqcn } => {
442 let receiver_type_params: &[mir_types::Union] = &[];
443 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
445 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
446 if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
447 ea.codebase.mark_method_referenced_at(
451 fqcn,
452 method_name,
453 ea.file.clone(),
454 call.method.span.start,
455 call.method.span.end,
456 );
457 if method.is_deprecated {
459 ea.emit(
460 IssueKind::DeprecatedMethodCall {
461 class: fqcn.to_string(),
462 method: method_name.to_string(),
463 },
464 Severity::Info,
465 span,
466 );
467 }
468 check_method_visibility(ea, &method, ctx, span);
470
471 let arg_names: Vec<Option<String>> = call
473 .args
474 .iter()
475 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
476 .collect();
477 check_args(
478 ea,
479 CheckArgsParams {
480 fn_name: method_name,
481 params: &method.params,
482 arg_types: &arg_types,
483 arg_spans: &arg_spans,
484 arg_names: &arg_names,
485 call_span: span,
486 has_spread: call.args.iter().any(|a| a.unpack),
487 },
488 );
489
490 let ret_raw = method
491 .effective_return_type()
492 .cloned()
493 .unwrap_or_else(Union::mixed);
494 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
496
497 let class_tps = ea.codebase.get_class_template_params(fqcn);
499 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
500
501 if !method.template_params.is_empty() {
503 let method_bindings = infer_template_bindings(
504 &method.template_params,
505 &method.params,
506 &arg_types,
507 );
508 for key in method_bindings.keys() {
509 if bindings.contains_key(key) {
510 ea.emit(
511 IssueKind::ShadowedTemplateParam {
512 name: key.to_string(),
513 },
514 Severity::Info,
515 span,
516 );
517 }
518 }
519 bindings.extend(method_bindings);
520 for (name, inferred, bound) in
521 check_template_bounds(&bindings, &method.template_params)
522 {
523 ea.emit(
524 IssueKind::InvalidTemplateParam {
525 name: name.to_string(),
526 expected_bound: format!("{}", bound),
527 actual: format!("{}", inferred),
528 },
529 Severity::Error,
530 span,
531 );
532 }
533 }
534
535 let ret = if !bindings.is_empty() {
536 ret_raw.substitute_templates(&bindings)
537 } else {
538 ret_raw
539 };
540 result = Union::merge(&result, &ret);
541 } else if ea.codebase.type_exists(fqcn)
542 && !ea.codebase.has_unknown_ancestor(fqcn)
543 {
544 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
551 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
552 if is_interface
553 || is_abstract
554 || ea.codebase.get_method(fqcn, "__call").is_some()
555 {
556 result = Union::merge(&result, &Union::mixed());
557 } else {
558 ea.emit(
559 IssueKind::UndefinedMethod {
560 class: fqcn.to_string(),
561 method: method_name.to_string(),
562 },
563 Severity::Error,
564 span,
565 );
566 result = Union::merge(&result, &Union::mixed());
567 }
568 } else {
569 result = Union::merge(&result, &Union::mixed());
570 }
571 }
572 Atomic::TObject => {
573 result = Union::merge(&result, &Union::mixed());
574 }
575 Atomic::TTemplateParam { .. } => {
579 result = Union::merge(&result, &Union::mixed());
580 }
581 _ => {
582 result = Union::merge(&result, &Union::mixed());
583 }
584 }
585 }
586
587 if nullsafe && obj_ty.is_nullable() {
588 result.add_type(Atomic::TNull);
589 }
590
591 let final_ty = if result.is_empty() {
592 Union::mixed()
593 } else {
594 result
595 };
596 for atomic in &obj_ty.types {
600 if let Atomic::TNamedObject { fqcn, .. } = atomic {
601 ea.record_symbol(
602 call.method.span,
603 SymbolKind::MethodCall {
604 class: fqcn.clone(),
605 method: Arc::from(method_name),
606 },
607 final_ty.clone(),
608 );
609 break;
610 }
611 }
612 final_ty
613 }
614
615 pub fn analyze_static_method_call<'a, 'arena, 'src>(
620 ea: &mut ExpressionAnalyzer<'a>,
621 call: &StaticMethodCallExpr<'arena, 'src>,
622 ctx: &mut Context,
623 span: Span,
624 ) -> Union {
625 let method_name = match &call.method.kind {
626 ExprKind::Identifier(name) => name.as_str(),
627 _ => return Union::mixed(),
628 };
629
630 let fqcn = match &call.class.kind {
631 ExprKind::Identifier(name) => ea.codebase.resolve_class_name(&ea.file, name.as_ref()),
632 _ => return Union::mixed(),
633 };
634
635 let fqcn = resolve_static_class(&fqcn, ctx);
636
637 let arg_types: Vec<Union> = call
638 .args
639 .iter()
640 .map(|arg| {
641 let ty = ea.analyze(&arg.value, ctx);
642 if arg.unpack {
643 spread_element_type(&ty)
644 } else {
645 ty
646 }
647 })
648 .collect();
649 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
650
651 if let Some(method) = ea.codebase.get_method(&fqcn, method_name) {
652 let method_start = call.class.span.end + 2;
655 let method_end = method_start + method_name.len() as u32;
656 ea.codebase.mark_method_referenced_at(
657 &fqcn,
658 method_name,
659 ea.file.clone(),
660 method_start,
661 method_end,
662 );
663 if method.is_deprecated {
665 ea.emit(
666 IssueKind::DeprecatedMethodCall {
667 class: fqcn.clone(),
668 method: method_name.to_string(),
669 },
670 Severity::Info,
671 span,
672 );
673 }
674 let arg_names: Vec<Option<String>> = call
675 .args
676 .iter()
677 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
678 .collect();
679 check_args(
680 ea,
681 CheckArgsParams {
682 fn_name: method_name,
683 params: &method.params,
684 arg_types: &arg_types,
685 arg_spans: &arg_spans,
686 arg_names: &arg_names,
687 call_span: span,
688 has_spread: call.args.iter().any(|a| a.unpack),
689 },
690 );
691 let ret_raw = method
692 .effective_return_type()
693 .cloned()
694 .unwrap_or_else(Union::mixed);
695 let fqcn_arc: std::sync::Arc<str> = Arc::from(fqcn.as_str());
696 let ret = substitute_static_in_return(ret_raw, &fqcn_arc);
697 let method_span = Span::new(method_start, method_end);
698 ea.record_symbol(
699 method_span,
700 SymbolKind::StaticCall {
701 class: fqcn_arc,
702 method: Arc::from(method_name),
703 },
704 ret.clone(),
705 );
706 ret
707 } else if ea.codebase.type_exists(&fqcn) && !ea.codebase.has_unknown_ancestor(&fqcn) {
708 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_str());
712 let is_abstract = ea.codebase.is_abstract_class(&fqcn);
713 if is_interface || is_abstract || ea.codebase.get_method(&fqcn, "__call").is_some() {
714 Union::mixed()
715 } else {
716 ea.emit(
717 IssueKind::UndefinedMethod {
718 class: fqcn,
719 method: method_name.to_string(),
720 },
721 Severity::Error,
722 span,
723 );
724 Union::mixed()
725 }
726 } else {
727 Union::mixed()
729 }
730 }
731
732 pub fn analyze_static_dyn_method_call<'a, 'arena, 'src>(
737 ea: &mut ExpressionAnalyzer<'a>,
738 call: &StaticDynMethodCallExpr<'arena, 'src>,
739 ctx: &mut Context,
740 ) -> Union {
741 for arg in call.args.iter() {
743 ea.analyze(&arg.value, ctx);
744 }
745 Union::mixed()
746 }
747}
748
749pub struct CheckArgsParams<'a> {
754 pub fn_name: &'a str,
755 pub params: &'a [FnParam],
756 pub arg_types: &'a [Union],
757 pub arg_spans: &'a [Span],
758 pub arg_names: &'a [Option<String>],
759 pub call_span: Span,
760 pub has_spread: bool,
761}
762
763pub fn check_constructor_args(
764 ea: &mut ExpressionAnalyzer<'_>,
765 class_name: &str,
766 p: CheckArgsParams<'_>,
767) {
768 let ctor_name = format!("{}::__construct", class_name);
769 check_args(
770 ea,
771 CheckArgsParams {
772 fn_name: &ctor_name,
773 ..p
774 },
775 );
776}
777
778fn check_args(ea: &mut ExpressionAnalyzer<'_>, p: CheckArgsParams<'_>) {
783 let CheckArgsParams {
784 fn_name,
785 params,
786 arg_types,
787 arg_spans,
788 arg_names,
789 call_span,
790 has_spread,
791 } = p;
792 let has_named = arg_names.iter().any(|n| n.is_some());
795
796 let mut param_to_arg: Vec<Option<(Union, Span)>> = vec![None; params.len()];
798
799 if has_named {
800 let mut positional = 0usize;
801 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
802 if let Some(Some(name)) = arg_names.get(i) {
803 if let Some(pi) = params.iter().position(|p| p.name.as_ref() == name.as_str()) {
805 param_to_arg[pi] = Some((ty.clone(), *span));
806 }
807 } else {
808 while positional < params.len() && param_to_arg[positional].is_some() {
810 positional += 1;
811 }
812 if positional < params.len() {
813 param_to_arg[positional] = Some((ty.clone(), *span));
814 positional += 1;
815 }
816 }
817 }
818 } else {
819 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
821 if i < params.len() {
822 param_to_arg[i] = Some((ty.clone(), *span));
823 }
824 }
825 }
826
827 let required_count = params
828 .iter()
829 .filter(|p| !p.is_optional && !p.is_variadic)
830 .count();
831 let provided_count = if params.iter().any(|p| p.is_variadic) {
832 arg_types.len()
833 } else {
834 arg_types.len().min(params.len())
835 };
836
837 if provided_count < required_count && !has_spread {
838 ea.emit(
839 IssueKind::InvalidArgument {
840 param: format!("#{}", provided_count + 1),
841 fn_name: fn_name.to_string(),
842 expected: format!("{} argument(s)", required_count),
843 actual: format!("{} provided", provided_count),
844 },
845 Severity::Error,
846 call_span,
847 );
848 return;
849 }
850
851 for (i, (param, slot)) in params.iter().zip(param_to_arg.iter()).enumerate() {
852 let (arg_ty, arg_span) = match slot {
853 Some(pair) => pair,
854 None => continue, };
856 let arg_span = *arg_span;
857 let _ = i;
858
859 if let Some(raw_param_ty) = ¶m.ty {
860 let param_ty_owned;
862 let param_ty: &Union = if param.is_variadic {
863 if let Some(elem_ty) = raw_param_ty.types.iter().find_map(|a| match a {
864 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
865 Some(*value.clone())
866 }
867 _ => None,
868 }) {
869 param_ty_owned = elem_ty;
870 ¶m_ty_owned
871 } else {
872 raw_param_ty
873 }
874 } else {
875 raw_param_ty
876 };
877 if !param_ty.is_nullable() && arg_ty.is_nullable() {
879 ea.emit(
880 IssueKind::PossiblyNullArgument {
881 param: param.name.to_string(),
882 fn_name: fn_name.to_string(),
883 },
884 Severity::Info,
885 arg_span,
886 );
887 } else if !param_ty.is_nullable()
888 && arg_ty.contains(|t| matches!(t, Atomic::TNull))
889 && arg_ty.is_single()
890 {
891 ea.emit(
892 IssueKind::NullArgument {
893 param: param.name.to_string(),
894 fn_name: fn_name.to_string(),
895 },
896 Severity::Error,
897 arg_span,
898 );
899 }
900
901 if !arg_ty.is_subtype_of_simple(param_ty)
904 && !param_ty.is_mixed()
905 && !arg_ty.is_mixed()
906 && !named_object_subtype(arg_ty, param_ty, ea)
907 && !param_contains_template_or_unknown(param_ty, ea)
908 && !param_contains_template_or_unknown(arg_ty, ea)
909 && !array_list_compatible(arg_ty, param_ty, ea)
910 && !param_ty.is_subtype_of_simple(arg_ty)
913 && !param_ty.remove_null().is_subtype_of_simple(arg_ty)
915 && !param_ty.types.iter().any(|p| Union::single(p.clone()).is_subtype_of_simple(arg_ty))
917 && !arg_ty.remove_null().is_subtype_of_simple(param_ty)
920 && !arg_ty.remove_false().is_subtype_of_simple(param_ty)
921 && !named_object_subtype(&arg_ty.remove_null(), param_ty, ea)
922 && !named_object_subtype(&arg_ty.remove_false(), param_ty, ea)
923 {
924 ea.emit(
925 IssueKind::InvalidArgument {
926 param: param.name.to_string(),
927 fn_name: fn_name.to_string(),
928 expected: format!("{}", param_ty),
929 actual: format!("{}", arg_ty),
930 },
931 Severity::Error,
932 arg_span,
933 );
934 }
935 }
936 }
937}
938
939fn named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
946 use mir_types::Atomic;
947 arg.types.iter().all(|a_atomic| {
949 let arg_fqcn: &Arc<str> = match a_atomic {
951 Atomic::TNamedObject { fqcn, .. } => fqcn,
952 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => {
953 if ea.codebase.traits.contains_key(fqcn.as_ref()) {
955 return true;
956 }
957 fqcn
958 }
959 Atomic::TParent { fqcn } => fqcn,
960 Atomic::TNever => return true,
962 Atomic::TClosure { .. } => {
964 return param.types.iter().any(|p| match p {
965 Atomic::TClosure { .. } | Atomic::TCallable { .. } => true,
966 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
967 _ => false,
968 });
969 }
970 Atomic::TCallable { .. } => {
972 return param.types.iter().any(|p| match p {
973 Atomic::TCallable { .. } | Atomic::TClosure { .. } => true,
974 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
975 _ => false,
976 });
977 }
978 Atomic::TClassString(Some(arg_cls)) => {
980 return param.types.iter().any(|p| match p {
981 Atomic::TClassString(None) | Atomic::TString => true,
982 Atomic::TClassString(Some(param_cls)) => {
983 arg_cls == param_cls
984 || ea
985 .codebase
986 .extends_or_implements(arg_cls.as_ref(), param_cls.as_ref())
987 }
988 _ => false,
989 });
990 }
991 Atomic::TNull => {
993 return param.types.iter().any(|p| matches!(p, Atomic::TNull));
994 }
995 Atomic::TFalse => {
997 return param
998 .types
999 .iter()
1000 .any(|p| matches!(p, Atomic::TFalse | Atomic::TBool));
1001 }
1002 _ => return false, };
1004
1005 if param
1007 .types
1008 .iter()
1009 .any(|p| matches!(p, Atomic::TCallable { .. }))
1010 {
1011 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1012 if ea.codebase.get_method(&resolved_arg, "__invoke").is_some()
1013 || ea
1014 .codebase
1015 .get_method(arg_fqcn.as_ref(), "__invoke")
1016 .is_some()
1017 {
1018 return true;
1019 }
1020 }
1021
1022 param.types.iter().any(|p_atomic| {
1023 let param_fqcn: &Arc<str> = match p_atomic {
1024 Atomic::TNamedObject { fqcn, .. } => fqcn,
1025 Atomic::TSelf { fqcn } => fqcn,
1026 Atomic::TStaticObject { fqcn } => fqcn,
1027 Atomic::TParent { fqcn } => fqcn,
1028 _ => return false,
1029 };
1030 let resolved_param = ea
1032 .codebase
1033 .resolve_class_name(&ea.file, param_fqcn.as_ref());
1034 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1035
1036 let is_same_class = resolved_param == resolved_arg
1038 || arg_fqcn.as_ref() == resolved_param.as_str()
1039 || resolved_arg == param_fqcn.as_ref();
1040
1041 if is_same_class {
1042 let arg_type_params = match a_atomic {
1043 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1044 _ => &[],
1045 };
1046 let param_type_params = match p_atomic {
1047 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1048 _ => &[],
1049 };
1050 if !arg_type_params.is_empty() || !param_type_params.is_empty() {
1051 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
1052 return generic_type_params_compatible(
1053 arg_type_params,
1054 param_type_params,
1055 &class_tps,
1056 ea,
1057 );
1058 }
1059 return true;
1060 }
1061
1062 if ea.codebase.extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1063 || ea.codebase.extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1064 || ea.codebase.extends_or_implements(&resolved_arg, &resolved_param)
1065 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), &resolved_arg)
1068 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), arg_fqcn.as_ref())
1069 || ea.codebase.extends_or_implements(&resolved_param, &resolved_arg)
1070 {
1071 return true;
1072 }
1073
1074 if !arg_fqcn.contains('\\') && !ea.codebase.type_exists(&resolved_arg) {
1079 for entry in ea.codebase.classes.iter() {
1080 if entry.value().short_name.as_ref() == arg_fqcn.as_ref() {
1081 let actual_fqcn = entry.key().clone();
1082 if ea
1083 .codebase
1084 .extends_or_implements(actual_fqcn.as_ref(), &resolved_param)
1085 || ea
1086 .codebase
1087 .extends_or_implements(actual_fqcn.as_ref(), param_fqcn.as_ref())
1088 {
1089 return true;
1090 }
1091 }
1092 }
1093 }
1094
1095 let iface_key = if ea.codebase.interfaces.contains_key(arg_fqcn.as_ref()) {
1099 Some(arg_fqcn.as_ref())
1100 } else if ea.codebase.interfaces.contains_key(resolved_arg.as_str()) {
1101 Some(resolved_arg.as_str())
1102 } else {
1103 None
1104 };
1105 if let Some(iface_fqcn) = iface_key {
1106 let compatible = ea.codebase.classes.iter().any(|entry| {
1107 let cls = entry.value();
1108 cls.all_parents.iter().any(|p| p.as_ref() == iface_fqcn)
1109 && (ea
1110 .codebase
1111 .extends_or_implements(entry.key().as_ref(), param_fqcn.as_ref())
1112 || ea
1113 .codebase
1114 .extends_or_implements(entry.key().as_ref(), &resolved_param))
1115 });
1116 if compatible {
1117 return true;
1118 }
1119 }
1120
1121 if arg_fqcn.contains('\\')
1124 && !ea.codebase.type_exists(arg_fqcn.as_ref())
1125 && !ea.codebase.type_exists(&resolved_arg)
1126 {
1127 return true;
1128 }
1129
1130 if param_fqcn.contains('\\')
1133 && !ea.codebase.type_exists(param_fqcn.as_ref())
1134 && !ea.codebase.type_exists(&resolved_param)
1135 {
1136 return true;
1137 }
1138
1139 false
1140 })
1141 })
1142}
1143
1144fn strict_named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1151 use mir_types::Atomic;
1152 arg.types.iter().all(|a_atomic| {
1153 let arg_fqcn: &Arc<str> = match a_atomic {
1154 Atomic::TNamedObject { fqcn, .. } => fqcn,
1155 Atomic::TNever => return true,
1156 _ => return false,
1157 };
1158 param.types.iter().any(|p_atomic| {
1159 let param_fqcn: &Arc<str> = match p_atomic {
1160 Atomic::TNamedObject { fqcn, .. } => fqcn,
1161 _ => return false,
1162 };
1163 let resolved_param = ea
1164 .codebase
1165 .resolve_class_name(&ea.file, param_fqcn.as_ref());
1166 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1167 resolved_param == resolved_arg
1169 || arg_fqcn.as_ref() == resolved_param.as_str()
1170 || resolved_arg == param_fqcn.as_ref()
1171 || ea
1172 .codebase
1173 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1174 || ea
1175 .codebase
1176 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1177 || ea
1178 .codebase
1179 .extends_or_implements(&resolved_arg, &resolved_param)
1180 })
1181 })
1182}
1183
1184fn generic_type_params_compatible(
1191 arg_params: &[Union],
1192 param_params: &[Union],
1193 template_params: &[mir_codebase::storage::TemplateParam],
1194 ea: &ExpressionAnalyzer<'_>,
1195) -> bool {
1196 if arg_params.len() != param_params.len() {
1198 return true;
1199 }
1200 if arg_params.is_empty() {
1202 return true;
1203 }
1204
1205 for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
1206 let variance = template_params
1207 .get(i)
1208 .map(|tp| tp.variance)
1209 .unwrap_or(mir_types::Variance::Invariant);
1210
1211 let compatible = match variance {
1212 mir_types::Variance::Covariant => {
1213 arg_p.is_subtype_of_simple(param_p)
1215 || param_p.is_mixed()
1216 || arg_p.is_mixed()
1217 || strict_named_object_subtype(arg_p, param_p, ea)
1218 }
1219 mir_types::Variance::Contravariant => {
1220 param_p.is_subtype_of_simple(arg_p)
1222 || arg_p.is_mixed()
1223 || param_p.is_mixed()
1224 || strict_named_object_subtype(param_p, arg_p, ea)
1225 }
1226 mir_types::Variance::Invariant => {
1227 arg_p == param_p
1229 || arg_p.is_mixed()
1230 || param_p.is_mixed()
1231 || (arg_p.is_subtype_of_simple(param_p) && param_p.is_subtype_of_simple(arg_p))
1232 }
1233 };
1234
1235 if !compatible {
1236 return false;
1237 }
1238 }
1239
1240 true
1241}
1242
1243fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1247 param_ty.types.iter().any(|atomic| match atomic {
1248 Atomic::TTemplateParam { .. } => true,
1249 Atomic::TNamedObject { fqcn, .. } => {
1250 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1251 }
1252 Atomic::TClassString(Some(inner)) => {
1254 !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
1255 }
1256 Atomic::TArray { key: _, value }
1257 | Atomic::TList { value }
1258 | Atomic::TNonEmptyArray { key: _, value }
1259 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1260 Atomic::TTemplateParam { .. } => true,
1261 Atomic::TNamedObject { fqcn, .. } => {
1262 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1263 }
1264 _ => false,
1265 }),
1266 _ => false,
1267 })
1268}
1269
1270fn substitute_static_in_return(ret: Union, receiver_fqcn: &Arc<str>) -> Union {
1273 use mir_types::Atomic;
1274 let from_docblock = ret.from_docblock;
1275 let types: Vec<Atomic> = ret
1276 .types
1277 .into_iter()
1278 .map(|a| match a {
1279 Atomic::TStaticObject { .. } | Atomic::TSelf { .. } => Atomic::TNamedObject {
1280 fqcn: receiver_fqcn.clone(),
1281 type_params: vec![],
1282 },
1283 other => other,
1284 })
1285 .collect();
1286 let mut result = Union::from_vec(types);
1287 result.from_docblock = from_docblock;
1288 result
1289}
1290
1291pub fn spread_element_type(arr_ty: &Union) -> Union {
1295 use mir_types::Atomic;
1296 let mut result = Union::empty();
1297 for atomic in arr_ty.types.iter() {
1298 match atomic {
1299 Atomic::TArray { value, .. }
1300 | Atomic::TNonEmptyArray { value, .. }
1301 | Atomic::TList { value }
1302 | Atomic::TNonEmptyList { value } => {
1303 for t in value.types.iter() {
1304 result.add_type(t.clone());
1305 }
1306 }
1307 Atomic::TKeyedArray { properties, .. } => {
1308 for (_key, prop) in properties.iter() {
1309 for t in prop.ty.types.iter() {
1310 result.add_type(t.clone());
1311 }
1312 }
1313 }
1314 _ => return Union::mixed(),
1316 }
1317 }
1318 if result.types.is_empty() {
1319 Union::mixed()
1320 } else {
1321 result
1322 }
1323}
1324
1325fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1331 arg_ty.types.iter().all(|av| {
1332 let av_fqcn: &Arc<str> = match av {
1334 Atomic::TNamedObject { fqcn, .. } => fqcn,
1335 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
1336 fqcn
1337 }
1338 Atomic::TArray { value, .. }
1340 | Atomic::TNonEmptyArray { value, .. }
1341 | Atomic::TList { value }
1342 | Atomic::TNonEmptyList { value } => {
1343 return param_ty.types.iter().any(|pv| {
1344 let pv_val: &Union = match pv {
1345 Atomic::TArray { value, .. }
1346 | Atomic::TNonEmptyArray { value, .. }
1347 | Atomic::TList { value }
1348 | Atomic::TNonEmptyList { value } => value,
1349 _ => return false,
1350 };
1351 union_compatible(value, pv_val, ea)
1352 });
1353 }
1354 Atomic::TKeyedArray { .. } => return true,
1355 _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
1356 };
1357
1358 param_ty.types.iter().any(|pv| {
1359 let pv_fqcn: &Arc<str> = match pv {
1360 Atomic::TNamedObject { fqcn, .. } => fqcn,
1361 Atomic::TSelf { fqcn }
1362 | Atomic::TStaticObject { fqcn }
1363 | Atomic::TParent { fqcn } => fqcn,
1364 _ => return false,
1365 };
1366 if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
1368 return true;
1369 }
1370 let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
1371 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
1372 resolved_param == resolved_arg
1373 || ea
1374 .codebase
1375 .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
1376 || ea
1377 .codebase
1378 .extends_or_implements(&resolved_arg, &resolved_param)
1379 || ea
1380 .codebase
1381 .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
1382 || ea
1383 .codebase
1384 .extends_or_implements(&resolved_param, &resolved_arg)
1385 })
1386 })
1387}
1388
1389fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1390 arg_ty.types.iter().all(|a_atomic| {
1391 let arg_value: &Union = match a_atomic {
1392 Atomic::TArray { value, .. }
1393 | Atomic::TNonEmptyArray { value, .. }
1394 | Atomic::TList { value }
1395 | Atomic::TNonEmptyList { value } => value,
1396 Atomic::TKeyedArray { .. } => return true, _ => return false,
1398 };
1399
1400 param_ty.types.iter().any(|p_atomic| {
1401 let param_value: &Union = match p_atomic {
1402 Atomic::TArray { value, .. }
1403 | Atomic::TNonEmptyArray { value, .. }
1404 | Atomic::TList { value }
1405 | Atomic::TNonEmptyList { value } => value,
1406 _ => return false,
1407 };
1408
1409 union_compatible(arg_value, param_value, ea)
1410 })
1411 })
1412}
1413
1414fn check_method_visibility(
1415 ea: &mut ExpressionAnalyzer<'_>,
1416 method: &MethodStorage,
1417 ctx: &Context,
1418 span: Span,
1419) {
1420 match method.visibility {
1421 Visibility::Private => {
1422 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1424 if caller_fqcn != method.fqcn.as_ref() {
1425 ea.emit(
1426 IssueKind::UndefinedMethod {
1427 class: method.fqcn.to_string(),
1428 method: method.name.to_string(),
1429 },
1430 Severity::Error,
1431 span,
1432 );
1433 }
1434 }
1435 Visibility::Protected => {
1436 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1438 if caller_fqcn.is_empty() {
1439 ea.emit(
1441 IssueKind::UndefinedMethod {
1442 class: method.fqcn.to_string(),
1443 method: method.name.to_string(),
1444 },
1445 Severity::Error,
1446 span,
1447 );
1448 } else {
1449 let allowed = caller_fqcn == method.fqcn.as_ref()
1451 || ea
1452 .codebase
1453 .extends_or_implements(caller_fqcn, method.fqcn.as_ref());
1454 if !allowed {
1455 ea.emit(
1456 IssueKind::UndefinedMethod {
1457 class: method.fqcn.to_string(),
1458 method: method.name.to_string(),
1459 },
1460 Severity::Error,
1461 span,
1462 );
1463 }
1464 }
1465 }
1466 Visibility::Public => {}
1467 }
1468}
1469
1470fn resolve_static_class(name: &str, ctx: &Context) -> String {
1471 match name.to_lowercase().as_str() {
1472 "self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
1473 "parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
1474 "static" => ctx
1475 .static_fqcn
1476 .as_deref()
1477 .unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
1478 .to_string(),
1479 _ => name.to_string(),
1480 }
1481}