Skip to content

Commit f13c2c7

Browse files
authored
Feature/disallow dynamic property option
1 parent 6a611f0 commit f13c2c7

12 files changed

+336
-65
lines changed

conf/config.level0.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ services:
146146
- phpstan.rules.rule
147147
arguments:
148148
reportMagicProperties: %reportMagicProperties%
149+
checkDynamicProperties: %checkDynamicProperties%
149150

150151
-
151152
class: PHPStan\Rules\Properties\AccessStaticPropertiesRule

conf/config.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ parameters:
6262
checkMissingTypehints: false
6363
checkTooWideReturnTypesInProtectedAndPublicMethods: false
6464
checkUninitializedProperties: false
65+
checkDynamicProperties: false
6566
inferPrivatePropertyTypeFromConstructor: false
6667
reportMaybes: false
6768
reportMaybesInMethodSignatures: false
@@ -252,6 +253,7 @@ parametersSchema:
252253
checkMissingTypehints: bool()
253254
checkTooWideReturnTypesInProtectedAndPublicMethods: bool()
254255
checkUninitializedProperties: bool()
256+
checkDynamicProperties: bool()
255257
inferPrivatePropertyTypeFromConstructor: bool()
256258

257259
tipsOfTheDay: bool()

src/Analyser/DirectScopeFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function __construct(
4646
* @param VariableTypeHolder[] $moreSpecificTypes
4747
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
4848
* @param array<string, true> $currentlyAssignedExpressions
49-
* @param array<string, bool> $currentlyAllowedUndefinedExpressions
49+
* @param array<string, true> $currentlyAllowedUndefinedExpressions
5050
* @param array<string, Type> $nativeExpressionTypes
5151
* @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack
5252
*

src/Analyser/LazyScopeFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function __construct(
3838
* @param VariableTypeHolder[] $moreSpecificTypes
3939
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
4040
* @param array<string, true> $currentlyAssignedExpressions
41-
* @param array<string, bool> $currentlyAllowedUndefinedExpressions
41+
* @param array<string, true> $currentlyAllowedUndefinedExpressions
4242
* @param array<string, Type> $nativeExpressionTypes
4343
* @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack
4444
*

src/Analyser/MutatingScope.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ class MutatingScope implements Scope
167167
* @param VariableTypeHolder[] $moreSpecificTypes
168168
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
169169
* @param array<string, true> $currentlyAssignedExpressions
170-
* @param array<string, bool> $currentlyAllowedUndefinedExpressions
170+
* @param array<string, true> $currentlyAllowedUndefinedExpressions
171171
* @param array<string, Type> $nativeExpressionTypes
172172
* @param array<MethodReflection|FunctionReflection> $inFunctionCallsStack
173173
*/
@@ -3906,11 +3906,11 @@ public function isInExpressionAssign(Expr $expr): bool
39063906
return array_key_exists($exprString, $this->currentlyAssignedExpressions);
39073907
}
39083908

3909-
public function setAllowedUndefinedExpression(Expr $expr, bool $isAllowed): self
3909+
public function setAllowedUndefinedExpression(Expr $expr): self
39103910
{
39113911
$exprString = $this->getNodeKey($expr);
39123912
$currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions;
3913-
$currentlyAllowedUndefinedExpressions[$exprString] = $isAllowed;
3913+
$currentlyAllowedUndefinedExpressions[$exprString] = true;
39143914

39153915
return $this->scopeFactory->create(
39163916
$this->context,
@@ -3964,7 +3964,7 @@ public function unsetAllowedUndefinedExpression(Expr $expr): self
39643964
public function isUndefinedExpressionAllowed(Expr $expr): bool
39653965
{
39663966
$exprString = $this->getNodeKey($expr);
3967-
return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions) && $this->currentlyAllowedUndefinedExpressions[$exprString];
3967+
return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions);
39683968
}
39693969

39703970
public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $certainty = null): self

src/Analyser/NodeScopeResolver.php

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,9 +1336,9 @@ private function processStmtNode(
13361336
$hasYield = false;
13371337
$throwPoints = [];
13381338
foreach ($stmt->vars as $var) {
1339-
$scope = $this->lookForEnterAllowedUndefinedVariable($scope, $var, true);
1339+
$scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
13401340
$scope = $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope();
1341-
$scope = $this->lookForExitAllowedUndefinedVariable($scope, $var);
1341+
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
13421342
$scope = $scope->unsetExpression($var);
13431343
}
13441344
} elseif ($stmt instanceof Node\Stmt\Use_) {
@@ -1355,9 +1355,9 @@ private function processStmtNode(
13551355
if (!$var instanceof Variable) {
13561356
throw new ShouldNotHappenException();
13571357
}
1358-
$scope = $this->lookForEnterAllowedUndefinedVariable($scope, $var, true);
1358+
$scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
13591359
$this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep());
1360-
$scope = $this->lookForExitAllowedUndefinedVariable($scope, $var);
1360+
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
13611361

13621362
if (!is_string($var->name)) {
13631363
continue;
@@ -1513,56 +1513,40 @@ private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $cla
15131513
);
15141514
}
15151515

1516-
private function lookForEnterAllowedUndefinedVariable(MutatingScope $scope, Expr $expr, bool $isAllowed): MutatingScope
1516+
private function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
15171517
{
1518-
if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
1519-
$scope = $scope->setAllowedUndefinedExpression($expr, $isAllowed);
1520-
}
1521-
if (!$expr instanceof Variable) {
1522-
return $this->lookForVariableCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr, $isAllowed));
1523-
}
1524-
1525-
return $scope;
1518+
return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
15261519
}
15271520

1528-
private function lookForExitAllowedUndefinedVariable(MutatingScope $scope, Expr $expr): MutatingScope
1521+
private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
15291522
{
1530-
if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
1531-
$scope = $scope->unsetAllowedUndefinedExpression($expr);
1532-
}
1533-
if (!$expr instanceof Variable) {
1534-
return $this->lookForVariableCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
1535-
}
1536-
1537-
return $scope;
1523+
return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
15381524
}
15391525

15401526
/**
15411527
* @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
15421528
*/
1543-
private function lookForVariableCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
1529+
private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
15441530
{
1545-
if ($expr instanceof Variable) {
1531+
if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
15461532
$scope = $callback($scope, $expr);
1547-
} elseif ($expr instanceof ArrayDimFetch) {
1548-
if ($expr->dim !== null) {
1549-
$scope = $callback($scope, $expr);
1550-
}
1533+
}
15511534

1552-
$scope = $this->lookForVariableCallback($scope, $expr->var, $callback);
1535+
if ($expr instanceof ArrayDimFetch) {
1536+
$scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
15531537
} elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
1554-
$scope = $this->lookForVariableCallback($scope, $expr->var, $callback);
1538+
$scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
15551539
} elseif ($expr instanceof StaticPropertyFetch) {
15561540
if ($expr->class instanceof Expr) {
1557-
$scope = $this->lookForVariableCallback($scope, $expr->class, $callback);
1541+
$scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
15581542
}
15591543
} elseif ($expr instanceof Array_ || $expr instanceof List_) {
15601544
foreach ($expr->items as $item) {
15611545
if ($item === null) {
15621546
continue;
15631547
}
15641548

1565-
$scope = $this->lookForVariableCallback($scope, $item->value, $callback);
1549+
$scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
15661550
}
15671551
}
15681552

@@ -2321,10 +2305,10 @@ static function (?Type $offsetType, Type $valueType) use (&$arrayType): void {
23212305
);
23222306
} elseif ($expr instanceof Coalesce) {
23232307
$nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left, false);
2324-
$condScope = $this->lookForEnterAllowedUndefinedVariable($nonNullabilityResult->getScope(), $expr->left, true);
2308+
$condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left);
23252309
$condResult = $this->processExprNode($expr->left, $condScope, $nodeCallback, $context->enterDeep());
23262310
$scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
2327-
$scope = $this->lookForExitAllowedUndefinedVariable($scope, $expr->left);
2311+
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left);
23282312

23292313
$rightScope = $scope->filterByFalseyValue(new Expr\Isset_([$expr->left]));
23302314
$rightResult = $this->processExprNode($expr->right, $rightScope, $nodeCallback, $context->enterDeep());
@@ -2395,26 +2379,26 @@ static function (?Type $offsetType, Type $valueType) use (&$arrayType): void {
23952379
}
23962380
} elseif ($expr instanceof Expr\Empty_) {
23972381
$nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr, true);
2398-
$scope = $this->lookForEnterAllowedUndefinedVariable($nonNullabilityResult->getScope(), $expr->expr, true);
2382+
$scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr);
23992383
$result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
24002384
$scope = $result->getScope();
24012385
$hasYield = $result->hasYield();
24022386
$throwPoints = $result->getThrowPoints();
24032387
$scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
2404-
$scope = $this->lookForExitAllowedUndefinedVariable($scope, $expr->expr);
2388+
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr);
24052389
} elseif ($expr instanceof Expr\Isset_) {
24062390
$hasYield = false;
24072391
$throwPoints = [];
24082392
$nonNullabilityResults = [];
24092393
foreach ($expr->vars as $var) {
24102394
$nonNullabilityResult = $this->ensureNonNullability($scope, $var, true);
2411-
$scope = $this->lookForEnterAllowedUndefinedVariable($nonNullabilityResult->getScope(), $var, true);
2395+
$scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var);
24122396
$result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep());
24132397
$scope = $result->getScope();
24142398
$hasYield = $hasYield || $result->hasYield();
24152399
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
24162400
$nonNullabilityResults[] = $nonNullabilityResult;
2417-
$scope = $this->lookForExitAllowedUndefinedVariable($scope, $var);
2401+
$scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
24182402
}
24192403
foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) {
24202404
$scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
@@ -3466,7 +3450,7 @@ static function (): void {
34663450
if ($arrayItem->value instanceof ArrayDimFetch && $arrayItem->value->dim === null) {
34673451
$itemScope = $itemScope->enterExpressionAssign($arrayItem->value);
34683452
}
3469-
$itemScope = $this->lookForEnterAllowedUndefinedVariable($itemScope, $arrayItem->value, true);
3453+
$itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value);
34703454
$itemResult = $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep());
34713455
$hasYield = $hasYield || $itemResult->hasYield();
34723456
$throwPoints = array_merge($throwPoints, $itemResult->getThrowPoints());

src/Analyser/ScopeFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface ScopeFactory
1717
* @param VariableTypeHolder[] $moreSpecificTypes
1818
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
1919
* @param array<string, true> $currentlyAssignedExpressions
20-
* @param array<string, bool> $currentlyAllowedUndefinedExpressions
20+
* @param array<string, true> $currentlyAllowedUndefinedExpressions
2121
* @param array<string, Type> $nativeExpressionTypes
2222
* @param array<MethodReflection|FunctionReflection> $inFunctionCallsStack
2323
*

src/Rules/Properties/AccessPropertiesRule.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function __construct(
3333
private ReflectionProvider $reflectionProvider,
3434
private RuleLevelHelper $ruleLevelHelper,
3535
private bool $reportMagicProperties,
36+
private bool $checkDynamicProperties,
3637
)
3738
{
3839
}
@@ -88,7 +89,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string
8889
];
8990
}
9091

91-
if ($scope->isUndefinedExpressionAllowed($node)) {
92+
if ($this->canAccessUndefinedProperties($scope, $node)) {
9293
return [];
9394
}
9495

@@ -162,4 +163,9 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string
162163
return [];
163164
}
164165

166+
private function canAccessUndefinedProperties(Scope $scope, Node\Expr $node): bool
167+
{
168+
return $scope->isUndefinedExpressionAllowed($node) && !$this->checkDynamicProperties;
169+
}
170+
165171
}

tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ protected function getRule(): Rule
1717
{
1818
$reflectionProvider = $this->createReflectionProvider();
1919
return new AccessPropertiesInAssignRule(
20-
new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), true),
20+
new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), true, true),
2121
);
2222
}
2323

0 commit comments

Comments
 (0)