TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
733 lines
25 KiB
733 lines
25 KiB
<?php
|
|
/**
|
|
* Range-proven loop variable optimizer.
|
|
*
|
|
* This pass narrows common PHP loop counters to php::Int without requiring
|
|
* `use native_types`. It is intentionally pattern-based: PHP arithmetic can
|
|
* widen integers to floats on overflow, so only monotonic counters with a
|
|
* statically bounded range are accepted.
|
|
*/
|
|
|
|
namespace TypePhp\Optimizer;
|
|
|
|
use TypePhp\Analysis\SsaBuilder;
|
|
use TypePhp\Analysis\SsaFlags;
|
|
use PhpParser\Node;
|
|
use PhpParser\Node\Expr;
|
|
use PhpParser\Node\Stmt;
|
|
use PhpParser\NodeAbstract;
|
|
|
|
trait LoopVarOptimizer
|
|
{
|
|
protected const array LOOP_NON_NEGATIVE_INT_FUNCTIONS = [
|
|
'strlen' => true,
|
|
'count' => true,
|
|
'sizeof' => true,
|
|
'mb_strlen' => true,
|
|
'substr_count' => true,
|
|
'mb_substr_count' => true,
|
|
];
|
|
|
|
protected function optimizeLoopVars(SsaBuilder $ssa): void
|
|
{
|
|
$stmts = $ssa->getStmts();
|
|
if (!$stmts) {
|
|
return;
|
|
}
|
|
|
|
$candidates = [];
|
|
$this->collectLoopVarCandidates($stmts, [], $candidates);
|
|
|
|
foreach ($candidates as $varName => $candidate) {
|
|
$escapedName = $this->escapeVarName($varName);
|
|
if ($this->hasArgument($escapedName)
|
|
|| $this->hasScopeGlobalVar($escapedName)
|
|
|| $this->isSuperGlobal($escapedName)) {
|
|
continue;
|
|
}
|
|
if (!$this->isLoopSsaVarStable($ssa, $varName)) {
|
|
continue;
|
|
}
|
|
if ($this->loopVarHasUnsafeUsage($varName, $stmts, $candidate['allowed'] ?? [])) {
|
|
continue;
|
|
}
|
|
$depFailed = false;
|
|
foreach ($candidate['deps'] ?? [] as $depName => $_) {
|
|
if (!$this->isLoopSsaVarStable($ssa, $depName)
|
|
|| $this->loopVarHasUnsafeUsage($depName, $stmts, $candidates[$depName]['allowed'] ?? [])) {
|
|
$depFailed = true;
|
|
break;
|
|
}
|
|
}
|
|
if ($depFailed) {
|
|
continue;
|
|
}
|
|
$this->context->localVars[$escapedName] = self::TYPE_INT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates
|
|
*/
|
|
protected function allowLoopNode(array &$candidates, string $varName, int $nodeId): void
|
|
{
|
|
$candidates[$varName]['allowed'][$nodeId] = true;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates
|
|
*/
|
|
protected function requireLoopVar(array &$candidates, string $varName, string $depName): void
|
|
{
|
|
if ($varName !== $depName) {
|
|
$candidates[$varName]['deps'][$depName] = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{id: int}> $safeVars
|
|
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates
|
|
*/
|
|
protected function collectLoopVarCandidates(array $stmts, array $safeVars, array &$candidates): void
|
|
{
|
|
foreach ($stmts as $stmt) {
|
|
if ($stmt instanceof Stmt\Expression) {
|
|
$this->trackLoopSafeAssignment($stmt->expr, $safeVars);
|
|
}
|
|
|
|
if ($stmt instanceof Stmt\While_) {
|
|
$this->tryCollectWhilePostDecCandidate($stmt, $safeVars, $candidates);
|
|
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates);
|
|
continue;
|
|
}
|
|
|
|
if ($stmt instanceof Stmt\For_) {
|
|
$this->tryCollectForCounterCandidate($stmt, $safeVars, $candidates);
|
|
$innerSafeVars = $safeVars;
|
|
foreach ($stmt->init as $init) {
|
|
$this->trackLoopSafeAssignment($init, $innerSafeVars);
|
|
}
|
|
$this->collectLoopVarCandidates($stmt->stmts, $innerSafeVars, $candidates);
|
|
continue;
|
|
}
|
|
|
|
if ($stmt instanceof Stmt\If_) {
|
|
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates);
|
|
foreach ($stmt->elseifs as $elseif) {
|
|
$this->collectLoopVarCandidates($elseif->stmts, $safeVars, $candidates);
|
|
}
|
|
if ($stmt->else) {
|
|
$this->collectLoopVarCandidates($stmt->else->stmts, $safeVars, $candidates);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($stmt instanceof Stmt\Do_) {
|
|
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates);
|
|
continue;
|
|
}
|
|
|
|
if ($stmt instanceof Stmt\Foreach_) {
|
|
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates);
|
|
continue;
|
|
}
|
|
|
|
if ($stmt instanceof Stmt\Switch_) {
|
|
foreach ($stmt->cases as $case) {
|
|
$this->collectLoopVarCandidates($case->stmts, $safeVars, $candidates);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($stmt instanceof Stmt\TryCatch) {
|
|
$this->collectLoopVarCandidates($stmt->stmts, $safeVars, $candidates);
|
|
foreach ($stmt->catches as $catch) {
|
|
$this->collectLoopVarCandidates($catch->stmts, $safeVars, $candidates);
|
|
}
|
|
if ($stmt->finally) {
|
|
$this->collectLoopVarCandidates($stmt->finally->stmts, $safeVars, $candidates);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{id: int}> $safeVars
|
|
*/
|
|
protected function trackLoopSafeAssignment(NodeAbstract $expr, array &$safeVars): void
|
|
{
|
|
if ($expr instanceof Expr\Assign
|
|
&& $expr->var instanceof Expr\Variable
|
|
&& is_string($expr->var->name)) {
|
|
$varName = $expr->var->name;
|
|
$info = $this->detectLoopIntExprInfo($expr->expr, $safeVars);
|
|
if ($info !== null) {
|
|
$safeVars[$varName] = [
|
|
'id' => spl_object_id($expr),
|
|
'nonNegative' => $info['nonNegative'],
|
|
'inclusiveSafe' => $info['inclusiveSafe'],
|
|
];
|
|
} else {
|
|
unset($safeVars[$varName]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if ($expr instanceof Expr\AssignOp
|
|
&& $expr->var instanceof Expr\Variable
|
|
&& is_string($expr->var->name)) {
|
|
unset($safeVars[$expr->var->name]);
|
|
return;
|
|
}
|
|
|
|
if (($expr instanceof Expr\PreInc || $expr instanceof Expr\PostInc
|
|
|| $expr instanceof Expr\PreDec || $expr instanceof Expr\PostDec)
|
|
&& $expr->var instanceof Expr\Variable
|
|
&& is_string($expr->var->name)) {
|
|
unset($safeVars[$expr->var->name]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{id: int}> $safeVars
|
|
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates
|
|
*/
|
|
protected function tryCollectWhilePostDecCandidate(Stmt\While_ $stmt, array $safeVars, array &$candidates): void
|
|
{
|
|
if (!$stmt->cond instanceof Expr\PostDec
|
|
|| !$stmt->cond->var instanceof Expr\Variable
|
|
|| !is_string($stmt->cond->var->name)) {
|
|
return;
|
|
}
|
|
|
|
$varName = $stmt->cond->var->name;
|
|
if (!isset($safeVars[$varName]) || !$safeVars[$varName]['nonNegative']) {
|
|
return;
|
|
}
|
|
|
|
if ($this->loopBodyMutatesAny($stmt->stmts, [$varName => true])) {
|
|
return;
|
|
}
|
|
|
|
$this->allowLoopNode($candidates, $varName, $safeVars[$varName]['id']);
|
|
$this->allowLoopNode($candidates, $varName, spl_object_id($stmt->cond));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{id: int}> $safeVars
|
|
* @param array<string, array{allowed?: array<int, bool>, deps?: array<string, bool>}> $candidates
|
|
*/
|
|
protected function tryCollectForCounterCandidate(Stmt\For_ $stmt, array $safeVars, array &$candidates): void
|
|
{
|
|
if (count($stmt->init) !== 1 || count($stmt->cond) !== 1 || count($stmt->loop) !== 1) {
|
|
return;
|
|
}
|
|
|
|
$init = $stmt->init[0];
|
|
if (!$init instanceof Expr\Assign
|
|
|| !$init->var instanceof Expr\Variable
|
|
|| !is_string($init->var->name)
|
|
|| $this->detectLoopIntExprInfo($init->expr, $safeVars) === null) {
|
|
return;
|
|
}
|
|
|
|
$counterName = $init->var->name;
|
|
$step = $this->detectLoopUnitStep($stmt->loop[0], $counterName);
|
|
if ($step === null) {
|
|
return;
|
|
}
|
|
|
|
$bound = $this->matchLoopBound($stmt->cond[0], $counterName, $safeVars, $step);
|
|
if ($bound === null) {
|
|
return;
|
|
}
|
|
|
|
$watchedVars = [$counterName => true];
|
|
foreach ($bound['vars'] as $varName => $_) {
|
|
$watchedVars[$varName] = true;
|
|
}
|
|
if ($this->loopBodyMutatesAny($stmt->stmts, $watchedVars)) {
|
|
return;
|
|
}
|
|
|
|
$conditionId = spl_object_id($stmt->cond[0]);
|
|
$this->allowLoopNode($candidates, $counterName, spl_object_id($init));
|
|
$this->allowLoopNode($candidates, $counterName, $conditionId);
|
|
$this->allowLoopNode($candidates, $counterName, spl_object_id($stmt->loop[0]));
|
|
|
|
foreach ($bound['intVars'] as $varName => $_) {
|
|
$this->requireLoopVar($candidates, $counterName, $varName);
|
|
$this->allowLoopNode($candidates, $varName, $safeVars[$varName]['id']);
|
|
$this->allowLoopNode($candidates, $varName, $conditionId);
|
|
}
|
|
}
|
|
|
|
protected function detectLoopUnitStep(NodeAbstract $expr, string $counterName): ?int
|
|
{
|
|
if (($expr instanceof Expr\PostInc || $expr instanceof Expr\PreInc)
|
|
&& $this->isVarNamed($expr->var, $counterName)) {
|
|
return 1;
|
|
}
|
|
|
|
if (($expr instanceof Expr\PostDec || $expr instanceof Expr\PreDec)
|
|
&& $this->isVarNamed($expr->var, $counterName)) {
|
|
return -1;
|
|
}
|
|
|
|
if ($expr instanceof Expr\AssignOp\Plus
|
|
&& $this->isVarNamed($expr->var, $counterName)
|
|
&& $expr->expr instanceof Node\Scalar\LNumber
|
|
&& $expr->expr->value === 1) {
|
|
return 1;
|
|
}
|
|
|
|
if ($expr instanceof Expr\AssignOp\Minus
|
|
&& $this->isVarNamed($expr->var, $counterName)
|
|
&& $expr->expr instanceof Node\Scalar\LNumber
|
|
&& $expr->expr->value === 1) {
|
|
return -1;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{id: int}> $safeVars
|
|
* @return array{vars: array<string, bool>, intVars: array<string, bool>}|null
|
|
*/
|
|
protected function matchLoopBound(NodeAbstract $expr, string $counterName, array $safeVars, int $step): ?array
|
|
{
|
|
if (!$expr instanceof Expr\BinaryOp\Smaller
|
|
&& !$expr instanceof Expr\BinaryOp\SmallerOrEqual
|
|
&& !$expr instanceof Expr\BinaryOp\Greater
|
|
&& !$expr instanceof Expr\BinaryOp\GreaterOrEqual) {
|
|
return null;
|
|
}
|
|
|
|
$inclusive = $expr instanceof Expr\BinaryOp\SmallerOrEqual
|
|
|| $expr instanceof Expr\BinaryOp\GreaterOrEqual;
|
|
$boundExpr = null;
|
|
if ($step > 0) {
|
|
if (($expr instanceof Expr\BinaryOp\Smaller || $expr instanceof Expr\BinaryOp\SmallerOrEqual)
|
|
&& $this->isVarNamed($expr->left, $counterName)) {
|
|
$boundExpr = $expr->right;
|
|
} elseif (($expr instanceof Expr\BinaryOp\Greater || $expr instanceof Expr\BinaryOp\GreaterOrEqual)
|
|
&& $this->isVarNamed($expr->right, $counterName)) {
|
|
$boundExpr = $expr->left;
|
|
}
|
|
} else {
|
|
if (($expr instanceof Expr\BinaryOp\Greater || $expr instanceof Expr\BinaryOp\GreaterOrEqual)
|
|
&& $this->isVarNamed($expr->left, $counterName)) {
|
|
$boundExpr = $expr->right;
|
|
} elseif (($expr instanceof Expr\BinaryOp\Smaller || $expr instanceof Expr\BinaryOp\SmallerOrEqual)
|
|
&& $this->isVarNamed($expr->right, $counterName)) {
|
|
$boundExpr = $expr->left;
|
|
}
|
|
}
|
|
|
|
if (!$boundExpr instanceof NodeAbstract) {
|
|
return null;
|
|
}
|
|
|
|
$info = $this->detectLoopIntExprInfo($boundExpr, $safeVars);
|
|
if ($info === null || ($inclusive && !$info['inclusiveSafe'])) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'vars' => $this->collectLoopExprVars($boundExpr),
|
|
'intVars' => $this->collectLoopExprSafeIntVars($boundExpr, $safeVars),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{id: int}> $safeVars
|
|
* @return array{nonNegative: bool, inclusiveSafe: bool}|null
|
|
*/
|
|
protected function detectLoopIntExprInfo(NodeAbstract $expr, array $safeVars): ?array
|
|
{
|
|
if ($expr instanceof Node\Scalar\LNumber) {
|
|
return [
|
|
'nonNegative' => $expr->value >= 0,
|
|
'inclusiveSafe' => $expr->value > PHP_INT_MIN && $expr->value < PHP_INT_MAX,
|
|
];
|
|
}
|
|
|
|
if ($expr instanceof Expr\Cast\Int_) {
|
|
return [
|
|
'nonNegative' => false,
|
|
'inclusiveSafe' => false,
|
|
];
|
|
}
|
|
|
|
if ($expr instanceof Expr\Variable && is_string($expr->name)) {
|
|
return $safeVars[$expr->name] ?? null;
|
|
}
|
|
|
|
if ($this->isLoopIntCall($expr)) {
|
|
$knownNonNegative = $this->isLoopKnownNonNegativeIntCall($expr);
|
|
return [
|
|
'nonNegative' => $knownNonNegative,
|
|
// PHP lengths/counts are bounded by addressable memory in
|
|
// supported runtimes, so inclusive loops over them stay int.
|
|
'inclusiveSafe' => $knownNonNegative,
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function isLoopIntCall(NodeAbstract $expr): bool
|
|
{
|
|
if (!$expr instanceof Expr\FuncCall
|
|
&& !$expr instanceof Expr\MethodCall
|
|
&& !$expr instanceof Expr\StaticCall
|
|
&& !$expr instanceof Expr\NullsafeMethodCall) {
|
|
return false;
|
|
}
|
|
|
|
return $this->detectTypeOfExpr($expr) === self::TYPE_INT;
|
|
}
|
|
|
|
protected function isLoopKnownNonNegativeIntCall(NodeAbstract $expr): bool
|
|
{
|
|
return $expr instanceof Expr\FuncCall
|
|
&& $expr->name instanceof Node\Name
|
|
&& isset(self::LOOP_NON_NEGATIVE_INT_FUNCTIONS[strtolower($expr->name->toString())]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, bool>
|
|
*/
|
|
protected function collectLoopExprVars(NodeAbstract $expr): array
|
|
{
|
|
$vars = [];
|
|
$this->collectLoopExprVarsInto($expr, $vars);
|
|
return $vars;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, bool> $vars
|
|
*/
|
|
protected function collectLoopExprVarsInto($node, array &$vars): void
|
|
{
|
|
if (!$node instanceof Node) {
|
|
return;
|
|
}
|
|
if ($node instanceof Expr\Variable && is_string($node->name)) {
|
|
$vars[$node->name] = true;
|
|
return;
|
|
}
|
|
foreach ($node->getSubNodeNames() as $name) {
|
|
$value = $node->$name;
|
|
if ($value instanceof Node) {
|
|
$this->collectLoopExprVarsInto($value, $vars);
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $item) {
|
|
$this->collectLoopExprVarsInto($item, $vars);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array{id: int}> $safeVars
|
|
* @return array<string, bool>
|
|
*/
|
|
protected function collectLoopExprSafeIntVars(NodeAbstract $expr, array $safeVars): array
|
|
{
|
|
$vars = [];
|
|
foreach ($this->collectLoopExprVars($expr) as $varName => $_) {
|
|
if (isset($safeVars[$varName])) {
|
|
$vars[$varName] = true;
|
|
}
|
|
}
|
|
return $vars;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, bool> $vars
|
|
*/
|
|
protected function loopBodyMutatesAny(array $stmts, array $vars): bool
|
|
{
|
|
foreach ($stmts as $stmt) {
|
|
if ($this->loopNodeMutatesAny($stmt, $vars)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, bool> $vars
|
|
*/
|
|
protected function loopNodeMutatesAny($node, array $vars): bool
|
|
{
|
|
if (!$node instanceof Node) {
|
|
return false;
|
|
}
|
|
|
|
if ($node instanceof Expr\Assign || $node instanceof Expr\AssignOp || $node instanceof Expr\AssignRef) {
|
|
if ($this->loopExprTargetsAny($node->var, $vars)
|
|
|| ($node instanceof Expr\AssignRef && $this->loopExprUsesAny($node->expr, $vars))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (($node instanceof Expr\PreInc || $node instanceof Expr\PostInc
|
|
|| $node instanceof Expr\PreDec || $node instanceof Expr\PostDec)
|
|
&& $this->loopExprTargetsAny($node->var, $vars)) {
|
|
return true;
|
|
}
|
|
|
|
if ($node instanceof Expr\FuncCall || $node instanceof Expr\MethodCall
|
|
|| $node instanceof Expr\StaticCall || $node instanceof Expr\NullsafeMethodCall) {
|
|
foreach ($node->args as $arg) {
|
|
if ($arg instanceof Node\Arg
|
|
&& $arg->byRef
|
|
&& $this->loopExprUsesAny($arg->value, $vars)) {
|
|
return true;
|
|
}
|
|
}
|
|
if ($node instanceof Expr\FuncCall
|
|
&& $node->name instanceof Node\Name
|
|
&& strtolower($node->name->toString()) === 'refval') {
|
|
foreach ($node->args as $arg) {
|
|
if ($arg instanceof Node\Arg && $this->loopExprUsesAny($arg->value, $vars)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($node instanceof Stmt\Unset_) {
|
|
foreach ($node->vars as $var) {
|
|
if ($this->loopExprTargetsAny($var, $vars)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($node instanceof Stmt\Foreach_) {
|
|
if ($this->loopExprTargetsAny($node->valueVar, $vars)
|
|
|| ($node->keyVar instanceof Node && $this->loopExprTargetsAny($node->keyVar, $vars))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
foreach ($node->getSubNodeNames() as $name) {
|
|
$value = $node->$name;
|
|
if ($value instanceof Node) {
|
|
if ($this->loopNodeMutatesAny($value, $vars)) {
|
|
return true;
|
|
}
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $item) {
|
|
if ($this->loopNodeMutatesAny($item, $vars)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, bool> $vars
|
|
*/
|
|
protected function loopExprTargetsAny($expr, array $vars): bool
|
|
{
|
|
while ($expr instanceof Expr\ArrayDimFetch || $expr instanceof Expr\PropertyFetch) {
|
|
$expr = $expr->var;
|
|
}
|
|
return $expr instanceof Expr\Variable
|
|
&& is_string($expr->name)
|
|
&& isset($vars[$expr->name]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, bool> $vars
|
|
*/
|
|
protected function loopExprUsesAny($node, array $vars): bool
|
|
{
|
|
if (!$node instanceof Node) {
|
|
return false;
|
|
}
|
|
if ($node instanceof Expr\Variable && is_string($node->name) && isset($vars[$node->name])) {
|
|
return true;
|
|
}
|
|
foreach ($node->getSubNodeNames() as $name) {
|
|
$value = $node->$name;
|
|
if ($value instanceof Node) {
|
|
if ($this->loopExprUsesAny($value, $vars)) {
|
|
return true;
|
|
}
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $item) {
|
|
if ($this->loopExprUsesAny($item, $vars)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected function isLoopSsaVarStable(SsaBuilder $ssa, string $varName): bool
|
|
{
|
|
foreach ($ssa->ssaVars as $ssaVar) {
|
|
if ($ssaVar->origName !== $varName) {
|
|
continue;
|
|
}
|
|
if ($ssaVar->flags & (SsaFlags::REFERENCE | SsaFlags::ESCAPED | SsaFlags::KILLED)) {
|
|
return false;
|
|
}
|
|
}
|
|
// SsaBuilder does not currently materialize all variables defined only
|
|
// in `for` headers. Those candidates are still checked by the AST
|
|
// whitelist and whole-function hazard scan in this optimizer.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, bool> $allowedNodes
|
|
*/
|
|
protected function loopVarHasUnsafeUsage(string $varName, array $stmts, array $allowedNodes): bool
|
|
{
|
|
foreach ($stmts as $stmt) {
|
|
if ($this->loopNodeHasIntHazard($stmt, $varName, $allowedNodes)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, bool> $allowedNodes
|
|
*/
|
|
protected function loopNodeHasIntHazard($node, string $varName, array $allowedNodes): bool
|
|
{
|
|
if (!$node instanceof Node) {
|
|
return false;
|
|
}
|
|
if (isset($allowedNodes[spl_object_id($node)])) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->loopExprHasIntHazard($node, $varName, $allowedNodes)) {
|
|
return true;
|
|
}
|
|
|
|
foreach ($node->getSubNodeNames() as $name) {
|
|
$value = $node->$name;
|
|
if ($value instanceof Node) {
|
|
if ($this->loopNodeHasIntHazard($value, $varName, $allowedNodes)) {
|
|
return true;
|
|
}
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $item) {
|
|
if ($this->loopNodeHasIntHazard($item, $varName, $allowedNodes)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, bool> $allowedNodes
|
|
*/
|
|
protected function loopExprHasIntHazard($expr, string $varName, array $allowedNodes): bool
|
|
{
|
|
if (!$expr instanceof Node) {
|
|
return false;
|
|
}
|
|
if (isset($allowedNodes[spl_object_id($expr)])) {
|
|
return false;
|
|
}
|
|
|
|
if ($expr instanceof Expr\BinaryOp) {
|
|
if (isset(self::SAFE_INT_BINARY_OPS[$expr->getType()])) {
|
|
return $this->loopExprHasIntHazard($expr->left, $varName, $allowedNodes)
|
|
|| $this->loopExprHasIntHazard($expr->right, $varName, $allowedNodes);
|
|
}
|
|
return $this->exprUsesVar($expr, $varName);
|
|
}
|
|
|
|
if ($expr instanceof Expr\Assign) {
|
|
if ($this->isVarNamed($expr->var, $varName)) {
|
|
return true;
|
|
}
|
|
return $this->loopExprHasIntHazard($expr->expr, $varName, $allowedNodes);
|
|
}
|
|
|
|
if ($expr instanceof Expr\AssignRef) {
|
|
return $this->isVarNamed($expr->var, $varName)
|
|
|| $this->exprUsesVar($expr->expr, $varName);
|
|
}
|
|
|
|
if ($expr instanceof Expr\AssignOp) {
|
|
if ($this->isVarNamed($expr->var, $varName)) {
|
|
return true;
|
|
}
|
|
return $this->loopExprHasIntHazard($expr->expr, $varName, $allowedNodes);
|
|
}
|
|
|
|
if ($expr instanceof Expr\PreInc || $expr instanceof Expr\PreDec
|
|
|| $expr instanceof Expr\PostInc || $expr instanceof Expr\PostDec) {
|
|
return $this->isVarNamed($expr->var, $varName);
|
|
}
|
|
|
|
if ($expr instanceof Expr\UnaryMinus) {
|
|
return $this->exprUsesVar($expr, $varName);
|
|
}
|
|
|
|
if ($expr instanceof Expr\FuncCall || $expr instanceof Expr\MethodCall
|
|
|| $expr instanceof Expr\StaticCall || $expr instanceof Expr\NullsafeMethodCall) {
|
|
foreach ($expr->args as $arg) {
|
|
if ($arg instanceof Node\Arg
|
|
&& $arg->byRef
|
|
&& $this->exprUsesVar($arg->value, $varName)) {
|
|
return true;
|
|
}
|
|
}
|
|
if ($expr instanceof Expr\FuncCall
|
|
&& $expr->name instanceof Node\Name
|
|
&& strtolower($expr->name->toString()) === 'refval') {
|
|
foreach ($expr->args as $arg) {
|
|
if ($arg instanceof Node\Arg && $this->exprUsesVar($arg->value, $varName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($expr instanceof Stmt\Unset_) {
|
|
foreach ($expr->vars as $var) {
|
|
if ($this->isVarNamed($var, $varName)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($expr->getSubNodeNames() as $name) {
|
|
$value = $expr->$name;
|
|
if ($value instanceof Node) {
|
|
if ($this->loopExprHasIntHazard($value, $varName, $allowedNodes)) {
|
|
return true;
|
|
}
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $item) {
|
|
if ($this->loopExprHasIntHazard($item, $varName, $allowedNodes)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|