use mago_allocator::Arena;
use mago_names::scope::NamespaceScope;
use mago_phpdoc_syntax::cst::Document;
use mago_phpdoc_syntax::cst::TagValue;
use mago_reporting::Annotation;
use mago_reporting::Issue;
use mago_span::HasSpan;
use mago_span::Span;
use mago_syntax::cst::*;
use mago_syntax::walker::MutWalker;
use mago_word::Word;
use mago_word::word;
use crate::issue::ScanningIssueKind;
use crate::metadata::class_like::ClassLikeMetadata;
use crate::metadata::flags::MetadataFlags;
use crate::metadata::parameter::FunctionLikeParameterMetadata;
use crate::metadata::property::PropertyMetadata;
use crate::metadata::property_hook::PropertyHookMetadata;
use crate::metadata::ttype::TypeMetadata;
use crate::misc::VariableIdentifier;
use crate::scanner::Context;
use crate::scanner::attribute::scan_attribute_lists;
use crate::scanner::docblock::HookParamTag;
use crate::scanner::docblock::find_most_trusted_tag;
use crate::scanner::docblock::parse_docblock;
use crate::scanner::inference::infer;
use crate::scanner::ttype::get_type_metadata_from_hint;
use crate::scanner::ttype::get_type_metadata_from_type;
use crate::scanner::ttype::merge_type_preserving_nullability;
use crate::scanner::version_claim::TypeOverride;
use crate::scanner::version_claim::evaluate_version_attributes;
use crate::ttype::resolution::TypeResolutionContext;
use crate::visibility::Visibility;
#[inline]
pub fn scan_promoted_property<'arena, A>(
parameter: &'arena FunctionLikeParameter<'arena>,
parameter_metadata: &mut FunctionLikeParameterMetadata,
class_like_metadata: &mut ClassLikeMetadata,
classname: Word,
type_context: &TypeResolutionContext,
context: &mut Context<'_, 'arena, A>,
scope: &NamespaceScope,
) -> PropertyMetadata
where
A: Arena,
{
debug_assert!(parameter.is_promoted_property(), "Parameter is not a promoted property");
let name = parameter_metadata.get_name();
let name_span = parameter_metadata.get_name_span();
let mut flags = MetadataFlags::PROMOTED_PROPERTY;
flags |= MetadataFlags::origin_flags(context.file.file_type);
if parameter_metadata.flags.has_default() {
flags |= MetadataFlags::HAS_DEFAULT;
}
if parameter.modifiers.contains_readonly() {
flags |= MetadataFlags::READONLY;
}
if parameter.modifiers.contains_abstract() {
flags |= MetadataFlags::ABSTRACT;
}
if parameter.modifiers.contains_static() {
flags |= MetadataFlags::STATIC;
}
let default_type_metadata = parameter_metadata.get_default_type().cloned();
let read_visibility = match parameter.modifiers.get_first_read_visibility() {
Some(visibility) => Visibility::try_from(visibility).unwrap_or(Visibility::Public),
None => Visibility::Public,
};
let write_visibility = match parameter.modifiers.get_first_write_visibility() {
Some(visibility) => Visibility::try_from(visibility).unwrap_or(Visibility::Public),
None => {
if parameter.modifiers.contains_readonly() {
Visibility::Protected
} else {
read_visibility
}
}
};
let mut property_metadata = PropertyMetadata::new(*name, flags);
property_metadata.set_default_type_metadata(default_type_metadata);
property_metadata.set_name_span(Some(name_span));
property_metadata.set_span(Some(parameter.span()));
property_metadata.set_visibility(read_visibility, write_visibility);
property_metadata.set_type_declaration_metadata(
parameter.hint.as_ref().map(|hint| get_type_metadata_from_hint(hint, Some(class_like_metadata.name), context)),
);
if let Some(hook_list) = ¶meter.hooks {
for hook in &hook_list.hooks {
let mut hook_metadata = scan_property_hook(hook, &property_metadata, context, scope);
class_like_metadata.issues.extend(hook_metadata.take_issues());
property_metadata.hooks.insert(hook_metadata.name, hook_metadata);
}
let name_bytes = name.0.as_bytes();
let prop_name = name_bytes.strip_prefix(b"$").unwrap_or(name_bytes);
property_metadata.set_is_virtual(!hooks_reference_backing_store(&hook_list.hooks, prop_name));
}
let mut used_parameter_type_from_docblock = false;
if let Some(type_metadata) = parameter_metadata.type_metadata.as_ref()
&& type_metadata.from_docblock
{
used_parameter_type_from_docblock = true;
property_metadata.type_metadata = Some(type_metadata.clone());
}
if let Some(document) = parse_docblock(context, parameter) {
for parse_error in document.errors {
class_like_metadata.issues.push(
Issue::error("Failed to parse promoted property docblock comment.")
.with_code(ScanningIssueKind::MalformedDocblockComment)
.with_annotation(Annotation::primary(parse_error.span()).with_message(parse_error.to_string()))
.with_note(parse_error.note())
.with_help(parse_error.help()),
);
}
update_property_metadata_from_docblock(
&mut property_metadata,
&document,
classname,
type_context,
scope,
class_like_metadata,
true,
);
if let Some(type_metadata) = property_metadata.type_metadata.as_ref()
&& type_metadata.from_docblock
&& !used_parameter_type_from_docblock
{
parameter_metadata.type_metadata = Some(type_metadata.clone());
}
}
property_metadata
}
#[inline]
pub fn scan_properties<'arena, A>(
property: &'arena Property<'arena>,
class_like_metadata: &mut ClassLikeMetadata,
classname: Word,
type_context: &TypeResolutionContext,
context: &mut Context<'_, 'arena, A>,
scope: &NamespaceScope,
) -> Vec<PropertyMetadata>
where
A: Arena,
{
let document = parse_docblock(context, property);
if let Some(document) = document.as_ref() {
for parse_error in document.errors {
class_like_metadata.issues.push(
Issue::error("Failed to parse property docblock comment.")
.with_code(ScanningIssueKind::MalformedDocblockComment)
.with_annotation(Annotation::primary(parse_error.span()).with_message(parse_error.to_string()))
.with_note(parse_error.note())
.with_help(parse_error.help()),
);
}
}
let mut flags = MetadataFlags::origin_flags(context.file.file_type);
match property {
Property::Plain(plain_property) => {
let verdict = evaluate_version_attributes(&plain_property.attribute_lists, context, context.php_version);
plain_property
.items
.iter()
.map(|item| {
let (name, name_span, has_default, default_type) =
scan_property_item(item, classname, context, scope);
let mut item_flags = flags;
if has_default {
item_flags |= MetadataFlags::HAS_DEFAULT;
}
if plain_property.modifiers.contains_readonly() {
item_flags |= MetadataFlags::READONLY;
}
if plain_property.modifiers.contains_abstract() {
item_flags |= MetadataFlags::ABSTRACT;
}
if plain_property.modifiers.contains_static() {
item_flags |= MetadataFlags::STATIC;
}
if plain_property.modifiers.contains_final() {
item_flags |= MetadataFlags::FINAL;
}
let read_visibility = match plain_property.modifiers.get_first_read_visibility() {
Some(visibility) => Visibility::try_from(visibility).unwrap_or(Visibility::Public),
None => Visibility::Public,
};
let write_visibility = match plain_property.modifiers.get_first_write_visibility() {
Some(visibility) => Visibility::try_from(visibility).unwrap_or(Visibility::Public),
None => {
if plain_property.modifiers.contains_readonly() {
Visibility::Protected
} else {
read_visibility
}
}
};
let mut metadata = PropertyMetadata::new(name, item_flags);
metadata.set_name_span(Some(name_span));
metadata.set_default_type_metadata(default_type);
metadata.set_visibility(read_visibility, write_visibility);
metadata.set_type_declaration_metadata(
plain_property
.hint
.as_ref()
.map(|hint| get_type_metadata_from_hint(hint, Some(class_like_metadata.name), context)),
);
if let Some(document) = document.as_ref() {
update_property_metadata_from_docblock(
&mut metadata,
document,
classname,
type_context,
scope,
class_like_metadata,
false,
);
}
if matches!(verdict.type_override, Some(TypeOverride::Untyped)) {
metadata.type_declaration_metadata = None;
metadata.type_metadata = None;
}
metadata.version_constraint = verdict.constraint.clone();
metadata
})
.collect()
}
Property::Hooked(hooked_property) => {
let verdict = evaluate_version_attributes(&hooked_property.attribute_lists, context, context.php_version);
let (name, name_span, has_default, default_type) =
scan_property_item(&hooked_property.item, classname, context, scope);
let read_visibility = match hooked_property.modifiers.get_first_read_visibility() {
Some(visibility) => Visibility::try_from(visibility).unwrap_or(Visibility::Public),
None => Visibility::Public,
};
let write_visibility = match hooked_property.modifiers.get_first_write_visibility() {
Some(visibility) => Visibility::try_from(visibility).unwrap_or(Visibility::Public),
None => read_visibility,
};
if has_default {
flags |= MetadataFlags::HAS_DEFAULT;
}
if hooked_property.modifiers.contains_abstract() {
flags |= MetadataFlags::ABSTRACT;
}
if hooked_property.modifiers.contains_final() {
flags |= MetadataFlags::FINAL;
}
let mut metadata = PropertyMetadata::new(name, flags);
metadata.set_name_span(Some(name_span));
metadata.set_default_type_metadata(default_type);
metadata.set_span(Some(hooked_property.span()));
metadata.set_visibility(read_visibility, write_visibility);
metadata.set_type_declaration_metadata(
hooked_property
.hint
.as_ref()
.map(|hint| get_type_metadata_from_hint(hint, Some(class_like_metadata.name), context)),
);
if let Some(document) = document.as_ref() {
update_property_metadata_from_docblock(
&mut metadata,
document,
classname,
type_context,
scope,
class_like_metadata,
false,
);
}
for hook in &hooked_property.hook_list.hooks {
let mut hook_metadata = scan_property_hook(hook, &metadata, context, scope);
class_like_metadata.issues.extend(hook_metadata.take_issues());
metadata.hooks.insert(hook_metadata.name, hook_metadata);
}
let name_bytes = name.0.as_bytes();
let prop_name = name_bytes.strip_prefix(b"$").unwrap_or(name_bytes);
metadata.set_is_virtual(!hooks_reference_backing_store(&hooked_property.hook_list.hooks, prop_name));
if matches!(verdict.type_override, Some(TypeOverride::Untyped)) {
metadata.type_declaration_metadata = None;
metadata.type_metadata = None;
}
metadata.version_constraint = verdict.constraint;
vec![metadata]
}
}
}
fn scan_property_hook<'arena, A>(
hook: &'arena PropertyHook<'arena>,
property_metadata: &PropertyMetadata,
context: &mut Context<'_, 'arena, A>,
scope: &NamespaceScope,
) -> PropertyHookMetadata
where
A: Arena,
{
let name = word(hook.name.value);
let is_get = hook.name.value == b"get";
let is_set = hook.name.value == b"set";
let is_abstract = matches!(hook.body, PropertyHookBody::Abstract(_));
let has_explicit_parameter = hook.parameter_list.is_some();
let mut flags = MetadataFlags::empty();
if hook.modifiers.contains_final() {
flags |= MetadataFlags::FINAL;
}
let mut parameter = if is_set {
if let Some(param_list) = &hook.parameter_list {
param_list.parameters.first().map(|p| scan_hook_parameter(p, property_metadata, context))
} else {
Some(create_implicit_value_parameter(property_metadata, hook.span()))
}
} else {
None
};
let attributes = scan_attribute_lists(&hook.attribute_lists, context);
let mut has_docblock = false;
let mut return_type_metadata = None;
let mut issues = Vec::new();
if let Some(document) = parse_docblock(context, hook) {
has_docblock = true;
for parse_error in document.errors {
issues.push(
Issue::error("Failed to parse property hook docblock comment.")
.with_code(ScanningIssueKind::MalformedDocblockComment)
.with_annotation(Annotation::primary(parse_error.span()).with_message(parse_error.to_string()))
.with_note(parse_error.note())
.with_help(parse_error.help()),
);
}
let parameter_tag = find_most_trusted_tag(&document, |tag| match &tag.value {
TagValue::Param(param) => Some(HookParamTag::Typed(*param)),
TagValue::TypelessParam(param) => Some(HookParamTag::Typeless(*param)),
_ => None,
});
if let Some(param_tag) = ¶meter_tag {
if !has_explicit_parameter {
issues.push(
Issue::error("The `@param` tag cannot be used on a set hook without an explicit parameter.")
.with_code(ScanningIssueKind::InvalidParamTag)
.with_annotation(
Annotation::primary(param_tag.span())
.with_message("This @param cannot be applied to implicit `$value` parameter"),
)
.with_note("Set hooks without an explicit parameter use an implicit `$value` parameter that inherits the property type.")
.with_help("Either add an explicit parameter `set(Type $value) {}` or remove the @param tag."),
);
} else if let Some(ref mut param) = parameter
&& let Some(param_type) = param_tag.get_type()
{
let type_context = TypeResolutionContext::new();
match get_type_metadata_from_type(param_type, None, &type_context, scope) {
Ok(docblock_type) => {
let native_type = param.type_declaration_metadata.as_ref();
let merged = merge_type_preserving_nullability(docblock_type, native_type);
param.set_type_metadata(Some(merged));
}
Err(typing_error) => {
issues.push(
Issue::error("Could not resolve the type for the @param tag.")
.with_code(ScanningIssueKind::InvalidParamTag)
.with_annotation(
Annotation::primary(typing_error.span()).with_message(typing_error.to_string()),
)
.with_note(typing_error.note())
.with_help(typing_error.help()),
);
}
}
}
}
let return_type_tag = find_most_trusted_tag(&document, |tag| match &tag.value {
TagValue::Return(return_tag) => Some(*return_tag),
_ => None,
});
if let Some(return_tag) = &return_type_tag
&& is_get
{
let type_context = TypeResolutionContext::new();
match get_type_metadata_from_type(return_tag.r#type, None, &type_context, scope) {
Ok(docblock_type) => {
return_type_metadata = Some(docblock_type);
}
Err(typing_error) => {
issues.push(
Issue::error("Could not resolve the type for the @return tag.")
.with_code(ScanningIssueKind::InvalidReturnTag)
.with_annotation(
Annotation::primary(typing_error.span()).with_message(typing_error.to_string()),
)
.with_note(typing_error.note())
.with_help(typing_error.help()),
);
}
}
}
}
PropertyHookMetadata::new(name, hook.span())
.with_flags(flags)
.with_parameter(parameter)
.with_returns_by_ref(hook.ampersand.is_some())
.with_is_abstract(is_abstract)
.with_attributes(attributes)
.with_return_type_metadata(return_type_metadata)
.with_has_docblock(has_docblock)
.with_issues(issues)
}
fn scan_hook_parameter<'arena, A>(
param: &'arena FunctionLikeParameter<'arena>,
property_metadata: &PropertyMetadata,
context: &mut Context<'_, 'arena, A>,
) -> FunctionLikeParameterMetadata
where
A: Arena,
{
let name = VariableIdentifier(word(param.variable.name));
let name_span = param.variable.span;
let mut flags = MetadataFlags::empty();
if param.ampersand.is_some() {
flags |= MetadataFlags::BY_REFERENCE;
}
let mut param_metadata = FunctionLikeParameterMetadata::new(name, param.span(), name_span, flags);
if let Some(hint) = ¶m.hint {
let type_meta = get_type_metadata_from_hint(hint, None, context);
param_metadata.set_type_declaration_metadata(Some(type_meta));
} else if let Some(prop_type) = &property_metadata.type_metadata {
param_metadata.set_type_declaration_metadata(Some(prop_type.clone()));
}
param_metadata
}
fn create_implicit_value_parameter(property_metadata: &PropertyMetadata, span: Span) -> FunctionLikeParameterMetadata {
let name = VariableIdentifier(word("$value"));
let mut param = FunctionLikeParameterMetadata::new(name, span, span, MetadataFlags::empty());
if let Some(type_meta) = &property_metadata.type_metadata {
param.set_type_declaration_metadata(Some(type_meta.clone()));
}
param
}
#[inline]
pub fn scan_property_item<'arena, A>(
property_item: &'arena PropertyItem<'arena>,
classname: Word,
context: &Context<'_, 'arena, A>,
scope: &NamespaceScope,
) -> (VariableIdentifier, Span, bool, Option<TypeMetadata>)
where
A: Arena,
{
match property_item {
PropertyItem::Abstract(property_abstract_item) => {
let name = VariableIdentifier(word(property_abstract_item.variable.name));
let name_span = property_abstract_item.variable.span;
let has_default = false;
let default_type = None;
(name, name_span, has_default, default_type)
}
PropertyItem::Concrete(property_concrete_item) => {
let name = VariableIdentifier(word(property_concrete_item.variable.name));
let name_span = property_concrete_item.variable.span;
let has_default = true;
let default_type = infer(context, scope, property_concrete_item.value, Some(classname)).map(|u| {
let mut type_metadata = TypeMetadata::new(u, property_concrete_item.value.span());
type_metadata.inferred = true;
type_metadata
});
(name, name_span, has_default, default_type)
}
}
}
fn update_property_metadata_from_docblock(
property_metadata: &mut PropertyMetadata,
document: &Document<'_>,
classname: Word,
type_context: &TypeResolutionContext,
scope: &NamespaceScope,
class_like_metadata: &mut ClassLikeMetadata,
allow_param_tag: bool,
) {
for tag in document.tags() {
match &tag.value {
TagValue::Internal(_) => {
property_metadata.flags |= MetadataFlags::INTERNAL;
}
TagValue::Experimental(_) => {
property_metadata.flags |= MetadataFlags::EXPERIMENTAL;
}
TagValue::Deprecated(_) => {
property_metadata.flags |= MetadataFlags::DEPRECATED;
}
TagValue::NotDeprecated(_) => {
property_metadata.flags.set(MetadataFlags::DEPRECATED, false);
}
TagValue::Readonly(_) => {
property_metadata.flags |= MetadataFlags::READONLY;
}
_ => {}
}
}
let var_type = find_most_trusted_tag(document, |tag| match &tag.value {
TagValue::Var(var) => Some(var.r#type),
_ => None,
});
let docblock_type = var_type.or_else(|| {
if !allow_param_tag {
return None;
}
find_most_trusted_tag(document, |tag| match &tag.value {
TagValue::Param(param) => Some(param.r#type),
_ => None,
})
});
if let Some(docblock_type) = docblock_type {
match get_type_metadata_from_type(docblock_type, Some(classname), type_context, scope) {
Ok(property_type_metadata) => {
let real_type = property_metadata.type_declaration_metadata.as_ref();
let property_type_metadata = merge_type_preserving_nullability(property_type_metadata, real_type);
property_metadata.set_type_metadata(Some(property_type_metadata));
}
Err(typing_error) => class_like_metadata.issues.push(
Issue::error("Could not resolve the property type from its docblock.")
.with_code(ScanningIssueKind::InvalidVarTag)
.with_annotation(Annotation::primary(typing_error.span()).with_message(typing_error.to_string()))
.with_note(typing_error.note())
.with_help(typing_error.help()),
),
}
}
}
fn hooks_reference_backing_store<'arena>(
hooks: impl IntoIterator<Item = &'arena PropertyHook<'arena>>,
property_name: &[u8],
) -> bool {
struct Walker<'arena> {
property_name: &'arena [u8],
found: bool,
assignment_seen: bool,
}
impl<'arena> Walker<'arena> {
fn new(property_name: &'arena [u8]) -> Self {
Self { property_name, found: false, assignment_seen: false }
}
fn check_access<'ast>(
&mut self,
object: &'ast Expression<'arena>,
property: &'ast ClassLikeMemberSelector<'arena>,
) {
if self.found {
return;
}
let Expression::Variable(Variable::Direct(direct_variable)) = object else {
return;
};
if direct_variable.name != b"$this" {
return;
}
let ClassLikeMemberSelector::Identifier(identifier) = property else {
return;
};
if identifier.value == self.property_name {
self.found = true;
}
}
}
impl<'ast, 'arena> MutWalker<'ast, 'arena, ()> for Walker<'arena> {
fn walk_in_property_access(&mut self, access: &'ast PropertyAccess<'arena>, _: &mut ()) {
self.check_access(access.object, &access.property);
}
fn walk_in_null_safe_property_access(&mut self, access: &'ast NullSafePropertyAccess<'arena>, _: &mut ()) {
self.check_access(access.object, &access.property);
}
fn walk_in_assignment(&mut self, _: &'ast Assignment<'arena>, _: &mut ()) {
self.assignment_seen = true;
}
}
let mut walker = Walker::new(property_name);
for hook in hooks {
walker.assignment_seen = false;
walker.walk_property_hook_body(&hook.body, &mut ());
if walker.found {
return true;
}
if hook.name.value == b"set"
&& matches!(hook.body, PropertyHookBody::Concrete(PropertyHookConcreteBody::Expression(_)))
&& !walker.assignment_seen
{
return true;
}
}
false
}