Skip to content

Commit dc579c0

Browse files
staabmjakubvojacek
andauthored
Dibi type inference
Co-authored-by: Jakub Vojacek <[email protected]> Co-authored-by: Markus Staab <[email protected]> Co-authored-by: jakubvojacek <[email protected]>
1 parent 89474dc commit dc579c0

20 files changed

+5259
-880
lines changed

.phpstan-dba-mysqli.cache

+152-596
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/extensions.neon

+6
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,9 @@ services:
9090
class: staabm\PHPStanDba\Ast\PreviousConnectingVisitor
9191
tags:
9292
- phpstan.parser.richParserNodeVisitor
93+
94+
-
95+
class: staabm\PHPStanDba\Extensions\DibiConnectionFetchDynamicReturnTypeExtension
96+
tags:
97+
- phpstan.broker.dynamicMethodReturnTypeExtension
98+

config/rules.neon

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
services:
2+
-
3+
class: staabm\PHPStanDba\Rules\SyntaxErrorInDibiPreparedStatementMethodRule
4+
tags: [phpstan.rules.rule]
5+
arguments:
6+
classMethods:
7+
- 'Dibi\Connection::fetch'
8+
- 'Dibi\Connection::fetchSingle'
9+
- 'Dibi\Connection::query'
10+
- 'Dibi\Connection::fetchAll'
11+
- 'Dibi\Connection::fetchPairs'
12+
213
-
314
class: staabm\PHPStanDba\Rules\SyntaxErrorInPreparedStatementMethodRule
415
tags: [phpstan.rules.rule]
@@ -24,11 +35,6 @@ services:
2435
- 'Doctrine\DBAL\Connection::iterateAssociativeIndexed'
2536
- 'Doctrine\DBAL\Connection::iterateColumn'
2637
- 'Doctrine\DBAL\Connection::executeUpdate' # deprecated in doctrine
27-
- 'Dibi\Connection::fetch'
28-
- 'Dibi\Connection::fetchSingle'
29-
- 'Dibi\Connection::query'
30-
- 'Dibi\Connection::fetchAll'
31-
- 'Dibi\Connection::fetchPairs'
3238

3339
-
3440
class: staabm\PHPStanDba\Rules\PdoStatementExecuteMethodRule

src/DibiReflection/DibiReflection.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace staabm\PHPStanDba\DibiReflection;
6+
7+
final class DibiReflection
8+
{
9+
public function rewriteQuery(string $queryString): ?string
10+
{
11+
$queryString = str_replace('%in', '(1)', $queryString);
12+
$queryString = str_replace('%lmt', 'LIMIT 1', $queryString);
13+
$queryString = str_replace('%ofs', ', 1', $queryString);
14+
$queryString = preg_replace('#%(i|s)#', "'1'", $queryString) ?? '';
15+
$queryString = preg_replace('#%(t|d)#', '"2000-1-1"', $queryString) ?? '';
16+
$queryString = preg_replace('#%(and|or)#', '(1 = 1)', $queryString) ?? '';
17+
$queryString = preg_replace('#%~?like~?#', '"%1%"', $queryString) ?? '';
18+
19+
if (strpos($queryString, '%n') > 0) {
20+
$queryString = null;
21+
} elseif (strpos($queryString, '%ex') > 0) {
22+
$queryString = null;
23+
} elseif (0 !== preg_match('#^\s*(START|ROLLBACK|SET|SAVEPOINT|SHOW)#i', $queryString)) {
24+
$queryString = null;
25+
}
26+
27+
return $queryString;
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace staabm\PHPStanDba\Extensions;
6+
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\MethodReflection;
11+
use PHPStan\Type\ArrayType;
12+
use PHPStan\Type\Constant\ConstantArrayType;
13+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\MixedType;
16+
use PHPStan\Type\NullType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\TypeCombinator;
19+
use PHPStan\Type\UnionType;
20+
use staabm\PHPStanDba\DibiReflection\DibiReflection;
21+
use staabm\PHPStanDba\QueryReflection\QueryReflection;
22+
use staabm\PHPStanDba\QueryReflection\QueryReflector;
23+
use staabm\PHPStanDba\UnresolvableQueryException;
24+
25+
final class DibiConnectionFetchDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
26+
{
27+
public function getClass(): string
28+
{
29+
return \Dibi\Connection::class;
30+
}
31+
32+
public function isMethodSupported(MethodReflection $methodReflection): bool
33+
{
34+
return \in_array($methodReflection->getName(), [
35+
'fetch',
36+
'fetchAll',
37+
'fetchPairs',
38+
'fetchSingle',
39+
], true);
40+
}
41+
42+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
43+
{
44+
$args = $methodCall->getArgs();
45+
46+
if (\count($args) < 1) {
47+
return null;
48+
}
49+
50+
if ($scope->getType($args[0]->value) instanceof MixedType) {
51+
return null;
52+
}
53+
54+
try {
55+
return $this->inferType($methodReflection, $args[0]->value, $scope);
56+
} catch (UnresolvableQueryException $exception) {
57+
// simulation not possible.. use default value
58+
}
59+
60+
return null;
61+
}
62+
63+
private function inferType(MethodReflection $methodReflection, Expr $queryExpr, Scope $scope): ?Type
64+
{
65+
$queryReflection = new QueryReflection();
66+
$queryStrings = $queryReflection->resolveQueryStrings($queryExpr, $scope);
67+
68+
return $this->createFetchType($queryStrings, $methodReflection);
69+
}
70+
71+
/**
72+
* @param iterable<string> $queryStrings
73+
*/
74+
private function createFetchType(iterable $queryStrings, MethodReflection $methodReflection): ?Type
75+
{
76+
$queryReflection = new QueryReflection();
77+
$dibiReflection = new DibiReflection();
78+
79+
$fetchTypes = [];
80+
foreach ($queryStrings as $queryString) {
81+
$queryString = $dibiReflection->rewriteQuery($queryString);
82+
if (null === $queryString) {
83+
continue;
84+
}
85+
86+
$resultType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_ASSOC);
87+
88+
if (null === $resultType) {
89+
return null;
90+
}
91+
92+
$fetchResultType = $this->reduceResultType($methodReflection, $resultType);
93+
if (null === $fetchResultType) {
94+
return null;
95+
}
96+
97+
$fetchTypes[] = $fetchResultType;
98+
}
99+
100+
if (\count($fetchTypes) > 1) {
101+
return TypeCombinator::union(...$fetchTypes);
102+
}
103+
if (1 === \count($fetchTypes)) {
104+
return $fetchTypes[0];
105+
}
106+
107+
return null;
108+
}
109+
110+
private function reduceResultType(MethodReflection $methodReflection, Type $resultType): ?Type
111+
{
112+
$methodName = $methodReflection->getName();
113+
114+
if ('fetch' === $methodName) {
115+
return new UnionType([new NullType(), $resultType]);
116+
} elseif ('fetchAll' === $methodName) {
117+
return new ArrayType(new IntegerType(), $resultType);
118+
} elseif ('fetchPairs' === $methodName && $resultType instanceof ConstantArrayType && 2 === \count($resultType->getValueTypes())) {
119+
return new ArrayType($resultType->getValueTypes()[0], $resultType->getValueTypes()[1]);
120+
} elseif ('fetchSingle' === $methodName && $resultType instanceof ConstantArrayType && 1 === \count($resultType->getValueTypes())) {
121+
return new UnionType([new NullType(), $resultType->getValueTypes()[0]]);
122+
}
123+
124+
return null;
125+
}
126+
}

0 commit comments

Comments
 (0)