parent
af0b6f9582
commit
981b8f54d7
91 changed files with 4887 additions and 61 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeClosureCapture |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
function makeNativeCapturingClosure(): Closure |
||||||
|
{ |
||||||
|
$native = new NativeClosureCapture(); |
||||||
|
return static function () use ($native): int { |
||||||
|
return 1; |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeClosureParameter |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
function makeNativeParameterClosure(): Closure |
||||||
|
{ |
||||||
|
return static function (NativeClosureParameter $native): int { |
||||||
|
return $native->value; |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeClosureReturn |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
function makeNativeReturningClosure(): Closure |
||||||
|
{ |
||||||
|
return static function () { |
||||||
|
return new NativeClosureReturn(); |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeDynamicMagicMethod |
||||||
|
{ |
||||||
|
public function __get(string $name): mixed |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeDynamicStaticMagicMethod |
||||||
|
{ |
||||||
|
public static function __callStatic(string $name, array $arguments): mixed |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInstanceofTarget |
||||||
|
{ |
||||||
|
public int $value = 0; |
||||||
|
} |
||||||
|
|
||||||
|
function testNativeInstanceof(string $class): bool |
||||||
|
{ |
||||||
|
$value = new NativeInstanceofTarget(); |
||||||
|
return $value instanceof $class; |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
interface NativeInterfaceArgumentContract |
||||||
|
{ |
||||||
|
public function value(): int; |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInterfaceArgumentValue implements NativeInterfaceArgumentContract |
||||||
|
{ |
||||||
|
public function value(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function consumeNativeInterfaceArgument(NativeInterfaceArgumentContract $value): int |
||||||
|
{ |
||||||
|
return $value->value(); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
consumeNativeInterfaceArgument(new NativeInterfaceArgumentValue()); |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
interface NativeInterfaceAssignmentContract |
||||||
|
{ |
||||||
|
public function value(): int; |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInterfaceAssignmentValue implements NativeInterfaceAssignmentContract |
||||||
|
{ |
||||||
|
public function value(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function replaceNativeInterfaceAssignment( |
||||||
|
NativeInterfaceAssignmentContract $target, |
||||||
|
NativeInterfaceAssignmentValue $value, |
||||||
|
): void { |
||||||
|
$target = $value; |
||||||
|
} |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
interface NativeInterfacePropertyContract |
||||||
|
{ |
||||||
|
public function value(): int; |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInterfacePropertyValue implements NativeInterfacePropertyContract |
||||||
|
{ |
||||||
|
public function value(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInterfacePropertyHolder |
||||||
|
{ |
||||||
|
public NativeInterfacePropertyContract $value; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$holder = new NativeInterfacePropertyHolder(); |
||||||
|
$holder->value = new NativeInterfacePropertyValue(); |
||||||
|
} |
||||||
@ -0,0 +1,20 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
interface NativeInterfaceReturnContract |
||||||
|
{ |
||||||
|
public function value(): int; |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInterfaceReturnValue implements NativeInterfaceReturnContract |
||||||
|
{ |
||||||
|
public function value(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function makeNativeInterfaceReturn(): NativeInterfaceReturnContract |
||||||
|
{ |
||||||
|
return new NativeInterfaceReturnValue(); |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeMissingCountable implements Countable |
||||||
|
{ |
||||||
|
} |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeNarrowArrayAccess implements ArrayAccess |
||||||
|
{ |
||||||
|
public function offsetExists(string $offset): bool |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public function offsetGet(mixed $offset): mixed |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public function offsetSet(mixed $offset, mixed $value): void |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function offsetUnset(mixed $offset): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeProtectedCountable implements Countable |
||||||
|
{ |
||||||
|
protected function count(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeJsonValue |
||||||
|
{ |
||||||
|
public function toArray(): array |
||||||
|
{ |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeJsonValue(); |
||||||
|
json_encode($value); |
||||||
|
} |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class MissingNativeConversion |
||||||
|
{ |
||||||
|
public int $value; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new MissingNativeConversion(); |
||||||
|
$value->toInt(); |
||||||
|
} |
||||||
@ -0,0 +1,16 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class InvalidNativeConversion |
||||||
|
{ |
||||||
|
public function toArray(): string |
||||||
|
{ |
||||||
|
return ''; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new InvalidNativeConversion(); |
||||||
|
$value->toArray(); |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeMixedReturn {} |
||||||
|
|
||||||
|
function makeNativeMixed(): mixed |
||||||
|
{ |
||||||
|
return new NativeMixedReturn(); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void {} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeArrayElement |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
function storeNativeInArray(): array |
||||||
|
{ |
||||||
|
return [new NativeArrayElement()]; |
||||||
|
} |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativePropertyValue |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
function storeNativeInPhpProperty(): void |
||||||
|
{ |
||||||
|
$phpObject = new stdClass(); |
||||||
|
$phpObject->value = new NativePropertyValue(); |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeWithReadonlyProperty |
||||||
|
{ |
||||||
|
public readonly int $value; |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeWithStaticMethod |
||||||
|
{ |
||||||
|
public static function create(): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeStaticProperty |
||||||
|
{ |
||||||
|
public static int $value = 1; |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeWithStdContainer |
||||||
|
{ |
||||||
|
public std\vector $values; |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
trait NativeDynamicMagicTrait |
||||||
|
{ |
||||||
|
public function __serialize(): array |
||||||
|
{ |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeTraitDynamicMagicMethod |
||||||
|
{ |
||||||
|
use NativeDynamicMagicTrait; |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
trait NativeStaticMethodTrait |
||||||
|
{ |
||||||
|
public static function value(): int |
||||||
|
{ |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeTraitStaticMethod |
||||||
|
{ |
||||||
|
use NativeStaticMethodTrait; |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeUnionMember |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
function invalidNativeUnion(NativeUnionMember|string $value): void |
||||||
|
{ |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeUntypedProperty |
||||||
|
{ |
||||||
|
public $value; |
||||||
|
} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeUntypedReturn {} |
||||||
|
|
||||||
|
function makeNativeUntyped() |
||||||
|
{ |
||||||
|
return new NativeUntypedReturn(); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void {} |
||||||
@ -0,0 +1,14 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeConstructorArgument |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
class ZendBackedConstructor |
||||||
|
{ |
||||||
|
public function __construct(NativeConstructorArgument $native) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
class ZendBackedParent {} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInvalidChild extends ZendBackedParent {} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativePropertyType |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
} |
||||||
|
|
||||||
|
class ZendObjectWithNativeProperty |
||||||
|
{ |
||||||
|
public NativePropertyType $value; |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhp\Tests\NativeClass; |
||||||
|
|
||||||
|
use PhpParser\Node\Stmt\Class_; |
||||||
|
use PhpParser\NodeTraverser; |
||||||
|
use PhpParser\NodeVisitor\NameResolver; |
||||||
|
use PhpParser\ParserFactory; |
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use TypePhp\Transform\NativeClassAttributeLowering; |
||||||
|
|
||||||
|
final class NativeClassAttributeLoweringTest extends TestCase |
||||||
|
{ |
||||||
|
public function testConsumesNativeAttributeAndMarksNamedClass(): void |
||||||
|
{ |
||||||
|
$parser = (new ParserFactory())->createForNewestSupportedVersion(); |
||||||
|
$nodes = $parser->parse('<?php #[Native] class Point {}');
|
||||||
|
$traverser = new NodeTraverser(); |
||||||
|
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); |
||||||
|
$traverser->addVisitor(new NativeClassAttributeLowering()); |
||||||
|
$nodes = $traverser->traverse($nodes); |
||||||
|
|
||||||
|
$this->assertInstanceOf(Class_::class, $nodes[0]); |
||||||
|
$this->assertTrue(NativeClassAttributeLowering::isNative($nodes[0])); |
||||||
|
$this->assertSame([], $nodes[0]->attrGroups); |
||||||
|
} |
||||||
|
|
||||||
|
public function testLeavesOrdinaryClassUnmarked(): void |
||||||
|
{ |
||||||
|
$parser = (new ParserFactory())->createForNewestSupportedVersion(); |
||||||
|
$nodes = $parser->parse('<?php class Point {}'); |
||||||
|
$traverser = new NodeTraverser(); |
||||||
|
$traverser->addVisitor(new NativeClassAttributeLowering()); |
||||||
|
$nodes = $traverser->traverse($nodes); |
||||||
|
|
||||||
|
$this->assertFalse(NativeClassAttributeLowering::isNative($nodes[0])); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,246 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace TypePhp\Tests\NativeClass; |
||||||
|
|
||||||
|
use TypePhp\Exception\TestError; |
||||||
|
|
||||||
|
final class NativeClassValidationTest extends \BaseTest |
||||||
|
{ |
||||||
|
public function testRejectsUntypedProperty(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native class properties must declare a type'); |
||||||
|
$this->compile('native-class-untyped-property.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsStaticProperty(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native class static properties are not supported'); |
||||||
|
$this->compile('native-class-static-property.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsInheritanceAcrossObjectModels(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native and ZendVM-backed classes cannot inherit from each other'); |
||||||
|
$this->compile('native-class-zend-inheritance.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsStaticMethod(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native class static methods are not supported'); |
||||||
|
$this->compile('native-class-static-method.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsReadonlyPropertyUntilNativeWriteStateIsImplemented(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native class readonly properties are not supported'); |
||||||
|
$this->compile('native-class-readonly-property.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsStdContainerProperty(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native class properties cannot use Std Container types'); |
||||||
|
$this->compile('native-class-std-container-property.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsDynamicInstanceofBecauseNativeClassesHaveNoRuntimeTypeLookup(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Dynamic instanceof is not supported for native objects'); |
||||||
|
$this->compile('native-class-instanceof.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectStoredInPhpArray(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot be stored in PHP arrays'); |
||||||
|
$this->compile('native-class-php-array.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectStoredInPhpObjectProperty(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot be stored in PHP arrays, PHP object properties'); |
||||||
|
$this->compile('native-class-php-property.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeTypedPropertyOnZendObject(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native object types can only be used as properties of native classes'); |
||||||
|
$this->compile('native-class-zend-native-property.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectCapturedByClosure(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot be captured by Zend closures'); |
||||||
|
$this->compile('native-class-closure-capture.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectClosureParameter(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Zend closures cannot declare native object parameters or return types'); |
||||||
|
$this->compile('native-class-closure-parameter.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectReturnedByUntypedClosure(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Zend closures cannot return native objects'); |
||||||
|
$this->compile('native-class-closure-return.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectParameterOnZendConstructor(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Zend-backed constructors cannot accept or return native objects'); |
||||||
|
$this->compile('native-class-zend-constructor.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsUnsupportedNativeObjectUnion(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native object types cannot be combined with other union or intersection members'); |
||||||
|
$this->compile('native-class-union-signature.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsIncorrectNativeKeywordReturnType(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('must return exactly `array`'); |
||||||
|
$this->compile('native-class-keyword-return-type.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsMissingNativeKeywordMethod(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('must define `toInt()`'); |
||||||
|
$this->compile('native-class-keyword-missing.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectPassedToJsonEncode(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot cross a dynamic PHP/ZendVM call boundary'); |
||||||
|
$this->compile('native-class-json-encode.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectFromUntypedReturn(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native object return values require an explicit native class return type'); |
||||||
|
$this->compile('native-class-untyped-return.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectFromMixedReturn(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native object return values require an explicit native class return type'); |
||||||
|
$this->compile('native-class-mixed-return.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectPassedToInterfaceParameter(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot be converted to interface `NativeInterfaceArgumentContract`'); |
||||||
|
$this->compile('native-class-interface-argument.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectAssignedToInterfaceVariable(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot be assigned to interface-typed variables'); |
||||||
|
$this->compile('native-class-interface-assignment.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectAssignedToInterfaceProperty(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot be assigned to interface-typed properties'); |
||||||
|
$this->compile('native-class-interface-property.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNativeObjectReturnedAsInterface(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native objects cannot be returned as interface `NativeInterfaceReturnContract`'); |
||||||
|
$this->compile('native-class-interface-return.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsDynamicMagicMethod(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__get()`'); |
||||||
|
$this->compile('native-class-dynamic-magic-method.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsDynamicStaticMagicMethodBeforeGenericStaticDiagnostic(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__callStatic()`'); |
||||||
|
$this->compile('native-class-dynamic-static-magic-method.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsDynamicMagicMethodInjectedByTrait(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native classes do not support dynamic magic method `__serialize()`'); |
||||||
|
$this->compile('native-class-trait-dynamic-magic-method.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testDynamicMagicMethodDenyListIsComplete(): void |
||||||
|
{ |
||||||
|
$trait = new \ReflectionClass(\TypePhp\NativeClass\NativeClassSupportTrait::class); |
||||||
|
$constant = $trait->getReflectionConstant('UNSUPPORTED_NATIVE_MAGIC_METHODS'); |
||||||
|
$this->assertNotFalse($constant); |
||||||
|
$this->assertSame([ |
||||||
|
'__call', |
||||||
|
'__callstatic', |
||||||
|
'__get', |
||||||
|
'__set', |
||||||
|
'__isset', |
||||||
|
'__unset', |
||||||
|
'__sleep', |
||||||
|
'__wakeup', |
||||||
|
'__serialize', |
||||||
|
'__unserialize', |
||||||
|
'__set_state', |
||||||
|
'__debuginfo', |
||||||
|
], array_keys($constant->getValue())); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsStaticMethodInjectedByTrait(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('Native class static methods are not supported'); |
||||||
|
$this->compile('native-class-trait-static-method.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsMissingInternalInterfaceMethod(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('must implement method `Countable::count()`'); |
||||||
|
$this->compile('native-class-internal-interface-missing-method.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNonPublicInternalInterfaceMethod(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('must be compatible with `Countable::count()`'); |
||||||
|
$this->compile('native-class-internal-interface-visibility.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testRejectsNarrowedInternalInterfaceParameter(): void |
||||||
|
{ |
||||||
|
$this->expectException(TestError::class); |
||||||
|
$this->expectExceptionMessage('must be compatible with `ArrayAccess::offsetExists()`'); |
||||||
|
$this->compile('native-class-internal-interface-parameter.php'); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,866 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\NativeClass; |
||||||
|
|
||||||
|
use TypePhp\Entity\ArgInfo; |
||||||
|
use TypePhp\Entity\ClassDef; |
||||||
|
use TypePhp\Entity\FunctionDef; |
||||||
|
use TypePhp\Entity\MethodDef; |
||||||
|
use TypePhp\Entity\PropertyDef; |
||||||
|
use TypePhp\Resolver\Reflection; |
||||||
|
use PhpParser\Modifiers; |
||||||
|
use TypePhp\Type; |
||||||
|
use PhpParser\NodeAbstract; |
||||||
|
use PhpParser\Node; |
||||||
|
|
||||||
|
trait NativeClassSupportTrait |
||||||
|
{ |
||||||
|
/** |
||||||
|
* Magic methods whose semantics require Zend object handlers, runtime |
||||||
|
* method resolution, dynamic properties, or Zend serialization state. |
||||||
|
* Native classes deliberately have none of those runtime facilities. |
||||||
|
*/ |
||||||
|
private const UNSUPPORTED_NATIVE_MAGIC_METHODS = [ |
||||||
|
'__call' => true, |
||||||
|
'__callstatic' => true, |
||||||
|
'__get' => true, |
||||||
|
'__set' => true, |
||||||
|
'__isset' => true, |
||||||
|
'__unset' => true, |
||||||
|
'__sleep' => true, |
||||||
|
'__wakeup' => true, |
||||||
|
'__serialize' => true, |
||||||
|
'__unserialize' => true, |
||||||
|
'__set_state' => true, |
||||||
|
'__debuginfo' => true, |
||||||
|
]; |
||||||
|
|
||||||
|
protected function assertNativeMagicMethodSupported(NodeAbstract $node, string $method): void |
||||||
|
{ |
||||||
|
if (!$this->classDef?->nativeObject |
||||||
|
|| !isset(self::UNSUPPORTED_NATIVE_MAGIC_METHODS[strtolower($method)]) |
||||||
|
) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$this->fatalError( |
||||||
|
$node, |
||||||
|
"Native classes do not support dynamic magic method `{$method}()`", |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Native classes have no zend_class_entry, so Zend cannot perform the |
||||||
|
* normal MINIT-time interface verification for them. Convert an internal |
||||||
|
* reflection signature into the compiler's existing MethodDef model and |
||||||
|
* run the same compatibility checker used for project interfaces. |
||||||
|
*/ |
||||||
|
protected function checkInternalInterfaceImplementation( |
||||||
|
NodeAbstract $node, |
||||||
|
ClassDef $classDef, |
||||||
|
string $interfaceName, |
||||||
|
): void { |
||||||
|
$interface = Reflection::getClass($interfaceName); |
||||||
|
if ($interface === null) { |
||||||
|
$this->fatalError($node, "Internal interface `{$interfaceName}` is not available"); |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($interface->getMethods() as $method) { |
||||||
|
$childMethodDef = $this->findClassMethodDef($classDef, $method->getName(), $classDef->isAbstract()); |
||||||
|
if ($childMethodDef === null) { |
||||||
|
if ($classDef->isAbstract()) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$this->fatalError( |
||||||
|
$node, |
||||||
|
"Class `{$classDef->getNamespacedName(false)}` must implement method " . |
||||||
|
"`{$interfaceName}::{$method->getName()}()`", |
||||||
|
); |
||||||
|
} |
||||||
|
$this->validateMethodOverrideSignature( |
||||||
|
$childMethodDef->node ?? $node, |
||||||
|
$method->getName(), |
||||||
|
$childMethodDef, |
||||||
|
$this->createInternalInterfaceMethodDef($method), |
||||||
|
$interfaceName, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function createInternalInterfaceMethodDef(\ReflectionMethod $method): MethodDef |
||||||
|
{ |
||||||
|
$flags = Modifiers::PUBLIC | Modifiers::ABSTRACT; |
||||||
|
if ($method->isStatic()) { |
||||||
|
$flags |= Modifiers::STATIC; |
||||||
|
} |
||||||
|
|
||||||
|
$methodDef = new MethodDef($flags, $method->getName()); |
||||||
|
$functionDef = new FunctionDef($method->getName(), Type::VAR, ''); |
||||||
|
$functionDef->method = true; |
||||||
|
$functionDef->declaringClass = $method->getDeclaringClass()->getName(); |
||||||
|
$functionDef->returnsByRef = $method->returnsReference(); |
||||||
|
$functionDef->returnTypeUndeclared = $method->getReturnType() === null; |
||||||
|
$this->applyReflectedReturnType($functionDef, $method->getReturnType(), $method->getDeclaringClass()); |
||||||
|
|
||||||
|
foreach ($method->getParameters() as $parameter) { |
||||||
|
$argument = new ArgInfo(); |
||||||
|
$argument->name = $parameter->getName(); |
||||||
|
$argument->phpName = $parameter->getName(); |
||||||
|
$argument->byRef = $parameter->isPassedByReference(); |
||||||
|
$argument->variadic = $parameter->isVariadic(); |
||||||
|
$argument->undeclared = $parameter->getType() === null; |
||||||
|
$argument->nullable = $parameter->allowsNull(); |
||||||
|
$this->applyReflectedParameterType($argument, $parameter->getType(), $method->getDeclaringClass()); |
||||||
|
if ($parameter->isOptional() || $parameter->isVariadic()) { |
||||||
|
// Compatibility only needs to distinguish required from |
||||||
|
// optional parameters; the concrete default is irrelevant. |
||||||
|
$argument->defaultValue = new Node\Expr\ConstFetch(new Node\Name('null')); |
||||||
|
} |
||||||
|
$functionDef->argInfoList[] = $argument; |
||||||
|
} |
||||||
|
$functionDef->argCountRequired = $method->getNumberOfRequiredParameters(); |
||||||
|
$methodDef->functionDef = $functionDef; |
||||||
|
return $methodDef; |
||||||
|
} |
||||||
|
|
||||||
|
private function applyReflectedReturnType( |
||||||
|
FunctionDef $function, |
||||||
|
?\ReflectionType $type, |
||||||
|
\ReflectionClass $declaringClass, |
||||||
|
): void { |
||||||
|
if ($type === null) { |
||||||
|
$function->returnTypeStr = ''; |
||||||
|
return; |
||||||
|
} |
||||||
|
$node = $this->reflectionTypeToNode($type, $declaringClass); |
||||||
|
$function->returnTypeStr = $this->typeCheckNodeToString($node); |
||||||
|
if ($node instanceof Node\NullableType |
||||||
|
|| $node instanceof Node\UnionType |
||||||
|
|| $node instanceof Node\IntersectionType |
||||||
|
) { |
||||||
|
$typeInfo = $this->buildTypeCheckFromNode($node); |
||||||
|
$function->returnType = Type::VAR; |
||||||
|
$function->returnTypeCheck = $typeInfo['check']; |
||||||
|
$function->returnTypeNode = $node; |
||||||
|
return; |
||||||
|
} |
||||||
|
[$function->returnType, $function->returnClass] = $this->resolveReflectedNamedType($node); |
||||||
|
} |
||||||
|
|
||||||
|
private function applyReflectedParameterType( |
||||||
|
ArgInfo $argument, |
||||||
|
?\ReflectionType $type, |
||||||
|
\ReflectionClass $declaringClass, |
||||||
|
): void { |
||||||
|
if ($type === null) { |
||||||
|
$argument->type = Type::VAR; |
||||||
|
return; |
||||||
|
} |
||||||
|
$node = $this->reflectionTypeToNode($type, $declaringClass); |
||||||
|
$argument->typeStr = $this->typeCheckNodeToString($node); |
||||||
|
if ($node instanceof Node\NullableType |
||||||
|
|| $node instanceof Node\UnionType |
||||||
|
|| $node instanceof Node\IntersectionType |
||||||
|
) { |
||||||
|
$typeInfo = $this->buildTypeCheckFromNode($node); |
||||||
|
$argument->type = Type::VAR; |
||||||
|
$argument->typeCheck = $typeInfo['check']; |
||||||
|
$argument->typeNode = $node; |
||||||
|
return; |
||||||
|
} |
||||||
|
[$argument->type, $argument->declaredClass] = $this->resolveReflectedNamedType($node); |
||||||
|
if ($argument->declaredClass !== '' && !$this->isInterface($argument->declaredClass)) { |
||||||
|
$argument->class = $argument->declaredClass; |
||||||
|
} |
||||||
|
$argument->explicitMixed = strtolower($argument->typeStr) === 'mixed'; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{string, string} */ |
||||||
|
private function resolveReflectedNamedType(NodeAbstract $node): array |
||||||
|
{ |
||||||
|
$name = $this->parseIdentifier($node); |
||||||
|
$lower = strtolower(ltrim($name, '\\')); |
||||||
|
if (isset($this->zendTypeMap[$lower])) { |
||||||
|
return [$this->getTypeFromZendType($lower), '']; |
||||||
|
} |
||||||
|
return [Type::OBJECT, ltrim($name, '\\')]; |
||||||
|
} |
||||||
|
|
||||||
|
private function reflectionTypeToNode( |
||||||
|
\ReflectionType $type, |
||||||
|
\ReflectionClass $declaringClass, |
||||||
|
bool $allowNullableWrapper = true, |
||||||
|
): NodeAbstract { |
||||||
|
if ($type instanceof \ReflectionUnionType) { |
||||||
|
return new Node\UnionType(array_map( |
||||||
|
fn (\ReflectionType $member): NodeAbstract => |
||||||
|
$this->reflectionTypeToNode($member, $declaringClass, false), |
||||||
|
$type->getTypes(), |
||||||
|
)); |
||||||
|
} |
||||||
|
if ($type instanceof \ReflectionIntersectionType) { |
||||||
|
return new Node\IntersectionType(array_map( |
||||||
|
fn (\ReflectionType $member): NodeAbstract => |
||||||
|
$this->reflectionTypeToNode($member, $declaringClass, false), |
||||||
|
$type->getTypes(), |
||||||
|
)); |
||||||
|
} |
||||||
|
|
||||||
|
/** @var \ReflectionNamedType $type */ |
||||||
|
$name = $type->getName(); |
||||||
|
$lower = strtolower($name); |
||||||
|
if ($lower === 'self') { |
||||||
|
$node = new Node\Name\FullyQualified($declaringClass->getName()); |
||||||
|
} elseif ($lower === 'parent') { |
||||||
|
$parent = $declaringClass->getParentClass(); |
||||||
|
$node = $parent === false |
||||||
|
? new Node\Name('parent') |
||||||
|
: new Node\Name\FullyQualified($parent->getName()); |
||||||
|
} elseif ($lower === 'static') { |
||||||
|
$node = new Node\Name('static'); |
||||||
|
} elseif ($type->isBuiltin()) { |
||||||
|
$node = new Node\Identifier($name); |
||||||
|
} else { |
||||||
|
$node = new Node\Name\FullyQualified($name); |
||||||
|
} |
||||||
|
|
||||||
|
if ($allowNullableWrapper |
||||||
|
&& $type->allowsNull() |
||||||
|
&& !in_array($lower, ['mixed', 'null'], true) |
||||||
|
) { |
||||||
|
return new Node\NullableType($node); |
||||||
|
} |
||||||
|
return $node; |
||||||
|
} |
||||||
|
|
||||||
|
protected function isNativeObjectClass(string $class): bool |
||||||
|
{ |
||||||
|
$class = ltrim($class, '\\'); |
||||||
|
return $class !== '' && $this->hasClass($class) && $this->getClass($class)->nativeObject; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectCppName(string|ClassDef $class): string |
||||||
|
{ |
||||||
|
$classDef = $class instanceof ClassDef ? $class : $this->getClass(ltrim($class, '\\')); |
||||||
|
return self::PREFIX . $this->getNativeName('', $classDef->namespace, $classDef->name); |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectDescriptorName(string|ClassDef $class): string |
||||||
|
{ |
||||||
|
return $this->getNativeObjectCppName($class) . '__type'; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectPointerType(string|ClassDef $class): string |
||||||
|
{ |
||||||
|
return $this->getNativeObjectCppName($class) . ' *'; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectArgumentType(ArgInfo $argument): ?string |
||||||
|
{ |
||||||
|
$class = $argument->declaredClass ?: $argument->class; |
||||||
|
if ((!$argument->byRef && $argument->type !== Type::OBJECT) |
||||||
|
|| !$this->isNativeObjectClass($class) |
||||||
|
) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return $this->getNativeObjectPointerType($class) . ($argument->byRef ? '&' : ''); |
||||||
|
} |
||||||
|
|
||||||
|
protected function resolveNullableNativeObjectType(?NodeAbstract $type, int $declarationKind): ?array |
||||||
|
{ |
||||||
|
$inner = null; |
||||||
|
if ($type instanceof Node\NullableType) { |
||||||
|
$inner = $type->type; |
||||||
|
} elseif ($type instanceof Node\UnionType && count($type->types) === 2) { |
||||||
|
foreach ($type->types as $member) { |
||||||
|
if ($member instanceof Node\Identifier && strtolower($member->toString()) === 'null') { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($inner !== null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$inner = $member; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!$inner instanceof Node\Name) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
[$innerType, $class] = $this->resolveTypeDecl($inner, $declarationKind); |
||||||
|
if ($innerType !== Type::OBJECT || !$this->isNativeObjectClass($class)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return [Type::OBJECT, $class]; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<string> */ |
||||||
|
protected function getNativeObjectClassesFromTypeNode(?NodeAbstract $type, int $declarationKind): array |
||||||
|
{ |
||||||
|
if ($type === null || $type instanceof Node\Identifier) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
if ($type instanceof Node\NullableType) { |
||||||
|
return $this->getNativeObjectClassesFromTypeNode($type->type, $declarationKind); |
||||||
|
} |
||||||
|
if ($type instanceof Node\UnionType || $type instanceof Node\IntersectionType) { |
||||||
|
$classes = []; |
||||||
|
foreach ($type->types as $member) { |
||||||
|
foreach ($this->getNativeObjectClassesFromTypeNode($member, $declarationKind) as $class) { |
||||||
|
$classes[strtolower($class)] = $class; |
||||||
|
} |
||||||
|
} |
||||||
|
return array_values($classes); |
||||||
|
} |
||||||
|
if (!$type instanceof Node\Name) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
[$resolvedType, $class] = $this->resolveTypeDecl($type, $declarationKind); |
||||||
|
return $resolvedType === Type::OBJECT && $this->isNativeObjectClass($class) ? [$class] : []; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve a common Native pointer type for value-selection branches. |
||||||
|
* Null is accepted as the empty state; any Zend/non-object branch makes |
||||||
|
* the expression unsuitable for the Native object model. |
||||||
|
* |
||||||
|
* @param list<NodeAbstract> $expressions |
||||||
|
*/ |
||||||
|
protected function getCommonNativeObjectExpressionClass(array $expressions): string |
||||||
|
{ |
||||||
|
$common = ''; |
||||||
|
foreach ($expressions as $expression) { |
||||||
|
if ($this->isNull($expression)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$class = $this->detectClassOfExpr($expression); |
||||||
|
if (!$this->isNativeObjectClass($class)) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
if ($common === '') { |
||||||
|
$common = $class; |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($this->isObjectClassStaticallyAssignableTo($class, $common)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($this->isObjectClassStaticallyAssignableTo($common, $class)) { |
||||||
|
$common = $class; |
||||||
|
continue; |
||||||
|
} |
||||||
|
return ''; |
||||||
|
} |
||||||
|
return $common; |
||||||
|
} |
||||||
|
|
||||||
|
protected function assertSupportedNativeObjectTypeNode( |
||||||
|
?NodeAbstract $type, |
||||||
|
int $declarationKind, |
||||||
|
NodeAbstract $errorNode, |
||||||
|
): void { |
||||||
|
if (!$type instanceof Node\UnionType && !$type instanceof Node\IntersectionType) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$nativeClasses = $this->getNativeObjectClassesFromTypeNode($type, $declarationKind); |
||||||
|
if ($nativeClasses !== [] && $this->resolveNullableNativeObjectType($type, $declarationKind) === null) { |
||||||
|
$this->fatalError( |
||||||
|
$errorNode, |
||||||
|
'Native object types cannot be combined with other union or intersection members', |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectReturnType(FunctionDef $function): ?string |
||||||
|
{ |
||||||
|
if ($function->returnType !== Type::OBJECT || !$this->isNativeObjectClass($function->returnClass)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return $this->getNativeObjectPointerType($function->returnClass); |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectMethodThisType(FunctionDef $function): ?string |
||||||
|
{ |
||||||
|
if (!$function->method || !$this->isNativeObjectClass($function->declaringClass)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return $this->getNativeObjectCppName($function->declaringClass) . ' &'; |
||||||
|
} |
||||||
|
|
||||||
|
protected function functionUsesNativeObject(FunctionDef $function): bool |
||||||
|
{ |
||||||
|
if ($this->getNativeObjectReturnType($function) !== null |
||||||
|
|| $this->getNativeObjectMethodThisType($function) !== null |
||||||
|
) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
foreach ($function->argInfoList as $argument) { |
||||||
|
if ($this->getNativeObjectArgumentType($argument) !== null) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
protected function genNativeObjectParameterChecks(FunctionDef $function): string |
||||||
|
{ |
||||||
|
$code = ''; |
||||||
|
foreach ($function->argInfoList as $argument) { |
||||||
|
$class = $argument->declaredClass ?: $argument->class; |
||||||
|
if (!$argument->nullable && $this->isNativeObjectClass($class)) { |
||||||
|
$code .= $this->getIndent() . 'php::nativeGcRequireObject(' |
||||||
|
. $argument->name . ', "' . addslashes($class) . '");' . PHP_EOL; |
||||||
|
} |
||||||
|
} |
||||||
|
return $code; |
||||||
|
} |
||||||
|
|
||||||
|
protected function addNativeObject(string $name, string $class): void |
||||||
|
{ |
||||||
|
$this->context->nativeObjects[$name] = ltrim($class, '\\'); |
||||||
|
$this->context->objects[$name] = ltrim($class, '\\'); |
||||||
|
} |
||||||
|
|
||||||
|
protected function isNativeObjectVar(string $name): bool |
||||||
|
{ |
||||||
|
return isset($this->context->nativeObjects[$name]); |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectVarClass(string $name): string |
||||||
|
{ |
||||||
|
return $this->context->nativeObjects[$name] ?? ''; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectReceiver(string $name): string |
||||||
|
{ |
||||||
|
if ($name === 'this_') { |
||||||
|
return 'this_'; |
||||||
|
} |
||||||
|
$class = $this->getNativeObjectVarClass($name); |
||||||
|
return 'php::nativeDeref(' . $name . ', "' . addslashes($class) . '")'; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectMemberReceiver(string $name): string |
||||||
|
{ |
||||||
|
return $this->getNativeObjectReceiver($name) . '.'; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Materialize a Native-producing expression as a precisely rooted local. |
||||||
|
* This is shared by chained method and property access so neither path can |
||||||
|
* accidentally pass a Native pointer through php::Variant. |
||||||
|
*/ |
||||||
|
protected function materializeNativeObjectReceiver(NodeAbstract $expr, string $class): string |
||||||
|
{ |
||||||
|
[$receiver, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); |
||||||
|
$this->appendCapturedStmtLinesToContext($beforeStmts); |
||||||
|
$object = $this->genTmpVarName(); |
||||||
|
$this->addLocalVar($object, $this->getNativeObjectPointerType($class)); |
||||||
|
$this->addNativeObject($object, $class); |
||||||
|
$this->context->beforeStmtLines[] = $object . ' = ' . $receiver . ';'; |
||||||
|
$this->appendCapturedStmtLinesToContext($afterStmts); |
||||||
|
// Keep the temporary in the precise root frame while the complete PHP |
||||||
|
// statement executes, then release that root at statement end. |
||||||
|
$this->context->afterStmtLines[] = $object . ' = nullptr;'; |
||||||
|
return $object; |
||||||
|
} |
||||||
|
|
||||||
|
protected function findNativeObjectProperty(string $class, string $property): ?PropertyDef |
||||||
|
{ |
||||||
|
while ($class !== '' && $this->hasClass($class)) { |
||||||
|
$classDef = $this->getClass($class); |
||||||
|
if ($classDef->hasProperty($property)) { |
||||||
|
return $classDef->getProperty($property); |
||||||
|
} |
||||||
|
$class = $classDef->extends; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
protected function findNativeObjectMethod(string $class, string $method): ?MethodDef |
||||||
|
{ |
||||||
|
while ($class !== '' && $this->isNativeObjectClass($class)) { |
||||||
|
$classDef = $this->getClass($class); |
||||||
|
if ($classDef->hasMethod($method)) { |
||||||
|
return $classDef->getMethod($method); |
||||||
|
} |
||||||
|
$class = $classDef->extends; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Native objects cannot fall back to PHPX/Zend conversion helpers. A |
||||||
|
* keyword conversion is therefore a statically checked ordinary Native |
||||||
|
* method call. __toString() is accepted as the PHP-compatible spelling of |
||||||
|
* toString(). |
||||||
|
*/ |
||||||
|
protected function resolveNativeObjectKeywordMethod( |
||||||
|
NodeAbstract $node, |
||||||
|
string $class, |
||||||
|
string $method, |
||||||
|
): string { |
||||||
|
$expectedType = self::KEYWORD_METHOD_MAP[$method] ?? null; |
||||||
|
if ($expectedType === null) { |
||||||
|
return $method; |
||||||
|
} |
||||||
|
$resolvedMethod = $method; |
||||||
|
$methodDef = $this->findNativeObjectMethod($class, $resolvedMethod); |
||||||
|
if ($method === 'toString' && $methodDef === null) { |
||||||
|
$resolvedMethod = '__toString'; |
||||||
|
$methodDef = $this->findNativeObjectMethod($class, $resolvedMethod); |
||||||
|
} |
||||||
|
if ($methodDef === null) { |
||||||
|
$this->fatalError($node, "Native class `{$class}` must define `{$method}()` for this conversion"); |
||||||
|
} |
||||||
|
$function = $methodDef->functionDef; |
||||||
|
if ($function->argInfoList !== []) { |
||||||
|
$this->fatalError($node, "Native conversion method `{$class}::{$resolvedMethod}()` must not accept arguments"); |
||||||
|
} |
||||||
|
if ($function->returnsByRef || $function->returnNullable || $function->returnType !== $expectedType) { |
||||||
|
$expectedTypeName = match ($expectedType) { |
||||||
|
Type::INT => 'int', |
||||||
|
Type::FLOAT => 'float', |
||||||
|
Type::STR => 'string', |
||||||
|
Type::BOOL => 'bool', |
||||||
|
Type::ARRAY => 'array', |
||||||
|
Type::STREAM => 'Stream', |
||||||
|
Type::BIGINT => 'BigInt', |
||||||
|
Type::BIGFLOAT => 'BigFloat', |
||||||
|
Type::DECIMAL => 'Decimal', |
||||||
|
Type::OBJECT => 'object', |
||||||
|
Type::VAR => 'mixed', |
||||||
|
default => $expectedType, |
||||||
|
}; |
||||||
|
$this->fatalError( |
||||||
|
$node, |
||||||
|
"Native conversion method `{$class}::{$resolvedMethod}()` must return exactly `{$expectedTypeName}`", |
||||||
|
); |
||||||
|
} |
||||||
|
return $resolvedMethod; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeVirtualMethodName(string $method): string |
||||||
|
{ |
||||||
|
return '__typephp_virtual_' . strtolower($method); |
||||||
|
} |
||||||
|
|
||||||
|
protected function isNativeVirtualMethod(ClassDef $class, MethodDef $method): bool |
||||||
|
{ |
||||||
|
if ($method->flags & (Modifiers::STATIC | Modifiers::PRIVATE | Modifiers::FINAL | Modifiers::ABSTRACT)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (in_array(strtolower($method->name), ['__construct', '__destruct', '__clone'], true)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($this->isOverrideMethod($class->getNamespacedName(false) . '::' . $method->name)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
$parent = $class->extends; |
||||||
|
while ($parent !== '' && $this->hasClass($parent)) { |
||||||
|
$parentDef = $this->getClass($parent); |
||||||
|
if ($parentDef->hasMethod($method->name)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
$parent = $parentDef->extends; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeMethodReturnCppType(FunctionDef $function): string |
||||||
|
{ |
||||||
|
return $function->returnsByRef |
||||||
|
? Type::REF |
||||||
|
: ($this->getNativeObjectReturnType($function) ?? $function->returnType); |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeMethodParameterDeclarations(FunctionDef $function): string |
||||||
|
{ |
||||||
|
$args = []; |
||||||
|
foreach ($function->argInfoList as $argument) { |
||||||
|
if ($argument->variadic) { |
||||||
|
$args[] = Type::ARRAY . ' ' . $argument->name; |
||||||
|
} else { |
||||||
|
$args[] = $this->genArgumentDeclaration($argument); |
||||||
|
} |
||||||
|
} |
||||||
|
return implode(', ', $args); |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectPropertyType(PropertyDef $property): string |
||||||
|
{ |
||||||
|
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { |
||||||
|
return $this->getNativeObjectPointerType($property->class); |
||||||
|
} |
||||||
|
return match ($property->type) { |
||||||
|
Type::STREAM, Type::BOX => Type::VAR, |
||||||
|
default => $property->type, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
protected function getNativeObjectPropertyCppName( |
||||||
|
string|PropertyDef $property, |
||||||
|
string|ClassDef|null $declaringClass = null, |
||||||
|
): string |
||||||
|
{ |
||||||
|
$name = $property instanceof PropertyDef ? $property->name : $property; |
||||||
|
if (!$property instanceof PropertyDef || !$property->isPrivate()) { |
||||||
|
return $this->escapeVarName($name); |
||||||
|
} |
||||||
|
if ($declaringClass === null) { |
||||||
|
throw new \LogicException('Native private property field requires its declaring class'); |
||||||
|
} |
||||||
|
return '__private_' . $this->getNativeObjectCppName($declaringClass) |
||||||
|
. '__' . $this->escapeVarName($name); |
||||||
|
} |
||||||
|
|
||||||
|
protected function isNativeObjectForbiddenPropertyType(PropertyDef $property): bool |
||||||
|
{ |
||||||
|
if (in_array($property->type, [ |
||||||
|
Type::BOX, |
||||||
|
Type::STD_ARRAY, |
||||||
|
Type::STD_VECTOR, |
||||||
|
Type::STD_MAP, |
||||||
|
Type::STD_ORDERED_MAP, |
||||||
|
], true)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
return in_array(strtolower(ltrim($property->class, '\\')), [ |
||||||
|
'std\\array', |
||||||
|
'std\\vector', |
||||||
|
'std\\map', |
||||||
|
'std\\ordered_map', |
||||||
|
], true); |
||||||
|
} |
||||||
|
|
||||||
|
protected function isNativeObjectInheritedPropertyRedeclaration( |
||||||
|
ClassDef $class, |
||||||
|
PropertyDef $property, |
||||||
|
): bool { |
||||||
|
$parent = $class->extends; |
||||||
|
while ($parent !== '' && $this->isNativeObjectClass($parent)) { |
||||||
|
$parentDef = $this->getClass($parent); |
||||||
|
if ($parentDef->hasProperty($property->name)) { |
||||||
|
$parentProperty = $parentDef->getProperty($property->name); |
||||||
|
if ($property->isPrivate() || $parentProperty->isPrivate()) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
// Property compatibility is validated separately. TypePHP |
||||||
|
// treats a compatible public/protected redeclaration as the |
||||||
|
// same inherited slot, so a Native child must not emit a |
||||||
|
// second C++ field with the same PHP property name. |
||||||
|
return true; |
||||||
|
} |
||||||
|
$parent = $parentDef->extends; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* C++ requires a base struct to be complete before defining a derived |
||||||
|
* struct. PHP source order has no such restriction, so emit Native class |
||||||
|
* definitions in inheritance order while retaining source order between |
||||||
|
* unrelated classes. |
||||||
|
* |
||||||
|
* @return list<ClassDef> |
||||||
|
*/ |
||||||
|
protected function getNativeObjectClassesInDeclarationOrder(): array |
||||||
|
{ |
||||||
|
$classes = array_values(array_filter( |
||||||
|
$this->symbols->classes(), |
||||||
|
static fn (ClassDef $class): bool => $class->nativeObject, |
||||||
|
)); |
||||||
|
$byName = []; |
||||||
|
foreach ($classes as $class) { |
||||||
|
$byName[strtolower(ltrim($class->getNamespacedName(false), '\\'))] = $class; |
||||||
|
} |
||||||
|
|
||||||
|
$ordered = []; |
||||||
|
$visited = []; |
||||||
|
$visit = function (ClassDef $class) use (&$visit, &$ordered, &$visited, $byName): void { |
||||||
|
$key = strtolower(ltrim($class->getNamespacedName(false), '\\')); |
||||||
|
if (isset($visited[$key])) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$visited[$key] = true; |
||||||
|
$parent = strtolower(ltrim($class->extends, '\\')); |
||||||
|
if ($parent !== '' && isset($byName[$parent])) { |
||||||
|
$visit($byName[$parent]); |
||||||
|
} |
||||||
|
$ordered[] = $class; |
||||||
|
}; |
||||||
|
foreach ($classes as $class) { |
||||||
|
$visit($class); |
||||||
|
} |
||||||
|
return $ordered; |
||||||
|
} |
||||||
|
|
||||||
|
protected function genNativeObjectDeclarations(): string |
||||||
|
{ |
||||||
|
$classes = $this->getNativeObjectClassesInDeclarationOrder(); |
||||||
|
if ($classes === []) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
$code = '// TypePHP Native Object declarations' . PHP_EOL; |
||||||
|
foreach ($classes as $class) { |
||||||
|
$code .= 'struct ' . $this->getNativeObjectCppName($class) . ';' . PHP_EOL; |
||||||
|
} |
||||||
|
$code .= PHP_EOL; |
||||||
|
|
||||||
|
foreach ($classes as $class) { |
||||||
|
$name = $this->getNativeObjectCppName($class); |
||||||
|
$parent = $class->extends !== '' && $this->isNativeObjectClass($class->extends) |
||||||
|
? ' : public ' . $this->getNativeObjectCppName($class->extends) |
||||||
|
: ''; |
||||||
|
$code .= 'struct ' . $name . $parent . ' {' . PHP_EOL; |
||||||
|
foreach ($class->properties as $property) { |
||||||
|
if ($property->flags & Modifiers::STATIC |
||||||
|
|| $this->isNativeObjectInheritedPropertyRedeclaration($class, $property) |
||||||
|
) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$type = $this->getNativeObjectPropertyType($property); |
||||||
|
// PHPX value types own their storage and must be initialized by |
||||||
|
// their C++ default constructor. PHP-level defaults are applied |
||||||
|
// by the generated allocation/constructor path, not in this |
||||||
|
// shared declaration header (which cannot reference file-local |
||||||
|
// literal tables). |
||||||
|
$default = null; |
||||||
|
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { |
||||||
|
$default = 'nullptr'; |
||||||
|
} elseif (in_array($property->type, [Type::INT, Type::FLOAT, Type::BOOL], true)) { |
||||||
|
$default = '0'; |
||||||
|
} |
||||||
|
$code .= ' ' . $type . ' ' . $this->getNativeObjectPropertyCppName($property, $class); |
||||||
|
if ($default !== null) { |
||||||
|
$code .= ' = ' . $default; |
||||||
|
} |
||||||
|
$code .= ';' . PHP_EOL; |
||||||
|
} |
||||||
|
foreach ($class->methods as $method) { |
||||||
|
if (!$this->isNativeVirtualMethod($class, $method)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$parentHasMethod = $class->extends !== '' |
||||||
|
&& $this->hasClass($class->extends) |
||||||
|
&& $this->getClass($class->extends)->hasMethod($method->name); |
||||||
|
$code .= ' virtual ' . $this->getNativeMethodReturnCppType($method->functionDef) |
||||||
|
. ' ' . $this->getNativeVirtualMethodName($method->name) . '(' |
||||||
|
. $this->getNativeMethodParameterDeclarations($method->functionDef) . ')' |
||||||
|
. ($parentHasMethod ? ' override' : '') . ';' . PHP_EOL; |
||||||
|
} |
||||||
|
$code .= '};' . PHP_EOL; |
||||||
|
$code .= 'void ' . $name . '__initialize(' . $name . ' &object);' . PHP_EOL; |
||||||
|
$code .= 'void ' . $name . '__gc_trace(void *object, php::NativeMarker &marker);' . PHP_EOL; |
||||||
|
$code .= 'extern const php::NativeTypeDescriptor ' |
||||||
|
. $this->getNativeObjectDescriptorName($class) . ';' . PHP_EOL . PHP_EOL; |
||||||
|
} |
||||||
|
return $code; |
||||||
|
} |
||||||
|
|
||||||
|
protected function genNativeObjectRuntimeDefinition(ClassDef $class): string |
||||||
|
{ |
||||||
|
$cpp = $this->getNativeObjectCppName($class); |
||||||
|
$prefix = $cpp . '__gc'; |
||||||
|
$code = ''; |
||||||
|
$code .= 'void ' . $cpp . '__initialize(' . $cpp . ' &this_) {' . PHP_EOL; |
||||||
|
if ($class->extends !== '' && $this->isNativeObjectClass($class->extends)) { |
||||||
|
$code .= ' ' . $this->getNativeObjectCppName($class->extends) . '__initialize(this_);' . PHP_EOL; |
||||||
|
} |
||||||
|
foreach ($class->properties as $property) { |
||||||
|
if ($property->isStatic() || $property->default === null) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($property->type === Type::OBJECT && $this->isNativeObjectClass($property->class)) { |
||||||
|
$value = 'nullptr'; |
||||||
|
} else { |
||||||
|
$value = $property->default; |
||||||
|
} |
||||||
|
$code .= ' this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ' = ' . $value . ';' . PHP_EOL; |
||||||
|
} |
||||||
|
$code .= '}' . PHP_EOL . PHP_EOL; |
||||||
|
foreach ($class->methods as $method) { |
||||||
|
if (!$this->isNativeVirtualMethod($class, $method)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$function = $method->functionDef; |
||||||
|
$returnType = $this->getNativeMethodReturnCppType($function); |
||||||
|
$args = array_map(static fn (ArgInfo $arg): string => $arg->name, $function->argInfoList); |
||||||
|
$nativeFunction = self::PREFIX . $this->getNativeName( |
||||||
|
$method->name, |
||||||
|
$class->namespace, |
||||||
|
$class->name, |
||||||
|
); |
||||||
|
$code .= $returnType . ' ' . $cpp . '::' . $this->getNativeVirtualMethodName($method->name) |
||||||
|
. '(' . $this->getNativeMethodParameterDeclarations($function) . ') {' . PHP_EOL; |
||||||
|
$call = $nativeFunction . '(*this' . ($args === [] ? '' : ', ' . implode(', ', $args)) . ')'; |
||||||
|
$code .= ' ' . ($returnType === Type::VOID ? '' : 'return ') . $call . ';' . PHP_EOL; |
||||||
|
$code .= '}' . PHP_EOL . PHP_EOL; |
||||||
|
} |
||||||
|
$code .= 'void ' . $prefix . '_trace(void *object, php::NativeMarker &marker) {' . PHP_EOL; |
||||||
|
$hasParentTrace = $class->extends !== '' && $this->isNativeObjectClass($class->extends); |
||||||
|
$nativeProperties = array_filter( |
||||||
|
$class->properties, |
||||||
|
fn (PropertyDef $property): bool => !$property->isStatic() |
||||||
|
&& !$this->isNativeObjectInheritedPropertyRedeclaration($class, $property) |
||||||
|
&& $property->type === Type::OBJECT |
||||||
|
&& $this->isNativeObjectClass($property->class), |
||||||
|
); |
||||||
|
if ($hasParentTrace || $nativeProperties !== []) { |
||||||
|
$code .= ' auto &this_ = *static_cast<' . $cpp . ' *>(object);' . PHP_EOL; |
||||||
|
} else { |
||||||
|
$code .= ' (void) object;' . PHP_EOL; |
||||||
|
$code .= ' (void) marker;' . PHP_EOL; |
||||||
|
} |
||||||
|
if ($hasParentTrace) { |
||||||
|
$code .= ' ' . $this->getNativeObjectCppName($class->extends) |
||||||
|
. '__gc_trace(static_cast<' . $this->getNativeObjectCppName($class->extends) . ' *>(&this_), marker);' |
||||||
|
. PHP_EOL; |
||||||
|
} |
||||||
|
foreach ($nativeProperties as $property) { |
||||||
|
$code .= ' marker.mark(this_.' . $this->getNativeObjectPropertyCppName($property, $class) . ');' . PHP_EOL; |
||||||
|
} |
||||||
|
$code .= '}' . PHP_EOL; |
||||||
|
|
||||||
|
$destructors = []; |
||||||
|
$destructorClass = $class; |
||||||
|
while (true) { |
||||||
|
if ($destructorClass->hasMethod('__destruct')) { |
||||||
|
$destructors[] = [ |
||||||
|
self::PREFIX . $this->getNativeName( |
||||||
|
'__destruct', |
||||||
|
$destructorClass->namespace, |
||||||
|
$destructorClass->name, |
||||||
|
), |
||||||
|
$this->getNativeObjectCppName($destructorClass), |
||||||
|
]; |
||||||
|
} |
||||||
|
if ($destructorClass->extends === '' || !$this->isNativeObjectClass($destructorClass->extends)) { |
||||||
|
break; |
||||||
|
} |
||||||
|
$destructorClass = $this->getClass($destructorClass->extends); |
||||||
|
} |
||||||
|
if ($destructors !== []) { |
||||||
|
$code .= 'static void ' . $prefix . '_finalize(void *object) {' . PHP_EOL; |
||||||
|
foreach ($destructors as [$destructor, $destructorCpp]) { |
||||||
|
$code .= ' ' . $destructor . '(*static_cast<' . $destructorCpp . ' *>(object));' . PHP_EOL; |
||||||
|
} |
||||||
|
$code .= '}' . PHP_EOL; |
||||||
|
} |
||||||
|
$code .= 'static void ' . $prefix . '_destroy(void *object) noexcept {' . PHP_EOL; |
||||||
|
$code .= ' static_cast<' . $cpp . ' *>(object)->~' . $cpp . '();' . PHP_EOL; |
||||||
|
$code .= '}' . PHP_EOL; |
||||||
|
$code .= 'const php::NativeTypeDescriptor ' . $this->getNativeObjectDescriptorName($class) . ' = {' . PHP_EOL; |
||||||
|
$code .= ' "' . addslashes($class->getNamespacedName(false)) . '",' . PHP_EOL; |
||||||
|
$code .= ' sizeof(' . $cpp . '),' . PHP_EOL; |
||||||
|
$code .= ' alignof(' . $cpp . '),' . PHP_EOL; |
||||||
|
$code .= ' ' . $prefix . '_trace,' . PHP_EOL; |
||||||
|
$code .= ' ' . ($destructors !== [] ? $prefix . '_finalize' : 'nullptr') . ',' . PHP_EOL; |
||||||
|
$code .= ' ' . $prefix . '_destroy,' . PHP_EOL; |
||||||
|
$code .= '};' . PHP_EOL . PHP_EOL; |
||||||
|
return $code; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP. |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Transform; |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\NodeVisitorAbstract; |
||||||
|
|
||||||
|
final class NativeClassAttributeLowering extends NodeVisitorAbstract |
||||||
|
{ |
||||||
|
public const string ATTRIBUTE = 'typephpNativeClass'; |
||||||
|
|
||||||
|
public function enterNode(Node $node): null |
||||||
|
{ |
||||||
|
self::lower($node); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public static function lower(Node $node): void |
||||||
|
{ |
||||||
|
if (!$node instanceof Node\Stmt\Class_) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (CompileTimeAttribute::consume($node, 'Native')) { |
||||||
|
$node->setAttribute(self::ATTRIBUTE, true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static function isNative(Node $node): bool |
||||||
|
{ |
||||||
|
return $node instanceof Node\Stmt\Class_ |
||||||
|
&& $node->getAttribute(self::ATTRIBUTE, false) === true; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: construction, typed properties and direct method calls |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class Point |
||||||
|
{ |
||||||
|
public int $x = 0; |
||||||
|
public int $y = 0; |
||||||
|
|
||||||
|
public function __construct(int $x, int $y) |
||||||
|
{ |
||||||
|
$this->x = $x; |
||||||
|
$this->y = $y; |
||||||
|
} |
||||||
|
|
||||||
|
public function sum(): int |
||||||
|
{ |
||||||
|
return $this->x + $this->y; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$point = new Point(20, 22); |
||||||
|
var_dump($point->x, $point->y, $point->sum()); |
||||||
|
unset($point); |
||||||
|
try { |
||||||
|
$point->sum(); |
||||||
|
} catch (Error $error) { |
||||||
|
echo "null guarded\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(20) |
||||||
|
int(22) |
||||||
|
int(42) |
||||||
|
null guarded |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: by-reference parameters replace the caller pointer |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeReferenceValue |
||||||
|
{ |
||||||
|
public int $value; |
||||||
|
|
||||||
|
public function __construct(int $value) |
||||||
|
{ |
||||||
|
$this->value = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function replaceNativeReference(NativeReferenceValue &$value): void |
||||||
|
{ |
||||||
|
$value = new NativeReferenceValue(42); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeReferenceValue(1); |
||||||
|
replaceNativeReference($value); |
||||||
|
var_dump($value->value); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(42) |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: call result arguments are evaluated left-to-right and precisely rooted |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeArgument |
||||||
|
{ |
||||||
|
public string $name; |
||||||
|
|
||||||
|
public function __construct(string $name) |
||||||
|
{ |
||||||
|
$this->name = $name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function makeNativeArgument(string $name): NativeArgument |
||||||
|
{ |
||||||
|
echo 'make:', $name, PHP_EOL; |
||||||
|
return new NativeArgument($name); |
||||||
|
} |
||||||
|
|
||||||
|
function consumeNativeArguments(NativeArgument $first, NativeArgument $second): void |
||||||
|
{ |
||||||
|
echo $first->name, ':', $second->name, PHP_EOL; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
consumeNativeArguments(makeNativeArgument('A'), makeNativeArgument('B')); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
make:A |
||||||
|
make:B |
||||||
|
A:B |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: chained assignment remains in the native object model |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeChainedValue |
||||||
|
{ |
||||||
|
public int $value = 42; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$first = $second = new NativeChainedValue(); |
||||||
|
var_dump($first->value, $second->value); |
||||||
|
var_dump($first === $second); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(42) |
||||||
|
int(42) |
||||||
|
bool(true) |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: method calls accept native object expressions as receivers |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeChainValue |
||||||
|
{ |
||||||
|
public int $value = 42; |
||||||
|
|
||||||
|
public function getValue(): int |
||||||
|
{ |
||||||
|
return $this->value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function makeNativeChainValue(): NativeChainValue |
||||||
|
{ |
||||||
|
return new NativeChainValue(); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
echo (new NativeChainValue())->getValue(), PHP_EOL; |
||||||
|
echo makeNativeChainValue()->getValue(), PHP_EOL; |
||||||
|
echo makeNativeChainValue()->value, PHP_EOL; |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
42 |
||||||
|
42 |
||||||
|
42 |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: clone is native and the class remains invisible to ZendVM |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeCloneValue |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
|
||||||
|
public function __clone(): void |
||||||
|
{ |
||||||
|
$this->value++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$first = new NativeCloneValue(); |
||||||
|
$second = clone $first; |
||||||
|
var_dump($first->value, $second->value); |
||||||
|
var_dump(class_exists('NativeCloneValue', false)); |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(1) |
||||||
|
int(2) |
||||||
|
bool(false) |
||||||
@ -0,0 +1,71 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: nullable, union and intersection fields use Var with runtime type checks |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
interface NativeCompositeLeft {} |
||||||
|
interface NativeCompositeRight {} |
||||||
|
|
||||||
|
class NativeCompositeBoth implements NativeCompositeLeft, NativeCompositeRight {} |
||||||
|
class NativeCompositeLeftOnly implements NativeCompositeLeft {} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeCompositeProperties |
||||||
|
{ |
||||||
|
public ?int $nullableInt; |
||||||
|
public int|string|null $unionValue; |
||||||
|
public NativeCompositeLeft&NativeCompositeRight $intersectionValue; |
||||||
|
} |
||||||
|
|
||||||
|
function writeNullable(NativeCompositeProperties $object, mixed $value): void |
||||||
|
{ |
||||||
|
$object->nullableInt = $value; |
||||||
|
} |
||||||
|
|
||||||
|
function writeUnion(NativeCompositeProperties $object, mixed $value): void |
||||||
|
{ |
||||||
|
$object->unionValue = $value; |
||||||
|
} |
||||||
|
|
||||||
|
function writeIntersection(NativeCompositeProperties $object, mixed $value): void |
||||||
|
{ |
||||||
|
$object->intersectionValue = $value; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = new NativeCompositeProperties(); |
||||||
|
var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue); |
||||||
|
|
||||||
|
writeNullable($object, 42); |
||||||
|
writeUnion($object, 'ok'); |
||||||
|
writeIntersection($object, new NativeCompositeBoth()); |
||||||
|
var_dump($object->nullableInt, $object->unionValue, $object->intersectionValue::class); |
||||||
|
|
||||||
|
try { |
||||||
|
writeNullable($object, 'bad'); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "type error\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
writeUnion($object, []); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "type error\n"; |
||||||
|
} |
||||||
|
try { |
||||||
|
writeIntersection($object, new NativeCompositeLeftOnly()); |
||||||
|
} catch (TypeError $error) { |
||||||
|
echo "type error\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
NULL |
||||||
|
NULL |
||||||
|
NULL |
||||||
|
int(42) |
||||||
|
string(2) "ok" |
||||||
|
string(19) "NativeCompositeBoth" |
||||||
|
type error |
||||||
|
type error |
||||||
|
type error |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: declarations are emitted in inheritance order |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeOrderChild extends NativeOrderParent |
||||||
|
{ |
||||||
|
public int $child = 2; |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeOrderParent |
||||||
|
{ |
||||||
|
public int $parent = 1; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeOrderChild(); |
||||||
|
echo $value->parent, ':', $value->child, PHP_EOL; |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
1:2 |
||||||
@ -0,0 +1,34 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: GC finalization runs destructors from derived to base exactly once |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeDestructorBase |
||||||
|
{ |
||||||
|
public function __destruct() |
||||||
|
{ |
||||||
|
echo 'B'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeDestructorChild extends NativeDestructorBase |
||||||
|
{ |
||||||
|
public function __destruct() |
||||||
|
{ |
||||||
|
echo 'C'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeDestructorChild(); |
||||||
|
$value = null; |
||||||
|
echo "done\n"; |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
done |
||||||
|
CB |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: tracing GC collects cycles and runs destructors once |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeNode |
||||||
|
{ |
||||||
|
public ?NativeNode $next = null; |
||||||
|
public string $name = ''; |
||||||
|
|
||||||
|
public function __construct(string $name) |
||||||
|
{ |
||||||
|
$this->name = $name; |
||||||
|
} |
||||||
|
|
||||||
|
public function __destruct() |
||||||
|
{ |
||||||
|
echo $this->name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$a = new NativeNode('A'); |
||||||
|
$b = new NativeNode('B'); |
||||||
|
$a->next = $b; |
||||||
|
$b->next = $a; |
||||||
|
$a = null; |
||||||
|
$b = null; |
||||||
|
echo "done\n"; |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
done |
||||||
|
BA |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: Getter and Setter generators lower to direct native methods |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeGeneratedAccessors |
||||||
|
{ |
||||||
|
#[Getter] |
||||||
|
#[Setter] |
||||||
|
private int $value = 1; |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
#[Getter] |
||||||
|
private string $name = 'native', |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = new NativeGeneratedAccessors(); |
||||||
|
var_dump($object->getValue()); |
||||||
|
var_dump($object->getName()); |
||||||
|
$object->setValue(42); |
||||||
|
var_dump($object->getValue()); |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(1) |
||||||
|
string(6) "native" |
||||||
|
int(42) |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: TypePHP globals and static locals retain request-rooted native objects |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeCounter |
||||||
|
{ |
||||||
|
public int $value; |
||||||
|
} |
||||||
|
|
||||||
|
function initializeGlobal(): void |
||||||
|
{ |
||||||
|
global $nativeGlobal; |
||||||
|
$nativeGlobal = new NativeCounter(); |
||||||
|
$nativeGlobal->value = 40; |
||||||
|
} |
||||||
|
|
||||||
|
function readGlobal(): int |
||||||
|
{ |
||||||
|
global $nativeGlobal; |
||||||
|
return $nativeGlobal->value; |
||||||
|
} |
||||||
|
|
||||||
|
function nextStatic(): int |
||||||
|
{ |
||||||
|
static $counter = new NativeCounter(); |
||||||
|
return ++$counter->value; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
initializeGlobal(); |
||||||
|
var_dump(readGlobal()); |
||||||
|
var_dump(nextStatic()); |
||||||
|
var_dump(nextStatic()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(40) |
||||||
|
int(1) |
||||||
|
int(2) |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: compatible inherited property declarations reuse one C++ field |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativePropertyBase |
||||||
|
{ |
||||||
|
public int $value = 1; |
||||||
|
|
||||||
|
public function writeFromBase(int $value): void |
||||||
|
{ |
||||||
|
$this->value = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativePropertyChild extends NativePropertyBase |
||||||
|
{ |
||||||
|
public int $value = 2; |
||||||
|
|
||||||
|
public function readFromChild(): int |
||||||
|
{ |
||||||
|
return $this->value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativePropertyChild(); |
||||||
|
var_dump($value->readFromChild()); |
||||||
|
$value->writeFromBase(42); |
||||||
|
var_dump($value->readFromChild()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(2) |
||||||
|
int(42) |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: statically resolved instanceof is folded at compile time |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInstanceofBase |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInstanceofChild extends NativeInstanceofBase |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeInstanceofOther |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
function makeInstanceofChild(): NativeInstanceofChild |
||||||
|
{ |
||||||
|
echo "made\n"; |
||||||
|
return new NativeInstanceofChild(); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = new NativeInstanceofChild(); |
||||||
|
var_dump($object instanceof NativeInstanceofChild); |
||||||
|
var_dump($object instanceof NativeInstanceofBase); |
||||||
|
var_dump($object instanceof NativeInstanceofOther); |
||||||
|
var_dump(makeInstanceofChild() instanceof NativeInstanceofChild); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
bool(true) |
||||||
|
bool(true) |
||||||
|
bool(false) |
||||||
|
made |
||||||
|
bool(true) |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: internal interfaces are compile-time contracts |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeCountableValue implements Countable |
||||||
|
{ |
||||||
|
public function count(): int |
||||||
|
{ |
||||||
|
return 3; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeCountableValue(); |
||||||
|
var_dump($value->count()); |
||||||
|
var_dump($value instanceof Countable); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(3) |
||||||
|
bool(true) |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: keyword conversions lower to exactly typed native methods |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeConversions |
||||||
|
{ |
||||||
|
public int $value = 7; |
||||||
|
|
||||||
|
public function toArray(): array |
||||||
|
{ |
||||||
|
return [$this->value]; |
||||||
|
} |
||||||
|
|
||||||
|
public function toInt(): int |
||||||
|
{ |
||||||
|
return $this->value; |
||||||
|
} |
||||||
|
|
||||||
|
public function toFloat(): float |
||||||
|
{ |
||||||
|
return $this->value + 0.5; |
||||||
|
} |
||||||
|
|
||||||
|
public function toBool(): bool |
||||||
|
{ |
||||||
|
return $this->value !== 0; |
||||||
|
} |
||||||
|
|
||||||
|
public function toString(): string |
||||||
|
{ |
||||||
|
return 'value=' . $this->value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeMagicString |
||||||
|
{ |
||||||
|
public function __toString(): string |
||||||
|
{ |
||||||
|
return 'magic'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeConversions(); |
||||||
|
var_dump($value->toArray()); |
||||||
|
var_dump($value->toInt()); |
||||||
|
var_dump($value->toFloat()); |
||||||
|
var_dump($value->toBool()); |
||||||
|
var_dump($value->toString()); |
||||||
|
var_dump((string) $value); |
||||||
|
var_dump(strval($value)); |
||||||
|
|
||||||
|
$magic = new NativeMagicString(); |
||||||
|
var_dump($magic->toString()); |
||||||
|
var_dump((string) $magic); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
int(7) |
||||||
|
} |
||||||
|
int(7) |
||||||
|
float(7.5) |
||||||
|
bool(true) |
||||||
|
string(7) "value=7" |
||||||
|
string(7) "value=7" |
||||||
|
string(7) "value=7" |
||||||
|
string(5) "magic" |
||||||
|
string(5) "magic" |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: __toString and __invoke lower to direct native calls |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeCallableLabel |
||||||
|
{ |
||||||
|
public string $label = 'native'; |
||||||
|
|
||||||
|
public function __toString(): string |
||||||
|
{ |
||||||
|
return $this->label; |
||||||
|
} |
||||||
|
|
||||||
|
public function __invoke(int $number): string |
||||||
|
{ |
||||||
|
return $this->label . ':' . $number; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeCallableLabel(); |
||||||
|
echo $value, "\n"; |
||||||
|
var_dump((string) $value); |
||||||
|
var_dump('value=' . $value); |
||||||
|
var_dump($value(42)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
native |
||||||
|
string(6) "native" |
||||||
|
string(12) "value=native" |
||||||
|
string(9) "native:42" |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: non-null parameters are validated at function entry |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeRequiredArgument |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
function acceptRequiredNative(NativeRequiredArgument $value): void |
||||||
|
{ |
||||||
|
echo "entered\n"; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeRequiredArgument(); |
||||||
|
unset($value); |
||||||
|
try { |
||||||
|
acceptRequiredNative($value); |
||||||
|
} catch (Error $error) { |
||||||
|
echo "rejected\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
rejected |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: nullable parameters and returns stay native pointers |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeNullableValue |
||||||
|
{ |
||||||
|
public int $value = 42; |
||||||
|
} |
||||||
|
|
||||||
|
function maybeNative(bool $create): ?NativeNullableValue |
||||||
|
{ |
||||||
|
if ($create) { |
||||||
|
return new NativeNullableValue(); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function readMaybeNative(?NativeNullableValue $value): int |
||||||
|
{ |
||||||
|
if ($value === null) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
return $value->value; |
||||||
|
} |
||||||
|
|
||||||
|
function maybeNativeUnion(bool $create): NativeNullableValue|null |
||||||
|
{ |
||||||
|
return $create ? new NativeNullableValue() : null; |
||||||
|
} |
||||||
|
|
||||||
|
function readMaybeNativeUnion(NativeNullableValue|null $value): int |
||||||
|
{ |
||||||
|
return $value === null ? -2 : $value->value; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(readMaybeNative(maybeNative(false))); |
||||||
|
var_dump(readMaybeNative(maybeNative(true))); |
||||||
|
var_dump(readMaybeNativeUnion(maybeNativeUnion(false))); |
||||||
|
var_dump(readMaybeNativeUnion(maybeNativeUnion(true))); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(-1) |
||||||
|
int(42) |
||||||
|
int(-2) |
||||||
|
int(42) |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: PHPX value properties retain normal string and array behavior |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativePhpValues |
||||||
|
{ |
||||||
|
public string $class = 'reserved'; |
||||||
|
public string $name = 'native'; |
||||||
|
public array $values = [1, 2]; |
||||||
|
public mixed $anything = null; |
||||||
|
public ?int $maybe = null; |
||||||
|
|
||||||
|
public function append(int $value): void |
||||||
|
{ |
||||||
|
$this->values[] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = new NativePhpValues(); |
||||||
|
$object->append(3); |
||||||
|
$object->anything = 42; |
||||||
|
$object->maybe = 7; |
||||||
|
var_dump($object->class, $object->name, $object->values, $object->anything, $object->maybe); |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(8) "reserved" |
||||||
|
string(6) "native" |
||||||
|
array(3) { |
||||||
|
[0]=> |
||||||
|
int(1) |
||||||
|
[1]=> |
||||||
|
int(2) |
||||||
|
[2]=> |
||||||
|
int(3) |
||||||
|
} |
||||||
|
int(42) |
||||||
|
int(7) |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: parent and child private properties use independent native slots |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativePrivateBase |
||||||
|
{ |
||||||
|
private int $value = 10; |
||||||
|
|
||||||
|
public function baseValue(): int |
||||||
|
{ |
||||||
|
return $this->value; |
||||||
|
} |
||||||
|
|
||||||
|
public function setBaseValue(int $value): void |
||||||
|
{ |
||||||
|
$this->value = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativePrivateChild extends NativePrivateBase |
||||||
|
{ |
||||||
|
private int $value = 20; |
||||||
|
|
||||||
|
public function childValue(): int |
||||||
|
{ |
||||||
|
return $this->value; |
||||||
|
} |
||||||
|
|
||||||
|
public function setChildValue(int $value): void |
||||||
|
{ |
||||||
|
$this->value = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativePrivateChild(); |
||||||
|
var_dump($value->baseValue(), $value->childValue()); |
||||||
|
$value->setBaseValue(11); |
||||||
|
$value->setChildValue(22); |
||||||
|
var_dump($value->baseValue(), $value->childValue()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(10) |
||||||
|
int(20) |
||||||
|
int(11) |
||||||
|
int(22) |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: PHP 8.4 property hooks use native getter and setter calls |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeHookValue |
||||||
|
{ |
||||||
|
private int $stored = 1; |
||||||
|
|
||||||
|
public int $value { |
||||||
|
get { |
||||||
|
return $this->stored * 2; |
||||||
|
} |
||||||
|
set(int $value) { |
||||||
|
$this->stored = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = new NativeHookValue(); |
||||||
|
var_dump($object->value); |
||||||
|
$object->value = 21; |
||||||
|
var_dump($object->value); |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(2) |
||||||
|
int(42) |
||||||
@ -0,0 +1,73 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: trait composition, inheritance and interface contracts |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
interface Named |
||||||
|
{ |
||||||
|
public function label(): string; |
||||||
|
} |
||||||
|
|
||||||
|
trait HasCounter |
||||||
|
{ |
||||||
|
public int $count = 0; |
||||||
|
|
||||||
|
public function increment(): void |
||||||
|
{ |
||||||
|
$this->count++; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeBase |
||||||
|
{ |
||||||
|
use HasCounter; |
||||||
|
|
||||||
|
public function __construct(int $initial) |
||||||
|
{ |
||||||
|
$this->count = $initial; |
||||||
|
} |
||||||
|
|
||||||
|
public function label(): string |
||||||
|
{ |
||||||
|
return 'base'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeChild extends NativeBase implements Named |
||||||
|
{ |
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
parent::__construct(2); |
||||||
|
} |
||||||
|
|
||||||
|
public function label(): string |
||||||
|
{ |
||||||
|
return 'child'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function nativeLabel(NativeBase $value): string |
||||||
|
{ |
||||||
|
return $value->label(); |
||||||
|
} |
||||||
|
|
||||||
|
function makeNativeChild(): NativeChild |
||||||
|
{ |
||||||
|
return new NativeChild(); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = makeNativeChild(); |
||||||
|
$value->increment(); |
||||||
|
var_dump($value->label(), nativeLabel($value), $value->count, $value instanceof Named); |
||||||
|
} |
||||||
|
|
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(5) "child" |
||||||
|
string(5) "child" |
||||||
|
int(3) |
||||||
|
bool(true) |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: unset and null clear only the local pointer slot |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeAliasValue |
||||||
|
{ |
||||||
|
public int $value = 42; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$first = new NativeAliasValue(); |
||||||
|
$alias = $first; |
||||||
|
unset($first); |
||||||
|
var_dump($first === null, $alias->value); |
||||||
|
|
||||||
|
$second = $alias; |
||||||
|
$alias = null; |
||||||
|
var_dump($alias === null, $second->value); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
bool(true) |
||||||
|
int(42) |
||||||
|
bool(true) |
||||||
|
int(42) |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: ternary, match and coalesce preserve native pointer types |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeSelectedValue |
||||||
|
{ |
||||||
|
public int $value; |
||||||
|
|
||||||
|
public function __construct(int $value) |
||||||
|
{ |
||||||
|
$this->value = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function selectWithMatch(int $kind): NativeSelectedValue |
||||||
|
{ |
||||||
|
return match ($kind) { |
||||||
|
1 => new NativeSelectedValue(10), |
||||||
|
default => new NativeSelectedValue(20), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function selectWithCoalesce(?NativeSelectedValue $value): NativeSelectedValue |
||||||
|
{ |
||||||
|
return $value ?? new NativeSelectedValue(30); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$first = true ? new NativeSelectedValue(1) : new NativeSelectedValue(2); |
||||||
|
var_dump($first->value); |
||||||
|
var_dump(selectWithMatch(1)->value, selectWithMatch(2)->value); |
||||||
|
var_dump(selectWithCoalesce(null)->value); |
||||||
|
var_dump(selectWithCoalesce($first)->value); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(1) |
||||||
|
int(10) |
||||||
|
int(20) |
||||||
|
int(30) |
||||||
|
int(1) |
||||||
@ -0,0 +1,30 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: native-only methods on Zend objects use direct calls and stay out of Zend metadata |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeMethodArgument |
||||||
|
{ |
||||||
|
public int $value = 42; |
||||||
|
} |
||||||
|
|
||||||
|
class ZendMethodHost |
||||||
|
{ |
||||||
|
public function read(NativeMethodArgument $value): int |
||||||
|
{ |
||||||
|
return $value->value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$host = new ZendMethodHost(); |
||||||
|
$value = new NativeMethodArgument(); |
||||||
|
var_dump($host->read($value)); |
||||||
|
var_dump(method_exists($host, 'read')); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(42) |
||||||
|
bool(false) |
||||||
@ -0,0 +1,43 @@ |
|||||||
|
--TEST-- |
||||||
|
Native class: properties without explicit defaults use their type zero values |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
class NativeZeroValue |
||||||
|
{ |
||||||
|
public bool $enabled; |
||||||
|
public int $count; |
||||||
|
public float $ratio; |
||||||
|
public string $name; |
||||||
|
public array $items; |
||||||
|
public mixed $value; |
||||||
|
public ?stdClass $object; |
||||||
|
public ?NativeZeroValue $child; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = new NativeZeroValue(); |
||||||
|
var_dump( |
||||||
|
$value->enabled, |
||||||
|
$value->count, |
||||||
|
$value->ratio, |
||||||
|
$value->name, |
||||||
|
$value->items, |
||||||
|
$value->value, |
||||||
|
$value->object, |
||||||
|
$value->child === null, |
||||||
|
); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
bool(false) |
||||||
|
int(0) |
||||||
|
float(0) |
||||||
|
string(0) "" |
||||||
|
array(0) { |
||||||
|
} |
||||||
|
NULL |
||||||
|
NULL |
||||||
|
bool(true) |
||||||
Loading…
Reference in new issue