1use std::sync::Arc;
2
3use php_ast::ast::{ExprKind, MethodCallExpr};
4use php_ast::Span;
5
6use mir_issues::{IssueKind, Severity};
7use mir_types::{Atomic, Union};
8
9use crate::context::Context;
10use crate::expr::ExpressionAnalyzer;
11use crate::generic::{build_class_bindings, check_template_bounds, infer_template_bindings};
12use crate::symbol::SymbolKind;
13
14use super::args::{
15 check_args, check_method_visibility, expr_can_be_passed_by_reference, spread_element_type,
16 substitute_static_in_return, CheckArgsParams,
17};
18use super::CallAnalyzer;
19
20impl CallAnalyzer {
21 pub fn analyze_method_call<'a, 'arena, 'src>(
22 ea: &mut ExpressionAnalyzer<'a>,
23 call: &MethodCallExpr<'arena, 'src>,
24 ctx: &mut Context,
25 span: Span,
26 nullsafe: bool,
27 ) -> Union {
28 let obj_ty = ea.analyze(call.object, ctx);
29
30 let method_name = match &call.method.kind {
31 ExprKind::Identifier(name) => name.as_str(),
32 _ => return Union::mixed(),
33 };
34
35 let arg_types: Vec<Union> = call
39 .args
40 .iter()
41 .map(|arg| {
42 let ty = ea.analyze(&arg.value, ctx);
43 if arg.unpack {
44 spread_element_type(&ty)
45 } else {
46 ty
47 }
48 })
49 .collect();
50
51 let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
52
53 if obj_ty.contains(|t| matches!(t, Atomic::TNull)) {
54 if nullsafe {
55 } else if obj_ty.is_single() {
57 ea.emit(
58 IssueKind::NullMethodCall {
59 method: method_name.to_string(),
60 },
61 Severity::Error,
62 span,
63 );
64 return Union::mixed();
65 } else {
66 ea.emit(
67 IssueKind::PossiblyNullMethodCall {
68 method: method_name.to_string(),
69 },
70 Severity::Info,
71 span,
72 );
73 }
74 }
75
76 if obj_ty.is_mixed() {
77 ea.emit(
78 IssueKind::MixedMethodCall {
79 method: method_name.to_string(),
80 },
81 Severity::Info,
82 span,
83 );
84 return Union::mixed();
85 }
86
87 let receiver = obj_ty.remove_null();
88 let mut result = Union::empty();
89
90 for atomic in &receiver.types {
91 match atomic {
92 Atomic::TNamedObject {
93 fqcn,
94 type_params: receiver_type_params,
95 } => {
96 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
97 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
98 result = Union::merge(
99 &result,
100 &resolve_method_return(
101 ea,
102 ctx,
103 call,
104 span,
105 method_name,
106 fqcn,
107 receiver_type_params.as_slice(),
108 &arg_types,
109 &arg_spans,
110 ),
111 );
112 }
113 Atomic::TSelf { fqcn }
114 | Atomic::TStaticObject { fqcn }
115 | Atomic::TParent { fqcn } => {
116 let fqcn_resolved = ea.codebase.resolve_class_name(&ea.file, fqcn);
117 let fqcn = &std::sync::Arc::from(fqcn_resolved.as_str());
118 result = Union::merge(
119 &result,
120 &resolve_method_return(
121 ea,
122 ctx,
123 call,
124 span,
125 method_name,
126 fqcn,
127 &[],
128 &arg_types,
129 &arg_spans,
130 ),
131 );
132 }
133 Atomic::TObject | Atomic::TTemplateParam { .. } => {
134 result = Union::merge(&result, &Union::mixed());
135 }
136 _ => {
137 result = Union::merge(&result, &Union::mixed());
138 }
139 }
140 }
141
142 if nullsafe && obj_ty.is_nullable() {
143 result.add_type(Atomic::TNull);
144 }
145
146 let final_ty = if result.is_empty() {
147 Union::mixed()
148 } else {
149 result
150 };
151
152 for atomic in &obj_ty.types {
153 if let Atomic::TNamedObject { fqcn, .. } = atomic {
154 ea.record_symbol(
155 call.method.span,
156 SymbolKind::MethodCall {
157 class: fqcn.clone(),
158 method: Arc::from(method_name),
159 },
160 final_ty.clone(),
161 );
162 break;
163 }
164 }
165 final_ty
166 }
167}
168
169#[allow(clippy::too_many_arguments)]
172fn resolve_method_return<'a, 'arena, 'src>(
173 ea: &mut ExpressionAnalyzer<'a>,
174 ctx: &Context,
175 call: &MethodCallExpr<'arena, 'src>,
176 span: Span,
177 method_name: &str,
178 fqcn: &Arc<str>,
179 receiver_type_params: &[Union],
180 arg_types: &[Union],
181 arg_spans: &[Span],
182) -> Union {
183 if let Some(method) = ea.codebase.get_method(fqcn, method_name) {
184 ea.codebase.mark_method_referenced_at(
185 fqcn,
186 method_name,
187 ea.file.clone(),
188 call.method.span.start,
189 call.method.span.end,
190 );
191 if let Some(msg) = method.deprecated.clone() {
192 ea.emit(
193 IssueKind::DeprecatedMethodCall {
194 class: fqcn.to_string(),
195 method: method_name.to_string(),
196 message: Some(msg).filter(|m| !m.is_empty()),
197 },
198 Severity::Info,
199 span,
200 );
201 }
202 check_method_visibility(ea, &method, ctx, span);
203
204 let arg_names: Vec<Option<String>> = call
205 .args
206 .iter()
207 .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
208 .collect();
209 let arg_can_be_byref: Vec<bool> = call
210 .args
211 .iter()
212 .map(|a| expr_can_be_passed_by_reference(&a.value))
213 .collect();
214 check_args(
215 ea,
216 CheckArgsParams {
217 fn_name: method_name,
218 params: &method.params,
219 arg_types,
220 arg_spans,
221 arg_names: &arg_names,
222 arg_can_be_byref: &arg_can_be_byref,
223 call_span: span,
224 has_spread: call.args.iter().any(|a| a.unpack),
225 },
226 );
227
228 let ret_raw = method
229 .effective_return_type()
230 .cloned()
231 .unwrap_or_else(Union::mixed);
232 let ret_raw = substitute_static_in_return(ret_raw, fqcn);
233
234 let class_tps = ea.codebase.get_class_template_params(fqcn);
235 let mut bindings = build_class_bindings(&class_tps, receiver_type_params);
236 for (k, v) in ea.codebase.get_inherited_template_bindings(fqcn) {
237 bindings.entry(k).or_insert(v);
238 }
239
240 if !method.template_params.is_empty() {
241 let method_bindings =
242 infer_template_bindings(&method.template_params, &method.params, arg_types);
243 for key in method_bindings.keys() {
244 if bindings.contains_key(key) {
245 ea.emit(
246 IssueKind::ShadowedTemplateParam {
247 name: key.to_string(),
248 },
249 Severity::Info,
250 span,
251 );
252 }
253 }
254 bindings.extend(method_bindings);
255 for (name, inferred, bound) in check_template_bounds(&bindings, &method.template_params)
256 {
257 ea.emit(
258 IssueKind::InvalidTemplateParam {
259 name: name.to_string(),
260 expected_bound: format!("{bound}"),
261 actual: format!("{inferred}"),
262 },
263 Severity::Error,
264 span,
265 );
266 }
267 }
268
269 if !bindings.is_empty() {
270 ret_raw.substitute_templates(&bindings)
271 } else {
272 ret_raw
273 }
274 } else if ea.codebase.type_exists(fqcn) && !ea.codebase.has_unknown_ancestor(fqcn) {
275 let is_interface = ea.codebase.interfaces.contains_key(fqcn.as_ref());
276 let is_abstract = ea.codebase.is_abstract_class(fqcn.as_ref());
277 if is_interface || is_abstract || ea.codebase.get_method(fqcn, "__call").is_some() {
278 Union::mixed()
279 } else {
280 ea.emit(
281 IssueKind::UndefinedMethod {
282 class: fqcn.to_string(),
283 method: method_name.to_string(),
284 },
285 Severity::Error,
286 span,
287 );
288 Union::mixed()
289 }
290 } else {
291 Union::mixed()
292 }
293}