Merge pull request 'fix(compiler): 检查构造函数可见性以防止非法调用' (#28) from compiler-fix-ctor-visibility-check into master

Reviewed-on: #28
pull/40/head
韩天峰 1 month ago
commit 5ba77eda81
  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. 11
      phpunit/code/constructor_visibility_private.php
  5. 11
      phpunit/code/constructor_visibility_protected.php
  6. 19
      phpunit/code/constructor_visibility_protected_foreign_class.php
  7. 29
      phpunit/code/trait_constructor_conflict.php
  8. 21
      phpunit/code/trait_constructor_private.php
  9. 21
      phpunit/code/trait_constructor_protected.php
  10. 78
      phpunit/src/ConstructorVisibilityTest.php
  11. 72
      src/CompilerBase.php
  12. 26
      tests/compiler/object_ctor/ctor-visibility-protected-subclass.phpt
  13. 27
      tests/compiler/trait/trait-ctor-basic.phpt
  14. 32
      tests/compiler/trait/trait-ctor-override.phpt
  15. 37
      tests/compiler/trait/trait-ctor-protected-subclass.phpt
  16. 30
      tests/compiler/trait/trait-ctor-with-args.phpt

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

@ -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,78 @@
<?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 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');
}
}

@ -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 = '';

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