1111use PHPStan \Rules \IdentifierRuleError ;
1212use PHPStan \Rules \Rule ;
1313use PHPStan \Rules \RuleErrorBuilder ;
14+ use PHPStan \Type \Php \ArrayColumnHelper ;
1415use PHPStan \Type \Type ;
1516use PHPStan \Type \VerbosityLevel ;
1617use function count ;
@@ -27,6 +28,7 @@ final class ArrayColumnRule implements Rule
2728
2829 public function __construct (
2930 private readonly ReflectionProvider $ reflectionProvider ,
31+ private readonly ArrayColumnHelper $ arrayColumnHelper ,
3032 private readonly bool $ treatPhpDocTypesAsCertain ,
3133 private readonly bool $ treatPhpDocTypesAsCertainTip ,
3234 )
@@ -95,37 +97,32 @@ private function checkColumn(Node\Expr $columnExpr, Type $valueType, Type $nativ
9597 {
9698 $ checkedValueType = $ this ->treatPhpDocTypesAsCertain ? $ valueType : $ nativeValueType ;
9799
98- // array_column() reads object properties (never ArrayAccess offsets), so
99- // only check when the elements are definitely objects. Array elements use
100- // offset access, scalars never have the member - leave those to other rules.
101- if (!$ checkedValueType ->isObject ()->yes ()) {
100+ $ columnType = $ scope ->getType ($ columnExpr );
101+ $ missingProperties = $ this ->arrayColumnHelper ->findMissingObjectProperties ($ checkedValueType , $ columnType );
102+ if ($ missingProperties === []) {
102103 return [];
103104 }
104105
105- $ columnType = $ scope ->getType ($ columnExpr );
106- $ propertyNames = $ columnType ->getConstantStrings ();
107- if ($ propertyNames === []) {
108- return [];
106+ $ nativeMissingPropertyNames = [];
107+ foreach ($ this ->arrayColumnHelper ->findMissingObjectProperties ($ nativeValueType , $ columnType ) as $ nativeMissingProperty ) {
108+ $ nativeMissingPropertyNames [$ nativeMissingProperty ->getValue ()] = true ;
109109 }
110110
111111 $ errors = [];
112- foreach ($ propertyNames as $ propertyNameType ) {
113- $ propertyName = $ propertyNameType ->getValue ();
114- if (!$ this ->isPropertyMissing ($ checkedValueType , $ propertyName )) {
115- continue ;
116- }
117-
112+ foreach ($ missingProperties as $ propertyNameType ) {
118113 $ errorBuilder = RuleErrorBuilder::message (sprintf (
119114 'Parameter %s of function array_column expects a valid property name, %s given, but %s does not have such property. ' ,
120115 $ parameter ,
121116 $ propertyNameType ->describe (VerbosityLevel::value ()),
122117 $ checkedValueType ->describe (VerbosityLevel::typeOnly ()),
123118 ))->identifier ('arrayColumn.property ' );
124119
125- if ($ this ->treatPhpDocTypesAsCertain && $ this ->treatPhpDocTypesAsCertainTip ) {
126- if (!$ nativeValueType ->isObject ()->yes () || !$ this ->isPropertyMissing ($ nativeValueType , $ propertyName )) {
127- $ errorBuilder ->treatPhpDocTypesAsCertainTip ();
128- }
120+ if (
121+ $ this ->treatPhpDocTypesAsCertain
122+ && $ this ->treatPhpDocTypesAsCertainTip
123+ && !isset ($ nativeMissingPropertyNames [$ propertyNameType ->getValue ()])
124+ ) {
125+ $ errorBuilder ->treatPhpDocTypesAsCertainTip ();
129126 }
130127
131128 $ errors [] = $ errorBuilder ->build ();
@@ -134,29 +131,4 @@ private function checkColumn(Node\Expr $columnExpr, Type $valueType, Type $nativ
134131 return $ errors ;
135132 }
136133
137- private function isPropertyMissing (Type $ valueType , string $ propertyName ): bool
138- {
139- $ classReflections = $ valueType ->getObjectClassReflections ();
140- if ($ classReflections === []) {
141- return false ;
142- }
143-
144- foreach ($ classReflections as $ classReflection ) {
145- if ($ classReflection ->isEnum ()) {
146- return false ;
147- }
148- if ($ classReflection ->hasInstanceProperty ($ propertyName )) {
149- return false ;
150- }
151- if ($ classReflection ->allowsDynamicProperties ()) {
152- return false ;
153- }
154- if ($ classReflection ->hasNativeMethod ('__isset ' ) && $ classReflection ->hasNativeMethod ('__get ' )) {
155- return false ;
156- }
157- }
158-
159- return true ;
160- }
161-
162134}
0 commit comments