From 4ab7169cfa6b60ad49016d73769e2319dd817444 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:51:32 +0000 Subject: [PATCH 1/7] Report `array_column()` reading a property absent from the array's object elements - Add `PHPStan\Rules\Functions\ArrayColumnRule` that checks `array_column()` calls whose source array holds objects: when the `$column_key` (and `$index_key`) argument is a constant string naming a property that none of the element classes declare, it reports the call instead of silently inferring an empty array. - Cover both parallel arguments: `#2 $column_key` and `#3 $index_key` are checked with the same logic. - Stay conservative to avoid false positives: only objects are checked (array elements use offset access, scalars are skipped), enums are skipped (their `name`/`value` are always present), and classes with `__isset` + `__get` magic or that still allow dynamic properties (e.g. on PHP < 8.2) are skipped because the property might exist at runtime. This matches `AccessPropertiesRule`, which likewise reports undefined properties on non-final/abstract/interface types. - `array_column()` reads object properties even on `ArrayAccess` elements (never the offset), so the check keys off `Type::isObject()` rather than offset accessibility. - Gate the rule behind the new `arrayColumnObjectArrays` bleeding-edge feature toggle and register it on level 5 next to the other array-function rules. - Add `ArrayColumnRuleTest` plus a fixture covering non-final/final classes, the index argument, magic/dynamic/enum/array/union element types and the original issue reproducer. --- conf/bleedingEdge.neon | 1 + conf/config.level5.neon | 7 + conf/config.neon | 1 + conf/parametersSchema.neon | 1 + src/Rules/Functions/ArrayColumnRule.php | 162 ++++++++++++++++++ .../Rules/Functions/ArrayColumnRuleTest.php | 61 +++++++ .../Rules/Functions/data/array-column.php | 98 +++++++++++ 7 files changed, 331 insertions(+) create mode 100644 src/Rules/Functions/ArrayColumnRule.php create mode 100644 tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/array-column.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 5f4e2d114a..3e8b7ad2fa 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -21,3 +21,4 @@ parameters: reportMethodPurityOverride: true checkDynamicConstantNameValues: true unusedLabel: true + arrayColumnObjectArrays: true diff --git a/conf/config.level5.neon b/conf/config.level5.neon index 534d58dde0..aa38361014 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -12,6 +12,8 @@ conditionalTags: phpstan.rules.rule: %featureToggles.checkPrintfParameterTypes% PHPStan\Rules\DateIntervalInstantiationRule: phpstan.rules.rule: %featureToggles.checkDateIntervalConstructor% + PHPStan\Rules\Functions\ArrayColumnRule: + phpstan.rules.rule: %featureToggles.arrayColumnObjectArrays% autowiredAttributeServices: # registers rules with #[RegisteredRule] attribute @@ -26,3 +28,8 @@ services: checkStrictPrintfPlaceholderTypes: %checkStrictPrintfPlaceholderTypes% - class: PHPStan\Rules\DateIntervalInstantiationRule + - + class: PHPStan\Rules\Functions\ArrayColumnRule + arguments: + treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain% + treatPhpDocTypesAsCertainTip: %tips.treatPhpDocTypesAsCertain% diff --git a/conf/config.neon b/conf/config.neon index 0e09a3ed22..9b5d96b56a 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -48,6 +48,7 @@ parameters: reportMethodPurityOverride: false checkDynamicConstantNameValues: false unusedLabel: false + arrayColumnObjectArrays: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index dcd030439b..6a38faced1 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -50,6 +50,7 @@ parametersSchema: reportMethodPurityOverride: bool() checkDynamicConstantNameValues: bool() unusedLabel: bool() + arrayColumnObjectArrays: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Rules/Functions/ArrayColumnRule.php b/src/Rules/Functions/ArrayColumnRule.php new file mode 100644 index 0000000000..29f5cd35a9 --- /dev/null +++ b/src/Rules/Functions/ArrayColumnRule.php @@ -0,0 +1,162 @@ + + */ +final class ArrayColumnRule implements Rule +{ + + public function __construct( + private readonly ReflectionProvider $reflectionProvider, + private readonly bool $treatPhpDocTypesAsCertain, + private readonly bool $treatPhpDocTypesAsCertainTip, + ) + { + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof Node\Name)) { + return []; + } + + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { + return []; + } + + $functionReflection = $this->reflectionProvider->getFunction($node->name, $scope); + if ($functionReflection->getName() !== 'array_column') { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $functionReflection->getVariants(), + $functionReflection->getNamedArgumentsVariants(), + ); + + $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node); + if ($normalizedFuncCall === null) { + return []; + } + + $args = $normalizedFuncCall->getArgs(); + if (count($args) < 2) { + return []; + } + + $arrayArg = $args[0]->value; + $valueType = $scope->getType($arrayArg)->getIterableValueType(); + $nativeValueType = $scope->getNativeType($arrayArg)->getIterableValueType(); + + $errors = []; + foreach ($this->checkColumn($args[1]->value, $valueType, $nativeValueType, '#2 $column_key', $scope) as $error) { + $errors[] = $error; + } + + if (count($args) >= 3) { + foreach ($this->checkColumn($args[2]->value, $valueType, $nativeValueType, '#3 $index_key', $scope) as $error) { + $errors[] = $error; + } + } + + return $errors; + } + + /** + * @return list + */ + private function checkColumn(Node\Expr $columnExpr, Type $valueType, Type $nativeValueType, string $parameter, Scope $scope): array + { + $checkedValueType = $this->treatPhpDocTypesAsCertain ? $valueType : $nativeValueType; + + // array_column() reads object properties (never ArrayAccess offsets), so + // only check when the elements are definitely objects. Array elements use + // offset access, scalars never have the member - leave those to other rules. + if (!$checkedValueType->isObject()->yes()) { + return []; + } + + $columnType = $scope->getType($columnExpr); + $propertyNames = $columnType->getConstantStrings(); + if ($propertyNames === []) { + return []; + } + + $errors = []; + foreach ($propertyNames as $propertyNameType) { + $propertyName = $propertyNameType->getValue(); + if (!$this->isPropertyMissing($checkedValueType, $propertyName)) { + continue; + } + + $errorBuilder = RuleErrorBuilder::message(sprintf( + 'Parameter %s of function array_column expects a valid property name, %s given, but %s does not have such property.', + $parameter, + $propertyNameType->describe(VerbosityLevel::value()), + $checkedValueType->describe(VerbosityLevel::typeOnly()), + ))->identifier('arrayColumn.property'); + + if ($this->treatPhpDocTypesAsCertain && $this->treatPhpDocTypesAsCertainTip) { + if (!$nativeValueType->isObject()->yes() || !$this->isPropertyMissing($nativeValueType, $propertyName)) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); + } + } + + $errors[] = $errorBuilder->build(); + } + + return $errors; + } + + private function isPropertyMissing(Type $valueType, string $propertyName): bool + { + $classReflections = $valueType->getObjectClassReflections(); + if ($classReflections === []) { + return false; + } + + foreach ($classReflections as $classReflection) { + if ($classReflection->isEnum()) { + return false; + } + if ($classReflection->hasInstanceProperty($propertyName)) { + return false; + } + if ($classReflection->allowsDynamicProperties()) { + return false; + } + if ($classReflection->hasNativeMethod('__isset') && $classReflection->hasNativeMethod('__get')) { + return false; + } + } + + return true; + } + +} diff --git a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php new file mode 100644 index 0000000000..f30cee51ff --- /dev/null +++ b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php @@ -0,0 +1,61 @@ + + */ +class ArrayColumnRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ArrayColumnRule( + self::createReflectionProvider(), + $this->shouldTreatPhpDocTypesAsCertain(), + true, + ); + } + + #[RequiresPhp('>= 8.2')] + public function testRule(): void + { + $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + $this->analyse([__DIR__ . '/data/array-column.php'], [ + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'wrong_key' given, but ArrayColumnRuleTest\\NonFinalObject does not have such property.", + 64, + $tipText, + ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", + 68, + $tipText, + ], + [ + "Parameter #3 \$index_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", + 70, + $tipText, + ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", + 71, + $tipText, + ], + [ + "Parameter #3 \$index_key of function array_column expects a valid property name, 'missing2' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", + 71, + $tipText, + ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'wrong_key' given, but ArrayColumnRuleTest\\NonFinalObject does not have such property.", + 96, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/array-column.php b/tests/PHPStan/Rules/Functions/data/array-column.php new file mode 100644 index 0000000000..30da37c2ab --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array-column.php @@ -0,0 +1,98 @@ += 8.2 + +namespace ArrayColumnRuleTest; + +class NonFinalObject +{ + + /** @var string */ + public $key = 'as'; + +} + +final class FinalObject +{ + + public int $id = 1; + + public string $name = 'a'; + + private int $secret = 2; + +} + +class MagicObject +{ + + public function __get(string $name): int + { + return 1; + } + + public function __isset(string $name): bool + { + return true; + } + +} + +#[\AllowDynamicProperties] +class DynamicObject +{ + +} + +enum Suit: string +{ + + case Hearts = 'H'; + +} + +/** + * @param NonFinalObject[] $a + * @param FinalObject[] $b + * @param MagicObject[] $c + * @param DynamicObject[] $d + * @param Suit[] $e + * @param array> $f + * @param list> $g + */ +function test(array $a, array $b, array $c, array $d, array $e, array $f, array $g): void +{ + array_column($a, 'key'); + array_column($a, 'wrong_key'); + + array_column($b, 'id'); + array_column($b, 'name'); + array_column($b, 'missing'); + array_column($b, 'name', 'id'); + array_column($b, 'name', 'missing'); + array_column($b, 'missing', 'missing2'); + array_column($b, 'secret'); + + array_column($c, 'anything'); + array_column($d, 'anything'); + + array_column($e, 'value'); + array_column($e, 'name'); + array_column($e, 'missing'); + + array_column($f, 'col'); + array_column($g, 'missing'); +} + +/** + * @param FinalObject[] $b + */ +function dynamicColumnName(array $b, string $column): void +{ + array_column($b, $column); +} + +function bug5101(): void +{ + $ar = [new NonFinalObject(), new NonFinalObject()]; + array_column($ar, 'wrong_key'); + array_column($ar, 'key'); +} From 2b4e252564118c899b7fe2e673a9c0c87d3403da Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Tue, 16 Jun 2026 21:17:12 +0000 Subject: [PATCH 2/7] Resolve array_column property checks through ArrayColumnHelper Move the object-property-missing detection used by ArrayColumnRule into ArrayColumnHelper, the single home for array_column() semantics, and have the rule delegate to it instead of duplicating the property resolution. Co-Authored-By: Claude Opus 4.8 --- src/Rules/Functions/ArrayColumnRule.php | 58 +++++------------ src/Type/Php/ArrayColumnHelper.php | 64 +++++++++++++++++++ .../Rules/Functions/ArrayColumnRuleTest.php | 2 + 3 files changed, 81 insertions(+), 43 deletions(-) diff --git a/src/Rules/Functions/ArrayColumnRule.php b/src/Rules/Functions/ArrayColumnRule.php index 29f5cd35a9..55ab316f1b 100644 --- a/src/Rules/Functions/ArrayColumnRule.php +++ b/src/Rules/Functions/ArrayColumnRule.php @@ -11,6 +11,7 @@ use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\Php\ArrayColumnHelper; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function count; @@ -27,6 +28,7 @@ final class ArrayColumnRule implements Rule public function __construct( private readonly ReflectionProvider $reflectionProvider, + private readonly ArrayColumnHelper $arrayColumnHelper, private readonly bool $treatPhpDocTypesAsCertain, private readonly bool $treatPhpDocTypesAsCertainTip, ) @@ -95,26 +97,19 @@ private function checkColumn(Node\Expr $columnExpr, Type $valueType, Type $nativ { $checkedValueType = $this->treatPhpDocTypesAsCertain ? $valueType : $nativeValueType; - // array_column() reads object properties (never ArrayAccess offsets), so - // only check when the elements are definitely objects. Array elements use - // offset access, scalars never have the member - leave those to other rules. - if (!$checkedValueType->isObject()->yes()) { + $columnType = $scope->getType($columnExpr); + $missingProperties = $this->arrayColumnHelper->findMissingObjectProperties($checkedValueType, $columnType); + if ($missingProperties === []) { return []; } - $columnType = $scope->getType($columnExpr); - $propertyNames = $columnType->getConstantStrings(); - if ($propertyNames === []) { - return []; + $nativeMissingPropertyNames = []; + foreach ($this->arrayColumnHelper->findMissingObjectProperties($nativeValueType, $columnType) as $nativeMissingProperty) { + $nativeMissingPropertyNames[$nativeMissingProperty->getValue()] = true; } $errors = []; - foreach ($propertyNames as $propertyNameType) { - $propertyName = $propertyNameType->getValue(); - if (!$this->isPropertyMissing($checkedValueType, $propertyName)) { - continue; - } - + foreach ($missingProperties as $propertyNameType) { $errorBuilder = RuleErrorBuilder::message(sprintf( 'Parameter %s of function array_column expects a valid property name, %s given, but %s does not have such property.', $parameter, @@ -122,10 +117,12 @@ private function checkColumn(Node\Expr $columnExpr, Type $valueType, Type $nativ $checkedValueType->describe(VerbosityLevel::typeOnly()), ))->identifier('arrayColumn.property'); - if ($this->treatPhpDocTypesAsCertain && $this->treatPhpDocTypesAsCertainTip) { - if (!$nativeValueType->isObject()->yes() || !$this->isPropertyMissing($nativeValueType, $propertyName)) { - $errorBuilder->treatPhpDocTypesAsCertainTip(); - } + if ( + $this->treatPhpDocTypesAsCertain + && $this->treatPhpDocTypesAsCertainTip + && !isset($nativeMissingPropertyNames[$propertyNameType->getValue()]) + ) { + $errorBuilder->treatPhpDocTypesAsCertainTip(); } $errors[] = $errorBuilder->build(); @@ -134,29 +131,4 @@ private function checkColumn(Node\Expr $columnExpr, Type $valueType, Type $nativ return $errors; } - private function isPropertyMissing(Type $valueType, string $propertyName): bool - { - $classReflections = $valueType->getObjectClassReflections(); - if ($classReflections === []) { - return false; - } - - foreach ($classReflections as $classReflection) { - if ($classReflection->isEnum()) { - return false; - } - if ($classReflection->hasInstanceProperty($propertyName)) { - return false; - } - if ($classReflection->allowsDynamicProperties()) { - return false; - } - if ($classReflection->hasNativeMethod('__isset') && $classReflection->hasNativeMethod('__get')) { - return false; - } - } - - return true; - } - } diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index aeb414875f..b9c001d400 100644 --- a/src/Type/Php/ArrayColumnHelper.php +++ b/src/Type/Php/ArrayColumnHelper.php @@ -11,6 +11,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; @@ -150,6 +151,69 @@ public function handleConstantArray(ConstantArrayType $arrayType, Type $columnTy return $builder->getArray(); } + /** + * Returns the property names from $columnType that the objects contained in + * $iterableValueType certainly lack, so that reading them via array_column() + * would yield nothing. + * + * Unlike the type-inference path (getOffsetOrProperty), this resolves + * properties at the ClassReflection level so that concrete and non-final + * element classes are reported - matching how AccessPropertiesRule reports + * direct property fetches. + * + * @return list + */ + public function findMissingObjectProperties(Type $iterableValueType, Type $columnType): array + { + // array_column() reads object properties (never ArrayAccess offsets), so + // only check when the elements are definitely objects. Array elements use + // offset access, scalars never have the member - leave those to other rules. + if (!$iterableValueType->isObject()->yes()) { + return []; + } + + $propertyNameTypes = $columnType->getConstantStrings(); + if ($propertyNameTypes === []) { + return []; + } + + $missing = []; + foreach ($propertyNameTypes as $propertyNameType) { + if (!$this->isObjectPropertyMissing($iterableValueType, $propertyNameType->getValue())) { + continue; + } + + $missing[] = $propertyNameType; + } + + return $missing; + } + + private function isObjectPropertyMissing(Type $iterableValueType, string $propertyName): bool + { + $classReflections = $iterableValueType->getObjectClassReflections(); + if ($classReflections === []) { + return false; + } + + foreach ($classReflections as $classReflection) { + if ($classReflection->isEnum()) { + return false; + } + if ($classReflection->hasInstanceProperty($propertyName)) { + return false; + } + if ($classReflection->allowsDynamicProperties()) { + return false; + } + if ($classReflection->hasNativeMethod('__isset') && $classReflection->hasNativeMethod('__get')) { + return false; + } + } + + return true; + } + /** * @return array{Type, TrinaryLogic} */ diff --git a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php index f30cee51ff..71ad851bb1 100644 --- a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use PHPStan\Type\Php\ArrayColumnHelper; use PHPUnit\Framework\Attributes\RequiresPhp; /** @@ -16,6 +17,7 @@ protected function getRule(): Rule { return new ArrayColumnRule( self::createReflectionProvider(), + self::getContainer()->getByType(ArrayColumnHelper::class), $this->shouldTreatPhpDocTypesAsCertain(), true, ); From e65531c0a741042fea327882411b1c02e54459d9 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 05:22:29 +0000 Subject: [PATCH 3/7] Check array_column() argument count before reflection lookups Co-Authored-By: Claude Opus 4.8 --- src/Rules/Functions/ArrayColumnRule.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Rules/Functions/ArrayColumnRule.php b/src/Rules/Functions/ArrayColumnRule.php index 55ab316f1b..5ffb910ffe 100644 --- a/src/Rules/Functions/ArrayColumnRule.php +++ b/src/Rules/Functions/ArrayColumnRule.php @@ -46,6 +46,12 @@ public function processNode(Node $node, Scope $scope): array return []; } + // array_column() requires at least the array and the column key - bail + // out before the more expensive reflection lookups below. + if (count($node->getArgs()) < 2) { + return []; + } + if (!$this->reflectionProvider->hasFunction($node->name, $scope)) { return []; } From ddc1425294a3582cc23319b63af4c72530406bcd Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 05:22:34 +0000 Subject: [PATCH 4/7] Report array_column() reading a property absent from enum cases A pure enum exposes only `name`; a backed enum also exposes `value`. Any other property name read via array_column() yields an empty result, so report it - mirroring the object-property check. Closes https://github.com/phpstan/phpstan/issues/12188 Co-Authored-By: Claude Opus 4.8 --- src/Type/Php/ArrayColumnHelper.php | 10 ++++++- .../Rules/Functions/ArrayColumnRuleTest.php | 27 ++++++++++++++----- .../Rules/Functions/data/array-column.php | 14 +++++++++- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/Type/Php/ArrayColumnHelper.php b/src/Type/Php/ArrayColumnHelper.php index b9c001d400..94268cb16f 100644 --- a/src/Type/Php/ArrayColumnHelper.php +++ b/src/Type/Php/ArrayColumnHelper.php @@ -198,7 +198,15 @@ private function isObjectPropertyMissing(Type $iterableValueType, string $proper foreach ($classReflections as $classReflection) { if ($classReflection->isEnum()) { - return false; + // Enum cases expose the read-only `name` property, and backed + // enums additionally expose `value`. Any other name is missing. + if ($propertyName === 'name') { + return false; + } + if ($propertyName === 'value' && $classReflection->isBackedEnum()) { + return false; + } + continue; } if ($classReflection->hasInstanceProperty($propertyName)) { return false; diff --git a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php index 71ad851bb1..84a7b76806 100644 --- a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php @@ -30,32 +30,47 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/array-column.php'], [ [ "Parameter #2 \$column_key of function array_column expects a valid property name, 'wrong_key' given, but ArrayColumnRuleTest\\NonFinalObject does not have such property.", - 64, + 72, $tipText, ], [ "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", - 68, + 76, $tipText, ], [ "Parameter #3 \$index_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", - 70, + 78, $tipText, ], [ "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", - 71, + 79, $tipText, ], [ "Parameter #3 \$index_key of function array_column expects a valid property name, 'missing2' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", - 71, + 79, + $tipText, + ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\Suit does not have such property.", + 87, + $tipText, + ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'value' given, but ArrayColumnRuleTest\\PureSuit does not have such property.", + 93, + $tipText, + ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\PureSuit does not have such property.", + 94, $tipText, ], [ "Parameter #2 \$column_key of function array_column expects a valid property name, 'wrong_key' given, but ArrayColumnRuleTest\\NonFinalObject does not have such property.", - 96, + 108, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/array-column.php b/tests/PHPStan/Rules/Functions/data/array-column.php index 30da37c2ab..a5c0f849f3 100644 --- a/tests/PHPStan/Rules/Functions/data/array-column.php +++ b/tests/PHPStan/Rules/Functions/data/array-column.php @@ -49,6 +49,13 @@ enum Suit: string } +enum PureSuit +{ + + case Hearts; + +} + /** * @param NonFinalObject[] $a * @param FinalObject[] $b @@ -57,8 +64,9 @@ enum Suit: string * @param Suit[] $e * @param array> $f * @param list> $g + * @param PureSuit[] $h */ -function test(array $a, array $b, array $c, array $d, array $e, array $f, array $g): void +function test(array $a, array $b, array $c, array $d, array $e, array $f, array $g, array $h): void { array_column($a, 'key'); array_column($a, 'wrong_key'); @@ -80,6 +88,10 @@ function test(array $a, array $b, array $c, array $d, array $e, array $f, array array_column($f, 'col'); array_column($g, 'missing'); + + array_column($h, 'name'); + array_column($h, 'value'); + array_column($h, 'missing'); } /** From 891a66abcac6de283db23a4e1806602a2e05b61b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 05:24:22 +0000 Subject: [PATCH 5/7] Add ArrayColumnRule test for union element type Covers array: a property present on either member is not reported, only one absent from both members is. Co-Authored-By: Claude Opus 4.8 --- tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php | 5 +++++ tests/PHPStan/Rules/Functions/data/array-column.php | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php index 84a7b76806..6ff08be384 100644 --- a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php @@ -72,6 +72,11 @@ public function testRule(): void "Parameter #2 \$column_key of function array_column expects a valid property name, 'wrong_key' given, but ArrayColumnRuleTest\\NonFinalObject does not have such property.", 108, ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject|ArrayColumnRuleTest\\NonFinalObject does not have such property.", + 119, + $tipText, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/array-column.php b/tests/PHPStan/Rules/Functions/data/array-column.php index a5c0f849f3..7bc4f74915 100644 --- a/tests/PHPStan/Rules/Functions/data/array-column.php +++ b/tests/PHPStan/Rules/Functions/data/array-column.php @@ -108,3 +108,13 @@ function bug5101(): void array_column($ar, 'wrong_key'); array_column($ar, 'key'); } + +/** + * @param array $union + */ +function unionElements(array $union): void +{ + array_column($union, 'key'); // exists on NonFinalObject + array_column($union, 'id'); // exists on FinalObject + array_column($union, 'missing'); // exists on neither +} From c15084f93c987a7d010d6e40693d1f39d885b665 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 05:27:56 +0000 Subject: [PATCH 6/7] Cover array_column() reading a missing property off DateTimeImmutable arrays Adds the issue #9671 reproducer: array_column() on a list of internal-class objects (DateTimeImmutable) reading a property the class lacks. Co-Authored-By: Claude Opus 4.8 --- .../PHPStan/Rules/Functions/ArrayColumnRuleTest.php | 5 +++++ tests/PHPStan/Rules/Functions/data/array-column.php | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php index 6ff08be384..466137f169 100644 --- a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php @@ -77,6 +77,11 @@ public function testRule(): void 119, $tipText, ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'Price' given, but DateTimeImmutable does not have such property.", + 130, + $tipText, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/array-column.php b/tests/PHPStan/Rules/Functions/data/array-column.php index 7bc4f74915..37adb1f765 100644 --- a/tests/PHPStan/Rules/Functions/data/array-column.php +++ b/tests/PHPStan/Rules/Functions/data/array-column.php @@ -118,3 +118,16 @@ function unionElements(array $union): void array_column($union, 'id'); // exists on FinalObject array_column($union, 'missing'); // exists on neither } + +class HelloWorld +{ + + /** + * @param list<\DateTimeImmutable> $dates + */ + public function bug9671(array $dates): void + { + array_column($dates, 'Price'); + } + +} From 5ee45e2ab5d8b92c18110ab82b2338db62afa951 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 17 Jun 2026 05:33:15 +0000 Subject: [PATCH 7/7] Cover array_column() property checks with named arguments Co-Authored-By: Claude Opus 4.8 --- .../Rules/Functions/ArrayColumnRuleTest.php | 15 +++++++++++++++ .../PHPStan/Rules/Functions/data/array-column.php | 10 ++++++++++ 2 files changed, 25 insertions(+) diff --git a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php index 466137f169..8b2fb5a92c 100644 --- a/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ArrayColumnRuleTest.php @@ -82,6 +82,21 @@ public function testRule(): void 130, $tipText, ], + [ + "Parameter #2 \$column_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", + 140, + $tipText, + ], + [ + "Parameter #3 \$index_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", + 141, + $tipText, + ], + [ + "Parameter #3 \$index_key of function array_column expects a valid property name, 'missing' given, but ArrayColumnRuleTest\\FinalObject does not have such property.", + 142, + $tipText, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/array-column.php b/tests/PHPStan/Rules/Functions/data/array-column.php index 37adb1f765..9b478782a5 100644 --- a/tests/PHPStan/Rules/Functions/data/array-column.php +++ b/tests/PHPStan/Rules/Functions/data/array-column.php @@ -131,3 +131,13 @@ public function bug9671(array $dates): void } } + +/** + * @param FinalObject[] $b + */ +function namedArguments(array $b): void +{ + array_column($b, column_key: 'missing'); + array_column($b, index_key: 'missing', column_key: 'name'); + array_column(array: $b, column_key: 'name', index_key: 'missing'); +}