diff --git a/src/QueryReflection/QueryReflection.php b/src/QueryReflection/QueryReflection.php index 047782dd1..d55f10df5 100644 --- a/src/QueryReflection/QueryReflection.php +++ b/src/QueryReflection/QueryReflection.php @@ -11,6 +11,7 @@ use PhpParser\Node\Scalar\EncapsedStringPart; use PHPStan\Analyser\Scope; use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; @@ -186,6 +187,27 @@ private function getSchemaReflection(): SchemaReflection return $this->schemaReflection; } + /** + * Determine if a query will be resolvable. + * + * - If yes, the query is a literal string. + * - If no, the query is a non-literal string or mixed type. + * - If maybe, the query is neither of the two. + * + * We will typically skip processing of queries that return no, which are + * likely part of a software abstraction layer that we know nothing about. + */ + public function isResolvable(Expr $queryExpr, Scope $scope): TrinaryLogic + { + $type = $scope->getType($queryExpr); + if ($type->isLiteralString()->yes()) { + return TrinaryLogic::createYes(); + } + $isStringOrMixed = $type->isSuperTypeOf(new StringType()); + + return $isStringOrMixed->negate(); + } + /** * @return iterable * diff --git a/src/QueryReflection/QuerySimulation.php b/src/QueryReflection/QuerySimulation.php index 5c9f7a353..99b6d3b12 100644 --- a/src/QueryReflection/QuerySimulation.php +++ b/src/QueryReflection/QuerySimulation.php @@ -20,7 +20,8 @@ use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use staabm\PHPStanDba\DbaException; -use staabm\PHPStanDba\UnresolvableQueryException; +use staabm\PHPStanDba\UnresolvableQueryMixedTypeException; +use staabm\PHPStanDba\UnresolvableQueryStringTypeException; /** * @internal @@ -30,7 +31,7 @@ final class QuerySimulation private const DATE_FORMAT = 'Y-m-d'; /** - * @throws UnresolvableQueryException + * @throws \staabm\PHPStanDba\UnresolvableQueryException */ public static function simulateParamValueType(Type $paramType, bool $preparedParam): ?string { @@ -94,6 +95,10 @@ public static function simulateParamValueType(Type $paramType, bool $preparedPar } // plain string types can contain anything.. we cannot reason about it + if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) { + throw new UnresolvableQueryStringTypeException('Cannot resolve query with variable type: ' . $paramType->describe(VerbosityLevel::precise())); + } + return null; } @@ -113,7 +118,7 @@ public static function simulateParamValueType(Type $paramType, bool $preparedPar // all types which we can't simulate and render a query unresolvable at analysis time if ($paramType instanceof MixedType || $paramType instanceof IntersectionType) { if (QueryReflection::getRuntimeConfiguration()->isDebugEnabled()) { - throw new UnresolvableQueryException('Cannot simulate parameter value for type: ' . $paramType->describe(VerbosityLevel::precise())); + throw new UnresolvableQueryMixedTypeException('Cannot simulate parameter value for type: ' . $paramType->describe(VerbosityLevel::precise())); } return null; diff --git a/src/Rules/PdoStatementExecuteMethodRule.php b/src/Rules/PdoStatementExecuteMethodRule.php index 535918009..ebc546a16 100644 --- a/src/Rules/PdoStatementExecuteMethodRule.php +++ b/src/Rules/PdoStatementExecuteMethodRule.php @@ -15,7 +15,6 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\MixedType; use staabm\PHPStanDba\PdoReflection\PdoStatementReflection; use staabm\PHPStanDba\QueryReflection\PlaceholderValidation; use staabm\PHPStanDba\QueryReflection\QueryReflection; @@ -67,7 +66,7 @@ private function checkErrors(MethodReflection $methodReflection, MethodCall $met if (null === $queryExpr) { return []; } - if ($scope->getType($queryExpr) instanceof MixedType) { + if ($queryReflection->isResolvable($queryExpr, $scope)->no()) { return []; } @@ -98,7 +97,7 @@ private function checkErrors(MethodReflection $methodReflection, MethodCall $met $parameters = $queryReflection->resolveParameters($parameterTypes) ?? []; } catch (UnresolvableQueryException $exception) { return [ - RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($methodCall->getLine())->build(), + RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($methodCall->getLine())->build(), ]; } @@ -111,7 +110,7 @@ private function checkErrors(MethodReflection $methodReflection, MethodCall $met } } catch (UnresolvableQueryException $exception) { return [ - RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($methodCall->getLine())->build(), + RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($methodCall->getLine())->build(), ]; } diff --git a/src/Rules/QueryPlanAnalyzerRule.php b/src/Rules/QueryPlanAnalyzerRule.php index 311bae2bc..30f07719e 100644 --- a/src/Rules/QueryPlanAnalyzerRule.php +++ b/src/Rules/QueryPlanAnalyzerRule.php @@ -14,7 +14,6 @@ use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use staabm\PHPStanDba\QueryReflection\QueryReflection; use staabm\PHPStanDba\Tests\QueryPlanAnalyzerRuleTest; @@ -89,20 +88,11 @@ public function processNode(Node $callLike, Scope $scope): array return []; } - $args = $callLike->getArgs(); - if (! \array_key_exists($queryArgPosition, $args)) { - return []; - } - - if ($scope->getType($args[$queryArgPosition]->value) instanceof MixedType) { - return []; - } - try { return $this->analyze($callLike, $scope); } catch (UnresolvableQueryException $exception) { return [ - RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($callLike->getLine())->build(), + RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($callLike->getLine())->build(), ]; } } @@ -125,8 +115,9 @@ private function analyze(CallLike $callLike, Scope $scope): array } $queryExpr = $args[0]->value; + $queryReflection = new QueryReflection(); - if ($scope->getType($queryExpr) instanceof MixedType) { + if ($queryReflection->isResolvable($queryExpr, $scope)->no()) { return []; } @@ -136,7 +127,6 @@ private function analyze(CallLike $callLike, Scope $scope): array } $ruleErrors = []; - $queryReflection = new QueryReflection(); $proposal = "\n\nConsider optimizing the query.\nIn some cases this is not a problem and this error should be ignored."; foreach ($queryReflection->analyzeQueryPlan($scope, $queryExpr, $parameterTypes) as $queryPlanResult) { diff --git a/src/Rules/SyntaxErrorInPreparedStatementMethodRule.php b/src/Rules/SyntaxErrorInPreparedStatementMethodRule.php index b05987198..3d93db029 100644 --- a/src/Rules/SyntaxErrorInPreparedStatementMethodRule.php +++ b/src/Rules/SyntaxErrorInPreparedStatementMethodRule.php @@ -14,7 +14,6 @@ use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use staabm\PHPStanDba\QueryReflection\PlaceholderValidation; use staabm\PHPStanDba\QueryReflection\QueryReflection; @@ -101,13 +100,12 @@ private function checkErrors(CallLike $callLike, Scope $scope): array } $queryExpr = $args[0]->value; + $queryReflection = new QueryReflection(); - if ($scope->getType($queryExpr) instanceof MixedType) { + if ($queryReflection->isResolvable($queryExpr, $scope)->no()) { return []; } - $queryReflection = new QueryReflection(); - $parameters = null; if (\count($args) > 1) { $parameterTypes = $scope->getType($args[1]->value); @@ -115,7 +113,7 @@ private function checkErrors(CallLike $callLike, Scope $scope): array $parameters = $queryReflection->resolveParameters($parameterTypes) ?? []; } catch (UnresolvableQueryException $exception) { return [ - RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($callLike->getLine())->build(), + RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($callLike->getLine())->build(), ]; } } @@ -152,7 +150,7 @@ private function checkErrors(CallLike $callLike, Scope $scope): array return $ruleErrors; } catch (UnresolvableQueryException $exception) { return [ - RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($callLike->getLine())->build(), + RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($callLike->getLine())->build(), ]; } } diff --git a/src/Rules/SyntaxErrorInQueryFunctionRule.php b/src/Rules/SyntaxErrorInQueryFunctionRule.php index 754b6b8e6..a1f8b693e 100644 --- a/src/Rules/SyntaxErrorInQueryFunctionRule.php +++ b/src/Rules/SyntaxErrorInQueryFunctionRule.php @@ -11,7 +11,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\MixedType; use staabm\PHPStanDba\QueryReflection\QueryReflection; use staabm\PHPStanDba\Tests\SyntaxErrorInQueryFunctionRuleTest; use staabm\PHPStanDba\UnresolvableQueryException; @@ -85,13 +84,15 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->getType($args[$queryArgPosition]->value) instanceof MixedType) { + $queryExpr = $args[$queryArgPosition]->value; + $queryReflection = new QueryReflection(); + + if ($queryReflection->isResolvable($queryExpr, $scope)->no()) { return []; } - $queryReflection = new QueryReflection(); try { - foreach ($queryReflection->resolveQueryStrings($args[$queryArgPosition]->value, $scope) as $queryString) { + foreach ($queryReflection->resolveQueryStrings($queryExpr, $scope) as $queryString) { $queryError = $queryReflection->validateQueryString($queryString); if (null !== $queryError) { return [ @@ -101,7 +102,7 @@ public function processNode(Node $node, Scope $scope): array } } catch (UnresolvableQueryException $exception) { return [ - RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($node->getLine())->build(), + RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($node->getLine())->build(), ]; } diff --git a/src/Rules/SyntaxErrorInQueryMethodRule.php b/src/Rules/SyntaxErrorInQueryMethodRule.php index 102a15d3a..7e200f44c 100644 --- a/src/Rules/SyntaxErrorInQueryMethodRule.php +++ b/src/Rules/SyntaxErrorInQueryMethodRule.php @@ -10,7 +10,6 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; -use PHPStan\Type\MixedType; use staabm\PHPStanDba\QueryReflection\QueryReflection; use staabm\PHPStanDba\UnresolvableQueryException; @@ -79,13 +78,15 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($scope->getType($args[$queryArgPosition]->value) instanceof MixedType) { + $queryExpr = $args[$queryArgPosition]->value; + $queryReflection = new QueryReflection(); + + if ($queryReflection->isResolvable($queryExpr, $scope)->no()) { return []; } try { - $queryReflection = new QueryReflection(); - $queryStrings = $queryReflection->resolveQueryStrings($args[$queryArgPosition]->value, $scope); + $queryStrings = $queryReflection->resolveQueryStrings($queryExpr, $scope); foreach ($queryStrings as $queryString) { $queryError = $queryReflection->validateQueryString($queryString); if (null !== $queryError) { @@ -96,7 +97,7 @@ public function processNode(Node $node, Scope $scope): array } } catch (UnresolvableQueryException $exception) { return [ - RuleErrorBuilder::message($exception->asRuleMessage())->tip(UnresolvableQueryException::RULE_TIP)->line($node->getLine())->build(), + RuleErrorBuilder::message($exception->asRuleMessage())->tip($exception::getTip())->line($node->getLine())->build(), ]; } diff --git a/src/UnresolvableQueryException.php b/src/UnresolvableQueryException.php index 963865f52..8be384136 100644 --- a/src/UnresolvableQueryException.php +++ b/src/UnresolvableQueryException.php @@ -4,9 +4,12 @@ namespace staabm\PHPStanDba; -final class UnresolvableQueryException extends DbaException +/** + * @api + */ +abstract class UnresolvableQueryException extends DbaException { - public const RULE_TIP = 'Make sure all variables involved have a non-mixed type and array-types are specified.'; + abstract public static function getTip(): string; public function asRuleMessage(): string { diff --git a/src/UnresolvableQueryMixedTypeException.php b/src/UnresolvableQueryMixedTypeException.php new file mode 100644 index 000000000..64f768d8c --- /dev/null +++ b/src/UnresolvableQueryMixedTypeException.php @@ -0,0 +1,13 @@ + @@ -149,7 +151,22 @@ public function testNotUsingIndexInDebugMode(): void [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 61, - 'Make sure all variables involved have a non-mixed type and array-types are specified.', + UnresolvableQueryMixedTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 67, + UnresolvableQueryStringTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 70, + UnresolvableQueryStringTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 73, + UnresolvableQueryStringTypeException::getTip(), ], ]); } diff --git a/tests/rules/UnresolvablePdoStatementRuleTest.php b/tests/rules/UnresolvablePdoStatementRuleTest.php index 793f11061..31a1ce755 100644 --- a/tests/rules/UnresolvablePdoStatementRuleTest.php +++ b/tests/rules/UnresolvablePdoStatementRuleTest.php @@ -8,7 +8,8 @@ use PHPStan\Testing\RuleTestCase; use staabm\PHPStanDba\QueryReflection\QueryReflection; use staabm\PHPStanDba\Rules\PdoStatementExecuteMethodRule; -use staabm\PHPStanDba\UnresolvableQueryException; +use staabm\PHPStanDba\UnresolvableQueryMixedTypeException; +use staabm\PHPStanDba\UnresolvableQueryStringTypeException; /** * @extends RuleTestCase @@ -45,12 +46,17 @@ public function testSyntaxErrorInQueryRule(): void [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 13, - UnresolvableQueryException::RULE_TIP, + UnresolvableQueryMixedTypeException::getTip(), ], [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 17, - UnresolvableQueryException::RULE_TIP, + UnresolvableQueryMixedTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 54, + UnresolvableQueryStringTypeException::getTip(), ], ]); } diff --git a/tests/rules/UnresolvablePreparedStatementRuleTest.php b/tests/rules/UnresolvablePreparedStatementRuleTest.php index 83bc9c06e..215248f5a 100644 --- a/tests/rules/UnresolvablePreparedStatementRuleTest.php +++ b/tests/rules/UnresolvablePreparedStatementRuleTest.php @@ -8,7 +8,8 @@ use PHPStan\Testing\RuleTestCase; use staabm\PHPStanDba\QueryReflection\QueryReflection; use staabm\PHPStanDba\Rules\SyntaxErrorInPreparedStatementMethodRule; -use staabm\PHPStanDba\UnresolvableQueryException; +use staabm\PHPStanDba\UnresolvableQueryMixedTypeException; +use staabm\PHPStanDba\UnresolvableQueryStringTypeException; /** * @extends RuleTestCase @@ -45,7 +46,12 @@ public function testSyntaxErrorInQueryRule(): void [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 11, - UnresolvableQueryException::RULE_TIP, + UnresolvableQueryMixedTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 30, + UnresolvableQueryStringTypeException::getTip(), ], ]); } diff --git a/tests/rules/UnresolvableQueryFunctionRuleTest.php b/tests/rules/UnresolvableQueryFunctionRuleTest.php index 4fc4eb2b6..1cbba100e 100644 --- a/tests/rules/UnresolvableQueryFunctionRuleTest.php +++ b/tests/rules/UnresolvableQueryFunctionRuleTest.php @@ -8,7 +8,8 @@ use PHPStan\Testing\RuleTestCase; use staabm\PHPStanDba\QueryReflection\QueryReflection; use staabm\PHPStanDba\Rules\SyntaxErrorInQueryFunctionRule; -use staabm\PHPStanDba\UnresolvableQueryException; +use staabm\PHPStanDba\UnresolvableQueryMixedTypeException; +use staabm\PHPStanDba\UnresolvableQueryStringTypeException; /** * @extends RuleTestCase @@ -45,12 +46,22 @@ public function testSyntaxErrorInQueryRule(): void [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 9, - UnresolvableQueryException::RULE_TIP, + UnresolvableQueryMixedTypeException::getTip(), ], [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 15, - UnresolvableQueryException::RULE_TIP, + UnresolvableQueryMixedTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 32, + UnresolvableQueryStringTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 37, + UnresolvableQueryStringTypeException::getTip(), ], ]); } diff --git a/tests/rules/UnresolvableQueryMethodRuleTest.php b/tests/rules/UnresolvableQueryMethodRuleTest.php index 9775ef103..b67bcea91 100644 --- a/tests/rules/UnresolvableQueryMethodRuleTest.php +++ b/tests/rules/UnresolvableQueryMethodRuleTest.php @@ -8,7 +8,8 @@ use PHPStan\Testing\RuleTestCase; use staabm\PHPStanDba\QueryReflection\QueryReflection; use staabm\PHPStanDba\Rules\SyntaxErrorInQueryMethodRule; -use staabm\PHPStanDba\UnresolvableQueryException; +use staabm\PHPStanDba\UnresolvableQueryMixedTypeException; +use staabm\PHPStanDba\UnresolvableQueryStringTypeException; /** * @extends RuleTestCase @@ -45,12 +46,22 @@ public function testSyntaxErrorInQueryRule(): void [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 11, - UnresolvableQueryException::RULE_TIP, + UnresolvableQueryMixedTypeException::getTip(), ], [ 'Unresolvable Query: Cannot simulate parameter value for type: mixed.', 17, - UnresolvableQueryException::RULE_TIP, + UnresolvableQueryMixedTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 34, + UnresolvableQueryStringTypeException::getTip(), + ], + [ + 'Unresolvable Query: Cannot resolve query with variable type: string.', + 39, + UnresolvableQueryStringTypeException::getTip(), ], ]); } diff --git a/tests/rules/data/unresolvable-pdo-statement.php b/tests/rules/data/unresolvable-pdo-statement.php index 15ab96574..6e5e83b91 100644 --- a/tests/rules/data/unresolvable-pdo-statement.php +++ b/tests/rules/data/unresolvable-pdo-statement.php @@ -47,4 +47,10 @@ public function noErrorOnStringValue(PDO $pdo, string $string) $stmt->bindValue(':email', '%|'.$string.'|%'); $stmt->execute(); } + + public function queryStringFragment(PDO $pdo, string $string) + { + $stmt = $pdo->prepare('SELECT email from ada WHERE '.$string); + $stmt->execute([':gesperrt' => 1]); + } } diff --git a/tests/rules/data/unresolvable-query-in-function.php b/tests/rules/data/unresolvable-query-in-function.php index 2d2a20537..920132f26 100644 --- a/tests/rules/data/unresolvable-query-in-function.php +++ b/tests/rules/data/unresolvable-query-in-function.php @@ -26,4 +26,14 @@ public function noErrorOnStringQuery(\mysqli $mysqli, string $query) { mysqli_query($mysqli, $query); } + + public function stringParam(\mysqli $mysqli, string $string) + { + mysqli_query($mysqli, 'SELECT adaid FROM ada WHERE gesperrt='.$string); + } + + public function queryStringFragment(\mysqli $mysqli, string $string) + { + mysqli_query($mysqli, 'SELECT adaid FROM ada WHERE '.$string); + } } diff --git a/tests/rules/data/unresolvable-query-in-method.php b/tests/rules/data/unresolvable-query-in-method.php index d43834ada..e596a82ab 100644 --- a/tests/rules/data/unresolvable-query-in-method.php +++ b/tests/rules/data/unresolvable-query-in-method.php @@ -28,4 +28,14 @@ public function noErrorOnStringQuery(PDO $pdo, string $query) { $pdo->query($query); } + + public function stringParam(PDO $pdo, string $string) + { + $pdo->query('SELECT email FROM ada WHERE gesperrt='.$string); + } + + public function stringQueryFragment(PDO $pdo, string $string) + { + $pdo->query('SELECT email FROM ada WHERE '.$string); + } } diff --git a/tests/rules/data/unresolvable-statement.php b/tests/rules/data/unresolvable-statement.php index bb5252bac..9ec03450d 100644 --- a/tests/rules/data/unresolvable-statement.php +++ b/tests/rules/data/unresolvable-statement.php @@ -24,4 +24,9 @@ public function noErrorOnStringQuery(Connection $connection, string $string) { $connection->preparedQuery($string, []); } + + public function stringQueryFragment(Connection $connection, string $string) + { + $connection->preparedQuery('SELECT email FROM ada WHERE '.$string, []); + } }