test(parser): add comprehensive tests for toArray method validation

- Add test case for Zend class to array declaration with parameters rejection
- Add test case for Zend class to array declaration with wrong return type
- Add test case for known Zend class missing required toArray method
- Add test case for known Zend class resolving toArray through magic call
- Add test case for native class rejecting toArray parameters at declaration
- Update existing native class keyword return type test file
- Create new test file for native class to array parameters validation
- Add dynamic method test for toArray dispatch supporting real methods and __call
master
韩天峰 18 hours ago
parent 3def25cc3c
commit 02fc0fb319
  1. 6
      phpunit/code/native-class-keyword-return-type.php
  2. 10
      phpunit/code/native-class-to-array-parameters.php
  3. 14
      phpunit/code/zend-class-to-array-magic.php
  4. 10
      phpunit/code/zend-class-to-array-missing.php
  5. 9
      phpunit/code/zend-class-to-array-parameters.php
  6. 9
      phpunit/code/zend-class-to-array-return-type.php
  7. 32
      phpunit/src/ClassTest.php
  8. 9
      phpunit/src/NativeClass/NativeClassValidationTest.php
  9. 11
      src/NativeClass/NativeClassSupportTrait.php
  10. 14
      src/Parser/MethodCallTrait.php
  11. 2
      src/Parser/UniversalMethodCall.php
  12. 25
      src/Preprocessor.php
  13. 70
      tests/compiler/object_property/toarray-dynamic-method.phpt

@ -8,9 +8,3 @@ class InvalidNativeConversion
return '';
}
}
function main(): void
{
$value = new InvalidNativeConversion();
$value->toArray();
}

@ -0,0 +1,10 @@
<?php
#[Native]
class NativeToArrayWithParameters
{
public function toArray(int $mode = 0): array
{
return [];
}
}

@ -0,0 +1,14 @@
<?php
class ZendMagicToArray
{
public function __call(string $name, array $arguments): array
{
return [$name];
}
}
function main(): void
{
(new ZendMagicToArray())->toArray();
}

@ -0,0 +1,10 @@
<?php
class ZendWithoutToArray
{
}
function main(): void
{
(new ZendWithoutToArray())->toArray();
}

@ -0,0 +1,9 @@
<?php
class ZendToArrayWithParameters
{
public function toArray(int $mode = 0): array
{
return [];
}
}

@ -0,0 +1,9 @@
<?php
class ZendToArrayWrongReturn
{
public function toArray(): string
{
return '';
}
}

@ -2,6 +2,38 @@
class ClassTest extends \BaseTest
{
public function testZendToArrayDeclarationCannotAcceptParameters(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Conversion method `ZendToArrayWithParameters::toArray()` must not accept arguments',
);
$this->compile('zend-class-to-array-parameters.php');
}
public function testZendToArrayDeclarationMustReturnArray(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Conversion method `ZendToArrayWrongReturn::toArray()` must return exactly `array`',
);
$this->compile('zend-class-to-array-return-type.php');
}
public function testKnownZendClassMustDefineToArray(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Class `ZendWithoutToArray` must define `toArray()` for this conversion',
);
$this->compile('zend-class-to-array-missing.php');
}
public function testKnownZendClassMayResolveToArrayThroughMagicCall(): void
{
$this->compile('zend-class-to-array-magic.php');
}
public function testOrdinaryClassCannotDeclareToAnyKeywordMethod(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);

@ -540,6 +540,15 @@ final class NativeClassValidationTest extends \BaseTest
$this->compile('native-class-keyword-return-type.php');
}
public function testRejectsNativeToArrayParametersAtDeclaration(): void
{
$this->expectException(TestError::class);
$this->expectExceptionMessage(
'Native conversion method `NativeToArrayWithParameters::toArray()` must not accept arguments',
);
$this->compile('native-class-to-array-parameters.php');
}
public function testRejectsMissingNativeKeywordMethod(): void
{
$this->expectException(TestError::class);

@ -1383,25 +1383,28 @@ trait NativeClassSupportTrait
if ($methodDef === null) {
$this->fatalError($node, "Native class `{$class}` must define `{$method}()` for this conversion");
}
$this->assertNativeObjectKeywordMethodSignature(
$this->assertKeywordConversionMethodSignature(
$node,
$class,
$resolvedMethod,
$methodDef->functionDef,
$expectedType,
true,
);
return $resolvedMethod;
}
protected function assertNativeObjectKeywordMethodSignature(
protected function assertKeywordConversionMethodSignature(
NodeAbstract $node,
string $class,
string $method,
FunctionDef $function,
string $expectedType,
bool $nativeClass,
): void {
$kind = $nativeClass ? 'Native conversion method' : 'Conversion method';
if ($function->argInfoList !== []) {
$this->fatalError($node, "Native conversion method `{$class}::{$method}()` must not accept arguments");
$this->fatalError($node, "{$kind} `{$class}::{$method}()` must not accept arguments");
}
$hasExactReturnType = $function->returnType === $expectedType;
if ($expectedType === Type::VAR) {
@ -1431,7 +1434,7 @@ trait NativeClassSupportTrait
};
$this->fatalError(
$node,
"Native conversion method `{$class}::{$method}()` must return exactly `{$expectedTypeName}`",
"{$kind} `{$class}::{$method}()` must return exactly `{$expectedTypeName}`",
);
}
}

@ -554,6 +554,20 @@ trait MethodCallTrait
$useDeclaredToArray = $methodName === 'toArray'
&& $receiverClass !== ''
&& $this->objectTypeDeclaresMethod($receiverClass, $methodName);
$useMagicToArray = $methodName === 'toArray'
&& $receiverClass !== ''
&& $this->objectTypeDeclaresMethod($receiverClass, '__call');
if ($methodName === 'toArray'
&& $receiverClass !== ''
&& !$useDeclaredToArray
&& !$useMagicToArray
&& ($this->hasClass($receiverClass) || $this->isInternalClass($receiverClass))
) {
$this->fatalError(
$expr,
"Class `{$receiverClass}` must define `toArray()` for this conversion",
);
}
if (!$useDeclaredToArray) {
return $this->genToConvertCall($object, $methodName, $receiverType);
}

@ -448,7 +448,7 @@ trait UniversalMethodCall
'toFloat' => 'php::toFloat(' . $receiver . ')',
'toString' => 'php::toString(' . $receiver . ')',
'toBool' => 'php::toBool(' . $receiver . ')',
'toArray' => 'php::toArray(' . $receiver . ')',
'toArray' => 'php::callToArray(' . $receiver . ')',
'toStream' => 'php::toStream(' . $receiver . ')',
'toBigInt' => 'php::BigInt::newInstance(' . $receiver . ')',
'toBigFloat' => 'php::BigFloat::newInstance(' . $receiver . ')',

@ -1868,13 +1868,24 @@ class Preprocessor extends CompilerBase
$this->classDef->addAbstractMethod($name, $flags, $this->methodDef);
}
if ($this->classDef->nativeObject && strtolower($name) === 'toany') {
$this->assertNativeObjectKeywordMethodSignature(
$normalizedMethod = strtolower($name);
if ($this->classDef->nativeObject && $normalizedMethod === 'toany') {
$this->assertKeywordConversionMethodSignature(
$v,
$this->classDef->getNamespacedName(false),
$name,
$this->methodDef->functionDef,
Type::VAR,
true,
);
} elseif ($normalizedMethod === 'toarray') {
$this->assertKeywordConversionMethodSignature(
$v,
$this->classDef->getNamespacedName(false),
$name,
$this->methodDef->functionDef,
Type::ARRAY,
$this->classDef->nativeObject,
);
}
@ -2007,6 +2018,16 @@ class Preprocessor extends CompilerBase
$methodDef->node = $stmt;
$methodDef->functionDef = $this->parseFunctionDecl($stmt);
$methodDef->functionDef->method = true;
if (strtolower($methodName) === 'toarray') {
$this->assertKeywordConversionMethodSignature(
$stmt,
$this->interfaceDef->getNamespacedName(false),
$methodName,
$methodDef->functionDef,
Type::ARRAY,
false,
);
}
$this->interfaceDef->addMethod($methodDef);
$this->resetMethod();
$this->resetFunction();

@ -0,0 +1,70 @@
--TEST--
Dynamic toArray() dispatch supports real methods and __call
--FILE--
<?php
class DynamicToArrayValue
{
public function toArray(): array
{
return ['value' => 42];
}
}
class DynamicToArrayMagicOnly
{
public function __call(string $name, array $arguments): array
{
return ['magic' => $name];
}
}
function eraseToMixed(object $value): mixed
{
return $value;
}
function callDynamicToArray(mixed $value): array
{
return $value->toArray();
}
function dumpDynamicToArrayError(object $value): void
{
try {
callDynamicToArray(eraseToMixed($value));
} catch (Error $error) {
echo $error->getMessage(), "\n";
}
}
function main(): void
{
var_dump(callDynamicToArray(eraseToMixed(new DynamicToArrayValue())));
dumpDynamicToArrayError(new stdClass());
var_dump((new DynamicToArrayMagicOnly())->toArray());
var_dump(callDynamicToArray(eraseToMixed(new DynamicToArrayMagicOnly())));
$plain = new stdClass();
$plain->value = 7;
var_dump((array) $plain);
}
?>
--EXPECT--
array(1) {
["value"]=>
int(42)
}
Invalid callback stdClass::toArray, class stdClass does not have a method "toArray"
array(1) {
["magic"]=>
string(7) "toArray"
}
array(1) {
["magic"]=>
string(7) "toArray"
}
array(1) {
["value"]=>
int(7)
}
Loading…
Cancel
Save