Skip to content

Commit

Permalink
Add support for constant expression typehints (#65)
Browse files Browse the repository at this point in the history
* Add support for constant expression typehints
Fixing #39
  • Loading branch information
bram123 authored Dec 10, 2024
1 parent 0729fef commit 4761b75
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 17 deletions.
2 changes: 1 addition & 1 deletion src/Constraint/AccessorPairConstraint.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ protected function getTestValues(ReflectionMethod $method, ReflectionParameter $
}
}

return $this->valueProviderFactory->getProvider($typehint)->getValues();
return $this->valueProviderFactory->getProvider($typehint, $method)->getValues();
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Constraint/Typehint/PhpDocParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public function getReturnTypehint(string $originalDocComment): ?string
return $this->normalizeDocblock($matches[1]);
}

preg_match('/\*\s*@return\s+(.*?)(?:\s+|\*)/', $docComment, $matches);
preg_match('/\*\s*@return\s+(.*?)(?:\s+|\*\/)/', $docComment, $matches);
if (isset($matches[1])) {
return $this->normalizeDocblock($matches[1]);
}
Expand Down
11 changes: 6 additions & 5 deletions src/Constraint/ValueProvider/NativeValueProviderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use phpDocumentor\Reflection\Types\String_;
use phpDocumentor\Reflection\PseudoTypes\False_;
use phpDocumentor\Reflection\PseudoTypes\True_;
use ReflectionMethod;

/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
Expand All @@ -48,9 +49,9 @@ public function __construct(ValueProviderFactory $valueProviderFactory)
/**
* @throws LogicException
*/
public function getProvider(Type $typehint): ?ValueProvider
public function getProvider(Type $typehint, ?ReflectionMethod $method = null): ?ValueProvider
{
$provider = $this->getCompoundProvider($typehint);
$provider = $this->getCompoundProvider($typehint, $method);
if ($provider !== null) {
return $provider;
}
Expand All @@ -73,13 +74,13 @@ public function getProvider(Type $typehint): ?ValueProvider
return null;
}

protected function getCompoundProvider(Type $typehint): ?ValueProvider
protected function getCompoundProvider(Type $typehint, ?ReflectionMethod $method): ?ValueProvider
{
switch (get_class($typehint)) {
case Array_::class:
return new ArrayProvider(
$this->valueProviderFactory->getProvider($typehint->getValueType()),
$this->valueProviderFactory->getProvider($typehint->getKeyType())
$this->valueProviderFactory->getProvider($typehint->getValueType(), $method),
$this->valueProviderFactory->getProvider($typehint->getKeyType(), $method)
);
case ArrayShape::class:
$items = $typehint->getItems();
Expand Down
54 changes: 54 additions & 0 deletions src/Constraint/ValueProvider/Pseudo/ConstExpressionProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);

namespace DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo;

use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\ValueProvider;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\Self_;
use ReflectionClass;
use ReflectionMethod;
use RuntimeException;

class ConstExpressionProvider implements ValueProvider
{
protected Type $owner;
protected string $expression;
protected ?ReflectionMethod $method;

public function __construct(Type $owner, string $expression, ?ReflectionMethod $method)
{
$this->owner = $owner;
$this->expression = $expression;
$this->method = $method;
}

/**
* @inheritDoc
*/
public function getValues(): array
{
if ($this->owner instanceof Object_ && $this->owner->getFqsen() !== null) {
/** @var class-string $fqsen */
$fqsen = (string)$this->owner->getFqsen();

$constClass = new ReflectionClass($fqsen);
} elseif ($this->owner instanceof Self_ && $this->method !== null) {
$constClass = $this->method->getDeclaringClass();
} else {
throw new RuntimeException('ConstExpressionProvider can only be used with object or self typehints');
}

$constants = $constClass->getConstants();
$expression = str_replace('*', '.*', $this->expression);
$values = [];
foreach ($constants as $constant => $value) {
if (preg_match('/^' . $expression . '$/', $constant) === 1) {
$values[] = $value;
}
}

return $values;
}
}
11 changes: 8 additions & 3 deletions src/Constraint/ValueProvider/PseudoValueProviderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\CallableStringProvider;
use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\ClassStringProvider;
use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\ConstExpressionProvider;
use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\DirectValueProvider;
use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\HtmlEscapedStringProvider;
use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\ListProvider;
Expand All @@ -18,6 +19,7 @@
use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Scalar\StringProvider;
use LogicException;
use phpDocumentor\Reflection\PseudoTypes\CallableString;
use phpDocumentor\Reflection\PseudoTypes\ConstExpression;
use phpDocumentor\Reflection\PseudoTypes\FloatValue;
use phpDocumentor\Reflection\PseudoTypes\HtmlEscapedString;
use phpDocumentor\Reflection\PseudoTypes\IntegerRange;
Expand All @@ -37,6 +39,7 @@
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\ArrayKey;
use phpDocumentor\Reflection\Types\ClassString;
use ReflectionMethod;

/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
Expand All @@ -53,23 +56,25 @@ public function __construct(ValueProviderFactory $valueProviderFactory)
/**
* @throws LogicException
*/
public function getProvider(Type $typehint): ?ValueProvider
public function getProvider(Type $typehint, ?ReflectionMethod $method = null): ?ValueProvider
{
switch (get_class($typehint)) {
case ArrayKey::class:
return new ValueProviderList(new StringProvider(), new IntProvider());
case IntegerRange::class:
return new IntProvider((int)$typehint->getMinValue(), (int)$typehint->getMaxValue());
case List_::class:
return new ListProvider($this->valueProviderFactory->getProvider($typehint->getValueType()));
return new ListProvider($this->valueProviderFactory->getProvider($typehint->getValueType(), $method));
case NonEmptyList::class:
return new NonEmptyValueProvider(new ListProvider($this->valueProviderFactory->getProvider($typehint->getValueType())));
return new NonEmptyValueProvider(new ListProvider($this->valueProviderFactory->getProvider($typehint->getValueType(), $method)));
case NegativeInteger::class:
return new IntProvider(PHP_INT_MIN, -1);
case Numeric_::class:
return new ValueProviderList(new NumericStringProvider(), new IntProvider(), new FloatProvider(new IntProvider()));
case PositiveInteger::class:
return new IntProvider(1, PHP_INT_MAX);
case ConstExpression::class:
return new ConstExpressionProvider($typehint->getOwner(), $typehint->getExpression(), $method);
}

return $this->getPseudoStringProvider($typehint);
Expand Down
15 changes: 8 additions & 7 deletions src/Constraint/ValueProvider/ValueProviderFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use phpDocumentor\Reflection\Types\Intersection;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
use ReflectionMethod;

/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
Expand All @@ -33,7 +34,7 @@ public function __construct()
*
* @throws LogicException
*/
public function getProvider(Type $typehint): ValueProvider
public function getProvider(Type $typehint, ?ReflectionMethod $method = null): ValueProvider
{
// Support intersection typehints, such as "Iterator&Countable"
if ($typehint instanceof Intersection) {
Expand All @@ -42,12 +43,12 @@ public function getProvider(Type $typehint): ValueProvider

// Support union typehints, such as "string|null"
if ($typehint instanceof Compound) {
return new ValueProviderList(...$this->getProviders(iterator_to_array($typehint)));
return new ValueProviderList(...$this->getProviders(iterator_to_array($typehint), $method));
}

// Support nullable typehints, such as "?string". Adds a NullProvider to the regular typehint's ValueProvider.
if ($typehint instanceof Nullable) {
return new ValueProviderList(new NullProvider(), $this->getProvider($typehint->getActualType()));
return new ValueProviderList(new NullProvider(), $this->getProvider($typehint->getActualType(), $method));
}

// Support for fully namespaced class name
Expand All @@ -56,13 +57,13 @@ public function getProvider(Type $typehint): ValueProvider
}

// Check if the provider typehint is a PHP scalar type
$nativeProvider = $this->nativeProviderFactory->getProvider($typehint);
$nativeProvider = $this->nativeProviderFactory->getProvider($typehint, $method);
if ($nativeProvider !== null) {
return $nativeProvider;
}

// Check if the provider typehint is a PHP pseudoType
$pseudoProvider = $this->pseudoProviderFactory->getProvider($typehint);
$pseudoProvider = $this->pseudoProviderFactory->getProvider($typehint, $method);
if ($pseudoProvider !== null) {
return $pseudoProvider;
}
Expand All @@ -76,11 +77,11 @@ public function getProvider(Type $typehint): ValueProvider
* @return ValueProvider[]
* @throws LogicException
*/
protected function getProviders(array $typehints): array
protected function getProviders(array $typehints, ?ReflectionMethod $method): array
{
$providers = [];
foreach ($typehints as $typehint) {
$providers[] = $this->getProvider($typehint);
$providers[] = $this->getProvider($typehint, $method);
}

return $providers;
Expand Down
28 changes: 28 additions & 0 deletions tests/Integration/data/success/Regular/ConstOtherProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);

namespace DigitalRevolution\AccessorPairConstraint\Tests\Integration\data\success\Regular;

class ConstOtherProperty
{
/** @var ConstSelfProperty::CONSTANT_* */
private string $property;

/**
* @return ConstSelfProperty::CONSTANT_*
*/
public function getProperty(): string
{
return $this->property;
}

/**
* @param ConstSelfProperty::CONSTANT_* $property
*/
public function setProperty(string $property): self
{
$this->property = $property;

return $this;
}
}
31 changes: 31 additions & 0 deletions tests/Integration/data/success/Regular/ConstSelfProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);

namespace DigitalRevolution\AccessorPairConstraint\Tests\Integration\data\success\Regular;

class ConstSelfProperty
{
public const CONSTANT_1 = 'value1';
public const CONSTANT_2 = 'value2';

/** @var self::CONSTANT_* */
private string $property;

/**
* @return self::CONSTANT_*
*/
public function getProperty(): string
{
return $this->property;
}

/**
* @param self::CONSTANT_* $property
*/
public function setProperty(string $property): self
{
$this->property = $property;

return $this;
}
}
4 changes: 4 additions & 0 deletions tests/Unit/Constraint/Typehint/PhpDocParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ public static function returnTypehintProvider(): Generator
yield ['/** @return array */', 'array'];
yield ['/**@return array*/', 'array'];
yield ["/**\n *@return array\n */", 'array'];

// Constant expressions
yield ['/** @return self::CONSTANT_* */', 'self::CONSTANT_*'];
yield ['/** @return SomeClass::CONSTANT_* */', 'SomeClass::CONSTANT_*'];
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace DigitalRevolution\AccessorPairConstraint\Tests\Unit\Constraint\ValueProvider\Pseudo;

use DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\ConstExpressionProvider;
use DigitalRevolution\AccessorPairConstraint\Tests\Unit\Constraint\ValueProvider\AbstractValueProviderTestCase;
use DigitalRevolution\AccessorPairConstraint\Tests\Unit\Constraint\ValueProvider\Pseudo\data\ClassWithConsts;
use phpDocumentor\Reflection\Fqsen;
use phpDocumentor\Reflection\Types\Callable_;
use phpDocumentor\Reflection\Types\Object_;
use phpDocumentor\Reflection\Types\Self_;
use ReflectionMethod;

/**
* @coversDefaultClass \DigitalRevolution\AccessorPairConstraint\Constraint\ValueProvider\Pseudo\ConstExpressionProvider
* @covers ::__construct
*/
class ConstExpressionProviderTest extends AbstractValueProviderTestCase
{
/**
* @covers ::getValues
*/
public function testGetValues(): void
{
$valueProvider = new ConstExpressionProvider(new Object_(new Fqsen('\\' . ClassWithConsts::class)), 'CONST_*', null);
$values = $valueProvider->getValues();

static::assertValueTypes($values, ['string']);
static::assertContains('CONST_A', $values);
static::assertContains('CONST_B', $values);
static::assertNotContains('CONSTANT_A', $values);
}

/**
* @covers ::getValues
*/
public function testGetValuesSelf(): void
{
$valueProvider = new ConstExpressionProvider(new Self_(), 'CONST_*', new ReflectionMethod(ClassWithConsts::class, 'setConst'));
$values = $valueProvider->getValues();

static::assertValueTypes($values, ['string']);
static::assertContains('CONST_A', $values);
static::assertContains('CONST_B', $values);
static::assertNotContains('CONSTANT_A', $values);
}

/**
* @covers ::getValues
*/
public function testGetValuesInvalidType(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('ConstExpressionProvider can only be used with object or self typehints');

$valueProvider = new ConstExpressionProvider(new Callable_([]), 'CONST_*', null);
$valueProvider->getValues();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace DigitalRevolution\AccessorPairConstraint\Tests\Unit\Constraint\ValueProvider\Pseudo\data;

class ClassWithConsts
{
public const CONST_A = 'CONST_A';
public const CONST_B = 'CONST_B';
public const CONSTANT_A = 'CONSTANT_A';

/** @var string */
private string $const;

/**
* @param self::CONST_* $const
*/
public function setConst(string $const): self
{
$this->const = $const;

return $this;
}

/**
* @return self::CONST_*
*/
public function getConst(): string
{
return $this->const;
}
}

0 comments on commit 4761b75

Please sign in to comment.