From b9e3f28ba64d33068f1d61dc3209a70b75ee7c18 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 12 Oct 2025 17:29:04 +0200 Subject: [PATCH 1/5] Fix up TypeFactory return type. --- extension.neon | 4 + phpstan.neon | 7 ++ ...FactoryBuildDynamicReturnTypeExtension.php | 117 ++++++++++++++++++ .../Type/Fake/TypeFactoryCorrectUsage.php | 45 +++++++ .../Type/Fake/TypeFactoryIncorrectUsage.php | 33 +++++ ...oryBuildDynamicReturnTypeExtensionTest.php | 67 ++++++++++ 6 files changed, 273 insertions(+) create mode 100644 src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php create mode 100644 tests/TestCase/Type/Fake/TypeFactoryCorrectUsage.php create mode 100644 tests/TestCase/Type/Fake/TypeFactoryIncorrectUsage.php create mode 100644 tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php diff --git a/extension.neon b/extension.neon index 0d47eed..e1bb695 100644 --- a/extension.neon +++ b/extension.neon @@ -54,3 +54,7 @@ services: factory: CakeDC\PHPStan\Type\BaseTraitExpressionTypeResolverExtension(Cake\ORM\Locator\LocatorAwareTrait, fetchTable, %s\Model\Table\%sTable, defaultTable) tags: - phpstan.broker.expressionTypeResolverExtension + - + class: CakeDC\PHPStan\Type\TypeFactoryBuildDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/phpstan.neon b/phpstan.neon index b6df14b..e759bd6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,3 +13,10 @@ parameters: ignoreErrors: - identifier: missingType.generics + - + identifier: staticMethod.dynamicCall + path: tests/ + - + identifier: phpstanApi.runtimeReflection + message: '#Creating new ReflectionClass is a runtime reflection concept#' + path: src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php diff --git a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php new file mode 100644 index 0000000..624ca1b --- /dev/null +++ b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php @@ -0,0 +1,117 @@ +|null + */ + private ?array $typeMap = null; + + /** + * @return class-string + */ + public function getClass(): string + { + return TypeFactory::class; + } + + /** + * Checks if the method is supported. + * + * @param \PHPStan\Reflection\MethodReflection $methodReflection Method reflection + * @return bool + */ + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'build'; + } + + /** + * Returns the type from the static method call. + * + * @param \PHPStan\Reflection\MethodReflection $methodReflection Method reflection + * @param \PhpParser\Node\Expr\StaticCall $methodCall Static method call + * @param \PHPStan\Analyser\Scope $scope Scope + * @return \PHPStan\Type\Type|null + */ + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope, + ): ?Type { + $args = $methodCall->getArgs(); + if (count($args) === 0) { + return null; + } + + $argType = $scope->getType($args[0]->value); + $constantStrings = $argType->getConstantStrings(); + if (count($constantStrings) !== 1) { + return null; + } + + $typeName = $constantStrings[0]->getValue(); + $typeMap = $this->getTypeMap(); + + if (!isset($typeMap[$typeName])) { + return null; + } + + return new ObjectType($typeMap[$typeName]); + } + + /** + * Get the type map by reading TypeFactory's static property via reflection. + * This is cached after the first call. + * + * @return array + */ + private function getTypeMap(): array + { + if ($this->typeMap !== null) { + return $this->typeMap; + } + + try { + $reflection = new ReflectionClass(TypeFactory::class); + $property = $reflection->getProperty('_types'); + $property->setAccessible(true); + + /** @var array $defaultValue */ + $defaultValue = $property->getDefaultValue(); + $this->typeMap = $defaultValue; + + return $this->typeMap; + } catch (\ReflectionException $e) { + return []; + } + } +} diff --git a/tests/TestCase/Type/Fake/TypeFactoryCorrectUsage.php b/tests/TestCase/Type/Fake/TypeFactoryCorrectUsage.php new file mode 100644 index 0000000..8483240 --- /dev/null +++ b/tests/TestCase/Type/Fake/TypeFactoryCorrectUsage.php @@ -0,0 +1,45 @@ +setUserTimezone('America/New_York'); + } + + public function testTimestampTypeWithSetUserTimezone(): void + { + $type = TypeFactory::build('timestamp'); + $type->setUserTimezone('UTC'); + } + + public function testDateTimeFractionalTypeWithSetUserTimezone(): void + { + $type = TypeFactory::build('datetimefractional'); + $type->setUserTimezone('UTC'); + } + + public function testDateTimeTimezoneTypeWithSetUserTimezone(): void + { + $type = TypeFactory::build('timestamptimezone'); + $type->setUserTimezone('UTC'); + } + + public function testDateTypeWithSetLocaleFormat(): void + { + $type = TypeFactory::build('date'); + $type->setLocaleFormat('yyyy-MM-dd'); + } + + public function testTimeTypeWithSetLocaleFormat(): void + { + $type = TypeFactory::build('time'); + $type->setLocaleFormat('HH:mm:ss'); + } +} diff --git a/tests/TestCase/Type/Fake/TypeFactoryIncorrectUsage.php b/tests/TestCase/Type/Fake/TypeFactoryIncorrectUsage.php new file mode 100644 index 0000000..174fc8a --- /dev/null +++ b/tests/TestCase/Type/Fake/TypeFactoryIncorrectUsage.php @@ -0,0 +1,33 @@ +setUserTimezone('UTC'); + } + + public function testStringTypeWithSetUserTimezone(): void + { + $type = TypeFactory::build('string'); + $type->setUserTimezone('UTC'); + } + + public function testBoolTypeWithSetUserTimezone(): void + { + $type = TypeFactory::build('boolean'); + $type->setUserTimezone('UTC'); + } + + public function testJsonTypeWithNonExistentMethod(): void + { + $type = TypeFactory::build('json'); + $type->nonExistentMethod(); + } +} diff --git a/tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php b/tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php new file mode 100644 index 0000000..dd49f86 --- /dev/null +++ b/tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php @@ -0,0 +1,67 @@ +runPhpStan(__DIR__ . '/Fake/TypeFactoryCorrectUsage.php'); + $this->assertStringContainsString('[OK] No errors', $output); + } + + /** + * Test that TypeFactory::build() catches invalid method calls. + * + * @return void + */ + public function testTypeFactoryBuildCatchesInvalidMethodCalls(): void + { + $output = $this->runPhpStan(__DIR__ . '/Fake/TypeFactoryIncorrectUsage.php'); + + $this->assertStringContainsString('IntegerType::setUserTimezone()', $output); + $this->assertStringContainsString('StringType::setUserTimezone()', $output); + $this->assertStringContainsString('BoolType::setUserTimezone()', $output); + $this->assertStringContainsString('JsonType::nonExistentMethod()', $output); + $this->assertStringContainsString('Found 4 errors', $output); + } + + /** + * Run PHPStan on a file and return the output. + * + * @param string $file File to analyze + * @return string + */ + private function runPhpStan(string $file): string + { + $configFile = dirname(__DIR__, 3) . '/extension.neon'; + $command = sprintf( + 'cd %s && vendor/bin/phpstan analyze %s --level=max --configuration=%s --no-progress 2>&1', + escapeshellarg(dirname(__DIR__, 3)), + escapeshellarg($file), + escapeshellarg($configFile), + ); + + exec($command, $output, $exitCode); + + return implode("\n", $output); + } +} From cf3f7d08232e10a63a517e5743eaeca688fe3d34 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 12 Oct 2025 17:33:00 +0200 Subject: [PATCH 2/5] Fix up TypeFactory return type. --- phpstan.neon | 3 --- src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index e759bd6..339da2b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,9 +13,6 @@ parameters: ignoreErrors: - identifier: missingType.generics - - - identifier: staticMethod.dynamicCall - path: tests/ - identifier: phpstanApi.runtimeReflection message: '#Creating new ReflectionClass is a runtime reflection concept#' diff --git a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php index 624ca1b..0302d2e 100644 --- a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php +++ b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php @@ -21,6 +21,7 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use ReflectionClass; +use ReflectionException; /** * Provides return type for TypeFactory::build() based on the type name argument. @@ -110,7 +111,7 @@ private function getTypeMap(): array $this->typeMap = $defaultValue; return $this->typeMap; - } catch (\ReflectionException $e) { + } catch (ReflectionException $e) { return []; } } From a20adc5d1717e471bab8058c1439280504e4a91f Mon Sep 17 00:00:00 2001 From: Marcelo Rocha Date: Tue, 14 Oct 2025 21:29:29 -0300 Subject: [PATCH 3/5] Using ReflectionProvider --- phpstan.neon | 4 --- ...FactoryBuildDynamicReturnTypeExtension.php | 30 ++++++++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/phpstan.neon b/phpstan.neon index 339da2b..b6df14b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -13,7 +13,3 @@ parameters: ignoreErrors: - identifier: missingType.generics - - - identifier: phpstanApi.runtimeReflection - message: '#Creating new ReflectionClass is a runtime reflection concept#' - path: src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php diff --git a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php index 0302d2e..8f024fb 100644 --- a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php +++ b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php @@ -17,11 +17,11 @@ use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\MissingPropertyFromReflectionException; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use ReflectionClass; -use ReflectionException; /** * Provides return type for TypeFactory::build() based on the type name argument. @@ -36,6 +36,19 @@ class TypeFactoryBuildDynamicReturnTypeExtension implements DynamicStaticMethodR */ private ?array $typeMap = null; + /** + * @var \PHPStan\Reflection\ReflectionProvider + */ + protected ReflectionProvider $reflectionProvider; + + /** + * @param \PHPStan\Reflection\ReflectionProvider $reflectionProvider + */ + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + } + /** * @return class-string */ @@ -102,16 +115,17 @@ private function getTypeMap(): array } try { - $reflection = new ReflectionClass(TypeFactory::class); - $property = $reflection->getProperty('_types'); - $property->setAccessible(true); + $reflection = $this->reflectionProvider->getClass(TypeFactory::class); + $property = $reflection->getStaticProperty('_types'); - /** @var array $defaultValue */ - $defaultValue = $property->getDefaultValue(); + /** + * @var array $defaultValue + */ + $defaultValue = $property->getNativeReflection()->getValue(); $this->typeMap = $defaultValue; return $this->typeMap; - } catch (ReflectionException $e) { + } catch (MissingPropertyFromReflectionException $e) { return []; } } From 4e7085011c64a511f474c85bd4ef3408b30b1c92 Mon Sep 17 00:00:00 2001 From: Marcelo Rocha Date: Tue, 14 Oct 2025 21:36:06 -0300 Subject: [PATCH 4/5] phpqa fixes --- src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php index 8f024fb..3f42770 100644 --- a/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php +++ b/src/Type/TypeFactoryBuildDynamicReturnTypeExtension.php @@ -22,6 +22,7 @@ use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; +use ReflectionException; /** * Provides return type for TypeFactory::build() based on the type name argument. @@ -116,8 +117,10 @@ private function getTypeMap(): array try { $reflection = $this->reflectionProvider->getClass(TypeFactory::class); + /** + * @var \PHPStan\Reflection\Php\PhpPropertyReflection $property + */ $property = $reflection->getStaticProperty('_types'); - /** * @var array $defaultValue */ @@ -125,7 +128,7 @@ private function getTypeMap(): array $this->typeMap = $defaultValue; return $this->typeMap; - } catch (MissingPropertyFromReflectionException $e) { + } catch (MissingPropertyFromReflectionException | ReflectionException) { return []; } } From 27a680ab3b1ca38e8b2f36580529ca95b0dd0691 Mon Sep 17 00:00:00 2001 From: Marcelo Rocha Date: Tue, 14 Oct 2025 21:37:48 -0300 Subject: [PATCH 5/5] phpqa fixes --- ...ypeFactoryBuildDynamicReturnTypeExtensionTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php b/tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php index dd49f86..9ae3694 100644 --- a/tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php +++ b/tests/TestCase/Type/TypeFactoryBuildDynamicReturnTypeExtensionTest.php @@ -25,7 +25,7 @@ class TypeFactoryBuildDynamicReturnTypeExtensionTest extends TestCase public function testTypeFactoryBuildReturnsCorrectTypes(): void { $output = $this->runPhpStan(__DIR__ . '/Fake/TypeFactoryCorrectUsage.php'); - $this->assertStringContainsString('[OK] No errors', $output); + static::assertStringContainsString('[OK] No errors', $output); } /** @@ -37,11 +37,11 @@ public function testTypeFactoryBuildCatchesInvalidMethodCalls(): void { $output = $this->runPhpStan(__DIR__ . '/Fake/TypeFactoryIncorrectUsage.php'); - $this->assertStringContainsString('IntegerType::setUserTimezone()', $output); - $this->assertStringContainsString('StringType::setUserTimezone()', $output); - $this->assertStringContainsString('BoolType::setUserTimezone()', $output); - $this->assertStringContainsString('JsonType::nonExistentMethod()', $output); - $this->assertStringContainsString('Found 4 errors', $output); + static::assertStringContainsString('IntegerType::setUserTimezone()', $output); + static::assertStringContainsString('StringType::setUserTimezone()', $output); + static::assertStringContainsString('BoolType::setUserTimezone()', $output); + static::assertStringContainsString('JsonType::nonExistentMethod()', $output); + static::assertStringContainsString('Found 4 errors', $output); } /**