feat(compiler): 为类方法自动推断返回类型并优化属性访问检查

- 在 CompilerBase 中添加类方法返回类型自动推断逻辑
- 为魔术方法设置特定的返回类型标识符
- 移除 isInferredPhpDocType 条件判断简化类型处理
- 更新对象处理器设置使用自定义处理器覆盖 unset 操作
- 修改 SsaPropOptimizer 中的危险属性操作检测逻辑
- 移除对 unset($o->prop) 的特殊处理因为对象处理器已拒绝属性取消设置
- 移除动态调用对象暴露的安全性检查优化性能
- 删除不再需要的 isSafeObjectExposureCall 等辅助方法
pull/3/head
韩天峰 2 months ago
parent 25aeff51a7
commit 4acb632c13
  1. 14
      phpunit/src/SsaAnalysisTest.php
  2. 27
      src/Php/CompilerBase.php
  3. 90
      src/Php/Optimizer/SsaPropOptimizer.php
  4. 8
      src/gen_stub.php
  5. 2
      tests/aot/type_decl/012.phpt

@ -647,7 +647,7 @@ class SsaAnalysisTest extends TestCase
));
$result = $this->invoke('hasDangerousPropOps', 'obj', [$unset, $read]);
$this->assertTrue($result, 'unset($obj->prop) before a later access should be detected');
$this->assertFalse($result, 'unset($obj->prop) is blocked by the object handlers and cannot invalidate a hoisted reference');
}
public function testHasDangerousPropOpsUnsetDifferentObj(): void
@ -735,9 +735,9 @@ class SsaAnalysisTest extends TestCase
{
$objVar = new Expr\Variable('obj');
$propFetch = new Expr\PropertyFetch($objVar, 'prop');
$unset = new Stmt\Unset_([$propFetch]);
$assignRef = new Expr\AssignRef(new Expr\Variable('ref'), $propFetch);
$ifStmt = new Stmt\If_(new Expr\ConstFetch(new Node\Name('true')), [
'stmts' => [$unset],
'stmts' => [new Stmt\Expression($assignRef)],
'elseifs' => [],
'else' => null,
]);
@ -747,7 +747,7 @@ class SsaAnalysisTest extends TestCase
));
$result = $this->invoke('hasDangerousPropOps', 'obj', [$ifStmt, $read]);
$this->assertTrue($result, 'unset inside if before a later access should be detected');
$this->assertTrue($result, '&$obj->prop inside if before a later access should be detected');
}
public function testHasDangerousPropOpsNestedRefvalInAssignment(): void
@ -796,13 +796,13 @@ class SsaAnalysisTest extends TestCase
public function testCollectDangerousPropOpsDynamicPropertyWildcard(): void
{
$propFetch = new Expr\PropertyFetch(new Expr\Variable('obj'), new Expr\Variable('prop'));
$unset = new Stmt\Unset_([$propFetch]);
$write = new Stmt\Expression(new Expr\Assign($propFetch, new Scalar\LNumber(5)));
$read = new Stmt\Expression(new Expr\Assign(
new Expr\Variable('value'),
new Expr\PropertyFetch(new Expr\Variable('obj'), 'a')
));
$result = $this->invoke('collectDangerousPropOps', 'obj', [$unset, $read]);
$result = $this->invoke('collectDangerousPropOps', 'obj', [$write, $read]);
$this->assertSame(['a' => true], $result);
}
@ -816,7 +816,7 @@ class SsaAnalysisTest extends TestCase
));
$result = $this->invoke('collectDangerousPropOps', 'obj', [$stmt, $read]);
$this->assertSame(['a' => true], $result);
$this->assertSame([], $result, 'Passing the object to a dynamic call cannot unset the property, so it is not dangerous');
}
public function testCollectDangerousPropOpsInternalFunctionObjectArgumentIsSafe(): void

@ -5726,6 +5726,33 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function genEmbeddedCode(NodeAbstract $stmt): string
{
if ($stmt instanceof Node\Stmt\Class_) {
$stmt = clone $stmt;
$traverser = new \PhpParser\NodeTraverser();
$traverser->addVisitor(new class extends \PhpParser\NodeVisitorAbstract {
public function enterNode(Node $node)
{
if (!$node instanceof Node\Stmt\ClassMethod || $node->returnType !== null) {
return null;
}
$returnType = match (strtolower($node->name->toString())) {
'__construct', '__destruct' => null,
'__set', '__unserialize', '__unset', '__wakeup', '__clone' => 'void',
'__tostring' => 'string',
'__serialize', '__debuginfo', '__sleep' => 'array',
'__isset' => 'bool',
'__set_state' => 'object',
default => 'mixed',
};
if ($returnType !== null) {
$node->returnType = new Node\Identifier($returnType);
}
return null;
}
});
$stmt = $traverser->traverse([$stmt])[0];
}
return $this->printer->prettyPrint([$stmt]);
}

@ -13,17 +13,19 @@
* 2. No REFERENCE / ESCAPED / KILLED flags on the object's SSA vars
* 3. Class has no __get / __set magic methods
* 4. Property has a declared native type (int or float)
* 5. No unset($o->prop) on the property
* 6. No &$o->prop (reference capture of the property)
* 7. No func(&$o->prop) (property passed by reference)
* 8. First access is not inside a loop or nested block scope
* 5. No &$o->prop (reference capture of the property)
* 6. No func(&$o->prop) (property passed by reference)
* 7. First access is not inside a loop or nested block scope
*
* unset($o->prop) and exposing the object to dynamic calls are NOT dangerous:
* the object handlers reject property unset, so a hoisted reference cannot be
* invalidated by either path.
*/
namespace PhpAot\Php\Optimizer;
use PhpAot\Php\Analysis\SsaBuilder;
use PhpAot\Php\Analysis\SsaFlags;
use PhpAot\Php\Reflection;
use PhpParser\Node;
use PhpParser\Node\Expr;
@ -263,9 +265,11 @@ trait SsaPropOptimizer
* Scan function body for dangerous operations on object properties.
*
* Detects:
* - unset($o->prop) — destroys property slot
* - $ref = &$o->prop — property becomes reference, zval type changes
* - func(&$o->prop) or $obj->method(&$o->prop) — property passed by ref
*
* unset($o->prop) and passing the object to dynamic calls are intentionally
* not treated as dangerous: the object handlers reject property unset.
*/
protected function hasDangerousPropOps(string $objName, array $stmts): bool
{
@ -302,9 +306,10 @@ trait SsaPropOptimizer
if ($node instanceof Node\Stmt\Unset_) {
foreach ($node->vars as $var) {
// unset($o->prop) cannot destroy the slot: the object handlers
// reject property unset, so a hoisted reference stays valid.
$propName = $this->getPropNameOfObj($var, $objName);
if ($propName !== null) {
$events[] = ['kind' => 'danger', 'prop' => $propName];
$this->collectPropEventsInDynamicParts($var, $objName, $events);
} else {
$this->collectPropEvents($var, $objName, $events);
@ -372,17 +377,12 @@ trait SsaPropOptimizer
$this->collectPropEvents($arg->value, $objName, $events);
}
}
if (!$this->isSafeObjectExposureCall($node)) {
if (($node instanceof Expr\MethodCall || $node instanceof Expr\NullsafeMethodCall)
&& $this->isVarNamed($node->var, $objName)) {
$events[] = ['kind' => 'danger', 'prop' => '*'];
}
foreach ($node->args as $arg) {
if ($this->exprMayExposeObject($arg->value, $objName)) {
$events[] = ['kind' => 'danger', 'prop' => '*'];
}
}
}
// Exposing the object to a dynamic call (passing it as an argument or
// invoking a non-internal method on it) can no longer invalidate a
// hoisted property: the callee cannot unset the property, since the
// object handlers reject property unset. Only explicit by-reference
// captures (handled above) and direct &/refval on the property remain
// dangerous, so the receiver/argument exposure check is unnecessary.
return;
}
@ -438,60 +438,6 @@ trait SsaPropOptimizer
}
}
protected function isSafeObjectExposureCall(Expr\FuncCall|Expr\MethodCall|Expr\StaticCall|Expr\NullsafeMethodCall $node): bool
{
if ($node instanceof Expr\FuncCall) {
return $node->name instanceof Node\Name
&& $this->isInternalFunctionName($node->name);
}
if ($node instanceof Expr\StaticCall) {
return $node->class instanceof Node\Name
&& $node->name instanceof Node\Identifier
&& $this->isInternalClassCall($this->resolveStaticCallClassForSafety($node->class), $node->name->toString());
}
if (!$node->name instanceof Node\Identifier) {
return false;
}
$className = $this->detectClassOfExpr($node->var);
return $this->isInternalClassCall($className, $node->name->toString());
}
protected function isInternalFunctionName(Node\Name $name): bool
{
$functionName = ltrim($name->toString(), '\\');
if (str_contains($functionName, '\\')) {
return false;
}
return $this->isInternalFunction($functionName) || $this->isInternalFunction(strtolower($functionName));
}
protected function resolveStaticCallClassForSafety(Node\Name $classNode): string
{
$className = $classNode->toString();
if ($className === 'self') {
return $this->classDef ? $this->getFullClassName() : '';
}
if ($className === 'parent') {
return $this->classDef ? $this->classDef->extends : '';
}
if ($className === 'static') {
return '';
}
return $this->getNamespacedClassName($className);
}
protected function isInternalClassCall(string $className, string $methodName): bool
{
return $className !== ''
&& ($this->isInternalClass($className) || $this->isInternalInterface($className))
&& Reflection::hasMethod($className, $methodName);
}
/**
* Dynamic property name expressions can contain normal property reads:
* unset($o->{$other->name}) should still record $other->name if relevant.

@ -1181,7 +1181,7 @@ class ReturnInfo {
* based on the PHP version. Separate to allow using early returns
*/
private function beginArgInfoCompatible(string $funcInfoName, int $minArgs): string {
$effectiveType = $this->type ?? ($this->isInferredPhpDocType ? null : $this->phpDocType);
$effectiveType = $this->type ?? $this->phpDocType;
if ($effectiveType !== null) {
if (null !== $simpleReturnType = $effectiveType->tryToSimpleType()) {
if ($simpleReturnType->isBuiltin) {
@ -3781,7 +3781,11 @@ class ClassInfo {
$code .= $php80CondEnd;
}
$code .= "\n\tclass_entry->default_object_handlers = &php_aot_object_handlers;\n";
$code .= "\n\tstatic zend_object_handlers class_object_handlers;";
$code .= "\n\tmemcpy(&class_object_handlers, class_entry->default_object_handlers, sizeof(zend_object_handlers));";
$code .= "\n\tclass_object_handlers.unset_property = php_aot_unset_typed_property;";
$code .= "\n\tclass_entry->default_object_handlers = &class_object_handlers;";
$code .= "\n";
$code .= "\n\treturn class_entry;\n";

@ -3,7 +3,7 @@ Type Declarations
--FILE--
<?php
class SmallerTenClass {
public static function smallerTen($input) {
public static function smallerTen($input, $key) {
return $input < 10;
}
}

Loading…
Cancel
Save