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 @@ -26,3 +26,4 @@ parameters:
finiteTypesInHaystack: true
switchConditionAlwaysFalse: true
checkImportedClassNameCase: 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 @@ -57,6 +57,7 @@ parameters:
finiteTypesInHaystack: false
switchConditionAlwaysFalse: false
checkImportedClassNameCase: 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 @@ -55,6 +55,7 @@ parametersSchema:
finiteTypesInHaystack: bool()
switchConditionAlwaysFalse: bool()
checkImportedClassNameCase: 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;
}

}
53 changes: 49 additions & 4 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
use PHPStan\Node\IssetExpr;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Node\PropertyAssignNode;
use PHPStan\Node\Variable\VariableWrite;
use PHPStan\Node\VariableAssignNode;
use PHPStan\Node\VirtualNode;
use PHPStan\Php\PhpVersion;
Expand Down Expand Up @@ -147,10 +148,22 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$valueScope = $valueBeforeScope;
$valueImpurePoints = [];
if ($expr instanceof AssignRef) {
// both sides alias one slot from now on - neither is tracked for
// unused writes
$aliasedVar = $expr->var;
while ($aliasedVar instanceof ArrayDimFetch) {
$aliasedVar = $aliasedVar->var;
}
if ($aliasedVar instanceof Variable && is_string($aliasedVar->name)) {
$nodeScopeResolver->markVariableUntracked($aliasedVar->name);
}
$referencedExpr = $expr->expr;
while ($referencedExpr instanceof ArrayDimFetch) {
$referencedExpr = $referencedExpr->var;
}
if ($referencedExpr instanceof Variable && is_string($referencedExpr->name)) {
$nodeScopeResolver->markVariableUntracked($referencedExpr->name);
}

if ($referencedExpr instanceof PropertyFetch || $referencedExpr instanceof StaticPropertyFetch) {
$valueImpurePoints[] = new ImpurePoint(
Expand Down Expand Up @@ -602,6 +615,7 @@ private function doPrepareTarget(
targetReadResult: $targetReadResult,
targetChainResults: $targetChainResults,
variableNameResult: $variableNameResult,
writeSiteKind: $mode->getWriteSiteKind(),
);
}

Expand Down Expand Up @@ -801,6 +815,7 @@ private function doPrepareTarget(
offsetSetTargetResult: $offsetSetTargetResult,
targetReadResult: $targetReadResult,
targetChainResults: $targetChainResults,
writeSiteKind: $mode->getWriteSiteKind(),
);
}

Expand Down Expand Up @@ -866,6 +881,7 @@ private function doPrepareTarget(
propertyName: $propertyName,
targetReadResult: $targetReadResult,
targetChainResults: $targetChainResults,
writeSiteKind: $mode->getWriteSiteKind(),
);
}

Expand Down Expand Up @@ -926,6 +942,7 @@ private function doPrepareTarget(
propertyHolderType: $propertyHolderType,
targetReadResult: $targetReadResult,
targetChainResults: $targetChainResults,
writeSiteKind: $mode->getWriteSiteKind(),
);
}

Expand All @@ -942,6 +959,7 @@ private function doPrepareTarget(
$throwPoints,
$impurePoints,
$isAlwaysTerminating,
writeSiteKind: $mode->getWriteSiteKind(),
);
}

Expand Down Expand Up @@ -992,6 +1010,7 @@ private function doPrepareTarget(
assignedPropertyExpr: $assignedPropertyExpr,
existingOffsetTypes: $offsetTypes,
existingOffsetNativeTypes: $offsetNativeTypes,
writeSiteKind: $mode->getWriteSiteKind(),
);
}

Expand Down Expand Up @@ -1026,6 +1045,7 @@ private function doPrepareTarget(
$isAlwaysTerminating,
targetReadResult: $targetReadResult,
targetChainResults: $targetChainResults,
writeSiteKind: $mode->getWriteSiteKind(),
);
}

Expand Down Expand Up @@ -1084,7 +1104,6 @@ public function applyWrite(
$storedAssignedExprResult = $assignedExpr === $target->getAssignedExpr()
? $assignedValueResult ?? $storage->findExpressionResult($assignedExpr)
: $storage->findExpressionResult($assignedExpr);
$assignedValueResult = $storedAssignedExprResult;
$type = $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scopeBeforeAssignEval);

$conditionalExpressions = [];
Expand Down Expand Up @@ -1206,7 +1225,15 @@ public function applyWrite(
}

$nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage);
$scope = $scope->assignVariable($var->name, $type, $this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain()), TrinaryLogic::createYes());
$write = $target->getWriteSiteKind() !== null ? $nodeScopeResolver->recordVariableWrite($var, $target->getWriteSiteKind()) : null;
$scope = $scope->assignVariable(
$var->name,
$type,
$this->readAssignedValueType($nodeScopeResolver, $storedAssignedExprResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain()),
TrinaryLogic::createYes(),
write: $write,
supersededMarkerExprs: $write !== null ? $nodeScopeResolver->getVariableWriteMarkersToKill($var->name) : [],
);
foreach ($conditionalExpressions as $exprString => $holders) {
$scope = $scope->addConditionalExpressions((string) $exprString, $holders);
}
Expand Down Expand Up @@ -1304,7 +1331,19 @@ public function applyWrite(
if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) {
if ($var instanceof Variable && is_string($var->name)) {
$nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, new TypeExpr($valueToWrite)), $scopeBeforeAssignEval, $storage);
$scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes());
// an offset write on an object (ArrayAccess, mixed) mutates a shared
// handle - only a value container (array, string) makes it a write site
$write = $target->getWriteSiteKind() !== null && ($varType->isArray()->yes() || $varType->isString()->yes())
? $nodeScopeResolver->recordVariableWrite($var, VariableWrite::KIND_ARRAY_DIM_WRITE)
: null;
$scope = $scope->assignVariable(
$var->name,
$valueToWrite,
$nativeValueToWrite,
TrinaryLogic::createYes(),
write: $write,
supersededMarkerExprs: $write !== null ? $nodeScopeResolver->getVariableWriteMarkersToKill($var->name) : [],
);
} else {
if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
$nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
Expand Down Expand Up @@ -1517,6 +1556,9 @@ public function applyWrite(
if ($arrayItem === null) {
continue;
}
if ($arrayItem->byRef && $arrayItem->value instanceof Variable && is_string($arrayItem->value->name)) {
$nodeScopeResolver->markVariableUntracked($arrayItem->value->name);
}

$itemScope = $scope;
if ($enterExpressionAssign) {
Expand Down Expand Up @@ -1556,7 +1598,7 @@ public function applyWrite(
$getOffsetValueTypeExpr,
$nodeCallback,
$context,
$enterExpressionAssign ? AssignTargetWalkMode::assign() : AssignTargetWalkMode::virtualAssign(),
$enterExpressionAssign ? AssignTargetWalkMode::assign(VariableWrite::KIND_LIST_ITEM) : AssignTargetWalkMode::virtualAssign($target->getWriteSiteKind() !== null ? VariableWrite::KIND_LIST_ITEM : null),
);
$result = $this->applyWrite(
$nodeScopeResolver,
Expand Down Expand Up @@ -2117,6 +2159,9 @@ private function processArrayByRefItems(NodeScopeResolver $nodeScopeResolver, Mu
}

$refVarName = $arrayItem->value->name;
// `$root = [&$ref]` aliases the two slots from now on
$nodeScopeResolver->markVariableUntracked($rootVarName);
$nodeScopeResolver->markVariableUntracked($refVarName);
$dimFetchExpr = new ArrayDimFetch($parentExpr, $dimExpr);
// a plain variable read is scope state - no need to price a synthetic
// Variable node on demand (mirrors VariableHandler's typeCallback)
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/ExprHandler/EvalHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
{
$beforeScope = $scope;
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
// the evaluated code may read any variable
$nodeScopeResolver->markAllReachingVariablesRead($exprResult->getScope());
$scope = $exprResult->getScope()->invalidateVolatileExpressions();

return $this->expressionResultFactory->create(
Expand Down
4 changes: 4 additions & 0 deletions src/Analyser/ExprHandler/Helper/ClosureTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
$invalidateExpressions = [];

try {
// a throwaway write-tracking frame: this walk must not register the
// body's writes into the enclosing function-like's frame
$this->nodeScopeResolver->pushVariableWritesFrame($expr->params);
$walkStorage = new ExpressionResultStorage();
$closureScope->pushExpressionResultStorage($walkStorage);
$closureStatementResult = $this->nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $walkStorage, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$closureExecutionEnds, &$closureImpurePoints, &$invalidateExpressions): void {
Expand Down Expand Up @@ -259,6 +262,7 @@ static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpu
}, StatementContext::createTopLevel())->toPublic();
} finally {
$closureScope->popExpressionResultStorage();
$this->nodeScopeResolver->popVariableWritesFrame();
self::$resolveClosureTypeDepth--;
}

Expand Down
78 changes: 78 additions & 0 deletions src/Analyser/ExprHandler/Helper/FuncCallScopeEffectsHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,21 @@ public function applyCallScopeEffects(NodeScopeResolver $nodeScopeResolver, Stmt
)->getScope();
}

if ($functionReflection !== null) {
if ($functionReflection->getName() === 'compact') {
$compactedNames = $this->findCompactVariableNames($normalizedExpr, $argsResult, $scope);
if ($compactedNames === null) {
$nodeScopeResolver->markAllReachingVariablesRead($scope);
} else {
foreach ($compactedNames as $compactedName) {
$nodeScopeResolver->markVariableRead($compactedName, $scope);
}
}
} elseif (in_array($functionReflection->getName(), ['get_defined_vars', 'extract'], true)) {
$nodeScopeResolver->markAllReachingVariablesRead($scope);
}
}

if (
$functionReflection !== null
&& $functionReflection->getName() === 'extract'
Expand Down Expand Up @@ -506,4 +521,67 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
return $arrayType;
}

/**
* The variable names a compact() call reads, null when they cannot be
* enumerated (mirrors CompactFunctionReturnTypeExtension).
*
* @return list<string>|null
*/
private function findCompactVariableNames(FuncCall $funcCall, ArgsResult $argsResult, MutatingScope $scope): ?array
{
$names = [];
foreach ($funcCall->getArgs() as $arg) {
if ($arg->unpack) {
return null;
}
$argNames = $this->findConstantStringValues($argsResult->requireArgResult($arg->value)->getTypeOnScope($scope, false));
if ($argNames === null) {
return null;
}
foreach ($argNames as $argName) {
$names[] = $argName;
}
}

return $names;
}

/**
* @return list<string>|null
*/
private function findConstantStringValues(Type $type): ?array
{
$constantStrings = $type->getConstantStrings();
if (count($constantStrings) > 0) {
$values = [];
foreach ($constantStrings as $constantString) {
$values[] = $constantString->getValue();
}

return $values;
}

$constantArrays = $type->getConstantArrays();
if (count($constantArrays) === 0) {
return null;
}
$values = [];
foreach ($constantArrays as $constantArray) {
if ($constantArray->isUnsealed()->yes()) {
return null;
}
foreach ($constantArray->getValueTypes() as $valueType) {
$valueNames = $this->findConstantStringValues($valueType);
if ($valueNames === null) {
return null;
}
foreach ($valueNames as $valueName) {
$values[] = $valueName;
}
}
}

return $values;
}

}
Loading
Loading