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 { try {
$this->compile($file); $this->compile($file);
} catch (TestError | \RuntimeException $exception) { } catch (TestError|RuntimeException $exception) {
$this->assertStringContainsString($expected, $exception->getMessage()); $this->assertStringContainsString($expected, $exception->getMessage());
return; return;
} }
@ -37,6 +37,27 @@ class ConstructorVisibilityTest extends BaseTest
$this->exec('Cannot call protected Base::__construct()', 'constructor_visibility_protected_foreign_class.php'); $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 public function testTraitPrivateConstructorCannotBeCalledFromGlobalScope(): void
{ {
// trait 提供的私有构造器扁平化后等价于类的私有构造器 // trait 提供的私有构造器扁平化后等价于类的私有构造器

@ -3172,20 +3172,17 @@ class CompilerBase implements PropertyAccessContext
$className = $this->getNamespacedClassName($className); $className = $this->getNamespacedClassName($className);
} }
$ctorClassName = $className; $ctorClassName = $className;
if ($this->hasClass($className)) { if ($this->isAbstractClass($className)) {
$classDef = $this->getClass($className); $this->fatalError($expr, "abstract class `{$className}` cannot be instantiated");
if ($classDef->flags & Modifiers::ABSTRACT) { }
$this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); $constructor = $this->findConstructor($className);
} if ($constructor !== null
// 检查构造函数可见性(private/protected 在不可访问的上下文中被调用) && !$this->checkAccessibleByClassName($constructor['className'], $constructor['flags'])) {
$ctor = $this->findConstructor($className); $this->fatalError(
if ($ctor !== null && !$this->checkAccessible($ctor['classDef'], $ctor['flags'])) { $expr,
$this->fatalError( 'Cannot call ' . $this->visibilityLabel($constructor['flags']) . ' '
$expr, . $constructor['className'] . '::__construct()'
'Cannot call ' . $this->visibilityLabel($ctor['flags']) . ' ' );
. $ctor['classDef']->getNamespacedName() . '::__construct()'
);
}
} }
$cePtr = $this->getClassEntryPtr($className); $cePtr = $this->getClassEntryPtr($className);
} }
@ -3842,6 +3839,11 @@ class CompilerBase implements PropertyAccessContext
} }
protected function checkAccessible(ClassDef $classDef, int $flags): bool 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; $scopeClassDef = $this->classDef;
if ($this->functionDef !== null if ($this->functionDef !== null
@ -3852,7 +3854,7 @@ class CompilerBase implements PropertyAccessContext
// 私有方法,只能当前的类使用 // 私有方法,只能当前的类使用
if ($flags & Modifiers::PRIVATE) { if ($flags & Modifiers::PRIVATE) {
return $scopeClassDef !== null return $scopeClassDef !== null
&& strcasecmp($classDef->getNamespacedName(false), $scopeClassDef->getNamespacedName(false)) === 0; && $this->isSameClassName($declaringClass, $scopeClassDef->getNamespacedName(false));
} }
// 保护方法,只能当前类和子类使用 // 保护方法,只能当前类和子类使用
if ($flags & Modifiers::PROTECTED) { if ($flags & Modifiers::PROTECTED) {
@ -3861,7 +3863,7 @@ class CompilerBase implements PropertyAccessContext
} }
return $this->canAccessProtectedProperty( return $this->canAccessProtectedProperty(
$scopeClassDef->getNamespacedName(false), $scopeClassDef->getNamespacedName(false),
$classDef->getNamespacedName(false) $declaringClass
); );
} }
// 类外部调用,只允许调用 public 方法 // 类外部调用,只允许调用 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 protected function findConstructor(string $className): ?array
{ {
$current = $className; $current = $className;
while ($current !== '' && $current !== null) { while ($current !== '') {
if (!$this->hasClass($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; return null;
} }
$classDef = $this->getClass($current);
if ($classDef->hasMethod('__construct')) { $constructor = Reflection::getClass($current)?->getConstructor();
return ['classDef' => $classDef, 'flags' => $classDef->getMethod('__construct')->flags]; if ($constructor === null) {
return null;
} }
$current = $classDef->extends; return [
'className' => $constructor->getDeclaringClass()->getName(),
'flags' => $constructor->getModifiers(),
];
} }
return null; return null;
} }

Loading…
Cancel
Save