Skip to content

Commit

Permalink
Merge pull request #22 from veewee/psalm-plugin-for-multiple-properti…
Browse files Browse the repository at this point in the history
…es-actions

Infer properties_get and properties_set settings.
  • Loading branch information
veewee authored Feb 6, 2025
2 parents 9db448e + 5cb212b commit aa5fae1
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/analyzers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
php-versions: ['8.1', '8.2', '8.3', '8.4']
php-versions: ['8.2', '8.3', '8.4']
composer-options: ['--ignore-platform-req=php+']
fail-fast: false
name: PHP ${{ matrix.php-versions }} @ ${{ matrix.operating-system }}
Expand Down
2 changes: 2 additions & 0 deletions src/Psalm/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ private function getHooks(): iterable
{
yield Iso\Provider\ComposeProvider::class;
yield Lens\Provider\ComposeProvider::class;
yield Reflect\Provider\PropertiesSetProvider::class;
yield Reflect\Provider\PropertiesGetProvider::class;
yield Reflect\Provider\PropertyGetProvider::class;
yield Reflect\Provider\PropertySetProvider::class;
}
Expand Down
46 changes: 46 additions & 0 deletions src/Psalm/Reflect/Infer/PropertiesValuesType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\Psalm\Reflect\Infer;

use Psalm\Internal\Codebase\Reflection;
use Psalm\Type;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
use ReflectionProperty;
use VeeWee\Reflecta\Reflect\Exception\UnreflectableException;
use VeeWee\Reflecta\Reflect\Type\ReflectedClass;
use VeeWee\Reflecta\Reflect\Type\ReflectedProperty;
use function Psl\Dict\map;

final class PropertiesValuesType
{
/**
* @throws UnreflectableException
*/
public static function infer(
TNamedObject | TTemplateParam | null $objectType,
bool $partial = false,
): Union|null {
if (!$objectType) {
return null;
}

$class = ReflectedClass::fromFullyQualifiedClassName($objectType->value);
$properties = $class->properties();

return new Union([
new TKeyedArray(
map(
$properties,
static fn (ReflectedProperty $prop) => Reflection::getPsalmTypeFromReflectionType($prop->apply(
static fn (ReflectionProperty $reflected) => $reflected->getType()
))->setPossiblyUndefined($partial),
),
fallback_params: $class->isDynamic() ? [Type::getArrayKey(), Type::getMixed()] : null,
)
]);
}
}
49 changes: 49 additions & 0 deletions src/Psalm/Reflect/Provider/PropertiesGetProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\Psalm\Reflect\Provider;

use Psalm\Plugin\DynamicFunctionStorage;
use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface;
use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Type;
use Psalm\Type\Union;
use VeeWee\Reflecta\Psalm\Reflect\Infer\ObjectType;
use VeeWee\Reflecta\Psalm\Reflect\Infer\PropertiesValuesType;
use function array_key_exists;

final class PropertiesGetProvider implements DynamicFunctionStorageProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['veewee\reflecta\reflect\properties_get'];
}

public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage
{
$args = $event->getArgs();
$inferrer = $event->getArgTypeInferer();

$objectType = ObjectType::infer($inferrer, $args[0]);
$hasPredicate = array_key_exists(1, $args);
$predicateType = $hasPredicate ? $inferrer->infer($args[1]) : Type::getNull();
$valuesType = PropertiesValuesType::infer($objectType, partial: $hasPredicate);

if (!$objectType || !$valuesType) {
return null;
}

$storage = new DynamicFunctionStorage();
$storage->params = [
new FunctionLikeParameter('object', false, new Union([$objectType])),
new FunctionLikeParameter('predicate', false, $predicateType),
];
$storage->return_type = $valuesType;

return $storage;
}
}
49 changes: 49 additions & 0 deletions src/Psalm/Reflect/Provider/PropertiesSetProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);

namespace VeeWee\Reflecta\Psalm\Reflect\Provider;

use Psalm\Plugin\DynamicFunctionStorage;
use Psalm\Plugin\EventHandler\DynamicFunctionStorageProviderInterface;
use Psalm\Plugin\EventHandler\Event\DynamicFunctionStorageProviderEvent;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Type;
use Psalm\Type\Union;
use VeeWee\Reflecta\Psalm\Reflect\Infer\ObjectType;
use VeeWee\Reflecta\Psalm\Reflect\Infer\PropertiesValuesType;
use function array_key_exists;

final class PropertiesSetProvider implements DynamicFunctionStorageProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['veewee\reflecta\reflect\properties_set'];
}

public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $event): ?DynamicFunctionStorage
{
$args = $event->getArgs();
$inferrer = $event->getArgTypeInferer();

$objectType = ObjectType::infer($inferrer, $args[0]);
$valuesType = PropertiesValuesType::infer($objectType, partial: true);
$predicateType = array_key_exists(2, $args) ? $inferrer->infer($args[2]) : Type::getNull();

if (!$objectType || !$valuesType) {
return null;
}

$storage = new DynamicFunctionStorage();
$storage->params = [
new FunctionLikeParameter('object', false, new Union([$objectType])),
new FunctionLikeParameter('values', false, $valuesType),
new FunctionLikeParameter('predicate', false, $predicateType),
];
$storage->return_type = new Union([$objectType]);

return $storage;
}
}
10 changes: 10 additions & 0 deletions tests/fixtures/MultipleProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\TestFixtures;

final class MultipleProperties
{
public string $a;
public string $b;
public string $c;
}
81 changes: 81 additions & 0 deletions tests/static-analyzer/Reflect/properties_get.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\SaTests\Reflect;

use Closure;
use VeeWee\Reflecta\Reflect\Type\Visibility;
use VeeWee\Reflecta\TestFixtures\Dynamic;
use VeeWee\Reflecta\TestFixtures\MultipleProperties;
use VeeWee\Reflecta\TestFixtures\X;
use function VeeWee\Reflecta\Reflect\Predicate\property_visibility;
use function VeeWee\Reflecta\Reflect\properties_get;

/**
* @return array{z: int|null}
*/
function test_get_prop_return_type(): array
{
$x = new X();
$x->z = 123;

return properties_get($x);
}

/**
* @return array{z ?: int|null}
*/
function test_get_optional_prop_return_type(): array
{
$x = new X();
$x->z = 123;

return properties_get($x, property_visibility(Visibility::Private));
}

/**
* @return array{a: string, b: string, c: string}
*/
function test_get_multi_props_return_type(): array
{
$x = new MultipleProperties();

return properties_get($x);
}

/**
* @return array{a ?: string, b ?: string, c ?: string}
*/
function test_get_optional_multi_props_return_type(): array
{
$x = new MultipleProperties();

return properties_get($x, property_visibility(Visibility::Private));
}

/**
* @return array{x: string, ...<array-key, mixed>}
*/
function test_get_dynamic_props_return_type(): array
{
$x = new Dynamic();

return properties_get($x);
}

/**
* @return array{x ?: string, ...<array-key, mixed>}
*/
function test_get_optional_dynamic_props_return_type(): array
{
$x = new Dynamic();

return properties_get($x, property_visibility(Visibility::Private));
}

function test_get_mixed_return_type_on_templated_object(): array
{
$curried = static fn (): Closure => static fn (object $object): array => properties_get($object);
$x = new X();

return $curried()($x);
}
64 changes: 64 additions & 0 deletions tests/static-analyzer/Reflect/properties_set.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types=1);

namespace VeeWee\Reflecta\SaTests\Reflect;

use VeeWee\Reflecta\Reflect\Type\Visibility;
use VeeWee\Reflecta\TestFixtures\Dynamic;
use VeeWee\Reflecta\TestFixtures\MultipleProperties;
use VeeWee\Reflecta\TestFixtures\X;
use function VeeWee\Reflecta\Reflect\Predicate\property_visibility;
use function VeeWee\Reflecta\Reflect\properties_set;

function test_set_valid_prop_value_type(): X
{
$x = new X();
$x->z = 123;

return properties_set($x, ['z' => 456]);
}

function test_set_valid_prop_value_type_with_predicate(): X
{
$x = new X();
$x->z = 123;

return properties_set($x, ['z' => 456], property_visibility(Visibility::Private));
}

function test_set_partial_props(): MultipleProperties
{
$x = new MultipleProperties();
$x->a = '';
$x->b = '';

return properties_set($x, ['c' => 'foo']);
}

/**
* @psalm-suppress InvalidScalarArgument
*/
function test_set_invalid_prop_value_type(): X
{
$x = new X();
$x->z = 123;

return properties_set($x, ['z' => 'nope']);
}

/**
* @psalm-suppress InvalidArgument
*/
function test_assigning_unknown_property(): X
{
$x = new X();

return properties_set($x, ['unknown' => 'nope']);
}

function test_set_new_prop_on_dynamic_class(): Dynamic
{
$x = new Dynamic();
$x->x = 'string';

return properties_set($x, ['foo' => 'bar']);
}

0 comments on commit aa5fae1

Please sign in to comment.