From 57833e1bd950b8cf56926dfcaa188ab2fb48abf4 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 30 Jul 2026 20:57:41 +0200 Subject: [PATCH] Add ShellExecHandler and IssetExprHandler Ported from the resolve-type-rewrite-2 branch in the 2.2.x handler shape (resolveType/specifyTypes instead of result callbacks). The backtick operator had no handler: its interpolated parts were never walked, so node callbacks (rules, collectors) did not see the expressions inside the backticks and their throw/impure points were lost. The handler walks the parts like shell_exec() arguments, collects the implicit __toString throw points, and prices the operator as string|false|null instead of mixed. IssetExpr - the certainty marker type specifications wrap around an isset-tested expression - had no handler either and priced as mixed. The specifications only ever read its certainty, but anything asking for its type now gets the inner expression's type, letting the marker be priced like any other node rather than being special-cased in the resolution paths. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7 --- src/Analyser/ExprHandler/ShellExecHandler.php | 96 +++++++++++++++++++ .../ExprHandler/Virtual/IssetExprHandler.php | 71 ++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 src/Analyser/ExprHandler/ShellExecHandler.php create mode 100644 src/Analyser/ExprHandler/Virtual/IssetExprHandler.php diff --git a/src/Analyser/ExprHandler/ShellExecHandler.php b/src/Analyser/ExprHandler/ShellExecHandler.php new file mode 100644 index 0000000000..fa756b8865 --- /dev/null +++ b/src/Analyser/ExprHandler/ShellExecHandler.php @@ -0,0 +1,96 @@ + + */ +#[AutowiredService] +final class ShellExecHandler implements ExprHandler +{ + + public function __construct( + private ImplicitToStringCallHelper $implicitToStringCallHelper, + private ExpressionResultFactory $expressionResultFactory, + ) + { + } + + public function supports(Expr $expr): bool + { + return $expr instanceof ShellExec; + } + + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult + { + $beforeScope = $scope; + $hasYield = false; + $throwPoints = []; + $impurePoints = []; + $isAlwaysTerminating = false; + foreach ($expr->parts as $part) { + if (!$part instanceof Expr) { + continue; + } + $partResult = $nodeScopeResolver->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep()); + $hasYield = $hasYield || $partResult->hasYield(); + $throwPoints = array_merge($throwPoints, $partResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $partResult->getImpurePoints()); + + $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($part, $scope); + $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints()); + + $isAlwaysTerminating = $isAlwaysTerminating || $partResult->isAlwaysTerminating(); + $scope = $partResult->getScope(); + } + + return $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + ); + } + + public function resolveType(MutatingScope $scope, Expr $expr): Type + { + return TypeCombinator::union(new StringType(), new ConstantBooleanType(false), new NullType()); + } + + public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + { + return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + } + +} diff --git a/src/Analyser/ExprHandler/Virtual/IssetExprHandler.php b/src/Analyser/ExprHandler/Virtual/IssetExprHandler.php new file mode 100644 index 0000000000..8709610e6c --- /dev/null +++ b/src/Analyser/ExprHandler/Virtual/IssetExprHandler.php @@ -0,0 +1,71 @@ + + */ +#[AutowiredService] +final class IssetExprHandler implements ExprHandler +{ + + public function __construct(private ExpressionResultFactory $expressionResultFactory) + { + } + + public function supports(Expr $expr): bool + { + return $expr instanceof IssetExpr; + } + + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult + { + // a virtual node handler - the caller will only be interested in the + // type; the inner expr is not processed, its type is just reported + + return $this->expressionResultFactory->create( + $scope, + beforeScope: $scope, + expr: $expr, + hasYield: false, + isAlwaysTerminating: false, + throwPoints: [], + impurePoints: [], + ); + } + + public function resolveType(MutatingScope $scope, Expr $expr): Type + { + return $scope->getType($expr->getExpr()); + } + + public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes + { + return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context); + } + +}