Skip to content
Merged
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
4 changes: 4 additions & 0 deletions config/phpstan.services.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
9 changes: 3 additions & 6 deletions src/PhpCs/FiveLab/Sniffs/Formatting/ReadonlySniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;

/**
* Check the readonly.
*/
class ReadonlySniff implements Sniff
{
const SCOPES = [
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?php

declare(strict_types = 1);

/*
* This file is part of the FiveLab CiRules package
*
Expand All @@ -11,6 +9,8 @@
* file that was distributed with this source code
*/

declare(strict_types = 1);

namespace FiveLab\Component\CiRules\PhpCs\FiveLab\Sniffs\Formatting;

use FiveLab\Component\CiRules\PhpCs\FiveLab\ErrorCodes;
Expand All @@ -19,9 +19,6 @@
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;

/**
* Sniff for check whitespaces count before chain call.
*/
class WhiteSpaceBeforeChainCallSniff implements Sniff
{
const GAP = 4;
Expand Down Expand Up @@ -124,7 +121,7 @@ private function calculateWhitespacesBeforeFirstTokenOnLine(File $phpcsFile, int
}

/**
* Get non empty tokens on line
* Get non-empty tokens on line
*
* @param File $phpcsFile
* @param int $line
Expand All @@ -136,7 +133,7 @@ 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);
Expand Down
127 changes: 127 additions & 0 deletions src/PhpStan/FunctionStrictModeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

/*
* This file is part of the FiveLab CiRules package
*
* (c) FiveLab
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/

declare(strict_types = 1);

namespace FiveLab\Component\CiRules\PhpStan;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;

/**
* @implements Rule<Node>
*/
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<IdentifierRuleError>
*/
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;
}
}
13 changes: 4 additions & 9 deletions tests/PhpStan/ForbiddenNodeTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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.');
Expand All @@ -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.');
Expand All @@ -58,9 +55,7 @@ public function shouldSuccessProcessForEmpty(): void
);
}

/**
* @test
*/
#[Test]
public function shouldThrowErrorForInvalidNodeType(): void
{
$this->expectException(\InvalidArgumentException::class);
Expand Down
42 changes: 42 additions & 0 deletions tests/PhpStan/FunctionStrictModeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the FiveLab CiRules package
*
* (c) FiveLab
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code
*/

declare(strict_types = 1);

namespace FiveLab\Component\CiRules\Tests\PhpStan;

use FiveLab\Component\CiRules\PhpStan\FunctionStrictModeRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\Test;

class FunctionStrictModeRuleTest extends RuleTestCase
{
protected function getRule(): Rule
{
return new FunctionStrictModeRule();
}

#[Test]
public function shouldSuccessProcess(): void
{
$this->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],
],
);
}
}
5 changes: 2 additions & 3 deletions tests/PhpStan/MethodCallConsistencyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -28,9 +29,7 @@ protected function getRule(): Rule
return new MethodCallConsistencyRule($reflectionProvider);
}

/**
* @test
*/
#[Test]
public function shouldSuccessProcessForIsset(): void
{
$this->analyse(
Expand Down
34 changes: 34 additions & 0 deletions tests/PhpStan/Resources/function-strict-mode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

// success
$res = \in_array('a', ['a'], true);
$res = \in_array(1, [1], true);
$res = \in_array([1, 2], [1, 2, 3], true);

$res = \in_array(new stdClass(), [1, 'a']);
$res = \in_array(new stdClass(), [new stdClass(), 'a']);
$res = \in_array([new stdClass()], [new stdClass(), 1]);

$strict = true;
$res = \in_array(1, [1], $strict);

// errors
$res = \in_array('a', ['a']);
$res = \in_array(1, [1]);
$res = \in_array([1, 2], [1, 2, 3]);

$strict = false;
$res = \in_array('a', ['a'], $strict);

$res = \in_array(rand(0, 1) ? 1 : '1', [1, 2, 3]);

// ignore
$strict = rand(0, 1) === 1;
$res = \in_array('x', ['x'], $strict);

$args = ['a', ['a'], false];
$res = \in_array(...$args);

// fail-safe
$fn = 'in_array';
$res = $fn('a', ['a']);