Skip to main content

mir_analyzer/call/
static_call.rs

1use std::sync::Arc;
2
3use php_ast::ast::{ExprKind, StaticDynMethodCallExpr, StaticMethodCallExpr};
4use php_ast::Span;
5
6use mir_issues::{IssueKind, Severity};
7use mir_types::Union;
8
9use crate::context::Context;
10use crate::expr::ExpressionAnalyzer;
11use crate::symbol::SymbolKind;
12
13use super::args::{
14    check_args, expr_can_be_passed_by_reference, spread_element_type, substitute_static_in_return,
15    CheckArgsParams,
16};
17use super::method::resolve_method_from_db;
18use super::CallAnalyzer;
19
20fn extract_namespace(fqcn: &str) -> Option<&str> {
21    if let Some(pos) = fqcn.rfind('\\') {
22        Some(&fqcn[..pos])
23    } else {
24        None
25    }
26}
27
28impl CallAnalyzer {
29    pub fn analyze_static_method_call<'a, 'arena, 'src>(
30        ea: &mut ExpressionAnalyzer<'a>,
31        call: &StaticMethodCallExpr<'arena, 'src>,
32        ctx: &mut Context,
33        span: Span,
34    ) -> Union {
35        let method_name = match &call.method.kind {
36            ExprKind::Identifier(name) => name.as_str(),
37            _ => return Union::mixed(),
38        };
39
40        let fqcn = match &call.class.kind {
41            ExprKind::Identifier(name) => {
42                crate::db::resolve_name_via_db(ea.db, &ea.file, name.as_ref())
43            }
44            _ => return Union::mixed(),
45        };
46
47        let fqcn = resolve_static_class(&fqcn, ctx);
48
49        let arg_types: Vec<Union> = call
50            .args
51            .iter()
52            .map(|arg| {
53                let ty = ea.analyze(&arg.value, ctx);
54                if arg.unpack {
55                    spread_element_type(&ty)
56                } else {
57                    ty
58                }
59            })
60            .collect();
61        let arg_spans: Vec<Span> = call.args.iter().map(|a| a.span).collect();
62
63        let fqcn_arc: Arc<str> = Arc::from(fqcn.as_str());
64        let method_name_lower = method_name.to_lowercase();
65
66        let resolved = resolve_method_from_db(ea, &fqcn_arc, &method_name_lower);
67
68        if let Some(resolved) = resolved {
69            if !ea.inference_only {
70                let (line, col_start, col_end) = ea.span_to_ref_loc(call.method.span);
71                ea.db.record_reference_location(crate::db::RefLoc {
72                    symbol_key: Arc::from(format!("{}::{}", &fqcn, method_name.to_lowercase())),
73                    file: ea.file.clone(),
74                    line,
75                    col_start,
76                    col_end,
77                });
78            }
79            if let Some(msg) = resolved.deprecated.clone() {
80                ea.emit(
81                    IssueKind::DeprecatedMethodCall {
82                        class: fqcn.clone(),
83                        method: method_name.to_string(),
84                        message: Some(msg).filter(|m| !m.is_empty()),
85                    },
86                    Severity::Info,
87                    span,
88                );
89            }
90            if resolved.is_internal {
91                let calling_namespace = ea.db.file_namespace(&ea.file).map(|ns| ns.to_string());
92                let method_namespace =
93                    extract_namespace(&resolved.owner_fqcn).map(|s| s.to_string());
94                if calling_namespace != method_namespace {
95                    ea.emit(
96                        IssueKind::InternalMethod {
97                            class: fqcn.clone(),
98                            method: method_name.to_string(),
99                        },
100                        Severity::Warning,
101                        span,
102                    );
103                }
104            }
105            let arg_names: Vec<Option<String>> = call
106                .args
107                .iter()
108                .map(|a| a.name.as_ref().map(|n| n.to_string_repr().into_owned()))
109                .collect();
110            let arg_can_be_byref: Vec<bool> = call
111                .args
112                .iter()
113                .map(|a| expr_can_be_passed_by_reference(&a.value))
114                .collect();
115            check_args(
116                ea,
117                CheckArgsParams {
118                    fn_name: method_name,
119                    params: &resolved.params,
120                    arg_types: &arg_types,
121                    arg_spans: &arg_spans,
122                    arg_names: &arg_names,
123                    arg_can_be_byref: &arg_can_be_byref,
124                    call_span: span,
125                    has_spread: call.args.iter().any(|a| a.unpack),
126                },
127            );
128            let ret_raw = resolved.return_ty_raw;
129            let ret = substitute_static_in_return(ret_raw, &fqcn_arc);
130            ea.record_symbol(
131                call.method.span,
132                SymbolKind::StaticCall {
133                    class: fqcn_arc,
134                    method: Arc::from(method_name),
135                },
136                ret.clone(),
137            );
138            ret
139        } else if crate::db::type_exists_via_db(ea.db, &fqcn)
140            && !crate::db::has_unknown_ancestor_via_db(ea.db, &fqcn)
141        {
142            let (is_interface, is_abstract) = crate::db::class_kind_via_db(ea.db, &fqcn)
143                .map(|k| (k.is_interface, k.is_abstract))
144                .unwrap_or((false, false));
145            // Check for __callStatic in the full inheritance chain (not just direct methods)
146            let has_callstatic_magic =
147                crate::db::lookup_method_in_chain(ea.db, &fqcn, "__callstatic").is_some();
148            if is_interface || is_abstract || has_callstatic_magic {
149                Union::mixed()
150            } else {
151                ea.emit(
152                    IssueKind::UndefinedMethod {
153                        class: fqcn,
154                        method: method_name.to_string(),
155                    },
156                    Severity::Error,
157                    span,
158                );
159                Union::mixed()
160            }
161        } else if !crate::db::type_exists_via_db(ea.db, &fqcn)
162            && !matches!(fqcn.as_str(), "self" | "static" | "parent")
163        {
164            ea.emit(
165                IssueKind::UndefinedClass { name: fqcn },
166                Severity::Error,
167                call.class.span,
168            );
169            Union::mixed()
170        } else {
171            Union::mixed()
172        }
173    }
174
175    pub fn analyze_static_dyn_method_call<'a, 'arena, 'src>(
176        ea: &mut ExpressionAnalyzer<'a>,
177        call: &StaticDynMethodCallExpr<'arena, 'src>,
178        ctx: &mut Context,
179    ) -> Union {
180        for arg in call.args.iter() {
181            ea.analyze(&arg.value, ctx);
182        }
183        Union::mixed()
184    }
185}
186
187fn resolve_static_class(name: &str, ctx: &Context) -> String {
188    match name.to_lowercase().as_str() {
189        "self" => ctx.self_fqcn.as_deref().unwrap_or("self").to_string(),
190        "parent" => ctx.parent_fqcn.as_deref().unwrap_or("parent").to_string(),
191        "static" => ctx
192            .static_fqcn
193            .as_deref()
194            .unwrap_or(ctx.self_fqcn.as_deref().unwrap_or("static"))
195            .to_string(),
196        _ => name.to_string(),
197    }
198}