src/Phan/Language/Element/Method.php
<?php
declare(strict_types=1);
namespace Phan\Language\Element;
// Note: This file uses both class Phan\Language\Element\Flags and namespace ast\flags
use ast;
use ast\Node;
use Phan\Analysis\Analyzable;
use Phan\AST\UnionTypeVisitor;
use Phan\CodeBase;
use Phan\Config;
use Phan\Language\Context;
use Phan\Language\ElementContext;
use Phan\Language\FileRef;
use Phan\Language\FQSEN\FullyQualifiedMethodName;
use Phan\Language\Scope\FunctionLikeScope;
use Phan\Language\Type\GenericArrayType;
use Phan\Language\UnionType;
use Phan\Memoize;
/**
* Phan's representation of a class's method.
*
* @phan-file-suppress PhanPartialTypeMismatchArgument
* @method FullyQualifiedMethodName getDefiningFQSEN() @phan-suppress-current-line PhanParamSignaturePHPDocMismatchReturnType
*/
class Method extends ClassElement implements FunctionInterface
{
use Analyzable;
use Memoize;
use FunctionTrait;
use ClosedScopeElement;
/**
* @var ?FullyQualifiedMethodName If this was originally defined in a trait, this is the trait's defining fqsen.
* This is tracked separately from getDefiningFQSEN() in order to not break access checks on protected/private methods.
* Used for dead code detection.
*/
private $real_defining_fqsen;
/**
* @var ?Method The defining method, if this method was inherited.
* This is only set if this is needed to recursively infer method types - do not use this.
*
* This may become out of date in language server mode.
*/
private $defining_method_for_type_fetching;
/**
* @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 FullyQualifiedMethodName $fqsen
* A fully qualified name for the element
*
* @param ?list<Parameter> $parameter_list
* A list of parameters to set on this method
*/
public function __construct(
Context $context,
string $name,
UnionType $type,
int $flags,
FullyQualifiedMethodName $fqsen,
$parameter_list
) {
$internal_scope = new FunctionLikeScope(
$context->getScope(),
$fqsen
);
$context = $context->withScope($internal_scope);
if ($type->hasTemplateType()) {
$this->recordHasTemplateType();
}
parent::__construct(
$context,
FullyQualifiedMethodName::canonicalName($name),
$type,
$flags,
$fqsen
);
// Presume that this is the original definition
// of this method, and let it be overwritten
// if it isn't.
$this->setDefiningFQSEN($fqsen);
$this->real_defining_fqsen = $fqsen;
// Record the FQSEN of this method (With the current Clazz),
// to prevent recursing from a method into itself in non-quick mode.
$this->setInternalScope($internal_scope);
if ($parameter_list !== null) {
$this->setParameterList($parameter_list);
}
$this->checkForTemplateTypes();
}
public function __clone()
{
$this->setInternalScope(clone($this->getInternalScope()));
}
/**
* Sets hasTemplateType to true if it finds any template types in the parameters or methods
*/
public function checkForTemplateTypes(): void
{
if ($this->getUnionType()->hasTemplateTypeRecursive()) {
$this->recordHasTemplateType();
return;
}
foreach ($this->parameter_list as $parameter) {
if ($parameter->getUnionType()->hasTemplateTypeRecursive()) {
$this->recordHasTemplateType();
return;
}
}
}
/**
* @return bool
* True if this is a magic phpdoc method (declared via (at)method on class declaration phpdoc)
*/
public function isFromPHPDoc(): bool
{
return $this->getPhanFlagsHasState(Flags::IS_FROM_PHPDOC);
}
/**
* Sets whether this is a magic phpdoc method (declared via (at)method on class declaration phpdoc)
* @param bool $from_phpdoc - True if this is a magic phpdoc method
*/
public function setIsFromPHPDoc(bool $from_phpdoc): void
{
$this->setPhanFlags(
Flags::bitVectorWithState(
$this->getPhanFlags(),
Flags::IS_FROM_PHPDOC,
$from_phpdoc
)
);
}
/**
* Returns true if this element is overridden by at least one other element
*/
public function isOverriddenByAnother(): bool
{
return $this->getPhanFlagsHasState(Flags::IS_OVERRIDDEN_BY_ANOTHER);
}
/**
* Sets whether this method is overridden by another method
*
* @param bool $is_overridden_by_another
* True if this method is overridden by another method
*/
public function setIsOverriddenByAnother(bool $is_overridden_by_another): void
{
$this->setPhanFlags(Flags::bitVectorWithState(
$this->getPhanFlags(),
Flags::IS_OVERRIDDEN_BY_ANOTHER,
$is_overridden_by_another
));
}
/**
* @return bool
* True if this is an abstract method
*/
public function isAbstract(): bool
{
return $this->getFlagsHasState(ast\flags\MODIFIER_ABSTRACT);
}
/**
* @return bool
* True if this is a final method
*/
public function isFinal(): bool
{
return $this->getFlagsHasState(ast\flags\MODIFIER_FINAL);
}
/**
* @return bool
* True if this should be analyzed as if it is a final method
*/
public function isEffectivelyFinal(): bool
{
if ($this->isFinal()) {
return true;
}
return Config::getValue('assume_no_external_class_overrides')
&& !$this->isOverriddenByAnother()
&& !$this->isAbstract();
}
/**
* @return bool
* True if this method returns a reference
*/
public function returnsRef(): bool
{
return $this->getFlagsHasState(ast\flags\FUNC_RETURNS_REF);
}
/**
* Returns true if this is a magic method
* (Names are all normalized in FullyQualifiedMethodName::make())
*/
public function isMagic(): bool
{
return \array_key_exists($this->name, FullyQualifiedMethodName::MAGIC_METHOD_NAME_SET);
}
/**
* Returns the return union type of this magic method, if known.
*/
public function getUnionTypeOfMagicIfKnown(): ?UnionType
{
$type_string = FullyQualifiedMethodName::MAGIC_METHOD_TYPE_MAP[$this->name] ?? null;
return $type_string ? UnionType::fromFullyQualifiedPHPDocString($type_string) : null;
}
/**
* Returns true if this is a magic method which should have return type of void
* (Names are all normalized in FullyQualifiedMethodName::make())
*/
public function isMagicAndVoid(): bool
{
return \array_key_exists($this->name, FullyQualifiedMethodName::MAGIC_VOID_METHOD_NAME_SET);
}
/**
* Returns true if this is the `__construct` method
* (Does not return true for php4 constructors)
*/
public function isNewConstructor(): bool
{
// NOTE: This is normalized to lowercase by canonicalName
return $this->name === '__construct';
}
/**
* Returns true if this is a placeholder for the `__construct` method that was never declared
*/
public function isFakeConstructor(): bool
{
return ($this->getPhanFlags() & Flags::IS_FAKE_CONSTRUCTOR) !== 0;
}
/**
* Returns true if this is the magic `__call` method
*/
public function isMagicCall(): bool
{
// NOTE: This is normalized to lowercase by canonicalName
return $this->name === '__call';
}
/**
* Returns true if this is the magic `__callStatic` method
*/
public function isMagicCallStatic(): bool
{
// NOTE: This is normalized to lowercase by canonicalName
return $this->name === '__callStatic';
}
/**
* @return Method
* A default constructor for the given class
*/
public static function defaultConstructorForClass(
Clazz $clazz,
CodeBase $code_base
): Method {
if ($clazz->getFQSEN()->getNamespace() === '\\' && $clazz->hasMethodWithName($code_base, $clazz->getName(), true)) {
$old_style_constructor = $clazz->getMethodByName($code_base, $clazz->getName());
} else {
$old_style_constructor = null;
}
$method_fqsen = FullyQualifiedMethodName::make(
$clazz->getFQSEN(),
'__construct'
);
$method = new Method(
$old_style_constructor ? $old_style_constructor->getContext() : $clazz->getContext(),
'__construct',
$clazz->getUnionType(),
0,
$method_fqsen,
$old_style_constructor ? $old_style_constructor->getParameterList() : null
);
if ($old_style_constructor) {
$method->setRealParameterList($old_style_constructor->getRealParameterList());
$method->setNumberOfRequiredParameters($old_style_constructor->getNumberOfRequiredParameters());
$method->setNumberOfOptionalParameters($old_style_constructor->getNumberOfOptionalParameters());
$method->setRealReturnType($old_style_constructor->getRealReturnType());
$method->setUnionType($old_style_constructor->getUnionType());
}
$method->setPhanFlags($method->getPhanFlags() | Flags::IS_FAKE_CONSTRUCTOR);
return $method;
}
/**
* Convert this method to a method from phpdoc.
* Used when importing methods with mixins.
*
* Precondition: This is not a magic method
*/
public function asPHPDocMethod(Clazz $class): Method
{
$method = clone($this);
$method->setFlags($method->getFlags() & (
ast\flags\MODIFIER_PUBLIC |
ast\flags\MODIFIER_PROTECTED |
ast\flags\MODIFIER_PRIVATE |
ast\flags\MODIFIER_STATIC
)); // clear MODIFIER_ABSTRACT and other flags
$method->setPhanFlags(
($method->getPhanFlags() | Flags::IS_FROM_PHPDOC) & ~(Flags::IS_OVERRIDDEN_BY_ANOTHER | Flags::IS_OVERRIDE)
);
// TODO: Handle template. Possibly support @mixin Foo<stdClass, bool> and resolve methods.
// $method->setPhanFlags(Flags::IS_FROM_PHPDOC);
$method->clearNode();
// Set the new FQSEN but keep the defining FQSEN
$method->setFQSEN(FullyQualifiedMethodName::make($class->getFQSEN(), $method->getName()));
return $method;
}
/**
* @param Clazz $clazz - The class to treat as the defining class of the alias. (i.e. the inheriting class)
* @param string $alias_method_name - The alias method name.
* @param int $new_visibility_flags (0 if unchanged)
* @return Method
*
* An alias from a trait use, which is treated as though it was defined in $clazz
* E.g. if you import a trait's method as private/protected, it becomes private/protected **to the class which used the trait**
*
* The resulting alias doesn't inherit the Node of the method body, so aliases won't have a redundant analysis step.
*/
public function createUseAlias(
Clazz $clazz,
string $alias_method_name,
int $new_visibility_flags
): Method {
$method_fqsen = FullyQualifiedMethodName::make(
$clazz->getFQSEN(),
$alias_method_name
);
$method = new Method(
$this->getContext(),
$alias_method_name,
$this->getUnionTypeWithUnmodifiedStatic(),
$this->getFlags(),
$method_fqsen,
$this->getParameterList()
);
$method->setPhanFlags($this->getPhanFlags() & ~(Flags::IS_OVERRIDE | Flags::IS_OVERRIDDEN_BY_ANOTHER));
switch ($new_visibility_flags) {
case ast\flags\MODIFIER_PUBLIC:
case ast\flags\MODIFIER_PROTECTED:
case ast\flags\MODIFIER_PRIVATE:
// Replace the visibility with the new visibility.
$method->setFlags(Flags::bitVectorWithState(
Flags::bitVectorWithState(
$method->getFlags(),
ast\flags\MODIFIER_PUBLIC | ast\flags\MODIFIER_PROTECTED | ast\flags\MODIFIER_PRIVATE,
false
),
$new_visibility_flags,
true
));
break;
default:
break;
}
$defining_fqsen = $this->getDefiningFQSEN();
if ($method->isPublic()) {
$method->setDefiningFQSEN($defining_fqsen);
}
$method->real_defining_fqsen = $defining_fqsen;
$method->setRealParameterList($this->getRealParameterList());
$method->setRealReturnType($this->getRealReturnType());
$method->setNumberOfRequiredParameters($this->getNumberOfRequiredParameters());
$method->setNumberOfOptionalParameters($this->getNumberOfOptionalParameters());
// Copy the comment so that features such as templates will work
$method->comment = $this->comment;
return $method;
}
/**
* These magic **instance** methods don't inherit pureness from the class in question
*/
private const NON_PURE_METHOD_NAME_SET = [
'__clone' => true,
'__construct' => true,
'__destruct' => true,
'__set' => true, // This could exist in a pure class to throw exceptions or do nothing
'__unserialize' => true,
'__unset' => true, // This could exist in a pure class to throw exceptions or do nothing
'__wakeup' => true,
];
/**
* @param Context $context
* The context in which the node appears
*
* @param CodeBase $code_base
*
* @param Node $node
* An AST node representing a method
*
* @param ?Clazz $class
* This will be mandatory in a future Phan release
*
* @return Method
* A Method representing the AST node in the
* given context
*/
public static function fromNode(
Context $context,
CodeBase $code_base,
Node $node,
FullyQualifiedMethodName $fqsen,
?Clazz $class = null
): Method {
// Create the skeleton method object from what
// we know so far
$method = new Method(
$context,
(string)$node->children['name'],
UnionType::empty(),
$node->flags,
$fqsen,
null
);
$doc_comment = $node->children['docComment'] ?? '';
$method->setDocComment($doc_comment);
// Parse the comment above the method to get
// extra meta information about the method.
$comment = Comment::fromStringInContext(
$doc_comment,
$code_base,
$context,
$node->lineno,
Comment::ON_METHOD
);
// Defer adding params to the local scope for user functions. (FunctionTrait::addParamsToScopeOfFunctionOrMethod)
// See PostOrderAnalysisVisitor->analyzeCallToMethod
$method->setComment($comment);
// Record @internal, @deprecated, and @phan-pure
$method->setPhanFlags($method->getPhanFlags() | $comment->getPhanFlagsForMethod());
$element_context = new ElementContext($method);
// @var list<Parameter>
// The list of parameters specified on the
// method
$parameter_list = Parameter::listFromNode(
$element_context,
$code_base,
$node->children['params']
);
$method->setParameterList($parameter_list);
foreach ($parameter_list as $parameter) {
if ($parameter->getUnionType()->hasTemplateTypeRecursive()) {
$method->recordHasTemplateType();
break;
}
}
// Add each parameter to the scope of the function
// NOTE: it's important to clone this,
// because we don't want any assignments to modify the original Parameter
foreach ($parameter_list as $parameter) {
$method->getInternalScope()->addVariable(
$parameter->cloneAsNonVariadic()
);
}
foreach ($comment->getTemplateTypeList() as $template_type) {
$method->getInternalScope()->addTemplateType($template_type);
}
if (!$method->isPHPInternal()) {
// If the method is Analyzable, set the node so that
// we can come back to it whenever we like and
// rescan it
$method->setNode($node);
}
// Keep an copy of the original parameter list, to check for fatal errors later on.
$method->setRealParameterList($parameter_list);
$required_parameter_count = self::computeNumberOfRequiredParametersForList($parameter_list);
$method->setNumberOfRequiredParameters($required_parameter_count);
$method->setNumberOfOptionalParameters(\count($parameter_list) - $required_parameter_count);
// Check to see if the comment specifies that the
// method is deprecated
$method->setIsDeprecated($comment->isDeprecated());
// Set whether or not the method is internal to
// the namespace.
$method->setIsNSInternal($comment->isNSInternal());
// Set whether or not the comment indicates that the method is intended
// to override another method.
$method->setIsOverrideIntended($comment->isOverrideIntended());
$method->setSuppressIssueSet($comment->getSuppressIssueSet());
$class = $class ?? $context->getClassInScope($code_base);
if ($method->isMagicCall() || $method->isMagicCallStatic()) {
$method->setNumberOfOptionalParameters(FunctionInterface::INFINITE_PARAMETERS);
$method->setNumberOfRequiredParameters(0);
}
if ($class->isPure() && !$method->isStatic() &&
!\array_key_exists(\strtolower($method->getName()), self::NON_PURE_METHOD_NAME_SET)) {
$method->setIsPure();
}
$is_trait = $class->isTrait();
// Add the syntax-level return type to the method's union type
// if it exists
$return_type = $node->children['returnType'];
if ($return_type instanceof Node) {
// TODO: Avoid resolving this, but only in traits
$return_union_type = (new UnionTypeVisitor($code_base, $context))->fromTypeInSignature(
$return_type
);
$method->setUnionType($method->getUnionType()->withUnionType($return_union_type)->withRealTypeSet($return_union_type->getTypeSet()));
// TODO: Replace 'self' with the real class when not in a trait
} else {
$return_union_type = UnionType::empty();
}
// TODO: Deprecate the setRealReturnType API due to properly tracking real return type?
$method->setRealReturnType($return_union_type);
// If available, add in the doc-block annotated return type
// for the method.
if ($comment->hasReturnUnionType()) {
$comment_return_union_type = $comment->getReturnType();
if (!$is_trait) {
$comment_return_union_type = $comment_return_union_type->withSelfResolvedInContext($context);
}
$signature_union_type = $method->getUnionType();
$new_type = self::computeNewTypeForComment($code_base, $context, $signature_union_type, $comment_return_union_type);
$method->setUnionType($new_type);
$method->setPHPDocReturnType($comment_return_union_type);
}
$element_context->freeElementReference();
// Populate the original return type.
$method->setOriginalReturnType();
return $method;
}
private static function computeNewTypeForComment(CodeBase $code_base, Context $context, UnionType $signature_union_type, UnionType $comment_return_union_type): UnionType
{
$new_type = $comment_return_union_type;
foreach ($comment_return_union_type->getTypeSet() as $type) {
if (!$type->asPHPDocUnionType()->canAnyTypeStrictCastToUnionType($code_base, $signature_union_type)) {
// Allow `@return static` to override a real type of MyClass.
// php8 may add a real type of static.
$resolved_type = $type->withStaticResolvedInContext($context);
if ($resolved_type === $type || !$resolved_type->asPHPDocUnionType()->canAnyTypeStrictCastToUnionType($code_base, $signature_union_type)) {
$new_type = $new_type->withoutType($type);
}
}
}
if ($new_type !== $comment_return_union_type) {
$new_type = $signature_union_type->withUnionType($new_type)->withRealTypeSet($signature_union_type->getRealTypeSet());
if ($comment_return_union_type->hasRealTypeSet() && !$new_type->hasRealTypeSet()) {
$new_type = $new_type->withRealTypeSet($comment_return_union_type->getRealTypeSet());
}
return $new_type;
}
if ($comment_return_union_type->hasRealTypeSet() && !$signature_union_type->hasRealTypeSet()) {
return $comment_return_union_type;
}
return $comment_return_union_type->withRealTypeSet($signature_union_type->getRealTypeSet());
}
/**
* Ensure that this clone will use the return type of the ancestor method
*/
public function ensureClonesReturnType(Method $original_method): void
{
if ($this->defining_method_for_type_fetching) {
return;
}
// Get the real ancestor of C::method() if C extends B and B extends A
$original_method = $original_method->defining_method_for_type_fetching ?? $original_method;
// Don't bother with methods that can't have types inferred recursively
if ($original_method->isAbstract() || $original_method->isFromPHPDoc() || $original_method->isPHPInternal()) {
return;
}
if (!$original_method->getUnionType()->isEmpty() || !$original_method->getRealReturnType()->isEmpty()) {
// This heuristic is used as little as possible.
// It will only use this fallback of directly using the (possibly modified)
// parent's type if the parent method declaration had no phpdoc return type and no real return type (and nothing was guessed such as `void`).
return;
}
$this->defining_method_for_type_fetching = $original_method;
}
public function setUnionType(UnionType $union_type): void
{
$this->defining_method_for_type_fetching = null;
parent::setUnionType($union_type);
}
protected function getUnionTypeWithStatic(): UnionType
{
return parent::getUnionType();
}
/**
* @return UnionType
* The return type of this method in its given context.
*/
public function getUnionType(): UnionType
{
if ($this->defining_method_for_type_fetching) {
$union_type = $this->defining_method_for_type_fetching->getUnionTypeWithStatic();
} else {
$union_type = parent::getUnionType();
}
// If the type is 'static', add this context's class
// to the return type
if ($union_type->hasStaticType()) {
$union_type = $union_type->withType(
$this->getFQSEN()->getFullyQualifiedClassName()->asType()
);
}
// If the type is a generic array of 'static', add
// a generic array of this context's class to the return type
if ($union_type->genericArrayElementTypes()->hasStaticType()) {
// TODO: Base this on the static array type...
$key_type_enum = GenericArrayType::keyTypeFromUnionTypeKeys($union_type);
$union_type = $union_type->withType(
$this->getFQSEN()->getFullyQualifiedClassName()->asType()->asGenericArrayType($key_type_enum)
);
}
return $union_type;
}
public function getUnionTypeWithUnmodifiedStatic(): UnionType
{
return parent::getUnionType();
}
public function getFQSEN(): FullyQualifiedMethodName
{
return $this->fqsen;
}
/**
* @return \Generator
* @phan-return \Generator<Method>
* The set of all alternates to this method
* @suppress PhanParamSignatureMismatch
*/
public function alternateGenerator(CodeBase $code_base): \Generator
{
// Workaround so that methods of generic classes will have the resolved template types
yield $this;
$fqsen = $this->getFQSEN();
$alternate_id = $fqsen->getAlternateId() + 1;
$fqsen = $fqsen->withAlternateId($alternate_id);
while ($code_base->hasMethodWithFQSEN($fqsen)) {
yield $code_base->getMethodByFQSEN($fqsen);
$fqsen = $fqsen->withAlternateId(++$alternate_id);
}
}
/**
* @param CodeBase $code_base
* The code base with which to look for classes
*
* @return Method[]
* 0 or more Methods that this Method is overriding
* (Abstract methods are returned before concrete methods)
*/
public function getOverriddenMethods(
CodeBase $code_base
): array {
// Get the class that defines this method
$class = $this->getClass($code_base);
// Get the list of ancestors of that class
$ancestor_class_list = $class->getAncestorClassList(
$code_base
);
$defining_fqsen = $this->getDefiningFQSEN();
$method_list = [];
$abstract_method_list = [];
// Hunt for any ancestor classes that define a method with
// the same name as this one.
foreach ($ancestor_class_list as $ancestor_class) {
// TODO: Handle edge cases in traits.
// A trait may be earlier in $ancestor_class_list than the parent, but the parent may define abstract classes.
// TODO: What about trait aliasing rules?
if ($ancestor_class->hasMethodWithName($code_base, $this->name, true)) {
$method = $ancestor_class->getMethodByName(
$code_base,
$this->name
);
if ($method->getDefiningFQSEN() === $defining_fqsen) {
// Skip it, this method **is** the one which defined this.
continue;
}
// We initialize the overridden method's scope to ensure that
// analyzers are aware of the full param/return types of the overridden method.
$method->ensureScopeInitialized($code_base);
if ($method->isAbstract()) {
// TODO: check for trait conflicts, etc.
$abstract_method_list[] = $method;
continue;
}
$method_list[] = $method;
}
}
// Return abstract methods before concrete methods, in order to best check method compatibility.
$method_list = \array_merge($abstract_method_list, $method_list);
// Give up on throwing exceptions if this method doesn't override anything.
// Mixins and traits result in too many edge cases: https://github.com/phan/phan/issues/3796
return $method_list;
}
/**
* @return FullyQualifiedMethodName the FQSEN with the original definition (Even if this is private/protected and inherited from a trait). Used for dead code detection.
* Inheritance tests use getDefiningFQSEN() so that access checks won't break.
*/
public function getRealDefiningFQSEN(): FullyQualifiedMethodName
{
return $this->real_defining_fqsen ?? $this->getDefiningFQSEN();
}
/**
* @return string
* A string representation of this method signature (preferring phpdoc types)
*/
public function __toString(): string
{
$string = '';
// TODO: should this representation and other representations include visibility?
$string .= 'function ';
if ($this->returnsRef()) {
$string .= '&';
}
$string .= $this->name;
$string .= '(' . \implode(', ', \array_map(function (Parameter $param): string {
return $param->toStubString($this->isPHPInternal());
}, $this->getParameterList())) . ')';
$union_type = $this->getUnionTypeWithUnmodifiedStatic();
if (!$union_type->isEmpty()) {
$string .= ' : ' . (string)$union_type;
}
return $string;
}
/**
* @return string
* A string representation of this method signature
* (Based on real types only, instead of phpdoc+real types)
*/
public function toRealSignatureString(): string
{
$string = '';
$string .= 'function ';
if ($this->returnsRef()) {
$string .= '&';
}
$string .= $this->name;
$string .= '(' . \implode(', ', \array_map(function (Parameter $param): string {
return $param->toStubString($this->isPHPInternal());
}, $this->getRealParameterList())) . ')';
if (!$this->getRealReturnType()->isEmpty()) {
$string .= ' : ' . (string)$this->getRealReturnType();
}
return $string;
}
public function getMarkupDescription(): string
{
$string = '';
// It's an error to have visibility or abstract in an interface's stub (e.g. JsonSerializable)
if ($this->isPrivate()) {
$string .= 'private ';
} elseif ($this->isProtected()) {
$string .= 'protected ';
} else {
$string .= 'public ';
}
if ($this->isAbstract()) {
$string .= 'abstract ';
}
if ($this->isStatic()) {
$string .= 'static ';
}
$string .= 'function ';
if ($this->returnsRef()) {
$string .= '&';
}
$string .= $this->name;
$string .= '(' . $this->getParameterStubText() . ')';
if ($this->isPHPInternal()) {
$return_type = $this->getUnionType();
} else {
$return_type = $this->real_return_type;
}
if ($return_type && !$return_type->isEmpty()) {
// Use PSR-12 style with no space before `:`
$string .= ': ' . (string)$return_type;
}
return $string;
}
/**
* Returns this method's visibility ('private', 'protected', or 'public')
*/
public function getVisibilityName(): string
{
if ($this->isPrivate()) {
return 'private';
} elseif ($this->isProtected()) {
return 'protected';
} else {
return 'public';
}
}
/**
* Returns a PHP stub that can be used in the output of `tool/make_stubs`
*/
public function toStub(bool $class_is_interface = false): string
{
$string = ' ';
if ($this->isFinal()) {
$string .= 'final ';
}
// It's an error to have visibility or abstract in an interface's stub (e.g. JsonSerializable)
if (!$class_is_interface) {
$string .= $this->getVisibilityName() . ' ';
if ($this->isAbstract()) {
$string .= 'abstract ';
}
}
if ($this->isStatic()) {
$string .= 'static ';
}
$string .= 'function ';
if ($this->returnsRef()) {
$string .= '&';
}
$string .= $this->name;
$string .= '(' . $this->getRealParameterStubText() . ')';
if (!$this->getRealReturnType()->isEmpty()) {
$string .= ' : ' . (string)$this->getRealReturnType();
}
if ($this->isAbstract()) {
$string .= ';';
} else {
$string .= ' {}';
}
return $string;
}
/**
* Does this method have template types anywhere in its parameters or return type?
* (This check is recursive)
*/
public function hasTemplateType(): bool
{
return $this->getPhanFlagsHasState(Flags::HAS_TEMPLATE_TYPE);
}
private function recordHasTemplateType(): void
{
$this->setPhanFlags($this->getPhanFlags() | Flags::HAS_TEMPLATE_TYPE);
}
/**
* Attempt to convert this template method into a method with concrete types
* Either returns the original method or a clone of the method with more type information.
*/
public function resolveTemplateType(
CodeBase $code_base,
UnionType $object_union_type
): Method {
$defining_fqsen = $this->getDefiningClassFQSEN();
$defining_class = $code_base->getClassByFQSEN($defining_fqsen);
if (!$defining_class->isGeneric()) {
// ???
return $this;
}
$expected_type = $defining_fqsen->asType();
foreach ($object_union_type->getTypeSet() as $type) {
if (!$type->hasTemplateParameterTypes()) {
continue;
}
if (!$type->isObjectWithKnownFQSEN()) {
continue;
}
$expanded_type = $type->withIsNullable(false)->asExpandedTypes($code_base);
foreach ($expanded_type->getTypeSet() as $candidate) {
if (!$candidate->isTemplateSubtypeOf($expected_type)) {
continue;
}
// $candidate is $expected_type<T...>
$result = $this->cloneWithTemplateParameterTypeMap($candidate->getTemplateParameterTypeMap($code_base));
return $result;
}
}
// E.g. we can have `MyClass @implements MyBaseClass<string>` - so we check the expanded types for any template types, as well
foreach ($object_union_type->asExpandedTypes($code_base)->getTypeSet() as $type) {
if (!$type->hasTemplateParameterTypes()) {
continue;
}
if (!$type->isObjectWithKnownFQSEN()) {
continue;
}
$expanded_type = $type->withIsNullable(false)->asExpandedTypes($code_base);
foreach ($expanded_type->getTypeSet() as $candidate) {
if (!$candidate->isTemplateSubtypeOf($expected_type)) {
continue;
}
// $candidate is $expected_type<T...>
$result = $this->cloneWithTemplateParameterTypeMap($candidate->getTemplateParameterTypeMap($code_base));
return $result;
}
}
return $this;
}
/**
* @param array<string,UnionType> $template_type_map
* A map from template type identifier to a concrete type
*/
private function cloneWithTemplateParameterTypeMap(array $template_type_map): Method
{
$result = clone($this);
$result->cloneParameterList();
foreach ($result->parameter_list as $parameter) {
$parameter->setUnionType($parameter->getUnionType()->withTemplateParameterTypeMap($template_type_map));
}
$result->setUnionType($result->getUnionType()->withTemplateParameterTypeMap($template_type_map));
$result->setPhanFlags($result->getPhanFlags() & ~Flags::HAS_TEMPLATE_TYPE);
if (Config::get_track_references()) {
// Quick and dirty fix to make dead code detection work on this clone.
// Consider making this an object instead.
// @see AddressableElement::addReference()
$result->reference_list = &$this->reference_list;
}
return $result;
}
/**
* @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 && $file_ref->isInFunctionLikeScope() && $file_ref->getFunctionLikeFQSEN() === $this->fqsen) {
// Don't track methods calling themselves
return;
}
$this->reference_list[$file_ref->__toString()] = $file_ref;
}
}
}