Skip to content

Commit

Permalink
Add a rule to check that arrays are hinted to doctrine
Browse files Browse the repository at this point in the history
  • Loading branch information
BackEndTea committed Feb 9, 2024
1 parent e5442ed commit 462d7c1
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 0 deletions.
149 changes: 149 additions & 0 deletions src/Rules/Doctrine/DBAL/ArrayParameterTypeRule.php
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

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctri...

Access to undefined constant Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET.

Check failure on line 124 in src/Rules/Doctrine/DBAL/ArrayParameterTypeRule.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctri...

Access to undefined constant Doctrine\DBAL\Connection::ARRAY_PARAM_OFFSET.
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 [];
}

}
41 changes: 41 additions & 0 deletions tests/Rules/Doctrine/DBAL/ArrayParameterTypeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?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();
}

}
65 changes: 65 additions & 0 deletions tests/Rules/Doctrine/DBAL/data/connection.php
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,
]
);

}

0 comments on commit 462d7c1

Please sign in to comment.