Skip to content

Commit

Permalink
Allow dynamic properties from PHPDoc
Browse files Browse the repository at this point in the history
  • Loading branch information
fluffycondor committed May 4, 2023
1 parent fc233da commit c69620d
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ public static function checkFullyQualifiedClassLikeName(

/**
* Gets the fully-qualified class name from a Name object
*
* @return class-string

Check failure on line 393 in src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php

View workflow job for this annotation

GitHub Actions / build

MoreSpecificReturnType

src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php:393:16: MoreSpecificReturnType: The declared return type 'class-string' for Psalm\Internal\Analyzer\ClassLikeAnalyzer::getFQCLNFromNameObject is more specific than the inferred return type 'string' (see https://psalm.dev/070)
*/
public static function getFQCLNFromNameObject(
PhpParser\Node\Name $class_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,8 @@ private static function handleNonExistentProperty(
?string $var_id,
bool &$has_valid_fetch_type
): void {
if ($config->use_phpdoc_property_without_magic_or_parent
if (($config->use_phpdoc_property_without_magic_or_parent
|| $class_storage->hasAttributeIncludingParents('AllowDynamicProperties', $codebase))
&& isset($class_storage->pseudo_property_get_types['$' . $prop_name])
) {
$stmt_type = $class_storage->pseudo_property_get_types['$' . $prop_name];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static function resolve(
?string $fq_classlike_name
): AttributeStorage {
if ($stmt->name instanceof PhpParser\Node\Name\FullyQualified) {
/** @var class-string $fq_type_string */
$fq_type_string = (string)$stmt->name;
} else {
$fq_type_string = ClassLikeAnalyzer::getFQCLNFromNameObject($stmt->name, $aliases);
Expand Down
3 changes: 2 additions & 1 deletion src/Psalm/Storage/AttributeStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class AttributeStorage
{
use ImmutableNonCloneableTrait;
/**
* @var string
* @var class-string
*/
public $fq_class_name;

Expand All @@ -33,6 +33,7 @@ final class AttributeStorage
public $name_location;

/**
* @param class-string $fq_class_name
* @param list<AttributeArg> $args
*/
public function __construct(
Expand Down
36 changes: 36 additions & 0 deletions src/Psalm/Storage/ClassLikeStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Psalm\Aliases;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Config;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\MethodIdentifier;
Expand All @@ -13,7 +14,9 @@
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;

use function array_map;
use function array_values;
use function in_array;

final class ClassLikeStorage implements HasAttributesInterface
{
Expand Down Expand Up @@ -486,6 +489,24 @@ public function getAttributeStorages(): array
return $this->attributes;
}

public function hasAttributeIncludingParents(
string $fq_class_name,
Codebase $codebase,
): bool {
if ($this->hasAttribute($fq_class_name)) {
return true;
}

foreach ($this->parent_classes as $parent_class) {
$parent_class_storage = $codebase->classlike_storage_provider->get($parent_class);
if ($parent_class_storage->hasAttribute($fq_class_name)) {
return true;
}
}

return false;
}

/**
* Get the template constraint types for the class.
*
Expand All @@ -511,4 +532,19 @@ public function hasSealedMethods(Config $config): bool
{
return $this->sealed_methods ?? $config->seal_all_methods;
}

/**
* @param class-string $fq_class_name
*/
private function hasAttribute(string $fq_class_name): bool
{
return in_array(
$fq_class_name,
array_map(
fn(AttributeStorage $storage) => $storage->fq_class_name,
$this->attributes,
),
true,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Psalm\Tests\Internal\Analyzer\Statements\Expression\Fetch;

use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;

final class AtomicPropertyFetchAnalyzerTest extends TestCase
{
use ValidCodeAnalysisTestTrait;

public function providerValidCodeParse(): iterable
{
return [
'allowDynamicProperties' => [
'code' => '<?php
/** @property-read string $foo */
#[\AllowDynamicProperties]
class A {
public function __construct(string $key, string $value)
{
$this->$key = $value;
}
}
echo (new A("foo", "bar"))->foo;
',
'assertions' => [],
'ignores_issues' => [],
'php_version' => '8.2',
],
'allowDynamicProperties for child' => [
'code' => '<?php
/** @property-read string $foo */
#[\AllowDynamicProperties]
class A {
public function __construct(string $key, string $value)
{
$this->$key = $value;
}
}
class B extends A {}
echo (new B("foo", "bar"))->foo;
',
'assertions' => [],
'ignores_issues' => [],
'php_version' => '8.2',
],
'allowDynamicProperties for grandchild' => [
'code' => '<?php
/** @property-read string $foo */
#[\AllowDynamicProperties]
class A {
public function __construct(string $key, string $value)
{
$this->$key = $value;
}
}
class B extends A {}
class C extends B {}
echo (new C("foo", "bar"))->foo;
',
'assertions' => [],
'ignores_issues' => [],
'php_version' => '8.2',
],
];
}
}

0 comments on commit c69620d

Please sign in to comment.