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
36 changes: 35 additions & 1 deletion src/Rules/Properties/AccessStaticPropertiesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Internal\SprintfHelper;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\ClassNameCheck;
Expand Down Expand Up @@ -175,9 +176,21 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
$classType = $scope->resolveTypeByName($node->class);
}
} else {
$exprToCheck = NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class);
// A constant class-name string or a class-string type describes the
// object it names, so route the check through that object type. This
// makes constant-string / class-string unions behave exactly like
// object unions with regard to rule levels (member filtering when
// checkUnionTypes is off, whole-union check when it is on).
// Every other type (objects, or unions with members that are not
// class-strings) yields no class name here and keeps the old path.
$objectType = $scope->getType($exprToCheck)->getClassStringObjectType();
if ($this->canCheckThroughObjectType($objectType)) {
$exprToCheck = new TypeExpr($objectType);
}
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $node->class),
$exprToCheck,
sprintf('Access to static property $%s on an unknown class %%s.', SprintfHelper::escapeFormatString($name)),
static fn (Type $type): bool => $type->canAccessProperties()->yes() && $type->hasStaticProperty($name)->yes(),
);
Expand Down Expand Up @@ -339,4 +352,25 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
]);
}

/**
* Class-strings without a class name (class-string, class-string<object>,
* class-string<T of object>) and class-strings naming a trait keep the
* string code path: RuleLevelHelper would filter the object type out or
* report the trait as an unknown class.
*/
private function canCheckThroughObjectType(Type $objectType): bool
{
if (count($objectType->getObjectClassNames()) === 0) {
return false;
}

foreach ($objectType->getObjectClassReflections() as $classReflection) {
if ($classReflection->isTrait()) {
return false;
}
}

return true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,46 @@ public function testAsymmetricVisibility(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testWritesOnStringUnions(): void
{
$this->analyse([__DIR__ . '/data/static-property-string-unions-assign.php'], [
[
'Access to an undefined static property StaticPropertyStringUnionsAssign\Baz::$missing.',
32,
],
[
'Access to an undefined static property StaticPropertyStringUnionsAssign\Baz::$missing.',
38,
],
[
'Access to an undefined static property StaticPropertyStringUnionsAssign\Baz::$missing.',
44,
],
[
'Access to private property $secret of class StaticPropertyStringUnionsAssign\Foo.',
50,
],
[
'Access to an undefined static property StaticPropertyStringUnionsAssign\Baz|StaticPropertyStringUnionsAssign\Foo::$count.',
56,
],
]);
}

#[RequiresPhp('>= 8.5.0')]
public function testAsymmetricVisibilityOnConstantString(): void
{
$this->analyse([__DIR__ . '/data/static-property-string-unions-asymmetric.php'], [
[
'Access to private(set) property $privateSet of class StaticPropertyStringUnionsAsymmetric\Foo.',
16,
],
[
'Access to protected(set) property $protectedSet of class StaticPropertyStringUnionsAsymmetric\Foo.',
17,
],
]);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@
class AccessStaticPropertiesRuleTest extends RuleTestCase
{

private bool $checkUnionTypes = true;

protected function getRule(): Rule
{
$reflectionProvider = self::createReflectionProvider();
$ruleLevelHelper = new RuleLevelHelper(
$reflectionProvider,
checkNullables: true,
checkThisOnly: false,
checkUnionTypes: true,
checkUnionTypes: $this->checkUnionTypes,
checkExplicitMixed: false,
checkImplicitMixed: false,
checkBenevolentUnionTypes: false,
Expand Down Expand Up @@ -396,4 +398,95 @@ public function testBug15002(): void
$this->analyse([__DIR__ . '/../Methods/data/bug-15002.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testStaticPropertyAccessOnStringUnions(): void
{
$this->analyse([__DIR__ . '/data/static-property-string-unions.php'], [
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
32,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz|StaticPropertyStringUnions\Foo::$create.',
38,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz|StaticPropertyStringUnions\Foo::$create.',
50,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz|StaticPropertyStringUnions\Foo::$create.',
58,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
66,
],
[
'Static access to instance property StaticPropertyStringUnions\Foo::$doBar.',
80,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$missing.',
118,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$missing.',
119,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$missing.',
120,
],
[
'Access to private property $privateProperty of class StaticPropertyStringUnions\WithNonPublic.',
126,
],
[
'Access to protected property $protectedProperty of class StaticPropertyStringUnions\WithNonPublic.',
127,
],
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testStaticPropertyAccessOnStringUnionsWithoutCheckUnionTypes(): void
{
$this->checkUnionTypes = false;
$this->analyse([__DIR__ . '/data/static-property-string-unions.php'], [
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
32,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$create.',
66,
],
[
'Static access to instance property StaticPropertyStringUnions\Foo::$doBar.',
80,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$missing.',
118,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$missing.',
119,
],
[
'Access to an undefined static property StaticPropertyStringUnions\Baz::$missing.',
120,
],
[
'Access to private property $privateProperty of class StaticPropertyStringUnions\WithNonPublic.',
126,
],
[
'Access to protected property $protectedProperty of class StaticPropertyStringUnions\WithNonPublic.',
127,
],
]);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace StaticPropertyStringUnionsAssign;

class Foo
{

public static int $count = 0;

private static int $secret = 0;

}

class Baz
{

}

trait SomeTrait
{

public static int $traitProperty = 0;

}

class Runner
{

public function writeToMissingProperty(): void
{
$class = Baz::class;
$class::$missing = 1;
}

public function incrementMissingProperty(): void
{
$class = Baz::class;
$class::$missing++;
}

public function coalesceAssignMissingProperty(): void
{
$class = Baz::class;
$class::$missing ??= 1;
}

public function writeToPrivateProperty(): void
{
$class = Foo::class;
$class::$secret = 1;
}

public function writeToUnion(bool $flag): void
{
$class = $flag ? Foo::class : Baz::class;
$class::$count = 1;
}

public function writeToExistingProperty(): void
{
$class = Foo::class;
$class::$count = 1;
}

public function writeThroughTrait(): void
{
$trait = SomeTrait::class;
$trait::$traitProperty = 1;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1); // lint >= 8.5

namespace StaticPropertyStringUnionsAsymmetric;

class Foo
{

public private(set) static int $privateSet = 0;

public protected(set) static int $protectedSet = 0;

}

function (): void {
$class = Foo::class;
$class::$privateSet = 1;
$class::$protectedSet = 1;
echo $class::$privateSet;
};
Loading
Loading