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
3 changes: 3 additions & 0 deletions src/Rules/Comparison/ConstantConditionInTraitHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public function emitNoError(
}

/**
* $value tells apart the contexts in which the same expression resolves to a different
* constant result. Rules whose message already encodes the result can pass a fixed value.
*
* @param class-string<Rule<covariant Node>> $ruleName
*/
public function emitError(
Expand Down
18 changes: 16 additions & 2 deletions src/Rules/Variables/EmptyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
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\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Rule;
use PHPStan\Type\Type;
Expand All @@ -16,7 +19,10 @@
final class EmptyRule implements Rule
{

public function __construct(private IssetCheck $issetCheck)
public function __construct(
private IssetCheck $issetCheck,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
)
{
}

Expand All @@ -25,7 +31,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 @@ -60,6 +66,14 @@ public function processNode(Node $node, Scope $scope): array
});

if ($error === null) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node->expr);
return [];
}

if ($scope->isInTrait()) {
// IssetCheck's message already distinguishes the possible outcomes,
// so the contexts only need to be told apart by error/no error.
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $node->expr, true, $error);
return [];
}

Expand Down
19 changes: 17 additions & 2 deletions src/Rules/Variables/IssetRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
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\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Rule;
use PHPStan\Type\Type;
Expand All @@ -16,7 +19,10 @@
final class IssetRule implements Rule
{

public function __construct(private IssetCheck $issetCheck)
public function __construct(
private IssetCheck $issetCheck,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
)
{
}

Expand All @@ -25,7 +31,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 @@ -42,8 +48,17 @@ public function processNode(Node $node, Scope $scope): array
return 'is not nullable';
});
if ($error === null) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $var);
continue;
}

if ($scope->isInTrait()) {
// IssetCheck's message already distinguishes the possible outcomes,
// so the contexts only need to be told apart by error/no error.
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $var, true, $error);
continue;
}

$messages[] = $error;
}

Expand Down
25 changes: 17 additions & 8 deletions src/Rules/Variables/NullCoalesceRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
namespace PHPStan\Rules\Variables;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Rule;
Expand All @@ -22,6 +25,7 @@ final class NullCoalesceRule implements Rule

public function __construct(
private IssetCheck $issetCheck,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
#[AutowiredParameter(ref: '%featureToggles.unnecessaryNullCoalesce%')]
private bool $unnecessaryNullCoalesce,
)
Expand All @@ -33,7 +37,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 @@ -60,17 +64,22 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$error = $this->issetCheck->check($left, $scope, sprintf('on left side of %s', $operator), 'nullCoalesce', $typeMessageCallback);
if ($error !== null) {
return [$error];
$error = $this->issetCheck->check($left, $scope, sprintf('on left side of %s', $operator), 'nullCoalesce', $typeMessageCallback)
?? $this->checkUnnecessaryNullCoalesce($left, $right, $operator, $scope);

if ($error === null) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $left);
return [];
}

$unnecessaryError = $this->checkUnnecessaryNullCoalesce($left, $right, $operator, $scope);
if ($unnecessaryError !== null) {
return [$unnecessaryError];
if ($scope->isInTrait()) {
// The error messages already distinguish the possible outcomes,
// so the contexts only need to be told apart by error/no error.
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $left, true, $error);
return [];
}

return [];
return [$error];
}

private function checkUnnecessaryNullCoalesce(Node\Expr $left, Node\Expr $right, string $operator, Scope $scope): ?IdentifierRuleError
Expand Down
44 changes: 37 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,19 @@ 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(),
true,
$this->treatPhpDocTypesAsCertain,
),
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
),
new ConstantConditionInTraitRule(),
]);
}

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

public function testInTrait(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/isset-in-trait.php'], [
[
'Property IssetInTrait\FirstNonNullableProperty::$k (int<1, max>) in empty() is not falsy.',
84,
],
[
'Property IssetInTrait\SecondNonNullableProperty::$k (int<1, max>) in empty() is not falsy.',
84,
],
[
'Variable $s in empty() always exists and is not falsy.',
121,
],
]);
}

}
51 changes: 44 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,19 @@ 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(),
true,
$this->treatPhpDocTypesAsCertain,
),
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
),
new ConstantConditionInTraitRule(),
]);
}

protected function shouldTreatPhpDocTypesAsCertain(): bool
Expand Down Expand Up @@ -612,4 +622,31 @@ public function testBug14610(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14610.php'], []);
}

public function testInTrait(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/isset-in-trait.php'], [
[
'Property IssetInTrait\FirstNonNullableProperty::$k (int<1, max>) in isset() is not nullable.',
83,
],
[
'Property IssetInTrait\SecondNonNullableProperty::$k (int<1, max>) in isset() is not nullable.',
83,
],
[
'Variable $s in isset() always exists and is not nullable.',
120,
],
]);
}

public function testBug14416(): void
{
$this->treatPhpDocTypesAsCertain = true;

$this->analyse([__DIR__ . '/data/bug-14416.php'], []);
}

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

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(),
), true);
// @phpstan-ignore argument.type
return new CompositeRule([
new NullCoalesceRule(
new IssetCheck(
new PropertyDescriptor(),
new PropertyReflectionFinder(),
true,
$this->shouldTreatPhpDocTypesAsCertain(),
),
self::getContainer()->getByType(ConstantConditionInTraitHelper::class),
true,
),
new ConstantConditionInTraitRule(),
]);
}

public function testCoalesceRule(): void
Expand Down Expand Up @@ -560,4 +571,22 @@ public function testBug14393(): void
]);
}

public function testInTrait(): void
{
$this->analyse([__DIR__ . '/data/isset-in-trait.php'], [
[
'Property IssetInTrait\FirstNonNullableProperty::$k (int<1, max>) on left side of ?? is not nullable.',
82,
],
[
'Property IssetInTrait\SecondNonNullableProperty::$k (int<1, max>) on left side of ?? is not nullable.',
82,
],
[
'Variable $s on left side of ?? always exists and is not nullable.',
119,
],
]);
}

}
Loading
Loading