mirror of
https://github.com/danog/Valinor.git
synced 2025-01-22 13:21:35 +01:00
feat: allow psalm
and phpstan
prefix in docblocks
The following annotations are now properly handled: `@psalm-param`, `@phpstan-param`, `@psalm-return` and `@phpstan-return`. If one of those found along with a basic `@param` or `@return` annotation, it will override the basic value.
This commit is contained in:
parent
34f519b583
commit
64e0a2d5ac
@ -20,7 +20,6 @@ use RuntimeException;
|
|||||||
|
|
||||||
use function get_class;
|
use function get_class;
|
||||||
use function implode;
|
use function implode;
|
||||||
use function preg_match;
|
|
||||||
use function preg_match_all;
|
use function preg_match_all;
|
||||||
use function preg_replace;
|
use function preg_replace;
|
||||||
use function spl_object_hash;
|
use function spl_object_hash;
|
||||||
@ -29,6 +28,10 @@ use function trim;
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
final class Reflection
|
final class Reflection
|
||||||
{
|
{
|
||||||
|
private const TOOL_NONE = '';
|
||||||
|
private const TOOL_EXPRESSION = '((?<tool>psalm|phpstan)-)';
|
||||||
|
private const TYPE_EXPRESSION = '(?<type>[\w\s?|&<>\'",-:\\\\\[\]{}]+)';
|
||||||
|
|
||||||
/** @var array<class-string, ReflectionClass<object>> */
|
/** @var array<class-string, ReflectionClass<object>> */
|
||||||
private static array $classReflection = [];
|
private static array $classReflection = [];
|
||||||
|
|
||||||
@ -110,28 +113,46 @@ final class Reflection
|
|||||||
{
|
{
|
||||||
if ($reflection instanceof ReflectionProperty) {
|
if ($reflection instanceof ReflectionProperty) {
|
||||||
$docComment = self::sanitizeDocComment($reflection);
|
$docComment = self::sanitizeDocComment($reflection);
|
||||||
$regex = "@var\s+([\w\s?|&<>'\",-:\\\\\[\]{}]+)";
|
$expression = sprintf('@%s?var\s+%s', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION);
|
||||||
} else {
|
} else {
|
||||||
$docComment = self::sanitizeDocComment($reflection->getDeclaringFunction());
|
$docComment = self::sanitizeDocComment($reflection->getDeclaringFunction());
|
||||||
$regex = "@param\s+([\w\s?|&<>'\",-:\\\\\[\]{}]+)\s+\\$$reflection->name(\W+|$)";
|
$expression = sprintf('@%s?param\s+%s\s+\$\b%s\b', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION, $reflection->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! preg_match("/$regex/", $docComment, $matches)) {
|
if (! preg_match_all("/$expression/", $docComment, $matches)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $matches[1];
|
foreach ($matches['tool'] as $index => $tool) {
|
||||||
|
if ($tool === self::TOOL_NONE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($matches['type'][$index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($matches['type'][0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function docBlockReturnType(ReflectionFunctionAbstract $reflection): ?string
|
public static function docBlockReturnType(ReflectionFunctionAbstract $reflection): ?string
|
||||||
{
|
{
|
||||||
$docComment = self::sanitizeDocComment($reflection);
|
$docComment = self::sanitizeDocComment($reflection);
|
||||||
|
|
||||||
if (! preg_match("/@return\s+([\w\s?|&<>'\",-:\\\\\[\]{}]+)(\W*|$)/", $docComment, $matches)) {
|
$expression = sprintf('/@%s?return\s+%s/', self::TOOL_EXPRESSION, self::TYPE_EXPRESSION);
|
||||||
|
|
||||||
|
if (! preg_match_all($expression, $docComment, $matches)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return trim($matches[1]);
|
foreach ($matches['tool'] as $index => $tool) {
|
||||||
|
if ($tool === self::TOOL_NONE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($matches['type'][$index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($matches['type'][0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -143,7 +164,9 @@ final class Reflection
|
|||||||
$types = [];
|
$types = [];
|
||||||
$docComment = self::sanitizeDocComment($reflection);
|
$docComment = self::sanitizeDocComment($reflection);
|
||||||
|
|
||||||
preg_match_all('/@(phpstan|psalm)-type\s+([a-zA-Z]\w*)\s*=?\s*([\w\s?|&<>\'",-:\\\\\[\]{}]+)/', $docComment, $matches);
|
$expression = sprintf('/@(phpstan|psalm)-type\s+([a-zA-Z]\w*)\s*=?\s*%s/', self::TYPE_EXPRESSION);
|
||||||
|
|
||||||
|
preg_match_all($expression, $docComment, $matches);
|
||||||
|
|
||||||
foreach ($matches[2] as $key => $name) {
|
foreach ($matches[2] as $key => $name) {
|
||||||
$types[(string)$name] = $matches[3][$key];
|
$types[(string)$name] = $matches[3][$key];
|
||||||
@ -161,7 +184,8 @@ final class Reflection
|
|||||||
$types = [];
|
$types = [];
|
||||||
$docComment = self::sanitizeDocComment($reflection);
|
$docComment = self::sanitizeDocComment($reflection);
|
||||||
|
|
||||||
preg_match_all('/@(phpstan|psalm)-import-type\s+([a-zA-Z]\w*)\s*from\s*([\w\s?|&<>\'",-:\\\\\[\]{}]+)/', $docComment, $matches);
|
$expression = sprintf('/@(phpstan|psalm)-import-type\s+([a-zA-Z]\w*)\s*from\s*%s/', self::TYPE_EXPRESSION);
|
||||||
|
preg_match_all($expression, $docComment, $matches);
|
||||||
|
|
||||||
foreach ($matches[2] as $key => $name) {
|
foreach ($matches[2] as $key => $name) {
|
||||||
/** @var class-string $classString */
|
/** @var class-string $classString */
|
||||||
@ -180,6 +204,6 @@ final class Reflection
|
|||||||
{
|
{
|
||||||
$docComment = preg_replace('#^\s*/\*\*([^/]+)/\s*$#', '$1', $reflection->getDocComment() ?: '');
|
$docComment = preg_replace('#^\s*/\*\*([^/]+)/\s*$#', '$1', $reflection->getDocComment() ?: '');
|
||||||
|
|
||||||
return preg_replace('/\s*\*\s*(\S*)/', '$1', $docComment); // @phpstan-ignore-line
|
return trim(preg_replace('/\s*\*\s*(\S*)/', "\n\$1", $docComment)); // @phpstan-ignore-line
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace CuyZ\Valinor\Tests\Unit\Utility\Reflection;
|
namespace CuyZ\Valinor\Tests\Unit\Utility\Reflection;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
use CuyZ\Valinor\Tests\Fake\FakeReflector;
|
use CuyZ\Valinor\Tests\Fake\FakeReflector;
|
||||||
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeIntersectionType;
|
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeIntersectionType;
|
||||||
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeUnionType;
|
use CuyZ\Valinor\Tests\Fixture\Object\ObjectWithPropertyWithNativeUnionType;
|
||||||
@ -11,8 +12,10 @@ use CuyZ\Valinor\Utility\Reflection\Reflection;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
use ReflectionFunction;
|
use ReflectionFunction;
|
||||||
|
use ReflectionParameter;
|
||||||
use ReflectionProperty;
|
use ReflectionProperty;
|
||||||
use ReflectionType;
|
use ReflectionType;
|
||||||
|
use Reflector;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use stdClass;
|
use stdClass;
|
||||||
|
|
||||||
@ -142,19 +145,17 @@ final class ReflectionTest extends TestCase
|
|||||||
self::assertSame('Countable&Iterator', Reflection::flattenType($type));
|
self::assertSame('Countable&Iterator', Reflection::flattenType($type));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_docblock_return_type_is_fetched_correctly(): void
|
/**
|
||||||
{
|
* @param non-empty-string $expectedType
|
||||||
$callable =
|
* @dataProvider callables_with_docblock_typed_return_type
|
||||||
/**
|
*/
|
||||||
* @return int
|
public function test_docblock_return_type_is_fetched_correctly(
|
||||||
*/
|
callable $dockblockTypedCallable,
|
||||||
static function () {
|
string $expectedType
|
||||||
return 42;
|
): void {
|
||||||
};
|
$type = Reflection::docBlockReturnType(new ReflectionFunction(Closure::fromCallable($dockblockTypedCallable)));
|
||||||
|
|
||||||
$type = Reflection::docBlockReturnType(new ReflectionFunction($callable));
|
self::assertSame($expectedType, $type);
|
||||||
|
|
||||||
self::assertSame('int', $type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_docblock_return_type_with_no_docblock_returns_null(): void
|
public function test_docblock_return_type_with_no_docblock_returns_null(): void
|
||||||
@ -166,4 +167,258 @@ final class ReflectionTest extends TestCase
|
|||||||
|
|
||||||
self::assertNull($type);
|
self::assertNull($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ReflectionParameter|ReflectionProperty $property
|
||||||
|
* @param non-empty-string $expectedType
|
||||||
|
* @dataProvider objects_with_docblock_typed_properties
|
||||||
|
*/
|
||||||
|
public function test_docblock_var_type_is_fetched_correctly(
|
||||||
|
Reflector $property,
|
||||||
|
string $expectedType
|
||||||
|
): void {
|
||||||
|
self::assertEquals($expectedType, Reflection::docBlockType($property));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<non-empty-string,array{0:callable,1:non-empty-string}>
|
||||||
|
*/
|
||||||
|
public function callables_with_docblock_typed_return_type(): iterable
|
||||||
|
{
|
||||||
|
yield 'phpdoc' => [
|
||||||
|
/** @return int */
|
||||||
|
fn () => 42,
|
||||||
|
'int',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpdoc followed by new line' => [
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fn () => 42,
|
||||||
|
'int',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpdoc literal string' => [
|
||||||
|
/** @return 'foo' */
|
||||||
|
fn () => 'foo',
|
||||||
|
'\'foo\'',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm' => [
|
||||||
|
/** @psalm-return int */
|
||||||
|
fn () => 42,
|
||||||
|
'int',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm trailing' => [
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
* @psalm-return positive-int
|
||||||
|
*/
|
||||||
|
fn () => 42,
|
||||||
|
'positive-int',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm leading' => [
|
||||||
|
/**
|
||||||
|
* @psalm-return positive-int
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
fn () => 42,
|
||||||
|
'positive-int',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan' => [
|
||||||
|
/** @phpstan-return int */
|
||||||
|
fn () => 42,
|
||||||
|
'int',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan trailing' => [
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
* @phpstan-return positive-int
|
||||||
|
*/
|
||||||
|
fn () => 42,
|
||||||
|
'positive-int',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan leading' => [
|
||||||
|
/**
|
||||||
|
* @phpstan-return positive-int
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
fn () => 42,
|
||||||
|
'positive-int',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return iterable<non-empty-string,array{0:ReflectionProperty|ReflectionParameter,1:non-empty-string}>
|
||||||
|
*/
|
||||||
|
public function objects_with_docblock_typed_properties(): iterable
|
||||||
|
{
|
||||||
|
yield 'phpdoc @var' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/** @var string */
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpdoc @var followed by new line' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm @var standalone' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/** @psalm-var string */
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm @var leading' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/**
|
||||||
|
* @psalm-var non-empty-string
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm @var trailing' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
* @psalm-var non-empty-string
|
||||||
|
*/
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan @var standalone' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/** @phpstan-var string */
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan @var leading' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/**
|
||||||
|
* @phpstan-var non-empty-string
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan @var trailing' => [
|
||||||
|
new ReflectionProperty(new class () {
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
* @phpstan-var non-empty-string
|
||||||
|
*/
|
||||||
|
public $foo;
|
||||||
|
}, 'foo'),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpdoc @param' => [
|
||||||
|
new ReflectionParameter(
|
||||||
|
/** @param string $string */
|
||||||
|
static function ($string): void {
|
||||||
|
},
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm @param standalone' => [
|
||||||
|
new ReflectionParameter(
|
||||||
|
/** @psalm-param string $string */
|
||||||
|
static function ($string): void {
|
||||||
|
},
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm @param leading' => [
|
||||||
|
new ReflectionParameter(
|
||||||
|
/**
|
||||||
|
* @psalm-param non-empty-string $string
|
||||||
|
* @param string $string
|
||||||
|
*/
|
||||||
|
static function ($string): void {
|
||||||
|
},
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'psalm @param trailing' => [
|
||||||
|
new ReflectionParameter(
|
||||||
|
/**
|
||||||
|
* @param string $string
|
||||||
|
* @psalm-param non-empty-string $string
|
||||||
|
*/
|
||||||
|
static function ($string): void {
|
||||||
|
},
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan @param standalone' => [
|
||||||
|
new ReflectionParameter(
|
||||||
|
/** @phpstan-param string $string */
|
||||||
|
static function ($string): void {
|
||||||
|
},
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan @param leading' => [
|
||||||
|
new ReflectionParameter(
|
||||||
|
/**
|
||||||
|
* @phpstan-param non-empty-string $string
|
||||||
|
* @param string $string
|
||||||
|
*/
|
||||||
|
static function ($string): void {
|
||||||
|
},
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
|
||||||
|
yield 'phpstan @param trailing' => [
|
||||||
|
new ReflectionParameter(
|
||||||
|
/**
|
||||||
|
* @param string $string
|
||||||
|
* @phpstan-param non-empty-string $string
|
||||||
|
*/
|
||||||
|
static function ($string): void {
|
||||||
|
},
|
||||||
|
'string',
|
||||||
|
),
|
||||||
|
'non-empty-string',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user