From da4933758c18183e367740bfd41c95632c54c6d6 Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Tue, 21 Jul 2026 09:57:07 +0200 Subject: [PATCH 1/3] Add dynamic return type extension for pg_last_notice() Co-Authored-By: Claude Fable 5 --- ...PgLastNoticeDynamicReturnTypeExtension.php | 79 +++++++++++++++++++ .../PHPStan/Analyser/nsrt/pg-last-notice.php | 27 +++++++ 2 files changed, 106 insertions(+) create mode 100644 src/Type/Php/PgLastNoticeDynamicReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/pg-last-notice.php diff --git a/src/Type/Php/PgLastNoticeDynamicReturnTypeExtension.php b/src/Type/Php/PgLastNoticeDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..1daab3787c --- /dev/null +++ b/src/Type/Php/PgLastNoticeDynamicReturnTypeExtension.php @@ -0,0 +1,79 @@ +getName() === 'pg_last_notice'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): Type + { + $args = $functionCall->getArgs(); + if (!isset($args[1])) { + return new StringType(); + } + + $types = []; + foreach ($scope->getType($args[1]->value)->getConstantScalarValues() as $value) { + if ($value === self::NOTICE_LAST) { + $types[] = new StringType(); + } elseif ($value === self::NOTICE_ALL) { + $types[] = $this->createAllNoticesType(); + } elseif ($value === self::NOTICE_CLEAR) { + $types[] = new ConstantBooleanType(true); + } else { + return $this->createUnknownModeType(); + } + } + + if ($types === []) { + return $this->createUnknownModeType(); + } + + return TypeCombinator::union(...$types); + } + + private function createAllNoticesType(): Type + { + return TypeCombinator::intersect( + new ArrayType(new IntegerType(), new StringType()), + new AccessoryArrayListType(), + ); + } + + private function createUnknownModeType(): Type + { + return TypeCombinator::union( + new StringType(), + $this->createAllNoticesType(), + new ConstantBooleanType(true), + ); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/pg-last-notice.php b/tests/PHPStan/Analyser/nsrt/pg-last-notice.php new file mode 100644 index 0000000000..6d618f28d8 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pg-last-notice.php @@ -0,0 +1,27 @@ +', pg_last_notice($connection, PGSQL_NOTICE_ALL)); + assertType('true', pg_last_notice($connection, PGSQL_NOTICE_CLEAR)); + assertType('list|string|true', pg_last_notice($connection, $mode)); + assertType('list|string|true', pg_last_notice($connection, 42)); + + if ($mode === PGSQL_NOTICE_LAST || $mode === PGSQL_NOTICE_ALL) { + assertType('list|string', pg_last_notice($connection, $mode)); + } + } + +} From 45a10949e7c41aa1707d20aac9472bff56341573 Mon Sep 17 00:00:00 2001 From: Jakub Vrana Date: Tue, 21 Jul 2026 10:31:24 +0200 Subject: [PATCH 2/3] Add dynamic return type extension for pg_result_status() Co-Authored-By: Claude Fable 5 --- ...ResultStatusDynamicReturnTypeExtension.php | 56 +++++++++++++++++++ .../Analyser/nsrt/pg-result-status.php | 22 ++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/Type/Php/PgResultStatusDynamicReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/pg-result-status.php diff --git a/src/Type/Php/PgResultStatusDynamicReturnTypeExtension.php b/src/Type/Php/PgResultStatusDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..8dc1326a9f --- /dev/null +++ b/src/Type/Php/PgResultStatusDynamicReturnTypeExtension.php @@ -0,0 +1,56 @@ +getName() === 'pg_result_status'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + if (!isset($args[1])) { + return new IntegerType(); + } + + $types = []; + foreach ($scope->getType($args[1]->value)->getConstantScalarValues() as $value) { + if ($value === self::STATUS_LONG) { + $types[] = new IntegerType(); + } elseif ($value === self::STATUS_STRING) { + $types[] = new StringType(); + } else { + return null; + } + } + + if ($types === []) { + return null; + } + + return TypeCombinator::union(...$types); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/pg-result-status.php b/tests/PHPStan/Analyser/nsrt/pg-result-status.php new file mode 100644 index 0000000000..cd4750974d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pg-result-status.php @@ -0,0 +1,22 @@ + Date: Tue, 21 Jul 2026 10:31:25 +0200 Subject: [PATCH 3/3] Add dynamic return type extension for pg_insert(), pg_update(), pg_delete() and pg_select() Co-Authored-By: Claude Fable 5 --- .../Php/PgDmlDynamicReturnTypeExtension.php | 104 ++++++++++++++++++ tests/PHPStan/Analyser/nsrt/pg-dml.php | 46 ++++++++ 2 files changed, 150 insertions(+) create mode 100644 src/Type/Php/PgDmlDynamicReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/pg-dml.php diff --git a/src/Type/Php/PgDmlDynamicReturnTypeExtension.php b/src/Type/Php/PgDmlDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..769cd0ad39 --- /dev/null +++ b/src/Type/Php/PgDmlDynamicReturnTypeExtension.php @@ -0,0 +1,104 @@ + */ + private array $flagsArgPositions = [ + 'pg_insert' => 3, + 'pg_update' => 4, + 'pg_delete' => 3, + 'pg_select' => 3, + ]; + + public function __construct(private BitwiseFlagHelper $bitwiseFlagAnalyser) + { + } + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return array_key_exists($functionReflection->getName(), $this->flagsArgPositions); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $functionName = $functionReflection->getName(); + $args = $functionCall->getArgs(); + $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( + $scope, + $args, + $functionReflection->getVariants(), + )->getReturnType(); + + $flagsArgPosition = $this->flagsArgPositions[$functionName]; + if (!isset($args[$flagsArgPosition])) { + // default flags are PGSQL_DML_EXEC + return $this->getTypeFromFlags($functionName, TrinaryLogic::createNo(), TrinaryLogic::createYes(), $defaultReturnType); + } + + $flagsExpr = $args[$flagsArgPosition]->value; + + return $this->getTypeFromFlags( + $functionName, + $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsExpr, $scope, 'PGSQL_DML_STRING'), + $this->bitwiseFlagAnalyser->bitwiseOrContainsConstant($flagsExpr, $scope, 'PGSQL_DML_EXEC'), + $defaultReturnType, + ); + } + + private function getTypeFromFlags(string $functionName, TrinaryLogic $containsString, TrinaryLogic $containsExec, Type $defaultReturnType): ?Type + { + if ($functionName === 'pg_insert') { + // with PGSQL_DML_EXEC the result object is returned even when PGSQL_DML_STRING is also set + if ($containsExec->yes()) { + return TypeCombinator::remove( + TypeCombinator::remove($defaultReturnType, new StringType()), + new ConstantBooleanType(true), + ); + } + if (!$containsExec->no()) { + return null; + } + if ($containsString->yes()) { + return TypeCombinator::union(new StringType(), new ConstantBooleanType(false)); + } + if ($containsString->no()) { + return new BooleanType(); + } + + return null; + } + + if ($containsString->yes()) { + return TypeCombinator::union(new StringType(), new ConstantBooleanType(false)); + } + if (!$containsString->no()) { + return null; + } + + return TypeCombinator::remove($defaultReturnType, new StringType()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/pg-dml.php b/tests/PHPStan/Analyser/nsrt/pg-dml.php new file mode 100644 index 0000000000..8fb25d2468 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/pg-dml.php @@ -0,0 +1,46 @@ +|false', pg_select($connection, 'table', [])); + assertType('array|false', pg_select($connection, 'table', [], PGSQL_DML_EXEC)); + assertType('string|false', pg_select($connection, 'table', [], PGSQL_DML_STRING)); + assertType('array|string|false', pg_select($connection, 'table', [], $flags)); + } + +}