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
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ parameters:
checkImportedClassNameCase: true
sortWithoutEffect: true
unresolvedTemplateArguments: true
unusedVariable: true
5 changes: 5 additions & 0 deletions conf/config.level4.neon
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ conditionalTags:
phpstan.rules.rule: %featureToggles.finiteTypesInHaystack%
PHPStan\Rules\Comparison\SwitchConditionRule:
phpstan.rules.rule: %featureToggles.switchConditionAlwaysFalse%
PHPStan\Rules\DeadCode\UnusedVariableRule:
phpstan.rules.rule: %featureToggles.unusedVariable%

parameters:
checkAdvancedIsset: true
Expand Down Expand Up @@ -49,3 +51,6 @@ services:
class: PHPStan\Rules\Comparison\SwitchConditionRule
arguments:
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%

-
class: PHPStan\Rules\DeadCode\UnusedVariableRule
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ parameters:
checkImportedClassNameCase: false
sortWithoutEffect: false
unresolvedTemplateArguments: false
unusedVariable: true
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ parametersSchema:
checkImportedClassNameCase: bool()
sortWithoutEffect: bool()
unresolvedTemplateArguments: bool()
unusedVariable: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
36 changes: 30 additions & 6 deletions src/Analyser/AssignTargetWalkMode.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace PHPStan\Analyser;

use PHPStan\Node\Variable\VariableWrite;

/**
* How AssignHandler::prepareTarget() walks the assignment target.
*
Expand All @@ -13,37 +15,51 @@
* isset descriptor). The read happens inside the one target walk instead of
* callers re-processing the target with a noop callback.
*
* The mode also says whether the write is a source-level write site of a
* local variable (recorded for the unused-variable check) - a by-ref
* write-back or a call's scope effect is not.
*
* @internal
*/
final class AssignTargetWalkMode
{

/**
* @param VariableWrite::KIND_*|null $writeSiteKind
*/
private function __construct(
private bool $enterExpressionAssign,
private bool $producesTargetReadResult,
private bool $issetSemanticsForRead,
private ?int $writeSiteKind,
)
{
}

public static function assign(): self
/**
* @param VariableWrite::KIND_* $writeSiteKind
*/
public static function assign(int $writeSiteKind = VariableWrite::KIND_ASSIGN): self
{
return new self(true, false, false);
return new self(true, false, false, $writeSiteKind);
}

public static function virtualAssign(): self
/**
* @param VariableWrite::KIND_*|null $writeSiteKind
*/
public static function virtualAssign(?int $writeSiteKind = null): self
{
return new self(false, false, false);
return new self(false, false, false, $writeSiteKind);
}

public static function readModifyWrite(): self
{
return new self(false, true, false);
return new self(false, true, false, VariableWrite::KIND_READ_MODIFY_WRITE);
}

public static function coalesceReadModifyWrite(): self
{
return new self(true, true, true);
return new self(true, true, true, VariableWrite::KIND_READ_MODIFY_WRITE);
}

public function enterExpressionAssign(): bool
Expand All @@ -61,4 +77,12 @@ public function issetSemanticsForRead(): bool
return $this->issetSemanticsForRead;
}

/**
* @return VariableWrite::KIND_*|null
*/
public function getWriteSiteKind(): ?int
{
return $this->writeSiteKind;
}

}
31 changes: 28 additions & 3 deletions src/Analyser/ExprHandler/ArrayDimFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt;
use PHPStan\Analyser\ExpressionContext;
Expand All @@ -23,6 +24,7 @@
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Analyser\VariableWriteOffset;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Reflection\ParametersAcceptorSelector;
Expand All @@ -32,6 +34,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_merge;
use function is_string;

/**
* @implements ExprHandler<ArrayDimFetch>
Expand All @@ -57,18 +60,40 @@ public function supports(Expr $expr): bool
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
{
$beforeScope = $scope;
// the receiver is read as a container - the offset read itself is
// recorded below, once the dimension is known; both flow into the value
if ($expr->dim === null) {
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep());
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeepKeepingValueFlow()->enterArrayDimFetchRoot());
$this->markOffsetRead($nodeScopeResolver, $expr, null, $scope, $context);

return $this->composeResult($nodeScopeResolver, $stmt, $expr, null, $varResult, $storage, $context, $beforeScope);
}

$dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->dim, $scope, $storage, $nodeCallback, $context->enterDeep());
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $dimResult->getScope(), $storage, $nodeCallback, $context->enterDeep());
$dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->dim, $scope, $storage, $nodeCallback, $context->enterDeepKeepingValueFlow());
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $dimResult->getScope(), $storage, $nodeCallback, $context->enterDeepKeepingValueFlow()->enterArrayDimFetchRoot());
$this->markOffsetRead($nodeScopeResolver, $expr, VariableWriteOffset::fromType($dimResult->getType()), $dimResult->getScope(), $context);

return $this->composeResult($nodeScopeResolver, $stmt, $expr, $dimResult, $varResult, $storage, $context, $beforeScope);
}

/**
* Records the read of one offset of a local variable for the
* unused-variable check; an unset() target discards the offset's writes
* instead of reading them.
*
* @param int|string|null $offset
*/
private function markOffsetRead(NodeScopeResolver $nodeScopeResolver, ArrayDimFetch $expr, $offset, MutatingScope $scope, ExpressionContext $context): void
{
if ($context->isUnsetTarget()) {
return;
}
if (!$expr->var instanceof Variable || !is_string($expr->var->name)) {
return;
}
$nodeScopeResolver->markVariableOffsetRead($expr->var->name, $offset, $scope, $context->getValueFlowTarget());
}

/**
* Builds the offset read's ExpressionResult from the already-walked
* dimension and receiver results - the chain is not re-walked (only the
Expand Down
45 changes: 43 additions & 2 deletions src/Analyser/ExprHandler/ArrayHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\VariableWriteOffset;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\LiteralArrayItem;
use PHPStan\Node\LiteralArrayNode;
use PHPStan\Node\Variable\VariableWrite;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\CallableType;
Expand All @@ -27,6 +29,8 @@
use function array_key_exists;
use function array_merge;
use function count;
use function is_int;
use function max;
use function spl_object_id;

/**
Expand All @@ -36,6 +40,9 @@
final class ArrayHandler implements ExprHandler
{

/** Offsets of a literal array tracked as separate writes of the assigned variable. */
private const TRACKED_LITERAL_ITEMS_LIMIT = 32;

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private ExpressionResultFactory $expressionResultFactory,
Expand All @@ -57,11 +64,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$throwPoints = [];
$impurePoints = [];
$isAlwaysTerminating = false;
// a literal assigned straight to a variable writes each of its constant
// offsets: the items become offset writes of that variable, so a read of
// one offset leaves the others unused
$literalWrite = $context->getValueFlowTarget();
if ($literalWrite !== null && (!$context->isValueFlowDirect() || $literalWrite->isOffsetWrite())) {
$literalWrite = null;
}
$nextIndex = 0;
$nextIndexKnown = true;
$trackedItems = 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->enterDeep());
$keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeepKeepingValueFlow());
$itemResults[spl_object_id($arrayItem->key)] = $keyResult;
$hasYield = $hasYield || $keyResult->hasYield();
$throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());
Expand All @@ -70,7 +88,30 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scope = $keyResult->getScope();
}

$valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep());
$valueContext = $context->enterDeepKeepingValueFlow();
if ($literalWrite !== null) {
if ($arrayItem->unpack) {
$itemOffset = null;
$nextIndexKnown = false;
} elseif ($keyResult === null) {
$itemOffset = $nextIndexKnown ? $nextIndex++ : null;
} else {
$itemOffset = VariableWriteOffset::fromType($keyResult->getType());
if (is_int($itemOffset)) {
$nextIndex = max($nextIndex, $itemOffset + 1);
} elseif ($itemOffset === null) {
$nextIndexKnown = false;
}
}
if ($trackedItems < self::TRACKED_LITERAL_ITEMS_LIMIT) {
$trackedItems++;
$itemWrite = $nodeScopeResolver->recordVariableOffsetWrite($arrayItem, $literalWrite->getVariableName(), VariableWrite::KIND_ARRAY_LITERAL_ITEM, $itemOffset, $literalWrite);
if ($itemWrite !== null) {
$valueContext = $context->enterDeep()->enterValueFlow($itemWrite, false);
}
}
}
$valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $valueContext);
$itemResults[spl_object_id($arrayItem->value)] = $valueResult;
$hasYield = $hasYield || $valueResult->hasYield();
$throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
Expand Down
Loading
Loading