From 9b1f5b79421f6a3701043988f004075248010e27 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 16:50:25 +0800 Subject: [PATCH] fix(compiler): harden constructor visibility checks --- ...nstructor_visibility_inherited_private.php | 17 +++++ ...onstructor_visibility_internal_private.php | 6 ++ .../constructor_visibility_namespaced.php | 15 +++++ phpunit/src/ConstructorVisibilityTest.php | 23 ++++++- src/CompilerBase.php | 66 ++++++++++++------- 5 files changed, 101 insertions(+), 26 deletions(-) create mode 100644 phpunit/code/constructor_visibility_inherited_private.php create mode 100644 phpunit/code/constructor_visibility_internal_private.php create mode 100644 phpunit/code/constructor_visibility_namespaced.php diff --git a/phpunit/code/constructor_visibility_inherited_private.php b/phpunit/code/constructor_visibility_inherited_private.php new file mode 100644 index 00000000..53ace63a --- /dev/null +++ b/phpunit/code/constructor_visibility_inherited_private.php @@ -0,0 +1,17 @@ +compile($file); - } catch (TestError | \RuntimeException $exception) { + } catch (TestError|RuntimeException $exception) { $this->assertStringContainsString($expected, $exception->getMessage()); return; } @@ -37,6 +37,27 @@ class ConstructorVisibilityTest extends BaseTest $this->exec('Cannot call protected Base::__construct()', 'constructor_visibility_protected_foreign_class.php'); } + public function testInheritedPrivateConstructorCannotBeCalledFromChildScope(): void + { + $this->exec( + 'Cannot call private PrivateConstructorParent::__construct()', + 'constructor_visibility_inherited_private.php' + ); + } + + public function testInternalPrivateConstructorCannotBeCalled(): void + { + $this->exec('Cannot call private Closure::__construct()', 'constructor_visibility_internal_private.php'); + } + + public function testNamespacedConstructorUsesPhpClassNameInDiagnostic(): void + { + $this->exec( + 'Cannot call private ConstructorVisibility\Hidden::__construct()', + 'constructor_visibility_namespaced.php' + ); + } + public function testTraitPrivateConstructorCannotBeCalledFromGlobalScope(): void { // trait 提供的私有构造器扁平化后等价于类的私有构造器 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 387b03ff..5a2f709a 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3172,20 +3172,17 @@ class CompilerBase implements PropertyAccessContext $className = $this->getNamespacedClassName($className); } $ctorClassName = $className; - if ($this->hasClass($className)) { - $classDef = $this->getClass($className); - if ($classDef->flags & Modifiers::ABSTRACT) { - $this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); - } - // 检查构造函数可见性(private/protected 在不可访问的上下文中被调用) - $ctor = $this->findConstructor($className); - if ($ctor !== null && !$this->checkAccessible($ctor['classDef'], $ctor['flags'])) { - $this->fatalError( - $expr, - 'Cannot call ' . $this->visibilityLabel($ctor['flags']) . ' ' - . $ctor['classDef']->getNamespacedName() . '::__construct()' - ); - } + if ($this->isAbstractClass($className)) { + $this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); + } + $constructor = $this->findConstructor($className); + if ($constructor !== null + && !$this->checkAccessibleByClassName($constructor['className'], $constructor['flags'])) { + $this->fatalError( + $expr, + 'Cannot call ' . $this->visibilityLabel($constructor['flags']) . ' ' + . $constructor['className'] . '::__construct()' + ); } $cePtr = $this->getClassEntryPtr($className); } @@ -3842,6 +3839,11 @@ class CompilerBase implements PropertyAccessContext } protected function checkAccessible(ClassDef $classDef, int $flags): bool + { + return $this->checkAccessibleByClassName($classDef->getNamespacedName(false), $flags); + } + + protected function checkAccessibleByClassName(string $declaringClass, int $flags): bool { $scopeClassDef = $this->classDef; if ($this->functionDef !== null @@ -3852,7 +3854,7 @@ class CompilerBase implements PropertyAccessContext // 私有方法,只能当前的类使用 if ($flags & Modifiers::PRIVATE) { return $scopeClassDef !== null - && strcasecmp($classDef->getNamespacedName(false), $scopeClassDef->getNamespacedName(false)) === 0; + && $this->isSameClassName($declaringClass, $scopeClassDef->getNamespacedName(false)); } // 保护方法,只能当前类和子类使用 if ($flags & Modifiers::PROTECTED) { @@ -3861,7 +3863,7 @@ class CompilerBase implements PropertyAccessContext } return $this->canAccessProtectedProperty( $scopeClassDef->getNamespacedName(false), - $classDef->getNamespacedName(false) + $declaringClass ); } // 类外部调用,只允许调用 public 方法 @@ -3869,23 +3871,37 @@ class CompilerBase implements PropertyAccessContext } /** - * 沿继承链查找定义 __construct 的类及其可见性标志。 - * 返回 ['classDef' => ClassDef, 'flags' => int],未找到(例如构造函数定义在内部类)时返回 null。 + * 沿继承链查找实际调用的构造函数,包括项目类继承的内部类构造函数。 * - * @return array{classDef: ClassDef, flags: int}|null + * @return array{className: string, flags: int}|null */ protected function findConstructor(string $className): ?array { $current = $className; - while ($current !== '' && $current !== null) { - if (!$this->hasClass($current)) { + while ($current !== '') { + if ($this->hasClass($current)) { + $classDef = $this->getClass($current); + if ($classDef->hasMethod('__construct')) { + return [ + 'className' => $classDef->getNamespacedName(false), + 'flags' => $classDef->getMethod('__construct')->flags, + ]; + } + $current = $classDef->extends; + continue; + } + if (!$this->isInternalClass($current)) { return null; } - $classDef = $this->getClass($current); - if ($classDef->hasMethod('__construct')) { - return ['classDef' => $classDef, 'flags' => $classDef->getMethod('__construct')->flags]; + + $constructor = Reflection::getClass($current)?->getConstructor(); + if ($constructor === null) { + return null; } - $current = $classDef->extends; + return [ + 'className' => $constructor->getDeclaringClass()->getName(), + 'flags' => $constructor->getModifiers(), + ]; } return null; }