-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a rule to check that arrays are hinted to doctrine
- Loading branch information
1 parent
e5442ed
commit 4b7aa3a
Showing
3 changed files
with
254 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Doctrine\DBAL; | ||
|
||
use Doctrine\DBAL\Connection; | ||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Type\ConstantScalarType; | ||
use PHPStan\Type\ObjectType; | ||
use PHPStan\Type\VerbosityLevel; | ||
use function array_map; | ||
use function count; | ||
use function in_array; | ||
use function is_int; | ||
|
||
/** | ||
* @implements Rule<Node\Expr\MethodCall> | ||
*/ | ||
class ArrayParameterTypeRule implements Rule | ||
{ | ||
|
||
private const CONNECTION_QUERY_METHODS_LOWER = [ | ||
'fetchassociative', | ||
'fetchnumeric', | ||
'fetchone', | ||
'delete', | ||
'insert', | ||
'fetchallnumeric', | ||
'fetchallassociative', | ||
'fetchallkeyvalue', | ||
'fetchallassociativeindexed', | ||
'fetchfirstcolumn', | ||
'iteratenumeric', | ||
'iterateassociative', | ||
'iteratekeyvalue', | ||
'iterateassociativeindexed', | ||
'iteratecolumn', | ||
'executequery', | ||
'executecachequery', | ||
'executestatement', | ||
]; | ||
|
||
public function getNodeType(): string | ||
{ | ||
return Node\Expr\MethodCall::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (! $node->name instanceof Node\Identifier) { | ||
return []; | ||
} | ||
|
||
if (count($node->getArgs()) < 2) { | ||
return []; | ||
} | ||
|
||
$calledOnType = $scope->getType($node->var); | ||
|
||
$connection = 'Doctrine\DBAL\Connection'; | ||
if (! (new ObjectType($connection))->isSuperTypeOf($calledOnType)->yes()) { | ||
return []; | ||
} | ||
|
||
$methodName = $node->name->toLowerString(); | ||
if (! in_array($methodName, self::CONNECTION_QUERY_METHODS_LOWER, true)) { | ||
return []; | ||
} | ||
|
||
$params = $scope->getType($node->getArgs()[1]->value); | ||
|
||
$typesArray = $node->getArgs()[2] ?? null; | ||
$typesArrayType = $typesArray !== null | ||
? $scope->getType($typesArray->value) | ||
: null; | ||
|
||
foreach ($params->getConstantArrays() as $arrayType) { | ||
$values = $arrayType->getValueTypes(); | ||
$keys = []; | ||
foreach ($values as $i => $value) { | ||
if (!$value->isArray()->yes()) { | ||
continue; | ||
} | ||
|
||
$keys[] = $arrayType->getKeyTypes()[$i]; | ||
} | ||
|
||
if ($keys === []) { | ||
continue; | ||
} | ||
|
||
$typeConstantArrays = $typesArrayType !== null | ||
? $typesArrayType->getConstantArrays() | ||
: []; | ||
|
||
if ($typeConstantArrays === []) { | ||
return array_map( | ||
static function (ConstantScalarType $type) { | ||
return RuleErrorBuilder::message( | ||
'Parameter at ' | ||
. $type->describe(VerbosityLevel::precise()) | ||
. ' is an array, but is not hinted as such to doctrine.' | ||
) | ||
->identifier('doctrine.parameterType') | ||
->build(); | ||
}, | ||
$keys | ||
); | ||
} | ||
|
||
foreach ($typeConstantArrays as $typeConstantArray) { | ||
$issueKeys = []; | ||
foreach ($keys as $key) { | ||
$valueType = $typeConstantArray->getOffsetValueType($key); | ||
|
||
$values = $valueType->getConstantScalarValues(); | ||
if ($values === []) { | ||
$issueKeys[] = $key; | ||
} | ||
|
||
foreach ($values as $scalarValue) { | ||
if (is_int($scalarValue) && !(($scalarValue & Connection::ARRAY_PARAM_OFFSET) !== Connection::ARRAY_PARAM_OFFSET)) { | ||
Check failure on line 124 in src/Rules/Doctrine/DBAL/ArrayParameterTypeRule.php GitHub Actions / PHPStan (8.3, composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctri...
Check failure on line 124 in src/Rules/Doctrine/DBAL/ArrayParameterTypeRule.php GitHub Actions / PHPStan (8.3, composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctri...
|
||
continue; | ||
} | ||
|
||
$issueKeys[] = $key; | ||
} | ||
|
||
return array_map( | ||
static function (ConstantScalarType $type) { | ||
return RuleErrorBuilder::message( | ||
'Parameter at ' | ||
. $type->describe(VerbosityLevel::precise()) | ||
. ' is an array, but is not hinted as such to doctrine.' | ||
)->identifier('doctrine.parameterType') | ||
->build(); | ||
}, | ||
$issueKeys | ||
); | ||
} | ||
} | ||
} | ||
|
||
return []; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Doctrine\DBAL; | ||
|
||
use PHPStan\Rules\Rule; | ||
use PHPStan\Testing\RuleTestCase; | ||
|
||
/** | ||
* @extends RuleTestCase<ArrayParameterTypeRule> | ||
*/ | ||
final class ArrayParameterTypeRuleTest extends RuleTestCase | ||
{ | ||
public function testRule(): void | ||
{ | ||
$this->analyse([__DIR__ . '/data/connection.php'], [ | ||
[ | ||
'Parameter at 0 is an array, but is not hinted as such to doctrine.', | ||
11, | ||
], | ||
[ | ||
"Parameter at 'a' is an array, but is not hinted as such to doctrine.", | ||
20, | ||
], | ||
[ | ||
"Parameter at 'a' is an array, but is not hinted as such to doctrine.", | ||
29, | ||
], | ||
[ | ||
"Parameter at 'a' is an array, but is not hinted as such to doctrine.", | ||
40, | ||
], | ||
]); | ||
} | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new ArrayParameterTypeRule(); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php | ||
|
||
namespace PHPStan\Rules\Doctrine\DBAL; | ||
|
||
use Doctrine\DBAL\ArrayParameterType; | ||
use Doctrine\DBAL\Connection; | ||
use Doctrine\DBAL\ParameterType; | ||
|
||
function check(Connection $connection, array $data) { | ||
|
||
$connection->executeQuery( | ||
'SELECT 1 FROM table WHERE a IN (?) AND b = ?', | ||
[ | ||
|
||
$data, | ||
3 | ||
] | ||
); | ||
|
||
$connection->fetchOne( | ||
'SELECT 1 FROM table WHERE a IN (:a) AND b = :b', | ||
[ | ||
|
||
'a' => $data, | ||
'b' => 3 | ||
] | ||
); | ||
|
||
$connection->fetchOne( | ||
'SELECT 1 FROM table WHERE a IN (:a) AND b = :b', | ||
[ | ||
'a' => $data, | ||
'b' => 3 | ||
], | ||
[ | ||
'b' => ParameterType::INTEGER, | ||
] | ||
); | ||
|
||
$connection->fetchOne( | ||
'SELECT 1 FROM table WHERE a IN (:a) AND b = :b', | ||
[ | ||
'a' => $data, | ||
'b' => 3 | ||
], | ||
[ | ||
'a' => ParameterType::INTEGER, | ||
'b' => ParameterType::INTEGER, | ||
] | ||
); | ||
|
||
|
||
$connection->fetchOne( | ||
'SELECT 1 FROM table WHERE a IN (:a) AND b = :b', | ||
[ | ||
'a' => $data, | ||
'b' => 3 | ||
], | ||
[ | ||
'a' => ArrayParameterType::INTEGER, | ||
'b' => ParameterType::INTEGER, | ||
] | ||
); | ||
|
||
} |