From 1cf86c9c637f56a0377054a75fb91ba1a298ca38 Mon Sep 17 00:00:00 2001 From: PurHur Date: Fri, 29 May 2026 19:07:01 +0000 Subject: [PATCH 1/2] Language: interface/abstract/trait validation VM (#144) Register interface methods, reject missing implements at class declaration, block abstract instantiation, and detect horizontal trait method conflicts. Co-authored-by: Cursor --- lib/Compiler.php | 12 +- lib/VM.php | 18 ++- lib/VM/ClassEntry.php | 2 + lib/VM/ClassFlags.php | 38 +++++ lib/VM/ClassValidator.php | 133 ++++++++++++++++++ .../language/abstract_class_concrete.phpt | 13 ++ .../language/abstract_class_instantiate.phpt | 10 ++ .../interface_implements_missing.phpt | 11 ++ .../language/interface_implements_ok.phpt | 13 ++ .../cases/language/trait_use_conflict.phpt | 16 +++ test/unit/TraitsInterfacesAbstractTest.php | 67 +++++++++ 11 files changed, 325 insertions(+), 8 deletions(-) create mode 100644 lib/VM/ClassFlags.php create mode 100644 lib/VM/ClassValidator.php create mode 100644 test/compliance/cases/language/abstract_class_concrete.phpt create mode 100644 test/compliance/cases/language/abstract_class_instantiate.phpt create mode 100644 test/compliance/cases/language/interface_implements_missing.phpt create mode 100644 test/compliance/cases/language/interface_implements_ok.phpt create mode 100644 test/compliance/cases/language/trait_use_conflict.phpt create mode 100644 test/unit/TraitsInterfacesAbstractTest.php diff --git a/lib/Compiler.php b/lib/Compiler.php index af38a1f5d10..77e75527350 100755 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -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), diff --git a/lib/VM.php b/lib/VM.php index 79682e8fd52..0eccdc8db05 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -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; @@ -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: @@ -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); @@ -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; @@ -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: diff --git a/lib/VM/ClassEntry.php b/lib/VM/ClassEntry.php index 4833e9e964e..51c65e3b2ca 100755 --- a/lib/VM/ClassEntry.php +++ b/lib/VM/ClassEntry.php @@ -31,6 +31,8 @@ class ClassEntry { public bool $isTrait = false; /** True for `abstract class` declarations (#3385). */ public bool $isAbstract = false; + /** @var array lowercase method names declared abstract on this class */ + public array $abstractMethods = []; /** @var array trait FQCN => FQCN from direct `use Trait;` (#3119) */ public array $usedTraits = []; /** @var list */ diff --git a/lib/VM/ClassFlags.php b/lib/VM/ClassFlags.php new file mode 100644 index 00000000000..06c9ce64acf --- /dev/null +++ b/lib/VM/ClassFlags.php @@ -0,0 +1,38 @@ +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 + */ + 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); + } +} diff --git a/test/compliance/cases/language/abstract_class_concrete.phpt b/test/compliance/cases/language/abstract_class_concrete.phpt new file mode 100644 index 00000000000..a809fc06766 --- /dev/null +++ b/test/compliance/cases/language/abstract_class_concrete.phpt @@ -0,0 +1,13 @@ +--TEST-- +abstract class concrete subclass (issue #144) +--FILE-- +f(), "\n"; +--EXPECT-- +42 diff --git a/test/compliance/cases/language/abstract_class_instantiate.phpt b/test/compliance/cases/language/abstract_class_instantiate.phpt new file mode 100644 index 00000000000..44b980238d1 --- /dev/null +++ b/test/compliance/cases/language/abstract_class_instantiate.phpt @@ -0,0 +1,10 @@ +--TEST-- +abstract class cannot be instantiated (issue #144) +--FILE-- +f(), "\n"; +--EXPECT_EXIT-- +255 diff --git a/test/unit/TraitsInterfacesAbstractTest.php b/test/unit/TraitsInterfacesAbstractTest.php new file mode 100644 index 00000000000..64dce3a377b --- /dev/null +++ b/test/unit/TraitsInterfacesAbstractTest.php @@ -0,0 +1,67 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('abstract method'); + $runtime->run($runtime->parseAndCompile($code, 'iface_missing.php')); + } + + public function testAbstractClassInstantiationFails(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +expectException(\LogicException::class); + $this->expectExceptionMessage('Cannot instantiate abstract class A'); + $runtime->run($runtime->parseAndCompile($code, 'abstract_new.php')); + } + + public function testTraitConflictFailsAtClassDeclaration(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +expectException(\LogicException::class); + $this->expectExceptionMessage('collision with'); + $runtime->run($runtime->parseAndCompile($code, 'trait_conflict.php')); + } + + public function testValidInterfaceImplementationRuns(): void + { + $runtime = new Runtime(); + $code = <<<'PHP' +run($runtime->parseAndCompile($code, 'iface_ok.php')); + $this->assertSame("ok\n", ob_get_clean()); + } +} From f0073abdd18e32a35c19be45a0aeb01a5e2cf84a Mon Sep 17 00:00:00 2001 From: PurHur Date: Sat, 30 May 2026 18:41:04 +0200 Subject: [PATCH 2/2] Align TraitsInterfacesAbstractTest with compile-time validation Master already rejects missing interface methods, abstract instantiation, and trait collisions at parseAndCompile via CompileError. Co-authored-by: Cursor --- test/unit/TraitsInterfacesAbstractTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/TraitsInterfacesAbstractTest.php b/test/unit/TraitsInterfacesAbstractTest.php index 64dce3a377b..a4321709e50 100644 --- a/test/unit/TraitsInterfacesAbstractTest.php +++ b/test/unit/TraitsInterfacesAbstractTest.php @@ -19,9 +19,9 @@ interface I { public function m(): void; } class C implements I {} echo "ok\n"; PHP; - $this->expectException(\LogicException::class); + $this->expectException(\CompileError::class); $this->expectExceptionMessage('abstract method'); - $runtime->run($runtime->parseAndCompile($code, 'iface_missing.php')); + $runtime->parseAndCompile($code, 'iface_missing.php'); } public function testAbstractClassInstantiationFails(): void @@ -32,9 +32,9 @@ public function testAbstractClassInstantiationFails(): void abstract class A { public function f(): int { return 1; } } new A(); PHP; - $this->expectException(\LogicException::class); + $this->expectException(\CompileError::class); $this->expectExceptionMessage('Cannot instantiate abstract class A'); - $runtime->run($runtime->parseAndCompile($code, 'abstract_new.php')); + $runtime->parseAndCompile($code, 'abstract_new.php'); } public function testTraitConflictFailsAtClassDeclaration(): void @@ -46,9 +46,9 @@ trait T1 { public function f(): int { return 1; } } trait T2 { public function f(): int { return 2; } } class C { use T1, T2; } PHP; - $this->expectException(\LogicException::class); + $this->expectException(\CompileError::class); $this->expectExceptionMessage('collision with'); - $runtime->run($runtime->parseAndCompile($code, 'trait_conflict.php')); + $runtime->parseAndCompile($code, 'trait_conflict.php'); } public function testValidInterfaceImplementationRuns(): void