use std::collections::HashMap;
use std::path::Path;
use php_ast::{ExprKind, NamespaceBody, Stmt, StmtKind};
use crate::ast::ParsedDoc;
type MetaEntries = HashMap<(String, String), Vec<(Option<String>, String)>>;
#[derive(Debug, Default, Clone)]
pub struct PhpStormMeta {
entries: MetaEntries,
}
impl PhpStormMeta {
pub fn load(root: &Path) -> Self {
let path = root.join(".phpstorm.meta.php");
let text = match std::fs::read_to_string(&path) {
Ok(t) => t,
Err(_) => return Self::default(),
};
let doc = ParsedDoc::parse(text);
let mut meta = Self::default();
collect_overrides(&doc.program().stmts, &mut meta);
meta
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn resolve_return_type(
&self,
class_name: &str,
method_name: &str,
arg: &str,
) -> Option<&str> {
let key = (class_name.to_lowercase(), method_name.to_lowercase());
let pairs = self.entries.get(&key)?;
let needle = arg.trim_start_matches('\\');
for (literal, ret) in pairs {
if let Some(lit) = literal
&& lit.trim_start_matches('\\').eq_ignore_ascii_case(needle)
{
return Some(ret.as_str());
}
}
for (literal, ret) in pairs {
if literal.is_none() {
return Some(ret.as_str());
}
}
None
}
}
fn collect_overrides(stmts: &[Stmt<'_, '_>], meta: &mut PhpStormMeta) {
for stmt in stmts {
match &stmt.kind {
StmtKind::Namespace(ns) => {
let name = ns.name.as_ref().map(|n| n.to_string_repr());
if name.as_deref() == Some("PHPSTORM_META")
&& let NamespaceBody::Braced(inner) = &ns.body
{
collect_overrides(inner, meta);
}
}
StmtKind::Expression(expr) => {
if let ExprKind::FunctionCall(f) = &expr.kind
&& let ExprKind::Identifier(name) = &f.name.kind
&& name.eq_ignore_ascii_case("override")
&& f.args.len() == 2
{
parse_override(&f.args[0].value, &f.args[1].value, meta);
}
}
_ => {}
}
}
}
fn parse_override(
target: &php_ast::Expr<'_, '_>,
mapping: &php_ast::Expr<'_, '_>,
meta: &mut PhpStormMeta,
) {
let (class_name, method_name) = match extract_static_call_target(target) {
Some(pair) => pair,
None => return,
};
let pairs = match extract_map_pairs(mapping) {
Some(p) => p,
None => return,
};
let key = (class_name.to_lowercase(), method_name.to_lowercase());
meta.entries.entry(key).or_default().extend(pairs);
}
fn extract_static_call_target(expr: &php_ast::Expr<'_, '_>) -> Option<(String, String)> {
let ExprKind::StaticMethodCall(s) = &expr.kind else {
return None;
};
let class_name = extract_class_name(s.class)?;
let method_name = s.method.to_string();
Some((class_name, method_name))
}
fn extract_class_name(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
match &expr.kind {
ExprKind::Identifier(name) => {
let s = name.trim_start_matches('\\');
let short = s.rsplit('\\').next().unwrap_or(s);
Some(short.to_string())
}
_ => None,
}
}
fn extract_map_pairs(expr: &php_ast::Expr<'_, '_>) -> Option<Vec<(Option<String>, String)>> {
let ExprKind::FunctionCall(f) = &expr.kind else {
return None;
};
if !matches!(&f.name.kind, ExprKind::Identifier(n) if n.eq_ignore_ascii_case("map")) {
return None;
}
let array_arg = f.args.first()?;
let ExprKind::Array(elements) = &array_arg.value.kind else {
return None;
};
let mut pairs: Vec<(Option<String>, String)> = Vec::new();
for elem in elements.iter() {
let key_str = elem.key.as_ref().and_then(|k| extract_string_or_class(k));
let val_str = extract_string_or_class(&elem.value);
if let Some(ret_type) = val_str {
pairs.push((key_str, ret_type));
}
}
Some(pairs)
}
fn extract_string_or_class(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
match &expr.kind {
ExprKind::String(s) => {
let raw = s.trim_start_matches('\\');
if raw.is_empty() {
None
} else {
let short = raw.rsplit('\\').next().unwrap_or(raw);
Some(short.to_string())
}
}
ExprKind::ClassConstAccess(c) => {
if c.member == "class" {
extract_class_name(c.class)
} else {
None
}
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_meta(src: &str) -> PhpStormMeta {
let doc = ParsedDoc::parse(src.to_string());
let mut meta = PhpStormMeta::default();
collect_overrides(&doc.program().stmts, &mut meta);
meta
}
#[test]
fn parses_simple_override() {
let src = r#"<?php
namespace PHPSTORM_META {
override(\App\Container::make(0), map([
\App\UserService::class => \App\UserService::class,
]));
}"#;
let meta = parse_meta(src);
assert!(!meta.is_empty());
let ret = meta.resolve_return_type("Container", "make", "UserService");
assert_eq!(ret, Some("UserService"));
}
#[test]
fn parses_string_literal_key() {
let src = r#"<?php
namespace PHPSTORM_META {
override(\App\Container::get(0), map([
'UserService' => \App\UserService::class,
]));
}"#;
let meta = parse_meta(src);
let ret = meta.resolve_return_type("Container", "get", "UserService");
assert_eq!(ret, Some("UserService"));
}
#[test]
fn wildcard_fallback() {
let src = r#"<?php
namespace PHPSTORM_META {
override(\App\Container::make(0), map([
'' => \stdClass::class,
]));
}"#;
let meta = parse_meta(src);
let ret = meta.resolve_return_type("Container", "make", "Anything");
assert_eq!(ret, Some("stdClass"));
}
#[test]
fn no_match_returns_none() {
let src = r#"<?php
namespace PHPSTORM_META {
override(\App\Container::make(0), map([
'Foo' => \Foo::class,
]));
}"#;
let meta = parse_meta(src);
let ret = meta.resolve_return_type("Container", "make", "Bar");
assert!(ret.is_none());
}
}