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 src/Rules/Comparison/ConstantConditionRuleHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private function shouldSkip(Scope $scope, Expr $expr): bool
|| $expr instanceof Expr\StaticCall
) && !$expr->isFirstClassCallable()
) {
$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $expr);
$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $expr, false);
if ($isAlways !== null) {
return true;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array
}

$functionName = (string) $node->name;
$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node);
$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, true);
if ($isAlways === null) {
return [];
}
Expand All @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}

$isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node);
$isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node, true);
if ($isAlways !== null) {
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}
Expand Down
89 changes: 89 additions & 0 deletions src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public function __construct(
public function findSpecifiedType(
Scope $scope,
Expr $node,
bool $ignoreTraitContext,
): ?bool
{
if ($node instanceof FuncCall) {
Expand Down Expand Up @@ -198,6 +199,27 @@ public function findSpecifiedType(
}
} elseif ($functionName === 'method_exists' && $argsCount >= 2) {
$objectArg = $args[0]->value;

if (
$ignoreTraitContext
&& $this->isExpressionDependentOnTraitContext($scope, $objectArg)
) {
$traitReflection = $scope->getTraitReflection();
if ($traitReflection === null) {
return null;
}
$methodArgValue = $args[1]->value;
$methodArgType = $this->treatPhpDocTypesAsCertain ? $scope->getType($methodArgValue) : $scope->getNativeType($methodArgValue);
$constantMethodNames = $methodArgType->getConstantStrings();
if (count($constantMethodNames) === 0) {
return null;
}
foreach ($constantMethodNames as $constantMethodName) {
if (!$traitReflection->hasNativeMethod($constantMethodName->getValue())) {
return null;
}
}
}
$objectType = $this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg);

if ($objectType instanceof ConstantStringType
Expand Down Expand Up @@ -310,6 +332,14 @@ public function findSpecifiedType(
continue;
}

if (
$ignoreTraitContext
&& $this->isExpressionDependentOnTraitContext($scope, $sureType[0])
) {
$results[] = TrinaryLogic::createMaybe();
continue;
}

if ($this->treatPhpDocTypesAsCertain) {
$argumentType = $scope->getType($sureType[0]);
} else {
Expand All @@ -336,6 +366,14 @@ public function findSpecifiedType(
continue;
}

if (
$ignoreTraitContext
&& $this->isExpressionDependentOnTraitContext($scope, $sureNotType[0])
) {
$results[] = TrinaryLogic::createMaybe();
continue;
}

if ($this->treatPhpDocTypesAsCertain) {
$argumentType = $scope->getType($sureNotType[0]);
} else {
Expand All @@ -356,6 +394,57 @@ public function findSpecifiedType(
return $result->maybe() ? null : $result->yes();
}

private function isExpressionDependentOnTraitContext(Scope $scope, Expr $expr): bool
{
if (!$scope->isInTrait()) {
return false;
}

if (self::isExpressionDependentOnThis($expr)) {
return true;
}

$classReflection = $scope->getClassReflection();
if ($classReflection === null) {
return false;
}

$type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr);
foreach ($type->getObjectClassNames() as $className) {
if ($className === $classReflection->getName()) {
return true;
}
}

return false;
}

public static function isExpressionDependentOnThis(Expr $expr): bool
{
if ($expr instanceof Expr\Variable && $expr->name === 'this') {
return true;
}

if ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
return self::isExpressionDependentOnThis($expr->var);
}

if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) {
return self::isExpressionDependentOnThis($expr->var);
}

if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\StaticCall) {
if ($expr->class instanceof Expr) {
return self::isExpressionDependentOnThis($expr->class);
}

$className = $expr->class->toString();
return in_array($className, ['self', 'static', 'parent'], true);
}

return false;
}

private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool
{
if ($expr === $node) {
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node);
$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, true);
if ($isAlways === null) {
return [];
}
Expand All @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}

$isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node);
$isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node, true);
if ($isAlways !== null) {
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node);
$isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, true);
if ($isAlways === null) {
return [];
}
Expand All @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}

$isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node);
$isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node, true);
if ($isAlways !== null) {
return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public function getTypeFromFunctionCall(
$isAlways = $this->getHelper()->findSpecifiedType(
$scope,
$functionCall,
false,
);
if ($isAlways === null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ public function testBug12273(): void
]);
}

#[RequiresPhp('>= 8.1')]
public function testBug12798(): void
{
$this->analyse([__DIR__ . '/../Comparison/data/bug-12798.php'], []);
}

public function testBug12981(): void
{
$this->analyse([__DIR__ . '/data/bug-12981.php'], [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,10 @@ public function testBug6702(): void
$this->analyse([__DIR__ . '/data/bug-6702.php'], []);
}

public function testBug13023(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13023.php'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,6 @@ public function testImpossibleCheckTypeFunctionCall(): void
'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'method\' will always evaluate to true.',
650,
],
[
'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'someAnother\' will always evaluate to true.',
653,
],
[
'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.',
656,
],
[
'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.',
659,
Expand Down Expand Up @@ -1207,4 +1199,42 @@ public function testBug13799(): void
]);
}

public function testBug13023(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13023.php'], []);
}

#[RequiresPhp('>= 8.1')]
public function testBug7599(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-7599.php'], []);
}

public function testBug9095(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-9095.php'], []);
}

public function testBug13474(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13474.php'], []);
}

public function testBug13687(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-13687.php'], []);
}

#[RequiresPhp('>= 8.1')]
public function testBug12798(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-12798.php'], []);
}

}
50 changes: 50 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-12798.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types = 1); // lint >= 8.1

namespace Bug12798;

interface Colorable
{
public function color(): string;
}

trait HasColors
{
/** @return array<string|int, string> */
public static function colors(): array {
return array_reduce(self::cases(), function (array $colors, self $case) {
$key = is_subclass_of($case, \BackedEnum::class) ? $case->value : $case->name;
$color = is_subclass_of($case, Colorable::class) ? $case->color() : 'gray';

$colors[$key] = $color;
$colors[$color] = $color;
return $colors;
}, []);
}
}

enum AlertLevelBacked: int implements Colorable
{
use HasColors;

case Low = 1;
case Medium = 2;
case Critical = 3;

public function color(): string
{
return match ($this) {
self::Low => 'green',
self::Medium => 'yellow',
self::Critical => 'red',
};
}
}

enum AlertLevel
{
use HasColors;

case Low;
case Medium;
case Critical;
}
Loading
Loading