fix(compiler): harden constructor visibility checks

pull/28/head
韩天峰 1 month ago
parent 9cdc8b1b09
commit 9b1f5b7942
  1. 17
      phpunit/code/constructor_visibility_inherited_private.php
  2. 6
      phpunit/code/constructor_visibility_internal_private.php
  3. 15
      phpunit/code/constructor_visibility_namespaced.php
  4. 23
      phpunit/src/ConstructorVisibilityTest.php
  5. 66
      src/CompilerBase.php

@ -0,0 +1,17 @@
<?php
class PrivateConstructorParent
{
private function __construct()
{
}
}
class PrivateConstructorChild extends PrivateConstructorParent
{
}
function main()
{
new PrivateConstructorChild();
}

@ -0,0 +1,6 @@
<?php
function main()
{
new Closure();
}

@ -0,0 +1,15 @@
<?php
namespace ConstructorVisibility;
class Hidden
{
private function __construct()
{
}
}
function main()
{
new Hidden();
}

@ -12,7 +12,7 @@ class ConstructorVisibilityTest extends BaseTest
{
try {
$this->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 提供的私有构造器扁平化后等价于类的私有构造器

@ -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;
}

Loading…
Cancel
Save