diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 6a1fe74bf2..d4ed64ff9b 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -287,4 +287,17 @@ public function supportsNeverReturnTypeInArrowFunction(): bool return $this->versionId>= 80200; } + // https://www.php.net/manual/en/migration80.incompatible.php + // Any array that has a number n as its first numeric key will use n+1 for its next implicit key, even if n is negative. + public function supportsNegativeArrayIndexOnCreate(): bool + { + return $this->versionId>= 80000; + } + + // See https://www.php.net/manual/en/migration83.incompatible.php#migration83.incompatible.core.negative-index-to-empty-array + public function supportsNegativeArrayIndexOnAssign(): bool + { + return $this->versionId>= 80300; + } + } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 875df63b9b..328c710392 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -562,6 +562,9 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type $arrayBuilder->setOffsetValueType($offsetType, $valueType->getIterableValueType(), !$valueType->isIterableAtLeastOnce()->yes()); } } else { + if (!$this->phpVersion->supportsNegativeArrayIndexOnCreate()) { + $arrayBuilder->resetNextAutoIndexToZeroIfNegative(); + } $arrayBuilder->setOffsetValueType( $arrayItem->key !== null ? $getTypeCallback($arrayItem->key) : null, $valueType, diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 7bd109897e..6487f7ef83 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -694,6 +694,10 @@ public function getOffsetValueType(Type $offsetType): Type public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type { $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); + $phpVersion = PhpVersionStaticAccessor::getInstance(); + if (!$phpVersion->supportsNegativeArrayIndexOnAssign()) { + $builder->resetNextAutoIndexToZeroIfNegative(); + } $builder->setOffsetValueType($offsetType, $valueType); return $builder->getArray(); diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 8547fa39a9..d16c7fd7ab 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -181,6 +181,14 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt $this->nextAutoIndexes[] = $newAutoIndex; } } + if (count($this->keyTypes) === 1 && $offsetType->getValue() < 0) { + $newAutoIndex = $offsetType->getValue() + 1; + if (!$optional) { + $this->nextAutoIndexes = [$newAutoIndex]; + } else { + $this->nextAutoIndexes[] = $newAutoIndex; + } + } } else { $this->isList = TrinaryLogic::createNo(); } @@ -314,4 +322,12 @@ public function isList(): bool return $this->isList->yes(); } + public function resetNextAutoIndexToZeroIfNegative(): void + { + if (count($this->nextAutoIndexes) !== 1 || $this->nextAutoIndexes[0]>= 0) { + return; + } + $this->nextAutoIndexes = [0]; + } + } diff --git a/tests/PHPStan/Analyser/Bug10862Test.php b/tests/PHPStan/Analyser/Bug10862Test.php new file mode 100644 index 0000000000..1eb6533217 --- /dev/null +++ b/tests/PHPStan/Analyser/Bug10862Test.php @@ -0,0 +1,36 @@ +assertFileAsserts($assertType, $file, ...$args); + } + +} diff --git a/tests/PHPStan/Analyser/data/bug-10862-php8.0.php b/tests/PHPStan/Analyser/data/bug-10862-php8.0.php new file mode 100644 index 0000000000..e3444adaaa --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10862-php8.0.php @@ -0,0 +1,88 @@ + 1, 2]; + +assertType('array{0, 1}', array_keys($a)); + +$a = [4 => 1, 5]; + +assertType('array{4, 5}', array_keys($a)); + +$a = [-4 => 1, 2]; + +assertType('array{-4, -3}', array_keys($a)); + +$a = [-4 => 1, -2 => 1, 2]; + +assertType('array{-4, -2, -1}', array_keys($a)); + +$a = [-3 => 1, -4 => 1, -2 => 1, 2]; + +assertType('array{-3, -4, -2, -1}', array_keys($a)); + +$a = [-1 => 1, 2]; + +assertType('array{-1, 0}', array_keys($a)); + +$a = [0 => 1, 2]; + +assertType('array{0, 1}', array_keys($a)); + +$a = [-268 => 1, -267 => 1, -266 => 1, -265 => 1, -264 => 1, -263 => 1, -262 => 1, -261 => 1, -260 => 1, -259 => 1, -258 => 1, -257 => 1, -256 => 1, -255 => 1, -254 => 1, -253 => 1, -252 => 1, -251 => 1, -250 => 1, -249 => 1, -248 => 1, -247 => 1, -246 => 1, -245 => 1, -244 => 1, -243 => 1, -242 => 1, -241 => 1, -240 => 1, -239 => 1, -238 => 1, -237 => 1, -236 => 1, -235 => 1, -234 => 1, -233 => 1, -232 => 1, -231 => 1, -230 => 1, -229 => 1, -228 => 1, -227 => 1, -226 => 1, -225 => 1, -224 => 1, -223 => 1, -222 => 1, -221 => 1, -220 => 1, -219 => 1, -218 => 1, -217 => 1, -216 => 1, -215 => 1, -214 => 1, -213 => 1, -212 => 1, -211 => 1, -210 => 1, -209 => 1, -208 => 1, -207 => 1, -206 => 1, -205 => 1, -204 => 1, -203 => 1, -202 => 1, -201 => 1, -200 => 1, -199 => 1, -198 => 1, -197 => 1, -196 => 1, -195 => 1, -194 => 1, -193 => 1, -192 => 1, -191 => 1, -190 => 1, -189 => 1, -188 => 1, -187 => 1, -186 => 1, -185 => 1, -184 => 1, -183 => 1, -182 => 1, -181 => 1, -180 => 1, -179 => 1, -178 => 1, -177 => 1, -176 => 1, -175 => 1, -174 => 1, -173 => 1, -172 => 1, -171 => 1, -170 => 1, -169 => 1, -168 => 1, -167 => 1, -166 => 1, -165 => 1, -164 => 1, -163 => 1, -162 => 1, -161 => 1, -160 => 1, -159 => 1, -158 => 1, -157 => 1, -156 => 1, -155 => 1, -154 => 1, -153 => 1, -152 => 1, -151 => 1, -150 => 1, -149 => 1, -148 => 1, -147 => 1, -146 => 1, -145 => 1, -144 => 1, -143 => 1, -142 => 1, -141 => 1, -140 => 1, -139 => 1, -138 => 1, -137 => 1, -136 => 1, -135 => 1, -134 => 1, -133 => 1, -132 => 1, -131 => 1, -130 => 1, -129 => 1, -128 => 1, -127 => 1, -126 => 1, -125 => 1, -124 => 1, -123 => 1, -122 => 1, -121 => 1, -120 => 1, -119 => 1, -118 => 1, -117 => 1, -116 => 1, -115 => 1, -114 => 1, -113 => 1, -112 => 1, -111 => 1, -110 => 1, -109 => 1, -108 => 1, -107 => 1, -106 => 1, -105 => 1, -104 => 1, -103 => 1, -102 => 1, -101 => 1, -100 => 1, -99 => 1, -98 => 1, -97 => 1, -96 => 1, -95 => 1, -94 => 1, -93 => 1, -92 => 1, -91 => 1, -90 => 1, -89 => 1, -88 => 1, -87 => 1, -86 => 1, -85 => 1, -84 => 1, -83 => 1, -82 => 1, -81 => 1, -80 => 1, -79 => 1, -78 => 1, -77 => 1, -76 => 1, -75 => 1, -74 => 1, -73 => 1, -72 => 1, -71 => 1, -70 => 1, -69 => 1, -68 => 1, -67 => 1, -66 => 1, -65 => 1, -64 => 1, -63 => 1, -62 => 1, -61 => 1, -60 => 1, -59 => 1, -58 => 1, -57 => 1, -56 => 1, -55 => 1, -54 => 1, -53 => 1, -52 => 1, -51 => 1, -50 => 1, -49 => 1, -48 => 1, -47 => 1, -46 => 1, -45 => 1, -44 => 1, -43 => 1, -42 => 1, -41 => 1, -40 => 1, -39 => 1, -38 => 1, -37 => 1, -36 => 1, -35 => 1, -34 => 1, -33 => 1, -32 => 1, -31 => 1, -30 => 1, -29 => 1, -28 => 1, -27 => 1, -26 => 1, -25 => 1, -24 => 1, -23 => 1, -22 => 1, -21 => 1, -20 => 1, -19 => 1, -18 => 1, -17 => 1, -16 => 1, -15 => 1, -14 => 1, -13 => 1, -12 => 1, -11 => 1, -10 => 1, 2]; + +assertType('non-empty-array&oversized-array', array_keys($a)); diff --git a/tests/PHPStan/Analyser/data/bug-10862-php8.3.php b/tests/PHPStan/Analyser/data/bug-10862-php8.3.php new file mode 100644 index 0000000000..47e29702f3 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10862-php8.3.php @@ -0,0 +1,88 @@ + 1, 2]; + +assertType('array{0, 1}', array_keys($a)); + +$a = [4 => 1, 5]; + +assertType('array{4, 5}', array_keys($a)); + +$a = [-4 => 1, 2]; + +assertType('array{-4, -3}', array_keys($a)); + +$a = [-4 => 1, -2 => 1, 2]; + +assertType('array{-4, -2, -1}', array_keys($a)); + +$a = [-3 => 1, -4 => 1, -2 => 1, 2]; + +assertType('array{-3, -4, -2, -1}', array_keys($a)); + +$a = [-1 => 1, 2]; + +assertType('array{-1, 0}', array_keys($a)); + +$a = [0 => 1, 2]; + +assertType('array{0, 1}', array_keys($a)); + +$a = [-268 => 1, -267 => 1, -266 => 1, -265 => 1, -264 => 1, -263 => 1, -262 => 1, -261 => 1, -260 => 1, -259 => 1, -258 => 1, -257 => 1, -256 => 1, -255 => 1, -254 => 1, -253 => 1, -252 => 1, -251 => 1, -250 => 1, -249 => 1, -248 => 1, -247 => 1, -246 => 1, -245 => 1, -244 => 1, -243 => 1, -242 => 1, -241 => 1, -240 => 1, -239 => 1, -238 => 1, -237 => 1, -236 => 1, -235 => 1, -234 => 1, -233 => 1, -232 => 1, -231 => 1, -230 => 1, -229 => 1, -228 => 1, -227 => 1, -226 => 1, -225 => 1, -224 => 1, -223 => 1, -222 => 1, -221 => 1, -220 => 1, -219 => 1, -218 => 1, -217 => 1, -216 => 1, -215 => 1, -214 => 1, -213 => 1, -212 => 1, -211 => 1, -210 => 1, -209 => 1, -208 => 1, -207 => 1, -206 => 1, -205 => 1, -204 => 1, -203 => 1, -202 => 1, -201 => 1, -200 => 1, -199 => 1, -198 => 1, -197 => 1, -196 => 1, -195 => 1, -194 => 1, -193 => 1, -192 => 1, -191 => 1, -190 => 1, -189 => 1, -188 => 1, -187 => 1, -186 => 1, -185 => 1, -184 => 1, -183 => 1, -182 => 1, -181 => 1, -180 => 1, -179 => 1, -178 => 1, -177 => 1, -176 => 1, -175 => 1, -174 => 1, -173 => 1, -172 => 1, -171 => 1, -170 => 1, -169 => 1, -168 => 1, -167 => 1, -166 => 1, -165 => 1, -164 => 1, -163 => 1, -162 => 1, -161 => 1, -160 => 1, -159 => 1, -158 => 1, -157 => 1, -156 => 1, -155 => 1, -154 => 1, -153 => 1, -152 => 1, -151 => 1, -150 => 1, -149 => 1, -148 => 1, -147 => 1, -146 => 1, -145 => 1, -144 => 1, -143 => 1, -142 => 1, -141 => 1, -140 => 1, -139 => 1, -138 => 1, -137 => 1, -136 => 1, -135 => 1, -134 => 1, -133 => 1, -132 => 1, -131 => 1, -130 => 1, -129 => 1, -128 => 1, -127 => 1, -126 => 1, -125 => 1, -124 => 1, -123 => 1, -122 => 1, -121 => 1, -120 => 1, -119 => 1, -118 => 1, -117 => 1, -116 => 1, -115 => 1, -114 => 1, -113 => 1, -112 => 1, -111 => 1, -110 => 1, -109 => 1, -108 => 1, -107 => 1, -106 => 1, -105 => 1, -104 => 1, -103 => 1, -102 => 1, -101 => 1, -100 => 1, -99 => 1, -98 => 1, -97 => 1, -96 => 1, -95 => 1, -94 => 1, -93 => 1, -92 => 1, -91 => 1, -90 => 1, -89 => 1, -88 => 1, -87 => 1, -86 => 1, -85 => 1, -84 => 1, -83 => 1, -82 => 1, -81 => 1, -80 => 1, -79 => 1, -78 => 1, -77 => 1, -76 => 1, -75 => 1, -74 => 1, -73 => 1, -72 => 1, -71 => 1, -70 => 1, -69 => 1, -68 => 1, -67 => 1, -66 => 1, -65 => 1, -64 => 1, -63 => 1, -62 => 1, -61 => 1, -60 => 1, -59 => 1, -58 => 1, -57 => 1, -56 => 1, -55 => 1, -54 => 1, -53 => 1, -52 => 1, -51 => 1, -50 => 1, -49 => 1, -48 => 1, -47 => 1, -46 => 1, -45 => 1, -44 => 1, -43 => 1, -42 => 1, -41 => 1, -40 => 1, -39 => 1, -38 => 1, -37 => 1, -36 => 1, -35 => 1, -34 => 1, -33 => 1, -32 => 1, -31 => 1, -30 => 1, -29 => 1, -28 => 1, -27 => 1, -26 => 1, -25 => 1, -24 => 1, -23 => 1, -22 => 1, -21 => 1, -20 => 1, -19 => 1, -18 => 1, -17 => 1, -16 => 1, -15 => 1, -14 => 1, -13 => 1, -12 => 1, -11 => 1, -10 => 1, 2]; + +assertType('non-empty-array&oversized-array', array_keys($a)); diff --git a/tests/PHPStan/Analyser/data/bug-10862.php b/tests/PHPStan/Analyser/data/bug-10862.php new file mode 100644 index 0000000000..5aba94b9eb --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10862.php @@ -0,0 +1,88 @@ + 1, 2]; + +assertType('array{0, 1}', array_keys($a)); + +$a = [4 => 1, 5]; + +assertType('array{4, 5}', array_keys($a)); + +$a = [-4 => 1, 2]; + +assertType('array{-4, 0}', array_keys($a)); + +$a = [-4 => 1, -2 => 1, 2]; + +assertType('array{-4, -2, 0}', array_keys($a)); + +$a = [-3 => 1, -4 => 1, -2 => 1, 2]; + +assertType('array{-3, -4, -2, 0}', array_keys($a)); + +$a = [-1 => 1, 2]; + +assertType('array{-1, 0}', array_keys($a)); + +$a = [0 => 1, 2]; + +assertType('array{0, 1}', array_keys($a)); + +$a = [-268 => 1, -267 => 1, -266 => 1, -265 => 1, -264 => 1, -263 => 1, -262 => 1, -261 => 1, -260 => 1, -259 => 1, -258 => 1, -257 => 1, -256 => 1, -255 => 1, -254 => 1, -253 => 1, -252 => 1, -251 => 1, -250 => 1, -249 => 1, -248 => 1, -247 => 1, -246 => 1, -245 => 1, -244 => 1, -243 => 1, -242 => 1, -241 => 1, -240 => 1, -239 => 1, -238 => 1, -237 => 1, -236 => 1, -235 => 1, -234 => 1, -233 => 1, -232 => 1, -231 => 1, -230 => 1, -229 => 1, -228 => 1, -227 => 1, -226 => 1, -225 => 1, -224 => 1, -223 => 1, -222 => 1, -221 => 1, -220 => 1, -219 => 1, -218 => 1, -217 => 1, -216 => 1, -215 => 1, -214 => 1, -213 => 1, -212 => 1, -211 => 1, -210 => 1, -209 => 1, -208 => 1, -207 => 1, -206 => 1, -205 => 1, -204 => 1, -203 => 1, -202 => 1, -201 => 1, -200 => 1, -199 => 1, -198 => 1, -197 => 1, -196 => 1, -195 => 1, -194 => 1, -193 => 1, -192 => 1, -191 => 1, -190 => 1, -189 => 1, -188 => 1, -187 => 1, -186 => 1, -185 => 1, -184 => 1, -183 => 1, -182 => 1, -181 => 1, -180 => 1, -179 => 1, -178 => 1, -177 => 1, -176 => 1, -175 => 1, -174 => 1, -173 => 1, -172 => 1, -171 => 1, -170 => 1, -169 => 1, -168 => 1, -167 => 1, -166 => 1, -165 => 1, -164 => 1, -163 => 1, -162 => 1, -161 => 1, -160 => 1, -159 => 1, -158 => 1, -157 => 1, -156 => 1, -155 => 1, -154 => 1, -153 => 1, -152 => 1, -151 => 1, -150 => 1, -149 => 1, -148 => 1, -147 => 1, -146 => 1, -145 => 1, -144 => 1, -143 => 1, -142 => 1, -141 => 1, -140 => 1, -139 => 1, -138 => 1, -137 => 1, -136 => 1, -135 => 1, -134 => 1, -133 => 1, -132 => 1, -131 => 1, -130 => 1, -129 => 1, -128 => 1, -127 => 1, -126 => 1, -125 => 1, -124 => 1, -123 => 1, -122 => 1, -121 => 1, -120 => 1, -119 => 1, -118 => 1, -117 => 1, -116 => 1, -115 => 1, -114 => 1, -113 => 1, -112 => 1, -111 => 1, -110 => 1, -109 => 1, -108 => 1, -107 => 1, -106 => 1, -105 => 1, -104 => 1, -103 => 1, -102 => 1, -101 => 1, -100 => 1, -99 => 1, -98 => 1, -97 => 1, -96 => 1, -95 => 1, -94 => 1, -93 => 1, -92 => 1, -91 => 1, -90 => 1, -89 => 1, -88 => 1, -87 => 1, -86 => 1, -85 => 1, -84 => 1, -83 => 1, -82 => 1, -81 => 1, -80 => 1, -79 => 1, -78 => 1, -77 => 1, -76 => 1, -75 => 1, -74 => 1, -73 => 1, -72 => 1, -71 => 1, -70 => 1, -69 => 1, -68 => 1, -67 => 1, -66 => 1, -65 => 1, -64 => 1, -63 => 1, -62 => 1, -61 => 1, -60 => 1, -59 => 1, -58 => 1, -57 => 1, -56 => 1, -55 => 1, -54 => 1, -53 => 1, -52 => 1, -51 => 1, -50 => 1, -49 => 1, -48 => 1, -47 => 1, -46 => 1, -45 => 1, -44 => 1, -43 => 1, -42 => 1, -41 => 1, -40 => 1, -39 => 1, -38 => 1, -37 => 1, -36 => 1, -35 => 1, -34 => 1, -33 => 1, -32 => 1, -31 => 1, -30 => 1, -29 => 1, -28 => 1, -27 => 1, -26 => 1, -25 => 1, -24 => 1, -23 => 1, -22 => 1, -21 => 1, -20 => 1, -19 => 1, -18 => 1, -17 => 1, -16 => 1, -15 => 1, -14 => 1, -13 => 1, -12 => 1, -11 => 1, -10 => 1, 2]; + +assertType('non-empty-array&oversized-array', array_keys($a));

AltStyle によって変換されたページ (->オリジナル) /