From 02fc0fb31939effe0f29115dddfedbd70b7b529e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 25 Aug 2026 10:21:54 +0800 Subject: [PATCH] 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 --- .../code/native-class-keyword-return-type.php | 6 -- .../code/native-class-to-array-parameters.php | 10 +++ phpunit/code/zend-class-to-array-magic.php | 14 ++++ phpunit/code/zend-class-to-array-missing.php | 10 +++ .../code/zend-class-to-array-parameters.php | 9 +++ .../code/zend-class-to-array-return-type.php | 9 +++ phpunit/src/ClassTest.php | 32 +++++++++ .../NativeClass/NativeClassValidationTest.php | 9 +++ src/NativeClass/NativeClassSupportTrait.php | 11 +-- src/Parser/MethodCallTrait.php | 14 ++++ src/Parser/UniversalMethodCall.php | 2 +- src/Preprocessor.php | 25 ++++++- .../toarray-dynamic-method.phpt | 70 +++++++++++++++++++ 13 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 phpunit/code/native-class-to-array-parameters.php create mode 100644 phpunit/code/zend-class-to-array-magic.php create mode 100644 phpunit/code/zend-class-to-array-missing.php create mode 100644 phpunit/code/zend-class-to-array-parameters.php create mode 100644 phpunit/code/zend-class-to-array-return-type.php create mode 100644 tests/compiler/object_property/toarray-dynamic-method.phpt diff --git a/phpunit/code/native-class-keyword-return-type.php b/phpunit/code/native-class-keyword-return-type.php index 2b944df0..037e2e83 100644 --- a/phpunit/code/native-class-keyword-return-type.php +++ b/phpunit/code/native-class-keyword-return-type.php @@ -8,9 +8,3 @@ class InvalidNativeConversion return ''; } } - -function main(): void -{ - $value = new InvalidNativeConversion(); - $value->toArray(); -} diff --git a/phpunit/code/native-class-to-array-parameters.php b/phpunit/code/native-class-to-array-parameters.php new file mode 100644 index 00000000..f949cb89 --- /dev/null +++ b/phpunit/code/native-class-to-array-parameters.php @@ -0,0 +1,10 @@ +toArray(); +} diff --git a/phpunit/code/zend-class-to-array-missing.php b/phpunit/code/zend-class-to-array-missing.php new file mode 100644 index 00000000..03dddc28 --- /dev/null +++ b/phpunit/code/zend-class-to-array-missing.php @@ -0,0 +1,10 @@ +toArray(); +} diff --git a/phpunit/code/zend-class-to-array-parameters.php b/phpunit/code/zend-class-to-array-parameters.php new file mode 100644 index 00000000..01436ea1 --- /dev/null +++ b/phpunit/code/zend-class-to-array-parameters.php @@ -0,0 +1,9 @@ +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); diff --git a/phpunit/src/NativeClass/NativeClassValidationTest.php b/phpunit/src/NativeClass/NativeClassValidationTest.php index fb36837d..6f50b4d9 100644 --- a/phpunit/src/NativeClass/NativeClassValidationTest.php +++ b/phpunit/src/NativeClass/NativeClassValidationTest.php @@ -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); diff --git a/src/NativeClass/NativeClassSupportTrait.php b/src/NativeClass/NativeClassSupportTrait.php index ac45227e..843c9c9a 100644 --- a/src/NativeClass/NativeClassSupportTrait.php +++ b/src/NativeClass/NativeClassSupportTrait.php @@ -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}`", ); } } diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 8b9d438b..730b7e6d 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -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); } diff --git a/src/Parser/UniversalMethodCall.php b/src/Parser/UniversalMethodCall.php index 03b18b0a..310c8e52 100644 --- a/src/Parser/UniversalMethodCall.php +++ b/src/Parser/UniversalMethodCall.php @@ -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 . ')', diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 5cfa2db1..8c6640af 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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(); diff --git a/tests/compiler/object_property/toarray-dynamic-method.phpt b/tests/compiler/object_property/toarray-dynamic-method.phpt new file mode 100644 index 00000000..a7f125c0 --- /dev/null +++ b/tests/compiler/object_property/toarray-dynamic-method.phpt @@ -0,0 +1,70 @@ +--TEST-- +Dynamic toArray() dispatch supports real methods and __call +--FILE-- + 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) +}