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) { + $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 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 提供的私有构造器扁平化后等价于类的私有构造器 + $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..5a2f709a 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3172,11 +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"); - } + 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); } @@ -3833,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 @@ -3843,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) { @@ -3852,13 +3863,60 @@ class CompilerBase implements PropertyAccessContext } return $this->canAccessProtectedProperty( $scopeClassDef->getNamespacedName(false), - $classDef->getNamespacedName(false) + $declaringClass ); } // 类外部调用,只允许调用 public 方法 return true; } + /** + * 沿继承链查找实际调用的构造函数,包括项目类继承的内部类构造函数。 + * + * @return array{className: string, flags: int}|null + */ + protected function findConstructor(string $className): ?array + { + $current = $className; + 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; + } + + $constructor = Reflection::getClass($current)?->getConstructor(); + if ($constructor === null) { + return null; + } + return [ + 'className' => $constructor->getDeclaringClass()->getName(), + 'flags' => $constructor->getModifiers(), + ]; + } + 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