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
5 changes: 5 additions & 0 deletions src/Analyser/ExpressionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public function getBeforeScope(): MutatingScope
return $this->beforeScope;
}

public function getExpr(): Expr
{
return $this->expr;
}

public function hasYield(): bool
{
return $this->hasYield;
Expand Down
21 changes: 18 additions & 3 deletions src/Rules/Variables/EmptyRule.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\RegisteredRule;
use PHPStan\Node\EmptyExpressionNode;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Rule;
use PHPStan\Type\Type;
Expand All @@ -17,7 +20,10 @@
final class EmptyRule implements Rule
{

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

Expand All @@ -26,9 +32,10 @@ public function getNodeType(): string
return EmptyExpressionNode::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$error = $this->issetCheck->check($node->getExprResult(), $scope, 'in empty()', 'empty', static function (Type $type): ?string {
$exprResult = $node->getExprResult();
$error = $this->issetCheck->check($exprResult, $scope, 'in empty()', 'empty', static function (Type $type): ?string {
$isNull = $type->isNull();
if ($isNull->maybe()) {
return null;
Expand Down Expand Up @@ -61,6 +68,14 @@ public function processNode(Node $node, Scope $scope): array
});

if ($error === null) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $exprResult->getExpr());
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, $exprResult->getExpr(), 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,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\RegisteredRule;
use PHPStan\Node\IssetExpressionNode;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Rule;
use PHPStan\Type\Type;
Expand All @@ -17,7 +20,10 @@
final class IssetRule implements Rule
{

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

Expand All @@ -26,7 +32,7 @@ public function getNodeType(): string
return IssetExpressionNode::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$messages = [];
foreach ($node->getVarResults() as $varResult) {
Expand All @@ -43,8 +49,17 @@ public function processNode(Node $node, Scope $scope): array
return 'is not nullable';
});
if ($error === null) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $varResult->getExpr());
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, $varResult->getExpr(), true, $error);
continue;
}

$messages[] = $error;
}

Expand Down
26 changes: 17 additions & 9 deletions src/Rules/Variables/NullCoalesceRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
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\Node\CoalesceExpressionNode;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\IssetCheck;
use PHPStan\Rules\Rule;
Expand All @@ -23,6 +26,7 @@ final class NullCoalesceRule implements Rule

public function __construct(
private IssetCheck $issetCheck,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
#[AutowiredParameter(ref: '%featureToggles.unnecessaryNullCoalesce%')]
private bool $unnecessaryNullCoalesce,
)
Expand All @@ -34,10 +38,11 @@ public function getNodeType(): string
return CoalesceExpressionNode::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
$subjectResult = $node->getSubjectResult();
$error = $this->issetCheck->check(
$node->getSubjectResult(),
$subjectResult,
$scope,
$node->getOperatorDescription(),
'nullCoalesce',
Expand All @@ -53,18 +58,21 @@ static function (Type $type): ?string {

return 'is not nullable';
},
);
) ?? $this->checkUnnecessaryNullCoalesce($node, $scope);

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

$unnecessaryError = $this->checkUnnecessaryNullCoalesce($node, $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, $subjectResult->getExpr(), true, $error);
return [];
}

return [];
return [$error];
}

private function checkUnnecessaryNullCoalesce(CoalesceExpressionNode $node, Scope $scope): ?IdentifierRuleError
Expand Down
42 changes: 36 additions & 6 deletions tests/PHPStan/Rules/Variables/EmptyRuleTest.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\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 @@ -19,11 +22,18 @@ class EmptyRuleTest extends RuleTestCase

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

protected function shouldTreatPhpDocTypesAsCertain(): bool
Expand Down Expand Up @@ -257,4 +267,24 @@ public function testNullCoalesceAssignRightSideScope(): void
$this->analyse([__DIR__ . '/data/null-coalesce-assign-right-side-scope.php'], []);
}

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,
],
]);
}

}
49 changes: 43 additions & 6 deletions tests/PHPStan/Rules/Variables/IssetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

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\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 @@ -18,11 +21,18 @@ class IssetRuleTest extends RuleTestCase

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

protected function shouldTreatPhpDocTypesAsCertain(): bool
Expand Down Expand Up @@ -617,4 +627,31 @@ public function testNullCoalesceAssignRightSideScope(): void
$this->analyse([__DIR__ . '/data/null-coalesce-assign-right-side-scope.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'], []);
}

}
41 changes: 35 additions & 6 deletions tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,37 @@

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

public function testCoalesceRule(): void
Expand Down Expand Up @@ -611,4 +622,22 @@ public function testBug15046(): 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