diff --git a/build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php b/build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php new file mode 100644 index 0000000000..01762e05ca --- /dev/null +++ b/build/PHPStan/Build/OrChainIdenticalComparisonToInArrayRule.php @@ -0,0 +1,162 @@ + + */ +final class OrChainIdenticalComparisonToInArrayRule implements Rule +{ + + public function __construct( + private ExprPrinter $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 + */ + 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->printExpr($subjectNode); + $values = [$subjectAndValue['value']]; + + foreach ($comparisons as $comparison) { + if (!$comparison instanceof Identical) { + return []; + } + + $currentSubjectAndValue = $this->getSubjectAndValue($comparison); + if ($currentSubjectAndValue === null) { + return []; + } + + if ($this->printer->printExpr($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 + */ + 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 $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'))), + ]); + } + +} diff --git a/build/phpstan.neon b/build/phpstan.neon index 8d0654a833..1e79bf303a 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -131,6 +131,7 @@ rules: - PHPStan\Build\OverrideAttributeThirdPartyMethodRule - PHPStan\Build\SkipTestsWithRequiresPhpAttributeRule - PHPStan\Build\MemoizationPropertyRule + - PHPStan\Build\OrChainIdenticalComparisonToInArrayRule services: - diff --git a/src/Fixable/PhpPrinterIndentationDetectorVisitor.php b/src/Fixable/PhpPrinterIndentationDetectorVisitor.php index d7b09945f9..af696b5b95 100644 --- a/src/Fixable/PhpPrinterIndentationDetectorVisitor.php +++ b/src/Fixable/PhpPrinterIndentationDetectorVisitor.php @@ -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; @@ -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; } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 48336cb01c..af6302edfc 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -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(); } } @@ -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(); } } diff --git a/src/Rules/Keywords/ContinueBreakInLoopRule.php b/src/Rules/Keywords/ContinueBreakInLoopRule.php index bff732b9f3..e07d63a5b7 100644 --- a/src/Rules/Keywords/ContinueBreakInLoopRule.php +++ b/src/Rules/Keywords/ContinueBreakInLoopRule.php @@ -10,6 +10,7 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use function array_reverse; +use function in_array; use function sprintf; /** @@ -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) { diff --git a/src/Rules/UnusedFunctionParametersCheck.php b/src/Rules/UnusedFunctionParametersCheck.php index 64b9cf9621..10ec0753e1 100644 --- a/src/Rules/UnusedFunctionParametersCheck.php +++ b/src/Rules/UnusedFunctionParametersCheck.php @@ -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; @@ -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(); } } diff --git a/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php index 8d2ceddc98..8ade76245a 100644 --- a/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php +++ b/src/Type/Php/DsMapDynamicMethodThrowTypeExtension.php @@ -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 @@ -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 diff --git a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php index c6517e866d..a53b2c5c7d 100644 --- a/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/StrWordCountFunctionDynamicReturnTypeExtension.php @@ -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 @@ -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()); } diff --git a/src/Type/Regex/RegexGroupParser.php b/src/Type/Regex/RegexGroupParser.php index 80151501be..3162a1c783 100644 --- a/src/Type/Regex/RegexGroupParser.php +++ b/src/Type/Regex/RegexGroupParser.php @@ -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 ''; } diff --git a/tests/PHPStan/Build/OrChainIdenticalComparisonToInArrayRuleTest.php b/tests/PHPStan/Build/OrChainIdenticalComparisonToInArrayRuleTest.php new file mode 100644 index 0000000000..06caa79fa4 --- /dev/null +++ b/tests/PHPStan/Build/OrChainIdenticalComparisonToInArrayRuleTest.php @@ -0,0 +1,49 @@ + + */ +final class OrChainIdenticalComparisonToInArrayRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new OrChainIdenticalComparisonToInArrayRule(new ExprPrinter(new Printer()), 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'); + } + +} diff --git a/tests/PHPStan/Build/data/or-chain-identical-comparison.php b/tests/PHPStan/Build/data/or-chain-identical-comparison.php new file mode 100644 index 0000000000..ddce971508 --- /dev/null +++ b/tests/PHPStan/Build/data/or-chain-identical-comparison.php @@ -0,0 +1,31 @@ +