From b02d862387d0d88fa553c42f791a526a2cd51e3e Mon Sep 17 00:00:00 2001 From: Geoff Appleby Date: Thu, 23 Oct 2025 02:34:43 -0700 Subject: [PATCH] Update to PHPStan 2 --- composer.json | 4 +++- src/InnerList.php | 3 +++ src/Item.php | 3 +++ src/Serializer.php | 22 +++++++++++++--------- tests/Httpwg/HttpwgTestBase.php | 14 +++++++++++++- tests/ItemTest.php | 2 +- tests/OuterListTest.php | 1 + tests/ParsingRulesetTrait.php | 3 +++ tests/SerializeListTest.php | 2 +- tests/SerializingRulesetTrait.php | 3 ++- 10 files changed, 43 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index b4ae51a..f570259 100644 --- a/composer.json +++ b/composer.json @@ -37,9 +37,11 @@ "phpmd/phpmd": "^2.15", "phpstan/phpstan": "^1.12", "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/phpunit": "^10.5", "slevomat/coding-standard": "^8.15", - "squizlabs/php_codesniffer": "^3.11 || ^4.0" + "squizlabs/php_codesniffer": "^4.0" }, "config": { "sort-packages": true, diff --git a/src/InnerList.php b/src/InnerList.php index 11a3160..7d713cc 100644 --- a/src/InnerList.php +++ b/src/InnerList.php @@ -4,6 +4,9 @@ namespace gapple\StructuredFields; +/** + * @method array getValue() + */ class InnerList implements TupleInterface { use TupleTrait; diff --git a/src/Item.php b/src/Item.php index 6f11f7a..26d9df4 100644 --- a/src/Item.php +++ b/src/Item.php @@ -4,6 +4,9 @@ namespace gapple\StructuredFields; +/** + * @method mixed getValue() + */ class Item implements TupleInterface { use TupleTrait; diff --git a/src/Serializer.php b/src/Serializer.php index 35bce35..de7d9d5 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -39,27 +39,27 @@ public static function serializeItem(mixed $value, ?object $parameters = null): } /** - * @param iterable $value + * @param iterable $value */ public static function serializeList(iterable $value): string { if ($value instanceof \Traversable) { - // @todo Checking for Traversable is not required for PHP ^8.2.0. $value = iterator_to_array($value); } $returnValue = array_map(function ($item) { - if ($item instanceof TupleInterface) { - $itemValue = $item->getValue(); - $itemParameters = $item->getParameters(); - } elseif (is_array($item) && count($item) === 2) { + if (is_array($item) && count($item) === 2) { $itemValue = $item[0]; $itemParameters = $item[1]; + } elseif ($item instanceof TupleInterface) { + $itemValue = $item->getValue(); + $itemParameters = $item->getParameters(); } else { throw new SerializeException("Invalid item in list"); } if (is_array($itemValue)) { + /** @var array $itemValue */ return self::serializeInnerList($itemValue, $itemParameters); } else { return self::serializeItem($itemValue, $itemParameters); @@ -73,8 +73,6 @@ public static function serializeList(iterable $value): string * Serialize an object as a dictionary. * * Either a Traversable object can be provided, or the public properties of the object will be extracted. - * - * @param Dictionary|object $value */ public static function serializeDictionary(object $value): string { @@ -84,6 +82,10 @@ public static function serializeDictionary(object $value): string $value = get_object_vars($value); } + /** + * @var string $key + * @var TupleInterface|list{TupleInterface|list{mixed,object}, object} $item + */ foreach ($value as $key => $item) { if (!empty($returnValue)) { $returnValue .= ', '; @@ -102,6 +104,7 @@ public static function serializeDictionary(object $value): string if ($itemValue === true) { $returnValue .= self::serializeParameters($itemParameters); } elseif (is_array($itemValue)) { + /** @var array $itemValue */ $returnValue .= '=' . self::serializeInnerList($itemValue, $itemParameters); } else { $returnValue .= '=' . self::serializeItem($itemValue, $itemParameters); @@ -189,7 +192,7 @@ private static function serializeDecimal(float $value): string // Casting to a string loses a digit on long numbers, but is preserved // by json_encode (e.g. 111111111111.111). - /** @var string $result */ + /** @var non-empty-string $result */ $result = json_encode(round($value, 3, PHP_ROUND_HALF_EVEN)); if (!str_contains($result, '.')) { @@ -250,6 +253,7 @@ private static function serializeParameters(object $value): string $value = get_object_vars($value); } + /** @var string $key */ foreach ($value as $key => $item) { $returnValue .= ';' . self::serializeKey($key); diff --git a/tests/Httpwg/HttpwgTestBase.php b/tests/Httpwg/HttpwgTestBase.php index 8e2fee9..a02e53e 100644 --- a/tests/Httpwg/HttpwgTestBase.php +++ b/tests/Httpwg/HttpwgTestBase.php @@ -6,6 +6,15 @@ use gapple\Tests\StructuredFields\RulesetTestBase; use gapple\Tests\StructuredFields\SerializingRulesetTrait; +/** + * @phpstan-import-type ExpectedItem from HttpwgRuleExpectedConverter + * @phpstan-import-type ExpectedOuterList from HttpwgRuleExpectedConverter + * @phpstan-import-type ExpectedDictionary from HttpwgRuleExpectedConverter + * @phpstan-type RuleDefinition object{ + * header_type: 'item'|'list'|'dictionary', + * expected: ExpectedItem|ExpectedOuterList|ExpectedDictionary + * } + */ abstract class HttpwgTestBase extends RulesetTestBase { use SerializingRulesetTrait; @@ -27,7 +36,7 @@ protected static function rulesetDataProvider(): array throw new \RuntimeException("Unable to read ruleset JSON file."); } - /** @var array<\stdClass>|null $rules */ + /** @var array|null $rules */ $rules = json_decode($rulesJson); if (is_null($rules) || json_last_error() !== JSON_ERROR_NONE) { throw new \RuntimeException("Unable to parse ruleset JSON file."); @@ -38,8 +47,11 @@ protected static function rulesetDataProvider(): array if (isset($rawRule->expected)) { try { $rawRule->expected = match ($rawRule->header_type) { + // @phpstan-ignore argument.type 'item' => HttpwgRuleExpectedConverter::item($rawRule->expected), + // @phpstan-ignore argument.type 'list' => HttpwgRuleExpectedConverter::list($rawRule->expected), + // @phpstan-ignore argument.type 'dictionary' => HttpwgRuleExpectedConverter::dictionary($rawRule->expected), default => throw new \UnexpectedValueException('Unknown header type'), }; diff --git a/tests/ItemTest.php b/tests/ItemTest.php index b8a165e..84a0e05 100644 --- a/tests/ItemTest.php +++ b/tests/ItemTest.php @@ -41,7 +41,7 @@ public function testArraySet(): void $item[0] = 'Modified Value'; $item[1] = (object) ['paramKey' => 'Modified param value']; $this->assertEquals('Modified Value', $item[0]); - $this->assertEquals('Modified param value', $item[1]->paramKey); // @phpstan-ignore-line + $this->assertEquals('Modified param value', $item[1]->paramKey); } public function testArrayIndexIsset(): void diff --git a/tests/OuterListTest.php b/tests/OuterListTest.php index 48c5bff..8f8b098 100644 --- a/tests/OuterListTest.php +++ b/tests/OuterListTest.php @@ -85,6 +85,7 @@ public function testIteration(): void $this->assertIsIterable($list); $iterated = 0; + /** @var int $key */ foreach ($list as $key => $value) { $this->assertEquals($listValues[$key], $value); $iterated++; diff --git a/tests/ParsingRulesetTrait.php b/tests/ParsingRulesetTrait.php index 182ec01..030d0ae 100644 --- a/tests/ParsingRulesetTrait.php +++ b/tests/ParsingRulesetTrait.php @@ -23,6 +23,9 @@ public static function parseRulesetDataProvider(): array public function testParsing(Rule $record): void { try { + if (is_null($record->raw)) { + throw new \RuntimeException("Raw value not defined for parsing test"); + } $raw = implode(', ', $record->raw); $parsedValue = Parser::{'parse' . ucfirst($record->header_type)}($raw); diff --git a/tests/SerializeListTest.php b/tests/SerializeListTest.php index a6fa1c9..f4bed51 100644 --- a/tests/SerializeListTest.php +++ b/tests/SerializeListTest.php @@ -140,6 +140,6 @@ public function testNestedInnerListObject(): void $this->expectException(SerializeException::class); $this->expectExceptionMessage("Inner lists cannot be nested"); - Serializer::serializeList($list); + Serializer::serializeList($list); // @phpstan-ignore argument.type } } diff --git a/tests/SerializingRulesetTrait.php b/tests/SerializingRulesetTrait.php index c12d756..a6da0a6 100644 --- a/tests/SerializingRulesetTrait.php +++ b/tests/SerializingRulesetTrait.php @@ -23,6 +23,7 @@ public static function serializeRulesetDataProvider(): array public function testSerializing(Rule $record): void { try { + // @phpstan-ignore argument.type $serializedValue = Serializer::{'serialize' . ucfirst($record->header_type)}($record->expected); if ($record->must_fail) { @@ -30,7 +31,7 @@ public function testSerializing(Rule $record): void } $this->assertEquals( - implode(', ', $record->canonical ?? $record->raw), + implode(', ', $record->canonical ?? $record->raw ?? []), $serializedValue, '"' . $record->name . '" was not serialized to expected value' );