Skip to content

Simplify by replacing chained $var === const with in_array() #4098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: 2.1.x
Choose a base branch
from
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
163 changes: 163 additions & 0 deletions build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php declare(strict_types = 1);

namespace PHPStan\Build;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\ArrayItem;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\BinaryOp\BooleanOr;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt\If_;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\File\FileHelper;
use PHPStan\Fixable\PhpPrinter;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_map;
use function array_merge;
use function array_shift;
use function count;
use function dirname;
use function str_starts_with;

/**
* @implements Rule<If_>
*/
final class OrChainIdenticalComparisonToInArrayRule implements Rule
{

public function __construct(
#[AutowiredParameter]
private PhpPrinter $printer,
private FileHelper $fileHelper,
private bool $skipTests = true,
)
{
}

public function getNodeType(): string
{
return If_::class;
}

public function processNode(Node $node, Scope $scope): array
{
$errors = $this->processConditionNode($node->cond, $scope);
foreach ($node->elseifs as $elseifCondNode) {
$errors = array_merge($errors, $this->processConditionNode($elseifCondNode->cond, $scope));
}

return $errors;
}

/**
* @return list<IdentifierRuleError>
*/
public function processConditionNode(Expr $condNode, Scope $scope): array
{
$comparisons = $this->unpackOrChain($condNode);
if (count($comparisons) < 2) {
return [];
}

$firstComparison = array_shift($comparisons);
if (!$firstComparison instanceof Identical) {
return [];
}

$subjectAndValue = $this->getSubjectAndValue($firstComparison);
if ($subjectAndValue === null) {
return [];
}

if ($this->skipTests && str_starts_with($this->fileHelper->normalizePath($scope->getFile()), $this->fileHelper->normalizePath(dirname(__DIR__, 3) . '/tests'))) {
return [];
}

$subjectNode = $subjectAndValue['subject'];
$subjectStr = $this->printer->prettyPrintExpr($subjectNode);
$values = [$subjectAndValue['value']];

foreach ($comparisons as $comparison) {
if (!$comparison instanceof Identical) {
return [];
}

$currentSubjectAndValue = $this->getSubjectAndValue($comparison);
if ($currentSubjectAndValue === null) {
return [];
}

if ($this->printer->prettyPrintExpr($currentSubjectAndValue['subject']) !== $subjectStr) {
return [];
}

$values[] = $currentSubjectAndValue['value'];
}

$errorBuilder = RuleErrorBuilder::message('This chain of identical comparisons can be simplified using in_array().')
->line($condNode->getStartLine())
->fixNode($condNode, static fn (Expr $node) => self::createInArrayCall($subjectNode, $values))
->identifier('or.chainIdenticalComparison');

return [$errorBuilder->build()];
}

/**
* @return list<Expr>
*/
private function unpackOrChain(Expr $node): array
{
if ($node instanceof BooleanOr) {
return [...$this->unpackOrChain($node->left), ...$this->unpackOrChain($node->right)];
}

return [$node];
}

/**
* @phpstan-assert-if-true Scalar|ClassConstFetch|ConstFetch $node
*/
private static function isSubjectNode(Expr $node): bool
{
return $node instanceof Scalar || $node instanceof ClassConstFetch || $node instanceof ConstFetch;
}

/**
* @return array{subject: Expr, value: Scalar|ClassConstFetch|ConstFetch}|null
*/
private function getSubjectAndValue(Identical $comparison): ?array
{
if (self::isSubjectNode($comparison->left) && !self::isSubjectNode($comparison->left)) {
return ['subject' => $comparison->right, 'value' => $comparison->left];
}

if (!self::isSubjectNode($comparison->left) && self::isSubjectNode($comparison->right)) {
return ['subject' => $comparison->left, 'value' => $comparison->right];
}

return null;
}

/**
* @param list<Scalar|ClassConstFetch|ConstFetch> $values
*/
private static function createInArrayCall(Expr $subjectNode, array $values): FuncCall
{
return new FuncCall(new Name('\in_array'), [
new Arg($subjectNode),
new Arg(new Array_(array_map(static fn ($value)=> new ArrayItem($value), $values))),
new Arg(new ConstFetch(new Name('true'))),
]);
}

}
1 change: 1 addition & 0 deletions build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ rules:
- PHPStan\Build\OverrideAttributeThirdPartyMethodRule
- PHPStan\Build\SkipTestsWithRequiresPhpAttributeRule
- PHPStan\Build\MemoizationPropertyRule
- PHPStan\Build\OrChainIdenticalComparisonToInArrayRule

services:
-
Expand Down
2 changes: 2 additions & 0 deletions src/Fixable/PhpPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use Override;
use PhpParser\Node;
use PhpParser\PrettyPrinter\Standard;
use PHPStan\DependencyInjection\AutowiredService;
use function count;
use function rtrim;

#[AutowiredService]
final class PhpPrinter extends Standard
{

Expand Down
3 changes: 2 additions & 1 deletion src/Fixable/PhpPrinterIndentationDetectorVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpParser\NodeVisitor;
use PhpParser\NodeVisitorAbstract;
use function count;
use function in_array;
use function is_array;
use function preg_match;
use function preg_match_all;
Expand Down Expand Up @@ -47,7 +48,7 @@ public function enterNode(Node $node): ?int
$text = $this->origTokens->getTokenCode($node->getStartTokenPos(), $firstStmt->getStartTokenPos(), 0);

$c = preg_match_all('~\n([\\x09\\x20]*)~', $text, $matches, PREG_SET_ORDER);
if ($c === 0 || $c === false) {
if (in_array($c, [0, false], true)) {
return null;
}

Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -864,7 +864,7 @@ public function getDivType(Expr $left, Expr $right, callable $getTypeCallback):

$rightScalarValues = $rightType->toNumber()->getConstantScalarValues();
foreach ($rightScalarValues as $scalarValue) {
if ($scalarValue === 0 || $scalarValue === 0.0) {
if (in_array($scalarValue, [0, 0.0], true)) {
return new ErrorType();
}
}
Expand Down Expand Up @@ -938,7 +938,7 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback):
$rightScalarValues = $rightType->toNumber()->getConstantScalarValues();
foreach ($rightScalarValues as $scalarValue) {

if ($scalarValue === 0 || $scalarValue === 0.0) {
if (in_array($scalarValue, [0, 0.0], true)) {
return new ErrorType();
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/Rules/Keywords/ContinueBreakInLoopRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function array_reverse;
use function in_array;
use function sprintf;

/**
Expand Down Expand Up @@ -52,13 +53,13 @@ public function processNode(Node $node, Scope $scope): array
->build(),
];
}
if (
$parentStmtType === Stmt\For_::class
|| $parentStmtType === Stmt\Foreach_::class
|| $parentStmtType === Stmt\Do_::class
|| $parentStmtType === Stmt\While_::class
|| $parentStmtType === Stmt\Switch_::class
) {
if (in_array($parentStmtType, [
Stmt\For_::class,
Stmt\Foreach_::class,
Stmt\Do_::class,
Stmt\While_::class,
Stmt\Switch_::class,
], true)) {
$value--;
}
if ($value === 0) {
Expand Down
3 changes: 2 additions & 1 deletion src/Rules/UnusedFunctionParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use function array_combine;
use function array_map;
use function array_merge;
use function in_array;
use function is_array;
use function is_string;
use function sprintf;
Expand Down Expand Up @@ -78,7 +79,7 @@ private function getUsedVariables(Scope $scope, $node): array
if ($node instanceof Node) {
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
$functionName = $this->reflectionProvider->resolveFunctionName($node->name, $scope);
if ($functionName === 'func_get_args' || $functionName === 'get_defined_vars') {
if (in_array($functionName, ['func_get_args', 'get_defined_vars'], true)) {
return $scope->getDefinedVariables();
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\VoidType;
use function count;
use function in_array;

#[AutowiredService]
final class DsMapDynamicMethodThrowTypeExtension implements DynamicMethodThrowTypeExtension
Expand All @@ -18,7 +19,7 @@ final class DsMapDynamicMethodThrowTypeExtension implements DynamicMethodThrowTy
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getDeclaringClass()->getName() === 'Ds\Map'
&& ($methodReflection->getName() === 'get' || $methodReflection->getName() === 'remove');
&& in_array($methodReflection->getName(), ['get', 'remove'], true);
}

public function getThrowTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use function count;
use function in_array;

#[AutowiredService]
final class StrWordCountFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
Expand All @@ -35,14 +36,14 @@ public function getTypeFromFunctionCall(
$argsCount = count($functionCall->getArgs());
if ($argsCount === 1) {
return new IntegerType();
} elseif ($argsCount === 2 || $argsCount === 3) {
} elseif (in_array($argsCount, [2, 3], true)) {
$formatType = $scope->getType($functionCall->getArgs()[1]->value);
if ($formatType instanceof ConstantIntegerType) {
$val = $formatType->getValue();
if ($val === 0) {
// return word count
return new IntegerType();
} elseif ($val === 1 || $val === 2) {
} elseif (in_array($val, [1, 2], true)) {
// return [word] or [offset => word]
return new ArrayType(new IntegerType(), new StringType());
}
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Regex/RegexGroupParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $ap
}
}

if ($token === 'anchor' || $token === 'match_point_reset') {
if (in_array($token, ['anchor', 'match_point_reset'], true)) {
return '';
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace PHPStan\Build;

use PHPStan\File\FileHelper;
use PHPStan\Fixable\PhpPrinter;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<OrChainIdenticalComparisonToInArrayRule>
*/
final class OrChainIdenticalComparisonToInArrayRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new OrChainIdenticalComparisonToInArrayRule(new PhpPrinter(), self::getContainer()->getByType(FileHelper::class), false);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/or-chain-identical-comparison.php'], [
[
'This chain of identical comparisons can be simplified using in_array().',
7,
],
[
'This chain of identical comparisons can be simplified using in_array().',
11,
],
[
'This chain of identical comparisons can be simplified using in_array().',
15,
],
[
'This chain of identical comparisons can be simplified using in_array().',
17,
],
]);
}

public function testFix(): void
{
$this->fix(__DIR__ . '/data/or-chain-identical-comparison.php', __DIR__ . '/data/or-chain-identical-comparison.php.fixed');
}

}
31 changes: 31 additions & 0 deletions tests/PHPStan/Build/data/or-chain-identical-comparison.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

if ($var === 'foo') {
echo 'ok';
}

if ($var === 'foo' || $var === 'bar') {
echo 'ok';
}

if ($var === 'foo' || $var === 'bar' || $var === 'buz') {
echo 'ok';
}

if ($var === 'foo' || $var === 'bar' || $var === 'buz') {
echo 'ok';
} elseif ($var === 'foofoo' || $var === 'barbar' || $var === 'buzbuz') {
echo 'ok';
}

if ($var === 'foo' || $var === 'bar' || $var2 === 'buz') {
echo 'no';
}

if ($var === 'foo' || $var2 === 'bar' || $var === 'buz') {
echo 'no';
}

if ($var === 'foo' || $var2 === 'bar' || $var2 === 'buz') {
echo 'no';
}
Loading
Loading