Skip to content

Commit

Permalink
Merge pull request #7414 from b2pweb/call-docblock-method-using-parent
Browse files Browse the repository at this point in the history
Add support of docblock method using parent keyword
  • Loading branch information
orklah committed Jan 24, 2022
2 parents 0619b40 + 280de4b commit f1c4b62
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 56 deletions.
Expand Up @@ -481,37 +481,16 @@ private static function handleNamedCall(
$class_storage->final
);

$old_data_provider = $statements_analyzer->node_data;

$statements_analyzer->node_data = clone $statements_analyzer->node_data;

$context->vars_in_scope['$tmp_mixin_var'] = $new_lhs_type;

$fake_method_call_expr = new VirtualMethodCall(
new VirtualVariable(
'tmp_mixin_var',
$stmt->class->getAttributes()
),
return self::forwardCallToInstanceMethod(
$statements_analyzer,
$stmt,
$stmt_name,
$stmt->getArgs(),
$stmt->getAttributes()
$context,
'tmp_mixin_var',
true
);

if (MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_method_call_expr,
$context
) === false) {
return false;
}

$fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr);

$statements_analyzer->node_data = $old_data_provider;

$statements_analyzer->node_data->setType($stmt, $fake_method_call_type ?? Type::getMixed());

return true;
}
}
}
Expand Down Expand Up @@ -707,6 +686,44 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem {
if ($pseudo_method_storage->return_type) {
return true;
}
} elseif ($stmt->class instanceof PhpParser\Node\Name && $stmt->class->parts[0] === 'parent'
&& !$codebase->methodExists($method_id)
&& !$statements_analyzer->isStatic()
) {
// In case of parent::xxx() call on instance method context (i.e. not static context)
// with nonexistent method, we try to forward to instance method call for resolve pseudo method.

// Use parent type as static type for the method call
$context->vars_in_scope['$tmp_parent_var'] = new Union([$lhs_type_part]);

if (self::forwardCallToInstanceMethod(
$statements_analyzer,
$stmt,
$stmt_name,
$context,
'tmp_parent_var'
) === false) {
return false;
}

// Resolve actual static return type according to caller (i.e. $this) static type
if (isset($context->vars_in_scope['$this'])
&& $method_call_type = $statements_analyzer->node_data->getType($stmt)
) {
$method_call_type = clone $method_call_type;

foreach ($method_call_type->getAtomicTypes() as $name => $type) {
if ($type instanceof TNamedObject && $type->was_static && $type->value === $fq_class_name) {
// Replace parent&static type to actual static type
$method_call_type->removeType($name);
$method_call_type->addType($context->vars_in_scope['$this']->getSingleAtomic());
}
}

$statements_analyzer->node_data->setType($stmt, $method_call_type);
}

return true;
}

if (!$context->check_methods) {
Expand Down Expand Up @@ -817,37 +834,12 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem {
}

if ($is_dynamic_this_method) {
$old_data_provider = $statements_analyzer->node_data;

$statements_analyzer->node_data = clone $statements_analyzer->node_data;

$fake_method_call_expr = new VirtualMethodCall(
new VirtualVariable(
'this',
$stmt->class->getAttributes()
),
$stmt_name,
$stmt->getArgs(),
$stmt->getAttributes()
);

if (MethodCallAnalyzer::analyze(
return self::forwardCallToInstanceMethod(
$statements_analyzer,
$fake_method_call_expr,
$stmt,
$stmt_name,
$context
) === false) {
return false;
}

$fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr);

$statements_analyzer->node_data = $old_data_provider;

if ($fake_method_call_type) {
$statements_analyzer->node_data->setType($stmt, $fake_method_call_type);
}

return true;
);
}
}

Expand Down Expand Up @@ -1082,4 +1074,59 @@ private static function findPseudoMethodAndClassStorages(

return null;
}

/**
* Forward static call to instance call, using `VirtualMethodCall` and `MethodCallAnalyzer::analyze()`
* The resolved method return type will be set as type of the $stmt node.
*
* @param StatementsAnalyzer $statements_analyzer
* @param PhpParser\Node\Expr\StaticCall $stmt
* @param PhpParser\Node\Identifier $stmt_name
* @param Context $context
* @param string $virtual_var_name Temporary var name to use for create the fake MethodCall statement.
* @param bool $always_set_node_type If true, when the method has no declared typed, mixed will be set on node.
*
* @return bool Result of analysis. False if the call is invalid.
*
* @see MethodCallAnalyzer::analyze()
*/
private static function forwardCallToInstanceMethod(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\StaticCall $stmt,
PhpParser\Node\Identifier $stmt_name,
Context $context,
string $virtual_var_name = 'this',
bool $always_set_node_type = false
): bool {
$old_data_provider = $statements_analyzer->node_data;

$statements_analyzer->node_data = clone $statements_analyzer->node_data;

$fake_method_call_expr = new VirtualMethodCall(
new VirtualVariable($virtual_var_name, $stmt->class->getAttributes()),
$stmt_name,
$stmt->getArgs(),
$stmt->getAttributes()
);

if (MethodCallAnalyzer::analyze(
$statements_analyzer,
$fake_method_call_expr,
$context
) === false) {
return false;
}

$fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr);

$statements_analyzer->node_data = $old_data_provider;

if ($fake_method_call_type) {
$statements_analyzer->node_data->setType($stmt, $fake_method_call_type);
} elseif ($always_set_node_type) {
$statements_analyzer->node_data->setType($stmt, Type::getMixed());
}

return true;
}
}
34 changes: 34 additions & 0 deletions tests/MagicMethodAnnotationTest.php
Expand Up @@ -819,6 +819,40 @@ function consumeInt(int $i): void {}
consumeInt(B::bar());'
],
'callUsingParent' => [
'<?php
/**
* @method static create(array $data)
*/
class Model {
public function __call(string $name, array $arguments) {
/** @psalm-suppress UnsafeInstantiation */
return new static;
}
}
class BlahModel extends Model {
/**
* @param mixed $input
* @return static
*/
public function create($input): BlahModel
{
return parent::create([]);
}
}
class FooModel extends Model {}
function consumeFoo(FooModel $a): void {}
function consumeBlah(BlahModel $a): void {}
$b = new FooModel();
consumeFoo($b->create([]));
$d = new BlahModel();
consumeBlah($d->create([]));'
],
'returnThisShouldKeepGenerics' => [
'<?php
/**
Expand Down
38 changes: 38 additions & 0 deletions tests/MethodCallTest.php
Expand Up @@ -1005,6 +1005,34 @@ function fooOrNull(): ?Foo {
[],
'8.0'
],
'parentMagicMethodCall' => [
'<?php
class Model {
/**
* @return static
*/
public function __call(string $method, array $args) {
/** @psalm-suppress UnsafeInstantiation */
return new static;
}
}
class BlahModel extends Model {
/**
* @param mixed $input
*/
public function create($input): BlahModel
{
return parent::create([]);
}
}
$m = new BlahModel();
$n = $m->create([]);',
[
'$n' => 'BlahModel',
]
],
];
}

Expand Down Expand Up @@ -1538,6 +1566,16 @@ function fooOrNull(): ?Foo {
false,
'8.0'
],
'undefinedMethodOnParentCallWithMethodExistsOnSelf' => [
'<?php
class A {}
class B extends A {
public function foo(): string {
return parent::foo();
}
}',
'error_message' => 'UndefinedMethod',
],
];
}
}

0 comments on commit f1c4b62

Please sign in to comment.