Skip to content

Commit

Permalink
Make Phan's inferred return type for a method affect inherited methods
Browse files Browse the repository at this point in the history
  • Loading branch information
TysonAndre committed May 4, 2019
1 parent b65ce2a commit 7120a60
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 1 deletion.
3 changes: 3 additions & 0 deletions NEWS.md
Expand Up @@ -12,6 +12,9 @@ New features(CLI, Configs):

New features(Analysis):
+ Emit `PhanDeprecatedClassConstant` for code using a constant marked with `@deprecated`.
+ When recursively inferring the return type of `BaseClass::method()` from its return statements,
make that also affect the inherited copies of that method (`SubClass::method()`). (#2718)
This change is limited to methods with no return type in the phpdoc or real signature.

Plugins:
+ Add more forms of checks such as `$x !== null ? $x : null` to `PhanPluginDuplicateConditionalNullCoalescing` (#2691)
Expand Down
2 changes: 2 additions & 0 deletions src/Phan/Language/Element/Clazz.php
Expand Up @@ -1486,6 +1486,7 @@ public function addMethod(
}

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,
Expand All @@ -1494,6 +1495,7 @@ public function addMethod(

// 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
Expand Down
45 changes: 44 additions & 1 deletion src/Phan/Language/Element/Method.php
Expand Up @@ -38,6 +38,14 @@ class Method extends ClassElement implements FunctionInterface
*/
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
Expand Down Expand Up @@ -541,12 +549,47 @@ static function (int $carry, Parameter $parameter) : int {
return $method;
}

/**
* Ensure that this clone will use the return type of the ancestor method
* @return void
*/
public function ensureClonesReturnType(Method $original_method)
{
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);
}

/**
* @return UnionType
* The type of this method in its given context.
* The return type of this method in its given context.
*/
public function getUnionType() : UnionType
{
if ($this->defining_method_for_type_fetching) {
return $this->defining_method_for_type_fetching->getUnionType();
}
$union_type = parent::getUnionType();

// If the type is 'static', add this context's class
Expand Down
2 changes: 2 additions & 0 deletions src/Phan/Language/FQSEN/FullyQualifiedClassElement.php
Expand Up @@ -107,6 +107,8 @@ public function getCanonicalFQSEN() : FQSEN
* @param $fully_qualified_string
* An FQSEN string like '\Namespace\Class::methodName'
*
* @return static
*
* @throws InvalidArgumentException if the $fully_qualified_string doesn't have a '::' delimiter
*
* @throws FQSENException if the class or element FQSEN is invalid
Expand Down

0 comments on commit 7120a60

Please sign in to comment.