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