From 9cdc8b1b091c42fcfbbbc5f8d46d19e22d279bbf Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:41:24 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(compiler):=20=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E5=87=BD=E6=95=B0=E5=8F=AF=E8=A7=81=E6=80=A7?= =?UTF-8?q?=E4=BB=A5=E9=98=B2=E6=AD=A2=E9=9D=9E=E6=B3=95=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../code/constructor_visibility_private.php | 11 ++++ .../code/constructor_visibility_protected.php | 11 ++++ ...tor_visibility_protected_foreign_class.php | 19 +++++++ phpunit/code/trait_constructor_conflict.php | 29 ++++++++++ phpunit/code/trait_constructor_private.php | 21 +++++++ phpunit/code/trait_constructor_protected.php | 21 +++++++ phpunit/src/ConstructorVisibilityTest.php | 57 +++++++++++++++++++ src/CompilerBase.php | 42 ++++++++++++++ .../ctor-visibility-protected-subclass.phpt | 26 +++++++++ tests/compiler/trait/trait-ctor-basic.phpt | 27 +++++++++ tests/compiler/trait/trait-ctor-override.phpt | 32 +++++++++++ .../trait/trait-ctor-protected-subclass.phpt | 37 ++++++++++++ .../compiler/trait/trait-ctor-with-args.phpt | 30 ++++++++++ 13 files changed, 363 insertions(+) create mode 100644 phpunit/code/constructor_visibility_private.php create mode 100644 phpunit/code/constructor_visibility_protected.php create mode 100644 phpunit/code/constructor_visibility_protected_foreign_class.php create mode 100644 phpunit/code/trait_constructor_conflict.php create mode 100644 phpunit/code/trait_constructor_private.php create mode 100644 phpunit/code/trait_constructor_protected.php create mode 100644 phpunit/src/ConstructorVisibilityTest.php create mode 100644 tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt create mode 100644 tests/compiler/trait/trait-ctor-basic.phpt create mode 100644 tests/compiler/trait/trait-ctor-override.phpt create mode 100644 tests/compiler/trait/trait-ctor-protected-subclass.phpt create mode 100644 tests/compiler/trait/trait-ctor-with-args.phpt diff --git a/phpunit/code/constructor_visibility_private.php b/phpunit/code/constructor_visibility_private.php new file mode 100644 index 00000000..89f7c1aa --- /dev/null +++ b/phpunit/code/constructor_visibility_private.php @@ -0,0 +1,11 @@ +compile($file); + } catch (TestError | \RuntimeException $exception) { + $this->assertStringContainsString($expected, $exception->getMessage()); + return; + } + $this->fail('Expected compile-time error was not thrown'); + } + + public function testPrivateConstructorCannotBeCalledFromOutside(): void + { + // 私有构造器不能从类外部通过 `new` 调用 + $this->exec('Cannot call private TestClass::__construct()', 'constructor_visibility_private.php'); + } + + public function testProtectedConstructorCannotBeCalledFromGlobalScope(): void + { + // 保护构造器不能从全局作用域调用 + $this->exec('Cannot call protected TestClass::__construct()', 'constructor_visibility_protected.php'); + } + + public function testProtectedConstructorCannotBeCalledFromNonSubclass(): void + { + // 保护构造器不能从非子类的其它类内部调用 + $this->exec('Cannot call protected Base::__construct()', 'constructor_visibility_protected_foreign_class.php'); + } + + public function testTraitPrivateConstructorCannotBeCalledFromGlobalScope(): void + { + // trait 提供的私有构造器扁平化后等价于类的私有构造器 + $this->exec('Cannot call private TestClass::__construct()', 'trait_constructor_private.php'); + } + + public function testTraitProtectedConstructorCannotBeCalledFromGlobalScope(): void + { + // trait 提供的保护构造器扁平化后等价于类的保护构造器 + $this->exec('Cannot call protected TestClass::__construct()', 'trait_constructor_protected.php'); + } + + public function testConflictingTraitConstructorMustBeResolved(): void + { + // 两个 trait 各自声明 __construct 时必须显式解决冲突 + $this->exec('Trait `TraitB` method `__construct` already exists', 'trait_constructor_conflict.php'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index c183b5a9..387b03ff 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3177,6 +3177,15 @@ class CompilerBase implements PropertyAccessContext 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()' + ); + } } $cePtr = $this->getClassEntryPtr($className); } @@ -3859,6 +3868,39 @@ class CompilerBase implements PropertyAccessContext return true; } + /** + * 沿继承链查找定义 __construct 的类及其可见性标志。 + * 返回 ['classDef' => ClassDef, 'flags' => int],未找到(例如构造函数定义在内部类)时返回 null。 + * + * @return array{classDef: ClassDef, flags: int}|null + */ + protected function findConstructor(string $className): ?array + { + $current = $className; + while ($current !== '' && $current !== null) { + if (!$this->hasClass($current)) { + return null; + } + $classDef = $this->getClass($current); + if ($classDef->hasMethod('__construct')) { + return ['classDef' => $classDef, 'flags' => $classDef->getMethod('__construct')->flags]; + } + $current = $classDef->extends; + } + return null; + } + + protected function visibilityLabel(int $flags): string + { + if ($flags & Modifiers::PRIVATE) { + return 'private'; + } + if ($flags & Modifiers::PROTECTED) { + return 'protected'; + } + return 'public'; + } + protected function genDebugInfo(?NodeAbstract $stmt = null, string $functionName = '', int $startLine = 0): string { $code = ''; diff --git a/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt b/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt new file mode 100644 index 00000000..a1db317b --- /dev/null +++ b/tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt @@ -0,0 +1,26 @@ +--TEST-- +Constructor visibility - protected constructor accessible from subclass +--FILE-- + +--EXPECT-- +bool(true) diff --git a/tests/compiler/trait/trait-ctor-basic.phpt b/tests/compiler/trait/trait-ctor-basic.phpt new file mode 100644 index 00000000..1cf76b71 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-basic.phpt @@ -0,0 +1,27 @@ +--TEST-- +Trait __construct is used by the composing class +--FILE-- + +--EXPECT-- +trait ctor diff --git a/tests/compiler/trait/trait-ctor-override.phpt b/tests/compiler/trait/trait-ctor-override.phpt new file mode 100644 index 00000000..5b40f487 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-override.phpt @@ -0,0 +1,32 @@ +--TEST-- +Class __construct overrides the one provided by a trait +--FILE-- + +--EXPECT-- +class ctor diff --git a/tests/compiler/trait/trait-ctor-protected-subclass.phpt b/tests/compiler/trait/trait-ctor-protected-subclass.phpt new file mode 100644 index 00000000..319fd2f3 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-protected-subclass.phpt @@ -0,0 +1,37 @@ +--TEST-- +Trait protected __construct is accessible from a subclass +--FILE-- + +--EXPECT-- +base ctor +sub ctor diff --git a/tests/compiler/trait/trait-ctor-with-args.phpt b/tests/compiler/trait/trait-ctor-with-args.phpt new file mode 100644 index 00000000..5177cb28 --- /dev/null +++ b/tests/compiler/trait/trait-ctor-with-args.phpt @@ -0,0 +1,30 @@ +--TEST-- +Trait __construct with arguments and $this property access +--FILE-- +value = $value; + echo "value=" . $this->value . "\n"; + } +} + +class TestClass +{ + use TestTrait; +} + +function main() +{ + new TestClass(42); +} +?> +--EXPECT-- +value=42 -- 2.34.1 From 9b1f5b79421f6a3701043988f004075248010e27 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 24 Jul 2026 16:50:25 +0800 Subject: [PATCH 2/2] 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; } -- 2.34.1