Skip to content

Commit 1a39cc6

Browse files
committed
Warn when queries cannot be resolved unexpectedly
Fixes #504. This adds new functionality to debug mode to warn about queries that cannot be resolved due to the presence of non-literal string variables in the query. This helps to identify places where you might expect a query to be resolvable, but an inline parameter or a variable query fragment prevent it. Some scenarios where this might occur include string query fragments where using `@phpstandba-inference-placeholder` may be needed, e.g. /** @var string $condition */ $query = 'SELECT * FROM table WHERE ' . $condition; Or when not using prepared statements, e.g. /** @var string $param */ $query = 'SELECT * FROM table WHERE foo=' . $conn->quote($param); Since these queries cannot be analyzed, they will now emit the following warning: > Unresolvable Query: Cannot resolve query with variable type: string. > Consider replacing variable strings with prepared statements or @phpstandba-inference-placeholder. Without this warning, errors in these queries will go undetected. Implementation notes: 1. We make `UnresolvableQueryException` abstract, and add child classes for each type of unresolvable query error so that we can provide customized tips for each one. 2. Special care had to be taken to retain the ability to ignore queries that are _entirely_ StringType, because it was explicitly desired for these queries to not return an error (even in debug mode). This is because such queries are likely part of a s/w layer that we know nothing about. Therefore, we early return before checking parameters for any query that is is a superset of `StringType` (which includes the previous `MixedType` condition, and any mix of the two).
1 parent e0f23fd commit 1a39cc6

18 files changed

+147
-35
lines changed

src/QueryReflection/QuerySimulation.php

+8-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
use PHPStan\Type\UnionType;
2121
use PHPStan\Type\VerbosityLevel;
2222
use staabm\PHPStanDba\DbaException;
23-
use staabm\PHPStanDba\UnresolvableQueryException;
23+
use staabm\PHPStanDba\UnresolvableQueryMixedTypeException;
24+
use staabm\PHPStanDba\UnresolvableQueryStringTypeException;
2425

2526
/**
2627
* @internal
@@ -30,7 +31,7 @@ final class QuerySimulation
3031
private const DATE_FORMAT = 'Y-m-d';
3132

3233
/**
33-
* @throws UnresolvableQueryException
34+
* @throws \staabm\PHPStanDba\UnresolvableQueryException
3435
*/
3536
public static function simulateParamValueType(Type $paramType, bool $preparedParam): ?string
3637
{
@@ -94,6 +95,10 @@ public static function simulateParamValueType(Type $paramType, bool $preparedPar
9495
}
9596

9697
// plain string types can contain anything.. we cannot reason about it
98+
if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
99+
throw new UnresolvableQueryStringTypeException('Cannot resolve query with variable type: '.$paramType->describe(VerbosityLevel::precise()));
100+
}
101+
97102
return null;
98103
}
99104

@@ -113,7 +118,7 @@ public static function simulateParamValueType(Type $paramType, bool $preparedPar
113118
// all types which we can't simulate and render a query unresolvable at analysis time
114119
if ($paramType instanceof MixedType || $paramType instanceof IntersectionType) {
115120
if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) {
116-
throw new UnresolvableQueryException('Cannot simulate parameter value for type: '.$paramType->describe(VerbosityLevel::precise()));
121+
throw new UnresolvableQueryMixedTypeException('Cannot simulate parameter value for type: '.$paramType->describe(VerbosityLevel::precise()));
117122
}
118123

119124
return null;

src/Rules/PdoStatementExecuteMethodRule.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use PHPStan\Type\Constant\ConstantArrayType;
1616
use PHPStan\Type\Constant\ConstantIntegerType;
1717
use PHPStan\Type\Constant\ConstantStringType;
18-
use PHPStan\Type\MixedType;
18+
use PHPStan\Type\StringType;
1919
use staabm\PHPStanDba\PdoReflection\PdoStatementReflection;
2020
use staabm\PHPStanDba\QueryReflection\PlaceholderValidation;
2121
use staabm\PHPStanDba\QueryReflection\QueryReflection;
@@ -67,7 +67,7 @@ private function checkErrors(MethodReflection $methodReflection, MethodCall $met
6767
if (null === $queryExpr) {
6868
return [];
6969
}
70-
if ($scope->getType($queryExpr) instanceof MixedType) {
70+
if ($scope->getType($queryExpr)->isSuperTypeOf(new StringType())->yes()) {
7171
return [];
7272
}
7373

@@ -98,7 +98,7 @@ private function checkErrors(MethodReflection $methodReflection, MethodCall $met
9898
$parameters = $queryReflection->resolveParameters($parameterTypes) ?? [];
9999
} catch (UnresolvableQueryException $exception) {
100100
return [
101-
RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($methodCall->getLine())->build(),
101+
RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($methodCall->getLine())->build(),
102102
];
103103
}
104104

@@ -111,7 +111,7 @@ private function checkErrors(MethodReflection $methodReflection, MethodCall $met
111111
}
112112
} catch (UnresolvableQueryException $exception) {
113113
return [
114-
RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($methodCall->getLine())->build(),
114+
RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($methodCall->getLine())->build(),
115115
];
116116
}
117117

src/Rules/QueryPlanAnalyzerRule.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
use PHPStan\Rules\RuleError;
1515
use PHPStan\Rules\RuleErrorBuilder;
1616
use PHPStan\ShouldNotHappenException;
17-
use PHPStan\Type\MixedType;
1817
use PHPStan\Type\ObjectType;
18+
use PHPStan\Type\StringType;
1919
use staabm\PHPStanDba\QueryReflection\QueryReflection;
2020
use staabm\PHPStanDba\Tests\QueryPlanAnalyzerRuleTest;
2121
use staabm\PHPStanDba\UnresolvableQueryException;
@@ -94,15 +94,15 @@ public function processNode(Node $callLike, Scope $scope): array
9494
return [];
9595
}
9696

97-
if ($scope->getType($args[$queryArgPosition]->value) instanceof MixedType) {
97+
if ($scope->getType($args[$queryArgPosition]->value)->isSuperTypeOf(new StringType())->yes()) {
9898
return [];
9999
}
100100

101101
try {
102102
return $this->analyze($callLike, $scope);
103103
} catch (UnresolvableQueryException $exception) {
104104
return [
105-
RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($callLike->getLine())->build(),
105+
RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($callLike->getLine())->build(),
106106
];
107107
}
108108
}
@@ -126,7 +126,7 @@ private function analyze(CallLike $callLike, Scope $scope): array
126126

127127
$queryExpr = $args[0]->value;
128128

129-
if ($scope->getType($queryExpr) instanceof MixedType) {
129+
if ($scope->getType($queryExpr)->isSuperTypeOf(new StringType())->yes()) {
130130
return [];
131131
}
132132

src/Rules/SyntaxErrorInPreparedStatementMethodRule.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
use PHPStan\Rules\RuleError;
1515
use PHPStan\Rules\RuleErrorBuilder;
1616
use PHPStan\ShouldNotHappenException;
17-
use PHPStan\Type\MixedType;
1817
use PHPStan\Type\ObjectType;
18+
use PHPStan\Type\StringType;
1919
use staabm\PHPStanDba\QueryReflection\PlaceholderValidation;
2020
use staabm\PHPStanDba\QueryReflection\QueryReflection;
2121
use staabm\PHPStanDba\UnresolvableQueryException;
@@ -102,7 +102,7 @@ private function checkErrors(CallLike $callLike, Scope $scope): array
102102

103103
$queryExpr = $args[0]->value;
104104

105-
if ($scope->getType($queryExpr) instanceof MixedType) {
105+
if ($scope->getType($queryExpr)->isSuperTypeOf(new StringType())->yes()) {
106106
return [];
107107
}
108108

@@ -115,7 +115,7 @@ private function checkErrors(CallLike $callLike, Scope $scope): array
115115
$parameters = $queryReflection->resolveParameters($parameterTypes) ?? [];
116116
} catch (UnresolvableQueryException $exception) {
117117
return [
118-
RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($callLike->getLine())->build(),
118+
RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($callLike->getLine())->build(),
119119
];
120120
}
121121
}
@@ -152,7 +152,7 @@ private function checkErrors(CallLike $callLike, Scope $scope): array
152152
return $ruleErrors;
153153
} catch (UnresolvableQueryException $exception) {
154154
return [
155-
RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($callLike->getLine())->build(),
155+
RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($callLike->getLine())->build(),
156156
];
157157
}
158158
}

src/Rules/SyntaxErrorInQueryFunctionRule.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
use PHPStan\Rules\Rule;
1212
use PHPStan\Rules\RuleErrorBuilder;
1313
use PHPStan\ShouldNotHappenException;
14-
use PHPStan\Type\MixedType;
14+
use PHPStan\Type\StringType;
1515
use staabm\PHPStanDba\QueryReflection\QueryReflection;
1616
use staabm\PHPStanDba\Tests\SyntaxErrorInQueryFunctionRuleTest;
1717
use staabm\PHPStanDba\UnresolvableQueryException;
@@ -85,7 +85,7 @@ public function processNode(Node $node, Scope $scope): array
8585
return [];
8686
}
8787

88-
if ($scope->getType($args[$queryArgPosition]->value) instanceof MixedType) {
88+
if ($scope->getType($args[$queryArgPosition]->value)->isSuperTypeOf(new StringType())->yes()) {
8989
return [];
9090
}
9191

@@ -101,7 +101,7 @@ public function processNode(Node $node, Scope $scope): array
101101
}
102102
} catch (UnresolvableQueryException $exception) {
103103
return [
104-
RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($node->getLine())->build(),
104+
RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($node->getLine())->build(),
105105
];
106106
}
107107

src/Rules/SyntaxErrorInQueryMethodRule.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use PHPStan\Rules\Rule;
1111
use PHPStan\Rules\RuleErrorBuilder;
1212
use PHPStan\ShouldNotHappenException;
13-
use PHPStan\Type\MixedType;
13+
use PHPStan\Type\StringType;
1414
use staabm\PHPStanDba\QueryReflection\QueryReflection;
1515
use staabm\PHPStanDba\UnresolvableQueryException;
1616

@@ -79,7 +79,7 @@ public function processNode(Node $node, Scope $scope): array
7979
return [];
8080
}
8181

82-
if ($scope->getType($args[$queryArgPosition]->value) instanceof MixedType) {
82+
if ($scope->getType($args[$queryArgPosition]->value)->isSuperTypeOf(new StringType())->yes()) {
8383
return [];
8484
}
8585

@@ -96,7 +96,7 @@ public function processNode(Node $node, Scope $scope): array
9696
}
9797
} catch (UnresolvableQueryException $exception) {
9898
return [
99-
RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($node->getLine())->build(),
99+
RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($node->getLine())->build(),
100100
];
101101
}
102102

src/UnresolvableQueryException.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
namespace staabm\PHPStanDba;
44

5-
final class UnresolvableQueryException extends DbaException
5+
/**
6+
* @api
7+
*/
8+
abstract class UnresolvableQueryException extends DbaException
69
{
7-
public const RULE_TIP = 'Make sure all variables involved have a non-mixed type and array-types are specified.';
10+
abstract public static function getTip(): string;
811

912
public function asRuleMessage(): string
1013
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace staabm\PHPStanDba;
4+
5+
final class UnresolvableQueryMixedTypeException extends UnresolvableQueryException
6+
{
7+
public static function getTip(): string
8+
{
9+
return 'Make sure all variables involved have a non-mixed type and array-types are specified.';
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace staabm\PHPStanDba;
4+
5+
final class UnresolvableQueryStringTypeException extends UnresolvableQueryException
6+
{
7+
public static function getTip(): string
8+
{
9+
return 'Consider replacing variable strings with prepared statements or @phpstandba-inference-placeholder.';
10+
}
11+
}

tests/rules/QueryPlanAnalyzerRuleTest.php

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use PHPStan\Testing\RuleTestCase;
77
use staabm\PHPStanDba\QueryReflection\QueryReflection;
88
use staabm\PHPStanDba\Rules\QueryPlanAnalyzerRule;
9+
use staabm\PHPStanDba\UnresolvableQueryMixedTypeException;
10+
use staabm\PHPStanDba\UnresolvableQueryStringTypeException;
911

1012
/**
1113
* @extends RuleTestCase<QueryPlanAnalyzerRule>
@@ -137,7 +139,22 @@ public function testNotUsingIndexInDebugMode(): void
137139
[
138140
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
139141
61,
140-
'Make sure all variables involved have a non-mixed type and array-types are specified.',
142+
UnresolvableQueryMixedTypeException::getTip(),
143+
],
144+
[
145+
'Unresolvable Query: Cannot resolve query with variable type: string.',
146+
67,
147+
UnresolvableQueryStringTypeException::getTip(),
148+
],
149+
[
150+
'Unresolvable Query: Cannot resolve query with variable type: string.',
151+
70,
152+
UnresolvableQueryStringTypeException::getTip(),
153+
],
154+
[
155+
'Unresolvable Query: Cannot resolve query with variable type: string.',
156+
73,
157+
UnresolvableQueryStringTypeException::getTip(),
141158
],
142159
]);
143160
}

tests/rules/UnresolvablePdoStatementRuleTest.php

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use PHPStan\Testing\RuleTestCase;
77
use staabm\PHPStanDba\QueryReflection\QueryReflection;
88
use staabm\PHPStanDba\Rules\PdoStatementExecuteMethodRule;
9-
use staabm\PHPStanDba\UnresolvableQueryException;
9+
use staabm\PHPStanDba\UnresolvableQueryMixedTypeException;
10+
use staabm\PHPStanDba\UnresolvableQueryStringTypeException;
1011

1112
/**
1213
* @extends RuleTestCase<PdoStatementExecuteMethodRule>
@@ -43,12 +44,17 @@ public function testSyntaxErrorInQueryRule(): void
4344
[
4445
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
4546
13,
46-
UnresolvableQueryException::RULE_TIP,
47+
UnresolvableQueryMixedTypeException::getTip(),
4748
],
4849
[
4950
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
5051
17,
51-
UnresolvableQueryException::RULE_TIP,
52+
UnresolvableQueryMixedTypeException::getTip(),
53+
],
54+
[
55+
'Unresolvable Query: Cannot resolve query with variable type: string.',
56+
54,
57+
UnresolvableQueryStringTypeException::getTip(),
5258
],
5359
]);
5460
}

tests/rules/UnresolvablePreparedStatementRuleTest.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use PHPStan\Testing\RuleTestCase;
77
use staabm\PHPStanDba\QueryReflection\QueryReflection;
88
use staabm\PHPStanDba\Rules\SyntaxErrorInPreparedStatementMethodRule;
9-
use staabm\PHPStanDba\UnresolvableQueryException;
9+
use staabm\PHPStanDba\UnresolvableQueryMixedTypeException;
10+
use staabm\PHPStanDba\UnresolvableQueryStringTypeException;
1011

1112
/**
1213
* @extends RuleTestCase<SyntaxErrorInPreparedStatementMethodRule>
@@ -43,7 +44,12 @@ public function testSyntaxErrorInQueryRule(): void
4344
[
4445
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
4546
11,
46-
UnresolvableQueryException::RULE_TIP,
47+
UnresolvableQueryMixedTypeException::getTip(),
48+
],
49+
[
50+
'Unresolvable Query: Cannot resolve query with variable type: string.',
51+
30,
52+
UnresolvableQueryStringTypeException::getTip(),
4753
],
4854
]);
4955
}

tests/rules/UnresolvableQueryFunctionRuleTest.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use PHPStan\Testing\RuleTestCase;
77
use staabm\PHPStanDba\QueryReflection\QueryReflection;
88
use staabm\PHPStanDba\Rules\SyntaxErrorInQueryFunctionRule;
9-
use staabm\PHPStanDba\UnresolvableQueryException;
9+
use staabm\PHPStanDba\UnresolvableQueryMixedTypeException;
10+
use staabm\PHPStanDba\UnresolvableQueryStringTypeException;
1011

1112
/**
1213
* @extends RuleTestCase<SyntaxErrorInQueryFunctionRule>
@@ -43,12 +44,22 @@ public function testSyntaxErrorInQueryRule(): void
4344
[
4445
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
4546
9,
46-
UnresolvableQueryException::RULE_TIP,
47+
UnresolvableQueryMixedTypeException::getTip(),
4748
],
4849
[
4950
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
5051
15,
51-
UnresolvableQueryException::RULE_TIP,
52+
UnresolvableQueryMixedTypeException::getTip(),
53+
],
54+
[
55+
'Unresolvable Query: Cannot resolve query with variable type: string.',
56+
32,
57+
UnresolvableQueryStringTypeException::getTip(),
58+
],
59+
[
60+
'Unresolvable Query: Cannot resolve query with variable type: string.',
61+
37,
62+
UnresolvableQueryStringTypeException::getTip(),
5263
],
5364
]);
5465
}

tests/rules/UnresolvableQueryMethodRuleTest.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
use PHPStan\Testing\RuleTestCase;
77
use staabm\PHPStanDba\QueryReflection\QueryReflection;
88
use staabm\PHPStanDba\Rules\SyntaxErrorInQueryMethodRule;
9-
use staabm\PHPStanDba\UnresolvableQueryException;
9+
use staabm\PHPStanDba\UnresolvableQueryMixedTypeException;
10+
use staabm\PHPStanDba\UnresolvableQueryStringTypeException;
1011

1112
/**
1213
* @extends RuleTestCase<SyntaxErrorInQueryMethodRule>
@@ -43,12 +44,22 @@ public function testSyntaxErrorInQueryRule(): void
4344
[
4445
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
4546
11,
46-
UnresolvableQueryException::RULE_TIP,
47+
UnresolvableQueryMixedTypeException::getTip(),
4748
],
4849
[
4950
'Unresolvable Query: Cannot simulate parameter value for type: mixed.',
5051
17,
51-
UnresolvableQueryException::RULE_TIP,
52+
UnresolvableQueryMixedTypeException::getTip(),
53+
],
54+
[
55+
'Unresolvable Query: Cannot resolve query with variable type: string.',
56+
34,
57+
UnresolvableQueryStringTypeException::getTip(),
58+
],
59+
[
60+
'Unresolvable Query: Cannot resolve query with variable type: string.',
61+
39,
62+
UnresolvableQueryStringTypeException::getTip(),
5263
],
5364
]);
5465
}

0 commit comments

Comments
 (0)