Skip to content

Commit 18d532f

Browse files
vranaclaude
andcommitted
Add dynamic return type extension for pg_insert(), pg_update(), pg_delete() and pg_select()
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2dade7d commit 18d532f

5 files changed

Lines changed: 220 additions & 4 deletions

File tree

‎resources/functionMap.php‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7792,7 +7792,7 @@
77927792
'pg_copy_from' => ['bool', 'connection'=>'resource', 'table_name'=>'string', 'rows'=>'array', 'delimiter='=>'string', 'null_as='=>'string'],
77937793
'pg_copy_to' => ['array|false', 'connection'=>'resource', 'table_name'=>'string', 'delimiter='=>'string', 'null_as='=>'string'],
77947794
'pg_dbname' => ['string', 'connection='=>'resource'],
7795-
'pg_delete' => ['mixed', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int'],
7795+
'pg_delete' => ['string|bool', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int'],
77967796
'pg_end_copy' => ['bool', 'connection='=>'resource'],
77977797
'pg_escape_bytea' => ['string', 'connection'=>'resource', 'data'=>'string'],
77987798
'pg_escape_bytea\'1' => ['string', 'data'=>'string'],
@@ -7829,7 +7829,7 @@
78297829
'pg_get_pid' => ['int|false', 'connection'=>'resource'],
78307830
'pg_get_result' => ['resource|false', 'connection='=>'resource'],
78317831
'pg_host' => ['string', 'connection='=>'resource'],
7832-
'pg_insert' => ['mixed', 'db'=>'resource', 'table'=>'string', 'values'=>'array', 'options='=>'int'],
7832+
'pg_insert' => ['resource|string|bool', 'db'=>'resource', 'table'=>'string', 'values'=>'array', 'options='=>'int'],
78337833
'pg_last_error' => ['string', 'connection='=>'resource', 'operation='=>'int'],
78347834
'pg_last_notice' => ['string', 'connection'=>'resource', 'option='=>'int'],
78357835
'pg_last_oid' => ['string|false', 'result'=>'resource'],
@@ -7868,7 +7868,7 @@
78687868
'pg_result_error_field' => ['string|false|null', 'result'=>'resource', 'fieldcode'=>'int'],
78697869
'pg_result_seek' => ['bool', 'result'=>'resource', 'offset'=>'int'],
78707870
'pg_result_status' => ['mixed', 'result'=>'resource', 'result_type='=>'int'],
7871-
'pg_select' => ['mixed', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int', 'result_type='=>'int'],
7871+
'pg_select' => ['array<int, array>|string|false', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int', 'result_type='=>'int'],
78727872
'pg_send_execute' => ['bool', 'connection'=>'resource', 'stmtname'=>'string', 'params'=>'array'],
78737873
'pg_send_prepare' => ['bool', 'connection'=>'resource', 'stmtname'=>'string', 'query'=>'string'],
78747874
'pg_send_query' => ['bool', 'connection'=>'resource', 'query'=>'string'],
@@ -7885,7 +7885,7 @@
78857885
'pg_unescape_bytea' => ['string', 'data'=>'string'],
78867886
'pg_untrace' => ['bool', 'connection='=>'resource'],
78877887
'pg_untrace\'1' => ['bool'],
7888-
'pg_update' => ['mixed', 'db'=>'resource', 'table'=>'string', 'fields'=>'array', 'ids'=>'array', 'options='=>'int'],
7888+
'pg_update' => ['string|bool', 'db'=>'resource', 'table'=>'string', 'fields'=>'array', 'ids'=>'array', 'options='=>'int'],
78897889
'pg_version' => ['array', 'connection='=>'resource'],
78907890
'Phar::__construct' => ['void', 'fname'=>'string', 'flags='=>'int', 'alias='=>'string'],
78917891
'Phar::addEmptyDir' => ['', 'dirname'=>'string'],

‎resources/functionMap_php81delta.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
'UnitEnum::cases' => ['list<static>'],
3131
],
3232
'old' => [
33+
'pg_delete' => ['string|bool', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int'],
3334
'pg_escape_bytea' => ['string', 'connection'=>'resource', 'data'=>'string'],
3435
'pg_escape_bytea\'1' => ['string', 'data'=>'string'],
3536
'pg_escape_identifier' => ['string|false', 'connection'=>'resource', 'data'=>'string'],
@@ -48,6 +49,7 @@
4849
'pg_field_is_null\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'],
4950
'pg_field_prtlen' => ['int|false', 'result'=>'', 'field_name_or_number'=>''],
5051
'pg_field_prtlen\'1' => ['int', 'result'=>'', 'row'=>'int', 'field_name_or_number'=>'string|int'],
52+
'pg_insert' => ['resource|string|bool', 'db'=>'resource', 'table'=>'string', 'values'=>'array', 'options='=>'int'],
5153
'pg_lo_export' => ['bool', 'connection'=>'resource', 'oid'=>'int', 'filename'=>'string'],
5254
'pg_lo_export\'1' => ['bool', 'oid'=>'int', 'pathname'=>'string'],
5355
'pg_lo_import' => ['int|string|false', 'connection'=>'resource', 'pathname'=>'string', 'oid'=>''],
@@ -62,6 +64,7 @@
6264
'pg_query\'1' => ['resource|false', 'query'=>'string'],
6365
'pg_query_params' => ['resource|false', 'connection'=>'resource', 'query'=>'string', 'params'=>'array'],
6466
'pg_query_params\'1' => ['resource|false', 'query'=>'string', 'params'=>'array'],
67+
'pg_select' => ['array<int, array>|string|false', 'db'=>'resource', 'table'=>'string', 'ids'=>'array', 'options='=>'int', 'result_type='=>'int'],
6568
'pg_set_client_encoding' => ['int', 'connection'=>'resource', 'encoding'=>'string'],
6669
'pg_set_client_encoding\'1' => ['int', 'encoding'=>'string'],
6770
'pg_set_error_verbosity' => ['int|false', 'connection'=>'resource', 'verbosity'=>'int'],
@@ -70,5 +73,6 @@
7073
'pg_tty\'1' => ['string'],
7174
'pg_untrace' => ['bool', 'connection='=>'resource'],
7275
'pg_untrace\'1' => ['bool'],
76+
'pg_update' => ['string|bool', 'db'=>'resource', 'table'=>'string', 'fields'=>'array', 'ids'=>'array', 'options='=>'int'],
7377
]
7478
];
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\TrinaryLogic;
11+
use PHPStan\Type\BitwiseFlagHelper;
12+
use PHPStan\Type\BooleanType;
13+
use PHPStan\Type\Constant\ConstantBooleanType;
14+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
15+
use PHPStan\Type\StringType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
18+
use PHPStan\Type\UnionType;
19+
use function array_key_exists;
20+
21+
#[AutowiredService]
22+
final class PgDmlDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
23+
{
24+
25+
/** @var array<string, int> */
26+
private array $flagsArgPositions = [
27+
'pg_insert' => 3,
28+
'pg_update' => 4,
29+
'pg_delete' => 3,
30+
'pg_select' => 3,
31+
];
32+
33+
public function __construct(private BitwiseFlagHelper $bitwiseFlagAnalyser)
34+
{
35+
}
36+
37+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
38+
{
39+
return array_key_exists($functionReflection->getName(), $this->flagsArgPositions);
40+
}
41+
42+
public function getTypeFromFunctionCall(
43+
FunctionReflection $functionReflection,
44+
FuncCall $functionCall,
45+
Scope $scope,
46+
): ?Type
47+
{
48+
$functionName = $functionReflection->getName();
49+
$args = $functionCall->getArgs();
50+
$getDefaultReturnType = static fn (): Type => ParametersAcceptorSelector::selectFromArgs(
51+
$scope,
52+
$args,
53+
$functionReflection->getVariants(),
54+
)->getReturnType();
55+
56+
$flagsArgPosition = $this->flagsArgPositions[$functionName];
57+
if (!isset($args[$flagsArgPosition])) {
58+
// default flags are PGSQL_DML_EXEC
59+
return $this->getTypeFromFlags($functionName, TrinaryLogic::createNo(), TrinaryLogic::createYes(), $getDefaultReturnType);
60+
}
61+
62+
$flagsExpr = $args[$flagsArgPosition]->value;
63+
64+
return $this->getTypeFromFlags(
65+
$functionName,
66+
$this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsExpr, $scope, 'PGSQL_DML_STRING'),
67+
$this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsExpr, $scope, 'PGSQL_DML_EXEC'),
68+
$getDefaultReturnType,
69+
);
70+
}
71+
72+
/**
73+
* @param callable(): Type $getDefaultReturnType
74+
*/
75+
private function getTypeFromFlags(string $functionName, TrinaryLogic $containsString, TrinaryLogic $containsExec, callable $getDefaultReturnType): ?Type
76+
{
77+
if ($functionName === 'pg_insert') {
78+
// with PGSQL_DML_EXEC the result object is returned even when PGSQL_DML_STRING is also set
79+
if ($containsExec->yes()) {
80+
return TypeCombinator::remove(
81+
$getDefaultReturnType(),
82+
new UnionType([new StringType(), new ConstantBooleanType(true)]),
83+
);
84+
}
85+
if (!$containsExec->no()) {
86+
return null;
87+
}
88+
if ($containsString->yes()) {
89+
return new UnionType([new StringType(), new ConstantBooleanType(false)]);
90+
}
91+
if ($containsString->no()) {
92+
return new BooleanType();
93+
}
94+
95+
return null;
96+
}
97+
98+
if ($containsString->yes()) {
99+
return new UnionType([new StringType(), new ConstantBooleanType(false)]);
100+
}
101+
if (!$containsString->no()) {
102+
return null;
103+
}
104+
105+
return TypeCombinator::remove($getDefaultReturnType(), new StringType());
106+
}
107+
108+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php // lint < 8.1
2+
3+
namespace PgDmlResource;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param resource $connection
12+
*/
13+
public function doInsert($connection, int $flags): void
14+
{
15+
assertType('resource|false', pg_insert($connection, 'table', []));
16+
assertType('resource|false', pg_insert($connection, 'table', [], PGSQL_DML_EXEC));
17+
assertType('resource|false', pg_insert($connection, 'table', [], PGSQL_DML_EXEC | PGSQL_DML_STRING));
18+
assertType('string|false', pg_insert($connection, 'table', [], PGSQL_DML_STRING));
19+
assertType('string|false', pg_insert($connection, 'table', [], PGSQL_DML_NO_CONV | PGSQL_DML_STRING));
20+
assertType('bool', pg_insert($connection, 'table', [], PGSQL_DML_ASYNC));
21+
assertType('bool|resource|string', pg_insert($connection, 'table', [], $flags));
22+
}
23+
24+
/**
25+
* @param resource $connection
26+
*/
27+
public function doUpdate($connection, int $flags): void
28+
{
29+
assertType('bool', pg_update($connection, 'table', [], []));
30+
assertType('bool', pg_update($connection, 'table', [], [], PGSQL_DML_EXEC));
31+
assertType('string|false', pg_update($connection, 'table', [], [], PGSQL_DML_STRING));
32+
assertType('string|false', pg_update($connection, 'table', [], [], PGSQL_DML_EXEC | PGSQL_DML_STRING));
33+
assertType('bool|string', pg_update($connection, 'table', [], [], $flags));
34+
}
35+
36+
/**
37+
* @param resource $connection
38+
*/
39+
public function doDelete($connection, int $flags): void
40+
{
41+
assertType('bool', pg_delete($connection, 'table', []));
42+
assertType('bool', pg_delete($connection, 'table', [], PGSQL_DML_EXEC));
43+
assertType('string|false', pg_delete($connection, 'table', [], PGSQL_DML_STRING));
44+
assertType('bool|string', pg_delete($connection, 'table', [], $flags));
45+
}
46+
47+
/**
48+
* @param resource $connection
49+
*/
50+
public function doSelect($connection, int $flags): void
51+
{
52+
assertType('array<int, array>|false', pg_select($connection, 'table', []));
53+
assertType('array<int, array>|false', pg_select($connection, 'table', [], PGSQL_DML_EXEC));
54+
assertType('string|false', pg_select($connection, 'table', [], PGSQL_DML_STRING));
55+
assertType('array<int, array>|string|false', pg_select($connection, 'table', [], $flags));
56+
}
57+
58+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php // lint >= 8.1
2+
3+
namespace PgDml;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function doInsert(\PgSql\Connection $connection, int $flags): void
11+
{
12+
assertType('PgSql\Result|false', pg_insert($connection, 'table', []));
13+
assertType('PgSql\Result|false', pg_insert($connection, 'table', [], PGSQL_DML_EXEC));
14+
assertType('PgSql\Result|false', pg_insert($connection, 'table', [], PGSQL_DML_EXEC | PGSQL_DML_STRING));
15+
assertType('string|false', pg_insert($connection, 'table', [], PGSQL_DML_STRING));
16+
assertType('string|false', pg_insert($connection, 'table', [], PGSQL_DML_NO_CONV | PGSQL_DML_STRING));
17+
assertType('bool', pg_insert($connection, 'table', [], PGSQL_DML_ASYNC));
18+
assertType('bool|PgSql\Result|string', pg_insert($connection, 'table', [], $flags));
19+
}
20+
21+
public function doUpdate(\PgSql\Connection $connection, int $flags): void
22+
{
23+
assertType('bool', pg_update($connection, 'table', [], []));
24+
assertType('bool', pg_update($connection, 'table', [], [], PGSQL_DML_EXEC));
25+
assertType('string|false', pg_update($connection, 'table', [], [], PGSQL_DML_STRING));
26+
assertType('string|false', pg_update($connection, 'table', [], [], PGSQL_DML_EXEC | PGSQL_DML_STRING));
27+
assertType('bool|string', pg_update($connection, 'table', [], [], $flags));
28+
}
29+
30+
public function doDelete(\PgSql\Connection $connection, int $flags): void
31+
{
32+
assertType('bool', pg_delete($connection, 'table', []));
33+
assertType('bool', pg_delete($connection, 'table', [], PGSQL_DML_EXEC));
34+
assertType('string|false', pg_delete($connection, 'table', [], PGSQL_DML_STRING));
35+
assertType('bool|string', pg_delete($connection, 'table', [], $flags));
36+
}
37+
38+
public function doSelect(\PgSql\Connection $connection, int $flags): void
39+
{
40+
assertType('array<int, array>|false', pg_select($connection, 'table', []));
41+
assertType('array<int, array>|false', pg_select($connection, 'table', [], PGSQL_DML_EXEC));
42+
assertType('string|false', pg_select($connection, 'table', [], PGSQL_DML_STRING));
43+
assertType('array<int, array>|string|false', pg_select($connection, 'table', [], $flags));
44+
}
45+
46+
}

0 commit comments

Comments
 (0)