diff --git a/src/Analyser/ExprHandler/ArrayHandler.php b/src/Analyser/ExprHandler/ArrayHandler.php index ad28fe76aa0..bda5a1b6ecd 100644 --- a/src/Analyser/ExprHandler/ArrayHandler.php +++ b/src/Analyser/ExprHandler/ArrayHandler.php @@ -9,6 +9,7 @@ use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Scalar; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; @@ -24,6 +25,7 @@ use PHPStan\Analyser\VariableWriteOffset; use PHPStan\Dependency\Dependencies; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Node\Expr\TypeExpr; use PHPStan\Node\LiteralArrayItem; use PHPStan\Node\LiteralArrayNode; use PHPStan\Node\Variable\VariableWrite; @@ -32,15 +34,15 @@ use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\ArrayType; use PHPStan\Type\CallableType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_key_exists; -use function array_map; use function array_merge; -use function array_values; use function count; use function is_int; use function max; @@ -70,8 +72,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex { $beforeScope = $scope; $itemNodes = []; - $itemResults = []; + $itemTypes = []; $variableFlows = []; + $dependencies = []; $hasYield = false; $throwPoints = []; $impurePoints = []; @@ -83,6 +86,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $passedToType = $this->getExpectedArrayType($context->getPassedToType()); $nativePassedToType = $this->getExpectedArrayType($context->getNativePassedToType()); $hasExpectedType = $passedToType !== null || $nativePassedToType !== null; + $hasArrayRef = self::hasArrayReference($expr); $nextIndex = 0; foreach ($expr->items as $arrayItem) { $itemNodes[] = new LiteralArrayItem($scope, $arrayItem); @@ -90,11 +94,26 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $keyResult = null; if ($arrayItem->key !== null) { $keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeepKeepingValueFlow()); - $itemResults[spl_object_id($arrayItem->key)] = $keyResult; - $variableFlows[] = $keyResult->getVariableFlow(); + if (!$arrayItem->key instanceof Scalar\String_ && !$arrayItem->key instanceof Scalar\Int_ && !$arrayItem->key instanceof Scalar\Float_) { + $itemTypes[spl_object_id($arrayItem->key)] = [$keyResult->getType(), $keyResult->getNativeType()]; + } + $keyDeps = $keyResult->getDependencies(); + if ($keyDeps !== null) { + $dependencies[] = $keyDeps; + } + $keyFlow = $keyResult->getVariableFlow(); + if ($keyFlow !== null) { + $variableFlows[] = $keyFlow; + } $hasYield = $hasYield || $keyResult->hasYield(); - $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints()); + $keyThrow = $keyResult->getThrowPoints(); + if ($keyThrow !== []) { + $throwPoints = array_merge($throwPoints, $keyThrow); + } + $keyImpure = $keyResult->getImpurePoints(); + if ($keyImpure !== []) { + $impurePoints = array_merge($impurePoints, $keyImpure); + } $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating(); $scope = $keyResult->getScope(); } @@ -134,24 +153,48 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex ); } $valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $valueContext); - $itemResults[spl_object_id($arrayItem->value)] = $valueResult; - $variableFlows[] = $valueResult->getVariableFlow(); + if (!$arrayItem->value instanceof Scalar\String_ && !$arrayItem->value instanceof Scalar\Int_ && !$arrayItem->value instanceof Scalar\Float_) { + $itemTypes[spl_object_id($arrayItem->value)] = [$valueResult->getType(), $valueResult->getNativeType()]; + } + $valDeps = $valueResult->getDependencies(); + if ($valDeps !== null) { + $dependencies[] = $valDeps; + } + $valFlow = $valueResult->getVariableFlow(); + if ($valFlow !== null) { + $variableFlows[] = $valFlow; + } if ($arrayItem->byRef) { $variableFlows[] = VariableFlowBuilder::escapeRoot($arrayItem->value); } $hasYield = $hasYield || $valueResult->hasYield(); - $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints()); - $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints()); + $valThrow = $valueResult->getThrowPoints(); + if ($valThrow !== []) { + $throwPoints = array_merge($throwPoints, $valThrow); + } + $valImpure = $valueResult->getImpurePoints(); + if ($valImpure !== []) { + $impurePoints = array_merge($impurePoints, $valImpure); + } $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating(); $scope = $valueResult->getScope(); // the item's callback fires after its key and value were processed, // with the item's entry scope - callback-side asks answer from the // storage instead of re-walking the yet-unstored sub-expressions $nodeScopeResolver->callNodeCallback($nodeCallback, $arrayItem, $itemCallbackScope, $storage); + + if ($hasArrayRef) { + continue; + } + + if ($arrayItem->key !== null) { + $storage->removeExpressionResult($arrayItem->key); + } + $storage->removeExpressionResult($arrayItem->value); } $nodeScopeResolver->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage); if ($nodeScopeResolver->observingTemplateArgumentFrame($scope) !== null) { - $scope = $this->collectAbsorbedItems($expr, $itemResults, $scope); + $scope = $this->collectAbsorbedItems($expr, $itemTypes, $scope); } $result = $this->expressionResultFactory->create( @@ -163,16 +206,25 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: $isAlwaysTerminating, throwPoints: $throwPoints, impurePoints: $impurePoints, - typeCallback: function (bool $nativeTypesPromoted) use ($expr, $itemResults, $beforeScope): Type { - // each item type was captured at its own evaluation point in the - // sequence - resolving all items on any single scope (the old world) - // cannot handle items with side effects like [$b = 1, $b + 1, $b++] - $type = $this->initializerExprTypeResolver->getArrayType($expr, static function (Expr $inner) use ($itemResults, $nativeTypesPromoted): Type { + typeCallback: function (bool $nativeTypesPromoted) use ($expr, $itemTypes, $beforeScope): Type { + $type = $this->initializerExprTypeResolver->getArrayType($expr, static function (Expr $inner) use ($itemTypes, $nativeTypesPromoted): Type { + if ($inner instanceof Scalar\String_) { + return new ConstantStringType($inner->value); + } + if ($inner instanceof Scalar\Int_) { + return new ConstantIntegerType($inner->value); + } + if ($inner instanceof Scalar\Float_) { + return new ConstantFloatType($inner->value); + } + if ($inner instanceof TypeExpr) { + return $inner->getExprType(); + } $id = spl_object_id($inner); - if (array_key_exists($id, $itemResults)) { + if (array_key_exists($id, $itemTypes)) { return $nativeTypesPromoted - ? $itemResults[$id]->getNativeType() - : $itemResults[$id]->getType(); + ? $itemTypes[$id][1] + : $itemTypes[$id][0]; } throw new ShouldNotHappenException(); @@ -186,14 +238,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex new FullyQualified('is_callable'), [new Arg($expr)], ); - // isCallable() is asked last - it reflects the class named by the - // first item, which is expensive and unnecessary for arrays never - // narrowed by is_callable() if ( $beforeScope->hasExpressionType($isCallableCall)->yes() - // read the narrowed type from expressionTypes directly (the - // synthetic is_callable() call was never processed as a child), - // mirroring ConstFetchHandler's narrowed-constant lookup && $beforeScope->expressionTypes[$beforeScope->getNodeKey($isCallableCall)]->getType()->isTrue()->yes() && $type->isCallable()->maybe() ) { @@ -206,18 +252,20 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), ); - return $result->withDependencies(Dependencies::merge( - $this->getCallableDependencies($beforeScope, $expr, $itemResults, $result), - ...array_map(static fn (ExpressionResult $itemResult): ?Dependencies => $itemResult->getDependencies(), array_values($itemResults)), - )); + $callableDeps = $this->getCallableDependencies($beforeScope, $expr, $itemTypes, $result); + if ($callableDeps !== null) { + $dependencies[] = $callableDeps; + } + + return $result->withDependencies(Dependencies::merge(...$dependencies)); } /** * An array that may be a callable - `[Foo::class, 'method']` - depends on what calling it returns. * - * @param array $itemResults + * @param array $itemTypes */ - private function getCallableDependencies(MutatingScope $scope, Array_ $expr, array $itemResults, ExpressionResult $result): ?Dependencies + private function getCallableDependencies(MutatingScope $scope, Array_ $expr, array $itemTypes, ExpressionResult $result): ?Dependencies { if (count($expr->items) !== 2 || !isset($expr->items[0])) { return null; @@ -234,8 +282,17 @@ private function getCallableDependencies(MutatingScope $scope, Array_ $expr, arr return null; } - $firstItemResult = $itemResults[spl_object_id($expr->items[0]->value)] ?? null; - if ($firstItemResult === null || !$firstItemResult->getType()->isClassString()->yes()) { + $firstItemValue = $expr->items[0]->value; + $firstItemType = null; + if ($firstItemValue instanceof Scalar\String_) { + $firstItemType = new ConstantStringType($firstItemValue->value); + } elseif ($firstItemValue instanceof TypeExpr) { + $firstItemType = $firstItemValue->getExprType(); + } elseif (isset($itemTypes[spl_object_id($firstItemValue)])) { + $firstItemType = $itemTypes[spl_object_id($firstItemValue)][0]; + } + + if ($firstItemType === null || !$firstItemType->isClassString()->yes()) { return null; } @@ -256,25 +313,51 @@ private function getCallableDependencies(MutatingScope $scope, Array_ $expr, arr * A literal generalized by an unpacked item absorbs the closures of its items * into a wider value type - see ClosureSignatureInference::collectAbsorbed(). * - * @param array $itemResults + * @param array $itemTypes */ - private function collectAbsorbedItems(Array_ $expr, array $itemResults, MutatingScope $scope): MutatingScope + private function collectAbsorbedItems(Array_ $expr, array $itemTypes, MutatingScope $scope): MutatingScope { - $itemTypes = []; + $absorbedItemTypes = []; foreach ($expr->items as $arrayItem) { - $itemType = $itemResults[spl_object_id($arrayItem->value)]->getType(); + if ($arrayItem->value instanceof Scalar\String_ || $arrayItem->value instanceof Scalar\Int_ || $arrayItem->value instanceof Scalar\Float_) { + continue; + } + $id = spl_object_id($arrayItem->value); + if (!isset($itemTypes[$id])) { + continue; + } + $itemType = $itemTypes[$id][0]; if (!ClosureSignatureInference::hasMarkers($itemType)) { continue; } - $itemTypes[] = $itemType; + $absorbedItemTypes[] = $itemType; } - if ($itemTypes === []) { + if ($absorbedItemTypes === []) { return $scope; } - $arrayType = $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $inner): Type => $itemResults[spl_object_id($inner)]->getType()); + $arrayType = $this->initializerExprTypeResolver->getArrayType($expr, static function (Expr $inner) use ($itemTypes): Type { + if ($inner instanceof Scalar\String_) { + return new ConstantStringType($inner->value); + } + if ($inner instanceof Scalar\Int_) { + return new ConstantIntegerType($inner->value); + } + if ($inner instanceof Scalar\Float_) { + return new ConstantFloatType($inner->value); + } + if ($inner instanceof TypeExpr) { + return $inner->getExprType(); + } + $id = spl_object_id($inner); + if (array_key_exists($id, $itemTypes)) { + return $itemTypes[$id][0]; + } - return $scope->addTemplateArgumentConstraints(ClosureSignatureInference::collectAbsorbedInto($itemTypes, $arrayType)); + throw new ShouldNotHappenException(); + }); + + return $scope->addTemplateArgumentConstraints(ClosureSignatureInference::collectAbsorbedInto($absorbedItemTypes, $arrayType)); } private function getExpectedArrayType(?Type $type): ?Type @@ -299,4 +382,15 @@ private function getExpectedValueType(?Type $arrayType, Type $keyType): ?Type return $arrayType->getOffsetValueType($keyType); } + private static function hasArrayReference(Expr\Array_ $array): bool + { + foreach ($array->items as $item) { + if ($item->byRef || ($item->value instanceof Expr\Array_ && self::hasArrayReference($item->value))) { + return true; + } + } + + return false; + } + } diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index 0a1f5f83e84..eeaab7286f3 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -1372,7 +1372,7 @@ public function applyWrite( $scope = $scope->addConditionalExpressions((string) $exprString, $holders); } - if ($assignedExpr instanceof Expr\Array_) { + if ($assignedExpr instanceof Expr\Array_ && self::hasArrayReference($assignedExpr)) { $scope = $this->processArrayByRefItems($nodeScopeResolver, $scope, $storage, $var->name, $assignedExpr, new Variable($var->name)); } } else { diff --git a/src/Analyser/ExprHandler/ScalarHandler.php b/src/Analyser/ExprHandler/ScalarHandler.php index 3a737f65822..f012dac73b6 100644 --- a/src/Analyser/ExprHandler/ScalarHandler.php +++ b/src/Analyser/ExprHandler/ScalarHandler.php @@ -2,6 +2,7 @@ namespace PHPStan\Analyser\ExprHandler; +use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Scalar; use PhpParser\Node\Scalar\InterpolatedString; @@ -18,6 +19,9 @@ use PHPStan\Reflection\InitializerExprContext; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Turbo\ShadowedByTurboExtension; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; /** * @implements ExprHandler @@ -41,9 +45,16 @@ public function supports(Expr $expr): bool public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult { - // a literal's type and its initializer context (file/namespace/class) are - // lexical - identical on every scope - so build the context once here. - $initializerExprContext = InitializerExprContext::fromScope($scope); + if ($expr instanceof Node\Scalar\String_) { + $type = new ConstantStringType($expr->value); + } elseif ($expr instanceof Node\Scalar\Int_) { + $type = new ConstantIntegerType($expr->value); + } elseif ($expr instanceof Node\Scalar\Float_) { + $type = new ConstantFloatType($expr->value); + } else { + $initializerExprContext = InitializerExprContext::fromScope($scope); + $type = $this->initializerExprTypeResolver->getType($expr, $initializerExprContext); + } return $this->expressionResultFactory->create( $scope, @@ -53,8 +64,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex isAlwaysTerminating: false, throwPoints: [], impurePoints: [], - typeCallback: fn () => $this->initializerExprTypeResolver->getType($expr, $initializerExprContext), + typeCallback: null, specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), + type: $type, + nativeType: $type, ); } diff --git a/src/Analyser/ExpressionResultStorage.php b/src/Analyser/ExpressionResultStorage.php index a3de0d9a208..f7559382960 100644 --- a/src/Analyser/ExpressionResultStorage.php +++ b/src/Analyser/ExpressionResultStorage.php @@ -58,6 +58,11 @@ public function findExpressionResult(Expr $expr): ?ExpressionResult return $this->exprResults[$expr] ?? ($this->fallback !== null ? $this->fallback->findExpressionResult($expr) : null); } + public function removeExpressionResult(Expr $expr): void + { + unset($this->exprResults[$expr]); + } + /** * @param array{TemplateArgumentFrame|null, bool, bool, MutatingScope, MutatingScope, list} $walk */ diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index 4f91110f1a9..51c88c27e2a 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -63,6 +63,9 @@ public function processNode(Node $node, Scope $scope): array $autoGeneratedIndex = null; $seenKeys = []; $seenUnions = []; + $firstOccurrences = []; + /** @var array $seenKeysMap */ + $seenKeysMap = []; foreach ($node->getItemNodes() as $itemNode) { $item = $itemNode->getArrayItem(); if ($item === null) { @@ -153,7 +156,7 @@ public function processNode(Node $node, Scope $scope): array $duplicate = false; $newValues = $keyValues; foreach ($newValues as $k => $newValue) { - if (array_search($newValue, $seenKeys, true) !== false) { + if (((is_int($newValue) || is_string($newValue)) && isset($seenKeysMap[$newValue])) || array_search($newValue, $seenKeys, true) !== false) { unset($newValues[$k]); } @@ -179,10 +182,17 @@ public function processNode(Node $node, Scope $scope): array continue; } - $seenKeys[] = $seenUnions[$k][array_key_first($seenUnions[$k])]; + $promotedKey = $seenUnions[$k][array_key_first($seenUnions[$k])]; + $seenKeys[] = $promotedKey; + if (is_int($promotedKey) || is_string($promotedKey)) { + $seenKeysMap[$promotedKey] = true; + } unset($seenUnions[$k]); } $seenKeys[] = $newValue; + if (is_int($newValue) || is_string($newValue)) { + $seenKeysMap[$newValue] = true; + } } else { $seenUnions[] = $newValues; } @@ -196,20 +206,28 @@ public function processNode(Node $node, Scope $scope): array $value = (string) $value; } - $printedValue = $key !== null - ? $this->exprPrinter->printExpr($key) - : (is_string($value) ? var_export($value, true) : $value); - $printedValues[$value][] = $printedValue; - - if (!isset($valueLines[$value])) { - $valueLines[$value] = $item->getStartLine(); + if (!isset($firstOccurrences[$value])) { + $firstOccurrences[$value] = [$key, $item->getStartLine()]; + continue; } if (!$duplicate) { continue; } - $duplicateKeys[$value] = true; + if (!isset($duplicateKeys[$value])) { + $duplicateKeys[$value] = true; + $firstKey = $firstOccurrences[$value][0]; + $printedValues[$value][] = $firstKey !== null + ? $this->exprPrinter->printExpr($firstKey) + : (is_string($value) ? var_export($value, true) : $value); + $valueLines[$value] = $firstOccurrences[$value][1]; + } + + $printedValue = $key !== null + ? $this->exprPrinter->printExpr($key) + : (is_string($value) ? var_export($value, true) : $value); + $printedValues[$value][] = $printedValue; } } } diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 22991812fa2..7adf4fb9f82 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '1cb5faf'; + public const EXPECTED_EXTENSION_VERSION = 'c469421'; private static bool $active = false; diff --git a/tests/PHPStan/Analyser/ArrayCallableDependenciesTest.php b/tests/PHPStan/Analyser/ArrayCallableDependenciesTest.php new file mode 100644 index 00000000000..f6c68cf9140 --- /dev/null +++ b/tests/PHPStan/Analyser/ArrayCallableDependenciesTest.php @@ -0,0 +1,48 @@ +getByType(FileHelper::class); + $dataDir = $fileHelper->normalizePath(__DIR__ . '/data/array-callable-deps'); + $resultFile = $fileHelper->normalizePath($dataDir . '/result.php'); + $definiteFile = $fileHelper->normalizePath($dataDir . '/user-definite.php'); + $maybeFile = $fileHelper->normalizePath($dataDir . '/user-maybe.php'); + + $analyser = self::getContainer()->getByType(Analyser::class); + $analyserResult = $analyser->analyse([ + $definiteFile, + $maybeFile, + $fileHelper->normalizePath($dataDir . '/factory.php'), + $resultFile, + ]); + + $dependencies = $analyserResult->getDependencies(); + $this->assertNotNull($dependencies); + $this->assertArrayHasKey($definiteFile, $dependencies); + $this->assertArrayHasKey($maybeFile, $dependencies); + $this->assertContains($resultFile, $dependencies[$definiteFile]); + $this->assertNotContains($resultFile, $dependencies[$maybeFile]); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-callable-deps/factory.php b/tests/PHPStan/Analyser/data/array-callable-deps/factory.php new file mode 100644 index 00000000000..b111fe742df --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-callable-deps/factory.php @@ -0,0 +1,19 @@ +|int $classOrInt + */ +function maybeClassString($classOrInt): void +{ + $cb = [$classOrInt, 'create']; +} + +/** + * The first item is only maybe a class-string: no callable dependency. + * + * @param class-string|OtherFactory $classOrObject + */ +function maybeClassStringOrObject($classOrObject): void +{ + $cb = [$classOrObject, 'create']; +} + +/** + * An object-first array is not a `[Foo::class, 'method']` callable: no + * callable dependency. + */ +function objectFirst(Factory $factory): void +{ + $cb = [$factory, 'create']; +} diff --git a/turbo-ext/src/ArrayHandler.cpp b/turbo-ext/src/ArrayHandler.cpp index 7211f9d1d85..bb056e59ecd 100644 --- a/turbo-ext/src/ArrayHandler.cpp +++ b/turbo-ext/src/ArrayHandler.cpp @@ -4,13 +4,18 @@ * * A DI service (#[AutowiredService]): the constructor keeps the twin's * arginfo so Nette autowires it. processExpr() is registered as the class's - * handler entry (Engine.h). The twin's closures are native closures capturing - * what the PHP closures capture: the typeCallback ($this, $expr, - * $itemResults, $beforeScope); the item-type callback it hands to - * InitializerExprTypeResolver::getArrayType() ($itemResults, - * $nativeTypesPromoted) is a pt_ietr_get_type over the typeCallback's frame - * (the resolver calls it synchronously); the specifyTypesCallback is - * SpecifiedTypes::emptySpecifyCallback(), as in the twin. + * handler entry (Engine.h). The twin keeps only each non-scalar item's + * [type, nativeType] pair ($itemTypes) — the scalar keys/values re-derive + * their constant type from the node — and evicts each item's results from + * the storage once its callback ran (unless the literal has a by-reference + * item), so a large literal array does not pin every ExpressionResult. The + * twin's closures are native closures capturing what the PHP closures + * capture: the typeCallback ($this, $expr, $itemTypes, $beforeScope); the + * item-type callback it hands to InitializerExprTypeResolver::getArrayType() + * ($itemTypes, $nativeTypesPromoted) is a pt_ietr_get_type over the + * typeCallback's frame (the resolver calls it synchronously); the + * specifyTypesCallback is SpecifiedTypes::emptySpecifyCallback(), as in the + * twin. * * NodeScopeResolver, MutatingScope, ExpressionResult, ExpressionContext, * VariableFlow(Builder), VariableWriteOffset, TypeCombinator and the Type @@ -32,6 +37,7 @@ namespace slots = ptdecl::ArrayHandler::slot; namespace sigs = ptdecl::ArrayHandler::sig; #include "CallHandlerSupport.h" +#include "ParserVisitors.h" zend_class_entry *pt_ce_array_handler = nullptr; @@ -71,6 +77,132 @@ zval *itemByRef(zval *item) { return nodeProperty(pt_arh_item_by_ref_site, item, /* }}} */ +/* {{{ the nodes the twin resolves without a stored result: the plain + * scalars' value properties and TypeExpr::getExprType() */ + +using phpstanturbo::visitors::NodeProp; + +NodeProp pt_arh_scalar_string_value = PT_NODE_PROP(PT_CLASS_SCALAR_STRING, "value"); +NodeProp pt_arh_scalar_int_value = PT_NODE_PROP(PT_CLASS_SCALAR_INT, "value"); +NodeProp pt_arh_scalar_float_value = PT_NODE_PROP(PT_CLASS_SCALAR_FLOAT, "value"); +NodeProp pt_arh_type_expr_type = PT_NODE_PROP(PT_CLASS_TYPE_EXPR, "exprType"); + +/* $node-> (dereferenced); NULL = pending exception */ +zval *nodePropValue(NodeProp &prop, zval *node) +{ + zval *value = prop.of(Z_OBJ_P(node)); + if (EXPECTED(value != NULL && Z_TYPE_P(value) != IS_UNDEF)) return value; + if (EG(exception) != NULL) return NULL; + if (value == NULL) { + zend_throw_error(NULL, "Undefined property: %s::$%s", ZSTR_VAL(Z_OBJCE_P(node)->name), prop.name); + return NULL; + } + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(Z_OBJCE_P(node)->name), prop.name); + return NULL; +} + +/* $inner instanceof Scalar\String_ ? new ConstantStringType($inner->value) + * : (Int_ / Float_ alike): 1 = produced, 0 = not a plain scalar node, + * -1 = pending exception */ +int scalarConstantType(zval *node, zv::Val &out) +{ + if (Z_TYPE_P(node) != IS_OBJECT) return 0; + zend_class_entry *ce = Z_OBJCE_P(node); + zend_class_entry *stringCe = pt_class(PT_CLASS_SCALAR_STRING); + zend_class_entry *intCe = pt_class(PT_CLASS_SCALAR_INT); + zend_class_entry *floatCe = pt_class(PT_CLASS_SCALAR_FLOAT); + if (UNEXPECTED(stringCe == NULL || intCe == NULL || floatCe == NULL)) return -1; + if (instanceof_function(ce, stringCe)) { + zval *value = nodePropValue(pt_arh_scalar_string_value, node); + if (UNEXPECTED(value == NULL)) return -1; + if (EXPECTED(Z_TYPE_P(value) == IS_STRING)) { + zval z; + if (UNEXPECTED(!pt_constant_string_type_new(&z, Z_STR_P(value), false))) return -1; + out = zv::Val::adopt(z); + return 1; + } + out = pt_type_new_ce(pt_ce_constant_string_type, 1, value); + return out.isUndef() ? -1 : 1; + } + if (instanceof_function(ce, intCe)) { + zval *value = nodePropValue(pt_arh_scalar_int_value, node); + if (UNEXPECTED(value == NULL)) return -1; + if (EXPECTED(Z_TYPE_P(value) == IS_LONG)) { + zval z; + if (UNEXPECTED(!pt_constant_integer_type_new(&z, Z_LVAL_P(value)))) return -1; + out = zv::Val::adopt(z); + return 1; + } + out = pt_type_new_ce(pt_ce_constant_integer_type, 1, value); + return out.isUndef() ? -1 : 1; + } + if (instanceof_function(ce, floatCe)) { + zval *value = nodePropValue(pt_arh_scalar_float_value, node); + if (UNEXPECTED(value == NULL)) return -1; + if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { + zval z; + if (UNEXPECTED(!pt_constant_float_type_new(&z, Z_DVAL_P(value)))) return -1; + out = zv::Val::adopt(z); + return 1; + } + out = pt_type_new_ce(pt_ce_constant_float_type, 1, value); + return out.isUndef() ? -1 : 1; + } + return 0; +} + +/* whether $node is a Scalar\String_ / Scalar\Int_ / Scalar\Float_ (the twin's + * store skip); false = pending exception */ +[[nodiscard]] bool isPlainScalar(zval *node, bool &out) +{ + if (Z_TYPE_P(node) != IS_OBJECT) { + out = false; + return true; + } + zend_class_entry *ce = Z_OBJCE_P(node); + zend_class_entry *stringCe = pt_class(PT_CLASS_SCALAR_STRING); + zend_class_entry *intCe = pt_class(PT_CLASS_SCALAR_INT); + zend_class_entry *floatCe = pt_class(PT_CLASS_SCALAR_FLOAT); + if (UNEXPECTED(stringCe == NULL || intCe == NULL || floatCe == NULL)) return false; + out = instanceof_function(ce, stringCe) || instanceof_function(ce, intCe) || instanceof_function(ce, floatCe); + return true; +} + +/* $inner->getExprType() of a TypeExpr; UNDEF = pending exception */ +zv::Val typeExprType(zval *node) +{ + zval *type = nodePropValue(pt_arh_type_expr_type, node); + if (UNEXPECTED(type == NULL)) return zv::Val(); + if (UNEXPECTED(Z_TYPE_P(type) != IS_OBJECT)) { + zend_type_error("PHPStan\\Node\\Expr\\TypeExpr::getExprType(): Return value must be of type PHPStan\\Type\\Type, %s given", zend_zval_value_name(type)); + return zv::Val(); + } + return zv::Val::copyOf(zv::Ref(type)); +} + +/* }}} */ + +/* {{{ hasArrayReference()'s non-throwing reads (an Array_ node's items are + * always initialized ArrayItem nodes - a missing one simply answers "no + * reference here", like AssignHandler's port of the same twin static) */ + +pt_property_site pt_arh_ref_items_site; +pt_property_site pt_arh_ref_value_site; +pt_property_site pt_arh_ref_by_ref_site; + +/* $object->$name through the site; NULL when absent or uninitialized */ +zval *rawNodeProp(pt_property_site &site, zval *object, const char *name, size_t len) +{ + if (Z_TYPE_P(object) != IS_OBJECT) return NULL; + zval *slot = pt_property_cached(site, Z_OBJ_P(object), name, len); + if (slot == NULL) return NULL; + ZVAL_DEREF(slot); + if (Z_TYPE_P(slot) == IS_UNDEF) return NULL; + return slot; +} + +/* }}} */ + /* {{{ the VariableWrite value-flow target's getters */ /* $write->isOffsetWrite() / ->getVariableName() / ->getId(): the slot of @@ -170,8 +302,9 @@ class ArrayHandler uint32_t itemCount = Z_TYPE_P(items.raw()) == IS_ARRAY ? zend_hash_num_elements(Z_ARRVAL_P(items.raw())) : 0; zv::Arr itemNodes = zv::Arr::create(itemCount); - zv::Arr itemResults = zv::Arr::create(itemCount); + zv::Arr itemTypes = zv::Arr::create(itemCount); zv::Arr variableFlows = zv::Arr::create(itemCount); + zv::Arr dependencies = zv::Arr::empty(); bool hasYield = false; zv::Val throwPoints = zv::Val(zv::Arr::empty()); zv::Val impurePoints = zv::Val(zv::Arr::empty()); @@ -210,6 +343,7 @@ class ArrayHandler if (UNEXPECTED(nativePassedToType.isUndef())) return zv::Val(); } bool hasExpectedType = !passedToType.isNull() || !nativePassedToType.isNull(); + bool hasArrayRef = hasArrayReference(expr); bool hasLiteralWrite = !literalWrite.isNull(); zval nextIndex; ZVAL_LONG(&nextIndex, 0); @@ -249,11 +383,7 @@ class ArrayHandler zv::Val heldKey = zv::Val::copyOf(zv::Ref(key)); keyResult = pt_node_scope_resolver_process_expr_node(nodeScopeResolver, stmt, heldKey.raw(), scope.raw(), storage, nodeCallback, keyContext.raw()); if (UNEXPECTED(keyResult.isUndef())) return zv::Val(); - if (UNEXPECTED(Z_TYPE_P(heldKey.raw()) != IS_OBJECT)) { - zend_type_error("spl_object_id(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(heldKey.raw())); - return zv::Val(); - } - if (UNEXPECTED(!storeItemResult(itemResults, NULL, heldKey.raw(), keyResult.raw(), variableFlows, hasYield, throwPoints, impurePoints, isAlwaysTerminating, scope, hold))) return zv::Val(); + if (UNEXPECTED(!collectItemResult(itemTypes, dependencies, NULL, heldKey.raw(), keyResult.raw(), variableFlows, hasYield, throwPoints, impurePoints, isAlwaysTerminating, scope, hold))) return zv::Val(); } zval *unpack = itemUnpack(arrayItem); @@ -338,15 +468,22 @@ class ArrayHandler zv::Val heldValue = zv::Val::copyOf(zv::Ref(value)); zv::Val valueResult = pt_node_scope_resolver_process_expr_node(nodeScopeResolver, stmt, heldValue.raw(), scope.raw(), storage, nodeCallback, valueContext.raw()); if (UNEXPECTED(valueResult.isUndef())) return zv::Val(); - if (UNEXPECTED(Z_TYPE_P(heldValue.raw()) != IS_OBJECT)) { - zend_type_error("spl_object_id(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(heldValue.raw())); - return zv::Val(); - } - if (UNEXPECTED(!storeItemResult(itemResults, arrayItem, heldValue.raw(), valueResult.raw(), variableFlows, hasYield, throwPoints, impurePoints, isAlwaysTerminating, scope, hold))) return zv::Val(); + if (UNEXPECTED(!collectItemResult(itemTypes, dependencies, arrayItem, heldValue.raw(), valueResult.raw(), variableFlows, hasYield, throwPoints, impurePoints, isAlwaysTerminating, scope, hold))) return zv::Val(); // the item's callback fires after its key and value were processed, // with the item's entry scope if (UNEXPECTED(!pt_node_scope_resolver_call_node_callback(nodeScopeResolver, nodeCallback, arrayItem, itemCallbackScope.raw(), storage))) return zv::Val(); + + // an item's results are dead once their callback ran - unless a + // by-reference item keeps the literal's inner state alive + if (!hasArrayRef) { + zval *evictKey = itemKey(arrayItem); + if (UNEXPECTED(evictKey == NULL)) return zv::Val(); + if (Z_TYPE_P(evictKey) != IS_NULL && UNEXPECTED(!pt_expression_result_storage_remove(storage, evictKey))) return zv::Val(); + zval *evictValue = itemValue(arrayItem); + if (UNEXPECTED(evictValue == NULL)) return zv::Val(); + if (UNEXPECTED(!pt_expression_result_storage_remove(storage, evictValue))) return zv::Val(); + } } } { @@ -360,15 +497,15 @@ class ArrayHandler zv::Val observingFrame = pt_node_scope_resolver_observing_template_argument_frame(nodeScopeResolver, scope.raw()); if (UNEXPECTED(observingFrame.isUndef())) return zv::Val(); if (!observingFrame.isNull()) { - scope = collectAbsorbedItems(expr, itemResults.raw(), scope.raw()); + scope = collectAbsorbedItems(expr, itemTypes.raw(), scope.raw()); if (UNEXPECTED(scope.isUndef())) return zv::Val(); } } zv::Val variableFlow = pt_variable_flow_sequence_list(variableFlows.table()); if (UNEXPECTED(variableFlow.isUndef())) return zv::Val(); - zv::Val itemResultsValue(std::move(itemResults)); - zv::Val typeCallback = pt_native_closure(&typeCallbackBody, self, expr, itemResultsValue.raw(), beforeScope); + zv::Val itemTypesValue(std::move(itemTypes)); + zv::Val typeCallback = pt_native_closure(&typeCallbackBody, self, expr, itemTypesValue.raw(), beforeScope); zv::Val specifyTypesCallback = pt_specified_types_empty_specify_callback(); if (UNEXPECTED(specifyTypesCallback.isUndef())) return zv::Val(); @@ -377,66 +514,64 @@ class ArrayHandler zv::Val result = pt_expression_result_create(OBJ_PROP_NUM(self, slots::expressionResultFactory), args); if (UNEXPECTED(result.isUndef())) return zv::Val(); - // Dependencies::merge($callableDependencies, ...$itemResults' dependencies): - // the non-null ones gathered first, so that an array whose items depend on - // nothing allocates nothing - zv::Val callableDependencies = getCallableDependencies(beforeScope, expr, itemResultsValue.raw(), result.raw()); + // Dependencies::merge(...$dependencies): the items' own dependencies in + // processing order (gathered in the loop), the callable's last; an array + // whose items depend on nothing merges to null - withDependencies(null) + // is the same object then + zv::Val callableDependencies = getCallableDependencies(beforeScope, expr, itemTypesValue.raw(), result.raw()); if (UNEXPECTED(callableDependencies.isUndef())) return zv::Val(); - zv::Arr dependencyList; - if (!callableDependencies.isNull()) { - dependencyList = zv::Arr::create(1); - dependencyList.push(std::move(callableDependencies)); - } - for (auto entry : zv::ArrRef(itemResultsValue.raw())) { - zv::Val dependenciesHold; - zval *itemDependencies = pt_expression_result_dependencies(entry.value().deref().raw(), dependenciesHold); - if (UNEXPECTED(itemDependencies == NULL)) return zv::Val(); - if (Z_TYPE_P(itemDependencies) == IS_NULL) continue; - if (dependencyList.isUndef()) dependencyList = zv::Arr::create(1); - dependencyList.push(zv::Ref(itemDependencies)); - } - if (dependencyList.isUndef()) return result; - zv::Val dependencies = pt_dependencies_merge_list(dependencyList.table()); - if (UNEXPECTED(dependencies.isUndef())) return zv::Val(); - return pt_expression_result_with_dependencies(result.raw(), dependencies.raw()); + if (!callableDependencies.isNull()) dependencies.push(std::move(callableDependencies)); + if (zend_hash_num_elements(dependencies.table()) == 0) return result; + zv::Val mergedDependencies = pt_dependencies_merge_list(dependencies.table()); + if (UNEXPECTED(mergedDependencies.isUndef())) return zv::Val(); + return pt_expression_result_with_dependencies(result.raw(), mergedDependencies.raw()); } /* Mirrors collectAbsorbedItems() */ - zv::Val collectAbsorbedItems(zval *expr, zval *itemResults, zval *scopeArg) const + zv::Val collectAbsorbedItems(zval *expr, zval *itemTypes, zval *scopeArg) const { zv::Val scope = zv::Val::copyOf(zv::Ref(scopeArg)); zval *items = exprItems(expr); if (UNEXPECTED(items == NULL)) return zv::Val(); if (Z_TYPE_P(items) != IS_ARRAY) return scope; zv::Val itemsHold = zv::Val::copyOf(zv::Ref(items)); - zv::Arr itemTypes = zv::Arr::create(0); + zv::Arr absorbedTypes = zv::Arr::create(0); for (zv::ArrayEntry entry : zv::ArrRef(itemsHold.raw())) { zval *arrayItem = entry.value().deref().raw(); zval *value = itemValue(arrayItem); if (UNEXPECTED(value == NULL)) return zv::Val(); - zval *itemResult = zend_hash_index_find(Z_ARRVAL_P(itemResults), Z_OBJ_HANDLE_P(value)); - if (UNEXPECTED(itemResult == NULL)) { - zend_error(E_WARNING, "Undefined array key %u", Z_OBJ_HANDLE_P(value)); + bool plainScalar; + if (UNEXPECTED(!isPlainScalar(value, plainScalar))) return zv::Val(); + if (plainScalar) continue; + if (UNEXPECTED(Z_TYPE_P(value) != IS_OBJECT)) { + zend_type_error("spl_object_id(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(value)); + return zv::Val(); + } + zval *pair = zend_hash_index_find(Z_ARRVAL_P(itemTypes), Z_OBJ_HANDLE_P(value)); + if (pair == NULL || Z_TYPE_P(pair) == IS_NULL) continue; + ZVAL_DEREF(pair); + zval *itemType = Z_TYPE_P(pair) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(pair), 0) : NULL; + if (UNEXPECTED(itemType == NULL)) { + zend_type_error("PHPStan\\Analyser\\Generics\\ClosureSignatureInference::hasMarkers(): Argument #1 ($type) must be of type PHPStan\\Type\\Type, null given"); return zv::Val(); } - zv::Val itemType = pt_expression_result_get_type(itemResult); - if (UNEXPECTED(itemType.isUndef())) return zv::Val(); + ZVAL_DEREF(itemType); bool hasMarkers; - if (UNEXPECTED(!pt_closure_signature_inference_has_markers(itemType.raw(), hasMarkers))) return zv::Val(); + if (UNEXPECTED(!pt_closure_signature_inference_has_markers(itemType, hasMarkers))) return zv::Val(); if (!hasMarkers) continue; - itemTypes.push(std::move(itemType)); + absorbedTypes.push(zv::Ref(itemType)); } - if (zend_hash_num_elements(itemTypes.table()) == 0) return scope; + if (zend_hash_num_elements(absorbedTypes.table()) == 0) return scope; zv::Val arrayType; { - ItemTypeFrame frame{itemResults, false}; + ItemTypeFrame frame{itemTypes, false}; pt_ietr_get_type getTypeCallback{&itemTypeCallback, &frame, &itemTypeCallable}; arrayType = getArrayType(OBJ_PROP_NUM(self, slots::initializerExprTypeResolver), expr, getTypeCallback); if (UNEXPECTED(arrayType.isUndef())) return zv::Val(); } - zv::Val itemTypesHold(std::move(itemTypes)); - zv::Val absorbed = pt_closure_signature_inference_collect_absorbed_into(itemTypesHold.raw(), arrayType.raw()); + zv::Val absorbedTypesHold(std::move(absorbedTypes)); + zv::Val absorbed = pt_closure_signature_inference_collect_absorbed_into(absorbedTypesHold.raw(), arrayType.raw()); if (UNEXPECTED(absorbed.isUndef())) return zv::Val(); return pt_mutating_scope_add_template_argument_constraints(Z_OBJ_P(scope.raw()), absorbed.raw()); } @@ -453,7 +588,7 @@ class ArrayHandler /* Mirrors getCallableDependencies(): an array that may be a callable - * `[Foo::class, 'method']` - depends on what calling it returns; PHP null * for nothing, UNDEF = pending exception */ - static zv::Val getCallableDependencies(zval *scope, zval *expr, zval *itemResults, zval *result) + static zv::Val getCallableDependencies(zval *scope, zval *expr, zval *itemTypes, zval *result) { zval *items = exprItems(expr); if (UNEXPECTED(items == NULL)) return zv::Val(); @@ -491,14 +626,45 @@ class ArrayHandler } zval *value = itemValue(first); if (UNEXPECTED(value == NULL)) return zv::Val(); - if (UNEXPECTED(Z_TYPE_P(value) != IS_OBJECT)) { - zend_type_error("spl_object_id(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(value)); - return zv::Val(); + // $firstItemType: derived from the node of a plain string or a + // TypeExpr, the stored [type, nativeType] pair otherwise + zv::Val firstItemType = zv::Val::null(); + int isString = isInstanceOf(value, PT_CLASS_SCALAR_STRING); + if (UNEXPECTED(isString < 0)) return zv::Val(); + if (isString == 1) { + int produced = scalarConstantType(value, firstItemType); + if (UNEXPECTED(produced < 0)) return zv::Val(); + if (UNEXPECTED(produced != 1)) { + pt_throw_should_not_happen(); + return zv::Val(); + } + } else { + int isTypeExpr = isInstanceOf(value, PT_CLASS_TYPE_EXPR); + if (UNEXPECTED(isTypeExpr < 0)) return zv::Val(); + if (isTypeExpr == 1) { + firstItemType = typeExprType(value); + if (UNEXPECTED(firstItemType.isUndef())) return zv::Val(); + } else { + if (UNEXPECTED(Z_TYPE_P(value) != IS_OBJECT)) { + zend_type_error("spl_object_id(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(value)); + return zv::Val(); + } + zval *pair = zend_hash_index_find(Z_ARRVAL_P(itemTypes), Z_OBJ_HANDLE_P(value)); + if (pair != NULL && Z_TYPE_P(pair) != IS_NULL) { + ZVAL_DEREF(pair); + zval *stored = Z_TYPE_P(pair) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(pair), 0) : NULL; + if (UNEXPECTED(stored == NULL)) { + // the pair is [type, nativeType] by construction + pt_throw_should_not_happen(); + return zv::Val(); + } + ZVAL_DEREF(stored); + firstItemType = zv::Val::copyOf(zv::Ref(stored)); + } + } } - zval *firstItemResult = zend_hash_index_find(Z_ARRVAL_P(itemResults), Z_OBJ_HANDLE_P(value)); - if (firstItemResult == NULL || Z_TYPE_P(firstItemResult) == IS_NULL) return zv::Val::null(); - zv::Val firstItemType = pt_expression_result_get_type(firstItemResult); - if (UNEXPECTED(firstItemType.isUndef())) return zv::Val(); + + if (firstItemType.isNull()) return zv::Val::null(); zend_long isClassString = pt_type_call_trinary(Z_OBJ_P(firstItemType.raw()), PT_LC("isclassstring"), 0, NULL); if (UNEXPECTED(isClassString < 0)) return zv::Val(); if (isClassString != PT_TRI_YES) return zv::Val::null(); @@ -522,19 +688,39 @@ class ArrayHandler return pt_dependencies_create_in(scope, returnTypes.raw()); } - /* $itemResults[spl_object_id($node)] = $result; $variableFlows[] = - * $result->getVariableFlow(); (for a value: the escape of a by-reference - * item); the $hasYield / $throwPoints / $impurePoints / - * $isAlwaysTerminating folds; $scope = $result->getScope(); false = - * pending exception */ - [[nodiscard]] static bool storeItemResult(zv::Arr &itemResults, zval *arrayItem, zval *node, zval *result, zv::Arr &variableFlows, bool &hasYield, zv::Val &throwPoints, zv::Val &impurePoints, bool &isAlwaysTerminating, zv::Val &scope, zv::Val &hold) + /* the per-item bookkeeping of the twin's loop: a non-scalar node's + * [getType(), getNativeType()] pair goes to $itemTypes (the plain scalars' + * constant types are re-derived from the node wherever they are needed); + * the result's own dependencies and variable flow follow it (a + * by-reference value's escape too); the $hasYield / $throwPoints / + * $impurePoints / $isAlwaysTerminating folds; $scope = + * $result->getScope(); false = pending exception */ + [[nodiscard]] static bool collectItemResult(zv::Arr &itemTypes, zv::Arr &dependencies, zval *arrayItem, zval *node, zval *result, zv::Arr &variableFlows, bool &hasYield, zv::Val &throwPoints, zv::Val &impurePoints, bool &isAlwaysTerminating, zv::Val &scope, zv::Val &hold) { - zval stored; - ZVAL_COPY(&stored, result); - zend_hash_index_update(itemResults.table(), Z_OBJ_HANDLE_P(node), &stored); + bool plainScalar; + if (UNEXPECTED(!isPlainScalar(node, plainScalar))) return false; + if (!plainScalar) { + if (UNEXPECTED(Z_TYPE_P(node) != IS_OBJECT)) { + zend_type_error("spl_object_id(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(node)); + return false; + } + zv::Val type = pt_expression_result_get_type(result); + if (UNEXPECTED(type.isUndef())) return false; + zv::Val nativeType = pt_expression_result_get_native_type(result); + if (UNEXPECTED(nativeType.isUndef())) return false; + zv::Arr pair = zv::Arr::create(2); + pair.push(std::move(type)); + pair.push(std::move(nativeType)); + zval stored; + ZVAL_COPY(&stored, pair.raw()); + zend_hash_index_update(itemTypes.table(), Z_OBJ_HANDLE_P(node), &stored); + } + zval *itemDependencies = pt_expression_result_dependencies(result, hold); + if (UNEXPECTED(itemDependencies == NULL)) return false; + if (Z_TYPE_P(itemDependencies) != IS_NULL) dependencies.push(zv::Ref(itemDependencies)); zv::Val flow = pt_expression_result_variable_flow(result); if (UNEXPECTED(flow.isUndef())) return false; - variableFlows.push(std::move(flow)); + if (!flow.isNull()) variableFlows.push(std::move(flow)); if (arrayItem != NULL) { // only the value side: `if ($arrayItem->byRef)` zval *byRef = itemByRef(arrayItem); @@ -597,12 +783,33 @@ class ArrayHandler return pt_type_op(Z_OBJ_P(arrayType), PT_OP_GET_OFFSET_VALUE_TYPE, 1, keyType); } - /* static function (Expr $inner) use ($itemResults, $nativeTypesPromoted): + /* Mirrors hasArrayReference() */ + static bool hasArrayReference(zval *array) + { + zval *items = rawNodeProp(pt_arh_ref_items_site, array, PT_LC("items")); + if (items == NULL || Z_TYPE_P(items) != IS_ARRAY) return false; + for (zv::ArrayEntry entry : zv::TableRef(Z_ARRVAL_P(items))) { + zval *item = entry.value().deref().raw(); + zval *byRef = rawNodeProp(pt_arh_ref_by_ref_site, item, PT_LC("byRef")); + if (byRef != NULL && zend_is_true(byRef)) return true; + zval *value = rawNodeProp(pt_arh_ref_value_site, item, PT_LC("value")); + if (value == NULL) continue; + int isArray = isInstanceOf(value, PT_CLASS_ARRAY_EXPR); + if (isArray == 1) { + bool nested = false; + pt_engine_with_stack([&]() { nested = hasArrayReference(value); }); + if (nested) return true; + } + } + return false; + } + + /* static function (Expr $inner) use ($itemTypes, $nativeTypesPromoted): * Type — InitializerExprTypeResolver calls it synchronously, over the * typeCallback's frame */ struct ItemTypeFrame { - zval *itemResults; + zval *itemTypes; bool nativeTypesPromoted; }; @@ -616,24 +823,40 @@ class ArrayHandler zend_type_error("%s(): Argument #1 ($inner) must be of type PhpParser\\Node\\Expr, %s given", pt_arh_closure_name, zend_zval_value_name(inner)); return zv::Val(); } - zval *itemResult = zend_hash_index_find(Z_ARRVAL_P(frame->itemResults), Z_OBJ_HANDLE_P(inner)); - if (UNEXPECTED(itemResult == NULL)) { + // a plain scalar's constant type comes from the node, a TypeExpr's + // from itself; only the rest was stored in $itemTypes + zv::Val scalar; + int isScalar = scalarConstantType(inner, scalar); + if (UNEXPECTED(isScalar < 0)) return zv::Val(); + if (isScalar == 1) return scalar; + int isTypeExpr = isInstanceOf(inner, PT_CLASS_TYPE_EXPR); + if (UNEXPECTED(isTypeExpr < 0)) return zv::Val(); + if (isTypeExpr == 1) return typeExprType(inner); + zval *pair = zend_hash_index_find(Z_ARRVAL_P(frame->itemTypes), Z_OBJ_HANDLE_P(inner)); + if (pair == NULL) { + pt_throw_should_not_happen(); + return zv::Val(); + } + ZVAL_DEREF(pair); + zval *stored = Z_TYPE_P(pair) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(pair), frame->nativeTypesPromoted ? 1 : 0) : NULL; + if (UNEXPECTED(stored == NULL)) { pt_throw_should_not_happen(); return zv::Val(); } - return frame->nativeTypesPromoted ? pt_expression_result_get_native_type(itemResult) : pt_expression_result_get_type(itemResult); + ZVAL_DEREF(stored); + return zv::Val::copyOf(zv::Ref(stored)); } /* the callback as a PHP callable that outlives the call (the resolver * hands it to OversizedArrayBuilder): the closure over copies of - * $itemResults and $nativeTypesPromoted */ + * $itemTypes and $nativeTypesPromoted */ static zv::Val itemTypeCallable(void *data) { ItemTypeFrame *frame = static_cast(data); - return pt_native_closure(&itemTypeCallbackBody, frame->itemResults, frame->nativeTypesPromoted); + return pt_native_closure(&itemTypeCallbackBody, frame->itemTypes, frame->nativeTypesPromoted); } - /* the same closure called from PHP — captures: $itemResults, + /* the same closure called from PHP — captures: $itemTypes, * $nativeTypesPromoted */ static void itemTypeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { @@ -645,11 +868,11 @@ class ArrayHandler } /* the typeCallback's body */ - zv::Val resolveType(bool nativeTypesPromoted, zval *expr, zval *itemResults, zval *beforeScope) const + zv::Val resolveType(bool nativeTypesPromoted, zval *expr, zval *itemTypes, zval *beforeScope) const { zv::Val type; { - ItemTypeFrame frame{itemResults, nativeTypesPromoted}; + ItemTypeFrame frame{itemTypes, nativeTypesPromoted}; pt_ietr_get_type getTypeCallback{&itemTypeCallback, &frame, &itemTypeCallable}; type = getArrayType(OBJ_PROP_NUM(self, slots::initializerExprTypeResolver), expr, getTypeCallback); if (UNEXPECTED(type.isUndef())) return zv::Val(); @@ -710,8 +933,8 @@ class ArrayHandler return pt_type_combinator_intersect(2, intersectArgv); } - /* function (bool $nativeTypesPromoted) use ($expr, $itemResults, - * $beforeScope): Type — captures: $this, $expr, $itemResults, $beforeScope */ + /* function (bool $nativeTypesPromoted) use ($expr, $itemTypes, + * $beforeScope): Type — captures: $this, $expr, $itemTypes, $beforeScope */ static void typeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(!requireArguments(argc, 1, pt_arh_closure_name))) return; diff --git a/turbo-ext/src/AssignHandler.cpp b/turbo-ext/src/AssignHandler.cpp index f5a81e3b4bc..320afef54f3 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -2112,7 +2112,7 @@ class AssignHandler } } - if (ahIs(assignedExpr.raw(), PT_CLASS_ARRAY_EXPR)) { + if (ahIs(assignedExpr.raw(), PT_CLASS_ARRAY_EXPR) && hasArrayReference(assignedExpr.raw())) { AH_VALB(rootVariable, newVariable(varName)); AH_SETB(scope, processArrayByRefItems(nsr, scope.raw(), storage, varName, assignedExpr.raw(), rootVariable.raw())); } diff --git a/turbo-ext/src/ExpressionResultStorage.cpp b/turbo-ext/src/ExpressionResultStorage.cpp index 69a85909eb8..88e1c4e0b11 100644 --- a/turbo-ext/src/ExpressionResultStorage.cpp +++ b/turbo-ext/src/ExpressionResultStorage.cpp @@ -83,6 +83,17 @@ class ExpressionResultStorage } } + /* Mirrors removeExpressionResult(): unset($this->exprResults[$expr]) - + * this storage's own entries only, the read-only fallback chain keeps + * its copies (writes never reach it) */ + void removeExpressionResult(zval *expr) + { + zend_ulong id = Z_OBJ_HANDLE_P(expr); + zv::ObjRef obj(self); + zv::ArrRef(obj.propAt(slots::exprResults).raw()).unsetIndex(id); + zv::ArrRef(obj.propAt(slots::fallback).raw()).unsetIndex(id); + } + /* Mirrors storeByRefInvocationWalk(): the walks keyed by the closure's * handle as [closure, list of walks] - the closure is pinned like the * stored Exprs */ @@ -214,6 +225,16 @@ PT_MINIT_REGISTRATION(pt_register_expression_result_storage) ExpressionResultStorage(ZEND_THIS).findExpressionResult(expr).intoReturnValue(return_value); }); + cls.method(sigs::removeExpressionResult, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr; + zend_class_entry *exprCe = pt_class(PT_CLASS_EXPR); + if (UNEXPECTED(exprCe == NULL)) RETURN_THROWS(); + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(expr, exprCe) + ZEND_PARSE_PARAMETERS_END(); + ExpressionResultStorage(ZEND_THIS).removeExpressionResult(expr); + }); + cls.method(sigs::storeByRefInvocationWalk, [](INTERNAL_FUNCTION_PARAMETERS) { zval *closure, *walk; zend_class_entry *closureCe = pt_class(PT_CLASS_CLOSURE_EXPR); @@ -254,6 +275,15 @@ void pt_expression_result_storage_store(zval *storage, zval *expr, zval *express (void) pt_type_call(Z_OBJ_P(storage), "storeexpressionresult", sizeof("storeexpressionresult") - 1, 2, argv); } +bool pt_expression_result_storage_remove(zval *storage, zval *expr) +{ + if (EXPECTED(Z_OBJCE_P(storage) == pt_ce_expression_result_storage)) { + ExpressionResultStorage(storage).removeExpressionResult(expr); + return true; + } + return !pt_type_call(Z_OBJ_P(storage), "removeexpressionresult", sizeof("removeexpressionresult") - 1, 1, expr).isUndef(); +} + zv::Val pt_expression_result_storage_new() { return pt_type_new_ce(pt_ce_expression_result_storage, 0, NULL); diff --git a/turbo-ext/src/ScalarHandler.cpp b/turbo-ext/src/ScalarHandler.cpp index 73d47a6fea0..1eb7810a72c 100644 --- a/turbo-ext/src/ScalarHandler.cpp +++ b/turbo-ext/src/ScalarHandler.cpp @@ -5,9 +5,11 @@ * A DI service (#[AutowiredService]): the constructor keeps the twin's * arginfo so Nette autowires it. processExpr() is registered as the * class's handler entry (Engine.h), so native callers dispatch to it - * without an engine frame. The typeCallback is a native closure capturing - * what the twin's arrow function captures ($this, $expr and the initializer - * context); the result is built through pt_expression_result_create(). + * without an engine frame. The literal scalars (String_, Int_, Float_) get + * their constant type directly; anything else (the magic constants) goes + * through InitializerExprTypeResolver::getType() once, eagerly - the result + * is built through pt_expression_result_create() with the precomputed + * type/nativeType and no typeCallback, like the twin. * * SpecifiedTypes and InitializerExprTypeResolver are called through their * direct entries; InitializerExprContext, which stays PHP for now, through the @@ -22,11 +24,14 @@ namespace sigs = ptdecl::ScalarHandler::sig; #include "zv.h" #include "TypeTraits.h" #include "Engine.h" +#include "ParserVisitors.h" zend_class_entry *pt_ce_scalar_handler = nullptr; namespace { +using phpstanturbo::visitors::NodeProp; + /* {{{ the PHP collaborators (one site each; switch to their direct entries * once they are ported) */ @@ -45,6 +50,28 @@ zv::Val initializerExprType(zval *initializerExprTypeResolver, zval *expr, zval /* }}} */ +/* {{{ the scalar nodes' value properties */ + +NodeProp pt_sh_scalar_string_value = PT_NODE_PROP(PT_CLASS_SCALAR_STRING, "value"); +NodeProp pt_sh_scalar_int_value = PT_NODE_PROP(PT_CLASS_SCALAR_INT, "value"); +NodeProp pt_sh_scalar_float_value = PT_NODE_PROP(PT_CLASS_SCALAR_FLOAT, "value"); + +/* $node->value (dereferenced); NULL = pending exception */ +zval *nodeProp(NodeProp &prop, zval *node) +{ + zval *value = prop.of(Z_OBJ_P(node)); + if (EXPECTED(value != NULL && Z_TYPE_P(value) != IS_UNDEF)) return value; + if (EG(exception) != NULL) return NULL; + if (value == NULL) { + zend_throw_error(NULL, "Undefined property: %s::$%s", ZSTR_VAL(Z_OBJCE_P(node)->name), prop.name); + return NULL; + } + zend_throw_error(NULL, "Typed property %s::$%s must not be accessed before initialization", ZSTR_VAL(Z_OBJCE_P(node)->name), prop.name); + return NULL; +} + +/* }}} */ + } // namespace namespace phpstanturbo { @@ -87,15 +114,52 @@ class ScalarHandler (void) storage; (void) nodeCallback; (void) context; - // a literal's type and its initializer context (file/namespace/class) are - // lexical - identical on every scope - so build the context once here. - zv::Val initializerExprContext = initializerExprContextFromScope(scope); - if (UNEXPECTED(initializerExprContext.isUndef())) return zv::Val(); + // a literal's type is lexical - identical on every scope - so compute it + // once, eagerly; anything but the plain scalars (the magic constants) + // keeps going through the initializer resolver + zv::Val type; + if (visitors::isInstanceOf(Z_OBJ_P(expr), PT_CLASS_SCALAR_STRING)) { + zval *value = nodeProp(pt_sh_scalar_string_value, expr); + if (UNEXPECTED(value == NULL)) return zv::Val(); + if (EXPECTED(Z_TYPE_P(value) == IS_STRING)) { + zval out; + if (UNEXPECTED(!pt_constant_string_type_new(&out, Z_STR_P(value), false))) return zv::Val(); + type = zv::Val::adopt(out); + } else { + type = pt_type_new_ce(pt_ce_constant_string_type, 1, value); + } + } else if (visitors::isInstanceOf(Z_OBJ_P(expr), PT_CLASS_SCALAR_INT)) { + zval *value = nodeProp(pt_sh_scalar_int_value, expr); + if (UNEXPECTED(value == NULL)) return zv::Val(); + if (EXPECTED(Z_TYPE_P(value) == IS_LONG)) { + zval out; + if (UNEXPECTED(!pt_constant_integer_type_new(&out, Z_LVAL_P(value)))) return zv::Val(); + type = zv::Val::adopt(out); + } else { + type = pt_type_new_ce(pt_ce_constant_integer_type, 1, value); + } + } else if (visitors::isInstanceOf(Z_OBJ_P(expr), PT_CLASS_SCALAR_FLOAT)) { + zval *value = nodeProp(pt_sh_scalar_float_value, expr); + if (UNEXPECTED(value == NULL)) return zv::Val(); + if (EXPECTED(Z_TYPE_P(value) == IS_DOUBLE)) { + zval out; + if (UNEXPECTED(!pt_constant_float_type_new(&out, Z_DVAL_P(value)))) return zv::Val(); + type = zv::Val::adopt(out); + } else { + type = pt_type_new_ce(pt_ce_constant_float_type, 1, value); + } + } else { + zv::Val initializerExprContext = initializerExprContextFromScope(scope); + if (UNEXPECTED(initializerExprContext.isUndef())) return zv::Val(); + type = initializerExprType(OBJ_PROP_NUM(self, slots::initializerExprTypeResolver), expr, initializerExprContext.raw()); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + } - zv::Val typeCallback = pt_native_closure(&typeCallbackBody, self, expr, initializerExprContext.raw()); zv::Val specifyTypesCallback = pt_specified_types_empty_specify_callback(); if (UNEXPECTED(specifyTypesCallback.isUndef())) return zv::Val(); - pt_expression_result_args args(scope, scope, expr, false, false, NULL, NULL, typeCallback.raw(), specifyTypesCallback.raw()); + pt_expression_result_args args(scope, scope, expr, false, false, NULL, NULL, NULL, specifyTypesCallback.raw()); + args.withType(type.raw()); + args.withNativeType(type.raw()); return pt_expression_result_create(OBJ_PROP_NUM(self, slots::expressionResultFactory), args); } @@ -107,18 +171,6 @@ class ScalarHandler private: zend_object *self; - - /* fn () => $this->initializerExprTypeResolver->getType($expr, $initializerExprContext) - * — captures: $this, $expr, $initializerExprContext */ - static void typeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) - { - (void) argc; - (void) argv; - zval *handler = &captures[0]; - zv::Val type = initializerExprType(OBJ_PROP_NUM(Z_OBJ_P(handler), slots::initializerExprTypeResolver), &captures[1], &captures[2]); - if (UNEXPECTED(type.isUndef())) return; - type.intoReturnValue(return_value); - } }; } // namespace phpstanturbo diff --git a/turbo-ext/src/generated/ArrayHandler.h b/turbo-ext/src/generated/ArrayHandler.h index 57586518ce9..d33413f3499 100644 --- a/turbo-ext/src/generated/ArrayHandler.h +++ b/turbo-ext/src/generated/ArrayHandler.h @@ -53,17 +53,19 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\ExpressionResult\0" /* 407 */ "processExpr\0" /* 441 */ "PhpParser\\Node\\Expr\\Array_\0" /* 453 */ - "itemResults\0" /* 480 */ - "result\0" /* 492 */ - "PHPStan\\Dependency\\Dependencies\0" /* 499 */ - "getCallableDependencies\0" /* 531 */ - "collectAbsorbedItems\0" /* 555 */ - "type\0" /* 576 */ - "PHPStan\\Type\\Type\0" /* 581 */ - "getExpectedArrayType\0" /* 599 */ - "arrayType\0" /* 620 */ - "keyType\0" /* 630 */ - "getExpectedValueType"; /* 638 */ + "itemTypes\0" /* 480 */ + "result\0" /* 490 */ + "PHPStan\\Dependency\\Dependencies\0" /* 497 */ + "getCallableDependencies\0" /* 529 */ + "collectAbsorbedItems\0" /* 553 */ + "type\0" /* 574 */ + "PHPStan\\Type\\Type\0" /* 579 */ + "getExpectedArrayType\0" /* 597 */ + "arrayType\0" /* 618 */ + "keyType\0" /* 628 */ + "getExpectedValueType\0" /* 636 */ + "array\0" /* 657 */ + "hasArrayReference"; /* 663 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 28), /* __construct $initializerExprTypeResolver */ reg::packed(75, 0, 99), /* __construct $expressionResultFactory */ @@ -79,18 +81,20 @@ inline constexpr reg::PackedArg args[] = { reg::packed(177, 0, 407), /* processExpr return */ reg::packed(265, 0, 271), /* getCallableDependencies $scope */ reg::packed(152, 0, 453), /* getCallableDependencies $expr */ - reg::packed(480, MAY_BE_ARRAY), /* getCallableDependencies $itemResults */ - reg::packed(492, 0, 407), /* getCallableDependencies $result */ - reg::packed(177, MAY_BE_NULL, 499), /* getCallableDependencies return */ + reg::packed(480, MAY_BE_ARRAY), /* getCallableDependencies $itemTypes */ + reg::packed(490, 0, 407), /* getCallableDependencies $result */ + reg::packed(177, MAY_BE_NULL, 497), /* getCallableDependencies return */ reg::packed(152, 0, 453), /* collectAbsorbedItems $expr */ - reg::packed(480, MAY_BE_ARRAY), /* collectAbsorbedItems $itemResults */ + reg::packed(480, MAY_BE_ARRAY), /* collectAbsorbedItems $itemTypes */ reg::packed(265, 0, 271), /* collectAbsorbedItems $scope */ reg::packed(177, 0, 271), /* collectAbsorbedItems return */ - reg::packed(576, MAY_BE_NULL, 581), /* getExpectedArrayType $type */ - reg::packed(177, MAY_BE_NULL, 581), /* getExpectedArrayType return */ - reg::packed(620, MAY_BE_NULL, 581), /* getExpectedValueType $arrayType */ - reg::packed(630, 0, 581), /* getExpectedValueType $keyType */ - reg::packed(177, MAY_BE_NULL, 581), /* getExpectedValueType return */ + reg::packed(574, MAY_BE_NULL, 579), /* getExpectedArrayType $type */ + reg::packed(177, MAY_BE_NULL, 579), /* getExpectedArrayType return */ + reg::packed(618, MAY_BE_NULL, 579), /* getExpectedValueType $arrayType */ + reg::packed(628, 0, 579), /* getExpectedValueType $keyType */ + reg::packed(177, MAY_BE_NULL, 579), /* getExpectedValueType return */ + reg::packed(657, 0, 453), /* hasArrayReference $array */ + reg::packed(177, MAY_BE_BOOL), /* hasArrayReference return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -100,10 +104,11 @@ namespace sig { inline constexpr sigtab::Sig __construct = { { 140 /* __construct */, 2, 0, 2, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig supports = { { 178 /* supports */, 1, 2, 1, 3, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig processExpr = { { 441 /* processExpr */, 7, 4, 7, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getCallableDependencies = { { 531 /* getCallableDependencies */, 4, 12, 4, 16, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig collectAbsorbedItems = { { 555 /* collectAbsorbedItems */, 3, 17, 3, 20, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getExpectedArrayType = { { 599 /* getExpectedArrayType */, 1, 21, 1, 22, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getExpectedValueType = { { 638 /* getExpectedValueType */, 2, 23, 2, 25, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getCallableDependencies = { { 529 /* getCallableDependencies */, 4, 12, 4, 16, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig collectAbsorbedItems = { { 553 /* collectAbsorbedItems */, 3, 17, 3, 20, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getExpectedArrayType = { { 597 /* getExpectedArrayType */, 1, 21, 1, 22, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getExpectedValueType = { { 636 /* getExpectedValueType */, 2, 23, 2, 25, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig hasArrayReference = { { 663 /* hasArrayReference */, 1, 26, 1, 27, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; } // namespace sig } // namespace ptdecl::ArrayHandler diff --git a/turbo-ext/src/generated/ExpressionResultStorage.h b/turbo-ext/src/generated/ExpressionResultStorage.h index ca735826a60..04dc060a8bc 100644 --- a/turbo-ext/src/generated/ExpressionResultStorage.h +++ b/turbo-ext/src/generated/ExpressionResultStorage.h @@ -43,11 +43,12 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\ExpressionResult\0" /* 125 */ "storeExpressionResult\0" /* 159 */ "findExpressionResult\0" /* 181 */ - "closure\0" /* 202 */ - "PhpParser\\Node\\Expr\\Closure\0" /* 210 */ - "walk\0" /* 238 */ - "storeByRefInvocationWalk\0" /* 243 */ - "findByRefInvocationWalks"; /* 268 */ + "removeExpressionResult\0" /* 202 */ + "closure\0" /* 225 */ + "PhpParser\\Node\\Expr\\Closure\0" /* 233 */ + "walk\0" /* 261 */ + "storeByRefInvocationWalk\0" /* 266 */ + "findByRefInvocationWalks"; /* 291 */ inline constexpr reg::PackedArg args[] = { reg::packed(12, 0, 13), /* duplicate return */ reg::packed(64, 0, 13), /* mergeResults $other */ @@ -57,10 +58,12 @@ inline constexpr reg::PackedArg args[] = { reg::packed(12, MAY_BE_VOID), /* storeExpressionResult return */ reg::packed(83, 0, 88), /* findExpressionResult $expr */ reg::packed(12, MAY_BE_NULL, 125), /* findExpressionResult return */ - reg::packed(202, 0, 210), /* storeByRefInvocationWalk $closure */ - reg::packed(238, MAY_BE_ARRAY), /* storeByRefInvocationWalk $walk */ + reg::packed(83, 0, 88), /* removeExpressionResult $expr */ + reg::packed(12, MAY_BE_VOID), /* removeExpressionResult return */ + reg::packed(225, 0, 233), /* storeByRefInvocationWalk $closure */ + reg::packed(261, MAY_BE_ARRAY), /* storeByRefInvocationWalk $walk */ reg::packed(12, MAY_BE_VOID), /* storeByRefInvocationWalk return */ - reg::packed(202, 0, 210), /* findByRefInvocationWalks $closure */ + reg::packed(225, 0, 233), /* findByRefInvocationWalks $closure */ reg::packed(12, MAY_BE_ARRAY), /* findByRefInvocationWalks return */ }; using Sig = reg::Sig; @@ -73,8 +76,9 @@ inline constexpr sigtab::Sig duplicate = { { 54 /* duplicate */, 0, 0, 0, 0, ZEN inline constexpr sigtab::Sig mergeResults = { { 70 /* mergeResults */, 1, 1, 1, 2, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig storeExpressionResult = { { 159 /* storeExpressionResult */, 2, 3, 2, 5, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig findExpressionResult = { { 181 /* findExpressionResult */, 1, 6, 1, 7, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig storeByRefInvocationWalk = { { 243 /* storeByRefInvocationWalk */, 2, 8, 2, 10, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig findByRefInvocationWalks = { { 268 /* findByRefInvocationWalks */, 1, 11, 1, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig removeExpressionResult = { { 202 /* removeExpressionResult */, 1, 8, 1, 9, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig storeByRefInvocationWalk = { { 266 /* storeByRefInvocationWalk */, 2, 10, 2, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig findByRefInvocationWalks = { { 291 /* findByRefInvocationWalks */, 1, 13, 1, 14, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ExpressionResultStorage diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 1090ddfb29c..c477a8b2514 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -2364,6 +2364,7 @@ zv::Val pt_expression_result_on_non_nullability_deviced_scopes(zval *result, zva * method left pending) and $stack->push($storage) / pop(); false = pending * exception */ void pt_expression_result_storage_store(zval *storage, zval *expr, zval *expressionResult); +[[nodiscard]] bool pt_expression_result_storage_remove(zval *storage, zval *expr); [[nodiscard]] bool pt_expression_result_storage_stack_push(zval *stack, zval *storage); [[nodiscard]] bool pt_expression_result_storage_stack_pop(zval *stack); diff --git a/turbo-ext/src/zv.h b/turbo-ext/src/zv.h index 72ec560d599..d0e03bca366 100644 --- a/turbo-ext/src/zv.h +++ b/turbo-ext/src/zv.h @@ -389,6 +389,14 @@ class ArrRef : public Ref Z_TRY_ADDREF_P(borrowed.raw()); zend_hash_index_update(table(), index, borrowed.raw()); } + + /* unset($a[$index]); separates a shared table first (writes through the + * slot), releases a removed value */ + void unsetIndex(zend_ulong index) + { + SEPARATE_ARRAY(z); + zend_hash_index_del(table(), index); + } }; /* Owned PHP array under construction. diff --git a/turbo-ext/tests/smoke.php b/turbo-ext/tests/smoke.php index 9df729e25f5..a6a131379ad 100644 --- a/turbo-ext/tests/smoke.php +++ b/turbo-ext/tests/smoke.php @@ -4285,8 +4285,10 @@ public function create( } check(($hhResults['php']['variable / top']['type'] ?? null) === 'int' && ($hhResults['php']['dynamic name / top']['flow']['kind'] ?? null) === 'sequence', 'ScalarHandler/VariableHandler: the fixture resolves a variable and walks a dynamic name: ' . json_encode([$hhResults['php']['variable / top'] ?? null, $hhResults['php']['dynamic name / top'] ?? null])); // a native closure is not serializable and has no body when userland creates one +// (the closure comes from the native VariableHandler: the ScalarHandler's +// result carries a precomputed type and no typeCallback since 2.3.x) try { - serialize((new \ReflectionProperty(\PHPStan\Analyser\ExpressionResult::class, 'typeCallback'))->getValue((new \PHPStanTurbo\ScalarHandler($hhInitializer, $hhFactory))->processExpr($hhNodeScopeResolver, $hhStmt, $hhExprs['int'], $hhScope, new \PHPStan\Analyser\ExpressionResultStorage(), new \PHPStan\Analyser\NoopNodeCallback(), $hhContexts['top']))); + serialize((new \ReflectionProperty(\PHPStan\Analyser\ExpressionResult::class, 'typeCallback'))->getValue((new \PHPStanTurbo\VariableHandler($hhNativeFactory, $hhDefault, $hhIdentical, $hhInitializer))->processExpr($hhNodeScopeResolver, $hhStmt, $hhExprs['variable'], $hhScope, new \PHPStan\Analyser\ExpressionResultStorage(), new \PHPStan\Analyser\NoopNodeCallback(), $hhContexts['top']))); check(false, 'NativeClosure: serialize() must refuse'); } catch (\Exception $e) { check(str_contains($e->getMessage(), 'is not allowed'), 'NativeClosure: serialize() message: ' . $e->getMessage());