From b10e397e6fab1281b539acc4f54ebad1c2788493 Mon Sep 17 00:00:00 2001 From: Anasatasiia Sh Date: Mon, 14 Apr 2025 18:13:45 +0300 Subject: [PATCH 1/2] add rule about strict mode for in_array function --- config/phpstan.services.neon | 4 + .../Sniffs/Formatting/ReadonlySniff.php | 9 +- .../WhiteSpaceAroundClassPropertySniff.php | 2 +- .../WhiteSpaceBeforeChainCallSniff.php | 13 +- src/PhpStan/FunctionStrictModeRule.php | 127 ++++++++++++++++++ tests/PhpStan/ForbiddenNodeTypeRuleTest.php | 13 +- tests/PhpStan/FunctionStrictModeRuleTest.php | 33 +++++ .../PhpStan/MethodCallConsistencyRuleTest.php | 5 +- .../Resources/function-strict-mode.php | 34 +++++ 9 files changed, 213 insertions(+), 27 deletions(-) create mode 100644 src/PhpStan/FunctionStrictModeRule.php create mode 100644 tests/PhpStan/FunctionStrictModeRuleTest.php create mode 100644 tests/PhpStan/Resources/function-strict-mode.php diff --git a/config/phpstan.services.neon b/config/phpstan.services.neon index af41192..26f69aa 100644 --- a/config/phpstan.services.neon +++ b/config/phpstan.services.neon @@ -28,3 +28,7 @@ services: class: \FiveLab\Component\CiRules\PhpStan\MethodCallConsistencyRule arguments: [ @reflectionProvider ] tags: [ phpstan.rules.rule ] + + - + class: \FiveLab\Component\CiRules\PhpStan\FunctionStrictModeRule + tags: [ phpstan.rules.rule ] diff --git a/src/PhpCs/FiveLab/Sniffs/Formatting/ReadonlySniff.php b/src/PhpCs/FiveLab/Sniffs/Formatting/ReadonlySniff.php index 1747ce4..8c45cb1 100644 --- a/src/PhpCs/FiveLab/Sniffs/Formatting/ReadonlySniff.php +++ b/src/PhpCs/FiveLab/Sniffs/Formatting/ReadonlySniff.php @@ -17,9 +17,6 @@ use PHP_CodeSniffer\Files\File; use PHP_CodeSniffer\Sniffs\Sniff; -/** - * Check the readonly. - */ class ReadonlySniff implements Sniff { const SCOPES = [ @@ -43,10 +40,10 @@ public function process(File $phpcsFile, mixed $stackPtr): void return; } - $prev = $phpcsFile->getTokens()[$stackPtr-2]; - $next = $phpcsFile->getTokens()[$stackPtr+2]; + $prev = $phpcsFile->getTokens()[$stackPtr - 2]; + $next = $phpcsFile->getTokens()[$stackPtr + 2]; - if (!\in_array($prev['code'], self::SCOPES) && \in_array($next['code'], self::SCOPES)) { + if (!\in_array($prev['code'], self::SCOPES, true) && \in_array($next['code'], self::SCOPES, true)) { $phpcsFile->addError( 'Scope should be declared before readonly keyword.', $stackPtr, diff --git a/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceAroundClassPropertySniff.php b/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceAroundClassPropertySniff.php index 6e9e84c..0f9177b 100644 --- a/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceAroundClassPropertySniff.php +++ b/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceAroundClassPropertySniff.php @@ -52,7 +52,7 @@ public function process(File $phpcsFile, mixed $stackPtr): void $tokensOnLine = $this->getTokensOnLineNoWhiteSpaces($phpcsFile, $currentToken['line'] - 2); $tokensOnLine = \array_filter($tokensOnLine, static function (array $token): bool { - return \in_array($token['code'], [T_CONST, T_USE]); + return \in_array($token['code'], [T_CONST, T_USE], true); }); if ($this->previousIsMultiLineVariable($phpcsFile, $stackPtr)) { diff --git a/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php b/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php index 1a72d75..19a19ca 100644 --- a/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php +++ b/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php @@ -1,7 +1,5 @@ + * @return array */ private function getNonEmptyTokensOnLine(File $phpcsFile, int $line): array { $tokens = PhpCsUtils::getTokensOnLine($phpcsFile, $line); $tokens = \array_filter($tokens, static function (array $token): bool { - return !\in_array($token['code'], Tokens::$emptyTokens); + return !\in_array($token['code'], Tokens::$emptyTokens, true); }); return \array_values($tokens); diff --git a/src/PhpStan/FunctionStrictModeRule.php b/src/PhpStan/FunctionStrictModeRule.php new file mode 100644 index 0000000..40c5fd7 --- /dev/null +++ b/src/PhpStan/FunctionStrictModeRule.php @@ -0,0 +1,127 @@ + + */ +readonly class FunctionStrictModeRule implements Rule +{ + public function getNodeType(): string + { + return Node\Expr\FuncCall::class; + } + + /** + * {@inheritdoc} + * + * @param Node\Expr\FuncCall $node + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Name) { + return []; + } + + $funcName = $node->name->toString(); + + if ('in_array' !== $funcName) { + return []; + } + + if (3 === \count($node->args)) { + if (!$node->args[2] instanceof Node\Arg) { + return []; + } + + $modeType = $scope->getType($node->args[2]->value); + + if ($modeType->isFalse()->yes()) { + return [ + RuleErrorBuilder::message('The function in_array must be used in strict mode.') + ->identifier('functionCall.strictMode') + ->build(), + ]; + } + + return []; + } + + return $this->analyzeArgs($node, $scope); + } + + /** + * Analyze function arguments types + * + * @param Node\Expr\FuncCall $node + * @param Scope $scope + * + * @return list + */ + private function analyzeArgs(Node\Expr\FuncCall $node, Scope $scope): array + { + if (!$node->args[0] instanceof Node\Arg || !$node->args[1] instanceof Node\Arg) { + return []; + } + + $needleType = $scope->getType($node->args[0]->value); + $isNeedleScalarType = $this->isSafeToCompareNonStrict($needleType); + + if (!$isNeedleScalarType) { + return []; + } + + return [ + RuleErrorBuilder::message('The function in_array must be used in strict mode.') + ->identifier('functionCall.strictMode') + ->build(), + ]; + + } + + private function isSafeToCompareNonStrict(Type $argType): bool + { + if ($argType->isObject()->yes()) { + return false; + } + + if ($argType->isArray()->yes()) { + $valueType = $argType->getIterableValueType(); + + if ($valueType->isObject()->yes()) { + return false; + } + + if ($valueType instanceof UnionType) { + $types = $valueType->getTypes(); + + foreach ($types as $type) { + if ($type->isScalar()->no()) { + return false; + } + } + } + } + + return true; + } +} diff --git a/tests/PhpStan/ForbiddenNodeTypeRuleTest.php b/tests/PhpStan/ForbiddenNodeTypeRuleTest.php index aaf0492..ee58882 100644 --- a/tests/PhpStan/ForbiddenNodeTypeRuleTest.php +++ b/tests/PhpStan/ForbiddenNodeTypeRuleTest.php @@ -18,6 +18,7 @@ use PhpParser\Node\Expr\Isset_; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\Test; class ForbiddenNodeTypeRuleTest extends RuleTestCase { @@ -28,9 +29,7 @@ protected function getRule(): Rule return $this->rule; } - /** - * @test - */ + #[Test] public function shouldSuccessProcessForIsset(): void { $this->rule = new ForbiddenNodeTypeRule(Isset_::class, 'Language construct isset() should not be used.'); @@ -43,9 +42,7 @@ public function shouldSuccessProcessForIsset(): void ); } - /** - * @test - */ + #[Test] public function shouldSuccessProcessForEmpty(): void { $this->rule = new ForbiddenNodeTypeRule(Empty_::class, 'Language construct empty() should not be used.'); @@ -58,9 +55,7 @@ public function shouldSuccessProcessForEmpty(): void ); } - /** - * @test - */ + #[Test] public function shouldThrowErrorForInvalidNodeType(): void { $this->expectException(\InvalidArgumentException::class); diff --git a/tests/PhpStan/FunctionStrictModeRuleTest.php b/tests/PhpStan/FunctionStrictModeRuleTest.php new file mode 100644 index 0000000..f1ca707 --- /dev/null +++ b/tests/PhpStan/FunctionStrictModeRuleTest.php @@ -0,0 +1,33 @@ +analyse( + [__DIR__.'/Resources/function-strict-mode.php'], + [ + ['The function in_array must be used in strict mode.', 16], + ['The function in_array must be used in strict mode.', 17], + ['The function in_array must be used in strict mode.', 18], + ['The function in_array must be used in strict mode.', 21], + ['The function in_array must be used in strict mode.', 23], + ], + ); + } +} diff --git a/tests/PhpStan/MethodCallConsistencyRuleTest.php b/tests/PhpStan/MethodCallConsistencyRuleTest.php index c7df2c8..e830fe3 100644 --- a/tests/PhpStan/MethodCallConsistencyRuleTest.php +++ b/tests/PhpStan/MethodCallConsistencyRuleTest.php @@ -18,6 +18,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\PHPStanTestCase; use PHPStan\Testing\RuleTestCase; +use PHPUnit\Framework\Attributes\Test; class MethodCallConsistencyRuleTest extends RuleTestCase { @@ -28,9 +29,7 @@ protected function getRule(): Rule return new MethodCallConsistencyRule($reflectionProvider); } - /** - * @test - */ + #[Test] public function shouldSuccessProcessForIsset(): void { $this->analyse( diff --git a/tests/PhpStan/Resources/function-strict-mode.php b/tests/PhpStan/Resources/function-strict-mode.php new file mode 100644 index 0000000..ebd750d --- /dev/null +++ b/tests/PhpStan/Resources/function-strict-mode.php @@ -0,0 +1,34 @@ + Date: Mon, 14 Apr 2025 18:17:22 +0300 Subject: [PATCH 2/2] add rule about strict mode for in_array function --- .../Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php | 2 +- tests/PhpStan/FunctionStrictModeRuleTest.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php b/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php index 19a19ca..b46cc97 100644 --- a/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php +++ b/src/PhpCs/FiveLab/Sniffs/Formatting/WhiteSpaceBeforeChainCallSniff.php @@ -126,7 +126,7 @@ private function calculateWhitespacesBeforeFirstTokenOnLine(File $phpcsFile, int * @param File $phpcsFile * @param int $line * - * @return array + * @return array */ private function getNonEmptyTokensOnLine(File $phpcsFile, int $line): array { diff --git a/tests/PhpStan/FunctionStrictModeRuleTest.php b/tests/PhpStan/FunctionStrictModeRuleTest.php index f1ca707..3fe2ef7 100644 --- a/tests/PhpStan/FunctionStrictModeRuleTest.php +++ b/tests/PhpStan/FunctionStrictModeRuleTest.php @@ -1,5 +1,14 @@