Skip to content
Merged
6 changes: 6 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,12 @@ jobs:
cd e2e/bug-11857
composer install
../../bin/phpstan
- script: |
cd e2e/in-trait
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw")
../bashunit -a contains 'FooTrait.php:10:Strict comparison using === between int<0, max> and false will always evaluate to false.' "$OUTPUT"
../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Bar):18:Strict comparison using === between E2EInTrait\Bar and null will always evaluate to false.' "$OUTPUT"
../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Foo):18:Strict comparison using === between E2EInTrait\Foo and null will always evaluate to false.' "$OUTPUT"
- script: |
cd e2e/result-cache-meta-extension
composer install
Expand Down
12 changes: 0 additions & 12 deletions build/baseline-8.0.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,12 @@ parameters:
count: 1
path: ../src/Type/ClosureTypeFactory.php

-
message: '#^Strict comparison using \=\=\= between list\<non\-falsy\-string\> and false will always evaluate to false\.$#'
identifier: identical.alwaysFalse
count: 1
path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php

-
message: '#^Strict comparison using \=\=\= between int\<0, max\> and false will always evaluate to false\.$#'
identifier: identical.alwaysFalse
count: 1
path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php

-
message: '#^Strict comparison using \=\=\= between list\<non\-falsy\-string\> and false will always evaluate to false\.$#'
identifier: identical.alwaysFalse
count: 1
path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php

-
message: '#^Strict comparison using \=\=\= between list\<non\-falsy\-string\> and false will always evaluate to false\.$#'
identifier: identical.alwaysFalse
Expand Down
4 changes: 4 additions & 0 deletions e2e/in-trait/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
parameters:
level: 8
paths:
- src
20 changes: 20 additions & 0 deletions e2e/in-trait/src/Bar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace E2EInTrait;

class Bar
{

use FooTrait;

public function getSth(): ?self
{
return rand(0, 1) ? $this : null;
}

public function getSth2(): self
{
return $this;
}

}
20 changes: 20 additions & 0 deletions e2e/in-trait/src/Foo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace E2EInTrait;

class Foo
{

use FooTrait;

public function getSth(): self
{
return $this;
}

public function getSth2(): self
{
return $this;
}

}
23 changes: 23 additions & 0 deletions e2e/in-trait/src/FooTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace E2EInTrait;

trait FooTrait
{

public function doFoo(int $i): void
{
if (abs($i) === false) {

}

if ($this->getSth() === null) {

}

if ($this->getSth2() === null) {

}
}

}
22 changes: 22 additions & 0 deletions src/Analyser/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,28 @@ public function changeTraitFilePath(string $newFilePath): self
);
}

public function removeTraitContext(): self
{
if ($this->traitFilePath === null) {
throw new ShouldNotHappenException();
}

return new self(
$this->message,
$this->traitFilePath,
$this->line,
$this->canBeIgnored,
$this->filePath,
null,
$this->tip,
$this->nodeLine,
$this->nodeType,
$this->identifier,
$this->metadata,
$this->fixedErrorDiff,
);
}

public function getTraitFilePath(): ?string
{
return $this->traitFilePath;
Expand Down
5 changes: 5 additions & 0 deletions src/Analyser/RuleErrorTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PHPStan\Rules\MetadataRuleError;
use PHPStan\Rules\NonIgnorableRuleError;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrors\TransformedRuleError;
use PHPStan\Rules\TipRuleError;
use PHPStan\ShouldNotHappenException;
use SebastianBergmann\Diff\Differ;
Expand Down Expand Up @@ -55,6 +56,10 @@ public function transform(
Node $node,
): Error
{
if ($ruleError instanceof TransformedRuleError) {
return $ruleError->getError();
}

$line = $node->getStartLine();
$canBeIgnored = true;
$fileName = $scope->getFileDescription();
Expand Down
46 changes: 33 additions & 13 deletions src/Rules/Classes/ImpossibleInstanceOfRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@
namespace PHPStan\Rules\Classes;

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\Parser\LastConditionVisitor;
use PHPStan\Rules\Comparison\ConstantConditionInTraitHelper;
use PHPStan\Rules\Comparison\PossiblyImpureTipHelper;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
Expand All @@ -29,6 +33,8 @@ final class ImpossibleInstanceOfRule implements Rule

public function __construct(
private RuleLevelHelper $ruleLevelHelper,
private PossiblyImpureTipHelper $possiblyImpureTipHelper,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
#[AutowiredParameter]
private bool $treatPhpDocTypesAsCertain,
#[AutowiredParameter]
Expand All @@ -44,7 +50,7 @@ public function getNodeType(): string
return Node\Expr\Instanceof_::class;
}

public function processNode(Node $node, Scope $scope): array
public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataEmitter $scope): array
{
if ($node->class instanceof Node\Name) {
$className = $scope->resolveName($node->class);
Expand Down Expand Up @@ -74,40 +80,48 @@ public function processNode(Node $node, Scope $scope): array

$instanceofType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node) : $scope->getNativeType($node);
if (!$instanceofType instanceof ConstantBooleanType) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node);
return [];
}

$addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $node): RuleErrorBuilder {
if (!$this->treatPhpDocTypesAsCertain) {
return $ruleErrorBuilder;
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}

$instanceofTypeWithoutPhpDocs = $scope->getNativeType($node);
if ($instanceofTypeWithoutPhpDocs instanceof ConstantBooleanType) {
return $ruleErrorBuilder;
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}

if (!$this->treatPhpDocTypesAsCertainTip) {
return $ruleErrorBuilder;
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}

return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip();
$ruleErrorBuilder = $ruleErrorBuilder->treatPhpDocTypesAsCertainTip();

return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
};

if (!$instanceofType->getValue()) {
$exprType = $this->treatPhpDocTypesAsCertain ? $scope->getType($node->expr) : $scope->getNativeType($node->expr);

return [
$addTip(RuleErrorBuilder::message(sprintf(
'Instanceof between %s and %s will always evaluate to false.',
$exprType->describe(VerbosityLevel::typeOnly()),
$classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)),
)))->identifier('instanceof.alwaysFalse')->build(),
];
$ruleError = $addTip(RuleErrorBuilder::message(sprintf(
'Instanceof between %s and %s will always evaluate to false.',
$exprType->describe(VerbosityLevel::typeOnly()),
$classType->describe(VerbosityLevel::getRecommendedLevelByType($classType)),
)))->identifier('instanceof.alwaysFalse')->build();
if ($scope->isInTrait()) {
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, false, $ruleError);
return [];
}

return [$ruleError];
}

$isLast = $node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME);
if ($isLast === true && !$this->reportAlwaysTrueInLastCondition) {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $node);
return [];
}

Expand All @@ -123,7 +137,13 @@ public function processNode(Node $node, Scope $scope): array

$errorBuilder->identifier('instanceof.alwaysTrue');

return [$errorBuilder->build()];
$ruleError = $errorBuilder->build();
if ($scope->isInTrait()) {
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $node, true, $ruleError);
return [];
}

return [$ruleError];
}

}
44 changes: 39 additions & 5 deletions src/Rules/Comparison/BooleanAndConstantConditionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PHPStan\Rules\Comparison;

use PhpParser\Node;
use PHPStan\Analyser\CollectedDataEmitter;
use PHPStan\Analyser\NodeCallbackInvoker;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\RegisteredRule;
Expand All @@ -24,6 +26,7 @@ final class BooleanAndConstantConditionRule implements Rule
public function __construct(
private ConstantConditionRuleHelper $helper,
private PossiblyImpureTipHelper $possiblyImpureTipHelper,
private ConstantConditionInTraitHelper $constantConditionInTraitHelper,
#[AutowiredParameter]
private bool $treatPhpDocTypesAsCertain,
#[AutowiredParameter]
Expand All @@ -41,14 +44,16 @@ public function getNodeType(): string

public function processNode(
Node $node,
Scope $scope,
Scope&NodeCallbackInvoker&CollectedDataEmitter $scope,
): array
{
$errors = [];
$originalNode = $node->getOriginalNode();
$nodeText = $originalNode->getOperatorSigil();
$leftType = $this->helper->getBooleanType($scope, $originalNode->left);
$identifierType = $originalNode instanceof Node\Expr\BinaryOp\BooleanAnd ? 'booleanAnd' : 'logicalAnd';
$isInTrait = $scope->isInTrait();
$hasLeftOrRightError = false;
if ($leftType instanceof ConstantBooleanType) {
$addTipLeft = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder {
if (!$this->treatPhpDocTypesAsCertain) {
Expand Down Expand Up @@ -80,8 +85,18 @@ public function processNode(
if ($leftType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) {
$errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.');
}
$errors[] = $errorBuilder->build();
$ruleError = $errorBuilder->build();
$hasLeftOrRightError = true;
if ($isInTrait) {
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->left, $leftType->getValue(), $ruleError);
} else {
$errors[] = $ruleError;
}
} else {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left);
}
} else {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->left);
}

$rightScope = $node->getRightScope();
Expand Down Expand Up @@ -123,11 +138,21 @@ public function processNode(
if ($rightType->getValue() && $isLast === false && !$this->reportAlwaysTrueInLastCondition) {
$errorBuilder->tip('Remove remaining cases below this one and this error will disappear too.');
}
$errors[] = $errorBuilder->build();
$ruleError = $errorBuilder->build();
$hasLeftOrRightError = true;
if ($isInTrait) {
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode->right, $rightType->getValue(), $ruleError);
} else {
$errors[] = $ruleError;
}
} else {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right);
}
} else {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode->right);
}

if (count($errors) === 0 && !$scope->isInFirstLevelStatement()) {
if (count($errors) === 0 && !$hasLeftOrRightError && !$scope->isInFirstLevelStatement()) {
$nodeType = $this->treatPhpDocTypesAsCertain ? $scope->getType($originalNode) : $scope->getNativeType($originalNode);
if ($nodeType instanceof ConstantBooleanType) {
$addTip = function (RuleErrorBuilder $ruleErrorBuilder) use ($scope, $originalNode): RuleErrorBuilder {
Expand Down Expand Up @@ -161,8 +186,17 @@ public function processNode(

$errorBuilder->identifier(sprintf('%s.always%s', $identifierType, $nodeType->getValue() ? 'True' : 'False'));

$errors[] = $errorBuilder->build();
$ruleError = $errorBuilder->build();
if ($isInTrait) {
$this->constantConditionInTraitHelper->emitError(self::class, $scope, $originalNode, $nodeType->getValue(), $ruleError);
} else {
$errors[] = $ruleError;
}
} else {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode);
}
} else {
$this->constantConditionInTraitHelper->emitNoError(self::class, $scope, $originalNode);
}
}

Expand Down
Loading
Loading