From f13c2c7e9fa2ba5dc942fc6b8d78eb974587a28e Mon Sep 17 00:00:00 2001 From: rajyan <38206553+rajyan@users.noreply.github.com> Date: Thu, 21 Apr 2022 22:01:26 +0900 Subject: [PATCH] Feature/disallow dynamic property option --- conf/config.level0.neon | 1 + conf/config.neon | 2 + src/Analyser/DirectScopeFactory.php | 2 +- src/Analyser/LazyScopeFactory.php | 2 +- src/Analyser/MutatingScope.php | 8 +- src/Analyser/NodeScopeResolver.php | 62 ++-- src/Analyser/ScopeFactory.php | 2 +- src/Rules/Properties/AccessPropertiesRule.php | 8 +- .../AccessPropertiesInAssignRuleTest.php | 2 +- .../Properties/AccessPropertiesRuleTest.php | 288 +++++++++++++++++- .../AccessStaticPropertiesRuleTest.php | 4 - .../Rules/Properties/data/bug-3171.php | 20 ++ 12 files changed, 336 insertions(+), 65 deletions(-) create mode 100644 tests/PHPStan/Rules/Properties/data/bug-3171.php diff --git a/conf/config.level0.neon b/conf/config.level0.neon index 45fbd90efd..2da06d326a 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -146,6 +146,7 @@ services: - phpstan.rules.rule arguments: reportMagicProperties: %reportMagicProperties% + checkDynamicProperties: %checkDynamicProperties% - class: PHPStan\Rules\Properties\AccessStaticPropertiesRule diff --git a/conf/config.neon b/conf/config.neon index 05f200f666..968366aada 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -62,6 +62,7 @@ parameters: checkMissingTypehints: false checkTooWideReturnTypesInProtectedAndPublicMethods: false checkUninitializedProperties: false + checkDynamicProperties: false inferPrivatePropertyTypeFromConstructor: false reportMaybes: false reportMaybesInMethodSignatures: false @@ -252,6 +253,7 @@ parametersSchema: checkMissingTypehints: bool() checkTooWideReturnTypesInProtectedAndPublicMethods: bool() checkUninitializedProperties: bool() + checkDynamicProperties: bool() inferPrivatePropertyTypeFromConstructor: bool() tipsOfTheDay: bool() diff --git a/src/Analyser/DirectScopeFactory.php b/src/Analyser/DirectScopeFactory.php index 4364eb9ea0..15538eed25 100644 --- a/src/Analyser/DirectScopeFactory.php +++ b/src/Analyser/DirectScopeFactory.php @@ -46,7 +46,7 @@ public function __construct( * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions + * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack * diff --git a/src/Analyser/LazyScopeFactory.php b/src/Analyser/LazyScopeFactory.php index e5522da25b..e4d34f7818 100644 --- a/src/Analyser/LazyScopeFactory.php +++ b/src/Analyser/LazyScopeFactory.php @@ -38,7 +38,7 @@ public function __construct( * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions + * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes * @param array<(FunctionReflection|MethodReflection)> $inFunctionCallsStack * diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index fa068bbb7e..2d00b525f2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -167,7 +167,7 @@ class MutatingScope implements Scope * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions + * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes * @param array $inFunctionCallsStack */ @@ -3906,11 +3906,11 @@ public function isInExpressionAssign(Expr $expr): bool return array_key_exists($exprString, $this->currentlyAssignedExpressions); } - public function setAllowedUndefinedExpression(Expr $expr, bool $isAllowed): self + public function setAllowedUndefinedExpression(Expr $expr): self { $exprString = $this->getNodeKey($expr); $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions; - $currentlyAllowedUndefinedExpressions[$exprString] = $isAllowed; + $currentlyAllowedUndefinedExpressions[$exprString] = true; return $this->scopeFactory->create( $this->context, @@ -3964,7 +3964,7 @@ public function unsetAllowedUndefinedExpression(Expr $expr): self public function isUndefinedExpressionAllowed(Expr $expr): bool { $exprString = $this->getNodeKey($expr); - return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions) && $this->currentlyAllowedUndefinedExpressions[$exprString]; + return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions); } public function assignVariable(string $variableName, Type $type, ?TrinaryLogic $certainty = null): self diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 48cb5a518f..c1e8b62bd6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -1336,9 +1336,9 @@ private function processStmtNode( $hasYield = false; $throwPoints = []; foreach ($stmt->vars as $var) { - $scope = $this->lookForEnterAllowedUndefinedVariable($scope, $var, true); + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); $scope = $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); - $scope = $this->lookForExitAllowedUndefinedVariable($scope, $var); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); $scope = $scope->unsetExpression($var); } } elseif ($stmt instanceof Node\Stmt\Use_) { @@ -1355,9 +1355,9 @@ private function processStmtNode( if (!$var instanceof Variable) { throw new ShouldNotHappenException(); } - $scope = $this->lookForEnterAllowedUndefinedVariable($scope, $var, true); + $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var); $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep()); - $scope = $this->lookForExitAllowedUndefinedVariable($scope, $var); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); if (!is_string($var->name)) { continue; @@ -1513,48 +1513,32 @@ private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $cla ); } - private function lookForEnterAllowedUndefinedVariable(MutatingScope $scope, Expr $expr, bool $isAllowed): MutatingScope + private function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope { - if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { - $scope = $scope->setAllowedUndefinedExpression($expr, $isAllowed); - } - if (!$expr instanceof Variable) { - return $this->lookForVariableCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr, $isAllowed)); - } - - return $scope; + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); } - private function lookForExitAllowedUndefinedVariable(MutatingScope $scope, Expr $expr): MutatingScope + private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope { - if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { - $scope = $scope->unsetAllowedUndefinedExpression($expr); - } - if (!$expr instanceof Variable) { - return $this->lookForVariableCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr)); - } - - return $scope; + return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr)); } /** * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback */ - private function lookForVariableCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope + private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope { - if ($expr instanceof Variable) { + if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { $scope = $callback($scope, $expr); - } elseif ($expr instanceof ArrayDimFetch) { - if ($expr->dim !== null) { - $scope = $callback($scope, $expr); - } + } - $scope = $this->lookForVariableCallback($scope, $expr->var, $callback); + if ($expr instanceof ArrayDimFetch) { + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { - $scope = $this->lookForVariableCallback($scope, $expr->var, $callback); + $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); } elseif ($expr instanceof StaticPropertyFetch) { if ($expr->class instanceof Expr) { - $scope = $this->lookForVariableCallback($scope, $expr->class, $callback); + $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback); } } elseif ($expr instanceof Array_ || $expr instanceof List_) { foreach ($expr->items as $item) { @@ -1562,7 +1546,7 @@ private function lookForVariableCallback(MutatingScope $scope, Expr $expr, Closu continue; } - $scope = $this->lookForVariableCallback($scope, $item->value, $callback); + $scope = $this->lookForExpressionCallback($scope, $item->value, $callback); } } @@ -2321,10 +2305,10 @@ static function (?Type $offsetType, Type $valueType) use (&$arrayType): void { ); } elseif ($expr instanceof Coalesce) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left, false); - $condScope = $this->lookForEnterAllowedUndefinedVariable($nonNullabilityResult->getScope(), $expr->left, true); + $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left); $condResult = $this->processExprNode($expr->left, $condScope, $nodeCallback, $context->enterDeep()); $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions()); - $scope = $this->lookForExitAllowedUndefinedVariable($scope, $expr->left); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left); $rightScope = $scope->filterByFalseyValue(new Expr\Isset_([$expr->left])); $rightResult = $this->processExprNode($expr->right, $rightScope, $nodeCallback, $context->enterDeep()); @@ -2395,26 +2379,26 @@ static function (?Type $offsetType, Type $valueType) use (&$arrayType): void { } } elseif ($expr instanceof Expr\Empty_) { $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr, true); - $scope = $this->lookForEnterAllowedUndefinedVariable($nonNullabilityResult->getScope(), $expr->expr, true); + $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr); $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $result->hasYield(); $throwPoints = $result->getThrowPoints(); $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); - $scope = $this->lookForExitAllowedUndefinedVariable($scope, $expr->expr); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr); } elseif ($expr instanceof Expr\Isset_) { $hasYield = false; $throwPoints = []; $nonNullabilityResults = []; foreach ($expr->vars as $var) { $nonNullabilityResult = $this->ensureNonNullability($scope, $var, true); - $scope = $this->lookForEnterAllowedUndefinedVariable($nonNullabilityResult->getScope(), $var, true); + $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var); $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep()); $scope = $result->getScope(); $hasYield = $hasYield || $result->hasYield(); $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); $nonNullabilityResults[] = $nonNullabilityResult; - $scope = $this->lookForExitAllowedUndefinedVariable($scope, $var); + $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var); } foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) { $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); @@ -3466,7 +3450,7 @@ static function (): void { if ($arrayItem->value instanceof ArrayDimFetch && $arrayItem->value->dim === null) { $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); } - $itemScope = $this->lookForEnterAllowedUndefinedVariable($itemScope, $arrayItem->value, true); + $itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value); $itemResult = $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $itemResult->hasYield(); $throwPoints = array_merge($throwPoints, $itemResult->getThrowPoints()); diff --git a/src/Analyser/ScopeFactory.php b/src/Analyser/ScopeFactory.php index 7360e69533..bfb0912e9a 100644 --- a/src/Analyser/ScopeFactory.php +++ b/src/Analyser/ScopeFactory.php @@ -17,7 +17,7 @@ interface ScopeFactory * @param VariableTypeHolder[] $moreSpecificTypes * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions + * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes * @param array $inFunctionCallsStack * diff --git a/src/Rules/Properties/AccessPropertiesRule.php b/src/Rules/Properties/AccessPropertiesRule.php index ad713ac7c8..55d2e237f2 100644 --- a/src/Rules/Properties/AccessPropertiesRule.php +++ b/src/Rules/Properties/AccessPropertiesRule.php @@ -33,6 +33,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, private RuleLevelHelper $ruleLevelHelper, private bool $reportMagicProperties, + private bool $checkDynamicProperties, ) { } @@ -88,7 +89,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string ]; } - if ($scope->isUndefinedExpressionAllowed($node)) { + if ($this->canAccessUndefinedProperties($scope, $node)) { return []; } @@ -162,4 +163,9 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string return []; } + private function canAccessUndefinedProperties(Scope $scope, Node\Expr $node): bool + { + return $scope->isUndefinedExpressionAllowed($node) && !$this->checkDynamicProperties; + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php index e1a4f4b73e..861bc158be 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesInAssignRuleTest.php @@ -17,7 +17,7 @@ protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); return new AccessPropertiesInAssignRule( - new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), true), + new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, false, true, false), true, true), ); } diff --git a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php index ddfabe3eaf..1ee7b1aec8 100644 --- a/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessPropertiesRuleTest.php @@ -17,16 +17,19 @@ class AccessPropertiesRuleTest extends RuleTestCase private bool $checkUnionTypes; + private bool $checkDynamicProperties; + protected function getRule(): Rule { $reflectionProvider = $this->createReflectionProvider(); - return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false), true); + return new AccessPropertiesRule($reflectionProvider, new RuleLevelHelper($reflectionProvider, true, $this->checkThisOnly, $this->checkUnionTypes, false), true, $this->checkDynamicProperties); } public function testAccessProperties(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ @@ -145,18 +148,10 @@ public function testAccessProperties(): void 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', 299, ], - [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, - ], [ 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', 402, ], - [ - 'Cannot access property $array on stdClass|null.', - 412, - ], ], ); } @@ -165,6 +160,7 @@ public function testAccessPropertiesWithoutUnionTypes(): void { $this->checkThisOnly = false; $this->checkUnionTypes = false; + $this->checkDynamicProperties = false; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ @@ -267,10 +263,6 @@ public function testAccessPropertiesWithoutUnionTypes(): void 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', 299, ], - [ - 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', - 386, - ], ], ); } @@ -282,6 +274,7 @@ public function testRuleAssignOp(): void } $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/access-properties-assign-op.php'], [ [ 'Access to an undefined property TestAccessProperties\AssignOpNonexistentProperty::$flags.', @@ -294,6 +287,7 @@ public function testAccessPropertiesOnThisOnly(): void { $this->checkThisOnly = true; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse( [__DIR__ . '/data/access-properties.php'], [ @@ -305,10 +299,221 @@ public function testAccessPropertiesOnThisOnly(): void 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', 24, ], + ], + ); + } + + public function testAccessPropertiesOnDynamicProperties(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse( + [__DIR__ . '/data/access-properties.php'], + [ + [ + 'Access to an undefined property TestAccessProperties\BarAccessProperties::$loremipsum.', + 23, + ], + [ + 'Access to private property $foo of parent class TestAccessProperties\FooAccessProperties.', + 24, + ], + [ + 'Cannot access property $propertyOnString on string.', + 31, + ], + [ + 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', + 42, + ], + [ + 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', + 43, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', + 45, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', + 48, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$baz.', + 49, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$nonexistent.', + 51, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$nonexistent.', + 52, + ], + [ + 'Access to private property TestAccessProperties\FooAccessProperties::$foo.', + 58, + ], + [ + 'Access to protected property TestAccessProperties\FooAccessProperties::$bar.', + 59, + ], + [ + 'Access to property $foo on an unknown class TestAccessProperties\UnknownClass.', + 63, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyBaz.', + 65, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyBaz.', + 68, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyNonexistent.', + 69, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$emptyNonexistent.', + 70, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', + 75, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', + 76, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', + 76, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', + 77, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', + 77, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherNonexistent.', + 78, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', + 80, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', + 80, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', + 81, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', + 82, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', + 83, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$anotherEmptyNonexistent.', + 83, + ], + [ + 'Access to property $test on an unknown class TestAccessProperties\FirstUnknownClass.', + 146, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Access to property $test on an unknown class TestAccessProperties\SecondUnknownClass.', + 146, + 'Learn more at https://phpstan.org/user-guide/discovering-symbols', + ], + [ + 'Access to an undefined property TestAccessProperties\WithFooAndBarProperty|TestAccessProperties\WithFooProperty::$bar.', + 176, + ], + [ + 'Access to an undefined property TestAccessProperties\SomeInterface&TestAccessProperties\WithFooProperty::$bar.', + 193, + ], + [ + 'Cannot access property $ipsum on TestAccessProperties\FooAccessProperties|null.', + 207, + ], + [ + 'Cannot access property $foo on null.', + 220, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$lorem.', + 247, + ], + [ + 'Access to an undefined property TestAccessProperties\FooAccessProperties::$dolor.', + 250, + ], + [ + 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', + 264, + ], + [ + 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', + 266, + ], + [ + 'Access to an undefined property TestAccessProperties\NullCoalesce::$bar.', + 270, + ], + [ + 'Cannot access property $bar on TestAccessProperties\NullCoalesce|null.', + 272, + ], + [ + 'Cannot access property $foo on TestAccessProperties\NullCoalesce|null.', + 272, + ], + [ + 'Cannot access property $foo on TestAccessProperties\NullCoalesce|null.', + 272, + ], + [ + 'Access to an undefined property TestAccessProperties\IssetPropertyInWhile::$foo.', + 282, + ], + [ + 'Access to an undefined property class@anonymous/tests/PHPStan/Rules/Properties/data/access-properties.php:294::$barProperty.', + 299, + ], + [ + 'Access to an undefined property TestAccessProperties\PropertyIssetOnPossibleFalse|false::$foo.', + 315, + ], + [ + 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', + 379, + ], [ 'Access to an undefined property TestAccessProperties\AccessInIsset::$foo.', 386, ], + [ + 'Cannot access property $selfOrNull on TestAccessProperties\RevertNonNullabilityForIsset|null.', + 402, + ], + [ + 'Access to an undefined property stdClass|null::$array.', + 412, + ], ], ); } @@ -317,6 +522,7 @@ public function testAccessPropertiesAfterIsNullInBooleanOr(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/access-properties-after-isnull.php'], [ [ 'Cannot access property $fooProperty on null.', @@ -357,6 +563,7 @@ public function testDateIntervalChildProperties(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/date-interval-child-properties.php'], [ [ 'Access to an undefined property AccessPropertiesDateIntervalChild\DateIntervalChild::$nonexistent.', @@ -369,6 +576,7 @@ public function testClassExists(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/access-properties-class-exists.php'], [ [ @@ -398,6 +606,7 @@ public function testMixin(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/mixin.php'], [ [ 'Access to an undefined property MixinProperties\GenericFoo::$namee.', @@ -410,6 +619,7 @@ public function testBug3947(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-3947.php'], []); } @@ -421,6 +631,7 @@ public function testNullSafe(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/nullsafe-property-fetch.php'], [ [ @@ -450,6 +661,7 @@ public function testBug3371(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-3371.php'], []); } @@ -461,6 +673,7 @@ public function testBug4527(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-4527.php'], []); } @@ -468,6 +681,7 @@ public function testBug4808(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-4808.php'], []); } @@ -479,6 +693,7 @@ public function testBug5868(): void } $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-5868.php'], [ [ 'Cannot access property $child on Bug5868PropertyFetch\Foo|null.', @@ -507,6 +722,7 @@ public function testBug6385(): void $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-6385.php'], [ [ 'Access to an undefined property UnitEnum::$value.', @@ -526,6 +742,7 @@ public function testBug6566(): void } $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-6566.php'], []); } @@ -533,6 +750,7 @@ public function testBug6899(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-6899.php'], [ [ 'Cannot access property $prop on string.', @@ -553,6 +771,7 @@ public function testBug6026(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-6026.php'], []); } @@ -560,6 +779,7 @@ public function testBug3659(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-3659.php'], []); } @@ -567,15 +787,57 @@ public function testDynamicProperties(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/dynamic-properties.php'], []); } + public function testCheckDynamicProperties(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = true; + $this->analyse([__DIR__ . '/data/dynamic-properties.php'], [ + [ + 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', + 9, + ], + [ + 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', + 10, + ], + [ + 'Access to an undefined property DynamicProperties\Foo::$dynamicProperty.', + 11, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 14, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 15, + ], + [ + 'Access to an undefined property DynamicProperties\Bar::$dynamicProperty.', + 16, + ], + ]); + } public function testBug4559(): void { $this->checkThisOnly = false; $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; $this->analyse([__DIR__ . '/data/bug-4559.php'], []); } + public function testBug3171(): void + { + $this->checkThisOnly = false; + $this->checkUnionTypes = true; + $this->checkDynamicProperties = false; + $this->analyse([__DIR__ . '/data/bug-3171.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php index e5bdc4d0b7..409bcd4812 100644 --- a/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php +++ b/tests/PHPStan/Rules/Properties/AccessStaticPropertiesRuleTest.php @@ -164,10 +164,6 @@ public function testAccessStaticProperties(): void 'Static access to instance property ClassOrString::$instanceProperty.', 152, ], - [ - 'Access to an undefined static property AccessInIsset::$foo.', - 185, - ], [ 'Access to static property $foo on an unknown class TraitWithStaticProperty.', 209, diff --git a/tests/PHPStan/Rules/Properties/data/bug-3171.php b/tests/PHPStan/Rules/Properties/data/bug-3171.php new file mode 100644 index 0000000000..e7e90f7fdd --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-3171.php @@ -0,0 +1,20 @@ +property->someArray['test'] ?? 'test'; + } +} +