Skip to content

Commit 631b9d9

Browse files
phpstan-botclaude
andcommitted
Support named arguments for PDOStatement::fetchAll() overloaded parameters
PHP 8.4 accepts named arguments like `column:`, `class:`, and `constructorArgs:` for PDOStatement::fetchAll() despite its native signature using `mixed ...$args`. This commit: 1. Renames function map parameters to match PHP's actual named params (`mode`, `column`, `class`, `constructorArgs`, `callback`) 2. Updates Php8SignatureMapProvider to include function map parameters past the native variadic position in named argument variants, but only when all preceding param names align with native (preventing false positives for methods like PDO::query that don't support variadic-slot named params) 3. Enhances ParametersAcceptorSelector to perform name-based type matching when selecting among named argument variants, correctly rejecting variants whose required params don't match the call's named arguments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 11dd32e commit 631b9d9

5 files changed

Lines changed: 47 additions & 18 deletions

File tree

resources/functionMap.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7833,10 +7833,10 @@
78337833
'PDOStatement::errorInfo' => ['array'],
78347834
'PDOStatement::execute' => ['bool', 'bound_input_params='=>'?array'],
78357835
'PDOStatement::fetch' => ['mixed', 'how='=>'int', 'orientation='=>'int', 'offset='=>'int'],
7836-
'PDOStatement::fetchAll' => ['array|false', 'how='=>'int'],
7837-
'PDOStatement::fetchAll\'1' => ['array|false', 'fetch_column'=>'int', 'colno'=>'int'],
7838-
'PDOStatement::fetchAll\'2' => ['array|false', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs='=>'?array'],
7839-
'PDOStatement::fetchAll\'3' => ['array|false', 'fetch_func'=>'int', 'callback'=>'callable'],
7836+
'PDOStatement::fetchAll' => ['array|false', 'mode='=>'int'],
7837+
'PDOStatement::fetchAll\'1' => ['array|false', 'mode'=>'int', 'column'=>'int'],
7838+
'PDOStatement::fetchAll\'2' => ['array|false', 'mode'=>'int', 'class'=>'string', 'constructorArgs='=>'?array'],
7839+
'PDOStatement::fetchAll\'3' => ['array|false', 'mode'=>'int', 'callback'=>'callable'],
78407840
'PDOStatement::fetchColumn' => ['string|null|false|int', 'column_number='=>'int'],
78417841
'PDOStatement::fetchObject' => ['mixed', 'class_name='=>'string', 'ctor_args='=>'?array'],
78427842
'PDOStatement::getAttribute' => ['mixed', 'attribute'=>'int'],

resources/functionMap_php80delta.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,10 @@
9797
'opcache_get_configuration' => ['array{directives: array{\'opcache.enable\': bool, \'opcache.enable_cli\': bool, \'opcache.use_cwd\': bool, \'opcache.validate_timestamps\': bool, \'opcache.validate_permission\': bool, \'opcache.validate_root\'?: bool, \'opcache.dups_fix\': bool, \'opcache.revalidate_path\': bool, \'opcache.log_verbosity_level\': int, \'opcache.memory_consumption\': int, \'opcache.interned_strings_buffer\': int, \'opcache.max_accelerated_files\': int, \'opcache.max_wasted_percentage\': float, \'opcache.consistency_checks\': int, \'opcache.force_restart_timeout\': int, \'opcache.revalidate_freq\': int, \'opcache.preferred_memory_model\': string, \'opcache.blacklist_filename\': string, \'opcache.max_file_size\': int, \'opcache.error_log\': string, \'opcache.protect_memory\': bool, \'opcache.save_comments\': bool, \'opcache.record_warnings\': bool, \'opcache.enable_file_override\': bool, \'opcache.optimization_level\': int, \'opcache.lockfile_path\'?: string, \'opcache.mmap_base\'?: string, \'opcache.file_cache\': string, \'opcache.file_cache_only\': bool, \'opcache.file_cache_consistency_checks\': bool, \'opcache.file_cache_fallback\'?: bool, \'opcache.file_update_protection\': int, \'opcache.opt_debug_level\': int, \'opcache.restrict_api\': string, \'opcache.huge_code_pages\'?: bool, \'opcache.preload\': string, \'opcache.preload_user\'?: string, \'opcache.cache_id\'?: string, \'opcache.jit\'?: string, \'opcache.jit_buffer_size\'?: int, \'opcache.jit_debug\'?: int, \'opcache.jit_bisect_limit\'?: int, \'opcache.jit_blacklist_root_trace\'?: int, \'opcache.jit_blacklist_side_trace\'?: int, \'opcache.jit_hot_func\'?: int, \'opcache.jit_hot_loop\'?: int, \'opcache.jit_hot_return\'?: int, \'opcache.jit_hot_side_exit\'?: int, \'opcache.jit_max_exit_counters\'?: int, \'opcache.jit_max_loop_unrolls\'?: int, \'opcache.jit_max_polymorphic_calls\'?: int, \'opcache.jit_max_recursive_calls\'?: int, \'opcache.jit_max_recursive_returns\'?: int, \'opcache.jit_max_root_traces\'?: int, \'opcache.jit_max_side_traces\'?: int, \'opcache.jit_prof_threshold\'?: int}, version: array{version: non-empty-string, opcache_product_name: non-empty-string}, blacklist: list<string>}|false'],
9898
'parse_str' => ['void', 'encoded_string'=>'string', '&w_result'=>'array'],
9999
'password_hash' => ['non-empty-string', 'password'=>'string', 'algo'=>'string|int|null', 'options='=>'array'],
100-
'PDOStatement::fetchAll' => ['array', 'how='=>'int'],
101-
'PDOStatement::fetchAll\'1' => ['array', 'fetch_column'=>'int', 'colno'=>'int'],
102-
'PDOStatement::fetchAll\'2' => ['array', 'fetch_class'=>'int', 'classname'=>'string', 'ctorargs='=>'?array'],
103-
'PDOStatement::fetchAll\'3' => ['array', 'fetch_func'=>'int', 'callback'=>'callable'],
100+
'PDOStatement::fetchAll' => ['array', 'mode='=>'int'],
101+
'PDOStatement::fetchAll\'1' => ['array', 'mode'=>'int', 'column'=>'int'],
102+
'PDOStatement::fetchAll\'2' => ['array', 'mode'=>'int', 'class'=>'string', 'constructorArgs='=>'?array'],
103+
'PDOStatement::fetchAll\'3' => ['array', 'mode'=>'int', 'callback'=>'callable'],
104104
'PhpToken::tokenize' => ['list<PhpToken>', 'code'=>'string', 'flags='=>'int'],
105105
'PhpToken::is' => ['bool', 'kind'=>'string|int|string[]|int[]'],
106106
'PhpToken::isIgnorable' => ['bool'],

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ public static function selectFromTypes(
603603
}
604604

605605
$typesCount = count($types);
606+
$hasNamedTypes = count($types) > 0 && !array_is_list($types);
606607
$acceptableAcceptors = [];
607608

608609
foreach ($parametersAcceptors as $parametersAcceptor) {
@@ -650,11 +651,18 @@ public static function selectFromTypes(
650651
$acceptableAcceptor = GenericParametersAcceptorResolver::resolve($types, $acceptableAcceptor);
651652
foreach ($acceptableAcceptor->getParameters() as $i => $parameter) {
652653
if (!isset($types[$i])) {
653-
if (!$unpack || count($types) <= 0) {
654+
if ($hasNamedTypes && isset($types[$parameter->getName()])) {
655+
$type = $types[$parameter->getName()];
656+
} elseif ($hasNamedTypes && $parameter->isOptional()) {
657+
continue;
658+
} elseif ($hasNamedTypes) {
659+
$isSuperType = TrinaryLogic::createNo();
660+
break;
661+
} elseif ($unpack && count($types) > 0) {
662+
$type = $types[array_key_last($types)];
663+
} else {
654664
break;
655665
}
656-
657-
$type = $types[array_key_last($types)];
658666
} else {
659667
$type = $types[$i];
660668
}

src/Reflection/SignatureMap/Php8SignatureMapProvider.php

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,29 +274,45 @@ private function getMergedSignatures(FunctionSignature $nativeSignature, array $
274274
$nativeParam = $nativeParams[$i] ?? null;
275275
$allParamNamesMatchNative = $allParamNamesMatchNative && $nativeParam !== null && $functionParam->getName() === $nativeParam->getName();
276276
$hasMiddleVariadicParam = $hasMiddleVariadicParam || $isPrevParamVariadic;
277-
$isPrevParamVariadic = $functionParam->isVariadic() || (
278-
$nativeParam !== null
279-
? $nativeParam->isVariadic()
280-
: false
281-
);
277+
$isPrevParamVariadic = $functionParam->isVariadic();
282278
}
283279

284280
if ($hasMiddleVariadicParam) {
285281
continue;
286282
}
287283

288284
$parameters = [];
285+
$allPrecedingNamesMatch = true;
289286
foreach ($functionMapSignature->getParameters() as $i => $functionParam) {
290287
if (!array_key_exists($i, $nativeParams)) {
291288
continue 2;
292289
}
293290

294-
// it seems that variadic parameters cannot be named in native functions/methods.
295291
$nativeParam = $nativeParams[$i];
296292
if ($nativeParam->isVariadic()) {
293+
if ($allPrecedingNamesMatch) {
294+
// Include remaining function map params with their own names.
295+
// Some internal functions (e.g. PDOStatement::fetchAll) accept
296+
// specific named parameters for their variadic slot when the
297+
// function map aligns param names with native up to that point.
298+
foreach (array_slice($functionMapSignature->getParameters(), $i) as $remainingParam) {
299+
$parameters[] = new ParameterSignature(
300+
$remainingParam->getName(),
301+
$remainingParam->isOptional(),
302+
$remainingParam->getType(),
303+
$remainingParam->getNativeType(),
304+
$remainingParam->passedByReference(),
305+
$remainingParam->isVariadic(),
306+
$remainingParam->getDefaultValue(),
307+
$remainingParam->getOutType(),
308+
);
309+
}
310+
}
297311
break;
298312
}
299313

314+
$allPrecedingNamesMatch = $allPrecedingNamesMatch && $functionParam->getName() === $nativeParam->getName();
315+
300316
$parameters[] = new ParameterSignature(
301317
$nativeParam->getName(),
302318
$functionParam->isOptional(),

tests/PHPStan/Rules/Methods/data/bug-5509.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ public function test(\PDOStatement $stmt): void
2626
// With just mode - should not error
2727
$stmt->fetchAll(\PDO::FETCH_ASSOC);
2828

29-
// Named arguments
29+
// Named arguments - mode only
3030
$stmt->fetchAll(mode: \PDO::FETCH_DEFAULT);
3131
$stmt->fetchAll(mode: \PDO::FETCH_COLUMN);
3232
$stmt->fetchAll(mode: \PDO::FETCH_CLASS);
33+
34+
// Named arguments - all parameters
35+
$stmt->fetchAll(mode: \PDO::FETCH_COLUMN, column: 0);
36+
$stmt->fetchAll(mode: \PDO::FETCH_CLASS, class: \stdClass::class);
37+
$stmt->fetchAll(mode: \PDO::FETCH_CLASS, class: \stdClass::class, constructorArgs: [new \stdClass]);
3338
}
3439
}

0 commit comments

Comments
 (0)