- Extract throw and try/catch/finally parsing methods to ExceptionControlFlowTrait - Add ExceptionControlFlowTrait import to CompilerBase class - Remove duplicate method implementations from CompilerBase - Move all exception handling related functionality to dedicated trait file - Maintain same functionality while improvingpull/17/head
parent
a9e93eda28
commit
fd22ea34d9
2 changed files with 240 additions and 219 deletions
@ -0,0 +1,238 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP. |
||||
* |
||||
* Lowers throw and try/catch/finally control flow. |
||||
*/ |
||||
|
||||
namespace TypePhp\Parser; |
||||
|
||||
use PhpParser\Node; |
||||
use PhpParser\Node\Expr; |
||||
use PhpParser\Node\Expr\Variable; |
||||
use PhpParser\NodeFinder; |
||||
use TypePhp\Symbol; |
||||
|
||||
trait ExceptionControlFlowTrait |
||||
{ |
||||
protected function parseThrow(mixed $expr): string |
||||
{ |
||||
if ($this->method === '__destruct') { |
||||
$this->warning($expr, "Throwing exception in {$this->getFullClassName()}::__destruct() may cause memory leak"); |
||||
} |
||||
$type = $this->detectTypeOfExpr($expr->expr); |
||||
if ($this->isNewExpr($expr->expr)) { |
||||
$ex = $this->parseExpr($expr->expr); |
||||
return 'php::throwException(' . $ex . ')'; |
||||
} elseif ($this->isVarExpr($expr->expr)) { |
||||
$ex = $this->parseIdentifier($expr->expr); |
||||
if ($type == self::TYPE_OBJECT) { |
||||
return 'php::throwException(' . $ex . ')'; |
||||
} |
||||
} else { |
||||
$ex = $this->parseExpr($expr->expr); |
||||
} |
||||
if ($type != self::TYPE_VAR) { |
||||
$this->fatalError($expr, 'Can only throw objects'); |
||||
} |
||||
return 'php::throwValue(' . $ex . ')'; |
||||
} |
||||
|
||||
protected function parseTryCatch(mixed $v): string |
||||
{ |
||||
$code = $this->parseBeforeStmtLines() . PHP_EOL; |
||||
$code .= 'try {'; |
||||
$finally = $v->finally; |
||||
$stmts = $finally ? $this->injectFinallyBeforeReturn($v->stmts, $finally->stmts) : $v->stmts; |
||||
|
||||
$catches = $v->catches; |
||||
|
||||
// Pre-register catch variables before parsing the try block so that |
||||
// injected finally code referencing catch variables (e.g. $e) does not |
||||
// trigger undefined-variable errors. |
||||
if ($finally) { |
||||
foreach ($catches as $catch) { |
||||
if ($catch->var) { |
||||
$varName = $this->parseIdentifier($catch->var); |
||||
if (!$this->hasVar($varName) && $this->stmtListUsesVariable($finally->stmts, $varName)) { |
||||
$this->addLocalVar($varName, self::TYPE_OBJECT); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
$code .= PHP_EOL; |
||||
$code .= $this->parseBlockStmts($stmts); |
||||
$code .= $this->getIndent() . '}' . PHP_EOL; |
||||
|
||||
$exVar = $this->genTmpVarName(); |
||||
$this->addLocalVar($exVar, self::TYPE_VAR); |
||||
|
||||
$code .= 'catch(zend_object *_ex) {' . PHP_EOL; |
||||
$code .= $this->getIndent() . $exVar . ' = php::catchException();' . PHP_EOL; |
||||
if ($catches) { |
||||
$catchMatched = $this->genTmpVarName(); |
||||
$code .= $this->getIndent() . 'bool ' . $catchMatched . ' = false;' . PHP_EOL; |
||||
$this->indentLevel++; |
||||
foreach ($catches as $catch) { |
||||
$code .= $this->parseCatch($catch, $exVar, $catchMatched, $finally?->stmts ?? []); |
||||
} |
||||
$this->indentLevel--; |
||||
} |
||||
$code .= '}' . PHP_EOL; |
||||
|
||||
if ($finally) { |
||||
$code .= $this->parseStmts($finally->stmts); |
||||
$code .= PHP_EOL; |
||||
} |
||||
$rethrow = $this->inGeneratorBody |
||||
? 'typephp_fiber_rethrow(' . $exVar . ');' |
||||
: 'php::throwException(php::Object(' . $exVar . '));'; |
||||
$code .= 'if (' . $exVar . ') {' . PHP_EOL . $this->getIndent() . $rethrow . PHP_EOL . $this->getIndent() . '}'; |
||||
|
||||
return $code; |
||||
} |
||||
|
||||
protected function injectFinallyBeforeReturn(array $stmts, array $finallyStmts, int $localControlDepth = 0): array |
||||
{ |
||||
$result = []; |
||||
foreach ($stmts as $stmt) { |
||||
if ($stmt instanceof Node\Stmt\Return_) { |
||||
if ($stmt->expr) { |
||||
$tmpVar = $this->addTmpVar(self::TYPE_VAR); |
||||
$result[] = new Node\Stmt\Expression(new Expr\Assign(new Variable($tmpVar), $stmt->expr)); |
||||
array_push($result, ...$this->cloneStmtList($finallyStmts)); |
||||
$result[] = new Node\Stmt\Return_(new Variable($tmpVar)); |
||||
continue; |
||||
} |
||||
array_push($result, ...$this->cloneStmtList($finallyStmts)); |
||||
$result[] = $stmt; |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Node\Stmt\Break_ || $stmt instanceof Node\Stmt\Continue_) { |
||||
$level = $stmt->num instanceof Node\Scalar\Int_ ? $stmt->num->value : 1; |
||||
if ($level > $localControlDepth) { |
||||
array_push($result, ...$this->cloneStmtList($finallyStmts)); |
||||
} |
||||
$result[] = $stmt; |
||||
continue; |
||||
} |
||||
|
||||
if ($stmt instanceof Node\Stmt\Goto_) { |
||||
array_push($result, ...$this->cloneStmtList($finallyStmts)); |
||||
$result[] = $stmt; |
||||
continue; |
||||
} |
||||
|
||||
$result[] = $this->injectFinallyBeforeReturnInStmt($stmt, $finallyStmts, $localControlDepth); |
||||
} |
||||
return $result; |
||||
} |
||||
|
||||
protected function injectFinallyBeforeReturnInStmt(Node\Stmt $stmt, array $finallyStmts, int $localControlDepth): Node\Stmt |
||||
{ |
||||
if ($stmt instanceof Node\Stmt\If_) { |
||||
$stmt = clone $stmt; |
||||
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth); |
||||
foreach ($stmt->elseifs as $index => $elseIf) { |
||||
$elseIf = clone $elseIf; |
||||
$elseIf->stmts = $this->injectFinallyBeforeReturn($elseIf->stmts, $finallyStmts, $localControlDepth); |
||||
$stmt->elseifs[$index] = $elseIf; |
||||
} |
||||
if ($stmt->else) { |
||||
$stmt->else = clone $stmt->else; |
||||
$stmt->else->stmts = $this->injectFinallyBeforeReturn($stmt->else->stmts, $finallyStmts, $localControlDepth); |
||||
} |
||||
return $stmt; |
||||
} |
||||
|
||||
if ($stmt instanceof Node\Stmt\For_ |
||||
|| $stmt instanceof Node\Stmt\Foreach_ |
||||
|| $stmt instanceof Node\Stmt\While_ |
||||
|| $stmt instanceof Node\Stmt\Do_ |
||||
) { |
||||
$stmt = clone $stmt; |
||||
$stmt->stmts = $this->injectFinallyBeforeReturn($stmt->stmts, $finallyStmts, $localControlDepth + 1); |
||||
return $stmt; |
||||
} |
||||
|
||||
if ($stmt instanceof Node\Stmt\Switch_) { |
||||
$stmt = clone $stmt; |
||||
foreach ($stmt->cases as $index => $case) { |
||||
$case = clone $case; |
||||
$case->stmts = $this->injectFinallyBeforeReturn($case->stmts, $finallyStmts, $localControlDepth + 1); |
||||
$stmt->cases[$index] = $case; |
||||
} |
||||
return $stmt; |
||||
} |
||||
|
||||
return $stmt; |
||||
} |
||||
|
||||
protected function cloneStmtList(array $stmts): array |
||||
{ |
||||
return array_map(static fn (Node\Stmt $stmt): Node\Stmt => clone $stmt, $stmts); |
||||
} |
||||
|
||||
protected function stmtListUsesVariable(array $stmts, string $name): bool |
||||
{ |
||||
$nodeFinder = new NodeFinder(); |
||||
foreach ($nodeFinder->findInstanceOf($stmts, Variable::class) as $var) { |
||||
if (is_string($var->name) && $this->escapeVarName($var->name) === $name) { |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
protected function parseCatch(Node\Stmt\Catch_ $catch, string $exVar, string $catchMatched, array $finallyStmts = []): string |
||||
{ |
||||
$types = $catch->types; |
||||
$var = $catch->var ? $this->parseIdentifier($catch->var) : ''; |
||||
if ($var !== '' && !$this->hasVar($var)) { |
||||
$this->addLocalVar($var, self::TYPE_OBJECT); |
||||
} |
||||
|
||||
$code = $this->parseBeforeStmtLines() . PHP_EOL; |
||||
$code .= $this->getIndent() . 'if (!' . $catchMatched . ' && ' . $exVar . ' && '; |
||||
$conditions = []; |
||||
foreach ($types as $type) { |
||||
if ($this->isNameExpr($type) or $this->isFullNameExpr($type)) { |
||||
$class = $this->getNamespacedClassName($this->parseIdentifier($type)); |
||||
$ce = $this->getClassEntryPtr($class); |
||||
$conditions[] = Symbol::instanceOf() . '(' . $exVar . ', ' . $ce . ')'; |
||||
} else { |
||||
$this->fatalError($type, 'Unsupported catch type'); |
||||
} |
||||
} |
||||
|
||||
$code .= '(' . implode(' || ', $conditions) . ')) {' . PHP_EOL; |
||||
$this->indentLevel++; |
||||
$code .= $this->getIndent() . $catchMatched . ' = true;' . PHP_EOL; |
||||
if ($var !== '') { |
||||
$code .= $this->getIndent() . $var . ' = ' . $exVar . ';' . PHP_EOL; |
||||
} |
||||
$code .= $this->getIndent() . "{$exVar} = php::null;" . PHP_EOL; |
||||
$stmts = $finallyStmts ? $this->injectFinallyBeforeReturn($catch->stmts, $finallyStmts) : $catch->stmts; |
||||
if ($finallyStmts) { |
||||
$code .= $this->getIndent() . 'try {' . PHP_EOL; |
||||
$this->indentLevel++; |
||||
$code .= $this->parseStmts($stmts); |
||||
$this->indentLevel--; |
||||
$code .= $this->getIndent() . '} catch(zend_object *_catch_throw_ex) {' . PHP_EOL; |
||||
$this->indentLevel++; |
||||
$code .= $this->getIndent() . "{$exVar} = php::catchException();" . PHP_EOL; |
||||
$this->indentLevel--; |
||||
$code .= $this->getIndent() . '}' . PHP_EOL; |
||||
} else { |
||||
$code .= $this->parseStmts($stmts); |
||||
} |
||||
$this->indentLevel--; |
||||
$code .= $this->getIndent() . '}'; |
||||
|
||||
return $code; |
||||
} |
||||
|
||||
} |
||||
|
||||
Loading…
Reference in new issue