use std::rc::Rc;
use mago_atom::atom;
use mago_codex::ttype::add_optional_union_type;
use mago_codex::ttype::get_mixed;
use mago_codex::ttype::get_never;
use mago_codex::ttype::get_null;
use mago_syntax::ast::Expression;
use mago_syntax::ast::StaticPropertyAccess;
use mago_syntax::ast::Variable;
use crate::analyzable::Analyzable;
use crate::artifacts::AnalysisArtifacts;
use crate::context::Context;
use crate::context::block::BlockContext;
use crate::error::AnalysisError;
use crate::resolver::static_property::resolve_static_properties;
use crate::utils::expression::get_static_property_access_expression_id;
impl<'ast, 'arena> Analyzable<'ast, 'arena> for StaticPropertyAccess<'arena> {
fn analyze<'ctx>(
&'ast self,
context: &mut Context<'ctx, 'arena>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
) -> Result<(), AnalysisError> {
let property_access_id = get_static_property_access_expression_id(
self.class,
&self.property,
block_context.scope.get_class_like_name(),
context.resolved_names,
Some(context.codebase),
);
if context.settings.memoize_properties
&& let Some(property_access_id) = &property_access_id
&& let Some(existing_type) = block_context.locals.get(property_access_id).cloned()
{
add_memoized_static_property_reference(context, block_context, artifacts, self.class, &self.property)?;
artifacts.set_rc_expression_type(self, existing_type);
return Ok(());
}
let resolution_result =
resolve_static_properties(context, block_context, artifacts, self.class, &self.property)?;
let mut resulting_expression_type = None;
if !resolution_result.has_error_path {
for resolved_property in resolution_result.properties {
if let Some(declaring_class_id) = resolved_property.declaring_class_id {
artifacts.symbol_references.add_reference_for_property_read(
&block_context.scope,
declaring_class_id,
resolved_property.property_name,
);
}
resulting_expression_type = Some(add_optional_union_type(
resolved_property.property_type,
resulting_expression_type.as_ref(),
context.codebase,
));
}
if resolution_result.has_ambiguous_path
|| resolution_result.encountered_mixed
|| resolution_result.has_possibly_defined_property
{
resulting_expression_type =
Some(add_optional_union_type(get_mixed(), resulting_expression_type.as_ref(), context.codebase));
}
if resolution_result.has_invalid_path || resolution_result.encountered_null {
resulting_expression_type =
Some(add_optional_union_type(get_null(), resulting_expression_type.as_ref(), context.codebase));
}
}
let resulting_type = Rc::new(resulting_expression_type.unwrap_or_else(get_never));
if let Some(property_access_id) = property_access_id {
block_context.locals.insert(property_access_id, resulting_type.clone());
}
artifacts.set_rc_expression_type(self, resulting_type);
Ok(())
}
}
fn add_memoized_static_property_reference<'ctx, 'ast, 'arena>(
context: &mut Context<'ctx, 'arena>,
block_context: &mut BlockContext<'ctx>,
artifacts: &mut AnalysisArtifacts,
class: &'ast Expression<'arena>,
property: &'ast Variable<'arena>,
) -> Result<(), AnalysisError> {
let property_name = match property {
Variable::Direct(var) => atom(var.name),
_ => return Ok(()),
};
let class_name = match class.unparenthesized() {
Expression::Identifier(ident) => atom(context.resolved_names.get(ident)),
Expression::Self_(_) | Expression::Parent(_) | Expression::Static(_) => {
block_context.scope.get_class_like_name().unwrap_or_else(|| atom(""))
}
_ => return Ok(()),
};
if class_name.is_empty() {
return Ok(());
}
if let Some(declaring_class) = context.codebase.get_declaring_property_class(&class_name, &property_name) {
artifacts.symbol_references.add_reference_for_property_read(
&block_context.scope,
declaring_class,
property_name,
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::code::IssueCode;
use crate::test_analysis;
use indoc::indoc;
test_analysis! {
name = read_public_static_property_directly,
code = indoc! {r#"
<?php
class MyClass { public static string $prop = "value"; }
/** @param string $_s */
function i_take_string(string $_s): void {}
i_take_string(MyClass::$prop);
"#},
}
test_analysis! {
name = read_public_static_property_from_parent,
code = indoc! {r#"
<?php
class ParentClass { public static string $prop = "value"; }
class ChildClass extends ParentClass {}
/** @param string $_s */
function i_take_string(string $_s): void {}
i_take_string(ChildClass::$prop);
"#},
}
test_analysis! {
name = read_protected_static_property_from_child,
code = indoc! {r"
<?php
class ParentClass { protected static int $prop = 1; }
class ChildClass extends ParentClass {
public static function getValue(): int {
return self::$prop;
}
}
/** @param int $_i */
function i_take_int(int $_i): void {}
i_take_int(ChildClass::getValue());
"},
}
test_analysis! {
name = read_private_static_property_from_same_class,
code = indoc! {r"
<?php
class PrivateTest {
private static int $secret = 42;
public static function getSecret(): int {
return self::$secret;
}
}
"},
}
test_analysis! {
name = read_static_property_with_static_keyword,
code = indoc! {r#"
<?php
class Base { public static string $name = "Base"; }
class Sub extends Base { public static string $name = "Sub"; }
function get_name(Base $instance): string {
return $instance::$name;
}
"#},
}
test_analysis! {
name = read_static_property_dynamically_with_literal,
code = indoc! {r#"
<?php
class MyClass { public static string $prop = "value"; }
/** @param string $_s */
function i_take_string(string $_s): void {}
$propName = 'prop';
i_take_string(MyClass::${$propName});
"#},
}
test_analysis! {
name = read_undefined_static_property,
code = indoc! {r"
<?php
class MyClass {}
echo MyClass::$non_existent;
"},
issues = [
IssueCode::NonExistentProperty,
]
}
test_analysis! {
name = read_private_static_property_from_outside,
code = indoc! {r"
<?php
class MyClass { private static int $secret = 1; }
echo MyClass::$secret;
"},
issues = [
IssueCode::UnusedProperty,
IssueCode::InvalidPropertyRead,
IssueCode::NoValue,
]
}
test_analysis! {
name = read_private_static_property_from_parent_in_child,
code = indoc! {r"
<?php
class P { private static int $secret = 1; }
class C extends P {
public function getSecret() {
return self::$secret;
}
}
"},
issues = [
IssueCode::UnusedProperty,
IssueCode::NonExistentProperty,
]
}
test_analysis! {
name = read_protected_static_property_from_outside,
code = indoc! {r"
<?php
class MyClass { protected static int $prop = 1; }
echo MyClass::$prop;
"},
issues = [
IssueCode::InvalidPropertyRead,
IssueCode::NoValue,
]
}
test_analysis! {
name = read_instance_property_statically,
code = indoc! {r"
<?php
class HasInstanceProp { public int $instance_prop = 1; }
echo HasInstanceProp::$instance_prop;
"},
issues = [
IssueCode::InvalidStaticPropertyAccess,
IssueCode::NoValue,
]
}
test_analysis! {
name = read_static_property_on_interface,
code = indoc! {r"
<?php
interface MyInterface {}
echo MyInterface::$some_prop;
"},
issues = [
IssueCode::NonExistentProperty,
]
}
test_analysis! {
name = read_static_property_on_enum,
code = indoc! {r"
<?php
enum MyEnum {}
echo MyEnum::$some_prop;
"},
issues = [
IssueCode::NonExistentProperty,
]
}
test_analysis! {
name = reading_static_property_with_union_type,
code = indoc! {r#"
<?php
class A {
public static null|int $x = 1;
public static null|bool $y = true;
}
class B {
public static null|float $x = 2.5;
public static null|string $y = "hello";
}
/** @param 'x'|'y' $prop */
function delta(A|B $obj, string $prop): int|float|bool|string|null {
return $obj::${$prop};
}
"#},
}
test_analysis! {
name = static_property_reconciliation,
code = indoc! {r"
<?php
class A
{
private static null|string $foo = null;
public static function getFoo(): string
{
if (self::$foo !== null) {
return self::$foo;
}
self::$foo = 'bar';
return self::$foo;
}
}
"},
}
}