src/Phan/Language/Element/Clazz.php
<?php
declare(strict_types=1);
namespace Phan\Language\Element;
use ast;
use ast\Node;
use Closure;
use LogicException;
use Phan\Analysis\AbstractMethodAnalyzer;
use Phan\Analysis\ClassConstantTypesAnalyzer;
use Phan\Analysis\ClassInheritanceAnalyzer;
use Phan\Analysis\CompositionAnalyzer;
use Phan\Analysis\DuplicateClassAnalyzer;
use Phan\Analysis\ParentConstructorCalledAnalyzer;
use Phan\Analysis\PropertyTypesAnalyzer;
use Phan\CodeBase;
use Phan\Config;
use Phan\Exception\CodeBaseException;
use Phan\Exception\IssueException;
use Phan\Exception\RecursionDepthException;
use Phan\Issue;
use Phan\IssueFixSuggester;
use Phan\Language\Context;
use Phan\Language\Element\Comment\Property as CommentProperty;
use Phan\Language\ElementContext;
use Phan\Language\FileRef;
use Phan\Language\FQSEN\FullyQualifiedClassConstantName;
use Phan\Language\FQSEN\FullyQualifiedClassName;
use Phan\Language\FQSEN\FullyQualifiedMethodName;
use Phan\Language\FQSEN\FullyQualifiedPropertyName;
use Phan\Language\Scope\ClassScope;
use Phan\Language\Scope\GlobalScope;
use Phan\Language\Type;
use Phan\Language\Type\IterableType;
use Phan\Language\Type\LiteralStringType;
use Phan\Language\Type\MixedType;
use Phan\Language\Type\StaticType;
use Phan\Language\Type\TemplateType;
use Phan\Language\UnionType;
use Phan\Library\None;
use Phan\Library\Option;
use Phan\Library\Some;
use Phan\Memoize;
use Phan\Plugin\ConfigPluginSet;
use Phan\Suggestion;
use ReflectionClass;
use ReflectionProperty;
use RuntimeException;
use function count;
use function is_string;
/**
* Clazz represents the information Phan knows about a class, trait, or interface,
* the state of Phan populating that information (hydration),
* and methods to access that information.
*
* @see CodeBase for the data structures used for looking up classes or elements of classes (properties, methods, constants, etc)
*
* @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod
* @phan-file-suppress PhanPluginNoCommentOnPublicMethod TODO: Add comments
*/
class Clazz extends AddressableElement
{
use Memoize;
use ClosedScopeElement;
/**
* @var Type|null
* The type of the parent of this class if it extends
* anything, else null.
*/
private $parent_type = null;
/**
* @var int
* The line number of the parent of this class if it extends
* anything, else 0 if unknown/missing.
*/
private $parent_type_lineno = 0;
/**
* @var list<FullyQualifiedClassName>
* A possibly empty list of interfaces implemented
* by this class
*/
private $interface_fqsen_list = [];
/**
* @var list<int>
* Line numbers for indices of interface_fqsen_list.
*/
private $interface_fqsen_lineno = [];
/**
* @var list<FullyQualifiedClassName>
* A possibly empty list of traits used by this class
*/
private $trait_fqsen_list = [];
/**
* @var list<int>
* Line numbers for indices of trait_fqsen_list
*/
private $trait_fqsen_lineno = [];
/**
* @var array<string,TraitAdaptations>
* Maps lowercase fqsen of a method to the trait names which are hidden
* and the trait aliasing info
*/
private $trait_adaptations_map = [];
/**
* @var bool - hydrate() will check for this to avoid prematurely hydrating while looking for values of class constants.
*/
private $did_finish_parsing = true;
/**
* @var ?UnionType for Type->asExpandedTypes()
*
* TODO: This won't reverse in daemon mode?
*/
private $additional_union_types = null;
/**
* An additional id to disambiguate classes on the same line
* https://github.com/phan/phan/issues/1988
*/
private $decl_id = 0;
/**
* @var Context
*/
private $internal_context;
/**
* @var list<Type>
*/
private $mixin_types = [];
/**
* @param Context $context
* The context in which the structural element lives
*
* @param string $name
* The name of the typed structural element
*
* @param UnionType $type
* A '|' delimited set of types satisfied by this
* typed structural element.
*
* @param int $flags
* The flags property contains node specific flags. It is
* always defined, but for most nodes it is always zero.
* ast\kind_uses_flags() can be used to determine whether
* a certain kind has a meaningful flags value.
*
* @param FullyQualifiedClassName $fqsen
* A fully qualified name for this class
*
* @param Type|null $parent_type
* @param list<FullyQualifiedClassName> $interface_fqsen_list
* @param list<FullyQualifiedClassName> $trait_fqsen_list
*/
public function __construct(
Context $context,
string $name,
UnionType $type,
int $flags,
FullyQualifiedClassName $fqsen,
Type $parent_type = null,
array $interface_fqsen_list = [],
array $trait_fqsen_list = []
) {
parent::__construct(
$context,
$name,
$type,
$flags,
$fqsen
);
$this->parent_type = $parent_type;
$this->interface_fqsen_list = $interface_fqsen_list;
$this->trait_fqsen_list = $trait_fqsen_list;
$internal_scope = new ClassScope(
$context->getScope(),
$fqsen,
$flags
);
$this->setInternalScope($internal_scope);
$this->internal_context = $context->withScope($internal_scope);
}
private static function getASTFlagsForReflectionProperty(ReflectionProperty $prop): int
{
if ($prop->isPrivate()) {
return \ast\flags\MODIFIER_PRIVATE;
} elseif ($prop->isProtected()) {
return \ast\flags\MODIFIER_PROTECTED;
}
return 0;
}
/**
* @param CodeBase $code_base
* A reference to the entire code base in which this
* context exists
*
* @param ReflectionClass $class
* A reflection class representing a builtin class.
*
* @return Clazz
* A Class structural element representing the given named
* builtin.
*/
public static function fromReflectionClass(
CodeBase $code_base,
ReflectionClass $class
): Clazz {
// Build a set of flags based on the constitution
// of the built-in class
$flags = 0;
if ($class->isFinal()) {
$flags = \ast\flags\CLASS_FINAL;
} elseif ($class->isInterface()) {
$flags = \ast\flags\CLASS_INTERFACE;
} elseif ($class->isTrait()) {
$flags = \ast\flags\CLASS_TRAIT;
}
if ($class->isAbstract()) {
$flags |= \ast\flags\CLASS_ABSTRACT;
}
$context = new Context();
$class_name = $class->getName();
// @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid
$class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString($class_name);
// Build a base class element
$clazz = new Clazz(
$context,
$class_name,
UnionType::fromFullyQualifiedRealString('\\' . $class_name),
$flags,
$class_fqsen
);
// If this class has a parent class, add it to the
// class info
if (($parent_class = $class->getParentClass())) {
// @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid
$parent_class_fqsen = FullyQualifiedClassName::fromFullyQualifiedString(
'\\' . $parent_class->getName()
);
$parent_type = $parent_class_fqsen->asType();
$clazz->setParentType($parent_type);
}
if ($class_name === "Traversable") {
// Make sure that canCastToExpandedUnionType() works as expected for Traversable and its subclasses
$clazz->addAdditionalType(IterableType::instance(false));
}
$class_scope = new ClassScope(new GlobalScope(), $class_fqsen, $flags);
// Note: If there are multiple calls to Clazz->addProperty(),
// the UnionType from the first one will be used, subsequent calls to addProperty()
// will have no effect.
// As a result, we set the types from Phan's documented internal property types first,
// preferring them over the default values (which may be null, etc.).
foreach (UnionType::internalPropertyMapForClassName(
$clazz->getName()
) as $property_name => $property_type_string) {
// An asterisk indicates that the class supports
// dynamic properties
if ($property_name === '*') {
$clazz->setHasDynamicProperties(true);
continue;
}
$property_context = $context->withScope($class_scope);
$property_type =
UnionType::fromStringInContext(
$property_type_string,
new Context(),
Type::FROM_TYPE
);
$property_fqsen = FullyQualifiedPropertyName::make(
$clazz->getFQSEN(),
$property_name
);
if ($class->hasProperty($property_name)) {
$reflection_property = $class->getProperty($property_name);
$flags = self::getASTFlagsForReflectionProperty($reflection_property);
$real_type = self::getRealTypeForReflectionProperty($reflection_property);
} else {
$flags = 0;
$real_type = UnionType::empty();
}
$property = new Property(
$property_context,
$property_name,
$property_type->withRealTypeSet($real_type->getTypeSet()),
$flags,
$property_fqsen,
$real_type
);
// Record that Phan has known union types for this internal property,
// so that analysis of assignments to the property can account for it.
$property->setPHPDocUnionType($property_type);
$clazz->addProperty($code_base, $property, None::instance());
}
// n.b.: public properties on internal classes don't get
// listed via reflection until they're set unless
// they have a default value. Therefore, we don't
// bother iterating over `$class->getProperties()`
// `$class->getStaticProperties()`.
foreach ($class->getDefaultProperties() as $name => $default_value) {
$property_context = $context->withScope($class_scope);
$property_fqsen = FullyQualifiedPropertyName::make(
$clazz->getFQSEN(),
$name
);
if ($clazz->hasPropertyWithName($code_base, $name)) {
continue;
}
if ($class->hasProperty($name)) {
$reflection_property = $class->getProperty($name);
$flags = self::getASTFlagsForReflectionProperty($reflection_property);
$real_type = self::getRealTypeForReflectionProperty($reflection_property);
} else {
$flags = 0;
$real_type = UnionType::empty();
}
$property = new Property(
$property_context,
$name,
Type::fromObject($default_value)->asPHPDocUnionType()->withRealTypeSet($real_type->getTypeSet()),
$flags,
$property_fqsen,
$real_type
);
$clazz->addProperty($code_base, $property, None::instance());
}
foreach ($class->getProperties() as $reflection_property) {
// In PHP 7.4, it's possible for internal classes to have properties without defaults if they're uninitialized.
$name = $reflection_property->name;
if ($clazz->hasPropertyWithName($code_base, $name)) {
continue;
}
$property_context = $context->withScope($class_scope);
$property_fqsen = FullyQualifiedPropertyName::make(
$clazz->getFQSEN(),
$name
);
$real_type = self::getRealTypeForReflectionProperty($reflection_property);
$property = new Property(
$property_context,
$name,
$real_type->asRealUnionType(),
self::getASTFlagsForReflectionProperty($reflection_property),
$property_fqsen,
$real_type
);
$clazz->addProperty($code_base, $property, None::instance());
}
foreach ($class->getInterfaceNames() as $name) {
$clazz->addInterfaceClassFQSEN(
// @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid
FullyQualifiedClassName::fromFullyQualifiedString(
'\\' . $name
)
);
}
foreach ($class->getTraitNames() as $name) {
// TODO: optionally, support getTraitAliases()? This is low importance for internal PHP modules,
// it would be uncommon to see traits in internal PHP modules.
$clazz->addTraitFQSEN(
// @phan-suppress-next-line PhanThrowTypeAbsentForCall should be valid if extension is valid
FullyQualifiedClassName::fromFullyQualifiedString(
'\\' . $name
)
);
}
foreach ($class->getConstants() as $name => $value) {
$constant_fqsen = FullyQualifiedClassConstantName::make(
$clazz->getFQSEN(),
$name
);
$constant = new ClassConstant(
$context,
$name,
Type::fromObject($value)->asRealUnionType(), // TODO: These can vary based on OS/build flags
0,
$constant_fqsen
);
$constant->setNodeForValue($value);
$clazz->addConstant($code_base, $constant);
}
foreach ($class->getMethods() as $reflection_method) {
if ($reflection_method->getDeclaringClass()->name !== $class_name) {
continue;
}
$method_context = $context->withScope($class_scope);
$method_list =
FunctionFactory::methodListFromReflectionClassAndMethod(
$method_context,
$class,
$reflection_method
);
foreach ($method_list as $method) {
$clazz->addMethod($code_base, $method, None::instance());
}
}
return $clazz;
}
/**
* @suppress PhanUndeclaredMethod
*/
private static function getRealTypeForReflectionProperty(ReflectionProperty $property): UnionType
{
if (\PHP_VERSION_ID >= 70400) {
if ($property->hasType()) {
return UnionType::fromReflectionType($property->getType());
}
}
return UnionType::empty();
}
/**
* @param Type $parent_type
* The type of the parent (extended) class of this class.
*/
public function setParentType(Type $parent_type, int $lineno = 0): void
{
if ($this->getInternalScope()->hasAnyTemplateType()) {
// Get a reference to the local list of templated
// types. We'll use this to map templated types on the
// parent to locally templated types.
$template_type_map =
$this->getInternalScope()->getTemplateTypeMap();
// Figure out if the given parent type contains any template
// types.
$contains_templated_type = false;
foreach ($parent_type->getTemplateParameterTypeList() as $union_type) {
foreach ($union_type->getTypeSet() as $type) {
if (isset($template_type_map[$type->getName()])) {
$contains_templated_type = true;
break 2;
}
}
}
// If necessary, map the template parameter type list through the
// local list of templated types.
if ($contains_templated_type) {
$parent_type = Type::fromType(
$parent_type,
\array_map(static function (UnionType $union_type) use ($template_type_map): UnionType {
return UnionType::of(
\array_map(static function (Type $type) use ($template_type_map): Type {
return $template_type_map[$type->getName()] ?? $type;
}, $union_type->getTypeSet()),
[]
);
}, $parent_type->getTemplateParameterTypeList())
);
}
}
$this->parent_type = $parent_type;
$this->parent_type_lineno = $lineno;
// Add the parent to the union type of this class
$this->addAdditionalType($parent_type);
}
/**
* @return bool
* True if this class has a parent class
*/
public function hasParentType(): bool
{
return $this->parent_type !== null;
}
/**
* @return Option<Type>
* If a parent type is defined, get Some<Type>, else None.
*/
public function getParentTypeOption(): Option
{
if ($this->parent_type !== null) {
return new Some($this->parent_type);
}
return None::instance();
}
/**
* @return FullyQualifiedClassName
* The parent class of this class if one exists
*
* @throws LogicException
* An exception is thrown if this class has no parent
*/
public function getParentClassFQSEN(): FullyQualifiedClassName
{
if (!$this->parent_type) {
throw new LogicException("Class $this has no parent");
}
return FullyQualifiedClassName::fromType($this->parent_type);
}
/**
* @return Clazz
* The parent class of this class if defined
*
* @throws LogicException|RuntimeException
* An exception is thrown if this class has no parent
*/
public function getParentClass(CodeBase $code_base): Clazz
{
if (!$this->parent_type) {
throw new LogicException("Class $this has no parent");
}
$parent_fqsen = FullyQualifiedClassName::fromType($this->parent_type);
// invoking hasClassWithFQSEN also has the side effect of lazily loading the parent class definition.
if (!$code_base->hasClassWithFQSEN($parent_fqsen)) {
throw new RuntimeException("Failed to load parent Class $parent_fqsen of Class $this");
}
return $code_base->getClassByFQSEN(
$parent_fqsen
);
}
/**
* @return Clazz
* The parent class of this class if defined (does not trigger class hydration of the parent class, unlike getParentClass)
*
* @throws LogicException|RuntimeException
* An exception is thrown if this class has no parent
*/
private function getParentClassWithoutHydrating(CodeBase $code_base): Clazz
{
if (!$this->parent_type) {
throw new LogicException("Class $this has no parent");
}
$parent_fqsen = FullyQualifiedClassName::fromType($this->parent_type);
// invoking hasClassWithFQSEN also has the side effect of lazily loading the parent class definition.
if (!$code_base->hasClassWithFQSEN($parent_fqsen)) {
throw new RuntimeException("Failed to load parent Class $parent_fqsen of Class $this");
}
return $code_base->getClassByFQSENWithoutHydrating(
$parent_fqsen
);
}
/**
* @param list<Type> $mixin_types
*/
public function setMixinTypes(array $mixin_types): void
{
$this->mixin_types = $mixin_types;
}
/**
* Is this a subclass of $other?
*
* This only checks parent classes.
* It should not be used for traits or interfaces.
*
* This returns false if $this === $other
*
* @deprecated This may lead to infinite recursion when analyzing invalid code. asExpandedTypes should be used instead.
* @suppress PhanUnreferencedPublicMethod
* @suppress PhanDeprecatedFunction
*/
public function isSubclassOf(CodeBase $code_base, Clazz $other): bool
{
if (!$this->hasParentType()) {
return false;
}
if (!$code_base->hasClassWithFQSEN(
$this->getParentClassFQSEN()
)) {
// Let this emit an issue elsewhere for the
// parent not existing
return false;
}
// Get the parent class
$parent = $this->getParentClass($code_base);
if ($parent === $other) {
return true;
}
return $parent->isSubclassOf($code_base, $other);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return int
* This class's depth in the class hierarchy
*/
public function getHierarchyDepth(CodeBase $code_base): int
{
if (!$this->hasParentType()) {
return 0;
}
if (!$code_base->hasClassWithFQSEN(
$this->getParentClassFQSEN()
)) {
// Let this emit an issue elsewhere for the
// parent not existing
return 0;
}
// Get the parent class
$parent = $this->getParentClass($code_base);
// Prevent infinite loops
if ($parent === $this) {
return 0;
}
return (1 + $parent->getHierarchyDepth($code_base));
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return FullyQualifiedClassName
* The FQSEN of the root class on this class's hierarchy
*/
public function getHierarchyRootFQSEN(
CodeBase $code_base
): FullyQualifiedClassName {
$visited = [];
for ($current = $this; $current->hasParentType(); $current = $parent) {
$fqsen = $current->getFQSEN();
if (!$code_base->hasClassWithFQSEN(
$current->getParentClassFQSEN()
)) {
// Let this emit an issue elsewhere for the
// parent not existing
return $fqsen;
}
// Get the parent class
$parent = $current->getParentClass($code_base);
$visited[$fqsen->__toString()] = true;
// Prevent infinite loops
if (\array_key_exists($parent->getFQSEN()->__toString(), $visited)) {
return $fqsen;
}
}
return $current->getFQSEN();
}
/**
* Add the given FQSEN to the list of implemented
* interfaces for this class.
*
* @param FullyQualifiedClassName $fqsen
*/
public function addInterfaceClassFQSEN(FullyQualifiedClassName $fqsen, int $lineno = 0): void
{
$this->interface_fqsen_lineno[count($this->interface_fqsen_list)] = $lineno;
$this->interface_fqsen_list[] = $fqsen;
// Add the interface to the union type of this
// class
$this->addAdditionalType($fqsen->asType());
}
/**
* Get the list of interfaces implemented by this class
* @return list<FullyQualifiedClassName>
*/
public function getInterfaceFQSENList(): array
{
return $this->interface_fqsen_list;
}
/**
* Add a property to this class
*
* @param CodeBase $code_base
* A reference to the code base in which the ancestor exists
*
* @param Property $property
* The property to copy onto this class
*
* @param Option<Type> $type_option
* A possibly defined type used to define template
* parameter types when importing the property
*
* @param bool $from_trait
*/
public function addProperty(
CodeBase $code_base,
Property $property,
Option $type_option,
bool $from_trait = false
): void {
// Ignore properties we already have
// TODO: warn about private properties in subclass overriding ancestor private property.
$property_name = $property->getName();
if ($this->hasPropertyWithName($code_base, $property_name)) {
// TODO: Check if trait properties would be inherited first.
// TODO: Figure out semantics and use $from_trait?
self::checkPropertyCompatibility(
$code_base,
$property,
$this->getPropertyByName($code_base, $property_name)
);
return;
}
$property_fqsen = FullyQualifiedPropertyName::make(
$this->getFQSEN(),
$property_name
);
// TODO: defer template properties until the analysis phase? They might not be parsed or resolved yet.
$original_property_fqsen = $property->getFQSEN();
if ($original_property_fqsen !== $property_fqsen) {
$property = clone($property);
$property->setFQSEN($property_fqsen);
if ($property->hasStaticInUnionType()) {
$property->inheritStaticUnionType($original_property_fqsen->getFullyQualifiedClassName(), $this->getFQSEN());
}
// Private properties of traits are accessible from the class that used that trait
// (as well as from within the trait itself).
// Also, for inheritance purposes, treat protected properties the same way.
if ($from_trait) {
$property->setDefiningFQSEN($property_fqsen);
}
try {
// If we have a parent type defined, map the property's
// type through it
if ($type_option->isDefined()
&& !$property->hasUnresolvedFutureUnionType()
&& $property->getUnionType()->hasTemplateType()
) {
$property->setUnionType(
$property->getUnionType()->withTemplateParameterTypeMap(
$type_option->get()->getTemplateParameterTypeMap(
$code_base
)
)
);
}
} catch (IssueException $exception) {
Issue::maybeEmitInstance(
$code_base,
$property->getContext(),
$exception->getIssueInstance()
);
}
}
$code_base->addProperty($property);
}
private static function checkPropertyCompatibility(
CodeBase $code_base,
Property $inherited_property,
Property $overriding_property
): void {
$overriding_property->setIsOverride(true);
if ($inherited_property->isFromPHPDoc() || $inherited_property->isDynamicProperty() ||
$overriding_property->isFromPHPDoc() || $overriding_property->isDynamicProperty()) {
return;
}
if ($inherited_property->isStrictlyMoreVisibleThan($overriding_property)) {
if ($inherited_property->isPHPInternal()) {
if (!$overriding_property->checkHasSuppressIssueAndIncrementCount(Issue::PropertyAccessSignatureMismatchInternal)) {
Issue::maybeEmit(
$code_base,
new ElementContext($overriding_property),
Issue::PropertyAccessSignatureMismatchInternal,
$overriding_property->getFileRef()->getLineNumberStart(),
$overriding_property->asVisibilityAndFQSENString(),
$inherited_property->asVisibilityAndFQSENString()
);
}
} else {
if (!$overriding_property->checkHasSuppressIssueAndIncrementCount(Issue::PropertyAccessSignatureMismatchInternal)) {
Issue::maybeEmit(
$code_base,
new ElementContext($overriding_property),
Issue::PropertyAccessSignatureMismatch,
$overriding_property->getFileRef()->getLineNumberStart(),
$overriding_property,
$inherited_property,
$inherited_property->getFileRef()->getFile(),
$inherited_property->getFileRef()->getLineNumberStart()
);
}
}
}
// original_property is the one that the class is using.
// We added $property after that (so it likely in a base class, or a trait's property added after this property was added)
if ($overriding_property->isStatic() != $inherited_property->isStatic()) {
Issue::maybeEmit(
$code_base,
new ElementContext($overriding_property),
$overriding_property->isStatic() ? Issue::AccessNonStaticToStaticProperty : Issue::AccessStaticToNonStaticProperty,
$overriding_property->getFileRef()->getLineNumberStart(),
$inherited_property->asPropertyFQSENString(),
$overriding_property->asPropertyFQSENString()
);
}
}
/**
* @param array<string,CommentProperty> $magic_property_map mapping from property name to property
* @param CodeBase $code_base
* @return bool whether or not we defined the magic property map
*/
public function setMagicPropertyMap(
array $magic_property_map,
CodeBase $code_base
): bool {
if (count($magic_property_map) === 0) {
return true; // Vacuously true.
}
$class_fqsen = $this->getFQSEN();
$context = $this->internal_context;
foreach ($magic_property_map as $comment_parameter) {
// $phan_flags can be used to indicate if something is property-read or property-write
$phan_flags = $comment_parameter->getFlags();
$property_name = $comment_parameter->getName();
$property_fqsen = FullyQualifiedPropertyName::make(
$class_fqsen,
$property_name
);
$original_union_type = $comment_parameter->getUnionType();
$union_type = $original_union_type->withStaticResolvedInContext($context);
$property = new Property(
(clone($context))->withLineNumberStart($comment_parameter->getLine()),
$property_name,
$union_type,
0,
$property_fqsen,
UnionType::empty()
);
$property->setPHPDocUnionType($union_type);
if ($original_union_type !== $union_type) {
$phan_flags |= Flags::HAS_STATIC_UNION_TYPE;
}
$property->setPhanFlags($phan_flags | Flags::IS_FROM_PHPDOC);
$this->addProperty($code_base, $property, None::instance());
}
return true;
}
/**
* @param array<string,\Phan\Language\Element\Comment\Method> $magic_method_map mapping from method name to this.
* @param CodeBase $code_base
* @return bool whether or not we defined the magic method map
*/
public function setMagicMethodMap(
array $magic_method_map,
CodeBase $code_base
): bool {
if (count($magic_method_map) === 0) {
return true; // Vacuously true.
}
$class_fqsen = $this->getFQSEN();
$context = $this->internal_context;
$is_pure = $this->isPure();
foreach ($magic_method_map as $comment_method) {
// $flags is the same as the flags for `public` and non-internal?
// Or \ast\flags\MODIFIER_PUBLIC.
$flags = \ast\flags\MODIFIER_PUBLIC;
if ($comment_method->isStatic()) {
$flags |= \ast\flags\MODIFIER_STATIC;
}
$method_name = $comment_method->getName();
if ($this->hasMethodWithName($code_base, $method_name, true)) {
// No point, and this would hurt inference accuracy.
continue;
}
$method_fqsen = FullyQualifiedMethodName::make(
$class_fqsen,
$method_name
);
$method_context = (clone($context))->withLineNumberStart($comment_method->getLine());
$real_parameter_list = \array_map(static function (\Phan\Language\Element\Comment\Parameter $parameter) use ($method_context): Parameter {
return $parameter->asRealParameter($method_context);
}, $comment_method->getParameterList());
$method = new Method(
$method_context,
$method_name,
$comment_method->getUnionType(),
$flags,
$method_fqsen,
$real_parameter_list
);
$method->setRealParameterList($real_parameter_list);
$method->setNumberOfRequiredParameters($comment_method->getNumberOfRequiredParameters());
$method->setNumberOfOptionalParameters($comment_method->getNumberOfOptionalParameters());
$method->setIsFromPHPDoc(true);
if ($is_pure && !$comment_method->isStatic()) {
$method->setIsPure();
}
$this->addMethod($code_base, $method, None::instance());
}
return true;
}
public function hasPropertyWithName(
CodeBase $code_base,
string $name
): bool {
return $code_base->hasPropertyWithFQSEN(
FullyQualifiedPropertyName::make(
$this->getFQSEN(),
$name
)
);
}
/**
* Returns the property $name of this class.
* @see self::hasPropertyWithName()
*/
public function getPropertyByName(
CodeBase $code_base,
string $name
): Property {
return $code_base->getPropertyByFQSEN(
FullyQualifiedPropertyName::make(
$this->getFQSEN(),
$name
)
);
}
/**
* @param CodeBase $code_base
* A reference to the entire code base in which the
* property exists.
*
* @param string $name
* The name of the property
*
* @param Context $context
* The context of the caller requesting the property
*
* @return Property
* A property with the given name.
* Callers can check if the property is read-only when writing,
* or write-only when reading.
*
* @throws IssueException
* An exception may be thrown if the caller does not
* have access to the given property from the given
* context
*/
public function getPropertyByNameInContext(
CodeBase $code_base,
string $name,
Context $context,
bool $is_static,
Node $node = null,
bool $is_known_assignment = false
): Property {
// Get the FQSEN of the property we're looking for
$property_fqsen = FullyQualifiedPropertyName::make(
$this->getFQSEN(),
$name
);
$property = null;
// Figure out if we have the property and
// figure out if the property is accessible.
$is_property_accessible = false;
if ($code_base->hasPropertyWithFQSEN($property_fqsen)) {
$property = $code_base->getPropertyByFQSEN(
$property_fqsen
);
if ($is_static !== $property->isStatic()) {
if ($is_static) {
throw new IssueException(
Issue::fromType(Issue::AccessPropertyNonStaticAsStatic)(
$context->getFile(),
$context->getLineNumberStart(),
[$property->asPropertyFQSENString()]
)
);
} else {
throw new IssueException(
Issue::fromType(Issue::AccessPropertyStaticAsNonStatic)(
$context->getFile(),
$context->getLineNumberStart(),
[$property->asPropertyFQSENString()]
)
);
}
}
$is_property_accessible = $property->isAccessibleFromClass(
$code_base,
$context->getClassFQSENOrNull()
);
}
if ($is_static && $property) {
// If the property is from a trait, the (different) defining FQSEN is the FQSEN of the class using the FQSEN, not the trait.
$defining_fqsen = $property->getDefiningFQSEN();
if ($defining_fqsen !== $property_fqsen) {
if ($code_base->hasPropertyWithFQSEN($defining_fqsen)) {
$property = $code_base->getPropertyByFQSEN($defining_fqsen);
}
}
}
// If the property exists and is accessible, return it
if ($is_property_accessible) {
// @phan-suppress-next-line PhanTypeMismatchReturnNullable is_property_accessible ensures that this is non-null
return $property;
}
// Check to see if we can use a __get magic method
// TODO: What about __set?
if (!$is_static && $this->hasMethodWithName($code_base, '__get', true)) {
$method = $this->getMethodByName($code_base, '__get');
// Make sure the magic method is accessible
// TODO: Add defined at %s:%d for the property definition
if ($method->isPrivate()) {
throw new IssueException(
Issue::fromType(Issue::AccessPropertyPrivate)(
$context->getFile(),
$context->getLineNumberStart(),
[
$property ? $property->asPropertyFQSENString() : $property_fqsen,
$method->getContext()->getFile(),
$method->getContext()->getLineNumberStart()
]
)
);
} elseif ($method->isProtected()) {
if (!self::isAccessToElementOfThis($node)) {
throw new IssueException(
Issue::fromType(Issue::AccessPropertyProtected)(
$context->getFile(),
$context->getLineNumberStart(),
[
$property ? $property->asPropertyFQSENString() : $property_fqsen,
$method->getContext()->getFile(),
$method->getContext()->getLineNumberStart()
]
)
);
}
}
$property = new Property(
$context,
$name,
$method->getUnionType(),
0,
$property_fqsen,
UnionType::empty()
);
$property->setIsDynamicProperty(true);
$this->addProperty($code_base, $property, None::instance());
return $property;
} elseif ($property) {
// If we have a property, but it's inaccessible, emit
// an issue
if ($property->isPrivate()) {
throw new IssueException(
Issue::fromType(Issue::AccessPropertyPrivate)(
$context->getFile(),
$context->getLineNumberStart(),
[$property->asPropertyFQSENString(), $property->getContext()->getFile(), $property->getContext()->getLineNumberStart() ],
$this->suggestGettersOrSetters($code_base, $context, $property, $is_known_assignment)
)
);
}
if ($property->isProtected()) {
if (self::isAccessToElementOfThis($node)) {
return $property;
}
throw new IssueException(
Issue::fromType(Issue::AccessPropertyProtected)(
$context->getFile(),
$context->getLineNumberStart(),
[$property->asPropertyFQSENString(), $property->getContext()->getFile(), $property->getContext()->getLineNumberStart() ],
$this->suggestGettersOrSetters($code_base, $context, $property, $is_known_assignment)
)
);
}
}
// Check to see if missing properties are allowed
// or we're working with a class with dynamic
// properties such as stdClass.
if (!$is_static && (Config::getValue('allow_missing_properties')
|| $this->hasDynamicProperties($code_base))
) {
$property = new Property(
$context,
$name,
UnionType::empty(),
0,
$property_fqsen,
UnionType::empty()
);
$property->setIsDynamicProperty(true);
$this->addProperty($code_base, $property, None::instance());
return $property;
}
throw new IssueException(
Issue::fromType(Issue::UndeclaredProperty)(
$context->getFile(),
$context->getLineNumberStart(),
[$this->getFQSEN() . ($is_static ? '::$' : '->') . $name],
IssueFixSuggester::suggestSimilarProperty($code_base, $context, $this, $name, $is_static)
)
);
}
private function suggestGettersOrSetters(CodeBase $code_base, Context $context, Property $property, bool $is_known_assignment): ?Suggestion
{
if ($is_known_assignment) {
return $this->suggestSetters($code_base, $context, $property);
} else {
return $this->suggestGetters($code_base, $context, $property);
}
}
private function suggestSetters(CodeBase $code_base, Context $context, Property $property): ?Suggestion
{
$getters = $this->getSettersMap($code_base)[$property->getName()] ?? [];
if (!$getters) {
return null;
}
$suggestions = [];
// @phan-suppress-next-line PhanAccessMethodInternal
$class_fqsen_in_current_scope = IssueFixSuggester::maybeGetClassInCurrentScope($context);
foreach ($getters as $method) {
if ($method->isAccessibleFromClass($code_base, $class_fqsen_in_current_scope)) {
$suggestions[] = $method->getRepresentationForIssue();
}
}
if (!$suggestions) {
return null;
}
\sort($suggestions, \SORT_STRING);
return Suggestion::fromString('Did you mean ' . \implode(' or ', $suggestions));
}
/**
* @return array<string,list<Method>> maps property names to setters for that property
*/
private function getSettersMap(CodeBase $code_base): array
{
return $this->memoize(
__METHOD__,
/**
* @return array<string,list<Method>> maps property names to setters for that property (both instance and static properties)
*/
function () use ($code_base): array {
if ($this->isPHPInternal()) {
return [];
}
$setters = [];
foreach ($this->getMethodMap($code_base) as $method) {
if ($method->isStatic()) {
continue;
}
if ($method->getNumberOfParameters() === 0) {
continue;
}
$node = $method->getNode()->children['stmts'] ?? null;
if (!$node instanceof Node) {
continue;
}
$first_parameter = $method->getParameterList()[0] ?? null;
if (!$first_parameter) {
// func_get
continue;
}
$fetched_property_name = self::computeSetPropertyName($node, $first_parameter->getName());
if (is_string($fetched_property_name)) {
$setters[$fetched_property_name][] = $method;
}
}
return $setters;
}
);
}
private function suggestGetters(CodeBase $code_base, Context $context, Property $property): ?Suggestion
{
$getters = $this->getGettersMap($code_base)[$property->getName()] ?? [];
if (!$getters) {
return null;
}
$suggestions = [];
// @phan-suppress-next-line PhanAccessMethodInternal
$class_fqsen_in_current_scope = IssueFixSuggester::maybeGetClassInCurrentScope($context);
foreach ($getters as $method) {
if ($method->isAccessibleFromClass($code_base, $class_fqsen_in_current_scope)) {
$suggestions[] = $method->getRepresentationForIssue();
}
}
if (!$suggestions) {
return null;
}
return Suggestion::fromString('Did you mean ' . \implode(' or ', $suggestions));
}
/**
* @return array<string,list<Method>> maps property names to getters for that property
*/
public function getGettersMap(CodeBase $code_base): array
{
if ($this->isInterface()) {
return [];
}
return $this->memoize(
__METHOD__,
/**
* @return array<string,list<Method>> maps property names to getters for that property (for instance properties)
*/
function () use ($code_base): array {
if ($this->isPHPInternal()) {
return [];
}
// Hydrate the class so that getters from ancestor classes will also be accessible
$this->hydrate($code_base);
$getters = [];
foreach ($this->getMethodMap($code_base) as $method) {
if ($method->isStatic()) {
// TODO support static getters for static properties
continue;
}
$node = $method->getNode()->children['stmts'] ?? null;
if (!$node instanceof Node) {
continue;
}
$fetched_property_name = self::computeFetchedPropertyName($node);
if (is_string($fetched_property_name)) {
$getters[$fetched_property_name][] = $method;
}
}
return $getters;
}
);
}
private static function computeFetchedPropertyName(Node $node): ?string
{
if (count($node->children) !== 1) {
return null;
}
$stmt = $node->children[0];
if (!$stmt instanceof Node || $stmt->kind !== ast\AST_RETURN) {
return null;
}
return self::getPropName($stmt->children['expr']);
}
/**
* Returns the name of the instance property set to the parameter with name $expected_parameter_name, if this is a setter
*/
private static function computeSetPropertyName(Node $node, string $expected_parameter_name): ?string
{
if (count($node->children) !== 1) {
return null;
}
$stmt = $node->children[0];
if (!$stmt instanceof Node || $stmt->kind !== ast\AST_ASSIGN) {
return null;
}
$prop_name = self::getPropName($stmt->children['var']);
if (!is_string($prop_name)) {
return null;
}
$expr = $stmt->children['expr'];
if (!$expr instanceof Node || $expr->kind !== ast\AST_VAR) {
return null;
}
if ($expr->children['name'] === $expected_parameter_name) {
return $prop_name;
}
return null;
}
/**
* @param Node|string|int|float|null $node
*/
private static function getPropName($node): ?string
{
if (!$node instanceof Node) {
return null;
}
if ($node->kind !== ast\AST_PROP) {
return null;
}
$obj = $node->children['expr'];
if (!($obj instanceof Node && $obj->kind === ast\AST_VAR &&
$obj->children['name'] === 'this')) {
return null;
}
$prop = $node->children['prop'];
return is_string($prop) ? $prop : null;
}
/**
* Returns true if this is an access to a property or method of self/static/$this
*
* @param ?Node $node
*/
public static function isAccessToElementOfThis(?Node $node): bool
{
if (!($node instanceof Node)) {
return false;
}
$node = $node->children['expr'] ?? $node->children['class'];
if (!($node instanceof Node)) {
return false;
}
switch ($node->kind) {
case ast\AST_VAR:
$name = $node->children['name'];
return is_string($name) && $name === 'this';
case ast\AST_CONST:
$name = $node->children['name']->children['name'] ?? null;
return is_string($name) && \strcasecmp($name, 'static') === 0;
default:
return false;
}
}
/**
* @return array<string,Property>
* The list of properties on this class
*/
public function getPropertyMap(CodeBase $code_base): array
{
return $code_base->getPropertyMapByFullyQualifiedClassName(
$this->getFQSEN()
);
}
/**
* Inherit a class constant from an ancestor class
*/
public function inheritConstant(
CodeBase $code_base,
ClassConstant $constant
): void {
$constant_fqsen = FullyQualifiedClassConstantName::make(
$this->getFQSEN(),
$constant->getName()
);
if ($code_base->hasClassConstantWithFQSEN($constant_fqsen)) {
// If the constant with that name already exists, mark it as an override.
$overriding_constant = $code_base->getClassConstantByFQSEN($constant_fqsen);
$overriding_constant->setIsOverride(true);
self::checkConstantCompatibility(
$code_base,
$constant,
$code_base->getClassConstantByFQSEN(
$constant_fqsen
)
);
return;
}
// Warn if inheriting a class constant declared as @abstract without overriding it.
// Optionally, could check if other interfaces declared class constants with the same value, but low priority.
if ($constant->isPHPDocAbstract() && !$constant->isPrivate() && !$this->isAbstract() && $this->isClass()) {
Issue::maybeEmit(
$code_base,
$this->getContext(),
Issue::CommentAbstractOnInheritedConstant,
$this->getContext()->getLineNumberStart(),
$this->getFQSEN(),
$constant->getRealDefiningFQSEN(),
$constant->getContext()->getFile(),
$constant->getContext()->getLineNumberStart(),
'@abstract'
);
}
// Update the FQSEN if it's not associated with this
// class yet (always true)
if ($constant->getFQSEN() !== $constant_fqsen) {
$constant = clone($constant);
$constant->setFQSEN($constant_fqsen);
}
$code_base->addClassConstant($constant);
}
private static function checkConstantCompatibility(
CodeBase $code_base,
ClassConstant $inherited_constant,
ClassConstant $overriding_constant
): void {
// Traits don't have constants, thankfully, so the logic is simple.
if ($inherited_constant->isStrictlyMoreVisibleThan($overriding_constant)) {
if ($inherited_constant->isPHPInternal()) {
if (!$overriding_constant->checkHasSuppressIssueAndIncrementCount(Issue::ConstantAccessSignatureMismatchInternal)) {
Issue::maybeEmit(
$code_base,
$overriding_constant->getContext(),
Issue::ConstantAccessSignatureMismatchInternal,
$overriding_constant->getFileRef()->getLineNumberStart(),
$overriding_constant->asVisibilityAndFQSENString(),
$inherited_constant->asVisibilityAndFQSENString()
);
}
} else {
if (!$overriding_constant->checkHasSuppressIssueAndIncrementCount(Issue::ConstantAccessSignatureMismatchInternal)) {
Issue::maybeEmit(
$code_base,
$overriding_constant->getContext(),
Issue::ConstantAccessSignatureMismatch,
$overriding_constant->getFileRef()->getLineNumberStart(),
$overriding_constant->asVisibilityAndFQSENString(),
$inherited_constant->asVisibilityAndFQSENString(),
$inherited_constant->getFileRef()->getFile(),
$inherited_constant->getFileRef()->getLineNumberStart()
);
}
}
}
}
/**
* Add a class constant
*/
public function addConstant(
CodeBase $code_base,
ClassConstant $constant
): void {
$constant_fqsen = FullyQualifiedClassConstantName::make(
$this->getFQSEN(),
$constant->getName()
);
// Update the FQSEN if it's not associated with this
// class yet
if ($constant->getFQSEN() !== $constant_fqsen) {
$constant = clone($constant);
$constant->setFQSEN($constant_fqsen);
}
$code_base->addClassConstant($constant);
}
/**
* @return bool
* True if a constant with the given name is defined
* on this class.
*/
public function hasConstantWithName(
CodeBase $code_base,
string $name
): bool {
if ($code_base->hasClassConstantWithFQSEN(
FullyQualifiedClassConstantName::make(
$this->getFQSEN(),
$name
)
)) {
return true;
}
if (!$this->hydrateConstantsIndicatingFirstTime($code_base)) {
return false;
}
return $code_base->hasClassConstantWithFQSEN(
FullyQualifiedClassConstantName::make(
$this->getFQSEN(),
$name
)
);
}
/**
* @param CodeBase $code_base
* A reference to the entire code base in which the
* property exists.
*
* @param string $name
* The name of the class constant
*
* @param Context $context
* The context of the caller requesting the property
*
* @return ClassConstant
* A constant with the given name
*
* @throws IssueException
* An exception may be thrown if the caller does not
* have access to the given property from the given
* context
*/
public function getConstantByNameInContext(
CodeBase $code_base,
string $name,
Context $context
): ClassConstant {
$constant_fqsen = FullyQualifiedClassConstantName::make(
$this->getFQSEN(),
$name
);
if (!$code_base->hasClassConstantWithFQSEN($constant_fqsen)) {
throw new IssueException(
Issue::fromType(Issue::UndeclaredConstantOfClass)(
$context->getFile(),
$context->getLineNumberStart(),
[
$this->getFQSEN() . '::' . $constant_fqsen
],
IssueFixSuggester::suggestSimilarClassConstant($code_base, $context, $constant_fqsen)
)
);
}
$constant = $code_base->getClassConstantByFQSEN(
$constant_fqsen
);
if ($constant->isPublic()) {
// Most constants are public, check that first.
return $constant;
}
// Visibility checks for private/protected class constants:
// Are we within a class referring to the class
// itself?
$is_local_access = (
$context->isInClassScope()
&& $context->getClassInScope($code_base) === $constant->getClass($code_base)
);
if ($is_local_access) {
// Classes can always access constants declared in the same class
return $constant;
}
if ($constant->isPrivate()) {
// This is attempting to access a private constant from outside of the class
throw new IssueException(
Issue::fromType(Issue::AccessClassConstantPrivate)(
$context->getFile(),
$context->getLineNumberStart(),
[
(string)$constant_fqsen,
$constant->getContext()->getFile(),
$constant->getContext()->getLineNumberStart()
]
)
);
}
// We now know that $constant is a protected constant
// Are we within a class or an extending sub-class
// referring to the class?
$is_remote_access = $context->isInClassScope()
&& $context->getClassInScope($code_base)
->getUnionType()->canCastToExpandedUnionType(
$this->getUnionType(),
$code_base
);
if (!$is_remote_access) {
// And the access is not from anywhere on the class hierarchy, so throw
throw new IssueException(
Issue::fromType(Issue::AccessClassConstantProtected)(
$context->getFile(),
$context->getLineNumberStart(),
[
(string)$constant_fqsen,
$constant->getContext()->getFile(),
$constant->getContext()->getLineNumberStart()
]
)
);
}
// Valid access to a protected constant.
return $constant;
}
/**
* @return array<string,ClassConstant>
* The constants associated with this class
*/
public function getConstantMap(CodeBase $code_base): array
{
return $code_base->getClassConstantMapByFullyQualifiedClassName(
$this->getFQSEN()
);
}
/**
* Add a method to this class
*
* @param CodeBase $code_base
* A reference to the code base in which the ancestor exists
*
* @param Method $method
* The method to copy onto this class
*
* @param Option<Type> $type_option
* A possibly defined type used to define template
* parameter types when importing the method
*/
public function addMethod(
CodeBase $code_base,
Method $method,
Option $type_option
): void {
$method_fqsen = FullyQualifiedMethodName::make(
$this->getFQSEN(),
$method->getName(),
$method->getFQSEN()->getAlternateId()
);
$is_override = $code_base->hasMethodWithFQSEN($method_fqsen);
// Don't overwrite overridden methods with
// parent methods
if ($is_override) {
// Note that we're overriding something
// (but only do this if it's abstract)
// TODO: Consider all permutations of abstract and real methods on classes, interfaces, and traits.
$existing_method =
$code_base->getMethodByFQSEN($method_fqsen);
$existing_method_defining_fqsen = $existing_method->getDefiningFQSEN();
// Note: For private/protected methods, the defining FQSEN is set to the FQSEN of the inheriting class.
// So, when multiple traits are inherited, they may have identical defining FQSENs, but some may be abstract, and others may be implemented.
if ($method->getDefiningFQSEN() === $existing_method_defining_fqsen) {
if ($method->isAbstract() === $existing_method->isAbstract()) {
return;
}
} else {
self::markMethodAsOverridden($code_base, $existing_method_defining_fqsen);
}
if ($existing_method->getRealDefiningFQSEN() === $method->getRealDefiningFQSEN()) {
return;
}
if ($existing_method->getRealDefiningFQSEN() === $method_fqsen || $method->isAbstract() || !$existing_method->isAbstract() || $existing_method->isNewConstructor()) {
// TODO: What if both of these are abstract, and those get combined into an abstract class?
// Should phan check compatibility of the abstract methods it inherits?
$existing_method->setIsOverride(true);
// TODO: What happens for protected methods and traits with getDefiningFQSEN
self::markMethodAsOverridden($code_base, $method->getDefiningFQSEN());
// Don't add the method since it was already added
return;
} elseif ($method->getRealDefiningFQSEN() === $method_fqsen) {
$method->setIsOverride(true);
// TODO: What happens for traits with getDefiningFQSEN
self::markMethodAsOverridden($code_base, $existing_method->getDefiningFQSEN());
}
}
if ($method->getFQSEN() !== $method_fqsen) {
$original_method = $method;
$method = clone($method);
$method->setFQSEN($method_fqsen);
// When we inherit it from the ancestor class, it may be an override in the ancestor class,
// but that doesn't imply it's an override in *this* class.
$method->setIsOverride($is_override);
$method->setIsOverriddenByAnother(false);
// Clone the parameter list, so that modifying the parameters on the first call won't modify the others.
$method->cloneParameterList();
$method->ensureClonesReturnType($original_method);
// If we have a parent type defined, map the method's
// return type and parameter types through it
if ($type_option->isDefined()) {
// Map the method's return type
if ($method->getUnionType()->hasTemplateType()) {
$method->setUnionType(
$method->getUnionType()->withTemplateParameterTypeMap(
$type_option->get()->getTemplateParameterTypeMap(
$code_base
)
)
);
}
// Map each method parameter
$method->setParameterList(
\array_map(static function (Parameter $parameter) use ($type_option, $code_base): Parameter {
if (!$parameter->getUnionType()->hasTemplateType()) {
return $parameter;
}
$mapped_parameter = clone($parameter);
$mapped_parameter->setUnionType(
$mapped_parameter->getUnionType()->withTemplateParameterTypeMap(
$type_option->get()->getTemplateParameterTypeMap(
$code_base
)
)
);
return $mapped_parameter;
}, $method->getParameterList())
);
}
}
if ($method->hasYield()) {
// There's no phpdoc standard for template types of Generators at the moment.
$new_type = UnionType::fromFullyQualifiedRealString('\\Generator');
if (!$new_type->canCastToUnionType($method->getUnionType())) {
$method->setUnionType($new_type);
}
}
// Methods defined on interfaces are always abstract, but don't have that flag set.
// NOTE: __construct is special for the following reasons:
// 1. We automatically add __construct to class-like definitions (Not sure why it's done for interfaces)
// 2. If it's abstract, then PHP would enforce that signatures are compatible
if ($this->isInterface() && !$method->isNewConstructor()) {
$method->setFlags(Flags::bitVectorWithState($method->getFlags(), \ast\flags\MODIFIER_ABSTRACT, true));
}
if ($is_override) {
$method->setIsOverride(true);
}
$code_base->addMethod($method);
}
/**
* @param bool $is_direct_invocation @phan-mandatory-param
* @return bool
* True if this class has a method with the given name
*/
public function hasMethodWithName(
CodeBase $code_base,
string $name,
bool $is_direct_invocation = false
): bool {
// All classes have a constructor even if it hasn't
// been declared yet
if (!$is_direct_invocation && ('__construct' === \strtolower($name) && !$this->isTrait())) {
return true;
}
$method_fqsen = FullyQualifiedMethodName::make(
$this->getFQSEN(),
$name
);
if ($code_base->hasMethodWithFQSEN($method_fqsen)) {
return true;
}
if (!$this->hydrateIndicatingFirstTime($code_base)) {
return false;
}
return $code_base->hasMethodWithFQSEN($method_fqsen);
}
/**
* @return Method
* The method with the given name
*
* @throws CodeBaseException if the method (or a placeholder) could not be found (or created)
*/
public function getMethodByName(
CodeBase $code_base,
string $name
): Method {
$method_fqsen = FullyQualifiedMethodName::make(
$this->getFQSEN(),
$name
);
if (!$code_base->hasMethodWithFQSEN($method_fqsen)) {
if ('__construct' === $name) {
// Create a default constructor if it's requested
// but doesn't exist yet
$default_constructor =
Method::defaultConstructorForClass(
$this,
$code_base
);
$this->addMethod($code_base, $default_constructor, $this->getParentTypeOption());
return $default_constructor;
}
throw new CodeBaseException(
$method_fqsen,
"Method with name $name does not exist for class {$this->getFQSEN()}."
);
}
return $code_base->getMethodByFQSEN($method_fqsen);
}
/**
* @return array<string,Method>
* A list of methods on this class
*/
public function getMethodMap(CodeBase $code_base): array
{
return $code_base->getMethodMapByFullyQualifiedClassName(
$this->getFQSEN()
);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return bool
* True if this class has a magic '__call' method
*/
public function hasCallMethod(CodeBase $code_base): bool
{
return $this->hasMethodWithName($code_base, '__call', true);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return bool
* True if this class has a magic '__call' method,
* and (at)phan-forbid-undeclared-magic-methods doesn't exist on this class or ancestors
*/
public function allowsCallingUndeclaredInstanceMethod(CodeBase $code_base): bool
{
return $this->hasCallMethod($code_base) &&
!$this->getForbidUndeclaredMagicMethods($code_base);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return Method
* The magic `__call` method
*/
public function getCallMethod(CodeBase $code_base): Method
{
return self::makeCallMethodCloneForCaller($this->getMethodByName($code_base, '__call'));
}
private static function makeCallMethodCloneForCaller(Method $method): Method
{
$clone = new Method(
$method->getContext(),
$method->getName(),
$method->getUnionType(),
$method->getFlags(),
$method->getFQSEN(),
[
new VariadicParameter($method->getContext(), 'args', UnionType::empty(), 0)
]
);
$clone->setPhanFlags($method->getPhanFlags());
return $clone;
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return bool
* True if this class has a magic '__callStatic' method
*/
public function hasCallStaticMethod(CodeBase $code_base): bool
{
return $this->hasMethodWithName($code_base, '__callStatic', true);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return bool
* True if this class has a magic '__callStatic' method,
* and (at)phan-forbid-undeclared-magic-methods doesn't exist on this class or ancestors.
*/
public function allowsCallingUndeclaredStaticMethod(CodeBase $code_base): bool
{
return $this->hasCallStaticMethod($code_base) &&
!$this->getForbidUndeclaredMagicMethods($code_base);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return Method
* The magic `__callStatic` method
*/
public function getCallStaticMethod(CodeBase $code_base): Method
{
return self::makeCallMethodCloneForCaller($this->getMethodByName($code_base, '__callStatic'));
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return bool
* True if this class has a magic '__get' method
*/
public function hasGetMethod(CodeBase $code_base): bool
{
return $this->hasMethodWithName($code_base, '__get', true);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return bool
* True if this class has a magic '__set' method
*/
public function hasSetMethod(CodeBase $code_base): bool
{
return $this->hasMethodWithName($code_base, '__set', true);
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return bool
* True if this class has a magic '__get' or '__set'
* method
* @suppress PhanUnreferencedPublicMethod
*/
public function hasGetOrSetMethod(CodeBase $code_base): bool
{
return (
$this->hasGetMethod($code_base)
|| $this->hasSetMethod($code_base)
);
}
public function addTraitFQSEN(FullyQualifiedClassName $fqsen, int $lineno = 0): void
{
$this->trait_fqsen_lineno[count($this->trait_fqsen_list)] = $lineno;
$this->trait_fqsen_list[] = $fqsen;
// Add the trait to the union type of this class
$this->addAdditionalType($fqsen->asType());
}
public function addTraitAdaptations(TraitAdaptations $trait_adaptations): void
{
$key = \strtolower($trait_adaptations->getTraitFQSEN()->__toString());
$old_adaptations = $this->trait_adaptations_map[$key] ?? null;
if ($old_adaptations) {
$old_adaptations->alias_methods += $trait_adaptations->alias_methods;
$old_adaptations->hidden_methods += $trait_adaptations->hidden_methods;
} else {
$this->trait_adaptations_map[$key] = $trait_adaptations;
}
}
/**
* @return list<FullyQualifiedClassName>
* A list of FQSENs for included traits
*/
public function getTraitFQSENList(): array
{
return $this->trait_fqsen_list;
}
/**
* True if this class calls its parent constructor
*/
public function isParentConstructorCalled(): bool
{
return $this->getPhanFlagsHasState(Flags::IS_PARENT_CONSTRUCTOR_CALLED);
}
public function setIsParentConstructorCalled(
bool $is_parent_constructor_called
): void {
$this->setPhanFlags(Flags::bitVectorWithState(
$this->getPhanFlags(),
Flags::IS_PARENT_CONSTRUCTOR_CALLED,
$is_parent_constructor_called
));
}
/**
* Check if this class or its ancestors forbids undeclared magic properties.
*/
public function getForbidUndeclaredMagicProperties(CodeBase $code_base): bool
{
return $this->hasFlagsRecursive($code_base, Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES);
}
/**
* Set whether undeclared magic properties are forbidden
* (properties accessed through __get or __set, with no (at)property annotation on parent class)
* @param bool $forbid_undeclared_dynamic_properties - set to true to forbid.
* @suppress PhanUnreferencedPublicMethod
*/
public function setForbidUndeclaredMagicProperties(
bool $forbid_undeclared_dynamic_properties
): void {
$this->setPhanFlags(Flags::bitVectorWithState(
$this->getPhanFlags(),
Flags::CLASS_FORBID_UNDECLARED_MAGIC_PROPERTIES,
$forbid_undeclared_dynamic_properties
));
}
/**
* Check if this class or its ancestors forbids undeclared magic methods.
*/
public function getForbidUndeclaredMagicMethods(CodeBase $code_base): bool
{
return $this->hasFlagsRecursive($code_base, Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS);
}
/**
* Set whether undeclared magic methods are forbidden
* (methods accessed through __call or __callStatic, with no (at)method annotation on class)
* @param bool $forbid_undeclared_magic_methods - set to true to forbid.
* @suppress PhanUnreferencedPublicMethod
*/
public function setForbidUndeclaredMagicMethods(
bool $forbid_undeclared_magic_methods
): void {
$this->setPhanFlags(Flags::bitVectorWithState(
$this->getPhanFlags(),
Flags::CLASS_FORBID_UNDECLARED_MAGIC_METHODS,
$forbid_undeclared_magic_methods
));
}
/**
* Returns whether this class is `(at)immutable`
*
* This will warn if instance properties of instances of the class will not change after the object is constructed.
* - Methods of (at)immutable classes may change external state (e.g. perform I/O, modify other objects)
*/
public function isImmutable(): bool
{
return $this->getPhanFlagsHasState(Flags::IS_READ_ONLY);
}
/**
* Returns whether this class is `(at)pure`
*
* This will warn if instance properties of instances of the class will not change after the object is constructed.
* - Methods of (at)immutable classes may change external state (e.g. perform I/O, modify other objects)
*/
public function isPure(): bool
{
return $this->getPhanFlagsHasState(Flags::IS_SIDE_EFFECT_FREE);
}
/**
* @return bool
* True if this class has dynamic properties. (e.g. stdClass)
*/
public function hasDynamicProperties(CodeBase $code_base): bool
{
return $this->hasFlagsRecursive($code_base, Flags::CLASS_HAS_DYNAMIC_PROPERTIES);
}
private function hasFlagsRecursive(CodeBase $code_base, int $flags): bool
{
$current = $this;
$checked = [];
while (true) {
if ($current->getPhanFlagsHasState($flags)) {
return true;
}
if (!$current->hasParentType() || !$code_base->hasClassWithFQSEN($current->getParentClassFQSEN())) {
return false;
}
$checked[$current->getFQSEN()->__toString()] = true;
$current = $current->getParentClass($code_base);
if (\array_key_exists($current->getFQSEN()->__toString(), $checked)) {
// Prevent infinite recursion.
return false;
}
}
}
public function setHasDynamicProperties(
bool $has_dynamic_properties
): void {
$this->setPhanFlags(Flags::bitVectorWithState(
$this->getPhanFlags(),
Flags::CLASS_HAS_DYNAMIC_PROPERTIES,
$has_dynamic_properties
));
}
/**
* @return bool
* True if this is a final class
*/
public function isFinal(): bool
{
return $this->getFlagsHasState(\ast\flags\CLASS_FINAL);
}
/**
* @return bool
* True if this is an abstract class
*/
public function isAbstract(): bool
{
return $this->getFlagsHasState(\ast\flags\CLASS_ABSTRACT);
}
/**
* @return bool
* True if this is an interface
*/
public function isInterface(): bool
{
return $this->getFlagsHasState(\ast\flags\CLASS_INTERFACE);
}
/**
* @return bool
* True if this is a class (i.e. neither a trait nor an interface)
*/
public function isClass(): bool
{
return ($this->getFlags() & (ast\flags\CLASS_INTERFACE | ast\flags\CLASS_TRAIT)) === 0;
}
/**
* @return bool
* True if this class is a trait
*/
public function isTrait(): bool
{
return $this->getFlagsHasState(\ast\flags\CLASS_TRAIT);
}
/**
* @return bool
* True if this class is anonymous
*/
public function isAnonymous(): bool
{
return ($this->getFlags() & \ast\flags\CLASS_ANONYMOUS) > 0;
}
/**
* @return FullyQualifiedClassName
*/
public function getFQSEN()
{
return $this->fqsen;
}
/**
* @return list<FullyQualifiedClassName>
*/
public function getNonParentAncestorFQSENList(): array
{
return \array_merge(
$this->interface_fqsen_list,
$this->trait_fqsen_list
);
}
/**
* @return list<FullyQualifiedClassName>
*/
public function getAncestorFQSENList(): array
{
$ancestor_list = $this->getNonParentAncestorFQSENList();
if ($this->hasParentType()) {
$ancestor_list[] = $this->getParentClassFQSEN();
}
return $ancestor_list;
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @param list<FullyQualifiedClassName> $fqsen_list
* A list of class FQSENs to turn into a list of
* Clazz objects
*
* @return list<Clazz>
*/
private static function getClassListFromFQSENList(
CodeBase $code_base,
array $fqsen_list
): array {
$class_list = [];
foreach ($fqsen_list as $fqsen) {
if ($code_base->hasClassWithFQSEN($fqsen)) {
$class_list[] = $code_base->getClassByFQSEN($fqsen);
}
}
return $class_list;
}
/**
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*
* @return list<Clazz>
*/
public function getAncestorClassList(CodeBase $code_base): array
{
return self::getClassListFromFQSENList(
$code_base,
$this->getAncestorFQSENList()
);
}
/**
* Add class constants from all ancestors (parents, traits, ...)
* to this class
*
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*/
public function importConstantsFromAncestorClasses(CodeBase $code_base): void
{
if (!$this->isFirstExecution(__METHOD__)) {
return;
}
foreach ($this->interface_fqsen_list as $fqsen) {
if (!$code_base->hasClassWithFQSEN($fqsen)) {
continue;
}
$ancestor = $code_base->getClassByFQSENWithoutHydrating($fqsen);
$this->importConstantsFromAncestorClass(
$code_base,
$ancestor
);
}
foreach ($this->trait_fqsen_list as $fqsen) {
if (!$code_base->hasClassWithFQSEN($fqsen)) {
continue;
}
$ancestor = $code_base->getClassByFQSENWithoutHydrating($fqsen);
$this->importConstantsFromAncestorClass(
$code_base,
$ancestor
);
}
// Copy information from the parent(s)
$this->importConstantsFromParentClass($code_base);
}
/**
* Add properties, constants and methods from all
* ancestors (parents, traits, ...) to this class
*
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*/
public function importAncestorClasses(CodeBase $code_base): void
{
if (!$this->isFirstExecution(__METHOD__)) {
return;
}
$this->importConstantsFromAncestorClasses($code_base);
foreach ($this->interface_fqsen_list as $i => $fqsen) {
if (!$code_base->hasClassWithFQSEN($fqsen)) {
continue;
}
$ancestor = $code_base->getClassByFQSEN($fqsen);
if (!$ancestor->isInterface()) {
$this->emitWrongInheritanceCategoryWarning($code_base, $ancestor, 'Interface', $this->interface_fqsen_lineno[$i] ?? 0);
}
$this->importAncestorClass(
$code_base,
$ancestor,
None::instance()
);
}
foreach ($this->trait_fqsen_list as $i => $fqsen) {
if (!$code_base->hasClassWithFQSEN($fqsen)) {
continue;
}
$ancestor = $code_base->getClassByFQSEN($fqsen);
if (!$ancestor->isTrait()) {
$this->emitWrongInheritanceCategoryWarning($code_base, $ancestor, 'Trait', $this->trait_fqsen_lineno[$i] ?? 0);
}
$this->importAncestorClass(
$code_base,
$ancestor,
None::instance()
);
}
// Copy information from the parent(s)
$this->importParentClass($code_base);
foreach ($this->mixin_types as $type) {
$this->importMixin($code_base, $type);
}
}
public function getLinenoOfAncestorReference(FullyQualifiedClassName $fqsen): int
{
$class_line = $this->getFileRef()->getLineNumberStart();
foreach ($this->interface_fqsen_list as $i => $interface_fqsen) {
if ($interface_fqsen === $fqsen) {
return $this->interface_fqsen_lineno[$i] ?? $class_line;
}
}
foreach ($this->trait_fqsen_list as $i => $trait_fqsen) {
if ($trait_fqsen === $fqsen) {
return $this->trait_fqsen_lineno[$i] ?? $class_line;
}
}
return $class_line;
}
/**
* Import all methods of the other type as magic methods.
*/
private function importMixin(CodeBase $code_base, Type $type): void
{
$fqsen = FullyQualifiedClassName::fromType($type);
if (!$code_base->hasClassWithFQSEN($fqsen) || $fqsen === $this->fqsen) {
Issue::maybeEmit(
$code_base,
$this->internal_context,
Issue::InvalidMixin,
$this->internal_context->getLineNumberStart(),
$type
);
return;
}
$class = $code_base->getClassByFQSEN($fqsen);
foreach ($class->getMethodMap($code_base) as $name => $method) {
if ($method->isMagic() || !$method->isPublic()) {
// Skip __invoke, and private/protected methods
continue;
}
if ($this->hasMethodWithName($code_base, $name, true)) {
continue;
}
// Treat it as if all of the methods were added, with their real and phpdoc union types.
$this->addMethod($code_base, $method->asPHPDocMethod($this), None::instance());
}
foreach ($class->getPropertyMap($code_base) as $name => $property) {
if (!$property->isPublic() || $property->isStatic()) {
// Skip private/protected/static properties. There's no __getStatic().
continue;
}
if ($property->isDynamicProperty()) {
continue;
}
if ($this->hasPropertyWithName($code_base, $name)) {
continue;
}
// Treat it as if all of the properties were added, with their real and phpdoc union types.
// TODO: Finalize behavior for edge cases such as `static` and templates in union types
$new_property = clone($property);
$new_property->setFQSEN(FullyQualifiedPropertyName::make($this->getFQSEN(), $name));
$new_property->setPhanFlags($new_property->getPhanFlags() | Flags::IS_FROM_PHPDOC);
$this->addProperty($code_base, $new_property, None::instance());
}
}
/**
* Add constants from the parent of this class
*
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*/
private function importConstantsFromParentClass(CodeBase $code_base): void
{
if (!$this->isFirstExecution(__METHOD__)) {
return;
}
if (!$this->hasParentType()) {
return;
}
if ($this->getParentClassFQSEN() === $this->getFQSEN()) {
return;
}
// Let the parent class finder worry about this
if (!$code_base->hasClassWithFQSEN(
$this->getParentClassFQSEN()
)) {
return;
}
// Get the parent class
$parent = $this->getParentClassWithoutHydrating($code_base);
// import constants from that class
$this->importConstantsFromAncestorClass($code_base, $parent);
}
/**
* Add properties, constants and methods from the
* parent of this class
*
* @param CodeBase $code_base
* The entire code base from which we'll find ancestor
* details
*/
private function importParentClass(CodeBase $code_base): void
{
if (!$this->isFirstExecution(__METHOD__)) {
return;
}
if (!$this->hasParentType()) {
return;
}
if ($this->getParentClassFQSEN() === $this->getFQSEN()) {
return;
}
// Let the parent class finder worry about this
if (!$code_base->hasClassWithFQSEN(
$this->getParentClassFQSEN()
)) {
return;
}
// Get the parent class
$parent = $this->getParentClass($code_base);
if (!$parent->isClass()) {
$this->emitWrongInheritanceCategoryWarning($code_base, $parent, 'Class', $this->parent_type_lineno);
}
if ($parent->isFinal()) {
$this->emitExtendsFinalClassWarning($code_base, $parent);
}
// Tell the parent to import its own parents first
// Import elements from the parent
$this->importAncestorClass(
$code_base,
$parent,
$this->getParentTypeOption()
);
}
private function emitWrongInheritanceCategoryWarning(
CodeBase $code_base,
Clazz $ancestor,
string $expected_inheritance_category,
int $lineno
): void {
$context = $this->getContext();
if ($ancestor->isPHPInternal()) {
if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessWrongInheritanceCategoryInternal)) {
Issue::maybeEmit(
$code_base,
$context,
Issue::AccessWrongInheritanceCategoryInternal,
$lineno ?: $context->getLineNumberStart(),
(string)$ancestor,
$expected_inheritance_category
);
}
} else {
if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessWrongInheritanceCategory)) {
Issue::maybeEmit(
$code_base,
$context,
Issue::AccessWrongInheritanceCategory,
$lineno ?: $context->getLineNumberStart(),
(string)$ancestor,
$ancestor->getFileRef()->getFile(),
$ancestor->getFileRef()->getLineNumberStart(),
$expected_inheritance_category
);
}
}
}
private function emitExtendsFinalClassWarning(
CodeBase $code_base,
Clazz $ancestor
): void {
$context = $this->getContext();
if ($ancestor->isPHPInternal()) {
if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessExtendsFinalClassInternal)) {
Issue::maybeEmit(
$code_base,
$context,
Issue::AccessExtendsFinalClassInternal,
$this->parent_type_lineno ?: $context->getLineNumberStart(),
(string)$ancestor->getFQSEN()
);
}
} else {
if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::AccessExtendsFinalClass)) {
Issue::maybeEmit(
$code_base,
$context,
Issue::AccessExtendsFinalClass,
$this->parent_type_lineno ?: $context->getLineNumberStart(),
(string)$ancestor->getFQSEN(),
$ancestor->getFileRef()->getFile(),
$ancestor->getFileRef()->getLineNumberStart()
);
}
}
}
/**
* Add constants from the given class to this.
*
* @param CodeBase $code_base
* A reference to the code base in which the ancestor exists
*
* @param Clazz $class
* A class to import from
*/
public function importConstantsFromAncestorClass(
CodeBase $code_base,
Clazz $class
): void {
$key = \strtolower((string)$class->getFQSEN());
if (!$this->isFirstExecution(
__METHOD__ . ':' . $key
)) {
return;
}
$class->addReference($this->getContext());
// Make sure that the class imports its parents' constants first
// (And **only** the constants)
$class->hydrateConstants($code_base);
// Copy constants
foreach ($class->getConstantMap($code_base) as $constant) {
$this->inheritConstant($code_base, $constant);
}
}
/**
* @param FileRef $file_ref
* A reference to a location in which this typed structural
* element is referenced.
* @override
*/
public function addReference(FileRef $file_ref): void
{
if (Config::get_track_references()) {
// Currently, we don't need to track references to PHP-internal methods/functions/constants
// such as PHP_VERSION, strlen(), Closure::bind(), etc.
// This may change in the future.
if ($this->isPHPInternal()) {
return;
}
if ($file_ref instanceof Context) {
if ($file_ref->getClassFQSENOrNull() === $this->fqsen) {
// Don't count references declared within MyClass as references to MyClass for dead code detection
return;
}
}
$this->reference_list[$file_ref->__toString()] = $file_ref;
}
}
/**
* Add properties, constants and methods from the given
* class to this.
*
* @param CodeBase $code_base
* A reference to the code base in which the ancestor exists
*
* @param Clazz $class
* A class to import from
*
* @param Option<Type> $type_option
* A possibly defined ancestor type used to define template
* parameter types when importing ancestor properties and
* methods
*/
public function importAncestorClass(
CodeBase $code_base,
Clazz $class,
Option $type_option
): void {
$class_fqsen = $class->getFQSEN();
$key = \strtolower($class_fqsen->__toString());
if (!$this->isFirstExecution(
__METHOD__ . ':' . $key
)) {
return;
}
$next_class_fqsen = $class_fqsen->withAlternateId($class_fqsen->getAlternateId() + 1);
if (!$this->isPHPInternal() && $code_base->hasClassWithFQSEN($next_class_fqsen)) {
$this->warnAboutAmbiguousInheritance($code_base, $class, $next_class_fqsen);
}
// Constants should have been imported earlier, but call it again just in case
$this->importConstantsFromAncestorClass($code_base, $class);
// Make sure that the class imports its parents first
// NOTE: We already imported constants from $class in importConstantsFromAncestorClass
$class->hydrate($code_base);
$is_trait = $class->isTrait();
$trait_adaptations = $is_trait ? ($this->trait_adaptations_map[$key] ?? null) : null;
// Copy properties
foreach ($class->getPropertyMap($code_base) as $property) {
if ($property->isPHPDocAbstract() && !$property->isPrivate() &&
$this->isClass() && !$this->isAbstract() && !$this->hasPropertyWithName($code_base, $property->getName())) {
Issue::maybeEmit(
$code_base,
$this->getContext(),
Issue::CommentAbstractOnInheritedProperty,
$this->getContext()->getLineNumberStart(),
$this->getFQSEN(),
$property->getRealDefiningFQSEN(),
$property->getContext()->getFile(),
$property->getContext()->getLineNumberStart(),
'@abstract'
);
}
// TODO: check for conflicts in visibility and default values for traits.
// TODO: Check for ancestor classes with the same private property?
$this->addProperty(
$code_base,
$property,
$type_option,
$is_trait
);
}
// Copy methods
foreach ($class->getMethodMap($code_base) as $method) {
if (!\is_null($trait_adaptations) && count($trait_adaptations->hidden_methods) > 0) {
$method_name_key = \strtolower($method->getName());
if (isset($trait_adaptations->hidden_methods[$method_name_key])) {
// TODO: Record that the method was hidden, and check later on that all method that were hidden were actually defined?
continue;
}
}
// Workaround: For private methods, copy the method with a new defining class.
// If you import a trait's private method, it becomes private **to the class which used the trait** in PHP code.
// (But preserving the defining FQSEN is fine for this)
if ($is_trait) {
$method = $this->adaptInheritedMethodFromTrait($method);
}
$this->addMethod(
$code_base,
$method,
$type_option
);
}
if (!\is_null($trait_adaptations)) {
$this->importTraitAdaptations($code_base, $class, $trait_adaptations, $type_option);
}
}
private function adaptInheritedMethodFromTrait(Method $method): Method
{
$method_flags = $method->getFlags();
if (Flags::bitVectorHasState($method_flags, \ast\flags\MODIFIER_PRIVATE)) {
$method = $method->createUseAlias($this, $method->getName(), \ast\flags\MODIFIER_PRIVATE);
} elseif (Flags::bitVectorHasState($method_flags, \ast\flags\MODIFIER_PROTECTED)) {
$method = $method->createUseAlias($this, $method->getName(), \ast\flags\MODIFIER_PROTECTED);
} else {
$method = $method->createUseAlias($this, $method->getName(), \ast\flags\MODIFIER_PUBLIC);
}
$context = $this->getContext()->withScope($this->internal_scope);
$method->setUnionType(
$method->getUnionTypeWithUnmodifiedStatic()->withSelfResolvedInContext($context)
);
$method->setRealReturnType(
$method->getRealReturnType()->withSelfResolvedInContext($context)
);
$parameter_list = $method->getParameterList();
$changed = false;
foreach ($parameter_list as $i => $parameter) {
$old_type = $parameter->getNonVariadicUnionType();
$type = $old_type->withSelfResolvedInContext($context);
if ($type->hasStaticType()) {
$type = $type->withType($this->getFQSEN()->asType());
}
if ($old_type !== $type) {
$changed = true;
$parameter = clone($parameter);
$parameter->setUnionType($type);
$parameter_list[$i] = $parameter;
}
}
if ($changed) {
$method->setParameterList($parameter_list);
}
$real_parameter_list = $method->getRealParameterList();
$changed = false;
foreach ($real_parameter_list as $i => $parameter) {
$old_type = $parameter->getNonVariadicUnionType();
$type = $old_type->withSelfResolvedInContext($context);
if ($type->hasStaticType()) {
$type = $type->withType($this->getFQSEN()->asType());
}
if ($old_type !== $type) {
$changed = true;
$parameter = clone($parameter);
$parameter->setUnionType($type);
$real_parameter_list[$i] = $parameter;
}
}
if ($changed) {
$method->setRealParameterList($parameter_list);
}
return $method;
}
/**
* @param CodeBase $code_base
* @param Clazz $class
* @param TraitAdaptations $trait_adaptations
* @param Option<Type> $type_option
* A possibly defined ancestor type used to define template
* parameter types when importing ancestor properties and
* methods
*/
private function importTraitAdaptations(
CodeBase $code_base,
Clazz $class,
TraitAdaptations $trait_adaptations,
Option $type_option
): void {
foreach ($trait_adaptations->alias_methods ?? [] as $alias_method_name => $original_trait_alias_source) {
$source_method_name = $original_trait_alias_source->getSourceMethodName();
if ($class->hasMethodWithName($code_base, $source_method_name, true)) {
$source_method = $class->getMethodByName($code_base, $source_method_name);
} else {
$source_method = null;
}
if (!$source_method || $source_method->isFromPHPDoc()) {
Issue::maybeEmit(
$code_base,
$this->getContext(),
Issue::UndeclaredAliasedMethodOfTrait,
$original_trait_alias_source->getAliasLineno(), // TODO: Track line number in TraitAdaptation
\sprintf('%s::%s', (string)$this->getFQSEN(), $alias_method_name),
\sprintf('%s::%s', (string)$class->getFQSEN(), $source_method_name),
$class->getName()
);
continue;
}
$alias_method = $source_method->createUseAlias(
$this,
$alias_method_name,
$original_trait_alias_source->getAliasVisibilityFlags()
);
$this->addMethod($code_base, $alias_method, $type_option);
}
}
private function warnAboutAmbiguousInheritance(
CodeBase $code_base,
Clazz $inherited_class,
FullyQualifiedClassName $alternate_class_fqsen
): void {
$alternate_class = $code_base->getClassByFQSEN($alternate_class_fqsen);
if ($inherited_class->isTrait()) {
$issue_type = Issue::RedefinedUsedTrait;
} elseif ($inherited_class->isInterface()) {
$issue_type = Issue::RedefinedInheritedInterface;
} else {
$issue_type = Issue::RedefinedExtendedClass;
}
if ($this->checkHasSuppressIssueAndIncrementCount($issue_type)) {
return;
}
$first_context = $inherited_class->getContext();
$second_context = $alternate_class->getContext();
Issue::maybeEmit(
$code_base,
$this->getContext(),
$issue_type,
$this->getContext()->getLineNumberStart(),
$this->getFQSEN(),
$inherited_class->__toString(),
$first_context->getFile(),
$first_context->getLineNumberStart(),
$second_context->getFile(),
$second_context->getLineNumberStart()
);
}
/**
* @return int
* The number of references to this typed structural element
*/
public function getReferenceCount(
CodeBase $code_base
): int {
$count = parent::getReferenceCount($code_base);
/**
* A function that maps a list of elements to the
* total reference count for all elements
* @param array<string,AddressableElement> $list
*/
$list_count = function (array $list): int {
return \array_reduce($list, function (
int $count,
ClassElement $element
): int {
foreach ($element->reference_list as $reference) {
if ($reference instanceof Context && $reference->getClassFQSENOrNull() === $this->fqsen) {
continue;
}
$count++;
}
return $count;
}, 0);
};
// Sum up counts for all dependent elements
$count += $list_count($this->getPropertyMap($code_base));
$count += $list_count($this->getMethodMap($code_base));
$count += $list_count($this->getConstantMap($code_base));
return $count;
}
/**
* @return bool
* True if this class contains generic types
*/
public function isGeneric(): bool
{
return $this->getInternalScope()->hasAnyTemplateType();
}
/**
* @return array<string,TemplateType>
* The set of all template types parameterizing this generic
* class
*/
public function getTemplateTypeMap(): array
{
return $this->getInternalScope()->getTemplateTypeMap();
}
/**
* @return string
* A string describing this class
*/
public function __toString(): string
{
$string = '';
if ($this->isFinal()) {
$string .= 'final ';
}
if ($this->isAbstract()) {
$string .= 'abstract ';
}
if ($this->isInterface()) {
$string .= 'Interface ';
} elseif ($this->isTrait()) {
$string .= 'Trait ';
} else {
$string .= 'Class ';
}
$string .= (string)$this->getFQSEN()->getCanonicalFQSEN();
return $string;
}
private function toStubSignature(CodeBase $code_base): string
{
$string = '';
if ($this->isFinal()) {
$string .= 'final ';
}
if ($this->isAbstract() && !$this->isInterface()) {
$string .= 'abstract ';
}
if ($this->isInterface()) {
$string .= 'interface ';
} elseif ($this->isTrait()) {
$string .= 'trait ';
} else {
$string .= 'class ';
}
$string .= $this->getFQSEN()->getName();
$extend_types = [];
$implements_types = [];
$parent_implements_types = [];
if ($this->parent_type) {
$extend_types[] = FullyQualifiedClassName::fromType($this->parent_type);
$parent_class = $this->getParentClass($code_base);
$parent_implements_types = $parent_class->interface_fqsen_list;
}
if (count($this->interface_fqsen_list) > 0) {
if ($this->isInterface()) {
$extend_types = \array_merge($extend_types, $this->interface_fqsen_list);
} else {
$implements_types = $this->interface_fqsen_list;
if (count($parent_implements_types) > 0) {
$implements_types = \array_diff($implements_types, $parent_implements_types);
}
}
}
if (count($extend_types) > 0) {
$string .= ' extends ' . \implode(', ', $extend_types);
}
if (count($implements_types) > 0) {
$string .= ' implements ' . \implode(', ', $implements_types);
}
return $string;
}
public function getMarkupDescription(): string
{
$fqsen = $this->getFQSEN();
$string = '';
$namespace = \ltrim($fqsen->getNamespace(), '\\');
if ($namespace !== '') {
// Render the namespace one line above the class
$string .= "namespace $namespace;\n";
}
if ($this->isFinal()) {
$string .= 'final ';
}
if ($this->isAbstract() && !$this->isInterface()) {
$string .= 'abstract ';
}
if ($this->isInterface()) {
$string .= 'interface ';
} elseif ($this->isTrait()) {
$string .= 'trait ';
} else {
$string .= 'class ';
}
if ($this->isAnonymous()) {
$string .= 'anonymous_class';
} else {
$string .= $fqsen->getName();
}
return $string;
}
/**
* @suppress PhanUnreferencedPublicMethod (toStubInfo is used by callers for more flexibility)
*/
public function toStub(CodeBase $code_base): string
{
[$namespace, $string] = $this->toStubInfo($code_base);
$namespace_text = $namespace === '' ? '' : "$namespace ";
$string = \sprintf("namespace %s{\n%s}\n", $namespace_text, $string);
return $string;
}
/** @return array{0:string,1:string} [string $namespace, string $text] */
public function toStubInfo(CodeBase $code_base): array
{
$signature = $this->toStubSignature($code_base);
$stub = $signature;
$stub .= " {";
$constant_map = $this->getConstantMap($code_base);
if (count($constant_map) > 0) {
$stub .= "\n\n // constants\n";
$stub .= \implode("\n", \array_map(static function (ClassConstant $constant): string {
return $constant->toStub();
}, $constant_map));
}
$property_map = $this->getPropertyMap($code_base);
if (count($property_map) > 0) {
$stub .= "\n\n // properties\n";
$stub .= \implode("\n", \array_map(static function (Property $property): string {
return $property->toStub();
}, $property_map));
}
$reflection_class = new \ReflectionClass((string)$this->getFQSEN());
$method_map = \array_filter($this->getMethodMap($code_base), static function (Method $method) use ($reflection_class): bool {
if ($method->getFQSEN()->isAlternate()) {
return false;
}
$reflection_method = $reflection_class->getMethod($method->getName());
if ($reflection_method->class !== $reflection_class->name) {
return false;
}
return true;
});
if (count($method_map) > 0) {
$stub .= "\n\n // methods\n";
$is_interface = $this->isInterface();
$stub .= \implode("\n", \array_map(static function (Method $method) use ($is_interface): string {
return $method->toStub($is_interface);
}, $method_map));
}
$stub .= "\n}\n\n";
$namespace = \ltrim($this->getFQSEN()->getNamespace(), '\\');
return [$namespace, $stub];
}
protected function hydrateConstantsOnce(CodeBase $code_base): void
{
foreach ($this->getAncestorFQSENList() as $fqsen) {
if ($code_base->hasClassWithFQSEN($fqsen)) {
$code_base->getClassByFQSENWithoutHydrating(
$fqsen
)->hydrateConstants($code_base);
}
}
// Create the 'class' constant
$class_constant_value = \ltrim($this->getFQSEN()->__toString(), '\\');
$class_constant = new ClassConstant(
$this->getContext(),
'class',
LiteralStringType::instanceForValue(
$class_constant_value,
false
)->asRealUnionType(),
0,
FullyQualifiedClassConstantName::make(
$this->getFQSEN(),
'class'
)
);
$class_constant->setNodeForValue($class_constant_value);
$this->addConstant($code_base, $class_constant);
// Add variable '$this' to the scope
$this->getInternalScope()->addVariable(
new Variable(
$this->getContext(),
'this',
StaticType::instance(false)->asRealUnionType(),
0
)
);
// Fetch the constants declared within the class, to check if they have override annotations later.
$original_declared_class_constants = $this->getConstantMap($code_base);
// Load parent methods, properties, constants
$this->importConstantsFromAncestorClasses($code_base);
self::analyzeClassConstantOverrides($code_base, $original_declared_class_constants);
}
/**
* This method must be called before analysis
* begins.
*/
protected function hydrateOnce(CodeBase $code_base): void
{
// Ensure that we hydrate constants before hydrating properties and methods
$this->hydrateConstants($code_base);
foreach ($this->getAncestorFQSENList() as $fqsen) {
if ($code_base->hasClassWithFQSEN($fqsen)) {
$code_base->getClassByFQSENWithoutHydrating(
$fqsen
)->hydrate($code_base);
}
}
// Fetch the properties declared within the class, to check if they have override annotations later.
$original_declared_properties = $this->getPropertyMap($code_base);
$this->importAncestorClasses($code_base);
self::analyzePropertyOverrides($code_base, $original_declared_properties);
// Make sure there are no abstract methods on non-abstract classes
AbstractMethodAnalyzer::analyzeAbstractMethodsAreImplemented(
$code_base,
$this
);
}
/**
* @param ClassConstant[] $original_declared_class_constants
*/
private static function analyzeClassConstantOverrides(CodeBase $code_base, array $original_declared_class_constants): void
{
foreach ($original_declared_class_constants as $constant) {
if ($constant->isOverrideIntended() && !$constant->isOverride()) {
if ($constant->checkHasSuppressIssueAndIncrementCount(Issue::CommentOverrideOnNonOverrideConstant)) {
continue;
}
$context = $constant->getContext();
Issue::maybeEmit(
$code_base,
$context,
Issue::CommentOverrideOnNonOverrideConstant,
$context->getLineNumberStart(),
(string)$constant->getFQSEN()
);
}
}
}
/**
* @param array<string, Property> $original_declared_properties
*/
private static function analyzePropertyOverrides(CodeBase $code_base, array $original_declared_properties): void
{
foreach ($original_declared_properties as $property) {
if ($property->isOverrideIntended() && !$property->isOverride()) {
if ($property->checkHasSuppressIssueAndIncrementCount(Issue::CommentOverrideOnNonOverrideProperty)) {
continue;
}
$context = $property->getContext();
Issue::maybeEmit(
$code_base,
$context,
Issue::CommentOverrideOnNonOverrideProperty,
$context->getLineNumberStart(),
(string)$property->getFQSEN()
);
}
}
}
/**
* This method should be called after hydration
* @throws RecursionDepthException for deep class hierarchies
*/
final public function analyze(CodeBase $code_base): void
{
if ($this->isPHPInternal()) {
return;
}
// Make sure the parent classes exist
ClassInheritanceAnalyzer::analyzeClassInheritance(
$code_base,
$this
);
DuplicateClassAnalyzer::analyzeDuplicateClass(
$code_base,
$this
);
ParentConstructorCalledAnalyzer::analyzeParentConstructorCalled(
$code_base,
$this
);
PropertyTypesAnalyzer::analyzePropertyTypes(
$code_base,
$this
);
ClassConstantTypesAnalyzer::analyzeClassConstantTypes(
$code_base,
$this
);
// Analyze this class to make sure that we don't have conflicting
// types between similar inherited methods.
CompositionAnalyzer::analyzeComposition(
$code_base,
$this
);
$this->analyzeInheritedMethods($code_base);
// Let any configured plugins analyze the class
ConfigPluginSet::instance()->analyzeClass(
$code_base,
$this
);
}
private function analyzeInheritedMethods(CodeBase $code_base): void
{
if ($this->isClass() && !$this->isAbstract()) {
foreach ($this->getMethodMap($code_base) as $method) {
if ($method->getRealDefiningFQSEN() === $method->getFQSEN()) {
continue;
}
if ($method->isPHPDocAbstract() && !$method->isPrivate()) {
Issue::maybeEmit(
$code_base,
$this->getContext(),
Issue::CommentAbstractOnInheritedMethod,
$this->getContext()->getLineNumberStart(),
$this->getFQSEN(),
$method->getRealDefiningFQSEN(),
$method->getContext()->getFile(),
$method->getContext()->getLineNumberStart(),
'@abstract'
);
}
}
}
}
public function setDidFinishParsing(bool $did_finish_parsing): void
{
$this->did_finish_parsing = $did_finish_parsing;
}
/**
* @var bool have the class constants been hydrated
* (must be done before hydrating properties and methods to avoid recursive dependencies)
*/
protected $are_constants_hydrated;
/**
* This method must be called before analysis
* begins. It hydrates constants, but not properties/methods.
*/
protected function hydrateConstants(CodeBase $code_base): void
{
if (!$this->did_finish_parsing) {
return;
}
if ($this->are_constants_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently.
return;
}
if (!$code_base->shouldHydrateRequestedElements()) {
return;
}
$this->are_constants_hydrated = true;
$this->hydrateConstantsOnce($code_base);
}
/**
* This method must be called before analysis begins.
* This is identical to hydrateConstants(),
* but returns true only if this is the first time the element was hydrated.
* (i.e. true if there may be newly added constants)
*/
public function hydrateConstantsIndicatingFirstTime(CodeBase $code_base): bool
{
if (!$this->did_finish_parsing) { // Is **this** class fully parsed
return false;
}
if ($this->are_constants_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently.
return false;
}
if (!$code_base->shouldHydrateRequestedElements()) {
return false;
}
$this->are_constants_hydrated = true;
$this->hydrateConstantsOnce($code_base);
return true;
}
/**
* This method must be called before analysis
* begins.
* @override
*/
public function hydrate(CodeBase $code_base): void
{
if (!$this->did_finish_parsing) {
return;
}
if ($this->is_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently.
return;
}
if (!$code_base->shouldHydrateRequestedElements()) {
return;
}
$this->is_hydrated = true;
$this->hydrateOnce($code_base);
}
/**
* This method must be called before analysis begins.
* This is identical to hydrate(), but returns true only if this is the first time the element was hydrated.
* @internal
*/
public function hydrateIndicatingFirstTime(CodeBase $code_base): bool
{
if (!$this->did_finish_parsing) {
return false;
}
if ($this->is_hydrated) { // Same as isFirstExecution(), inlined due to being called frequently.
return false;
}
if (!$code_base->shouldHydrateRequestedElements()) {
return false;
}
$this->is_hydrated = true;
$this->hydrateOnce($code_base);
return true;
}
/**
* Used by daemon mode to restore an element to the state it had before parsing.
*/
public function createRestoreCallback(): Closure
{
// NOTE: Properties, Methods, and closures are restored separately.
$original_this = clone($this);
$original_union_type = $this->getUnionType();
return function () use ($original_union_type, $original_this): void {
// @phan-suppress-next-line PhanTypeSuspiciousNonTraversableForeach this is intentionally iterating over private properties of the clone.
foreach ($original_this as $key => $value) {
$this->{$key} = $value;
}
$this->setUnionType($original_union_type);
$this->memoizeFlushAll();
};
}
public function addAdditionalType(Type $type): void
{
$this->additional_union_types = ($this->additional_union_types ?? UnionType::empty())->withType($type);
}
public function getAdditionalTypes(): ?UnionType
{
return $this->additional_union_types;
}
/**
* @param array<string,UnionType> $template_parameter_type_map
*/
public function resolveParentTemplateType(array $template_parameter_type_map): UnionType
{
if (\count($template_parameter_type_map) === 0) {
return UnionType::empty();
}
if ($this->parent_type === null) {
return UnionType::empty();
}
if (!$this->parent_type->hasTemplateParameterTypes()) {
return UnionType::empty();
}
$parent_template_parameter_type_list = $this->parent_type->getTemplateParameterTypeList();
$changed = false;
foreach ($parent_template_parameter_type_list as $i => $template_type) {
$new_template_type = $template_type->withTemplateParameterTypeMap($template_parameter_type_map);
if ($template_type === $new_template_type) {
continue;
}
$parent_template_parameter_type_list[$i] = $new_template_type;
$changed = true;
}
if (!$changed) {
return UnionType::empty();
}
return Type::fromType($this->parent_type, $parent_template_parameter_type_list)->asPHPDocUnionType();
}
/**
* @return array<string,Property>
*/
public function getPropertyMapExcludingDynamicAndMagicProperties(CodeBase $code_base): array
{
return $this->memoize(__METHOD__, /** @return array<string,Property> */ function () use ($code_base): array {
// TODO: This won't work if a class declares both a real property and a magic property of the same name.
// Low priority because that is uncommon
return \array_filter(
$this->getPropertyMap($code_base),
static function (Property $property): bool {
return !$property->isDynamicOrFromPHPDoc();
}
);
});
}
public const CAN_ITERATE_STATUS_NO_PROPERTIES = 0;
public const CAN_ITERATE_STATUS_NO_ACCESSIBLE_PROPERTIES = 1;
public const CAN_ITERATE_STATUS_HAS_ACCESSIBLE_PROPERTIES = 2;
/**
* Returns an enum value (self::CAN_ITERATE_STATUS_*) indicating whether
* analyzed code iterating over an instance of this class has potential bugs.
* (and what type of bug it would be)
*/
public function checkCanIterateFromContext(
CodeBase $code_base,
Context $context
): int {
$accessing_class = $context->getClassFQSENOrNull();
return $this->memoize(
'can_iterate:' . (string)$accessing_class,
function () use ($accessing_class, $code_base): int {
$properties = $this->getPropertyMapExcludingDynamicAndMagicProperties($code_base);
foreach ($properties as $property) {
if ($property->isAccessibleFromClass($code_base, $accessing_class)) {
return self::CAN_ITERATE_STATUS_HAS_ACCESSIBLE_PROPERTIES;
}
}
if (count($properties) > 0) {
return self::CAN_ITERATE_STATUS_NO_ACCESSIBLE_PROPERTIES;
}
return self::CAN_ITERATE_STATUS_NO_PROPERTIES;
}
);
}
/**
* @return list<Closure(list<mixed>, Context):UnionType>
*/
public function getGenericConstructorBuilder(CodeBase $code_base): array
{
return $this->memoize(
'template_type_resolvers',
/**
* @return list<Closure(list<mixed>):UnionType>
*/
function () use ($code_base): array {
// Get the constructor so that we can figure out what
// template types we're going to be mapping
$constructor_method =
$this->getMethodByName($code_base, '__construct');
$template_type_resolvers = [];
foreach ($this->getTemplateTypeMap() as $template_type) {
$template_type_resolver = $constructor_method->getTemplateTypeExtractorClosure(
$code_base,
$template_type
);
if (!$template_type_resolver) {
// PhanTemplateTypeNotDeclaredInFunctionParams can be suppressed both on the class and on __construct()
if (!$this->checkHasSuppressIssueAndIncrementCount(Issue::TemplateTypeNotDeclaredInFunctionParams)) {
Issue::maybeEmit(
$code_base,
$constructor_method->getContext(),
Issue::GenericConstructorTypes,
$constructor_method->getContext()->getLineNumberStart(),
$template_type,
$this->getFQSEN()
);
}
/** @param list<\ast\Node|mixed> $unused_arg_list */
$template_type_resolver = static function (array $unused_arg_list): UnionType {
return MixedType::instance(false)->asPHPDocUnionType();
};
}
$template_type_resolvers[] = $template_type_resolver;
}
return $template_type_resolvers;
}
);
}
/**
* Given the FQSEN of an ancestor class and an element definition,
* return the overridden element's definition or null if this didn't override anything.
*
* TODO: Handle renamed elements from traits.
*
* @return ?ClassElement if non-null, this is of the same type as $element
*/
public static function getAncestorElement(CodeBase $code_base, FullyQualifiedClassName $ancestor_fqsen, ClassElement $element): ?ClassElement
{
if (!$code_base->hasClassWithFQSEN($ancestor_fqsen)) {
return null;
}
$ancestor_class = $code_base->getClassByFQSEN($ancestor_fqsen);
$name = $element->getName();
if ($element instanceof Method) {
if (!$ancestor_class->hasMethodWithName($code_base, $name, true)) {
return null;
}
return $ancestor_class->getMethodByName($code_base, $name);
} elseif ($element instanceof ClassConstant) {
if (!$ancestor_class->hasConstantWithName($code_base, $name)) {
return null;
}
$constant_fqsen = FullyQualifiedClassConstantName::make(
$ancestor_fqsen,
$name
);
return $code_base->getClassConstantByFQSEN($constant_fqsen);
} elseif ($element instanceof Property) {
if (!$ancestor_class->hasPropertyWithName($code_base, $name)) {
return null;
}
return $ancestor_class->getPropertyByName($code_base, $name);
}
return null;
}
private static function markMethodAsOverridden(CodeBase $code_base, FullyQualifiedMethodName $method_fqsen): void
{
if (!$code_base->hasMethodWithFQSEN($method_fqsen)) {
return;
}
$method = $code_base->getMethodByFQSEN($method_fqsen);
$method->setIsOverriddenByAnother(true);
}
/**
* Sets the declaration id of the node containing this user-defined class
*/
public function setDeclId(int $id): void
{
$this->decl_id = $id;
}
/**
* Gets the declaration id of the node containing this user-defined class.
* Returns 0 for internal classes.
*/
public function getDeclId(): int
{
return $this->decl_id;
}
/**
* Returns a context with the internal scope of this class (including suppression info)
* Equivalent to $clazz->getContext()->withScope($clazz->getInternalScope())
*
* TODO: Use this for more issues about class and class-like declarations.
*/
public function getInternalContext(): Context
{
return $this->internal_context;
}
}