Skip to content

Commit

Permalink
TypeMatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
vudaltsov committed Feb 24, 2024
1 parent a849136 commit 82ae705
Show file tree
Hide file tree
Showing 13 changed files with 741 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/.github/ export-ignore
/dev/ export-ignore
/tests/ export-ignore
/var/ export-ignore
/.gitattributes export-ignore
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,20 @@ jobs:
with:
composer-options: --optimize-autoloader
- run: composer test -- --colors=always --order-by=random

type-matcher-validate:
runs-on: ubuntu-latest
strategy:
matrix:
php: [8.1]
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
tools: composer:v2
coverage: none
- uses: ramsey/composer-install@v2
with:
composer-options: --optimize-autoloader
- run: composer type-matcher-validate
1 change: 1 addition & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

$finder = Finder::create()
->in([
__DIR__ . '/dev',
__DIR__ . '/src',
__DIR__ . '/tests',
])
Expand Down
9 changes: 7 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
},
"autoload-dev": {
"psr-4": {
"Typhoon\\Type\\": "tests/"
"Typhoon\\Type\\": [
"dev/",
"tests/"
]
}
},
"config": {
Expand All @@ -44,6 +47,8 @@
"phpstan": "vendor/bin/phpstan --verbose",
"pre-command-run": "mkdir -p var",
"psalm": "psalm --show-info --no-diff",
"test": "phpunit"
"test": "phpunit",
"type-matcher-generate": "@php dev/TypeMatcherGenerator/generate.php",
"type-matcher-validate": "@php dev/TypeMatcherGenerator/validate.php"
}
}
29 changes: 29 additions & 0 deletions dev/TypeMatcherGenerator/Param.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Typhoon\Type\TypeMatcherGenerator;

final class Param
{
public function __construct(
public readonly string $name,
public readonly string $nativeType,
public readonly ?string $phpDocType,
) {}

public function arg(): string
{
return '$' . $this->name;
}

public function native(): string
{
return sprintf('%s $%s', $this->nativeType, $this->name);
}

public function phpDoc(): string
{
return sprintf('%s', $this->phpDocType ?? $this->nativeType);
}
}
16 changes: 16 additions & 0 deletions dev/TypeMatcherGenerator/Type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Typhoon\Type\TypeMatcherGenerator;

final class Type
{
/**
* @param list<Param> $params
*/
public function __construct(
public readonly string $name,
public readonly array $params,
) {}
}
128 changes: 128 additions & 0 deletions dev/TypeMatcherGenerator/TypeMatcherGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

namespace Typhoon\Type\TypeMatcherGenerator;

final class TypeMatcherGenerator
{
/**
* @param list<Type> $types
*/
public function generate(array $types): string
{
return <<<PHP
<?php
declare(strict_types=1);
namespace Typhoon\\Type;
/**
* @api
* @template-covariant TReturn
* @implements TypeVisitor<TReturn>
*/
final class TypeMatcher implements TypeVisitor
{
{$this->properties($types)}
/**
{$this->constructorPhpDocParams($types)}
* @param ?\\Closure(): TReturn \$default
*/
public function __construct(
{$this->constructorParams($types)}
?\\Closure \$default = null,
) {
{$this->constructorPropertyAssignments($types)}
}
{$this->methods($types)}
}
PHP;
}

/**
* @param list<Type> $types
*/
private function constructorParams(array $types): string
{
return implode("\n", array_map(
static fn(Type $type): string => " ?\\Closure \${$type->name} = null,",
$types,
));
}

/**
* @param list<Type> $types
*/
private function constructorPhpDocParams(array $types): string
{
return implode("\n", array_map(
fn(Type $type): string => " * @param ?{$this->phpDocClosure($type)} \${$type->name}",
$types,
));
}

/**
* @param list<Type> $types
*/
private function constructorPropertyAssignments(array $types): string
{
return implode("\n", array_map(
static fn(Type $type): string => " \$this->{$type->name} = \${$type->name} ?? \$default ?? throw new \\InvalidArgumentException('Either \${$type->name} or \$default must be provided.');",
$types,
));
}

/**
* @param list<Type> $types
*/
private function methods(array $types): string
{
return implode("\n\n", array_map(
static function (Type $type): string {
$params = implode(', ', array_map(
static fn(Param $param): string => $param->native(),
$type->params,
));
$args = implode(', ', array_map(
static fn(Param $param): string => $param->arg(),
$type->params,
));

return <<<PHP
public function {$type->name}({$params}): mixed
{
return \$this->{$type->name}->__invoke({$args}, \$this);
}
PHP;
},
$types,
));
}

private function phpDocClosure(Type $type): string
{
return sprintf('\Closure(%s, TypeVisitor): TReturn', implode(', ', array_map(
static fn(Param $param): string => $param->phpDoc(),
$type->params,
)));
}

/**
* @param list<Type> $types
*/
private function properties(array $types): string
{
return implode("\n\n", array_map(
fn(Type $type): string => <<<PHP
/** @var {$this->phpDocClosure($type)} */
private readonly \\Closure \${$type->name};
PHP,
$types,
));
}
}
65 changes: 65 additions & 0 deletions dev/TypeMatcherGenerator/TypeVisitorParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types=1);

namespace Typhoon\Type\TypeMatcherGenerator;

use Typhoon\Type\TypeVisitor;

final class TypeVisitorParser
{
/**
* @return list<Type>
*/
public function parse(): array
{
$types = [];
$class = new \ReflectionClass(TypeVisitor::class);

foreach ($class->getMethods() as $method) {
$phpDoc = $method->getDocComment() === false ? '' : $method->getDocComment();
$types[] = new Type($method->name, array_map(
fn(\ReflectionParameter $parameter): Param => new Param(
$parameter->name,
$this->nativeType($parameter->getType() ?? throw new \ReflectionException()),
$this->phpDocParamType($phpDoc, $parameter->name),
),
$method->getParameters(),
));
}

return $types;
}

private function nativeType(\ReflectionType $type): string
{
if ($type instanceof \ReflectionUnionType) {
return implode('|', array_map($this->nativeType(...), $type->getTypes()));
}

if ($type instanceof \ReflectionIntersectionType) {
return implode('&', array_map($this->nativeType(...), $type->getTypes()));
}

if (!$type instanceof \ReflectionNamedType) {
throw new \RuntimeException();
}

$name = $type->getName();
$lastSlash = strrpos($name, '\\');
$name = substr($name, $lastSlash === false ? 0 : $lastSlash + 1);

if ($type->allowsNull() && $name !== 'null' && $name !== 'mixed') {
$name = '?' . $name;
}

return $name;
}

private function phpDocParamType(string $phpDoc, string $name): ?string
{
preg_match("/@param (.+) \\\${$name}\\s/", $phpDoc, $matches);

return $matches[1] ?? null;
}
}
12 changes: 12 additions & 0 deletions dev/TypeMatcherGenerator/generate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Typhoon\Type\TypeMatcherGenerator;

require_once __DIR__ . '/../../vendor/autoload.php';

$types = (new TypeVisitorParser())->parse();
$code = (new TypeMatcherGenerator())->generate($types);

file_put_contents(__DIR__ . '/../../src/TypeMatcher.php', $code);
13 changes: 13 additions & 0 deletions dev/TypeMatcherGenerator/validate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Typhoon\Type\TypeMatcherGenerator;

require_once __DIR__ . '/../../vendor/autoload.php';

$types = (new TypeVisitorParser())->parse();
$code = (new TypeMatcherGenerator())->generate($types);

/** @psalm-suppress ForbiddenCode */
exit(file_get_contents(__DIR__ . '/../../src/TypeMatcher.php') === $code ? 0 : 1);
1 change: 1 addition & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ includes:
parameters:
level: 9
paths:
- dev
- src
- tests
checkGenericClassInNonGenericObjectType: false
Expand Down
1 change: 1 addition & 0 deletions psalm.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
</enableExtensions>

<projectFiles>
<directory name="dev"/>
<directory name="src"/>
<directory name="tests"/>
<ignoreFiles>
Expand Down
Loading

0 comments on commit 82ae705

Please sign in to comment.