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 for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
369 bindings.entry(k).or_insert(v);
370 }
371
372 if !method.template_params.is_empty() {
374 let method_bindings = infer_template_bindings(
375 &method.template_params,
376 &method.params,
377 &arg_types,
378 );
379 for key in method_bindings.keys() {
380 if bindings.contains_key(key) {
381 ea.emit(
382 IssueKind::ShadowedTemplateParam {
383 name: key.to_string(),
384 },
385 Severity::Info,
386 span,
387 );
388 }
389 }
390 bindings.extend(method_bindings);
391 for (name, inferred, bound) in
392 check_template_bounds(&bindings, &method.template_params)
393 {
394 ea.emit(
395 IssueKind::InvalidTemplateParam {
396 name: name.to_string(),
397 expected_bound: format!("{}", bound),
398 actual: format!("{}", inferred),
399 },
400 Severity::Error,
401 span,
402 );
403 }
404 }
405
406 let ret = if !bindings.is_empty() {
407 ret_raw.substitute_templates(&bindings)
408 } else {
409 ret_raw
410 };
411 result = Union::merge(&result, &ret);
412 } else if ea.codebase.type_exists(fqcn)
413 && !ea.codebase.has_unknown_ancestor(fqcn)
414 {
415 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
422 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
423 if is_interface
424 || is_abstract
425 || ea.codebase.get_method(fqcn, "__call").is_some()
426 {
427 result = Union::merge(&result, &Union::mixed());
428 } else {
429 ea.emit(
430 IssueKind::UndefinedMethod {
431 class: fqcn.to_string(),
432 method: method_name.to_string(),
433 },
434 Severity::Error,
435 span,
436 );
437 result = Union::merge(&result, &Union::mixed());
438 }
439 } else {
440 result = Union::merge(&result, &Union::mixed());
441 }
442 }
443 Atomic::TSelf { fqcn }
444 | Atomic::TStaticObject { fqcn }
445 | Atomic::TParent { fqcn } => {
446 let receiver_type_params: &[mir_types::Union] = &[];
447 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
449 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
450 if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
451 ea.codebase.mark_method_referenced_at(
455 fqcn,
456 method_name,
457 ea.file.clone(),
458 call.method.span.start,
459 call.method.span.end,
460 );
461 if method.is_deprecated {
463 ea.emit(
464 IssueKind::DeprecatedMethodCall {
465 class: fqcn.to_string(),
466 method: method_name.to_string(),
467 },
468 Severity::Info,
469 span,
470 );
471 }
472 check_method_visibility(ea, &method, ctx, span);
474
475 let arg_names: Vec<Option<String>> = call
477 .args
478 .iter()
479 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
480 .collect();
481 check_args(
482 ea,
483 CheckArgsParams {
484 fn_name: method_name,
485 params: &method.params,
486 arg_types: &arg_types,
487 arg_spans: &arg_spans,
488 arg_names: &arg_names,
489 call_span: span,
490 has_spread: call.args.iter().any(|a| a.unpack),
491 },
492 );
493
494 let ret_raw = method
495 .effective_return_type()
496 .cloned()
497 .unwrap_or_else(Union::mixed);
498 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
500
501 let class_tps = ea.codebase.get_class_template_params(fqcn);
503 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
504 for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
506 bindings.entry(k).or_insert(v);
507 }
508
509 if !method.template_params.is_empty() {
511 let method_bindings = infer_template_bindings(
512 &method.template_params,
513 &method.params,
514 &arg_types,
515 );
516 for key in method_bindings.keys() {
517 if bindings.contains_key(key) {
518 ea.emit(
519 IssueKind::ShadowedTemplateParam {
520 name: key.to_string(),
521 },
522 Severity::Info,
523 span,
524 );
525 }
526 }
527 bindings.extend(method_bindings);
528 for (name, inferred, bound) in
529 check_template_bounds(&bindings, &method.template_params)
530 {
531 ea.emit(
532 IssueKind::InvalidTemplateParam {
533 name: name.to_string(),
534 expected_bound: format!("{}", bound),
535 actual: format!("{}", inferred),
536 },
537 Severity::Error,
538 span,
539 );
540 }
541 }
542
543 let ret = if !bindings.is_empty() {
544 ret_raw.substitute_templates(&bindings)
545 } else {
546 ret_raw
547 };
548 result = Union::merge(&result, &ret);
549 } else if ea.codebase.type_exists(fqcn)
550 && !ea.codebase.has_unknown_ancestor(fqcn)
551 {
552 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
559 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
560 if is_interface
561 || is_abstract
562 || ea.codebase.get_method(fqcn, "__call").is_some()
563 {
564 result = Union::merge(&result, &Union::mixed());
565 } else {
566 ea.emit(
567 IssueKind::UndefinedMethod {
568 class: fqcn.to_string(),
569 method: method_name.to_string(),
570 },
571 Severity::Error,
572 span,
573 );
574 result = Union::merge(&result, &Union::mixed());
575 }
576 } else {
577 result = Union::merge(&result, &Union::mixed());
578 }
579 }
580 Atomic::TObject => {
581 result = Union::merge(&result, &Union::mixed());
582 }
583 Atomic::TTemplateParam { .. } => {
587 result = Union::merge(&result, &Union::mixed());
588 }
589 _ => {
590 result = Union::merge(&result, &Union::mixed());
591 }
592 }
593 }
594
595 if nullsafe && obj_ty.is_nullable() {
596 result.add_type(Atomic::TNull);
597 }
598
599 let final_ty = if result.is_empty() {
600 Union::mixed()
601 } else {
602 result
603 };
604 for atomic in &obj_ty.types {
608 if let Atomic::TNamedObject { fqcn, .. } = atomic {
609 ea.record_symbol(
610 call.method.span,
611 SymbolKind::MethodCall {
612 class: fqcn.clone(),
613 method: Arc::from(method_name),
614 },
615 final_ty.clone(),
616 );
617 break;
618 }
619 }
620 final_ty
621 }
622
623 pub fn analyze_static_method_call<'a, 'arena, 'src>(
628 ea: &mut ExpressionAnalyzer<'a>,
629 call: &StaticMethodCallExpr<'arena, 'src>,
630 ctx: &mut Context,
631 span: Span,
632 ) -> Union {
633 let method_name = match &call.method.kind {
634 ExprKind::Identifier(name) => name.as_str(),
635 _ => return Union::mixed(),
636 };
637
638 let fqcn = match &call.class.kind {
639 ExprKind::Identifier(name) => ea.codebase.resolve_class_name(&ea.file, name.as_ref()),
640 _ => return Union::mixed(),
641 };
642
643 let fqcn = resolve_static_class(&fqcn, ctx);
644
645 let arg_types: Vec<Union> = call
646 .args
647 .iter()
648 .map(|arg| {
649 let ty = ea.analyze(&arg.value, ctx);
650 if arg.unpack {
651 spread_element_type(&ty)
652 } else {
653 ty
654 }
655 })
656 .collect();
657 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
658
659 if let Some(method) = ea.codebase.get_method(&fqcn, method_name) {
660 let method_start = call.class.span.end + 2;
663 let method_end = method_start + method_name.len() as u32;
664 ea.codebase.mark_method_referenced_at(
665 &fqcn,
666 method_name,
667 ea.file.clone(),
668 method_start,
669 method_end,
670 );
671 if method.is_deprecated {
673 ea.emit(
674 IssueKind::DeprecatedMethodCall {
675 class: fqcn.clone(),
676 method: method_name.to_string(),
677 },
678 Severity::Info,
679 span,
680 );
681 }
682 let arg_names: Vec<Option<String>> = call
683 .args
684 .iter()
685 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
686 .collect();
687 check_args(
688 ea,
689 CheckArgsParams {
690 fn_name: method_name,
691 params: &method.params,
692 arg_types: &arg_types,
693 arg_spans: &arg_spans,
694 arg_names: &arg_names,
695 call_span: span,
696 has_spread: call.args.iter().any(|a| a.unpack),
697 },
698 );
699 let ret_raw = method
700 .effective_return_type()
701 .cloned()
702 .unwrap_or_else(Union::mixed);
703 let fqcn_arc: std::sync::Arc<str> = Arc::from(fqcn.as_str());
704 let ret = substitute_static_in_return(ret_raw, &fqcn_arc);
705 let method_span = Span::new(method_start, method_end);
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 {
735 Union::mixed()
737 }
738 }
739
740 pub fn analyze_static_dyn_method_call<'a, 'arena, 'src>(
745 ea: &mut ExpressionAnalyzer<'a>,
746 call: &StaticDynMethodCallExpr<'arena, 'src>,
747 ctx: &mut Context,
748 ) -> Union {
749 for arg in call.args.iter() {
751 ea.analyze(&arg.value, ctx);
752 }
753 Union::mixed()
754 }
755}
756
757pub struct CheckArgsParams<'a> {
762 pub fn_name: &'a str,
763 pub params: &'a [FnParam],
764 pub arg_types: &'a [Union],
765 pub arg_spans: &'a [Span],
766 pub arg_names: &'a [Option<String>],
767 pub call_span: Span,
768 pub has_spread: bool,
769}
770
771pub fn check_constructor_args(
772 ea: &mut ExpressionAnalyzer<'_>,
773 class_name: &str,
774 p: CheckArgsParams<'_>,
775) {
776 let ctor_name = format!("{}::__construct", class_name);
777 check_args(
778 ea,
779 CheckArgsParams {
780 fn_name: &ctor_name,
781 ..p
782 },
783 );
784}
785
786fn check_args(ea: &mut ExpressionAnalyzer<'_>, p: CheckArgsParams<'_>) {
791 let CheckArgsParams {
792 fn_name,
793 params,
794 arg_types,
795 arg_spans,
796 arg_names,
797 call_span,
798 has_spread,
799 } = p;
800 let has_named = arg_names.iter().any(|n| n.is_some());
803
804 let mut param_to_arg: Vec<Option<(Union, Span)>> = vec![None; params.len()];
806
807 if has_named {
808 let mut positional = 0usize;
809 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
810 if let Some(Some(name)) = arg_names.get(i) {
811 if let Some(pi) = params.iter().position(|p| p.name.as_ref() == name.as_str()) {
813 param_to_arg[pi] = Some((ty.clone(), *span));
814 }
815 } else {
816 while positional < params.len() && param_to_arg[positional].is_some() {
818 positional += 1;
819 }
820 if positional < params.len() {
821 param_to_arg[positional] = Some((ty.clone(), *span));
822 positional += 1;
823 }
824 }
825 }
826 } else {
827 for (i, (ty, span)) in arg_types.iter().zip(arg_spans.iter()).enumerate() {
829 if i < params.len() {
830 param_to_arg[i] = Some((ty.clone(), *span));
831 }
832 }
833 }
834
835 let required_count = params
836 .iter()
837 .filter(|p| !p.is_optional && !p.is_variadic)
838 .count();
839 let provided_count = if params.iter().any(|p| p.is_variadic) {
840 arg_types.len()
841 } else {
842 arg_types.len().min(params.len())
843 };
844
845 if provided_count < required_count && !has_spread {
846 ea.emit(
847 IssueKind::InvalidArgument {
848 param: format!("#{}", provided_count + 1),
849 fn_name: fn_name.to_string(),
850 expected: format!("{} argument(s)", required_count),
851 actual: format!("{} provided", provided_count),
852 },
853 Severity::Error,
854 call_span,
855 );
856 return;
857 }
858
859 for (i, (param, slot)) in params.iter().zip(param_to_arg.iter()).enumerate() {
860 let (arg_ty, arg_span) = match slot {
861 Some(pair) => pair,
862 None => continue, };
864 let arg_span = *arg_span;
865 let _ = i;
866
867 if let Some(raw_param_ty) = ¶m.ty {
868 let param_ty_owned;
870 let param_ty: &Union = if param.is_variadic {
871 if let Some(elem_ty) = raw_param_ty.types.iter().find_map(|a| match a {
872 Atomic::TList { value } | Atomic::TNonEmptyList { value } => {
873 Some(*value.clone())
874 }
875 _ => None,
876 }) {
877 param_ty_owned = elem_ty;
878 ¶m_ty_owned
879 } else {
880 raw_param_ty
881 }
882 } else {
883 raw_param_ty
884 };
885 if !param_ty.is_nullable()
889 && !param_ty.is_mixed()
890 && arg_ty.is_single()
891 && arg_ty.contains(|t| matches!(t, Atomic::TNull))
892 {
893 ea.emit(
894 IssueKind::InvalidArgument {
895 param: param.name.to_string(),
896 fn_name: fn_name.to_string(),
897 expected: format!("{}", param_ty),
898 actual: format!("{}", arg_ty),
899 },
900 Severity::Error,
901 arg_span,
902 );
903 } else if !param_ty.is_nullable() && !param_ty.is_mixed() && arg_ty.is_nullable() {
904 ea.emit(
905 IssueKind::PossiblyNullArgument {
906 param: param.name.to_string(),
907 fn_name: fn_name.to_string(),
908 },
909 Severity::Info,
910 arg_span,
911 );
912 }
913
914 if !arg_ty.is_subtype_of_simple(param_ty)
917 && !param_ty.is_mixed()
918 && !arg_ty.is_mixed()
919 && !named_object_subtype(arg_ty, param_ty, ea)
920 && !param_contains_template_or_unknown(param_ty, ea)
921 && !param_contains_template_or_unknown(arg_ty, ea)
922 && !array_list_compatible(arg_ty, param_ty, ea)
923 && !(arg_ty.is_single() && param_ty.is_subtype_of_simple(arg_ty))
928 && !(arg_ty.is_single() && param_ty.remove_null().is_subtype_of_simple(arg_ty))
930 && !(arg_ty.is_single() && param_ty.types.iter().any(|p| Union::single(p.clone()).is_subtype_of_simple(arg_ty)))
932 && !arg_ty.remove_null().is_subtype_of_simple(param_ty)
935 && !arg_ty.remove_false().is_subtype_of_simple(param_ty)
936 && !named_object_subtype(&arg_ty.remove_null(), param_ty, ea)
937 && !named_object_subtype(&arg_ty.remove_false(), param_ty, ea)
938 {
939 ea.emit(
940 IssueKind::InvalidArgument {
941 param: param.name.to_string(),
942 fn_name: fn_name.to_string(),
943 expected: format!("{}", param_ty),
944 actual: format!("{}", arg_ty),
945 },
946 Severity::Error,
947 arg_span,
948 );
949 }
950 }
951 }
952}
953
954fn named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
961 use mir_types::Atomic;
962 arg.types.iter().all(|a_atomic| {
964 let arg_fqcn: &Arc<str> = match a_atomic {
966 Atomic::TNamedObject { fqcn, .. } => fqcn,
967 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } => {
968 if ea.codebase.traits.contains_key(fqcn.as_ref()) {
970 return true;
971 }
972 fqcn
973 }
974 Atomic::TParent { fqcn } => fqcn,
975 Atomic::TNever => return true,
977 Atomic::TClosure { .. } => {
979 return param.types.iter().any(|p| match p {
980 Atomic::TClosure { .. } | Atomic::TCallable { .. } => true,
981 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
982 _ => false,
983 });
984 }
985 Atomic::TCallable { .. } => {
987 return param.types.iter().any(|p| match p {
988 Atomic::TCallable { .. } | Atomic::TClosure { .. } => true,
989 Atomic::TNamedObject { fqcn, .. } => fqcn.as_ref() == "Closure",
990 _ => false,
991 });
992 }
993 Atomic::TClassString(Some(arg_cls)) => {
995 return param.types.iter().any(|p| match p {
996 Atomic::TClassString(None) | Atomic::TString => true,
997 Atomic::TClassString(Some(param_cls)) => {
998 arg_cls == param_cls
999 || ea
1000 .codebase
1001 .extends_or_implements(arg_cls.as_ref(), param_cls.as_ref())
1002 }
1003 _ => false,
1004 });
1005 }
1006 Atomic::TNull => {
1008 return param.types.iter().any(|p| matches!(p, Atomic::TNull));
1009 }
1010 Atomic::TFalse => {
1012 return param
1013 .types
1014 .iter()
1015 .any(|p| matches!(p, Atomic::TFalse | Atomic::TBool));
1016 }
1017 _ => return false, };
1019
1020 if param
1022 .types
1023 .iter()
1024 .any(|p| matches!(p, Atomic::TCallable { .. }))
1025 {
1026 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1027 if ea.codebase.get_method(&resolved_arg, "__invoke").is_some()
1028 || ea
1029 .codebase
1030 .get_method(arg_fqcn.as_ref(), "__invoke")
1031 .is_some()
1032 {
1033 return true;
1034 }
1035 }
1036
1037 param.types.iter().any(|p_atomic| {
1038 let param_fqcn: &Arc<str> = match p_atomic {
1039 Atomic::TNamedObject { fqcn, .. } => fqcn,
1040 Atomic::TSelf { fqcn } => fqcn,
1041 Atomic::TStaticObject { fqcn } => fqcn,
1042 Atomic::TParent { fqcn } => fqcn,
1043 _ => return false,
1044 };
1045 let resolved_param = ea
1047 .codebase
1048 .resolve_class_name(&ea.file, param_fqcn.as_ref());
1049 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1050
1051 let is_same_class = resolved_param == resolved_arg
1053 || arg_fqcn.as_ref() == resolved_param.as_str()
1054 || resolved_arg == param_fqcn.as_ref();
1055
1056 if is_same_class {
1057 let arg_type_params = match a_atomic {
1058 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1059 _ => &[],
1060 };
1061 let param_type_params = match p_atomic {
1062 Atomic::TNamedObject { type_params, .. } => type_params.as_slice(),
1063 _ => &[],
1064 };
1065 if !arg_type_params.is_empty() || !param_type_params.is_empty() {
1066 let class_tps = ea.codebase.get_class_template_params(&resolved_param);
1067 return generic_type_params_compatible(
1068 arg_type_params,
1069 param_type_params,
1070 &class_tps,
1071 ea,
1072 );
1073 }
1074 return true;
1075 }
1076
1077 if ea.codebase.extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1078 || ea.codebase.extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1079 || ea.codebase.extends_or_implements(&resolved_arg, &resolved_param)
1080 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), &resolved_arg)
1083 || ea.codebase.extends_or_implements(param_fqcn.as_ref(), arg_fqcn.as_ref())
1084 || ea.codebase.extends_or_implements(&resolved_param, &resolved_arg)
1085 {
1086 return true;
1087 }
1088
1089 if !arg_fqcn.contains('\\') && !ea.codebase.type_exists(&resolved_arg) {
1094 for entry in ea.codebase.classes.iter() {
1095 if entry.value().short_name.as_ref() == arg_fqcn.as_ref() {
1096 let actual_fqcn = entry.key().clone();
1097 if ea
1098 .codebase
1099 .extends_or_implements(actual_fqcn.as_ref(), &resolved_param)
1100 || ea
1101 .codebase
1102 .extends_or_implements(actual_fqcn.as_ref(), param_fqcn.as_ref())
1103 {
1104 return true;
1105 }
1106 }
1107 }
1108 }
1109
1110 let iface_key = if ea.codebase.interfaces.contains_key(arg_fqcn.as_ref()) {
1114 Some(arg_fqcn.as_ref())
1115 } else if ea.codebase.interfaces.contains_key(resolved_arg.as_str()) {
1116 Some(resolved_arg.as_str())
1117 } else {
1118 None
1119 };
1120 if let Some(iface_fqcn) = iface_key {
1121 let compatible = ea.codebase.classes.iter().any(|entry| {
1122 let cls = entry.value();
1123 cls.all_parents.iter().any(|p| p.as_ref() == iface_fqcn)
1124 && (ea
1125 .codebase
1126 .extends_or_implements(entry.key().as_ref(), param_fqcn.as_ref())
1127 || ea
1128 .codebase
1129 .extends_or_implements(entry.key().as_ref(), &resolved_param))
1130 });
1131 if compatible {
1132 return true;
1133 }
1134 }
1135
1136 if arg_fqcn.contains('\\')
1139 && !ea.codebase.type_exists(arg_fqcn.as_ref())
1140 && !ea.codebase.type_exists(&resolved_arg)
1141 {
1142 return true;
1143 }
1144
1145 if param_fqcn.contains('\\')
1148 && !ea.codebase.type_exists(param_fqcn.as_ref())
1149 && !ea.codebase.type_exists(&resolved_param)
1150 {
1151 return true;
1152 }
1153
1154 false
1155 })
1156 })
1157}
1158
1159fn strict_named_object_subtype(arg: &Union, param: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1166 use mir_types::Atomic;
1167 arg.types.iter().all(|a_atomic| {
1168 let arg_fqcn: &Arc<str> = match a_atomic {
1169 Atomic::TNamedObject { fqcn, .. } => fqcn,
1170 Atomic::TNever => return true,
1171 _ => return false,
1172 };
1173 param.types.iter().any(|p_atomic| {
1174 let param_fqcn: &Arc<str> = match p_atomic {
1175 Atomic::TNamedObject { fqcn, .. } => fqcn,
1176 _ => return false,
1177 };
1178 let resolved_param = ea
1179 .codebase
1180 .resolve_class_name(&ea.file, param_fqcn.as_ref());
1181 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, arg_fqcn.as_ref());
1182 resolved_param == resolved_arg
1184 || arg_fqcn.as_ref() == resolved_param.as_str()
1185 || resolved_arg == param_fqcn.as_ref()
1186 || ea
1187 .codebase
1188 .extends_or_implements(arg_fqcn.as_ref(), &resolved_param)
1189 || ea
1190 .codebase
1191 .extends_or_implements(arg_fqcn.as_ref(), param_fqcn.as_ref())
1192 || ea
1193 .codebase
1194 .extends_or_implements(&resolved_arg, &resolved_param)
1195 })
1196 })
1197}
1198
1199fn generic_type_params_compatible(
1206 arg_params: &[Union],
1207 param_params: &[Union],
1208 template_params: &[mir_codebase::storage::TemplateParam],
1209 ea: &ExpressionAnalyzer<'_>,
1210) -> bool {
1211 if arg_params.len() != param_params.len() {
1213 return true;
1214 }
1215 if arg_params.is_empty() {
1217 return true;
1218 }
1219
1220 for (i, (arg_p, param_p)) in arg_params.iter().zip(param_params.iter()).enumerate() {
1221 let variance = template_params
1222 .get(i)
1223 .map(|tp| tp.variance)
1224 .unwrap_or(mir_types::Variance::Invariant);
1225
1226 let compatible = match variance {
1227 mir_types::Variance::Covariant => {
1228 arg_p.is_subtype_of_simple(param_p)
1230 || param_p.is_mixed()
1231 || arg_p.is_mixed()
1232 || strict_named_object_subtype(arg_p, param_p, ea)
1233 }
1234 mir_types::Variance::Contravariant => {
1235 param_p.is_subtype_of_simple(arg_p)
1237 || arg_p.is_mixed()
1238 || param_p.is_mixed()
1239 || strict_named_object_subtype(param_p, arg_p, ea)
1240 }
1241 mir_types::Variance::Invariant => {
1242 arg_p == param_p
1244 || arg_p.is_mixed()
1245 || param_p.is_mixed()
1246 || (arg_p.is_subtype_of_simple(param_p) && param_p.is_subtype_of_simple(arg_p))
1247 }
1248 };
1249
1250 if !compatible {
1251 return false;
1252 }
1253 }
1254
1255 true
1256}
1257
1258fn param_contains_template_or_unknown(param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1262 param_ty.types.iter().any(|atomic| match atomic {
1263 Atomic::TTemplateParam { .. } => true,
1264 Atomic::TNamedObject { fqcn, .. } => {
1265 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1266 }
1267 Atomic::TClassString(Some(inner)) => {
1269 !inner.contains('\\') && !ea.codebase.type_exists(inner.as_ref())
1270 }
1271 Atomic::TArray { key: _, value }
1272 | Atomic::TList { value }
1273 | Atomic::TNonEmptyArray { key: _, value }
1274 | Atomic::TNonEmptyList { value } => value.types.iter().any(|v| match v {
1275 Atomic::TTemplateParam { .. } => true,
1276 Atomic::TNamedObject { fqcn, .. } => {
1277 !fqcn.contains('\\') && !ea.codebase.type_exists(fqcn.as_ref())
1278 }
1279 _ => false,
1280 }),
1281 _ => false,
1282 })
1283}
1284
1285fn substitute_static_in_return(ret: Union, receiver_fqcn: &Arc<str>) -> Union {
1288 use mir_types::Atomic;
1289 let from_docblock = ret.from_docblock;
1290 let types: Vec<Atomic> = ret
1291 .types
1292 .into_iter()
1293 .map(|a| match a {
1294 Atomic::TStaticObject { .. } | Atomic::TSelf { .. } => Atomic::TNamedObject {
1295 fqcn: receiver_fqcn.clone(),
1296 type_params: vec![],
1297 },
1298 other => other,
1299 })
1300 .collect();
1301 let mut result = Union::from_vec(types);
1302 result.from_docblock = from_docblock;
1303 result
1304}
1305
1306pub fn spread_element_type(arr_ty: &Union) -> Union {
1310 use mir_types::Atomic;
1311 let mut result = Union::empty();
1312 for atomic in arr_ty.types.iter() {
1313 match atomic {
1314 Atomic::TArray { value, .. }
1315 | Atomic::TNonEmptyArray { value, .. }
1316 | Atomic::TList { value }
1317 | Atomic::TNonEmptyList { value } => {
1318 for t in value.types.iter() {
1319 result.add_type(t.clone());
1320 }
1321 }
1322 Atomic::TKeyedArray { properties, .. } => {
1323 for (_key, prop) in properties.iter() {
1324 for t in prop.ty.types.iter() {
1325 result.add_type(t.clone());
1326 }
1327 }
1328 }
1329 _ => return Union::mixed(),
1331 }
1332 }
1333 if result.types.is_empty() {
1334 Union::mixed()
1335 } else {
1336 result
1337 }
1338}
1339
1340fn union_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1346 arg_ty.types.iter().all(|av| {
1347 let av_fqcn: &Arc<str> = match av {
1349 Atomic::TNamedObject { fqcn, .. } => fqcn,
1350 Atomic::TSelf { fqcn } | Atomic::TStaticObject { fqcn } | Atomic::TParent { fqcn } => {
1351 fqcn
1352 }
1353 Atomic::TArray { value, .. }
1355 | Atomic::TNonEmptyArray { value, .. }
1356 | Atomic::TList { value }
1357 | Atomic::TNonEmptyList { value } => {
1358 return param_ty.types.iter().any(|pv| {
1359 let pv_val: &Union = match pv {
1360 Atomic::TArray { value, .. }
1361 | Atomic::TNonEmptyArray { value, .. }
1362 | Atomic::TList { value }
1363 | Atomic::TNonEmptyList { value } => value,
1364 _ => return false,
1365 };
1366 union_compatible(value, pv_val, ea)
1367 });
1368 }
1369 Atomic::TKeyedArray { .. } => return true,
1370 _ => return Union::single(av.clone()).is_subtype_of_simple(param_ty),
1371 };
1372
1373 param_ty.types.iter().any(|pv| {
1374 let pv_fqcn: &Arc<str> = match pv {
1375 Atomic::TNamedObject { fqcn, .. } => fqcn,
1376 Atomic::TSelf { fqcn }
1377 | Atomic::TStaticObject { fqcn }
1378 | Atomic::TParent { fqcn } => fqcn,
1379 _ => return false,
1380 };
1381 if !pv_fqcn.contains('\\') && !ea.codebase.type_exists(pv_fqcn.as_ref()) {
1383 return true;
1384 }
1385 let resolved_param = ea.codebase.resolve_class_name(&ea.file, pv_fqcn.as_ref());
1386 let resolved_arg = ea.codebase.resolve_class_name(&ea.file, av_fqcn.as_ref());
1387 resolved_param == resolved_arg
1388 || ea
1389 .codebase
1390 .extends_or_implements(av_fqcn.as_ref(), &resolved_param)
1391 || ea
1392 .codebase
1393 .extends_or_implements(&resolved_arg, &resolved_param)
1394 || ea
1395 .codebase
1396 .extends_or_implements(pv_fqcn.as_ref(), &resolved_arg)
1397 || ea
1398 .codebase
1399 .extends_or_implements(&resolved_param, &resolved_arg)
1400 })
1401 })
1402}
1403
1404fn array_list_compatible(arg_ty: &Union, param_ty: &Union, ea: &ExpressionAnalyzer<'_>) -> bool {
1405 arg_ty.types.iter().all(|a_atomic| {
1406 let arg_value: &Union = match a_atomic {
1407 Atomic::TArray { value, .. }
1408 | Atomic::TNonEmptyArray { value, .. }
1409 | Atomic::TList { value }
1410 | Atomic::TNonEmptyList { value } => value,
1411 Atomic::TKeyedArray { .. } => return true, _ => return false,
1413 };
1414
1415 param_ty.types.iter().any(|p_atomic| {
1416 let param_value: &Union = match p_atomic {
1417 Atomic::TArray { value, .. }
1418 | Atomic::TNonEmptyArray { value, .. }
1419 | Atomic::TList { value }
1420 | Atomic::TNonEmptyList { value } => value,
1421 _ => return false,
1422 };
1423
1424 union_compatible(arg_value, param_value, ea)
1425 })
1426 })
1427}
1428
1429fn check_method_visibility(
1430 ea: &mut ExpressionAnalyzer<'_>,
1431 method: &MethodStorage,
1432 ctx: &Context,
1433 span: Span,
1434) {
1435 match method.visibility {
1436 Visibility::Private => {
1437 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1439 if caller_fqcn != method.fqcn.as_ref() {
1440 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 }
1449 }
1450 Visibility::Protected => {
1451 let caller_fqcn = ctx.self_fqcn.as_deref().unwrap_or("");
1453 if caller_fqcn.is_empty() {
1454 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 } else {
1464 let allowed = caller_fqcn == method.fqcn.as_ref()
1466 || ea
1467 .codebase
1468 .extends_or_implements(caller_fqcn, method.fqcn.as_ref());
1469 if !allowed {
1470 ea.emit(
1471 IssueKind::UndefinedMethod {
1472 class: method.fqcn.to_string(),
1473 method: method.name.to_string(),
1474 },
1475 Severity::Error,
1476 span,
1477 );
1478 }
1479 }
1480 }
1481 Visibility::Public => {}
1482 }
1483}
1484
1485fn resolve_static_class(name: &str, ctx: &Context) -> String {
1486 match name.to_lowercase().as_str() {
1487 "self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
1488 "parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
1489 "static" => ctx
1490 .static_fqcn
1491 .as_deref()
1492 .unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
1493 .to_string(),
1494 _ => name.to_string(),
1495 }
1496}