1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 22:01:48 +01:00
This commit is contained in:
kkmuffme 2023-12-19 11:07:11 +01:00
parent b38530ed0d
commit 1ff8518888
2 changed files with 136 additions and 0 deletions

View File

@ -7,6 +7,7 @@ use Psalm\CodeLocation;
use Psalm\Codebase; use Psalm\Codebase;
use Psalm\Context; use Psalm\Context;
use Psalm\Internal\Analyzer\AttributesAnalyzer; use Psalm\Internal\Analyzer\AttributesAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier; use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
@ -45,6 +46,7 @@ use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union; use Psalm\Type\Union;
@ -1253,6 +1255,37 @@ final class ArgumentsAnalyzer
return null; return null;
} }
private static function handleByRefReadonlyArg(
StatementsAnalyzer $statements_analyzer,
Context $context,
PhpParser\Node\Expr\PropertyFetch $stmt,
string $fq_class_name,
string $prop_name
): void {
$property_id = $fq_class_name . '::$' . $prop_name;
$codebase = $statements_analyzer->getCodebase();
$declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
$property_id,
false, # what does this do? @todo
$statements_analyzer,
);
$declaring_class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
if (isset($declaring_class_storage->properties[$prop_name])) {
$property_storage = $declaring_class_storage->properties[$prop_name];
InstancePropertyAssignmentAnalyzer::trackPropertyImpurity(
$statements_analyzer,
$stmt,
$property_id,
$property_storage,
$declaring_class_storage,
$context,
);
}
}
/** /**
* @return false|null * @return false|null
*/ */
@ -1274,6 +1307,46 @@ final class ArgumentsAnalyzer
'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift',
]; ];
if ($arg->value instanceof PhpParser\Node\Expr\PropertyFetch
&& $arg->value->name instanceof PhpParser\Node\Identifier) {
$prop_name = $arg->value->name->name;
if ($statements_analyzer->getFQCLN()) {
$fq_class_name = $statements_analyzer->getFQCLN();
self::handleByRefReadonlyArg(
$statements_analyzer,
$context,
$arg->value,
$fq_class_name,
$prop_name,
);
} else {
// @todo atm only works for simple fetch, $a->foo, not $a->foo->bar
// I guess there's a function to do this, but I couldn't locate it
$var_id = ExpressionIdentifier::getVarId(
$arg->value->var,
$statements_analyzer->getFQCLN(),
$statements_analyzer,
);
if ($var_id && isset($context->vars_in_scope[$var_id])) {
foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
$fq_class_name = $atomic_type->value;
self::handleByRefReadonlyArg(
$statements_analyzer,
$context,
$arg->value,
$fq_class_name,
$prop_name,
);
}
}
}
}
}
if (($var_id && isset($context->vars_in_scope[$var_id])) if (($var_id && isset($context->vars_in_scope[$var_id]))
|| ($method_id || ($method_id
&& in_array( && in_array(

View File

@ -764,6 +764,69 @@ class ImmutableAnnotationTest extends TestCase
}', }',
'error_message' => 'ImpurePropertyAssignment', 'error_message' => 'ImpurePropertyAssignment',
], ],
'readonlyByRefInClass' => [
'code' => '<?php
namespace World;
final readonly class Foo
{
public array $values;
public function __construct(array $values)
{
$this->values = $values;
}
public function bar(): mixed
{
return reset($this->values);
}
}',
'error_message' => 'InaccessibleProperty',
],
'readonlyByRef' => [
'code' => '<?php
namespace World;
final readonly class Foo
{
public array $values;
public function __construct(array $values)
{
$this->values = $values;
}
}
$x = new Foo([]);
reset($x->values);',
'error_message' => 'InaccessibleProperty',
],
'readonlyByRefCustomFunction' => [
'code' => '<?php
namespace World;
final readonly class Foo
{
public array $values;
public function __construct(array $values)
{
$this->values = $values;
}
}
/**
* @param string $a
* @param array $b
* @return void
*/
function bar($a, &$b) {}
$x = new Foo([]);
bar("hello", $x->values);',
'error_message' => 'InaccessibleProperty',
],
'preventUnset' => [ 'preventUnset' => [
'code' => '<?php 'code' => '<?php
/** /**