fix(compiler): 检查构造函数可见性以防止非法调用

pull/28/head
Yurun 1 month ago
parent 2287695b44
commit 9cdc8b1b09
  1. 11
      phpunit/code/constructor_visibility_private.php
  2. 11
      phpunit/code/constructor_visibility_protected.php
  3. 19
      phpunit/code/constructor_visibility_protected_foreign_class.php
  4. 29
      phpunit/code/trait_constructor_conflict.php
  5. 21
      phpunit/code/trait_constructor_private.php
  6. 21
      phpunit/code/trait_constructor_protected.php
  7. 57
      phpunit/src/ConstructorVisibilityTest.php
  8. 42
      src/CompilerBase.php
  9. 26
      tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt
  10. 27
      tests/compiler/trait/trait-ctor-basic.phpt
  11. 32
      tests/compiler/trait/trait-ctor-override.phpt
  12. 37
      tests/compiler/trait/trait-ctor-protected-subclass.phpt
  13. 30
      tests/compiler/trait/trait-ctor-with-args.phpt

@ -0,0 +1,11 @@
<?php
class TestClass
{
private function __construct(){}
}
function main()
{
new TestClass;
}

@ -0,0 +1,11 @@
<?php
class TestClass
{
protected function __construct(){}
}
function main()
{
new TestClass;
}

@ -0,0 +1,19 @@
<?php
class Base
{
protected function __construct(){}
}
class Other
{
public static function make(): Base
{
return new Base();
}
}
function main()
{
Other::make();
}

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
trait TraitA
{
public function __construct()
{
echo "A\n";
}
}
trait TraitB
{
public function __construct()
{
echo "B\n";
}
}
class TestClass
{
use TraitA, TraitB;
}
function main()
{
new TestClass();
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
trait TestTrait
{
private function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass();
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
trait TestTrait
{
protected function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass();
}

@ -0,0 +1,57 @@
<?php
use TypePhp\Exception\TestError;
class ConstructorVisibilityTest extends BaseTest
{
/**
* 编译期错误在转换阶段直接以 TestError 抛出,而在桩文件生成阶段
* (gen_stub.php) 会被包成 RuntimeException,这里两者都要捕获。
*/
protected function exec(string $expected, string $file): void
{
try {
$this->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');
}
}

@ -3177,6 +3177,15 @@ class CompilerBase implements PropertyAccessContext
if ($classDef->flags & Modifiers::ABSTRACT) { if ($classDef->flags & Modifiers::ABSTRACT) {
$this->fatalError($expr, "abstract class `{$className}` cannot be instantiated"); $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); $cePtr = $this->getClassEntryPtr($className);
} }
@ -3859,6 +3868,39 @@ class CompilerBase implements PropertyAccessContext
return true; 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 protected function genDebugInfo(?NodeAbstract $stmt = null, string $functionName = '', int $startLine = 0): string
{ {
$code = ''; $code = '';

@ -0,0 +1,26 @@
--TEST--
Constructor visibility - protected constructor accessible from subclass
--FILE--
<?php
class Base
{
protected function __construct(){}
}
class Sub extends Base
{
public static function make(): Base
{
return new Base();
}
}
function main()
{
$obj = Sub::make();
var_dump($obj instanceof Base);
}
?>
--EXPECT--
bool(true)

@ -0,0 +1,27 @@
--TEST--
Trait __construct is used by the composing class
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
public function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass();
}
?>
--EXPECT--
trait ctor

@ -0,0 +1,32 @@
--TEST--
Class __construct overrides the one provided by a trait
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
public function __construct()
{
echo "trait ctor\n";
}
}
class TestClass
{
use TestTrait;
public function __construct()
{
echo "class ctor\n";
}
}
function main()
{
new TestClass();
}
?>
--EXPECT--
class ctor

@ -0,0 +1,37 @@
--TEST--
Trait protected __construct is accessible from a subclass
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
protected function __construct()
{
echo "base ctor\n";
}
}
class BaseClass
{
use TestTrait;
}
class SubClass extends BaseClass
{
public function __construct()
{
new BaseClass();
echo "sub ctor\n";
}
}
function main()
{
new SubClass();
}
?>
--EXPECT--
base ctor
sub ctor

@ -0,0 +1,30 @@
--TEST--
Trait __construct with arguments and $this property access
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
private int $value = 0;
public function __construct(int $value)
{
$this->value = $value;
echo "value=" . $this->value . "\n";
}
}
class TestClass
{
use TestTrait;
}
function main()
{
new TestClass(42);
}
?>
--EXPECT--
value=42
Loading…
Cancel
Save