diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index a10fdc3d858..bc3b9689bc9 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '56d1522'; + public const EXPECTED_EXTENSION_VERSION = '827a46f'; private static bool $active = false; diff --git a/tests/PHPStan/Analyser/StmtHandler/ConditionalExpressionsRecordingScope.php b/tests/PHPStan/Analyser/StmtHandler/ConditionalExpressionsRecordingScope.php new file mode 100644 index 00000000000..a1d71f8d6db --- /dev/null +++ b/tests/PHPStan/Analyser/StmtHandler/ConditionalExpressionsRecordingScope.php @@ -0,0 +1,43 @@ + */ + public array $added = []; + + public function enterForeach(MutatingScope $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $valueName, ?string $keyName, bool $valueByRef): MutatingScope + { + return $this; + } + + public function mergeWith(?MutatingScope $otherScope, bool $preserveVacuousConditionals = false): MutatingScope + { + return $this; + } + + /** + * @param ConditionalExpressionHolder[] $conditionalExpressionHolders + */ + public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): MutatingScope + { + $this->added[] = [$exprString, count($conditionalExpressionHolders)]; + + return $this; + } + +} diff --git a/tests/PHPStan/Analyser/StmtHandler/ForeachHandlerTest.php b/tests/PHPStan/Analyser/StmtHandler/ForeachHandlerTest.php new file mode 100644 index 00000000000..d2ddab90b8a --- /dev/null +++ b/tests/PHPStan/Analyser/StmtHandler/ForeachHandlerTest.php @@ -0,0 +1,62 @@ +getService('typeSpecifier')) + ->create(ScopeContext::create(__FILE__)) + ->assignVariable('a', $iterateeType, $iterateeType, TrinaryLogic::createYes()); + // the scope factory only creates MutatingScope: rebuild the scope as + // the subclass from its constructor arguments + $args = []; + $reflection = new ReflectionClass(MutatingScope::class); + foreach ($reflection->getMethod('__construct')->getParameters() as $parameter) { + $args[$parameter->getName()] = $reflection->getProperty($parameter->getName())->getValue($scope); + } + $scope = new ConditionalExpressionsRecordingScope(...$args); + + $container->getByType(ForeachHandler::class)->processStmt( + $container->getByType(NodeScopeResolver::class), + new Foreach_(new Variable('a'), new Variable('v'), ['keyVar' => new Variable('k')]), + $scope, + new ExpressionResultStorage(), + new NoopNodeCallback(), + StatementContext::createDeep(), + ); + + $this->assertSame([ + ['$v', $expectedHolders], + ['$a[$k]', $expectedHolders], + ], $scope->added); + } + +} diff --git a/turbo-ext/src/zv.h b/turbo-ext/src/zv.h index 9bf834d7cc5..72ec560d599 100644 --- a/turbo-ext/src/zv.h +++ b/turbo-ext/src/zv.h @@ -589,7 +589,15 @@ class Args static zend_always_inline void set(zval *slot, const zval *value) { ZVAL_COPY_VALUE(slot, value); } static zend_always_inline void set(zval *slot, zend_object *value) { ZVAL_OBJ(slot, value); } static zend_always_inline void set(zval *slot, zend_string *value) { ZVAL_STR(slot, value); } - static zend_always_inline void set(zval *slot, HashTable *value) { ZVAL_ARR(slot, value); } + /* immutable tables (a PHP [] literal) are wrapped non-refcounted, like + * ZVAL_EMPTY_ARRAY: the call's addref of its arguments must not touch them */ + static zend_always_inline void set(zval *slot, HashTable *value) + { + ZVAL_ARR(slot, value); + if (GC_FLAGS(value) & IS_ARRAY_IMMUTABLE) { + Z_TYPE_INFO_P(slot) = IS_ARRAY; + } + } static zend_always_inline void set(zval *slot, bool value) { ZVAL_BOOL(slot, value); } static zend_always_inline void set(zval *slot, zend_long value) { ZVAL_LONG(slot, value); } static zend_always_inline void set(zval *slot, double value) { ZVAL_DOUBLE(slot, value); }