use std::cell::RefCell;
use std::collections::HashMap;
mod ast_update;
mod classes;
pub(crate) mod error_format;
mod functions;
mod use_statements;
use mago_span::HasSpan;
use mago_syntax::ast::*;
use crate::php_type::PhpType;
use crate::types::*;
use crate::util::strip_fqn_prefix;
pub(crate) struct DocblockCtx<'a> {
pub trivias: &'a [Trivia<'a>],
pub content: &'a str,
pub php_version: Option<PhpVersion>,
pub use_map: HashMap<String, String>,
pub namespace: Option<String>,
}
const ATTR_ELEMENT_AVAILABLE: &str = "PhpStormStubsElementAvailable";
const ATTR_LANGUAGE_LEVEL_TYPE_AWARE: &str = "LanguageLevelTypeAware";
const DEPRECATED_FQNS: &[&str] = &["Deprecated", "JetBrains\\PhpStorm\\Deprecated"];
impl DocblockCtx<'_> {
fn resolve_attr_last_segment(&self, short_name: &str) -> Option<&str> {
let fqn = self.use_map.get(short_name)?;
Some(fqn.rsplit('\\').next().unwrap_or(fqn))
}
pub(crate) fn is_element_available_attr(&self, attr_short_name: &str) -> bool {
let canonical = self
.resolve_attr_last_segment(attr_short_name)
.unwrap_or(attr_short_name);
canonical == ATTR_ELEMENT_AVAILABLE
}
fn is_language_level_type_aware_attr(&self, attr_short_name: &str) -> bool {
let canonical = self
.resolve_attr_last_segment(attr_short_name)
.unwrap_or(attr_short_name);
canonical == ATTR_LANGUAGE_LEVEL_TYPE_AWARE
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct VersionAvailability {
pub from: Option<PhpVersion>,
pub to: Option<PhpVersion>,
}
pub(crate) fn is_available_for_version(
attribute_lists: &Sequence<'_, attribute::AttributeList<'_>>,
ctx: &DocblockCtx<'_>,
php_version: PhpVersion,
) -> bool {
if let Some(avail) = extract_version_availability(attribute_lists, ctx) {
php_version.matches_range(avail.from, avail.to)
} else {
true
}
}
pub(crate) fn is_removed_for_version(
node: &impl HasSpan,
ctx: &DocblockCtx<'_>,
php_version: PhpVersion,
) -> bool {
let docblock_text = crate::docblock::get_docblock_text_for_node(ctx.trivias, ctx.content, node);
if let Some(text) = docblock_text
&& let Some(removed_ver) = crate::docblock::extract_removed_version(text)
{
return php_version >= removed_ver;
}
false
}
pub(crate) fn is_param_available_for_version(
param: &function_like::parameter::FunctionLikeParameter<'_>,
ctx: &DocblockCtx<'_>,
php_version: PhpVersion,
) -> bool {
if let Some(avail) = extract_version_availability(¶m.attribute_lists, ctx) {
php_version.matches_range(avail.from, avail.to)
} else {
true
}
}
fn extract_version_availability(
attribute_lists: &Sequence<'_, attribute::AttributeList<'_>>,
ctx: &DocblockCtx<'_>,
) -> Option<VersionAvailability> {
for attr_list in attribute_lists.iter() {
for attr in attr_list.attributes.iter() {
if !ctx.is_element_available_attr(attr.name.last_segment()) {
continue;
}
let arg_list = attr.argument_list.as_ref()?;
let mut from: Option<PhpVersion> = None;
let mut to: Option<PhpVersion> = None;
for arg in arg_list.arguments.iter() {
match arg {
argument::Argument::Named(named) => {
let name = named.name.value.to_string();
let value = extract_string_literal_value(named.value, ctx.content);
if let Some(ver_str) = value {
let ver = PhpVersion::from_composer_constraint(&ver_str);
match name.as_str() {
"from" => from = ver,
"to" => to = ver,
_ => {}
}
}
}
argument::Argument::Positional(positional) => {
let value = extract_string_literal_value(positional.value, ctx.content);
if let Some(ver_str) = value {
from = PhpVersion::from_composer_constraint(&ver_str);
}
}
}
}
return Some(VersionAvailability { from, to });
}
}
None
}
pub(crate) fn extract_language_level_type(
attribute_lists: &Sequence<'_, attribute::AttributeList<'_>>,
ctx: &DocblockCtx<'_>,
php_version: PhpVersion,
) -> Option<PhpType> {
for attr_list in attribute_lists.iter() {
for attr in attr_list.attributes.iter() {
if !ctx.is_language_level_type_aware_attr(attr.name.last_segment()) {
continue;
}
let arg_list = attr.argument_list.as_ref()?;
let mut default_type: Option<String> = None;
let mut version_map: Vec<(PhpVersion, String)> = Vec::new();
for arg in arg_list.arguments.iter() {
match arg {
argument::Argument::Named(named) => {
let name = named.name.value.to_string();
if name == "default" {
default_type = extract_string_literal_value(named.value, ctx.content);
}
}
argument::Argument::Positional(positional) => {
extract_version_type_pairs(positional.value, ctx.content, &mut version_map);
}
}
}
let mut best: Option<(PhpVersion, &str)> = None;
for (ver, type_str) in &version_map {
if *ver <= php_version && (best.is_none() || best.is_some_and(|(b, _)| *ver > b)) {
best = Some((*ver, type_str));
}
}
if let Some((_, type_str)) = best {
return if type_str.is_empty() {
None
} else {
Some(PhpType::parse(type_str))
};
}
if let Some(ref d) = default_type {
return if d.is_empty() {
None
} else {
Some(PhpType::parse(d))
};
}
return None;
}
}
None
}
pub(crate) fn extract_language_level_type_for_param(
param: &function_like::parameter::FunctionLikeParameter<'_>,
ctx: &DocblockCtx<'_>,
php_version: PhpVersion,
) -> Option<PhpType> {
extract_language_level_type(¶m.attribute_lists, ctx, php_version)
}
fn extract_version_type_pairs(
expr: &Expression<'_>,
content: &str,
out: &mut Vec<(PhpVersion, String)>,
) {
let elements: Box<dyn Iterator<Item = &ArrayElement<'_>>> = match expr {
Expression::Array(arr) => Box::new(arr.elements.iter()),
Expression::LegacyArray(arr) => Box::new(arr.elements.iter()),
_ => return,
};
for elem in elements {
if let ArrayElement::KeyValue(kv) = elem {
let key = extract_string_literal_value(kv.key, content);
let value = extract_string_literal_value(kv.value, content);
if let (Some(ver_str), Some(type_str)) = (key, value)
&& let Some(ver) = PhpVersion::from_composer_constraint(&ver_str)
{
out.push((ver, type_str));
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct DeprecatedAttribute {
pub reason: Option<String>,
pub since: Option<String>,
pub replacement: Option<String>,
}
pub(crate) struct DeprecationInfo {
pub message: Option<String>,
pub replacement: Option<String>,
}
impl DeprecatedAttribute {
pub fn to_message(&self) -> String {
match (&self.reason, &self.since) {
(Some(reason), Some(since)) => format!("{} (since PHP {})", reason, since),
(Some(reason), None) => reason.clone(),
(None, Some(since)) => format!("since PHP {}", since),
(None, None) => String::new(),
}
}
}
pub(crate) fn merge_deprecation_info(
docblock_msg: Option<String>,
attribute_lists: &Sequence<'_, attribute::AttributeList<'_>>,
doc_ctx: Option<&DocblockCtx<'_>>,
) -> DeprecationInfo {
if docblock_msg.is_some() {
return DeprecationInfo {
message: docblock_msg,
replacement: None,
};
}
let Some(ctx) = doc_ctx else {
return DeprecationInfo {
message: None,
replacement: None,
};
};
let Some(attr) = extract_deprecated_attribute(attribute_lists, ctx) else {
return DeprecationInfo {
message: None,
replacement: None,
};
};
if let Some(since_str) = &attr.since
&& let Some(target) = ctx.php_version
&& let Some(since_ver) = PhpVersion::from_composer_constraint(since_str)
&& (target.major, target.minor) < (since_ver.major, since_ver.minor)
{
return DeprecationInfo {
message: None,
replacement: None,
};
}
DeprecationInfo {
message: Some(attr.to_message()),
replacement: attr.replacement,
}
}
pub(crate) fn extract_deprecated_attribute(
attribute_lists: &Sequence<'_, attribute::AttributeList<'_>>,
ctx: &DocblockCtx<'_>,
) -> Option<DeprecatedAttribute> {
for attr_list in attribute_lists.iter() {
for attr in attr_list.attributes.iter() {
if !is_known_deprecated_attr(&attr.name, ctx) {
continue;
}
let Some(arg_list) = attr.argument_list.as_ref() else {
return Some(DeprecatedAttribute::default());
};
let mut reason: Option<String> = None;
let mut since: Option<String> = None;
let mut replacement: Option<String> = None;
for arg in arg_list.arguments.iter() {
match arg {
argument::Argument::Named(named) => {
let name = named.name.value.to_string();
let value = extract_string_literal_value(named.value, ctx.content);
match name.as_str() {
"reason" | "message" => reason = value,
"since" => since = value,
"replacement" => replacement = value,
_ => {}
}
}
argument::Argument::Positional(positional) => {
if reason.is_none() {
reason = extract_string_literal_value(positional.value, ctx.content);
}
}
}
}
return Some(DeprecatedAttribute {
reason,
since,
replacement,
});
}
}
None
}
fn is_known_deprecated_attr(name: &Identifier<'_>, ctx: &DocblockCtx<'_>) -> bool {
match name {
Identifier::FullyQualified(fq) => {
let stripped = strip_fqn_prefix(fq.value);
DEPRECATED_FQNS.contains(&stripped)
}
Identifier::Qualified(q) => {
let first_seg = q.value.split('\\').next().unwrap_or(q.value);
if let Some(resolved_prefix) = ctx.use_map.get(first_seg) {
let rest = &q.value[first_seg.len()..]; let fqn = format!("{}{}", resolved_prefix, rest);
DEPRECATED_FQNS.contains(&fqn.as_str())
} else {
let fqn = if let Some(ns) = &ctx.namespace {
format!("{}\\{}", ns, q.value)
} else {
q.value.to_string()
};
DEPRECATED_FQNS.contains(&fqn.as_str())
}
}
Identifier::Local(local) => {
if let Some(fqn) = ctx.use_map.get(local.value) {
DEPRECATED_FQNS.contains(&fqn.as_str())
} else {
let fqn = if let Some(ns) = &ctx.namespace {
format!("{}\\{}", ns, local.value)
} else {
local.value.to_string()
};
DEPRECATED_FQNS.contains(&fqn.as_str())
}
}
}
}
fn extract_string_literal_value(
expr: &expression::Expression<'_>,
content: &str,
) -> Option<String> {
let span = expr.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
let raw = content.get(start..end)?;
let trimmed = raw.trim();
if (trimmed.starts_with('\'') && trimmed.ends_with('\''))
|| (trimmed.starts_with('"') && trimmed.ends_with('"'))
{
Some(trimmed[1..trimmed.len() - 1].to_string())
} else {
Some(trimmed.to_string())
}
}
struct ParseCacheEntry {
content: String,
arena: Option<bumpalo::Bump>,
program_ptr: Option<*const ()>,
}
unsafe impl Send for ParseCacheEntry {}
thread_local! {
static PARSE_CACHE: RefCell<Option<ParseCacheEntry>> = const { RefCell::new(None) };
}
pub(crate) struct ParseCacheGuard {
owns_cache: bool,
}
impl Drop for ParseCacheGuard {
fn drop(&mut self) {
if self.owns_cache {
PARSE_CACHE.with(|cell| {
*cell.borrow_mut() = None;
});
}
}
}
pub(crate) fn with_parse_cache(content: &str) -> ParseCacheGuard {
let already_active = PARSE_CACHE.with(|cell| cell.borrow().is_some());
if already_active {
return ParseCacheGuard { owns_cache: false };
}
PARSE_CACHE.with(|cell| {
*cell.borrow_mut() = Some(ParseCacheEntry {
content: content.to_string(),
arena: None,
program_ptr: None,
});
});
ParseCacheGuard { owns_cache: true }
}
pub(crate) fn with_parsed_program<T: Default>(
content: &str,
method_name: &str,
f: impl FnOnce(&Program<'_>, &str) -> T,
) -> T {
let cache_state: u8 = PARSE_CACHE.with(|cell| {
let borrow = cell.borrow();
match borrow.as_ref() {
Some(e) if e.content == content => {
if e.program_ptr.is_some() {
2
} else {
1
}
}
_ => 0,
}
});
if cache_state == 1 {
PARSE_CACHE.with(|cell| {
let mut borrow = cell.borrow_mut();
let entry = borrow.as_mut().unwrap();
let arena = bumpalo::Bump::new();
let file_id = mago_database::file::FileId::new("input.php");
let program = mago_syntax::parser::parse_file_content(&arena, file_id, &entry.content);
let program_ptr: *const () = (program as *const Program<'_>).cast();
entry.program_ptr = Some(program_ptr);
entry.arena = Some(arena);
});
}
if cache_state >= 1 {
return PARSE_CACHE.with(|cell| {
let borrow = cell.borrow();
let entry = borrow.as_ref().unwrap();
let program: &Program<'_> =
unsafe { &*(entry.program_ptr.unwrap().cast::<Program<'_>>()) };
f(program, &entry.content)
});
}
let content_owned = content.to_string();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let arena = bumpalo::Bump::new();
let file_id = mago_database::file::FileId::new("input.php");
let program = mago_syntax::parser::parse_file_content(&arena, file_id, &content_owned);
f(program, &content_owned)
}));
match result {
Ok(value) => value,
Err(_) => {
tracing::error!("PHPantom: parser panicked in {}", method_name);
T::default()
}
}
}
pub(crate) fn extract_hint_type(hint: &Hint) -> PhpType {
match hint {
Hint::Identifier(ident) => PhpType::Named(ident.value().to_string()),
Hint::Nullable(nullable) => PhpType::Nullable(Box::new(extract_hint_type(nullable.hint))),
Hint::Union(union) => {
let mut members = Vec::new();
collect_union_members(union.left, &mut members);
collect_union_members(union.right, &mut members);
PhpType::Union(members)
}
Hint::Intersection(intersection) => {
let mut members = Vec::new();
collect_intersection_members(intersection.left, &mut members);
collect_intersection_members(intersection.right, &mut members);
PhpType::Intersection(members)
}
Hint::Null(_) => PhpType::null(),
Hint::True(_) => PhpType::true_(),
Hint::False(_) => PhpType::false_(),
Hint::Array(_) => PhpType::array(),
Hint::Callable(_) => PhpType::callable(),
Hint::Static(_) => PhpType::static_(),
Hint::Self_(_) => PhpType::self_(),
Hint::Parent(_) => PhpType::parent_(),
Hint::Void(_) => PhpType::void(),
Hint::Never(_) => PhpType::never(),
Hint::Float(_) => PhpType::float(),
Hint::Bool(_) => PhpType::bool(),
Hint::Integer(_) => PhpType::int(),
Hint::String(_) => PhpType::string(),
Hint::Object(_) => PhpType::object(),
Hint::Mixed(_) => PhpType::mixed(),
Hint::Iterable(_) => PhpType::iterable(),
Hint::Parenthesized(paren) => extract_hint_type(paren.hint),
}
}
fn collect_union_members(hint: &Hint, members: &mut Vec<PhpType>) {
match hint {
Hint::Union(union) => {
collect_union_members(union.left, members);
collect_union_members(union.right, members);
}
other => members.push(extract_hint_type(other)),
}
}
fn collect_intersection_members(hint: &Hint, members: &mut Vec<PhpType>) {
match hint {
Hint::Intersection(intersection) => {
collect_intersection_members(intersection.left, members);
collect_intersection_members(intersection.right, members);
}
other => members.push(extract_hint_type(other)),
}
}
pub(crate) fn extract_parameters(
parameter_list: &FunctionLikeParameterList,
content: Option<&str>,
php_version: Option<PhpVersion>,
doc_ctx: Option<&DocblockCtx<'_>>,
) -> Vec<ParameterInfo> {
parameter_list
.parameters
.iter()
.filter(|param| {
if let Some(ver) = php_version
&& let Some(ctx) = doc_ctx
{
is_param_available_for_version(param, ctx, ver)
} else {
true
}
})
.map(|param| {
let name = param.variable.name.to_string();
let is_variadic = param.ellipsis.is_some();
let is_reference = param.ampersand.is_some();
let has_default = param.default_value.is_some();
let is_required = !has_default && !is_variadic;
let native_type_hint = param.hint.as_ref().map(|h| extract_hint_type(h));
let type_hint = if let Some(ver) = php_version
&& let Some(ctx) = doc_ctx
{
extract_language_level_type_for_param(param, ctx, ver)
.or_else(|| native_type_hint.clone())
} else {
native_type_hint.clone()
};
let default_value = content.and_then(|src| {
let dv = param.default_value.as_ref()?;
let span = dv.value.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
src.get(start..end).map(|s| s.trim().to_string())
});
ParameterInfo {
name,
is_required,
native_type_hint: native_type_hint.clone(),
type_hint,
description: None,
default_value,
is_variadic,
is_reference,
closure_this_type: None,
}
})
.collect()
}
pub(crate) fn extract_visibility<'a>(
modifiers: impl Iterator<Item = &'a Modifier<'a>>,
) -> Visibility {
for m in modifiers {
if m.is_private() {
return Visibility::Private;
}
if m.is_protected() {
return Visibility::Protected;
}
if m.is_public() {
return Visibility::Public;
}
}
Visibility::Public
}
pub(crate) fn extract_property_info(property: &Property) -> Vec<PropertyInfo> {
let is_static = property.modifiers().iter().any(|m| m.is_static());
let visibility = extract_visibility(property.modifiers().iter());
let native_hint = property.hint().map(|h| extract_hint_type(h));
property
.variables()
.iter()
.map(|var| {
let raw_name = var.name.to_string();
let name = if let Some(stripped) = raw_name.strip_prefix('$') {
stripped.to_string()
} else {
raw_name
};
PropertyInfo {
name,
name_offset: var.span.start.offset,
type_hint: native_hint.clone(),
native_type_hint: native_hint.clone(),
description: None,
is_static,
visibility,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
is_virtual: false,
}
})
.collect()
}
use crate::Backend;
impl Backend {
pub fn parse_php(&self, content: &str) -> Vec<ClassInfo> {
Self::parse_php_versioned(content, None)
}
pub fn parse_php_versioned(content: &str, php_version: Option<PhpVersion>) -> Vec<ClassInfo> {
Self::parse_php_versioned_with_namespaces(content, php_version)
.into_iter()
.map(|(cls, _ns)| cls)
.collect()
}
pub fn parse_php_versioned_with_namespaces(
content: &str,
php_version: Option<PhpVersion>,
) -> Vec<(ClassInfo, Option<String>)> {
with_parsed_program(content, "parse_php", |program, content| {
let mut use_map = HashMap::new();
Self::extract_use_statements_from_statements(program.statements.iter(), &mut use_map);
let namespace = Self::extract_namespace_from_statements(program.statements.iter());
let doc_ctx = DocblockCtx {
trivias: program.trivia.as_slice(),
content,
php_version,
use_map,
namespace,
};
let mut result: Vec<(ClassInfo, Option<String>)> = Vec::new();
for statement in program.statements.iter() {
match statement {
Statement::Namespace(ns) => {
let block_ns: Option<String> = ns
.name
.as_ref()
.map(|ident| ident.value().to_string())
.filter(|n| !n.is_empty());
let mut block_classes = Vec::new();
Self::extract_classes_from_statements(
ns.statements().iter(),
&mut block_classes,
Some(&doc_ctx),
);
for cls in block_classes {
result.push((cls, block_ns.clone()));
}
}
Statement::Class(_)
| Statement::Interface(_)
| Statement::Trait(_)
| Statement::Enum(_) => {
let mut top_classes = Vec::new();
Self::extract_classes_from_statements(
std::iter::once(statement),
&mut top_classes,
Some(&doc_ctx),
);
for cls in top_classes {
result.push((cls, None));
}
}
_ => {}
}
}
result
})
}
pub fn parse_functions(&self, content: &str) -> Vec<FunctionInfo> {
self.parse_functions_versioned(content, None)
}
pub fn parse_functions_versioned(
&self,
content: &str,
php_version: Option<PhpVersion>,
) -> Vec<FunctionInfo> {
with_parsed_program(content, "parse_functions", |program, content| {
let mut use_map = HashMap::new();
Self::extract_use_statements_from_statements(program.statements.iter(), &mut use_map);
let namespace = Self::extract_namespace_from_statements(program.statements.iter());
let doc_ctx = DocblockCtx {
trivias: program.trivia.as_slice(),
content,
php_version,
use_map,
namespace,
};
let mut functions = Vec::new();
Self::extract_functions_from_statements(
program.statements.iter(),
&mut functions,
&None,
Some(&doc_ctx),
);
functions
})
}
pub fn parse_defines(&self, content: &str) -> Vec<(String, u32, Option<String>)> {
with_parsed_program(content, "parse_defines", |program, content| {
let mut defines = Vec::new();
Self::extract_defines_from_statements(program.statements.iter(), &mut defines, content);
defines
})
}
pub(crate) fn parse_use_statements(&self, content: &str) -> HashMap<String, String> {
with_parsed_program(content, "parse_use_statements", |program, _content| {
let mut use_map = HashMap::new();
Self::extract_use_statements_from_statements(program.statements.iter(), &mut use_map);
use_map
})
}
pub(crate) fn parse_namespace(&self, content: &str) -> Option<String> {
with_parsed_program(content, "parse_namespace", |program, _content| {
Self::extract_namespace_from_statements(program.statements.iter())
})
}
}