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
34 changes: 30 additions & 4 deletions src/Rules/IssetCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

use PhpParser\Node;
use PhpParser\Node\Expr;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\PropertyInitializationExpr;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Properties\PropertyDescriptor;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Type\NeverType;
Expand All @@ -27,6 +30,7 @@ final class IssetCheck
public function __construct(
private PropertyDescriptor $propertyDescriptor,
private PropertyReflectionFinder $propertyReflectionFinder,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
#[AutowiredParameter]
private bool $checkAdvancedIsset,
#[AutowiredParameter]
Expand All @@ -36,10 +40,32 @@ public function __construct(
}

/**
* @param class-string<Rule<covariant Node>>|null $ruleName
* @param ErrorIdentifier $identifier
* @param callable(Type): ?string $typeMessageCallback
*/
public function check(Expr $expr, Scope $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?IdentifierRuleError $error = null): ?IdentifierRuleError
public function check(Expr $expr, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?string $ruleName = null, ?IdentifierRuleError $error = null): ?IdentifierRuleError
{
$result = $this->checkInternal($expr, $scope, $operatorDescription, $identifier, $typeMessageCallback, $ruleName, $expr, $error);

if ($ruleName !== null && $scope->isInTrait()) {
if ($result !== null) {
$this->constantConditionInTraitHelper->emitError($ruleName, $scope, $expr, true, $result);
return null;
}
$this->constantConditionInTraitHelper->emitNoError($ruleName, $scope, $expr);
return null;
}

return $result;
}

/**
* @param class-string<Rule<covariant Node>>|null $ruleName
* @param ErrorIdentifier $identifier
* @param callable(Type): ?string $typeMessageCallback
*/
private function checkInternal(Expr $expr, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope, string $operatorDescription, string $identifier, callable $typeMessageCallback, ?string $ruleName, Expr $originalExpr, ?IdentifierRuleError $error = null): ?IdentifierRuleError
{
// mirrored in PHPStan\Analyser\MutatingScope::issetCheck()
if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) {
Expand Down Expand Up @@ -114,7 +140,7 @@ public function check(Expr $expr, Scope $scope, string $operatorDescription, str
), $typeMessageCallback, $identifier, 'offset');

if ($error !== null) {
return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error);
return $this->checkInternal($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $ruleName, $originalExpr, $error);
}
}

Expand Down Expand Up @@ -220,11 +246,11 @@ static function (Type $type) use ($typeMessageCallback): ?string {

if ($error !== null) {
if ($expr instanceof Node\Expr\PropertyFetch) {
return $this->check($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error);
return $this->checkInternal($expr->var, $scope, $operatorDescription, $identifier, $typeMessageCallback, $ruleName, $originalExpr, $error);
}

if ($expr->class instanceof Expr) {
return $this->check($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $error);
return $this->checkInternal($expr->class, $scope, $operatorDescription, $identifier, $typeMessageCallback, $ruleName, $originalExpr, $error);
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/Rules/Variables/EmptyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PHPStan\Rules\Variables;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Rules\IssetCheck;
Expand All @@ -25,7 +27,7 @@ public function getNodeType(): string
return Node\Expr\Empty_::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$error = $this->issetCheck->check($node->expr, $scope, 'in empty()', 'empty', static function (Type $type): ?string {
$isNull = $type->isNull();
Expand Down Expand Up @@ -57,7 +59,7 @@ public function processNode(Node $node, Scope $scope): array
}

return 'is not nullable';
});
}, self::class);

if ($error === null) {
return [];
Expand Down
6 changes: 4 additions & 2 deletions src/Rules/Variables/IssetRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PHPStan\Rules\Variables;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Rules\IssetCheck;
Expand All @@ -25,7 +27,7 @@ public function getNodeType(): string
return Node\Expr\Isset_::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$messages = [];
foreach ($node->vars as $var) {
Expand All @@ -40,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array
}

return 'is not nullable';
});
}, self::class);
if ($error === null) {
continue;
}
Expand Down
8 changes: 5 additions & 3 deletions src/Rules/Variables/NullCoalesceRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PHPStan\Rules\Variables;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Rules\IssetCheck;
Expand All @@ -25,7 +27,7 @@ public function getNodeType(): string
return Node\Expr::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$typeMessageCallback = static function (Type $type): ?string {
$isNull = $type->isNull();
Expand All @@ -41,9 +43,9 @@ public function processNode(Node $node, Scope $scope): array
};

if ($node instanceof Node\Expr\BinaryOp\Coalesce) {
$error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', 'nullCoalesce', $typeMessageCallback);
$error = $this->issetCheck->check($node->left, $scope, 'on left side of ??', 'nullCoalesce', $typeMessageCallback, self::class);
} elseif ($node instanceof Node\Expr\AssignOp\Coalesce) {
$error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', 'nullCoalesce', $typeMessageCallback);
$error = $this->issetCheck->check($node->var, $scope, 'on left side of ??=', 'nullCoalesce', $typeMessageCallback, self::class);
} else {
return [];
}
Expand Down
28 changes: 21 additions & 7 deletions tests/PHPStan/Rules/Variables/EmptyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@

namespace PHPStan\Rules\Variables;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Comparison\ConstantConditionInTraitRule;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Properties\PropertyDescriptor;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Rules\Rule;
use PHPStan\Testing\CompositeRule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<EmptyRule>
* @extends RuleTestCase<CompositeRule>
*/
class EmptyRuleTest extends RuleTestCase
{
Expand All @@ -20,12 +23,17 @@ class EmptyRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new EmptyRule(new IssetCheck(
new PropertyDescriptor(),
new PropertyReflectionFinder(),
true,
$this->treatPhpDocTypesAsCertain,
));
// @phpstan-ignore argument.type
return new CompositeRule([
new EmptyRule(new IssetCheck(
new PropertyDescriptor(),
new PropertyReflectionFinder(),
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
true,
$this->treatPhpDocTypesAsCertain,
)),
new ConstantConditionInTraitRule(),
]);
}

protected function shouldTreatPhpDocTypesAsCertain(): bool
Expand Down Expand Up @@ -252,4 +260,10 @@ public function testIssetAfterRememberedConstructor(): void
]);
}

public function testInTrait(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/isset-in-trait.php'], []);
}

}
37 changes: 30 additions & 7 deletions tests/PHPStan/Rules/Variables/IssetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

namespace PHPStan\Rules\Variables;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Comparison\ConstantConditionInTraitRule;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Properties\PropertyDescriptor;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Rules\Rule;
use PHPStan\Testing\CompositeRule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<IssetRule>
* @extends RuleTestCase<CompositeRule>
*/
class IssetRuleTest extends RuleTestCase
{
Expand All @@ -19,12 +22,17 @@ class IssetRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new IssetRule(new IssetCheck(
new PropertyDescriptor(),
new PropertyReflectionFinder(),
true,
$this->treatPhpDocTypesAsCertain,
));
// @phpstan-ignore argument.type
return new CompositeRule([
new IssetRule(new IssetCheck(
new PropertyDescriptor(),
new PropertyReflectionFinder(),
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
true,
$this->treatPhpDocTypesAsCertain,
)),
new ConstantConditionInTraitRule(),
]);
}

protected function shouldTreatPhpDocTypesAsCertain(): bool
Expand Down Expand Up @@ -562,4 +570,19 @@ public function testBug14393(): void
]);
}

public function testInTrait(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/isset-in-trait.php'], [
[
'Property IssetInTrait\ClassA::$j (int) in isset() is not nullable.',
36,
],
[
'Property IssetInTrait\ClassB::$j (int) in isset() is not nullable.',
36,
],
]);
}

}
36 changes: 29 additions & 7 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,36 @@

namespace PHPStan\Rules\Variables;

use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Comparison\ConstantConditionInTraitRule;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Properties\PropertyDescriptor;
use PHPStan\Rules\Properties\PropertyReflectionFinder;
use PHPStan\Rules\Rule;
use PHPStan\Testing\CompositeRule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<NullCoalesceRule>
* @extends RuleTestCase<CompositeRule>
*/
class NullCoalesceRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new NullCoalesceRule(new IssetCheck(
new PropertyDescriptor(),
new PropertyReflectionFinder(),
true,
$this->shouldTreatPhpDocTypesAsCertain(),
));
// @phpstan-ignore argument.type
return new CompositeRule([
new NullCoalesceRule(new IssetCheck(
new PropertyDescriptor(),
new PropertyReflectionFinder(),
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
true,
$this->shouldTreatPhpDocTypesAsCertain(),
)),
new ConstantConditionInTraitRule(),
]);
}

public function testCoalesceRule(): void
Expand Down Expand Up @@ -448,4 +456,18 @@ public function testBug14393(): void
]);
}

public function testInTrait(): void
{
$this->analyse([__DIR__ . '/data/isset-in-trait.php'], [
[
'Property IssetInTrait\ClassA::$j (int) on left side of ?? is not nullable.',
35,
],
[
'Property IssetInTrait\ClassB::$j (int) on left side of ?? is not nullable.',
35,
],
]);
}

}
53 changes: 53 additions & 0 deletions tests/PHPStan/Rules/Variables/data/isset-in-trait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace IssetInTrait;

// Trait where property type differs across classes (should suppress errors)
trait IssetTrait
{
public function doFoo(): void
{
var_dump($this->i ?? -1);
var_dump(isset($this->i));
var_dump(empty($this->i));
}
}

class MyClass
{
use IssetTrait;

public int $i = 10;
}

class MyClassNullable
{
use IssetTrait;

public ?int $i = null;
}

// Trait where property type is the same in all classes (should still report errors)
trait AlwaysNonNullableTrait
{
public function doFoo(): void
{
var_dump($this->j ?? -1);
var_dump(isset($this->j));
var_dump(empty($this->j));
}
}

class ClassA
{
use AlwaysNonNullableTrait;

public int $j = 10;
}

class ClassB
{
use AlwaysNonNullableTrait;

public int $j = 20;
}
Loading