Skip to content

Commit

Permalink
Dibi type inference
Browse files Browse the repository at this point in the history
Co-authored-by: Jakub Vojacek <jakub@motv.eu>
Co-authored-by: Markus Staab <maggus.staab@googlemail.com>
Co-authored-by: jakubvojacek <jakubvojacek@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 17, 2022
1 parent 89474dc commit dc579c0
Show file tree
Hide file tree
Showing 20 changed files with 5,259 additions and 880 deletions.
748 changes: 152 additions & 596 deletions .phpstan-dba-mysqli.cache

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions config/extensions.neon
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,9 @@ services:
class: staabm\PHPStanDba\Ast\PreviousConnectingVisitor
tags:
- phpstan.parser.richParserNodeVisitor

-
class: staabm\PHPStanDba\Extensions\DibiConnectionFetchDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension

16 changes: 11 additions & 5 deletions config/rules.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
services:
-
class: staabm\PHPStanDba\Rules\SyntaxErrorInDibiPreparedStatementMethodRule
tags: [phpstan.rules.rule]
arguments:
classMethods:
- 'Dibi\Connection::fetch'
- 'Dibi\Connection::fetchSingle'
- 'Dibi\Connection::query'
- 'Dibi\Connection::fetchAll'
- 'Dibi\Connection::fetchPairs'

-
class: staabm\PHPStanDba\Rules\SyntaxErrorInPreparedStatementMethodRule
tags: [phpstan.rules.rule]
Expand All @@ -24,11 +35,6 @@ services:
- 'Doctrine\DBAL\Connection::iterateAssociativeIndexed'
- 'Doctrine\DBAL\Connection::iterateColumn'
- 'Doctrine\DBAL\Connection::executeUpdate' # deprecated in doctrine
- 'Dibi\Connection::fetch'
- 'Dibi\Connection::fetchSingle'
- 'Dibi\Connection::query'
- 'Dibi\Connection::fetchAll'
- 'Dibi\Connection::fetchPairs'

-
class: staabm\PHPStanDba\Rules\PdoStatementExecuteMethodRule
Expand Down
29 changes: 29 additions & 0 deletions src/DibiReflection/DibiReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace staabm\PHPStanDba\DibiReflection;

final class DibiReflection
{
public function rewriteQuery(string $queryString): ?string
{
$queryString = str_replace('%in', '(1)', $queryString);
$queryString = str_replace('%lmt', 'LIMIT 1', $queryString);
$queryString = str_replace('%ofs', ', 1', $queryString);
$queryString = preg_replace('#%(i|s)#', "'1'", $queryString) ?? '';
$queryString = preg_replace('#%(t|d)#', '"2000-1-1"', $queryString) ?? '';
$queryString = preg_replace('#%(and|or)#', '(1 = 1)', $queryString) ?? '';
$queryString = preg_replace('#%~?like~?#', '"%1%"', $queryString) ?? '';

if (strpos($queryString, '%n') > 0) {
$queryString = null;
} elseif (strpos($queryString, '%ex') > 0) {
$queryString = null;
} elseif (0 !== preg_match('#^\s*(START|ROLLBACK|SET|SAVEPOINT|SHOW)#i', $queryString)) {
$queryString = null;
}

return $queryString;
}
}
126 changes: 126 additions & 0 deletions src/Extensions/DibiConnectionFetchDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace staabm\PHPStanDba\Extensions;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use staabm\PHPStanDba\DibiReflection\DibiReflection;
use staabm\PHPStanDba\QueryReflection\QueryReflection;
use staabm\PHPStanDba\QueryReflection\QueryReflector;
use staabm\PHPStanDba\UnresolvableQueryException;

final class DibiConnectionFetchDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return \Dibi\Connection::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return \in_array($methodReflection->getName(), [
'fetch',
'fetchAll',
'fetchPairs',
'fetchSingle',
], true);
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
{
$args = $methodCall->getArgs();

if (\count($args) < 1) {
return null;
}

if ($scope->getType($args[0]->value) instanceof MixedType) {
return null;
}

try {
return $this->inferType($methodReflection, $args[0]->value, $scope);
} catch (UnresolvableQueryException $exception) {
// simulation not possible.. use default value
}

return null;
}

private function inferType(MethodReflection $methodReflection, Expr $queryExpr, Scope $scope): ?Type
{
$queryReflection = new QueryReflection();
$queryStrings = $queryReflection->resolveQueryStrings($queryExpr, $scope);

return $this->createFetchType($queryStrings, $methodReflection);
}

/**
* @param iterable<string> $queryStrings
*/
private function createFetchType(iterable $queryStrings, MethodReflection $methodReflection): ?Type
{
$queryReflection = new QueryReflection();
$dibiReflection = new DibiReflection();

$fetchTypes = [];
foreach ($queryStrings as $queryString) {
$queryString = $dibiReflection->rewriteQuery($queryString);
if (null === $queryString) {
continue;
}

$resultType = $queryReflection->getResultType($queryString, QueryReflector::FETCH_TYPE_ASSOC);

if (null === $resultType) {
return null;
}

$fetchResultType = $this->reduceResultType($methodReflection, $resultType);
if (null === $fetchResultType) {
return null;
}

$fetchTypes[] = $fetchResultType;
}

if (\count($fetchTypes) > 1) {
return TypeCombinator::union(...$fetchTypes);
}
if (1 === \count($fetchTypes)) {
return $fetchTypes[0];
}

return null;
}

private function reduceResultType(MethodReflection $methodReflection, Type $resultType): ?Type
{
$methodName = $methodReflection->getName();

if ('fetch' === $methodName) {
return new UnionType([new NullType(), $resultType]);
} elseif ('fetchAll' === $methodName) {
return new ArrayType(new IntegerType(), $resultType);
} elseif ('fetchPairs' === $methodName && $resultType instanceof ConstantArrayType && 2 === \count($resultType->getValueTypes())) {
return new ArrayType($resultType->getValueTypes()[0], $resultType->getValueTypes()[1]);
} elseif ('fetchSingle' === $methodName && $resultType instanceof ConstantArrayType && 1 === \count($resultType->getValueTypes())) {
return new UnionType([new NullType(), $resultType->getValueTypes()[0]]);
}

return null;
}
}
Loading

0 comments on commit dc579c0

Please sign in to comment.