Skip to content

Commit

Permalink
pass smarter PlaceholderTypeProvider from mysqli::execute_query
Browse files Browse the repository at this point in the history
  • Loading branch information
schlndh committed Jun 15, 2024
1 parent 155b504 commit aefdfa7
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 5 deletions.
6 changes: 5 additions & 1 deletion src/Analyser/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
use MariaStan\Analyser\Exception\AnalyserException;
use MariaStan\Analyser\PlaceholderTypeProvider\MixedPlaceholderTypeProvider;
use MariaStan\Analyser\PlaceholderTypeProvider\PlaceholderTypeProvider;
use MariaStan\Analyser\PlaceholderTypeProvider\VarcharPlaceholderTypeProvider;
use MariaStan\Database\FunctionInfo\FunctionInfoRegistry;
use MariaStan\DbReflection\DbReflection;
use MariaStan\Parser\Exception\ParserException;
use MariaStan\Parser\MariaDbParser;
use MariaStan\PHPStan\Helper\PHPStanTypeVarcharPlaceholderTypeProvider;

use function mb_substr;

Expand All @@ -26,8 +28,10 @@ public function __construct(
/**
* @param ?PlaceholderTypeProvider $placeholderTypeProvider Pass PlaceholderTypeProvider to narrow down placeholder
* types. If you pass null, then all placeholders will get nullable mixed type. If you bind all placeholders as
* strings, you can pass {@see VarcharPlaceholderTypeProvider}.
* strings, you can pass {@see VarcharPlaceholderTypeProvider}, or {@see PHPStanTypeVarcharPlaceholderTypeProvider}
* @throws AnalyserException
* @see VarcharPlaceholderTypeProvider
* @see PHPStanTypeVarcharPlaceholderTypeProvider
*/
public function analyzeQuery(
string $query,
Expand Down
56 changes: 52 additions & 4 deletions src/PHPStan/Helper/MySQLi/PHPStanMySQLiHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
use MariaStan\Analyser\AnalyserError;
use MariaStan\Analyser\AnalyserResult;
use MariaStan\Analyser\Exception\AnalyserException;
use MariaStan\Analyser\PlaceholderTypeProvider\PlaceholderTypeProvider;
use MariaStan\PHPStan\Helper\AnalyserResultPHPStanParams;
use MariaStan\PHPStan\Helper\MariaStanError;
use MariaStan\PHPStan\Helper\MariaStanErrorIdentifiers;
use MariaStan\PHPStan\Helper\PHPStanReturnTypeHelper;
use MariaStan\PHPStan\Helper\PHPStanTypeVarcharPlaceholderTypeProvider;
use PhpParser\Node\Arg;
use PHPStan\Analyser\Scope;
use PHPStan\Type\ArrayType;
Expand All @@ -38,6 +40,7 @@
use function count;
use function implode;
use function ksort;
use function reset;

use const MYSQLI_ASSOC;
use const MYSQLI_BOTH;
Expand All @@ -51,8 +54,10 @@ public function __construct(
) {
}

public function prepare(Type $queryType): QueryPrepareCallResult
{
private function prepareImpl(
Type $queryType,
?PlaceholderTypeProvider $placeholderTypeProvider,
): QueryPrepareCallResult {
$constantStrings = $queryType->getConstantStrings();

if (count($constantStrings) === 0) {
Expand All @@ -73,7 +78,7 @@ public function prepare(Type $queryType): QueryPrepareCallResult

foreach ($constantStrings as $sqlType) {
try {
$analyserResults[] = $this->analyser->analyzeQuery($sqlType->getValue());
$analyserResults[] = $this->analyser->analyzeQuery($sqlType->getValue(), $placeholderTypeProvider);
} catch (AnalyserException $e) {
$errors[] = new MariaStanError(
$e->getMessage(),
Expand All @@ -98,6 +103,11 @@ public function prepare(Type $queryType): QueryPrepareCallResult
return new QueryPrepareCallResult($errors, $analyserResults);
}

public function prepare(Type $queryType): QueryPrepareCallResult
{
return $this->prepareImpl($queryType, null);
}

public function query(Type $queryType): QueryPrepareCallResult
{
$result = $this->prepare($queryType);
Expand Down Expand Up @@ -160,7 +170,8 @@ public function execute(AnalyserResultPHPStanParams $params, array $executeParam
/** @param array<array<Type>> $executeParamTypes possible params */
public function executeQuery(Type $queryType, array $executeParamTypes): QueryPrepareCallResult
{
$prepareResult = $this->prepare($queryType);
$placeholderTypeProvider = $this->createPlaceholderTypeProviderFromExecuteParamTypes($executeParamTypes);
$prepareResult = $this->prepareImpl($queryType, $placeholderTypeProvider);
$phpstanParams = $this->phpstanHelper
->createPhpstanParamsFromMultipleAnalyserResults($prepareResult->analyserResults);

Expand Down Expand Up @@ -298,4 +309,41 @@ public function getExecuteParamTypesFromArgument(Scope $scope, ?Arg $arg): array

return $this->getExecuteParamTypesFromType($scope->getType($arg->value));
}

/** @param array<array<Type>> $executeParamTypes */
private function createPlaceholderTypeProviderFromExecuteParamTypes(
array $executeParamTypes,
): ?PlaceholderTypeProvider {
if (count($executeParamTypes) === 0) {
return null;
}

$paramsByCount = [];

foreach ($executeParamTypes as $paramTypes) {
$paramsByCount[count($paramTypes)][] = $paramTypes;
}

// TODO: support different param counts. We'll need to match them to the placeholder count in query.
if (count($paramsByCount) !== 1) {
return null;
}

$paramTypes = reset($paramsByCount);
$typesByPosition = [];

foreach ($paramTypes as $types) {
foreach (array_values($types) as $i => $type) {
// The placeholders are indexed starting from 1
$typesByPosition[$i + 1][] = $type;
}
}

$unionedTypeByPosition = array_map(
static fn (array $positionTypes) => TypeCombinator::union(...$positionTypes),
$typesByPosition,
);

return new PHPStanTypeVarcharPlaceholderTypeProvider($unionedTypeByPosition);
}
}
61 changes: 61 additions & 0 deletions src/PHPStan/Helper/PHPStanTypeVarcharPlaceholderTypeProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace MariaStan\PHPStan\Helper;

use MariaStan\Analyser\PlaceholderTypeProvider\PlaceholderTypeProvider;
use MariaStan\Ast\Expr\Placeholder;
use MariaStan\Schema\DbType\DbType;
use MariaStan\Schema\DbType\EnumType;
use MariaStan\Schema\DbType\NullType;
use MariaStan\Schema\DbType\VarcharType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\TypeCombinator;

use function array_map;
use function array_unique;
use function count;

class PHPStanTypeVarcharPlaceholderTypeProvider implements PlaceholderTypeProvider
{
/** @param array<int|string, \PHPStan\Type\Type> $placeholderPHPStanTypes */
public function __construct(private readonly array $placeholderPHPStanTypes)
{
}

public function getPlaceholderDbType(Placeholder $placeholder): DbType
{
$phpstanType = $this->placeholderPHPStanTypes[$placeholder->name] ?? null;

if ($phpstanType === null) {
return new VarcharType();
}

if ($phpstanType->isNull()->yes()) {
return new NullType();
}

$phpstanType = TypeCombinator::removeNull($phpstanType);
$constantStrings = $phpstanType->toString()->getConstantStrings();

if (count($constantStrings) === 0) {
return new VarcharType();
}

$values = array_unique(array_map(static fn (ConstantStringType $t) => $t->getValue(), $constantStrings));

return new EnumType($values);
}

public function isPlaceholderNullable(Placeholder $placeholder): bool
{
$phpstanType = $this->placeholderPHPStanTypes[$placeholder->name] ?? null;

if ($phpstanType === null) {
return true;
}

return ! $phpstanType->isNull()->no();
}
}
31 changes: 31 additions & 0 deletions tests/PHPStan/Type/MySQLi/data/MySQLiTypeInferenceDataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use function in_array;
use function is_string;
use function PHPStan\Testing\assertType;
use function rand;

use const MYSQLI_ASSOC;
use const MYSQLI_BOTH;
Expand Down Expand Up @@ -1074,6 +1075,36 @@ public function testExecuteQuery(): void
}
}

// This is not executed.
public function checkExecuteQueryPlaceholderTypeProvider(mysqli $db): void
{
$row = $db->execute_query('SELECT ? val', [rand() ? 1 : 2])->fetch_assoc();

if (function_exists('assertType')) {
assertType("'1'|'2'", $row['val']);
}

$row = $db->execute_query('SELECT ? val', rand() ? [1] : ['a'])->fetch_assoc();

if (function_exists('assertType')) {
assertType("'1'|'a'", $row['val']);
}

$row = $db->execute_query('SELECT ? val', rand() ? [1] : [null])->fetch_assoc();

if (function_exists('assertType')) {
assertType("'1'|null", $row['val']);
}

$row = $db->execute_query('SELECT ? val', [null])->fetch_assoc();

if (function_exists('assertType')) {
assertType("null", $row['val']);
}

$this->expectNotToPerformAssertions();
}

/** @param string|array<string> $allowedTypes */
private function assertGettype(string|array $allowedTypes, mixed $value): void
{
Expand Down

0 comments on commit aefdfa7

Please sign in to comment.