fix: properly handle static anonymous functions

The `MethodObjectBuilder` was incorrectly used when a registered
constructor is a static anonymous functions — it was handled like a
static method closure `Class::method(...)` and would yield errors like
this:

```
Error: Call to undefined method 
stdClass::CuyZ\Valinor\Tests\Integration\Mapping\{closure}()
```

PHP Reflection does not provide any way of telling static functions and
closures of static methods apart, other than checking for the name
`{closure}`. We check that `{closure}` is actually the last part of the
fully-qualified name, instead of just checking that the string ends with
`{closure}`.
This commit is contained in:
Eduardo Dobay 2022-09-24 15:01:53 -03:00 committed by GitHub
parent 0e8f12e5f7
commit c009ab98cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 61 additions and 2 deletions

View File

@ -22,6 +22,8 @@ final class FunctionDefinition
private bool $isStatic;
private bool $isClosure;
private Parameters $parameters;
private Type $returnType;
@ -36,6 +38,7 @@ final class FunctionDefinition
?string $fileName,
?string $class,
bool $isStatic,
bool $isClosure,
Parameters $parameters,
Type $returnType
) {
@ -45,6 +48,7 @@ final class FunctionDefinition
$this->fileName = $fileName;
$this->class = $class;
$this->isStatic = $isStatic;
$this->isClosure = $isClosure;
$this->parameters = $parameters;
$this->returnType = $returnType;
}
@ -82,6 +86,11 @@ final class FunctionDefinition
return $this->isStatic;
}
public function isClosure(): bool
{
return $this->isClosure;
}
/**
* @phpstan-return Parameters
* @return Parameters&ParameterDefinition[]

View File

@ -40,6 +40,7 @@ final class FunctionDefinitionCompiler implements CacheCompiler
$fileName = var_export($value->fileName(), true);
$class = var_export($value->class(), true);
$isStatic = var_export($value->isStatic(), true);
$isClosure = var_export($value->isClosure(), true);
$parameters = implode(', ', $parameters);
$returnType = $this->typeCompiler->compile($value->returnType());
@ -51,6 +52,7 @@ final class FunctionDefinitionCompiler implements CacheCompiler
$fileName,
$class,
$isStatic,
$isClosure,
new \CuyZ\Valinor\Definition\Parameters($parameters),
$returnType
)

View File

@ -12,6 +12,7 @@ use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
use CuyZ\Valinor\Type\Parser\Factory\Specifications\HandleClassGenericSpecification;
use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
use CuyZ\Valinor\Utility\Polyfill;
use CuyZ\Valinor\Utility\Reflection\Reflection;
use ReflectionFunction;
use ReflectionParameter;
@ -44,17 +45,20 @@ final class ReflectionFunctionDefinitionRepository implements FunctionDefinition
$reflection->getParameters()
);
$name = $reflection->getName();
$class = $reflection->getClosureScopeClass();
$returnType = $typeResolver->resolveType($reflection);
$isClosure = $name === '{closure}' || Polyfill::str_ends_with($name, '\\{closure}');
return new FunctionDefinition(
$reflection->getName(),
$name,
Reflection::signature($reflection),
$this->attributesRepository->for($reflection),
$reflection->getFileName() ?: null,
// @PHP 8.0 nullsafe operator
$class ? $class->name : null,
$reflection->getClosureThis() === null,
$isClosure,
new Parameters(...$parameters),
$returnType
);

View File

@ -83,7 +83,7 @@ final class ConstructorObjectBuilderFactory implements ObjectBuilderFactory
$definition = $function->definition();
$functionClass = $definition->class();
if ($functionClass && $definition->isStatic()) {
if ($functionClass && $definition->isStatic() && ! $definition->isClosure()) {
$builders[] = new MethodObjectBuilder($className, $definition->name(), $definition->parameters());
} else {
$builders[] = new FunctionObjectBuilder($function, $classType);

View File

@ -22,6 +22,7 @@ final class FakeFunctionDefinition
$fileName ?? 'foo/bar',
stdClass::class,
true,
true,
new Parameters(
new ParameterDefinition(
'bar',

View File

@ -35,6 +35,7 @@ final class FunctionDefinitionCompilerTest extends TestCase
'foo/bar',
stdClass::class,
true,
true,
new Parameters(
new ParameterDefinition(
'bar',
@ -57,6 +58,7 @@ final class FunctionDefinitionCompilerTest extends TestCase
self::assertSame('foo:42-1337', $compiledFunction->signature());
self::assertSame('foo/bar', $compiledFunction->fileName());
self::assertSame(true, $compiledFunction->isStatic());
self::assertSame(true, $compiledFunction->isClosure());
self::assertSame(stdClass::class, $compiledFunction->class());
self::assertTrue($compiledFunction->parameters()->has('bar'));
self::assertInstanceOf(NativeStringType::class, $compiledFunction->returnType());

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace CuyZ\Valinor\Tests\Integration\Mapping;
use Closure;
use CuyZ\Valinor\Mapper\MappingError;
use CuyZ\Valinor\Mapper\Object\DynamicConstructor;
use CuyZ\Valinor\Mapper\Object\Exception\CannotInstantiateObject;
@ -37,6 +38,38 @@ final class ConstructorRegistrationMappingTest extends IntegrationTest
self::assertSame($object, $result);
}
public function test_registered_static_anonymous_function_constructor_is_used(): void
{
$object = new stdClass();
try {
$result = (new MapperBuilder())
->registerConstructor(static fn (): stdClass => $object)
->mapper()
->map(stdClass::class, []);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertSame($object, $result);
}
public function test_registered_anonymous_function_from_static_scope_constructor_is_used(): void
{
$object = new stdClass();
try {
$result = (new MapperBuilder())
->registerConstructor(SomeClassProvidingStaticClosure::getConstructor($object))
->mapper()
->map(stdClass::class, []);
} catch (MappingError $error) {
$this->mappingFail($error);
}
self::assertSame($object, $result);
}
public function test_registered_anonymous_function_constructor_with_docblock_is_used(): void
{
$object = new stdClass();
@ -780,3 +813,11 @@ final class SomeClassWithBothInheritedStaticConstructors
public SomeOtherClassWithInheritedStaticConstructor $someOtherChild;
}
final class SomeClassProvidingStaticClosure
{
public static function getConstructor(stdClass $object): Closure
{
return fn (): stdClass => $object;
}
}