Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of docblock method using parent keyword #7414

Merged
merged 4 commits into from Jan 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -445,37 +445,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 @@ -671,6 +650,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 @@ -781,37 +798,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 @@ -1046,4 +1038,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',
],
];
}
}