Skip to content

Commit 70a0fe7

Browse files
authored
Fix false positive with unnamed placeholders (#628)
1 parent c473688 commit 70a0fe7

File tree

4 files changed

+90
-14
lines changed

4 files changed

+90
-14
lines changed

src/QueryReflection/PlaceholderValidation.php

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,27 @@ public function checkQuery(Expr $queryExpr, Scope $scope, array $parameters): it
3434
return;
3535
}
3636

37+
$minPlaceholderCount = PHP_INT_MAX;
38+
$maxPlaceholderCount = 0;
3739
foreach ($queryStrings as $queryString) {
3840
$placeholderCount = $queryReflection->countPlaceholders($queryString);
39-
yield from $this->validateUnnamedPlaceholders($parameters, $placeholderCount);
41+
if ($placeholderCount < $minPlaceholderCount) {
42+
$minPlaceholderCount = $placeholderCount;
43+
}
44+
if ($placeholderCount > $maxPlaceholderCount) {
45+
$maxPlaceholderCount = $placeholderCount;
46+
}
4047
}
48+
49+
yield from $this->validateUnnamedPlaceholders($parameters, $minPlaceholderCount, $maxPlaceholderCount);
4150
}
4251

4352
/**
4453
* @param array<string|int, Parameter> $parameters
4554
*
4655
* @return iterable<string>
4756
*/
48-
private function validateUnnamedPlaceholders(array $parameters, int $placeholderCount): iterable
57+
private function validateUnnamedPlaceholders(array $parameters, int $minPlaceholderCount, int $maxPlaceholderCount): iterable
4958
{
5059
$parameterCount = \count($parameters);
5160
$minParameterCount = 0;
@@ -56,14 +65,22 @@ private function validateUnnamedPlaceholders(array $parameters, int $placeholder
5665
++$minParameterCount;
5766
}
5867

59-
if (0 === $parameterCount && 0 === $minParameterCount && 0 === $placeholderCount) {
68+
if (0 === $parameterCount
69+
&& 0 === $minParameterCount
70+
&& 0 === $minPlaceholderCount
71+
&& 0 === $maxPlaceholderCount
72+
) {
6073
return;
6174
}
6275

63-
if ($parameterCount !== $placeholderCount && $placeholderCount !== $minParameterCount) {
64-
$placeholderExpectation = sprintf('Query expects %s placeholder', $placeholderCount);
65-
if ($placeholderCount > 1) {
66-
$placeholderExpectation = sprintf('Query expects %s placeholders', $placeholderCount);
76+
if ($parameterCount > $maxPlaceholderCount || $minParameterCount < $minPlaceholderCount) {
77+
$placeholderExpectation = sprintf('Query expects %s placeholder', $minPlaceholderCount);
78+
if ($minPlaceholderCount > 1) {
79+
if ($minPlaceholderCount !== $maxPlaceholderCount) {
80+
$placeholderExpectation = sprintf('Query expects %s-%s placeholders', $minPlaceholderCount, $maxPlaceholderCount);
81+
} else {
82+
$placeholderExpectation = sprintf('Query expects %s placeholders', $minPlaceholderCount);
83+
}
6784
}
6885

6986
if (0 === $parameterCount) {

src/QueryReflection/QueryReflection.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -419,14 +419,23 @@ public function resolveParameters(Type $parameterTypes): ?array
419419

420420
if ($parameterTypes instanceof UnionType) {
421421
foreach ($parameterTypes->getConstantArrays() as $constantArray) {
422-
$parameters = $parameters + $this->resolveConstantArray($constantArray, true);
422+
foreach ($this->resolveConstantArray($constantArray) as $key => $resolvedParameter) {
423+
if (array_key_exists($key, $parameters)) {
424+
// required parameters should overrule optional parameters
425+
if (! $resolvedParameter->isOptional) {
426+
$parameters[$key] = $resolvedParameter;
427+
}
428+
} else {
429+
$parameters[$key] = $resolvedParameter;
430+
}
431+
}
423432
}
424433

425434
return $parameters;
426435
}
427436

428437
if ($parameterTypes instanceof ConstantArrayType) {
429-
return $this->resolveConstantArray($parameterTypes, false);
438+
return $this->resolveConstantArray($parameterTypes);
430439
}
431440

432441
return null;
@@ -437,7 +446,7 @@ public function resolveParameters(Type $parameterTypes): ?array
437446
*
438447
* @throws UnresolvableQueryException
439448
*/
440-
private function resolveConstantArray(ConstantArrayType $parameterTypes, bool $forceOptional): array
449+
private function resolveConstantArray(ConstantArrayType $parameterTypes): array
441450
{
442451
$parameters = [];
443452

@@ -447,9 +456,6 @@ private function resolveConstantArray(ConstantArrayType $parameterTypes, bool $f
447456

448457
foreach ($keyTypes as $i => $keyType) {
449458
$isOptional = \in_array($i, $optionalKeys, true);
450-
if ($forceOptional) {
451-
$isOptional = true;
452-
}
453459

454460
if ($keyType instanceof ConstantStringType) {
455461
$placeholderName = $keyType->getValue();

tests/rules/PdoStatementExecuteMethodRuleTest.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,16 @@ public function testParameterErrors(): void
115115

116116
public function testNamedPlaceholderBug(): void
117117
{
118-
$this->analyse([__DIR__ . '/data/named-placeholder-bug.php'], [
118+
$this->analyse([__DIR__ . '/data/named-placeholder-bug.php'], []);
119+
}
120+
121+
public function testPlaceholderBug(): void
122+
{
123+
$this->analyse([__DIR__ . '/data/placeholder-bug.php'], [
124+
[
125+
'Query expects 2-3 placeholders, but 1-3 values are given.',
126+
42,
127+
],
119128
]);
120129
}
121130
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace PlaceholderBug;
4+
5+
6+
use PDO;
7+
8+
class Foo
9+
{
10+
public function allGood(PDO $pdo, $vkFrom)
11+
{
12+
$values = [];
13+
$values[] = 1;
14+
15+
$fromCondition = '';
16+
if ('0' !== $vkFrom) {
17+
$fromCondition = 'and email = ?';
18+
$values[] = 'hello world';
19+
}
20+
21+
22+
$stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = ? ' . $fromCondition);
23+
$stmt->execute($values);
24+
}
25+
26+
public function sometimesWrongNumberOfParameters(PDO $pdo, $vkFrom)
27+
{
28+
$values = [];
29+
30+
$values[] = 1;
31+
if (rand(0,1)) {
32+
$values[] = 10;
33+
}
34+
35+
$fromCondition = '';
36+
if ('0' !== $vkFrom) {
37+
$fromCondition = 'and email = ?';
38+
$values[] = 'hello world';
39+
}
40+
41+
$stmt = $pdo->prepare('SELECT email, adaid FROM ada WHERE adaid = ? OR adaid = ? ' . $fromCondition);
42+
$stmt->execute($values);
43+
}
44+
}

0 commit comments

Comments
 (0)