Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 136 additions & 42 deletions src/Analyser/ExprHandler/ArrayHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -70,8 +72,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
{
$beforeScope = $scope;
$itemNodes = [];
$itemResults = [];
$itemTypes = [];
$variableFlows = [];
$dependencies = [];
$hasYield = false;
$throwPoints = [];
$impurePoints = [];
Expand All @@ -83,18 +86,34 @@ 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);
$itemCallbackScope = $scope;
$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();
}
Expand Down Expand Up @@ -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(
Expand All @@ -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();
Expand All @@ -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()
) {
Expand All @@ -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<int, ExpressionResult> $itemResults
* @param array<int, array{Type, Type}> $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;
Expand All @@ -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;
}

Expand All @@ -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<int, ExpressionResult> $itemResults
* @param array<int, array{Type, Type}> $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
Expand All @@ -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;
}

}
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
21 changes: 17 additions & 4 deletions src/Analyser/ExprHandler/ScalarHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Analyser\ExprHandler;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;
use PhpParser\Node\Scalar\InterpolatedString;
Expand All @@ -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<Scalar>
Expand All @@ -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,
Expand All @@ -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,
);
}

Expand Down
5 changes: 5 additions & 0 deletions src/Analyser/ExpressionResultStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<InternalThrowPoint>} $walk
*/
Expand Down
Loading
Loading