Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1576,13 +1576,11 @@ protected function compileClassLike(Op\Stmt\ClassLike $class, Block $block): OpC
if ($class instanceof Op\Stmt\Class_ && null !== $class->extends) {
$parentSlot = $this->compileOperand($class->extends, $block, true);
}
$readonlyVar = new Variable(Variable::TYPE_INTEGER);
$readonlyVar->int(
VM\ClassReadonly::fromClassFlags($class->flags) ? 1 : 0
);
$readonlyOperand = new Operand\Temporary;
$readonlyOperand->type = Type::int();
$readonlySlot = $block->registerConstant($readonlyOperand, $readonlyVar);
$classFlagsVar = new Variable(Variable::TYPE_INTEGER);
$classFlagsVar->int(VM\ClassFlags::pack($class->flags));
$classFlagsOperand = new Operand\Temporary;
$classFlagsOperand->type = Type::int();
$readonlySlot = $block->registerConstant($classFlagsOperand, $classFlagsVar);
$return = new OpCode(
$type,
$this->compileOperand($class->name, $block, true),
Expand Down
18 changes: 17 additions & 1 deletion lib/VM.php
Original file line number Diff line number Diff line change
Expand Up @@ -1614,7 +1614,9 @@ private function runFramesInner(): int
$classEntry->parentLc = $parentLc;
}
if (null !== $op->arg3 && isset($frame->block->constants[$op->arg3])) {
$classEntry->readonly = (bool) $frame->block->constants[$op->arg3]->toInt();
$classFlags = $frame->block->constants[$op->arg3]->toInt();
$classEntry->readonly = VM\ClassFlags::isReadonly($classFlags);
$classEntry->isAbstract = VM\ClassFlags::isAbstract($classFlags);
}
if ($op->isSealed) {
$classEntry->sealed = true;
Expand All @@ -1632,6 +1634,7 @@ private function runFramesInner(): int
$this->inheritFromParent($classEntry);
}
$this->inheritFromInterfaces($classEntry);
VM\ClassValidator::finalizeClassDefinition($classEntry, $this->context);
$this->context->classes[$lcname] = $classEntry;
break;
case OpCode::TYPE_NEW:
Expand Down Expand Up @@ -1662,6 +1665,11 @@ private function runFramesInner(): int
if ($class->isInterface) {
throw new \LogicException("Cannot instantiate interface $name");
}
try {
VM\ClassValidator::assertInstantiable($class);
} catch (\LogicException $e) {
return $this->raise($e->getMessage(), $frame);
}
$object = new ObjectEntry($class);
$this->initInstancePropertyDefaults($object);
$result->object($object);
Expand Down Expand Up @@ -3626,6 +3634,11 @@ protected function applyTraitUse(ClassEntry $entry, string $traitName, array $ow
$entry->methodParameterMetadata[$name] = $trait->methodParameterMetadata[$name];
}
}
foreach ($trait->abstractMethods as $name => $_) {
if (!isset($entry->methods[$name]) && !isset($entry->abstractMethods[$name])) {
$entry->abstractMethods[$name] = true;
}
}
foreach ($trait->staticProperties as $name => $storage) {
if (!isset($entry->staticProperties[$name])) {
$entry->staticProperties[$name] = $storage;
Expand Down Expand Up @@ -3907,9 +3920,12 @@ protected function defineClass(ClassEntry $entry, Block $block): void {
$method = new Func\PHP($entry->name.'::'.$name, $op->block1);
$method->deprecated = $op->deprecatedMetadata;
$entry->methods[$name] = $method;
unset($entry->abstractMethods[$name]);
if ('__construct' === $name) {
$entry->constructor = $method;
}
} else {
$entry->abstractMethods[$name] = true;
}
break;
case OpCode::TYPE_DECLARE_CLASS_CONST:
Expand Down
2 changes: 2 additions & 0 deletions lib/VM/ClassEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class ClassEntry {
public bool $isTrait = false;
/** True for `abstract class` declarations (#3385). */
public bool $isAbstract = false;
/** @var array<string, true> lowercase method names declared abstract on this class */
public array $abstractMethods = [];
/** @var array<string, string> trait FQCN => FQCN from direct `use Trait;` (#3119) */
public array $usedTraits = [];
/** @var list<string> */
Expand Down
38 changes: 38 additions & 0 deletions lib/VM/ClassFlags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\VM;

/**
* Packed class declaration flags stored on TYPE_DECLARE_CLASS arg3 (#1360, #144).
*/
final class ClassFlags
{
public const READONLY = 1;

public const ABSTRACT = 2;

public static function pack(int $classFlags): int
{
$packed = 0;
if (ClassReadonly::fromClassFlags($classFlags)) {
$packed |= self::READONLY;
}
if (ClassAbstract::fromClassFlags($classFlags)) {
$packed |= self::ABSTRACT;
}

return $packed;
}

public static function isReadonly(int $packed): bool
{
return 0 !== ($packed & self::READONLY);
}

public static function isAbstract(int $packed): bool
{
return 0 !== ($packed & self::ABSTRACT);
}
}
133 changes: 133 additions & 0 deletions lib/VM/ClassValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\VM;

/**
* Zend-style class/interface/trait validation (issue #144).
*
* php-src: Zend/zend_inheritance.c, zend_compile.c abstract/interface checks.
*/
final class ClassValidator
{
public static function finalizeClassDefinition(ClassEntry $entry, Context $context): void
{
if ($entry->isInterface || $entry->isTrait) {
return;
}

self::rebuildAbstractMethods($entry, $context);
self::validateInterfaceImplementation($entry, $context);
self::validateAbstractMethodsResolved($entry);
}

public static function assertInstantiable(ClassEntry $entry): void
{
if ($entry->isAbstract || [] !== $entry->abstractMethods) {
throw new \LogicException("Cannot instantiate abstract class {$entry->name}");
}
}

private static function rebuildAbstractMethods(ClassEntry $entry, Context $context): void
{
$abstract = [];
foreach ($entry->abstractMethods as $name => $_) {
$abstract[$name] = true;
}

$parentLc = $entry->parentLc;
while (null !== $parentLc && isset($context->classes[$parentLc])) {
$parent = $context->classes[$parentLc];
foreach ($parent->abstractMethods as $name => $_) {
$abstract[$name] = true;
}
$parentLc = $parent->parentLc;
}

foreach ($entry->methods as $name => $_) {
if (!isset($entry->abstractMethods[$name])) {
unset($abstract[$name]);
}
}

$entry->abstractMethods = $abstract;
}

private static function validateInterfaceImplementation(ClassEntry $entry, Context $context): void
{
$missing = [];
foreach ($entry->interfaces as $ifaceLc) {
foreach (self::collectInterfaceMethods($ifaceLc, $context) as $method) {
if (!isset($entry->methods[$method]) || isset($entry->abstractMethods[$method])) {
$missing[] = [$ifaceLc, $method];
}
}
}

if ([] === $missing) {
return;
}

$count = count($missing);
[$ifaceLc, $method] = $missing[0];
$ifaceName = $context->classes[$ifaceLc]->name ?? $ifaceLc;

throw new \LogicException(
"Class {$entry->name} contains {$count} abstract method"
.(1 === $count ? '' : 's')
." and must therefore be declared abstract or implement the remaining methods ({$ifaceName}::{$method})"
);
}

private static function validateAbstractMethodsResolved(ClassEntry $entry): void
{
if ($entry->isAbstract || [] === $entry->abstractMethods) {
return;
}

$count = count($entry->abstractMethods);
$first = array_key_first($entry->abstractMethods);

throw new \LogicException(
"Class {$entry->name} contains {$count} abstract method"
.(1 === $count ? '' : 's')
." and must therefore be declared abstract or implement the remaining methods ({$entry->name}::{$first})"
);
}

/**
* @return list<string>
*/
private static function collectInterfaceMethods(string $ifaceLc, Context $context): array
{
$methods = [];
$visited = [];
$queue = [$ifaceLc];
while ([] !== $queue) {
$lc = array_shift($queue);
if (isset($visited[$lc])) {
continue;
}
$visited[$lc] = true;
if (!isset($context->classes[$lc])) {
continue;
}
$iface = $context->classes[$lc];
if (!$iface->isInterface) {
continue;
}
foreach ($iface->methods as $name => $_) {
$methods[$name] = true;
}
foreach ($iface->abstractMethods as $name => $_) {
$methods[$name] = true;
}
foreach ($iface->interfaces as $parentIface) {
$queue[] = $parentIface;
}
}

return array_keys($methods);
}
}
13 changes: 13 additions & 0 deletions test/compliance/cases/language/abstract_class_concrete.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
abstract class concrete subclass (issue #144)
--FILE--
<?php
abstract class A {
abstract public function f(): int;
}
class C extends A {
public function f(): int { return 42; }
}
echo (new C)->f(), "\n";
--EXPECT--
42
10 changes: 10 additions & 0 deletions test/compliance/cases/language/abstract_class_instantiate.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
--TEST--
abstract class cannot be instantiated (issue #144)
--FILE--
<?php
abstract class A {
public function f(): int { return 1; }
}
new A();
--EXPECT_EXIT--
255
11 changes: 11 additions & 0 deletions test/compliance/cases/language/interface_implements_missing.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
--TEST--
missing interface method is rejected (issue #144)
--FILE--
<?php
interface I {
public function m(): void;
}
class C implements I {}
echo "ok\n";
--EXPECT_EXIT--
255
13 changes: 13 additions & 0 deletions test/compliance/cases/language/interface_implements_ok.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--TEST--
interface implementation with required method (issue #144)
--FILE--
<?php
interface I {
public function m(): void;
}
class C implements I {
public function m(): void {}
}
echo "ok\n";
--EXPECT--
ok
16 changes: 16 additions & 0 deletions test/compliance/cases/language/trait_use_conflict.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
horizontal trait method conflict (issue #144)
--FILE--
<?php
trait T1 {
public function f(): int { return 1; }
}
trait T2 {
public function f(): int { return 2; }
}
class C {
use T1, T2;
}
echo (new C)->f(), "\n";
--EXPECT_EXIT--
255
67 changes: 67 additions & 0 deletions test/unit/TraitsInterfacesAbstractTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace PHPCompiler\Test\Unit;

use PHPCompiler\Runtime;
use PHPUnit\Framework\TestCase;

/** @covers issue #144 */
final class TraitsInterfacesAbstractTest extends TestCase
{
public function testMissingInterfaceMethodFailsAtClassDeclaration(): void
{
$runtime = new Runtime();
$code = <<<'PHP'
<?php
interface I { public function m(): void; }
class C implements I {}
echo "ok\n";
PHP;
$this->expectException(\CompileError::class);
$this->expectExceptionMessage('abstract method');
$runtime->parseAndCompile($code, 'iface_missing.php');
}

public function testAbstractClassInstantiationFails(): void
{
$runtime = new Runtime();
$code = <<<'PHP'
<?php
abstract class A { public function f(): int { return 1; } }
new A();
PHP;
$this->expectException(\CompileError::class);
$this->expectExceptionMessage('Cannot instantiate abstract class A');
$runtime->parseAndCompile($code, 'abstract_new.php');
}

public function testTraitConflictFailsAtClassDeclaration(): void
{
$runtime = new Runtime();
$code = <<<'PHP'
<?php
trait T1 { public function f(): int { return 1; } }
trait T2 { public function f(): int { return 2; } }
class C { use T1, T2; }
PHP;
$this->expectException(\CompileError::class);
$this->expectExceptionMessage('collision with');
$runtime->parseAndCompile($code, 'trait_conflict.php');
}

public function testValidInterfaceImplementationRuns(): void
{
$runtime = new Runtime();
$code = <<<'PHP'
<?php
interface I { public function m(): void; }
class C implements I { public function m(): void {} }
echo "ok\n";
PHP;
ob_start();
$runtime->run($runtime->parseAndCompile($code, 'iface_ok.php'));
$this->assertSame("ok\n", ob_get_clean());
}
}