Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a rule to check that arrays are hinted to doctrine #541

Open
wants to merge 2 commits into
base: 1.4.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 [];
}

}
79 changes: 79 additions & 0 deletions tests/Rules/Doctrine/DBAL/ArrayParameterTypeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Doctrine\DBAL;

use Composer\InstalledVersions;
use Composer\Semver\VersionParser;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ArrayParameterTypeRule>
*/
final class ArrayParameterTypeRuleTest extends RuleTestCase
{

public function testRuleOlderDbal(): void
{
if(InstalledVersions::satisfies(
new VersionParser(),
'doctrine/dbal',
'^3.6 || ^4.0'
)) {
self::markTestSkipped('Test requires dbal 2.');
}
$this->analyse([__DIR__ . '/data/connection_dbal2.php'], [
[
'Parameter at 0 is an array, but is not hinted as such to doctrine.',
10,
],
[
"Parameter at 'a' is an array, but is not hinted as such to doctrine.",
19,
],
[
"Parameter at 'a' is an array, but is not hinted as such to doctrine.",
28,
],
[
"Parameter at 'a' is an array, but is not hinted as such to doctrine.",
39,
],
]);
}

public function testRule(): void
{
if(InstalledVersions::satisfies(
new VersionParser(),
'doctrine/dbal',
'<3.6'
)) {
self::markTestSkipped('Test requires dbal 3 or 4.');
}
$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,
]
);

}
64 changes: 64 additions & 0 deletions tests/Rules/Doctrine/DBAL/data/connection_dbal2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace PHPStan\Rules\Doctrine\DBAL;

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' => Connection::PARAM_INT_ARRAY,
'b' => ParameterType::INTEGER,
]
);

}