From 9cdc8b1b091c42fcfbbbc5f8d46d19e22d279bbf Mon Sep 17 00:00:00 2001 From: Yurun Date: Thu, 23 Jul 2026 21:41:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(compiler):=20=E6=A3=80=E6=9F=A5=E6=9E=84?= =?UTF-8?q?=E9=80=A0=E5=87=BD=E6=95=B0=E5=8F=AF=E8=A7=81=E6=80=A7=E4=BB=A5?= =?UTF-8?q?=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