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
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,7 @@ parameters:
-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
count: 2
count: 1
path: src/Type/Php/PropertyExistsTypeSpecifyingExtension.php

-
Expand Down
19 changes: 19 additions & 0 deletions src/Rules/Properties/AccessStaticPropertiesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
namespace PHPStan\Rules\Properties;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\NullsafeOperatorHelper;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
Expand Down Expand Up @@ -256,6 +262,19 @@ private function processSingleProperty(Scope $scope, StaticPropertyFetch $node,
]);
}

if ($node->class instanceof Name) {
$classExpr = new ClassConstFetch($node->class, new Identifier('class'));
} else {
$classExpr = $node->class;
}
$propertyExistsCall = new FuncCall(new FullyQualified('property_exists'), [
new Arg($classExpr),
new Arg(new String_($name)),
]);
if ($scope->getType($propertyExistsCall)->isTrue()->yes()) {
return [];
}

return array_merge($messages, [
RuleErrorBuilder::message(sprintf(
'Access to an undefined static property %s::$%s.',
Expand Down
34 changes: 27 additions & 7 deletions src/Type/Php/PropertyExistsTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Type\Accessory\HasPropertyType;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\UnionType;
use function count;

#[AutowiredService]
Expand Down Expand Up @@ -71,17 +73,35 @@
}

$objectType = $scope->getType($args[0]->value);
if ($objectType instanceof ConstantStringType) {
return new SpecifiedTypes([], []);
} elseif ($objectType->isObject()->yes()) {
$propertyNode = new PropertyFetch(
if ($objectType->isString()->yes()) {

Check warning on line 76 in src/Type/Php/PropertyExistsTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $objectType = $scope->getType($args[0]->value); - if ($objectType->isString()->yes()) { + if (!$objectType->isString()->no()) { return $this->typeSpecifier->create( new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), new ConstantBooleanType(true),

Check warning on line 76 in src/Type/Php/PropertyExistsTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ } $objectType = $scope->getType($args[0]->value); - if ($objectType->isString()->yes()) { + if (!$objectType->isString()->no()) { return $this->typeSpecifier->create( new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()), new ConstantBooleanType(true),
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('property_exists'), $node->getRawArgs()),
new ConstantBooleanType(true),
$context,
$scope,
);
}

if (!$objectType->isObject()->yes()) {

Check warning on line 85 in src/Type/Php/PropertyExistsTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ ); } - if (!$objectType->isObject()->yes()) { + if ($objectType->isObject()->no()) { return $this->typeSpecifier->create( $args[0]->value, new UnionType([

Check warning on line 85 in src/Type/Php/PropertyExistsTypeSpecifyingExtension.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ ); } - if (!$objectType->isObject()->yes()) { + if ($objectType->isObject()->no()) { return $this->typeSpecifier->create( $args[0]->value, new UnionType([
return $this->typeSpecifier->create(
$args[0]->value,
new Identifier($propertyNameType->getValue()),
new UnionType([
new IntersectionType([
new ObjectWithoutClassType(),
new HasPropertyType($propertyNameType->getValue()),
]),
new ClassStringType(),
]),
$context,
$scope,
);
} else {
return new SpecifiedTypes([], []);
}

$propertyNode = new PropertyFetch(
$args[0]->value,
new Identifier($propertyNameType->getValue()),
);

$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyNode, $scope);
if ($propertyReflection !== null) {
if (!$propertyReflection->isNative()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public function testRuleExpressionNames(): void
]);
}

public function testBug2861(): void
{
$this->analyse([__DIR__ . '/data/bug-2861-assign.php'], []);
}

#[RequiresPhp('>= 8.5.0')]
public function testAsymmetricVisibility(): void
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,9 @@ public function testBug8668Bis(): void
]);
}

public function testBug2861(): void
{
$this->analyse([__DIR__ . '/data/bug-2861.php'], []);
}

}
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-2861-assign.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace Bug2861Assign;

class Foo {
public static function test(): void {
if (property_exists(static::class, 'default')) {
static::$default = 'value';
}
}

/**
* @param class-string $className
*/
public static function testExpr(string $className): void {
if (property_exists($className, 'default')) {
$className::$default = 'value';
}
}
}
56 changes: 56 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-2861.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php declare(strict_types = 1);

namespace Bug2861;

trait EnumTrait {
/** @var mixed */
protected $value;

/** @param mixed $value */
final public function __construct($value) {
$this->value = $value;
}

/** @return static|null */
public static function getDefault() {
if (property_exists(static::class, 'default') && null !== static::$default) {
$obj = static::$default;
return new static($obj);
}
return null;
}
}

class Foo {
use EnumTrait;
public const BLA = 'bla';
}

class Bar {
use EnumTrait;
public static $default = 'bla';
public const BLA = 'bla';
}

class Baz {
use EnumTrait;

/** @return static|null */
public static function getDefault2() {
if (property_exists(self::class, 'default') && null !== self::$default) {
return new static(self::$default);
}
return null;
}
}

class ExpressionBased {
/**
* @param class-string $className
*/
public static function test(string $className): void {
if (property_exists($className, 'default')) {
echo $className::$default;
}
}
}
Loading