use std::collections::HashMap;
use crate::php_type::PhpType;
use crate::types::TypeAliasDef;
use crate::util::strip_fqn_prefix;
use mago_span::HasSpan;
use mago_syntax::ast::attribute::AttributeList;
use mago_syntax::ast::class_like::enum_case::EnumCaseItem;
use mago_syntax::ast::class_like::member::ClassLikeMember;
use mago_syntax::ast::class_like::method::{Method, MethodBody};
use mago_syntax::ast::class_like::property::{Property, PropertyItem};
use mago_syntax::ast::class_like::trait_use::{
TraitUseAdaptation, TraitUseMethodReference, TraitUseSpecification,
};
use mago_syntax::ast::sequence::Sequence;
fn has_scope_attribute(method: &Method<'_>) -> bool {
for attr_list in method.attribute_lists.iter() {
for attr in attr_list.attributes.iter() {
if attr.name.last_segment() == "Scope" {
return true;
}
}
}
false
}
fn extract_attribute_targets(
attribute_lists: &Sequence<'_, AttributeList<'_>>,
content: &str,
) -> u8 {
use crate::types::attribute_target;
for attr_list in attribute_lists.iter() {
for attr in attr_list.attributes.iter() {
let short = attr.name.last_segment();
if short != "Attribute" {
continue;
}
let Some(arg_list) = attr.argument_list.as_ref() else {
return attribute_target::TARGET_ALL;
};
let Some(first_arg) = arg_list.arguments.first() else {
return attribute_target::TARGET_ALL;
};
let span = first_arg.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
let Some(text) = content.get(start..end) else {
return attribute_target::TARGET_ALL;
};
return parse_attribute_target_flags(text);
}
}
0
}
fn parse_attribute_target_flags(text: &str) -> u8 {
use crate::types::attribute_target;
let text = text.trim();
if let Ok(n) = text.parse::<u8>() {
return n;
}
let mut flags: u8 = 0;
for part in text.split('|') {
let part = part.trim();
let constant = part
.strip_prefix("Attribute::")
.or_else(|| part.strip_prefix("\\Attribute::"))
.or_else(|| part.strip_prefix("self::"))
.unwrap_or(part);
flags |= match constant {
"TARGET_CLASS" => attribute_target::TARGET_CLASS,
"TARGET_FUNCTION" => attribute_target::TARGET_FUNCTION,
"TARGET_METHOD" => attribute_target::TARGET_METHOD,
"TARGET_PROPERTY" => attribute_target::TARGET_PROPERTY,
"TARGET_CLASS_CONSTANT" => attribute_target::TARGET_CLASS_CONSTANT,
"TARGET_PARAMETER" => attribute_target::TARGET_PARAMETER,
"TARGET_ALL" => attribute_target::TARGET_ALL,
_ => {
constant.trim().parse::<u8>().unwrap_or_default()
}
};
}
if flags == 0 {
attribute_target::TARGET_ALL
} else {
flags
}
}
use mago_syntax::ast::*;
use crate::Backend;
use crate::docblock;
use crate::types::*;
use crate::virtual_members::laravel::infer_relationship_from_body;
use super::{
DeprecationInfo, DocblockCtx, extract_hint_type, extract_parameters, extract_property_info,
extract_visibility, is_available_for_version, is_removed_for_version, merge_deprecation_info,
};
#[derive(Default)]
struct ClassDocblockInfo {
deprecation_message: Option<String>,
template_params: Vec<String>,
template_param_bounds: HashMap<String, PhpType>,
template_param_defaults: HashMap<String, PhpType>,
extends_generics: Vec<(String, Vec<PhpType>)>,
implements_generics: Vec<(String, Vec<PhpType>)>,
use_generics: Vec<(String, Vec<PhpType>)>,
type_aliases: HashMap<String, TypeAliasDef>,
mixins: Vec<String>,
mixin_generics: Vec<(String, Vec<PhpType>)>,
links: Vec<String>,
see_refs: Vec<String>,
raw_docblock: Option<String>,
}
fn extract_class_docblock<'a>(
node: &impl HasSpan,
doc_ctx: Option<&DocblockCtx<'a>>,
) -> ClassDocblockInfo {
let Some(ctx) = doc_ctx else {
return ClassDocblockInfo::default();
};
let Some(doc_text) = docblock::get_docblock_text_for_node(ctx.trivias, ctx.content, node)
else {
return ClassDocblockInfo::default();
};
let Some(info) = docblock::parse_docblock_for_tags(doc_text) else {
return ClassDocblockInfo::default();
};
let params_full = docblock::extract_template_params_full_from_info(&info);
let template_params: Vec<String> = params_full.iter().map(|(n, _, _, _)| n.clone()).collect();
let template_param_bounds: HashMap<String, PhpType> = params_full
.iter()
.filter_map(|(name, bound, _, _)| bound.as_ref().map(|b| (name.clone(), b.clone())))
.collect();
let template_param_defaults: HashMap<String, PhpType> = params_full
.into_iter()
.filter_map(|(name, _, _, default)| default.map(|d| (name, d)))
.collect();
let mixin_data = docblock::extract_mixin_tags_from_info(&info);
let mixins: Vec<String> = mixin_data.iter().map(|(name, _)| name.clone()).collect();
let mixin_generics: Vec<(String, Vec<PhpType>)> = mixin_data
.into_iter()
.filter(|(_, args)| !args.is_empty())
.collect();
ClassDocblockInfo {
deprecation_message: docblock::extract_deprecation_message_from_info(&info),
template_params,
template_param_bounds,
template_param_defaults,
extends_generics: docblock::extract_generics_tag_from_info(&info, "@extends"),
implements_generics: docblock::extract_generics_tag_from_info(&info, "@implements"),
use_generics: docblock::extract_generics_tag_from_info(&info, "@use"),
type_aliases: docblock::extract_type_aliases_from_info(&info),
mixins,
mixin_generics,
links: docblock::extract_link_urls_from_info(&info),
see_refs: docblock::extract_see_references_from_info(&info),
raw_docblock: Some(doc_text.to_string()),
}
}
fn extract_collected_by_attribute(
attribute_lists: &Sequence<'_, AttributeList<'_>>,
content: &str,
) -> Option<String> {
for attr_list in attribute_lists.iter() {
for attr in attr_list.attributes.iter() {
let short = attr.name.last_segment();
if short != "CollectedBy" {
continue;
}
let arg_list = attr.argument_list.as_ref()?;
let first_arg = arg_list.arguments.first()?;
let span = first_arg.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
let text = content.get(start..end)?;
let class_name = text.trim_end_matches("::class").trim();
if !class_name.is_empty() {
return Some(class_name.to_string());
}
}
}
None
}
fn extract_custom_collection(
attribute_lists: &Sequence<'_, AttributeList<'_>>,
use_generics: &[(String, Vec<PhpType>)],
methods: &[MethodInfo],
content: &str,
) -> Option<PhpType> {
if let Some(name) = extract_collected_by_attribute(attribute_lists, content) {
return Some(PhpType::Named(name));
}
for (trait_name, args) in use_generics {
let short = trait_name.rsplit('\\').next().unwrap_or(trait_name);
if short == "HasCollection" && !args.is_empty() {
return Some(args[0].clone());
}
}
extract_custom_collection_from_new_collection(methods)
}
fn extract_custom_collection_from_new_collection(methods: &[MethodInfo]) -> Option<PhpType> {
let method = methods.iter().find(|m| m.name == "newCollection")?;
let return_type = method.return_type.as_ref()?;
let base = return_type.base_name()?;
if base == "Illuminate\\Database\\Eloquent\\Collection" || base == "Collection" {
return None;
}
if base.is_empty() {
return None;
}
Some(return_type.clone())
}
fn extract_casts_definitions<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
content: &str,
) -> Vec<(String, String)> {
let mut property_text: Option<String> = None;
let mut method_text: Option<String> = None;
for member in members {
match member {
ClassLikeMember::Property(Property::Plain(plain)) => {
for item in plain.items.iter() {
let var_name = item.variable().name.to_string();
let stripped = var_name.strip_prefix('$').unwrap_or(&var_name);
if stripped != "casts" {
continue;
}
if let PropertyItem::Concrete(concrete) = item {
let span = concrete.value.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
if let Some(text) = content.get(start..end) {
property_text = Some(text.to_string());
}
}
}
}
ClassLikeMember::Method(method) if method.name.value == "casts" => {
if let MethodBody::Concrete(block) = &method.body {
let start = block.left_brace.start.offset as usize;
let end = block.right_brace.end.offset as usize;
if let Some(text) = content.get(start..end) {
method_text = Some(text.to_string());
}
}
}
_ => {}
}
}
let mut merged: Vec<(String, String)> = Vec::new();
if let Some(ref text) = property_text {
merged = parse_casts_array(text);
}
if let Some(ref text) = method_text
&& let Some(arr_start) = text.find("return")
{
let after_return = &text[arr_start + 6..];
if let Some(bracket_pos) = after_return.find('[') {
let array_text = &after_return[bracket_pos..];
let method_defs = parse_casts_array(array_text);
for (key, value) in method_defs {
if let Some(existing) = merged.iter_mut().find(|(k, _)| *k == key) {
existing.1 = value;
} else {
merged.push((key, value));
}
}
}
}
merged
}
fn parse_casts_array(text: &str) -> Vec<(String, String)> {
let mut results = Vec::new();
let trimmed = text.trim();
let inner = if let Some(s) = trimmed.strip_prefix('[') {
s.strip_suffix(']').unwrap_or(s)
} else {
return results;
};
for segment in inner.split(',') {
let segment = segment.trim();
if segment.is_empty() {
continue;
}
let Some(arrow_pos) = segment.find("=>") else {
continue;
};
let key_part = segment[..arrow_pos].trim();
let value_part = segment[arrow_pos + 2..].trim();
let key = extract_string_literal(key_part);
let value = extract_string_literal(value_part);
if let (Some(k), Some(v)) = (key, value)
&& !k.is_empty()
&& !v.is_empty()
{
results.push((k, v));
}
}
results
}
fn extract_string_literal(text: &str) -> Option<String> {
let t = text.trim();
if ((t.starts_with('\'') && t.ends_with('\'')) || (t.starts_with('"') && t.ends_with('"')))
&& t.len() >= 2
{
return Some(t[1..t.len() - 1].to_string());
}
if let Some(class_pos) = t.find("::class") {
let before = t[..class_pos].trim();
let name = strip_fqn_prefix(before);
if !name.is_empty() {
return Some(name.to_string());
}
}
None
}
fn extract_attributes_definitions<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
content: &str,
) -> Vec<(String, PhpType)> {
for member in members {
if let ClassLikeMember::Property(Property::Plain(plain)) = member {
for item in plain.items.iter() {
let var_name = item.variable().name.to_string();
let stripped = var_name.strip_prefix('$').unwrap_or(&var_name);
if stripped != "attributes" {
continue;
}
if let PropertyItem::Concrete(concrete) = item {
let span = concrete.value.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
if let Some(text) = content.get(start..end) {
return parse_attributes_array(text);
}
}
}
}
}
Vec::new()
}
fn parse_attributes_array(text: &str) -> Vec<(String, PhpType)> {
let mut results = Vec::new();
let trimmed = text.trim();
let inner = if let Some(s) = trimmed.strip_prefix('[') {
s.strip_suffix(']').unwrap_or(s)
} else {
return results;
};
for segment in inner.split(',') {
let segment = segment.trim();
if segment.is_empty() {
continue;
}
let Some(arrow_pos) = segment.find("=>") else {
continue;
};
let key_part = segment[..arrow_pos].trim();
let value_part = segment[arrow_pos + 2..].trim();
let Some(key) = extract_string_literal(key_part) else {
continue;
};
if key.is_empty() {
continue;
}
if let Some(php_type) = crate::util::infer_type_from_literal(value_part) {
results.push((key, php_type));
}
}
results
}
fn extract_timestamp_config<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
content: &str,
) -> (Option<bool>, Option<Option<String>>, Option<Option<String>>) {
let mut timestamps: Option<bool> = None;
let mut created_at: Option<Option<String>> = None;
let mut updated_at: Option<Option<String>> = None;
for member in members {
match member {
ClassLikeMember::Property(Property::Plain(plain)) => {
for item in plain.items.iter() {
let var_name = item.variable().name.to_string();
let stripped = var_name.strip_prefix('$').unwrap_or(&var_name);
if stripped != "timestamps" {
continue;
}
if let PropertyItem::Concrete(concrete) = item {
let span = concrete.value.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
if let Some(text) = content.get(start..end) {
let trimmed = text.trim();
if trimmed == "false" {
timestamps = Some(false);
} else if trimmed == "true" {
timestamps = Some(true);
}
}
}
}
}
ClassLikeMember::Constant(constant) => {
for item in constant.items.iter() {
let name = item.name.value.to_string();
if name != "CREATED_AT" && name != "UPDATED_AT" {
continue;
}
let span = item.value.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
let value = content.get(start..end).map(|t| t.trim());
let parsed = match value {
Some("null") | Some("NULL") => Some(None),
Some(v) => extract_string_literal(v).map(Some),
None => None,
};
if let Some(val) = parsed {
if name == "CREATED_AT" {
created_at = Some(val);
} else {
updated_at = Some(val);
}
}
}
}
_ => {}
}
}
(timestamps, created_at, updated_at)
}
fn extract_column_names<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
content: &str,
) -> Vec<String> {
let mut names = Vec::new();
let targets = ["fillable", "guarded", "hidden", "visible", "appends"];
for member in members {
if let ClassLikeMember::Property(Property::Plain(plain)) = member {
for item in plain.items.iter() {
let var_name = item.variable().name.to_string();
let stripped = var_name.strip_prefix('$').unwrap_or(&var_name);
if !targets.contains(&stripped) {
continue;
}
if let PropertyItem::Concrete(concrete) = item {
let span = concrete.value.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
if let Some(text) = content.get(start..end) {
for name in parse_string_list(text) {
if !names.contains(&name) {
names.push(name);
}
}
}
}
}
}
}
names
}
fn extract_dates_definitions<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
content: &str,
) -> Vec<String> {
let mut names = Vec::new();
for member in members {
if let ClassLikeMember::Property(Property::Plain(plain)) = member {
for item in plain.items.iter() {
let var_name = item.variable().name.to_string();
let stripped = var_name.strip_prefix('$').unwrap_or(&var_name);
if stripped != "dates" {
continue;
}
if let PropertyItem::Concrete(concrete) = item {
let span = concrete.value.span();
let start = span.start.offset as usize;
let end = span.end.offset as usize;
if let Some(text) = content.get(start..end) {
for name in parse_string_list(text) {
if !names.contains(&name) {
names.push(name);
}
}
}
}
}
}
}
names
}
fn parse_string_list(text: &str) -> Vec<String> {
let mut results = Vec::new();
let trimmed = text.trim();
let inner = if let Some(s) = trimmed.strip_prefix('[') {
s.strip_suffix(']').unwrap_or(s)
} else {
return results;
};
for segment in inner.split(',') {
let segment = segment.trim();
if segment.is_empty() {
continue;
}
if segment.contains("=>") {
continue;
}
if let Some(s) = extract_string_literal(segment)
&& !s.is_empty()
{
results.push(s);
}
}
results
}
fn infer_relationship_from_method<'a>(
method: &Method<'a>,
doc_ctx: Option<&DocblockCtx<'a>>,
) -> Option<PhpType> {
let ctx = doc_ctx?;
let MethodBody::Concrete(block) = &method.body else {
return None;
};
let start = block.left_brace.start.offset as usize;
let end = block.right_brace.end.offset as usize;
if end > ctx.content.len() || start >= end {
return None;
}
let start = ctx.content.floor_char_boundary(start);
let end = ctx.content.floor_char_boundary(end);
let body_text = &ctx.content[start..end];
infer_relationship_from_body(body_text)
}
impl Backend {
pub(crate) fn extract_classes_from_statements<'a>(
statements: impl Iterator<Item = &'a Statement<'a>>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
for statement in statements {
match statement {
Statement::Class(class) => {
if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
&& is_removed_for_version(class, ctx, ver)
{
continue;
}
let class_name = class.name.value.to_string();
let parent_class = class
.extends
.as_ref()
.and_then(|ext| ext.types.first().map(|ident| ident.value().to_string()));
let interfaces: Vec<String> = class
.implements
.as_ref()
.map(|imp| {
imp.types
.iter()
.map(|ident| ident.value().to_string())
.collect()
})
.unwrap_or_default();
let doc_info = extract_class_docblock(class, doc_ctx);
let ExtractedMembers {
methods,
properties,
constants,
used_traits,
trait_precedences,
trait_aliases,
inline_use_generics,
} = Self::extract_class_like_members(
class.members.iter(),
doc_ctx,
&doc_info.template_params,
);
let mut use_generics = doc_info.use_generics;
use_generics.extend(inline_use_generics);
let keyword_offset = class.class.span.start.offset;
let start_offset = class.left_brace.start.offset;
let end_offset = class.right_brace.end.offset;
let content = doc_ctx.map(|c| c.content).unwrap_or("");
let custom_collection = extract_custom_collection(
&class.attribute_lists,
&use_generics,
&methods,
content,
);
let casts_definitions =
extract_casts_definitions(class.members.iter(), content);
let attributes_definitions =
extract_attributes_definitions(class.members.iter(), content);
let column_names = extract_column_names(class.members.iter(), content);
let dates_definitions =
extract_dates_definitions(class.members.iter(), content);
let (timestamps, created_at_name, updated_at_name) =
extract_timestamp_config(class.members.iter(), content);
let attr_targets = extract_attribute_targets(&class.attribute_lists, content);
let class_depr = merge_deprecation_info(
doc_info.deprecation_message.clone(),
&class.attribute_lists,
doc_ctx,
);
classes.push(ClassInfo {
kind: ClassLikeKind::Class,
name: class_name,
methods: methods.into(),
properties: properties.into(),
constants: constants.into(),
start_offset,
end_offset,
keyword_offset,
parent_class,
interfaces,
used_traits,
mixins: doc_info.mixins,
mixin_generics: doc_info.mixin_generics,
is_final: class.modifiers.contains_final(),
is_abstract: class.modifiers.contains_abstract(),
deprecation_message: class_depr.message,
deprecated_replacement: class_depr.replacement,
links: doc_info.links,
see_refs: doc_info.see_refs,
template_params: doc_info.template_params,
template_param_bounds: doc_info.template_param_bounds,
template_param_defaults: doc_info.template_param_defaults,
extends_generics: doc_info.extends_generics,
implements_generics: doc_info.implements_generics,
use_generics,
type_aliases: doc_info.type_aliases,
trait_precedences,
trait_aliases,
class_docblock: doc_info.raw_docblock,
file_namespace: None,
backed_type: None,
attribute_targets: attr_targets,
laravel: Some(Box::new(LaravelMetadata {
custom_collection,
casts_definitions,
dates_definitions,
attributes_definitions,
column_names,
timestamps,
created_at_name,
updated_at_name,
})),
});
Self::find_anonymous_classes_in_members(class.members.iter(), classes, doc_ctx);
}
Statement::Interface(iface) => {
if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
&& is_removed_for_version(iface, ctx, ver)
{
continue;
}
let iface_name = iface.name.value.to_string();
let all_parents: Vec<String> = iface
.extends
.as_ref()
.map(|ext| {
ext.types
.iter()
.map(|ident| ident.value().to_string())
.collect()
})
.unwrap_or_default();
let parent_class = all_parents.first().cloned();
let doc_info = extract_class_docblock(iface, doc_ctx);
let ExtractedMembers {
methods,
properties,
constants,
used_traits,
trait_precedences,
trait_aliases,
inline_use_generics,
} = Self::extract_class_like_members(
iface.members.iter(),
doc_ctx,
&doc_info.template_params,
);
let keyword_offset = iface.interface.span.start.offset;
let start_offset = iface.left_brace.start.offset;
let end_offset = iface.right_brace.end.offset;
let iface_depr = merge_deprecation_info(
doc_info.deprecation_message.clone(),
&iface.attribute_lists,
doc_ctx,
);
classes.push(ClassInfo {
kind: ClassLikeKind::Interface,
name: iface_name,
methods: methods.into(),
properties: properties.into(),
constants: constants.into(),
start_offset,
end_offset,
keyword_offset,
parent_class,
interfaces: all_parents,
used_traits,
mixins: doc_info.mixins,
mixin_generics: doc_info.mixin_generics,
is_final: false,
is_abstract: false,
deprecation_message: iface_depr.message,
deprecated_replacement: iface_depr.replacement,
links: doc_info.links,
see_refs: doc_info.see_refs,
template_params: doc_info.template_params,
template_param_bounds: doc_info.template_param_bounds,
template_param_defaults: doc_info.template_param_defaults,
extends_generics: doc_info.extends_generics,
implements_generics: doc_info.implements_generics,
use_generics: {
let mut ug = doc_info.use_generics;
ug.extend(inline_use_generics);
ug
},
type_aliases: doc_info.type_aliases,
trait_precedences,
trait_aliases,
class_docblock: doc_info.raw_docblock,
file_namespace: None,
backed_type: None,
attribute_targets: 0,
laravel: None,
});
Self::find_anonymous_classes_in_members(iface.members.iter(), classes, doc_ctx);
}
Statement::Trait(trait_def) => {
if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
&& is_removed_for_version(trait_def, ctx, ver)
{
continue;
}
let trait_name = trait_def.name.value.to_string();
let doc_info = extract_class_docblock(trait_def, doc_ctx);
let ExtractedMembers {
methods,
properties,
constants,
used_traits,
trait_precedences,
trait_aliases,
inline_use_generics,
} = Self::extract_class_like_members(
trait_def.members.iter(),
doc_ctx,
&doc_info.template_params,
);
let keyword_offset = trait_def.r#trait.span.start.offset;
let start_offset = trait_def.left_brace.start.offset;
let end_offset = trait_def.right_brace.end.offset;
let trait_depr = merge_deprecation_info(
doc_info.deprecation_message.clone(),
&trait_def.attribute_lists,
doc_ctx,
);
classes.push(ClassInfo {
kind: ClassLikeKind::Trait,
name: trait_name,
methods: methods.into(),
properties: properties.into(),
constants: constants.into(),
start_offset,
end_offset,
keyword_offset,
parent_class: None,
interfaces: vec![],
used_traits,
mixins: doc_info.mixins,
mixin_generics: doc_info.mixin_generics,
is_final: false,
is_abstract: false,
deprecation_message: trait_depr.message,
deprecated_replacement: trait_depr.replacement,
links: doc_info.links,
see_refs: doc_info.see_refs,
template_params: doc_info.template_params,
template_param_bounds: doc_info.template_param_bounds,
template_param_defaults: doc_info.template_param_defaults,
extends_generics: vec![],
implements_generics: vec![],
use_generics: inline_use_generics,
type_aliases: HashMap::new(),
trait_precedences,
trait_aliases,
class_docblock: doc_info.raw_docblock,
file_namespace: None,
backed_type: None,
attribute_targets: 0,
laravel: None,
});
Self::find_anonymous_classes_in_members(
trait_def.members.iter(),
classes,
doc_ctx,
);
}
Statement::Enum(enum_def) => {
if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
&& is_removed_for_version(enum_def, ctx, ver)
{
continue;
}
let enum_name = enum_def.name.value.to_string();
let ExtractedMembers {
methods,
properties,
constants,
mut used_traits,
..
} = Self::extract_class_like_members(enum_def.members.iter(), doc_ctx, &[]);
let implicit_interface = if enum_def.backing_type_hint.is_some() {
"\\BackedEnum"
} else {
"\\UnitEnum"
};
used_traits.push(implicit_interface.to_string());
let doc_info = extract_class_docblock(enum_def, doc_ctx);
let interfaces: Vec<String> = enum_def
.implements
.as_ref()
.map(|imp| {
imp.types
.iter()
.map(|ident| ident.value().to_string())
.collect()
})
.unwrap_or_default();
let keyword_offset = enum_def.r#enum.span.start.offset;
let start_offset = enum_def.left_brace.start.offset;
let end_offset = enum_def.right_brace.end.offset;
let enum_depr = merge_deprecation_info(
doc_info.deprecation_message,
&enum_def.attribute_lists,
doc_ctx,
);
classes.push(ClassInfo {
kind: ClassLikeKind::Enum,
name: enum_name,
methods: methods.into(),
properties: properties.into(),
constants: constants.into(),
start_offset,
end_offset,
keyword_offset,
parent_class: None,
interfaces,
used_traits,
mixins: doc_info.mixins,
mixin_generics: doc_info.mixin_generics,
is_final: true,
is_abstract: false,
deprecation_message: enum_depr.message,
deprecated_replacement: enum_depr.replacement,
links: doc_info.links,
see_refs: doc_info.see_refs,
template_params: vec![],
template_param_bounds: HashMap::new(),
template_param_defaults: HashMap::new(),
extends_generics: vec![],
implements_generics: vec![],
use_generics: vec![],
type_aliases: HashMap::new(),
trait_precedences: vec![],
trait_aliases: vec![],
class_docblock: doc_info.raw_docblock,
file_namespace: None,
backed_type: enum_def.backing_type_hint.as_ref().and_then(|h| {
let ty = crate::parser::extract_hint_type(&h.hint);
if ty.is_string_type() {
Some(crate::types::BackedEnumType::String)
} else if ty.is_int() {
Some(crate::types::BackedEnumType::Int)
} else {
None
}
}),
attribute_targets: 0,
laravel: None,
});
Self::find_anonymous_classes_in_members(
enum_def.members.iter(),
classes,
doc_ctx,
);
}
Statement::Namespace(namespace) => {
Self::extract_classes_from_statements(
namespace.statements().iter(),
classes,
doc_ctx,
);
}
_ => {
Self::find_anonymous_classes_in_statement(statement, classes, doc_ctx);
}
}
}
}
fn extract_anonymous_class_info<'a>(
anon: &AnonymousClass<'a>,
doc_ctx: Option<&DocblockCtx<'a>>,
) -> ClassInfo {
let parent_class = anon
.extends
.as_ref()
.and_then(|ext| ext.types.first().map(|ident| ident.value().to_string()));
let interfaces: Vec<String> = anon
.implements
.as_ref()
.map(|imp| {
imp.types
.iter()
.map(|ident| ident.value().to_string())
.collect()
})
.unwrap_or_default();
let ExtractedMembers {
methods,
properties,
constants,
used_traits,
trait_precedences,
trait_aliases,
..
} = Self::extract_class_like_members(anon.members.iter(), doc_ctx, &[]);
let start_offset = anon.left_brace.start.offset;
let end_offset = anon.right_brace.end.offset;
let keyword_offset = 0;
let name = format!("__anonymous@{}", start_offset);
ClassInfo {
kind: ClassLikeKind::Class,
name,
methods: methods.into(),
properties: properties.into(),
constants: constants.into(),
start_offset,
end_offset,
keyword_offset,
parent_class,
interfaces,
used_traits,
mixins: vec![],
mixin_generics: vec![],
is_final: false,
is_abstract: false,
deprecation_message: None,
deprecated_replacement: None,
template_params: vec![],
template_param_bounds: HashMap::new(),
template_param_defaults: HashMap::new(),
extends_generics: vec![],
implements_generics: vec![],
use_generics: vec![],
type_aliases: HashMap::new(),
trait_precedences,
trait_aliases,
links: Vec::new(),
see_refs: Vec::new(),
class_docblock: None,
file_namespace: None,
backed_type: None,
attribute_targets: 0,
laravel: None,
}
}
pub(crate) fn find_anonymous_classes_in_statement<'a>(
statement: &'a Statement<'a>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
match statement {
Statement::Expression(expr_stmt) => {
Self::find_anonymous_classes_in_expression(expr_stmt.expression, classes, doc_ctx);
}
Statement::Return(ret) => {
if let Some(value) = &ret.value {
Self::find_anonymous_classes_in_expression(value, classes, doc_ctx);
}
}
Statement::Block(block) => {
Self::walk_statements_for_anonymous_classes(
block.statements.iter(),
classes,
doc_ctx,
);
}
Statement::If(if_stmt) => {
Self::find_anonymous_classes_in_if_body(&if_stmt.body, classes, doc_ctx);
}
Statement::While(while_stmt) => match &while_stmt.body {
WhileBody::Statement(stmt) => {
Self::find_anonymous_classes_in_statement(stmt, classes, doc_ctx);
}
WhileBody::ColonDelimited(body) => {
Self::walk_statements_for_anonymous_classes(
body.statements.iter(),
classes,
doc_ctx,
);
}
},
Statement::DoWhile(do_while) => {
Self::find_anonymous_classes_in_statement(do_while.statement, classes, doc_ctx);
}
Statement::For(for_stmt) => match &for_stmt.body {
ForBody::Statement(stmt) => {
Self::find_anonymous_classes_in_statement(stmt, classes, doc_ctx);
}
ForBody::ColonDelimited(body) => {
Self::walk_statements_for_anonymous_classes(
body.statements.iter(),
classes,
doc_ctx,
);
}
},
Statement::Foreach(foreach_stmt) => match &foreach_stmt.body {
ForeachBody::Statement(stmt) => {
Self::find_anonymous_classes_in_statement(stmt, classes, doc_ctx);
}
ForeachBody::ColonDelimited(body) => {
Self::walk_statements_for_anonymous_classes(
body.statements.iter(),
classes,
doc_ctx,
);
}
},
Statement::Switch(switch_stmt) => {
let cases = match &switch_stmt.body {
SwitchBody::BraceDelimited(b) => &b.cases,
SwitchBody::ColonDelimited(b) => &b.cases,
};
for case in cases.iter() {
let stmts = match case {
SwitchCase::Expression(c) => &c.statements,
SwitchCase::Default(c) => &c.statements,
};
Self::walk_statements_for_anonymous_classes(stmts.iter(), classes, doc_ctx);
}
}
Statement::Try(try_stmt) => {
Self::walk_statements_for_anonymous_classes(
try_stmt.block.statements.iter(),
classes,
doc_ctx,
);
for catch in try_stmt.catch_clauses.iter() {
Self::walk_statements_for_anonymous_classes(
catch.block.statements.iter(),
classes,
doc_ctx,
);
}
if let Some(finally) = &try_stmt.finally_clause {
Self::walk_statements_for_anonymous_classes(
finally.block.statements.iter(),
classes,
doc_ctx,
);
}
}
Statement::Function(func) => {
Self::walk_statements_for_anonymous_classes(
func.body.statements.iter(),
classes,
doc_ctx,
);
}
Statement::Class(class) => {
Self::find_anonymous_classes_in_members(class.members.iter(), classes, doc_ctx);
}
Statement::Interface(iface) => {
Self::find_anonymous_classes_in_members(iface.members.iter(), classes, doc_ctx);
}
Statement::Trait(trait_def) => {
Self::find_anonymous_classes_in_members(trait_def.members.iter(), classes, doc_ctx);
}
Statement::Enum(enum_def) => {
Self::find_anonymous_classes_in_members(enum_def.members.iter(), classes, doc_ctx);
}
Statement::Namespace(ns) => {
Self::walk_statements_for_anonymous_classes(
ns.statements().iter(),
classes,
doc_ctx,
);
}
Statement::Echo(echo) => {
for expr in echo.values.iter() {
Self::find_anonymous_classes_in_expression(expr, classes, doc_ctx);
}
}
_ => {}
}
}
fn find_anonymous_classes_in_members<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
for member in members {
if let ClassLikeMember::Method(method) = member
&& let MethodBody::Concrete(block) = &method.body
{
Self::walk_statements_for_anonymous_classes(
block.statements.iter(),
classes,
doc_ctx,
);
}
}
}
fn walk_statements_for_anonymous_classes<'a>(
statements: impl Iterator<Item = &'a Statement<'a>>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
for stmt in statements {
Self::find_anonymous_classes_in_statement(stmt, classes, doc_ctx);
}
}
fn find_anonymous_classes_in_if_body<'a>(
body: &'a IfBody<'a>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
match body {
IfBody::Statement(body) => {
Self::find_anonymous_classes_in_statement(body.statement, classes, doc_ctx);
for else_if in body.else_if_clauses.iter() {
Self::find_anonymous_classes_in_statement(else_if.statement, classes, doc_ctx);
}
if let Some(else_clause) = &body.else_clause {
Self::find_anonymous_classes_in_statement(
else_clause.statement,
classes,
doc_ctx,
);
}
}
IfBody::ColonDelimited(body) => {
Self::walk_statements_for_anonymous_classes(
body.statements.iter(),
classes,
doc_ctx,
);
for else_if in body.else_if_clauses.iter() {
Self::walk_statements_for_anonymous_classes(
else_if.statements.iter(),
classes,
doc_ctx,
);
}
if let Some(else_clause) = &body.else_clause {
Self::walk_statements_for_anonymous_classes(
else_clause.statements.iter(),
classes,
doc_ctx,
);
}
}
}
}
fn find_anonymous_classes_in_expression<'a>(
expr: &'a Expression<'a>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
match expr {
Expression::AnonymousClass(anon) => {
let info = Self::extract_anonymous_class_info(anon, doc_ctx);
classes.push(info);
Self::find_anonymous_classes_in_members(anon.members.iter(), classes, doc_ctx);
}
Expression::Assignment(assignment) => {
Self::find_anonymous_classes_in_expression(assignment.lhs, classes, doc_ctx);
Self::find_anonymous_classes_in_expression(assignment.rhs, classes, doc_ctx);
}
Expression::Parenthesized(paren) => {
Self::find_anonymous_classes_in_expression(paren.expression, classes, doc_ctx);
}
Expression::Binary(binary) => {
Self::find_anonymous_classes_in_expression(binary.lhs, classes, doc_ctx);
Self::find_anonymous_classes_in_expression(binary.rhs, classes, doc_ctx);
}
Expression::UnaryPrefix(unary) => {
Self::find_anonymous_classes_in_expression(unary.operand, classes, doc_ctx);
}
Expression::UnaryPostfix(unary) => {
Self::find_anonymous_classes_in_expression(unary.operand, classes, doc_ctx);
}
Expression::Conditional(cond) => {
Self::find_anonymous_classes_in_expression(cond.condition, classes, doc_ctx);
if let Some(then) = &cond.then {
Self::find_anonymous_classes_in_expression(then, classes, doc_ctx);
}
Self::find_anonymous_classes_in_expression(cond.r#else, classes, doc_ctx);
}
Expression::Call(call) => {
Self::find_anonymous_classes_in_argument_list(
call.get_argument_list(),
classes,
doc_ctx,
);
match call {
Call::Function(fc) => {
Self::find_anonymous_classes_in_expression(fc.function, classes, doc_ctx);
}
Call::Method(mc) => {
Self::find_anonymous_classes_in_expression(mc.object, classes, doc_ctx);
}
Call::NullSafeMethod(nmc) => {
Self::find_anonymous_classes_in_expression(nmc.object, classes, doc_ctx);
}
Call::StaticMethod(smc) => {
Self::find_anonymous_classes_in_expression(smc.class, classes, doc_ctx);
}
}
}
Expression::Instantiation(inst) => {
Self::find_anonymous_classes_in_expression(inst.class, classes, doc_ctx);
if let Some(args) = &inst.argument_list {
Self::find_anonymous_classes_in_argument_list(args, classes, doc_ctx);
}
}
Expression::Throw(throw) => {
Self::find_anonymous_classes_in_expression(throw.exception, classes, doc_ctx);
}
Expression::Clone(clone) => {
Self::find_anonymous_classes_in_expression(clone.object, classes, doc_ctx);
}
Expression::Yield(yld) => match yld {
Yield::Value(yv) => {
if let Some(value) = &yv.value {
Self::find_anonymous_classes_in_expression(value, classes, doc_ctx);
}
}
Yield::Pair(yp) => {
Self::find_anonymous_classes_in_expression(yp.key, classes, doc_ctx);
Self::find_anonymous_classes_in_expression(yp.value, classes, doc_ctx);
}
Yield::From(yf) => {
Self::find_anonymous_classes_in_expression(yf.iterator, classes, doc_ctx);
}
},
Expression::Match(match_expr) => {
Self::find_anonymous_classes_in_expression(match_expr.expression, classes, doc_ctx);
for arm in match_expr.arms.iter() {
let arm_expr = arm.expression();
Self::find_anonymous_classes_in_expression(arm_expr, classes, doc_ctx);
}
}
Expression::Array(array) => {
for element in array.elements.iter() {
Self::find_anonymous_classes_in_array_element(element, classes, doc_ctx);
}
}
Expression::LegacyArray(array) => {
for element in array.elements.iter() {
Self::find_anonymous_classes_in_array_element(element, classes, doc_ctx);
}
}
Expression::ArrayAccess(access) => {
Self::find_anonymous_classes_in_expression(access.array, classes, doc_ctx);
Self::find_anonymous_classes_in_expression(access.index, classes, doc_ctx);
}
Expression::Access(access) => match access {
Access::Property(pa) => {
Self::find_anonymous_classes_in_expression(pa.object, classes, doc_ctx);
}
Access::NullSafeProperty(npa) => {
Self::find_anonymous_classes_in_expression(npa.object, classes, doc_ctx);
}
Access::StaticProperty(spa) => {
Self::find_anonymous_classes_in_expression(spa.class, classes, doc_ctx);
}
Access::ClassConstant(cca) => {
Self::find_anonymous_classes_in_expression(cca.class, classes, doc_ctx);
}
},
Expression::Closure(closure) => {
Self::walk_statements_for_anonymous_classes(
closure.body.statements.iter(),
classes,
doc_ctx,
);
}
Expression::ArrowFunction(arrow) => {
Self::find_anonymous_classes_in_expression(arrow.expression, classes, doc_ctx);
}
Expression::Literal(_)
| Expression::Variable(_)
| Expression::Identifier(_)
| Expression::ConstantAccess(_)
| Expression::MagicConstant(_)
| Expression::Parent(_)
| Expression::Static(_)
| Expression::Self_(_)
| Expression::Error(_) => {}
_ => {}
}
}
fn find_anonymous_classes_in_argument_list<'a>(
args: &'a ArgumentList<'a>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
for arg in args.arguments.iter() {
let expr = match arg {
Argument::Positional(pos) => pos.value,
Argument::Named(named) => named.value,
};
Self::find_anonymous_classes_in_expression(expr, classes, doc_ctx);
}
}
fn find_anonymous_classes_in_array_element<'a>(
element: &'a ArrayElement<'a>,
classes: &mut Vec<ClassInfo>,
doc_ctx: Option<&DocblockCtx<'a>>,
) {
match element {
ArrayElement::KeyValue(kv) => {
Self::find_anonymous_classes_in_expression(kv.key, classes, doc_ctx);
Self::find_anonymous_classes_in_expression(kv.value, classes, doc_ctx);
}
ArrayElement::Value(v) => {
Self::find_anonymous_classes_in_expression(v.value, classes, doc_ctx);
}
ArrayElement::Variadic(v) => {
Self::find_anonymous_classes_in_expression(v.value, classes, doc_ctx);
}
ArrayElement::Missing(_) => {}
}
}
pub(crate) fn extract_class_like_members<'a>(
members: impl Iterator<Item = &'a ClassLikeMember<'a>>,
doc_ctx: Option<&DocblockCtx<'a>>,
class_template_params: &[String],
) -> ExtractedMembers {
let mut methods = Vec::new();
let mut properties = Vec::new();
let mut constants = Vec::new();
let mut used_traits = Vec::new();
let mut trait_precedences = Vec::new();
let mut trait_aliases = Vec::new();
let mut inline_use_generics: Vec<(String, Vec<PhpType>)> = Vec::new();
for member in members {
match member {
ClassLikeMember::Method(method) => {
if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
&& !is_available_for_version(&method.attribute_lists, ctx, ver)
{
continue;
}
if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
&& is_removed_for_version(method, ctx, ver)
{
continue;
}
let name = method.name.value.to_string();
let name_offset = method.name.span.start.offset;
let php_version = doc_ctx.and_then(|ctx| ctx.php_version);
let mut parameters = extract_parameters(
&method.parameter_list,
doc_ctx.map(|ctx| ctx.content),
php_version,
doc_ctx,
);
let raw_native_return_type = method
.return_type_hint
.as_ref()
.map(|rth| extract_hint_type(&rth.hint));
let native_return_type = if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
{
super::extract_language_level_type(&method.attribute_lists, ctx, ver)
.or(raw_native_return_type)
} else {
raw_native_return_type
};
let is_static = method.modifiers.iter().any(|m| m.is_static());
let visibility = extract_visibility(method.modifiers.iter());
let method_docblock_text = doc_ctx.and_then(|ctx| {
docblock::get_docblock_text_for_node(ctx.trivias, ctx.content, method)
});
let method_docblock_info =
method_docblock_text.and_then(docblock::parse_docblock_for_tags);
let (
return_type,
conditional_return,
deprecation_message,
method_deprecated_replacement,
method_template_params,
method_template_param_bounds,
method_template_bindings,
) = if let Some(ref info) = method_docblock_info {
let parsed_doc_type = docblock::extract_return_type_from_info(info);
let effective = docblock::resolve_effective_type_typed(
native_return_type.as_ref(),
parsed_doc_type.as_ref(),
);
let conditional = docblock::extract_conditional_return_type_from_info(info);
let tpl_params_with_bounds =
docblock::extract_template_params_with_bounds_from_info(info);
let tpl_params: Vec<String> = tpl_params_with_bounds
.iter()
.map(|(n, _)| n.clone())
.collect();
let tpl_param_bounds: HashMap<String, PhpType> = tpl_params_with_bounds
.into_iter()
.filter_map(|(n, b)| b.map(|b| (n, b)))
.collect();
let tpl_bindings = if !tpl_params.is_empty() {
docblock::extract_template_param_bindings_from_info(info, &tpl_params)
} else {
Vec::new()
};
let (tpl_params, tpl_bindings) = if name == "__construct"
&& tpl_bindings.is_empty()
&& !class_template_params.is_empty()
{
let class_bindings =
docblock::extract_template_param_bindings_from_info(
info,
class_template_params,
);
if !class_bindings.is_empty() {
(class_template_params.to_vec(), class_bindings)
} else {
(tpl_params, tpl_bindings)
}
} else {
(tpl_params, tpl_bindings)
};
let conditional = conditional.or_else(|| {
docblock::synthesize_template_conditional_from_info(
info,
&tpl_params,
effective.as_ref(),
false,
)
});
let depr_info = merge_deprecation_info(
docblock::extract_deprecation_message_from_info(info),
&method.attribute_lists,
doc_ctx,
);
let deprecation_message = depr_info.message;
(
effective,
conditional,
deprecation_message,
depr_info.replacement,
tpl_params,
tpl_param_bounds,
tpl_bindings,
)
} else {
let depr_info =
merge_deprecation_info(None, &method.attribute_lists, doc_ctx);
(
native_return_type.clone(),
None,
depr_info.message,
depr_info.replacement,
Vec::new(),
HashMap::new(),
Vec::new(),
)
};
if name == "__construct" {
for param in method.parameter_list.parameters.iter() {
if param.is_promoted_property() {
let raw_name = param.variable.name.to_string();
let prop_name =
raw_name.strip_prefix('$').unwrap_or(&raw_name).to_string();
let saved_native_hint =
param.hint.as_ref().map(|h| extract_hint_type(h));
let prop_visibility = extract_visibility(param.modifiers.iter());
let inline_var_type = doc_ctx.and_then(|ctx| {
let doc = docblock::get_docblock_text_for_node(
ctx.trivias,
ctx.content,
param,
)?;
docblock::extract_var_type(doc)
});
let type_hint = if let Some(ref var_type) = inline_var_type {
docblock::resolve_effective_type_typed(
saved_native_hint.as_ref(),
Some(var_type),
)
} else if let Some(ref info) = method_docblock_info {
let parsed =
docblock::extract_param_raw_type_from_info(info, &raw_name);
docblock::resolve_effective_type_typed(
saved_native_hint.as_ref(),
parsed.as_ref(),
)
} else {
saved_native_hint.clone()
};
let prop_name_offset = param.variable.span.start.offset;
properties.push(PropertyInfo {
name: prop_name,
name_offset: prop_name_offset,
native_type_hint: saved_native_hint,
type_hint,
description: None,
is_static: false,
visibility: prop_visibility,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
is_virtual: false,
});
}
}
}
let return_type = if return_type.is_none() {
infer_relationship_from_method(method, doc_ctx)
} else {
return_type
};
if let Some(ref info) = method_docblock_info {
for param in &mut parameters {
let param_doc_type =
docblock::extract_param_raw_type_from_info(info, ¶m.name);
if let Some(ref doc_type) = param_doc_type {
let effective = docblock::resolve_effective_type_typed(
param.type_hint.as_ref(),
Some(doc_type),
);
if effective.is_some() {
param.type_hint = effective;
}
}
}
for (this_type, param_name) in
docblock::extract_param_closure_this_from_info(info)
{
if let Some(param) =
parameters.iter_mut().find(|p| p.name == param_name)
{
param.closure_this_type = Some(this_type);
}
}
for (tag_name, tag_type) in docblock::extract_all_param_tags_from_info(info)
{
if !parameters.iter().any(|p| p.name == tag_name) {
let description =
docblock::extract_param_description_from_info(info, &tag_name);
parameters.push(ParameterInfo {
name: tag_name,
is_required: false,
type_hint: Some(tag_type),
native_type_hint: None,
description,
default_value: None,
is_variadic: false,
is_reference: false,
closure_this_type: None,
});
}
}
}
let has_scope_attr = has_scope_attribute(method);
let method_description = method_docblock_info
.as_ref()
.and_then(crate::hover::extract_description_from_info);
let return_description = method_docblock_info
.as_ref()
.and_then(docblock::extract_return_description_from_info);
let links = method_docblock_info
.as_ref()
.map(docblock::extract_link_urls_from_info)
.unwrap_or_default();
let see_refs = method_docblock_info
.as_ref()
.map(docblock::extract_see_references_from_info)
.unwrap_or_default();
if let Some(ref info) = method_docblock_info {
for param in &mut parameters {
param.description =
docblock::extract_param_description_from_info(info, ¶m.name);
}
}
let type_assertions = method_docblock_info
.as_ref()
.map(docblock::extract_type_assertions_from_info)
.unwrap_or_default();
let throws = method_docblock_info
.as_ref()
.map(docblock::extract_throws_tags_from_info)
.unwrap_or_default();
methods.push(MethodInfo {
name,
name_offset,
parameters,
native_return_type: native_return_type.clone(),
return_type,
description: method_description,
return_description,
links,
see_refs,
is_static,
visibility,
conditional_return,
deprecation_message,
deprecated_replacement: method_deprecated_replacement,
template_params: method_template_params,
template_param_bounds: method_template_param_bounds,
template_bindings: method_template_bindings,
has_scope_attribute: has_scope_attr,
is_abstract: method.is_abstract(),
is_virtual: false,
type_assertions,
throws,
});
}
ClassLikeMember::Property(property) => {
let mut prop_infos = extract_property_info(property);
let prop_attr_lists: Option<&Sequence<'_, AttributeList<'_>>> = match property {
Property::Plain(p) => Some(&p.attribute_lists),
Property::Hooked(h) => Some(&h.attribute_lists),
};
if let Some(ctx) = doc_ctx
&& let Some(ver) = ctx.php_version
&& let Some(attr_lists) = prop_attr_lists
&& let Some(override_type) =
super::extract_language_level_type(attr_lists, ctx, ver)
{
for prop in &mut prop_infos {
prop.type_hint = Some(override_type.clone());
prop.native_type_hint = Some(override_type.clone());
}
}
if let Some(ctx) = doc_ctx
&& let Some(doc_text) =
docblock::get_docblock_text_for_node(ctx.trivias, ctx.content, member)
{
let info = docblock::parse_docblock_for_tags(doc_text);
let docblock_msg = info
.as_ref()
.and_then(docblock::extract_deprecation_message_from_info);
let see_refs = info
.as_ref()
.map(docblock::extract_see_references_from_info)
.unwrap_or_default();
if !see_refs.is_empty() {
for prop in &mut prop_infos {
prop.see_refs = see_refs.clone();
}
}
let depr_info = if let Some(attr_lists) = prop_attr_lists {
merge_deprecation_info(docblock_msg, attr_lists, doc_ctx)
} else {
DeprecationInfo {
message: docblock_msg,
replacement: None,
}
};
if let Some(ref msg) = depr_info.message {
for prop in &mut prop_infos {
prop.deprecation_message = Some(msg.clone());
}
}
if let Some(ref repl) = depr_info.replacement {
for prop in &mut prop_infos {
prop.deprecated_replacement = Some(repl.clone());
}
}
if let Some(parsed_doc) =
info.as_ref().and_then(docblock::extract_var_type_from_info)
{
for prop in &mut prop_infos {
let effective = docblock::resolve_effective_type_typed(
prop.type_hint.as_ref(),
Some(&parsed_doc),
);
prop.type_hint = effective;
}
}
let description = info
.as_ref()
.and_then(crate::hover::extract_description_from_info)
.or_else(|| {
info.as_ref()
.and_then(crate::hover::extract_var_description_from_info)
});
if description.is_some() {
for prop in &mut prop_infos {
prop.description = description.clone();
}
}
}
if prop_infos.iter().all(|p| p.deprecation_message.is_none())
&& let Some(ctx) = doc_ctx
&& let Some(attr_lists) = prop_attr_lists
{
let depr_info = merge_deprecation_info(None, attr_lists, Some(ctx));
if let Some(ref msg) = depr_info.message {
for prop in &mut prop_infos {
prop.deprecation_message = Some(msg.clone());
}
}
if let Some(ref repl) = depr_info.replacement {
for prop in &mut prop_infos {
prop.deprecated_replacement = Some(repl.clone());
}
}
}
properties.append(&mut prop_infos);
}
ClassLikeMember::Constant(constant) => {
let type_hint = constant.hint.as_ref().map(|h| extract_hint_type(h));
let visibility = extract_visibility(constant.modifiers.iter());
let const_docblock_text = doc_ctx.and_then(|ctx| {
docblock::get_docblock_text_for_node(ctx.trivias, ctx.content, member)
});
let const_docblock_info =
const_docblock_text.and_then(docblock::parse_docblock_for_tags);
let depr_info = {
let docblock_msg = const_docblock_info
.as_ref()
.and_then(docblock::extract_deprecation_message_from_info);
merge_deprecation_info(docblock_msg, &constant.attribute_lists, doc_ctx)
};
let deprecation_message = depr_info.message;
let constant_deprecated_replacement = depr_info.replacement;
let const_see_refs = const_docblock_info
.as_ref()
.map(docblock::extract_see_references_from_info)
.unwrap_or_default();
let const_description = const_docblock_info
.as_ref()
.and_then(crate::hover::extract_description_from_info);
for item in constant.items.iter() {
let value = doc_ctx.and_then(|ctx| {
let start = item.value.span().start.offset as usize;
let end = item.value.span().end.offset as usize;
ctx.content.get(start..end).map(|s| s.to_string())
});
constants.push(ConstantInfo {
name: item.name.value.to_string(),
name_offset: item.name.span.start.offset,
type_hint: type_hint.clone(),
visibility,
deprecation_message: deprecation_message.clone(),
deprecated_replacement: constant_deprecated_replacement.clone(),
see_refs: const_see_refs.clone(),
description: const_description.clone(),
is_enum_case: false,
enum_value: None,
value,
is_virtual: false,
});
}
}
ClassLikeMember::EnumCase(enum_case) => {
let case_name = enum_case.item.name().value.to_string();
let case_name_offset = enum_case.item.name().span.start.offset;
let enum_value = if let EnumCaseItem::Backed(backed) = &enum_case.item {
let start = backed.value.span().start.offset as usize;
let end = backed.value.span().end.offset as usize;
doc_ctx
.and_then(|ctx| ctx.content.get(start..end))
.map(|s| s.to_string())
} else {
None
};
constants.push(ConstantInfo {
name: case_name,
name_offset: case_name_offset,
type_hint: None,
visibility: Visibility::Public,
deprecation_message: None,
deprecated_replacement: None,
see_refs: Vec::new(),
description: None,
is_enum_case: true,
enum_value,
value: None,
is_virtual: false,
});
}
ClassLikeMember::TraitUse(trait_use) => {
for trait_name_ident in trait_use.trait_names.iter() {
used_traits.push(trait_name_ident.value().to_string());
}
if let Some(ctx) = doc_ctx
&& let Some(doc_text) = docblock::get_docblock_text_for_node(
ctx.trivias,
ctx.content,
trait_use,
)
{
let tags = docblock::extract_generics_tag(doc_text, "@use");
inline_use_generics.extend(tags);
}
if let TraitUseSpecification::Concrete(spec) = &trait_use.specification {
for adaptation in spec.adaptations.iter() {
match adaptation {
TraitUseAdaptation::Precedence(prec) => {
let trait_name =
prec.method_reference.trait_name.value().to_string();
let method_name =
prec.method_reference.method_name.value.to_string();
let insteadof: Vec<String> = prec
.trait_names
.iter()
.map(|id| id.value().to_string())
.collect();
trait_precedences.push(TraitPrecedence {
trait_name,
method_name,
insteadof,
});
}
TraitUseAdaptation::Alias(alias_adapt) => {
let (trait_name, method_name) =
match &alias_adapt.method_reference {
TraitUseMethodReference::Identifier(ident) => {
(None, ident.value.to_string())
}
TraitUseMethodReference::Absolute(abs) => (
Some(abs.trait_name.value().to_string()),
abs.method_name.value.to_string(),
),
};
let alias =
alias_adapt.alias.as_ref().map(|a| a.value.to_string());
let visibility = alias_adapt.visibility.as_ref().map(|m| {
if m.is_private() {
Visibility::Private
} else if m.is_protected() {
Visibility::Protected
} else {
Visibility::Public
}
});
trait_aliases.push(TraitAlias {
trait_name,
method_name,
alias,
visibility,
});
}
}
}
}
}
}
}
ExtractedMembers {
methods,
properties,
constants,
used_traits,
trait_precedences,
trait_aliases,
inline_use_generics,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::attribute_target;
#[test]
fn parse_target_class_qualified() {
assert_eq!(
parse_attribute_target_flags("\\Attribute::TARGET_CLASS"),
attribute_target::TARGET_CLASS,
);
}
#[test]
fn parse_target_class_unqualified() {
assert_eq!(
parse_attribute_target_flags("Attribute::TARGET_CLASS"),
attribute_target::TARGET_CLASS,
);
}
#[test]
fn parse_target_method_self() {
assert_eq!(
parse_attribute_target_flags("self::TARGET_METHOD"),
attribute_target::TARGET_METHOD,
);
}
#[test]
fn parse_target_bare_constant() {
assert_eq!(
parse_attribute_target_flags("TARGET_PROPERTY"),
attribute_target::TARGET_PROPERTY,
);
}
#[test]
fn parse_target_numeric_literal() {
assert_eq!(parse_attribute_target_flags("1"), 1);
assert_eq!(parse_attribute_target_flags("63"), 63);
}
#[test]
fn parse_target_bitwise_or() {
let expected = attribute_target::TARGET_CLASS | attribute_target::TARGET_METHOD;
assert_eq!(
parse_attribute_target_flags("\\Attribute::TARGET_CLASS | \\Attribute::TARGET_METHOD"),
expected,
);
}
#[test]
fn parse_target_all() {
assert_eq!(
parse_attribute_target_flags("Attribute::TARGET_ALL"),
attribute_target::TARGET_ALL,
);
}
#[test]
fn parse_target_unrecognised_defaults_to_all() {
assert_eq!(
parse_attribute_target_flags("SOME_UNKNOWN_CONST"),
attribute_target::TARGET_ALL,
);
}
#[test]
fn parse_target_mixed_qualified_and_bare() {
let expected = attribute_target::TARGET_FUNCTION | attribute_target::TARGET_PARAMETER;
assert_eq!(
parse_attribute_target_flags("Attribute::TARGET_FUNCTION | TARGET_PARAMETER"),
expected,
);
}
#[test]
fn parse_target_whitespace_handling() {
assert_eq!(
parse_attribute_target_flags(" Attribute::TARGET_CLASS "),
attribute_target::TARGET_CLASS,
);
}
}