diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81458a2..cdeaece 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,12 +66,25 @@ jobs: mkdir -p build/logs php vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover=build/logs/clover.xml + # Infection 0.32.x requires PHP 8.3+, so keep the linked PHPStan/Infection checks on the basic 8.3 job. - name: Run phpstan - continue-on-error: true - if: ${{ matrix.php == '8.0' }} + if: ${{ matrix.php == '8.3' && matrix.composer == 'basic' }} run: | php vendor/bin/phpstan analyse + - name: Install Infection + if: ${{ matrix.php == '8.3' && matrix.composer == 'basic' }} + run: | + curl --fail --silent --show-error --location \ + --output infection.phar \ + https://github.com/infection/infection/releases/download/0.32.7/infection.phar + chmod +x infection.phar + + - name: Run Infection + if: ${{ matrix.php == '8.3' && matrix.composer == 'basic' }} + run: | + php infection.phar --threads=max --min-msi=0 --min-covered-msi=0 --no-progress + - name: Upload coverage results to Coveralls env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/infection.json.dist b/infection.json.dist index 7b38b1f..765f751 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -8,8 +8,9 @@ "phpUnit": { "customPath": "vendor\/bin\/phpunit" }, + "staticAnalysisTool": "phpstan", "tmpDir": "build/infection/", "logs": { "text": "infection-log.txt" } -} \ No newline at end of file +} diff --git a/phpstan-fixtures.neon b/phpstan-fixtures.neon new file mode 100644 index 0000000..ff963b3 --- /dev/null +++ b/phpstan-fixtures.neon @@ -0,0 +1,12 @@ +parameters: + level: 8 + reportUnmatchedIgnoredErrors: true + paths: + - %currentWorkingDirectory%/src/ + - %currentWorkingDirectory%/tests/ + +services: + - + class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/phpstan.neon b/phpstan.neon index ff963b3..f5b41c4 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,8 +2,12 @@ parameters: level: 8 reportUnmatchedIgnoredErrors: true paths: + # Keep the repo-wide CI pass focused on production code; fixture-style PHPStan tests run via phpstan-fixtures.neon. - %currentWorkingDirectory%/src/ - - %currentWorkingDirectory%/tests/ + ignoreErrors: + - + message: '#^Parameter \#1 \$data of static method Arrayy\\Arrayy<.*>::create\(\) expects .*, .* given\.$#' + path: %currentWorkingDirectory%/src/Arrayy.php services: - diff --git a/src/Arrayy.php b/src/Arrayy.php index 55f51ad..f9f63f4 100644 --- a/src/Arrayy.php +++ b/src/Arrayy.php @@ -282,6 +282,7 @@ public function add($value, $key = null) ); } + /* @phpstan-ignore argument.type */ $this->internalSet($key, $value); return $this; @@ -788,9 +789,11 @@ public function &offsetGet($offset) $value = null; if ($this->offsetExists($offset)) { + /* @phpstan-ignore argument.type, argument.templateType */ $value = &$this->__get($offset); } + /* @phpstan-ignore return.type */ return $value; } @@ -1052,7 +1055,7 @@ public function unserialize($string): self * @return $this *

(Mutable) Return this Arrayy object, with the appended values.

* - * @phpstan-param array $values + * @phpstan-param array $values * @phpstan-param TKey|null $key * @phpstan-return static */ @@ -1067,6 +1070,7 @@ public function appendArrayValues(array $values, $key = null) \is_array($this->array[$key]) ) { foreach ($values as $value) { + /* @phpstan-ignore assign.propertyType */ $this->array[$key][] = $value; } } else { @@ -7451,7 +7455,7 @@ protected function array_keys_recursive( * * @return void * - * @phpstan-param array|null $currentOffset + * @phpstan-param array|null $currentOffset * @psalm-mutation-free */ protected function callAtPath($path, $callable, &$currentOffset = null) @@ -7487,8 +7491,8 @@ protected function callAtPath($path, $callable, &$currentOffset = null) /** * Extracts the value of the given property or method from the object. * - * @param static $object - *

The object to extract the value from.

+ * @param mixed $object + *

The Arrayy instance, object, or other value from which to extract the property or method value.

* @param string $keyOrPropertyOrMethod *

The property or method for which the * value should be extracted.

@@ -7498,11 +7502,10 @@ protected function callAtPath($path, $callable, &$currentOffset = null) * @return mixed *

The value extracted from the specified property or method.

* - * @phpstan-param self $object */ - final protected function extractValue(self $object, string $keyOrPropertyOrMethod) + final protected function extractValue($object, string $keyOrPropertyOrMethod) { - if (isset($object[$keyOrPropertyOrMethod])) { + if ($object instanceof self && isset($object[$keyOrPropertyOrMethod])) { $return = $object->get($keyOrPropertyOrMethod); if ($return instanceof self) { @@ -7512,11 +7515,11 @@ final protected function extractValue(self $object, string $keyOrPropertyOrMetho return $return; } - if (\property_exists($object, $keyOrPropertyOrMethod)) { + if (\is_object($object) && \property_exists($object, $keyOrPropertyOrMethod)) { return $object->{$keyOrPropertyOrMethod}; } - if (\method_exists($object, $keyOrPropertyOrMethod)) { + if (\is_object($object) && \method_exists($object, $keyOrPropertyOrMethod)) { return $object->{$keyOrPropertyOrMethod}(); } @@ -8049,6 +8052,14 @@ protected function internalRemove($key): bool $key = \array_shift($path); } + if ($key === null) { + return false; + } + + if (\is_float($key)) { + return false; + } + unset($this->array[$key]); return true; diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php index 58fbcd1..2d7b222 100644 --- a/src/Collection/AbstractCollection.php +++ b/src/Collection/AbstractCollection.php @@ -317,10 +317,12 @@ public static function createFromJsonMapper(string $json) if (\is_array($jsonObject)) { foreach ($jsonObject as $jsonObjectSingle) { $collectionData = $mapper->map($jsonObjectSingle, $type); + /** @phpstan-var T $collectionData */ $return->add($collectionData); } } else { $collectionData = $mapper->map($jsonObject, $type); + /** @phpstan-var T $collectionData */ $return->add($collectionData); } } else { diff --git a/src/Create.php b/src/Create.php index 9241591..45d15fb 100644 --- a/src/Create.php +++ b/src/Create.php @@ -17,7 +17,10 @@ */ function create($data): Arrayy { - return new Arrayy($data); + /** @var Arrayy> $array */ + $array = new Arrayy($data); + + return $array; } } diff --git a/src/Mapper/Json.php b/src/Mapper/Json.php index a5724e0..d09e769 100644 --- a/src/Mapper/Json.php +++ b/src/Mapper/Json.php @@ -19,7 +19,7 @@ final class Json * Override class names that JsonMapper uses to create objects. * Useful when your setter methods accept abstract classes or interfaces. * - * @var array + * @var array */ public $classMap = []; @@ -33,7 +33,7 @@ final class Json * 2. Name of the unknown JSON property * 3. JSON value of the property * - * @var callable + * @var null|callable(object, string, mixed): void */ public $undefinedPropertyHandler; @@ -41,14 +41,14 @@ final class Json * Runtime cache for inspected classes. This is particularly effective if * mapArray() is called with a large number of objects * - * @var array property inspection result cache + * @var array> property inspection result cache */ private $arInspectedClasses = []; /** * Map data all data in $json into the given $object instance. * - * @param object|iterable $json + * @param object|iterable $json *

JSON object structure from json_decode()

* @param object|string $object *

Object to map $json data into

@@ -58,7 +58,8 @@ final class Json * * @see mapArray() * - * @template TObject + * @template TObject of object + * @phpstan-param object|iterable $json * @phpstan-param TObject|class-string $object *

Object to map $json data into.

* @phpstan-return TObject @@ -79,6 +80,11 @@ public function map($json, $object) $strClassName = \get_class($object); $rc = new \ReflectionClass($object); $strNs = $rc->getNamespaceName(); + + if (\is_object($json) && !($json instanceof \Traversable)) { + $json = \get_object_vars($json); + } + foreach ($json as $key => $jsonValue) { $key = $this->getSafeName($key); @@ -95,9 +101,8 @@ public function map($json, $object) ) = $this->arInspectedClasses[$strClassName][$key]; if (!$hasProperty) { - if (\is_callable($this->undefinedPropertyHandler)) { - \call_user_func( - $this->undefinedPropertyHandler, + if ($this->undefinedPropertyHandler !== null) { + ($this->undefinedPropertyHandler)( $object, $key, $jsonValue @@ -111,7 +116,7 @@ public function map($json, $object) continue; } - if ($this->isNullable($type)) { + if ($type !== null && $this->isNullable($type)) { if ($jsonValue === null) { $this->setProperty($object, $accessor, null); @@ -247,7 +252,7 @@ public function map($json, $object) /** * Map an array * - * @param array $json JSON array structure from json_decode() + * @param array $json JSON array structure from json_decode() * @param mixed $array Array or ArrayObject that gets filled with * data from $json * @param string|null $class Class name for children objects. @@ -261,6 +266,8 @@ public function map($json, $object) * @pslam-param null|class-string $class * * @return mixed Mapped $array is returned + * + * @phpstan-param array $json */ public function mapArray($json, $array, $class = null, $parent_key = '') { @@ -301,8 +308,12 @@ public function mapArray($json, $array, $class = null, $parent_key = '') ) && \count($typesTmp->getTypes()) === 1 + && + \class_exists($typesTmp->getTypes()[0]) ) { - $array[$key] = $this->map($jsonValue, $typesTmp->getTypes()[0]); + /** @var class-string $mappedClass */ + $mappedClass = $typesTmp->getTypes()[0]; + $array[$key] = $this->map($jsonValue, $mappedClass); $foundArrayy = true; break; @@ -404,7 +415,7 @@ private function getFullNamespace($type, $strNs) * @param \ReflectionClass $rc Reflection class to check * @param string $name Property name * - * @return array First value: if the property exists + * @return array{0: bool, 1: \ReflectionMethod|\ReflectionProperty|string|null, 2: string|null} First value: if the property exists * Second value: the accessor to use ( * Array-Key-String or ReflectionMethod or ReflectionProperty, or null) * Third value: type of the property @@ -485,7 +496,7 @@ private function inspectProperty(\ReflectionClass $rc, $name): array * * @param string $docblock Full method docblock * - * @return array + * @return array> */ private static function parseAnnotations($docblock): array { @@ -739,7 +750,7 @@ private function removeNullable($type) * * @internal * - * @template TClass + * @template TClass of object * @phpstan-param TClass|class-string $class * @phpstan-return TClass */ diff --git a/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php b/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php index feb0c21..fbb2b3f 100644 --- a/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php +++ b/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php @@ -38,6 +38,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, } $className = $scope->resolveName($methodCall->class); + /* @phpstan-ignore phpstanApi.runtimeReflection */ if (!\is_a($className, Arrayy::class, true)) { return null; } diff --git a/src/Type/DetectFirstValueTypeCollection.php b/src/Type/DetectFirstValueTypeCollection.php index b0693a1..308d2ff 100644 --- a/src/Type/DetectFirstValueTypeCollection.php +++ b/src/Type/DetectFirstValueTypeCollection.php @@ -26,7 +26,7 @@ final class DetectFirstValueTypeCollection extends Collection implements TypeInt * @param string $iteratorClass * @param bool $checkPropertiesInConstructor * - * @phpstan-param array|Arrayy> $data + * @phpstan-param array|Arrayy>|T $data * @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass */ public function __construct( diff --git a/src/TypeCheck/TypeCheckCallback.php b/src/TypeCheck/TypeCheckCallback.php index 8600770..2162683 100644 --- a/src/TypeCheck/TypeCheckCallback.php +++ b/src/TypeCheck/TypeCheckCallback.php @@ -52,6 +52,8 @@ public function checkType(&$value): bool /** * @return array + * + * @phpstan-return list */ public function getTypes(): array { diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php index 43fbbe7..28f5f80 100644 --- a/src/TypeCheck/TypeCheckPhpDoc.php +++ b/src/TypeCheck/TypeCheckPhpDoc.php @@ -14,11 +14,6 @@ */ final class TypeCheckPhpDoc extends AbstractTypeCheck implements TypeCheckInterface { - /** - * @var bool - */ - private $hasTypeDeclaration = false; - /** * @var string */ @@ -68,8 +63,6 @@ public static function fromDocTypeObject(string $property, $type) $tmpReflection = new self($property); if ($type) { - $tmpReflection->hasTypeDeclaration = true; - $docTypes = self::parseDocTypeObject($type); if (\is_array($docTypes) === true) { foreach ($docTypes as $docType) { @@ -94,8 +87,6 @@ public static function fromReflectionProperty(\ReflectionProperty $reflectionPro $docTypes = self::getTypesFromReflectionPropertyDocBlock($reflectionProperty); if ($docTypes !== null) { - $tmpReflection->hasTypeDeclaration = true; - if (\is_array($docTypes) === true) { foreach ($docTypes as $docType) { $tmpReflection->types[] = $docType; @@ -109,8 +100,6 @@ public static function fromReflectionProperty(\ReflectionProperty $reflectionPro return $tmpReflection; } else { - $tmpReflection->hasTypeDeclaration = true; - $docTypes = self::parseReflectionTypeObject($type); if (\is_array($docTypes) === true) { foreach ($docTypes as $docType) { @@ -237,7 +226,7 @@ public static function parseDocTypeObject($type) } /** - * @return list + * @return list */ private static function getScalarPseudoTypeClasses(): array { diff --git a/tests/Collection/StringTypeTest.php b/tests/Collection/StringTypeTest.php index ec4f5e2..a7eef14 100644 --- a/tests/Collection/StringTypeTest.php +++ b/tests/Collection/StringTypeTest.php @@ -51,7 +51,6 @@ public function testWrongValue(): void { $this->expectException(\TypeError::class); - /* @phpstan-ignore offsetAssign.valueType */ new StringCollection(['A', 'B', 'C', 1]); } diff --git a/tests/MetaPhpStanIntegrationTest.php b/tests/MetaPhpStanIntegrationTest.php index 2f530d2..c596b1c 100644 --- a/tests/MetaPhpStanIntegrationTest.php +++ b/tests/MetaPhpStanIntegrationTest.php @@ -77,7 +77,7 @@ private function runPhpStanFixture(string $fixtureFile): array 'analyse', '--no-progress', '--error-format=raw', - '--configuration=' . $repoRoot . '/phpstan.neon', + '--configuration=' . $repoRoot . '/phpstan-fixtures.neon', $repoRoot . '/tests/PHPStan/' . $fixtureFile, ];