diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 0c0e30fccbe..fa1fb7f1248 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -333,19 +333,32 @@ private static function analyzeArrayItem( } elseif ($key_type->isSingleIntLiteral()) { $item_key_value = $key_type->getSingleIntLiteral()->value; - if ($item_key_value >= $array_creation_info->int_offset) { - if ($item_key_value === $array_creation_info->int_offset) { + if ($item_key_value <= PHP_INT_MAX + && $item_key_value > $array_creation_info->int_offset + ) { + if ($item_key_value - 1 === $array_creation_info->int_offset) { $item_is_list_item = true; } - $array_creation_info->int_offset = $item_key_value + 1; + $array_creation_info->int_offset = $item_key_value; } } } else { $key_type = Type::getArrayKey(); } } else { + if ($array_creation_info->int_offset === PHP_INT_MAX) { + IssueBuffer::maybeAdd( + new InvalidArrayOffset( + 'Cannot add an item with an offset beyond PHP_INT_MAX', + new CodeLocation($statements_analyzer->getSource(), $item), + ), + ); + return; + } + $item_is_list_item = true; - $item_key_value = $array_creation_info->int_offset++; + $item_key_value = ++$array_creation_info->int_offset; + $key_atomic_type = new TLiteralInt($item_key_value); $array_creation_info->item_key_atomic_types[] = $key_atomic_type; $key_type = new Union([$key_atomic_type]); @@ -538,7 +551,17 @@ private static function handleUnpackedArray( $array_creation_info->item_key_atomic_types[] = Type::getAtomicStringFromLiteral($new_offset); $array_creation_info->all_list = false; } else { - $new_offset = $array_creation_info->int_offset++; + if ($array_creation_info->int_offset === PHP_INT_MAX) { + IssueBuffer::maybeAdd( + new InvalidArrayOffset( + 'Cannot add an item with an offset beyond PHP_INT_MAX', + new CodeLocation($statements_analyzer->getSource(), $item->value), + ), + $statements_analyzer->getSuppressedIssues(), + ); + continue 2; + } + $new_offset = ++$array_creation_info->int_offset; $array_creation_info->item_key_atomic_types[] = new TLiteralInt($new_offset); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php index f122b73f67d..43161d22f3b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php @@ -38,7 +38,12 @@ class ArrayCreationInfo */ public array $array_keys = []; - public int $int_offset = 0; + /** + * Holds the integer offset of the *last* element added + * + * -1 may mean no elements have been added yet, but can also mean there's an element with offset -1 + */ + public int $int_offset = -1; public bool $all_list = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index aa229281e52..3ba14ca6b01 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -666,17 +666,22 @@ private static function handleArrayItem( } elseif ($key_type->isSingleIntLiteral()) { $item_key_value = $key_type->getSingleIntLiteral()->value; - if ($item_key_value >= $array_creation_info->int_offset) { - if ($item_key_value === $array_creation_info->int_offset) { + if ($item_key_value <= PHP_INT_MAX + && $item_key_value > $array_creation_info->int_offset + ) { + if ($item_key_value - 1 === $array_creation_info->int_offset) { $item_is_list_item = true; } - $array_creation_info->int_offset = $item_key_value + 1; + $array_creation_info->int_offset = $item_key_value; } } } } else { + if ($array_creation_info->int_offset === PHP_INT_MAX) { + return false; + } $item_is_list_item = true; - $item_key_value = $array_creation_info->int_offset++; + $item_key_value = ++$array_creation_info->int_offset; $array_creation_info->item_key_atomic_types[] = new TLiteralInt($item_key_value); } @@ -760,7 +765,10 @@ private static function handleUnpackedArray( $new_offset = $key; $array_creation_info->item_key_atomic_types[] = Type::getAtomicStringFromLiteral($new_offset); } else { - $new_offset = $array_creation_info->int_offset++; + if ($array_creation_info->int_offset === PHP_INT_MAX) { + return false; + } + $new_offset = ++$array_creation_info->int_offset; $array_creation_info->item_key_atomic_types[] = new TLiteralInt($new_offset); } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 174b84f957a..6025758ca05 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -663,8 +663,22 @@ public function getList(): TList */ private function escapeAndQuote($name) { - if (is_string($name) && ($name === '' || preg_match('/[^a-zA-Z0-9_]/', $name))) { - $name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\''; + if (is_string($name)) { + $quote = false; + + if ($name === '' || preg_match('/[^a-zA-Z0-9_]/', $name)) { + $quote = true; + } + + if (preg_match('/^-?[1-9][0-9]*$/', $name) + && (string)(int) $name !== $name // overflow occured + ) { + $quote = true; + } + + if ($quote) { + $name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\''; + } } return $name; diff --git a/tests/ConstantTest.php b/tests/ConstantTest.php index d776c90d407..b72ac83c97e 100644 --- a/tests/ConstantTest.php +++ b/tests/ConstantTest.php @@ -1539,6 +1539,42 @@ class B $z = B::ARRAY['b']; PHP, ], + 'maxIntegerInArrayKey' => [ + 'code' => <<<'PHP' + 1]; + public const I = [9223372036854775807 => 1]; + + // PHP_INT_MAX + 1 + public const SO = ['9223372036854775808' => 1]; + } + $s = A::S; + $i = A::I; + $so = A::SO; + PHP, + 'assertions' => [ + '$s===' => 'array{9223372036854775807: 1}', + '$i===' => 'array{9223372036854775807: 1}', + '$so===' => "array{'9223372036854775808': 1}", + ], + ], + 'autoincrementAlmostOverflow' => [ + 'code' => <<<'PHP' + 0, + 1, // expected key = PHP_INT_MAX + ]; + } + $s = A::I; + PHP, + 'assertions' => [ + '$s===' => 'array{9223372036854775806: 0, 9223372036854775807: 1}', + ], + ], ]; } @@ -2060,6 +2096,40 @@ class Foo { ', 'error_message' => 'InvalidStringClass', ], + 'integerOverflowInArrayKey' => [ + 'code' => <<<'PHP' + 1]; + } + PHP, + 'error_message' => 'InvalidArrayOffset', + ], + 'autoincrementOverflow' => [ + 'code' => <<<'PHP' + 0, + 1, // this is a fatal error + ]; + } + PHP, + 'error_message' => 'InvalidArrayOffset', + ], + 'autoincrementOverflowWithUnpack' => [ + 'code' => <<<'PHP' + 0, + ...[1], // this is a fatal error + ]; + } + PHP, + 'error_message' => 'InvalidArrayOffset', + ], ]; } } diff --git a/tests/ExpressionTest.php b/tests/ExpressionTest.php new file mode 100644 index 00000000000..57186daf99d --- /dev/null +++ b/tests/ExpressionTest.php @@ -0,0 +1,100 @@ +, + * ignored_issues?: list, + * php_version?: string, + * } + * > + */ + public function providerValidCodeParse(): iterable + { + yield 'maxIntegerInArrayKey' => [ + 'code' => <<<'PHP' + 1]; + $i = [9223372036854775807 => 1]; + + // PHP_INT_MAX + 1 + $so = ['9223372036854775808' => 1]; + PHP, + 'assertions' => [ + '$s===' => 'array{9223372036854775807: 1}', + '$i===' => 'array{9223372036854775807: 1}', + '$so===' => "array{'9223372036854775808': 1}", + ], + ]; + yield 'autoincrementAlmostOverflow' => [ + 'code' => <<<'PHP' + 0, + 1, // expected key = PHP_INT_MAX + ]; + PHP, + 'assertions' => [ + '$a===' => 'array{9223372036854775806: 0, 9223372036854775807: 1}', + ], + ]; + } + + /** + * @return iterable< + * string, + * array{ + * code: string, + * error_message: string, + * ignored_issues?: list, + * php_version?: string, + * } + * > + */ + public function providerInvalidCodeParse(): iterable + { + yield 'integerOverflowInArrayKey' => [ + 'code' => <<<'PHP' + 1]; + PHP, + 'error_message' => 'InvalidArrayOffset', + ]; + + yield 'autoincrementOverflow' => [ + 'code' => <<<'PHP' + 0, + 1, // this is a fatal error + ]; + PHP, + 'error_message' => 'InvalidArrayOffset', + ]; + + yield 'autoincrementOverflowWithUnpack' => [ + 'code' => <<<'PHP' + 0, + ...[1], // this is a fatal error + ]; + PHP, + 'error_message' => 'InvalidArrayOffset', + ]; + } +}