Skip to content

Commit 86586e5

Browse files
staabmclxmstaab
andauthored
implement type inference for union-types in sql queries (#290)
Co-authored-by: Markus Staab <[email protected]>
1 parent 6bd6e84 commit 86586e5

18 files changed

+5458
-139
lines changed

.phpstan-dba-mysqli.cache

Lines changed: 3519 additions & 59 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.phpstan-dba-pdo.cache

Lines changed: 118 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Extensions/PdoPrepareDynamicReturnTypeExtension.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,11 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method
6969
private function inferType(Expr $queryExpr, Scope $scope): ?Type
7070
{
7171
$queryReflection = new QueryReflection();
72-
$queryString = $queryReflection->resolveQueryString($queryExpr, $scope);
73-
if (null === $queryString) {
74-
return null;
75-
}
72+
$queryStrings = $queryReflection->resolveQueryStrings($queryExpr, $scope);
7673

7774
$reflectionFetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode();
7875
$pdoStatementReflection = new PdoStatementReflection();
7976

80-
return $pdoStatementReflection->createGenericStatement($queryString, $reflectionFetchType);
77+
return $pdoStatementReflection->createGenericStatement($queryStrings, $reflectionFetchType);
8178
}
8279
}

src/Extensions/PdoQueryDynamicReturnTypeExtension.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,10 @@ private function inferType(MethodCall $methodCall, Expr $queryExpr, Scope $scope
8686
}
8787

8888
$queryReflection = new QueryReflection();
89-
$queryString = $queryReflection->resolveQueryString($queryExpr, $scope);
90-
if (null === $queryString) {
91-
return null;
92-
}
89+
$queryStrings = $queryReflection->resolveQueryStrings($queryExpr, $scope);
9390

9491
$pdoStatementReflection = new PdoStatementReflection();
9592

96-
return $pdoStatementReflection->createGenericStatement($queryString, $reflectionFetchType);
93+
return $pdoStatementReflection->createGenericStatement($queryStrings, $reflectionFetchType);
9794
}
9895
}

src/Extensions/PdoStatementExecuteTypeSpecifyingExtension.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,10 @@ private function inferStatementType(MethodReflection $methodReflection, MethodCa
6666
$parameterTypes = $scope->getType($args[0]->value);
6767

6868
$queryReflection = new QueryReflection();
69-
$queryString = $queryReflection->resolvePreparedQueryString($queryExpr, $parameterTypes, $scope);
70-
if (null === $queryString) {
71-
return null;
72-
}
69+
$queryStrings = $queryReflection->resolvePreparedQueryStrings($queryExpr, $parameterTypes, $scope);
7370

7471
$reflectionFetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode();
7572

76-
return $stmtReflection->createGenericStatement($queryString, $reflectionFetchType);
73+
return $stmtReflection->createGenericStatement($queryStrings, $reflectionFetchType);
7774
}
7875
}

src/Extensions/PdoStatementFetchDynamicReturnTypeExtension.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@
1313
use PHPStan\Type\ArrayType;
1414
use PHPStan\Type\Constant\ConstantBooleanType;
1515
use PHPStan\Type\DynamicMethodReturnTypeExtension;
16-
use PHPStan\Type\Generic\GenericObjectType;
1716
use PHPStan\Type\IntegerType;
1817
use PHPStan\Type\MixedType;
1918
use PHPStan\Type\Type;
2019
use PHPStan\Type\TypeCombinator;
21-
use PHPStan\Type\UnionType;
2220
use staabm\PHPStanDba\PdoReflection\PdoStatementReflection;
2321
use staabm\PHPStanDba\QueryReflection\QueryReflection;
2422

@@ -67,11 +65,8 @@ private function inferType(MethodReflection $methodReflection, MethodCall $metho
6765
$pdoStatementReflection = new PdoStatementReflection();
6866

6967
$statementType = $scope->getType($methodCall->var);
70-
if (!$statementType instanceof GenericObjectType) {
71-
return null;
72-
}
73-
7468
$fetchType = QueryReflection::getRuntimeConfiguration()->getDefaultFetchMode();
69+
7570
if (\count($args) > 0) {
7671
$fetchModeType = $scope->getType($args[0]->value);
7772
$fetchType = $pdoStatementReflection->getFetchType($fetchModeType);
@@ -90,6 +85,6 @@ private function inferType(MethodReflection $methodReflection, MethodCall $metho
9085
return new ArrayType(new IntegerType(), $rowType);
9186
}
9287

93-
return new UnionType([$rowType, new ConstantBooleanType(false)]);
88+
return TypeCombinator::union($rowType, new ConstantBooleanType(false));
9489
}
9590
}

src/PdoReflection/PdoStatementReflection.php

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use PHPStan\Type\Constant\ConstantStringType;
1515
use PHPStan\Type\Generic\GenericObjectType;
1616
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
18+
use PHPStan\Type\UnionType;
1719
use staabm\PHPStanDba\QueryReflection\ExpressionFinder;
1820
use staabm\PHPStanDba\QueryReflection\QueryReflection;
1921
use staabm\PHPStanDba\QueryReflection\QueryReflector;
@@ -68,17 +70,29 @@ public function getFetchType(Type $fetchModeType): ?int
6870
}
6971

7072
/**
73+
* @param iterable<string> $queryStrings
7174
* @param QueryReflector::FETCH_TYPE* $reflectionFetchType
7275
*/
73-
public function createGenericStatement(string $queryString, int $reflectionFetchType): ?GenericObjectType
76+
public function createGenericStatement(iterable $queryStrings, int $reflectionFetchType): ?Type
7477
{
75-
$queryReflection = new QueryReflection();
76-
$bothType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH);
78+
$genericObjects = [];
7779

78-
if ($bothType) {
79-
$rowTypeInFetchMode = $this->reduceStatementResultType($bothType, $reflectionFetchType);
80+
foreach ($queryStrings as $queryString) {
81+
$queryReflection = new QueryReflection();
82+
$bothType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_BOTH);
8083

81-
return new GenericObjectType(PDOStatement::class, [$rowTypeInFetchMode, $bothType]);
84+
if ($bothType) {
85+
$rowTypeInFetchMode = $this->reduceStatementResultType($bothType, $reflectionFetchType);
86+
87+
$genericObjects[] = new GenericObjectType(PDOStatement::class, [$rowTypeInFetchMode, $bothType]);
88+
}
89+
}
90+
91+
if (\count($genericObjects) > 1) {
92+
return TypeCombinator::union(...$genericObjects);
93+
}
94+
if (1 === \count($genericObjects)) {
95+
return $genericObjects[0];
8296
}
8397

8498
return null;
@@ -87,17 +101,35 @@ public function createGenericStatement(string $queryString, int $reflectionFetch
87101
/**
88102
* @param QueryReflector::FETCH_TYPE* $fetchType
89103
*/
90-
public function getRowType(GenericObjectType $statementType, int $fetchType): ?Type
104+
public function getRowType(Type $statementType, int $fetchType): ?Type
91105
{
92-
$genericTypes = $statementType->getTypes();
106+
if ($statementType instanceof UnionType) {
107+
$rowTypes = [];
93108

94-
if (2 !== \count($genericTypes)) {
95-
return null;
109+
foreach ($statementType->getTypes() as $type) {
110+
$rowType = $this->getRowType($type, $fetchType);
111+
if (null === $rowType) {
112+
return null;
113+
}
114+
$rowTypes[] = $rowType;
115+
}
116+
117+
return TypeCombinator::union(...$rowTypes);
96118
}
97119

98-
$bothType = $genericTypes[1];
120+
if ($statementType instanceof GenericObjectType) {
121+
$genericTypes = $statementType->getTypes();
122+
123+
if (2 !== \count($genericTypes)) {
124+
return null;
125+
}
126+
127+
$bothType = $genericTypes[1];
99128

100-
return $this->reduceStatementResultType($bothType, $fetchType);
129+
return $this->reduceStatementResultType($bothType, $fetchType);
130+
}
131+
132+
return null;
101133
}
102134

103135
/**

src/QueryReflection/QueryReflection.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ public function resolvePreparedQueryStrings(Expr $queryExpr, Type $parameterType
9999
}
100100

101101
/**
102+
* @deprecated use resolvePreparedQueryStrings() instead
103+
*
102104
* @throws UnresolvableQueryException
103105
*/
104106
public function resolvePreparedQueryString(Expr $queryExpr, Type $parameterTypes, Scope $scope): ?string
@@ -141,6 +143,8 @@ public function resolveQueryStrings(Expr $queryExpr, Scope $scope): iterable
141143
}
142144

143145
/**
146+
* @deprecated use resolveQueryStrings() instead
147+
*
144148
* @throws UnresolvableQueryException
145149
*/
146150
public function resolveQueryString(Expr $queryExpr, Scope $scope): ?string

tests/default/DbaInferenceTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function dataFileAsserts(): iterable
4646
}
4747

4848
yield from $this->gatherAssertTypes(__DIR__.'/data/bug254.php');
49+
yield from $this->gatherAssertTypes(__DIR__.'/data/pdo-union-result.php');
4950
}
5051

5152
/**

0 commit comments

Comments
 (0)