From a5e399fb89cf2006774ea9b32e8175ed581b5e44 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 8 Jun 2026 12:30:00 +0100 Subject: [PATCH 1/7] moving phpstan extenison ehre --- README.md | 47 ++++++ composer.json | 7 +- config/phpstan-extensions.neon | 26 +++ phpstan.neon | 8 +- src/Console/Terminal.php | 153 ++++++++++++++++++ src/Enum/ResultStatus.php | 12 ++ src/ErrorFormatter/SymplifyErrorFormatter.php | 143 ++++++++++++++++ src/FileSystem/FilesystemHelper.php | 17 ++ .../ContainerGetReturnTypeExtension.php | 44 +++++ .../LaravelContainerMakeTypeExtension.php | 44 +++++ .../NativeFunctionReturnTypeExtension.php | 31 ++++ ...SplFileInfoTolerantReturnTypeExtension.php | 42 +++++ .../ClassConstFetchReturnTypeResolver.php | 46 ++++++ ...ected_single_message_many_files_report.txt | 10 ++ .../SymplifyErrorFormatterTest.php | 52 ++++++ .../ContainerGetReturnTypeExtensionTest.php | 39 +++++ .../Source/ExternalService.php | 9 ++ .../config/type_extension.neon | 7 + .../data/container_get.php.inc | 16 ++ .../LaravelContainerMakeTypeExtensionTest.php | 39 +++++ .../Source/ExternalService.php | 9 ++ .../config/type_extension.neon | 7 + .../data/container_make.php.inc | 19 +++ .../NativeFunctionReturnTypeExtensionTest.php | 39 +++++ .../config/type_extension.neon | 5 + .../data/native_function.php.inc | 8 + ...ileInfoTolerantReturnTypeExtensionTest.php | 39 +++++ .../config/type_extension.neon | 5 + .../data/spl_file_info_real_path.php.inc | 15 ++ 29 files changed, 934 insertions(+), 4 deletions(-) create mode 100644 config/phpstan-extensions.neon create mode 100644 src/Console/Terminal.php create mode 100644 src/Enum/ResultStatus.php create mode 100644 src/ErrorFormatter/SymplifyErrorFormatter.php create mode 100644 src/FileSystem/FilesystemHelper.php create mode 100644 src/ReturnTypeExtension/ContainerGetReturnTypeExtension.php create mode 100644 src/ReturnTypeExtension/LaravelContainerMakeTypeExtension.php create mode 100644 src/ReturnTypeExtension/NativeFunctionReturnTypeExtension.php create mode 100644 src/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension.php create mode 100644 src/TypeResolver/ClassConstFetchReturnTypeResolver.php create mode 100644 tests/ErrorFormatter/Fixture/expected_single_message_many_files_report.txt create mode 100644 tests/ErrorFormatter/SymplifyErrorFormatterTest.php create mode 100644 tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php create mode 100644 tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/Source/ExternalService.php create mode 100644 tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon create mode 100644 tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/data/container_get.php.inc create mode 100644 tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php create mode 100644 tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/Source/ExternalService.php create mode 100644 tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon create mode 100644 tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/data/container_make.php.inc create mode 100644 tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/NativeFunctionReturnTypeExtensionTest.php create mode 100644 tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/config/type_extension.neon create mode 100644 tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/data/native_function.php.inc create mode 100644 tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/SplFileInfoTolerantReturnTypeExtensionTest.php create mode 100644 tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon create mode 100644 tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/data/spl_file_info_real_path.php.inc diff --git a/README.md b/README.md index a663510d1..eba0e72ce 100644 --- a/README.md +++ b/README.md @@ -2966,4 +2966,51 @@ final class SomeTest extends TestCase
+## 6. Type Extensions and Error Formatter + +These extensions were merged from the now-deprecated [`symplify/phpstan-extensions`](https://github.com/symplify/phpstan-extensions) +package. They load automatically once `phpstan/extension-installer` is set up - no extra +configuration is needed. + +
+ +### `symplify` Error Formatter + +A compact error format with pre-escaped, regex-ready messages that are easy to copy into +your `ignoreErrors` list. File paths are printed with line numbers and stay clickable in +the terminal (works best with [anthraxx/intellij-awesome-console](https://github.com/anthraxx/intellij-awesome-console)). + +Enable it in your `phpstan.neon`: + +```yaml +parameters: + errorFormat: symplify +``` + +or on the command line: + +```bash +vendor/bin/phpstan analyse --error-format symplify +``` + +
+ +### Type Extensions + +Always-on return type extensions that sharpen PHPStan inference for common framework calls: + +* **`ContainerGetReturnTypeExtension`** - `$container->get(SomeService::class)` returns + `SomeService` instead of plain `object` (Symfony `ContainerInterface`). + +* **`LaravelContainerMakeTypeExtension`** - `$container->make(SomeService::class)` and + `->get(SomeService::class)` return `SomeService` (Laravel `Illuminate\Container\Container`). + +* **`SplFileInfoTolerantReturnTypeExtension`** - `$splFileInfo->getRealPath()` returns + `string` instead of `string|false`, as Symfony Finder only yields existing files. + +* **`NativeFunctionReturnTypeExtension`** - `getcwd()`, `dirname()` and `realpath()` return + `string` instead of `string|false`. + +
+ Happy coding! diff --git a/composer.json b/composer.json index 1f1ef2994..8fce1d7af 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,11 @@ "nikic/php-parser": "^5.7", "phpunit/phpunit": "^13.0", "symfony/framework-bundle": "^6.2", - "symplify/easy-coding-standard": "^13.0", + "illuminate/container": "^11.0", + "phpecs/phpecs": "^2.2", "tomasvotruba/class-leak": "^2.1", "rector/rector": "^2.4", "phpstan/extension-installer": "^1.4", - "symplify/phpstan-extensions": "^12.0", "tomasvotruba/unused-public": "^2.2", "tomasvotruba/type-coverage": "^2.2", "shipmonk/composer-dependency-analyser": "^1.8", @@ -61,7 +61,8 @@ "includes": [ "config/services/services.neon", "config/ctor-rules.neon", - "config/mock-rules.neon" + "config/mock-rules.neon", + "config/phpstan-extensions.neon" ] } } diff --git a/config/phpstan-extensions.neon b/config/phpstan-extensions.neon new file mode 100644 index 000000000..3be053309 --- /dev/null +++ b/config/phpstan-extensions.neon @@ -0,0 +1,26 @@ +services: + # use with "errorFormat: symplify" in CLI/config + errorFormatter.symplify: + class: Symplify\PHPStanRules\ErrorFormatter\SymplifyErrorFormatter + + - Symplify\PHPStanRules\TypeResolver\ClassConstFetchReturnTypeResolver + + # Symfony Container::get($1) => $1 type + - + class: Symplify\PHPStanRules\ReturnTypeExtension\ContainerGetReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # Laravel Container::make($1) => $1 type + - + class: Symplify\PHPStanRules\ReturnTypeExtension\LaravelContainerMakeTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # Symfony Finder SplFileInfo::getRealPath() => string type + - + class: Symplify\PHPStanRules\ReturnTypeExtension\SplFileInfoTolerantReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + + # getcwd()/dirname()/realpath() => always "string" + - + class: Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension + tags: [phpstan.broker.dynamicFunctionReturnTypeExtension] diff --git a/phpstan.neon b/phpstan.neon index 564c4d1d4..ba80c1d5e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,7 @@ includes: - config/services/services.neon - config/naming-rules.neon + - config/phpstan-extensions.neon parameters: treatPhpDocTypesAsCertain: false @@ -46,7 +47,12 @@ parameters: - message: '#Generator expects value type array, list\|PHPStan\\TrinaryLogic\|string\|null> given#' - path: tests/ReturnTypeExtension/NodeGetAttributeTypeExtension/NodeGetAttributeTypeExtensionTest.php + path: tests/ReturnTypeExtension/* + + # PHPStan testing helper, intentionally extended + - + message: '#is not covered by backward compatibility promise#' + path: tests/ErrorFormatter/SymplifyErrorFormatterTest.php - '#Although PHPStan\\Node\\InClassNode is covered by backward compatibility promise, this instanceof assumption might break because (.*?) not guaranteed to always stay the same#' - '#PHPStan\\DependencyInjection\\NeonAdapter#' diff --git a/src/Console/Terminal.php b/src/Console/Terminal.php new file mode 100644 index 000000000..3240b6c4e --- /dev/null +++ b/src/Console/Terminal.php @@ -0,0 +1,153 @@ +> + */ + private const array DESCRIPTORSPEC = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + private static ?int $width = null; + + private static ?bool $stty = null; + + public static function getWidth(): int + { + $width = \getenv('COLUMNS'); + if ($width !== \false) { + return (int) \trim($width); + } + + if (self::$width === null) { + self::initDimensions(); + } + + return self::$width ?: 80; + } + + private static function hasSttyAvailable(): bool + { + if (self::$stty !== null) { + return self::$stty; + } + + if (! \function_exists('exec')) { + return \false; + } + + \exec('stty 2>&1', $output, $exitcode); + return self::$stty = $exitcode === 0; + } + + private static function initDimensions(): void + { + $consoleMode = self::getConsoleMode(); + + if ('\\' === \DIRECTORY_SEPARATOR) { + $width = self::getAnsiconWidth(); + if ($width !== null) { + self::$width = $width; + } elseif (! self::hasVt100Support() && self::hasSttyAvailable()) { + self::initDimensionsUsingStty(); + } elseif ($consoleMode) { + self::$width = $consoleMode[0]; + } + } else { + self::initDimensionsUsingStty(); + } + } + + private static function hasVt100Support(): bool + { + if (! \function_exists('sapi_windows_vt100_support')) { + return \false; + } + + $stdoutStream = \fopen('php://stdout', 'w'); + if ($stdoutStream === \false) { + return \false; + } + + return \sapi_windows_vt100_support($stdoutStream); + } + + private static function initDimensionsUsingStty(): void + { + $sttyColumns = self::getSttyColumns(); + + if ($sttyColumns) { + if (\preg_match('#rows.(\d+);.columns.(\d+);#i', $sttyColumns, $matches)) { + self::$width = (int) $matches[2]; + } elseif (\preg_match('#;.(\d+).rows;.(\d+).columns#i', $sttyColumns, $matches)) { + self::$width = (int) $matches[2]; + } + } + } + + /** + * @return array{0: int, 1: int}|null + */ + private static function getConsoleMode(): ?array + { + $info = self::readFromProcess('mode CON'); + if ($info === null) { + return null; + } + + if (! \preg_match('#--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n#', $info, $matches)) { + return null; + } + + return [(int) $matches[2], (int) $matches[1]]; + } + + private static function getSttyColumns(): ?string + { + return self::readFromProcess('stty -a | grep columns'); + } + + private static function readFromProcess(string $command): ?string + { + if (! \function_exists('proc_open')) { + return null; + } + + $process = \proc_open($command, self::DESCRIPTORSPEC, $pipes, null, null, [ + 'suppress_errors' => \true, + ]); + if (! \is_resource($process)) { + return null; + } + + $info = \stream_get_contents($pipes[1]); + \fclose($pipes[1]); + \fclose($pipes[2]); + \proc_close($process); + return $info; + } + + private static function getAnsiconWidth(): ?int + { + $ansicon = \getenv('ANSICON'); + if (! is_string($ansicon)) { + return null; + } + + if (\preg_match('#^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$#', \trim($ansicon), $matches)) { + return (int) $matches[1]; + } + + return null; + } +} diff --git a/src/Enum/ResultStatus.php b/src/Enum/ResultStatus.php new file mode 100644 index 000000000..d4c9f84e7 --- /dev/null +++ b/src/Enum/ResultStatus.php @@ -0,0 +1,12 @@ +.*?)(\s+\(in context.*?)?$#'; + + private ?Output $output = null; + + /** + * @return ResultStatus::* + */ + public function formatErrors(AnalysisResult $analysisResult, Output $output): int + { + $outputStyle = $output->getStyle(); + $this->output = $output; + + if ($analysisResult->getTotalErrorsCount() === 0 && $analysisResult->getWarnings() === []) { + $outputStyle->success('No errors'); + return ResultStatus::SUCCESS; + } + + $this->reportErrors($analysisResult, $outputStyle); + + $notFileSpecificErrors = $analysisResult->getNotFileSpecificErrors(); + foreach ($notFileSpecificErrors as $notFileSpecificError) { + $outputStyle->warning($notFileSpecificError); + } + + $warnings = $analysisResult->getWarnings(); + foreach ($warnings as $warning) { + $outputStyle->warning($warning); + } + + return ResultStatus::FAILURE; + } + + private function reportErrors(AnalysisResult $analysisResult, OutputStyle $outputStyle): void + { + if ($analysisResult->getFileSpecificErrors() === []) { + return; + } + + foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) { + $this->printSingleError($fileSpecificError, $outputStyle); + } + + $outputStyle->newLine(); + + $errorMessage = sprintf('Found %d errors', $analysisResult->getTotalErrorsCount()); + $outputStyle->error($errorMessage); + } + + private function separator(): void + { + $separator = str_repeat('-', Terminal::getWidth() - self::BULGARIAN_CONSTANT); + $this->writeln($separator); + } + + private function getRelativePath(string $filePath): string + { + // remove trait clutter + /** @var string $clearFilePath */ + $clearFilePath = preg_replace(self::FILE_WITH_TRAIT_CONTEXT_REGEX, '$1', $filePath); + + if (! file_exists($clearFilePath)) { + return $clearFilePath; + } + + return FilesystemHelper::resolveFromCwd($clearFilePath); + } + + private function regexMessage(string $message): string + { + // remove extra ".", that is really not part of message + $message = rtrim($message, '.'); + return '#' . preg_quote($message, '#') . '#'; + } + + private function writeln(string $separator): void + { + $this->output?->writeLineFormatted(' ' . $separator); + } + + private function printSingleError(Error $error, OutputStyle $outputStyle): void + { + $this->separator(); + + $relativeFilePath = $this->getRelativePath($error->getFile()); + $relativeLine = $relativeFilePath . ':' . $error->getLine(); + + // template error + $templateFilePath = $error->getMetadata()['template_file_path'] ?? null; + $templateLine = $error->getMetadata()['template_line'] ?? null; + + if ($templateFilePath && $templateLine) { + $templateFileLine = $this->getRelativePath($templateFilePath) . ':' . $templateLine; + $this->writeln($templateFileLine); + + $this->writeln('rendered in: ' . $relativeLine); + $this->separator(); + } else { + // clickable path + $this->writeln(' ' . $relativeLine); + $this->separator(); + } + + // ignored path - @todo include file + $regexMessage = $this->regexMessage($error->getMessage()); + $itemMessage = sprintf(" - '%s'", $regexMessage); + $this->writeln($itemMessage); + + if ($error->getIdentifier() !== null && $error->canBeIgnored()) { + $this->writeln(' 🪪 ' . $error->getIdentifier()); + } + + $this->separator(); + $outputStyle->newLine(); + } +} diff --git a/src/FileSystem/FilesystemHelper.php b/src/FileSystem/FilesystemHelper.php new file mode 100644 index 000000000..38e1faecd --- /dev/null +++ b/src/FileSystem/FilesystemHelper.php @@ -0,0 +1,17 @@ +getName() === 'get'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { + return $this->classConstFetchReturnTypeResolver->resolve($methodReflection, $methodCall); + } +} diff --git a/src/ReturnTypeExtension/LaravelContainerMakeTypeExtension.php b/src/ReturnTypeExtension/LaravelContainerMakeTypeExtension.php new file mode 100644 index 000000000..0ab0ac38e --- /dev/null +++ b/src/ReturnTypeExtension/LaravelContainerMakeTypeExtension.php @@ -0,0 +1,44 @@ +make() return type + * + * @see \Symplify\PHPStanRules\Tests\ReturnTypeExtension\LaravelContainerMakeTypeExtension\LaravelContainerMakeTypeExtensionTest + */ +final readonly class LaravelContainerMakeTypeExtension implements DynamicMethodReturnTypeExtension +{ + public function __construct( + private ClassConstFetchReturnTypeResolver $classConstFetchReturnTypeResolver + ) { + } + + public function getClass(): string + { + return Container::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return in_array($methodReflection->getName(), ['make', 'get'], true); + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { + return $this->classConstFetchReturnTypeResolver->resolve($methodReflection, $methodCall); + } +} diff --git a/src/ReturnTypeExtension/NativeFunctionReturnTypeExtension.php b/src/ReturnTypeExtension/NativeFunctionReturnTypeExtension.php new file mode 100644 index 000000000..45146577f --- /dev/null +++ b/src/ReturnTypeExtension/NativeFunctionReturnTypeExtension.php @@ -0,0 +1,31 @@ +getName(), ['getcwd', 'dirname', 'realpath'], true); + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $funcCall, + Scope $scope + ): Type { + return new StringType(); + } +} diff --git a/src/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension.php b/src/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension.php new file mode 100644 index 000000000..15f43238e --- /dev/null +++ b/src/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension.php @@ -0,0 +1,42 @@ +getRealPath() has no added value. Just pollutes code and config and makes it unreadable. + * + * This narrows validation only to custom created SplFileInfo. + * + * @see \Symplify\PHPStanRules\Tests\ReturnTypeExtension\SplFileInfoTolerantReturnTypeExtension\SplFileInfoTolerantReturnTypeExtensionTest + */ +final class SplFileInfoTolerantReturnTypeExtension implements DynamicMethodReturnTypeExtension +{ + public function getClass(): string + { + return SplFileInfo::class; + } + + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'getRealPath'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + return new StringType(); + } +} diff --git a/src/TypeResolver/ClassConstFetchReturnTypeResolver.php b/src/TypeResolver/ClassConstFetchReturnTypeResolver.php new file mode 100644 index 000000000..d72a93b4b --- /dev/null +++ b/src/TypeResolver/ClassConstFetchReturnTypeResolver.php @@ -0,0 +1,46 @@ +args[0])) { + return null; + } + + $firstArgOrVariadciPlaceholder = $methodCall->args[0]; + if (! $firstArgOrVariadciPlaceholder instanceof Arg) { + return new MixedType(); + } + + $firstValue = $firstArgOrVariadciPlaceholder->value; + if (! $firstValue instanceof ClassConstFetch) { + return new MixedType(); + } + + $className = null; + if ($firstValue->class instanceof Name && ($firstValue->name instanceof Identifier && $firstValue->name->toString() === 'class')) { + $className = $firstValue->class->toString(); + } + + if ($className !== null) { + return new ObjectType($className); + } + + return null; + } +} diff --git a/tests/ErrorFormatter/Fixture/expected_single_message_many_files_report.txt b/tests/ErrorFormatter/Fixture/expected_single_message_many_files_report.txt new file mode 100644 index 000000000..be51a3c04 --- /dev/null +++ b/tests/ErrorFormatter/Fixture/expected_single_message_many_files_report.txt @@ -0,0 +1,10 @@ + ---------------------------%s + /data/folder/with space/and unicode 😃/project/folder with unicode 😃/file name with "spaces" and unicode 😃.php:4 + ---------------------------%s + - '#Foo#' + ---------------------------%s + + + [ERROR] Found 2 errors + + [WARNING] first generic error diff --git a/tests/ErrorFormatter/SymplifyErrorFormatterTest.php b/tests/ErrorFormatter/SymplifyErrorFormatterTest.php new file mode 100644 index 000000000..07fd440fa --- /dev/null +++ b/tests/ErrorFormatter/SymplifyErrorFormatterTest.php @@ -0,0 +1,52 @@ +getByType(SymplifyErrorFormatter::class); + + $analysisResult = $this->getAnalysisResult($numFileErrors, $numGenericErrors); + $resultCode = $symplifyErrorFormatter->formatErrors($analysisResult, $this->getOutput()); + + $this->assertSame($expectedExitCode, $resultCode); + + $this->assertStringMatchesFormatFile($expectedOutputFile, $this->getOutputContent()); + } + + /** + * @return Iterator + */ + public static function provideData(): Iterator + { + yield ['Some message', 1, 1, 1, __DIR__ . '/Fixture/expected_single_message_many_files_report.txt']; + } + + /** + * @return string[] + */ + #[Override] + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/../../config/phpstan-extensions.neon']; + } +} diff --git a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php new file mode 100644 index 000000000..5fcf91af7 --- /dev/null +++ b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php @@ -0,0 +1,39 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return Iterator> + */ + public static function dataAsserts(): Iterator + { + yield from self::gatherAssertTypes(__DIR__ . '/data/container_get.php.inc'); + } + + /** + * @return string[] + */ + #[Override] + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/type_extension.neon']; + } +} diff --git a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/Source/ExternalService.php b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/Source/ExternalService.php new file mode 100644 index 000000000..46f15ad08 --- /dev/null +++ b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/Source/ExternalService.php @@ -0,0 +1,9 @@ +get($1) => $1 type + - + class: Symplify\PHPStanRules\ReturnTypeExtension\ContainerGetReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/data/container_get.php.inc b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/data/container_get.php.inc new file mode 100644 index 000000000..7b3010774 --- /dev/null +++ b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/data/container_get.php.inc @@ -0,0 +1,16 @@ +get(ExternalService::class); + assertType(ExternalService::class, $service); + } +} diff --git a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php new file mode 100644 index 000000000..a1ada6228 --- /dev/null +++ b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php @@ -0,0 +1,39 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return Iterator> + */ + public static function dataAsserts(): Iterator + { + yield from self::gatherAssertTypes(__DIR__ . '/data/container_make.php.inc'); + } + + /** + * @return string[] + */ + #[Override] + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/type_extension.neon']; + } +} diff --git a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/Source/ExternalService.php b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/Source/ExternalService.php new file mode 100644 index 000000000..743961b61 --- /dev/null +++ b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/Source/ExternalService.php @@ -0,0 +1,9 @@ +make($1) => $1 type + - + class: Symplify\PHPStanRules\ReturnTypeExtension\LaravelContainerMakeTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/data/container_make.php.inc b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/data/container_make.php.inc new file mode 100644 index 000000000..e191ee809 --- /dev/null +++ b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/data/container_make.php.inc @@ -0,0 +1,19 @@ +make(ExternalService::class); + assertType(ExternalService::class, $service); + + $service = $container->get(ExternalService::class); + assertType(ExternalService::class, $service); + } +} diff --git a/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/NativeFunctionReturnTypeExtensionTest.php b/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/NativeFunctionReturnTypeExtensionTest.php new file mode 100644 index 000000000..8d5e17c88 --- /dev/null +++ b/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/NativeFunctionReturnTypeExtensionTest.php @@ -0,0 +1,39 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return Iterator> + */ + public static function dataAsserts(): Iterator + { + yield from self::gatherAssertTypes(__DIR__ . '/data/native_function.php.inc'); + } + + /** + * @return string[] + */ + #[Override] + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/type_extension.neon']; + } +} diff --git a/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/config/type_extension.neon b/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/config/type_extension.neon new file mode 100644 index 000000000..251a8894a --- /dev/null +++ b/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/config/type_extension.neon @@ -0,0 +1,5 @@ +services: + # getcwd()/dirname()/realpath() => always "string" + - + class: Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension + tags: [phpstan.broker.dynamicFunctionReturnTypeExtension] diff --git a/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/data/native_function.php.inc b/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/data/native_function.php.inc new file mode 100644 index 000000000..ca3616568 --- /dev/null +++ b/tests/ReturnTypeExtension/NativeFunctionReturnTypeExtension/data/native_function.php.inc @@ -0,0 +1,8 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return Iterator> + */ + public static function dataAsserts(): Iterator + { + yield from self::gatherAssertTypes(__DIR__ . '/data/spl_file_info_real_path.php.inc'); + } + + /** + * @return string[] + */ + #[Override] + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/type_extension.neon']; + } +} diff --git a/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon new file mode 100644 index 000000000..c9d0e2837 --- /dev/null +++ b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon @@ -0,0 +1,5 @@ +services: + # $splFileInfo->getRealPath() => string type + - + class: Symplify\PHPStanRules\ReturnTypeExtension\SplFileInfoTolerantReturnTypeExtension + tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/data/spl_file_info_real_path.php.inc b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/data/spl_file_info_real_path.php.inc new file mode 100644 index 000000000..0106c030e --- /dev/null +++ b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/data/spl_file_info_real_path.php.inc @@ -0,0 +1,15 @@ +getRealPath(); + assertType('string', $realPath); + } +} From 6fae75b891e32875a51e967930cb3715bc5ad30a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 8 Jun 2026 22:39:49 +0100 Subject: [PATCH 2/7] bump deps --- .github/workflows/bare_run.yaml | 2 +- composer.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bare_run.yaml b/.github/workflows/bare_run.yaml index c357dd942..3e7ef3725 100644 --- a/.github/workflows/bare_run.yaml +++ b/.github/workflows/bare_run.yaml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - php_version: ['7.4', '8.0', '8.2'] + php_version: ['7.4', '8.0', '8.2', '8.4'] steps: - uses: actions/checkout@v3 diff --git a/composer.json b/composer.json index 8fce1d7af..f35def090 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "description": "Set of Symplify rules for PHPStan", "license": "MIT", "require": { - "php": ">=8.4", - "webmozart/assert": " ^2.0", + "php": "^8.4", + "webmozart/assert": "^2.0", "phpstan/phpstan": "^2.2", "nette/utils": "^4.1", "phpstan/phpdoc-parser": "^2.3" From 397159166ce62c3b2a82ee512c186363f3d20c8a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Jun 2026 00:15:18 +0100 Subject: [PATCH 3/7] fix CI: ignore Laravel/Finder/windows deps and skip phpstan extension types in class-leak --- .github/workflows/code_analysis.yaml | 2 +- composer-dependency-analyser.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/code_analysis.yaml b/.github/workflows/code_analysis.yaml index 4cc5a10dc..f4b4e0f26 100644 --- a/.github/workflows/code_analysis.yaml +++ b/.github/workflows/code_analysis.yaml @@ -38,7 +38,7 @@ jobs: - name: 'Check Active Classes' - run: vendor/bin/class-leak check src --ansi + run: vendor/bin/class-leak check src --ansi --skip-type "PHPStan\Type\DynamicMethodReturnTypeExtension" --skip-type "PHPStan\Type\DynamicFunctionReturnTypeExtension" name: ${{ matrix.actions.name }} runs-on: ubuntu-latest diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 493806f2a..3c80d4932 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -14,6 +14,15 @@ // optional classes ->ignoreUnknownClasses(['Symfony\Component\ExpressionLanguage\Expression']) + // windows-only function used in Terminal helper + ->ignoreUnknownFunctions(['sapi_windows_vt100_support']) + + // used only by the Symfony Finder SplFileInfo return type extension + ->ignoreErrorsOnPackage('symfony/finder', [ErrorType::SHADOW_DEPENDENCY]) + + // extension that runs on Laravel Container only + ->ignoreErrorsOnPackage('illuminate/container', [ErrorType::DEV_DEPENDENCY_IN_PROD]) + // already in phpstan/phpstan ->ignoreErrorsOnPackage('nikic/php-parser', [ErrorType::DEV_DEPENDENCY_IN_PROD]) ->ignoreErrorsOnPackage('symfony/routing', [ErrorType::SHADOW_DEPENDENCY]) From 8b20b0d3b1a8c7e421e974d57ca3425a7f779f32 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Jun 2026 06:26:47 +0100 Subject: [PATCH 4/7] nest ReturnTypeExtension namespaces by framework (Symfony, Laravel) --- config/phpstan-extensions.neon | 6 +++--- .../{ => Laravel}/LaravelContainerMakeTypeExtension.php | 2 +- .../{ => Symfony}/ContainerGetReturnTypeExtension.php | 2 +- .../SplFileInfoTolerantReturnTypeExtension.php | 2 +- .../ContainerGetReturnTypeExtensionTest.php | 2 +- .../config/type_extension.neon | 2 +- .../LaravelContainerMakeTypeExtensionTest.php | 2 +- .../config/type_extension.neon | 2 +- .../SplFileInfoTolerantReturnTypeExtensionTest.php | 2 +- .../config/type_extension.neon | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) rename src/ReturnTypeExtension/{ => Laravel}/LaravelContainerMakeTypeExtension.php (95%) rename src/ReturnTypeExtension/{ => Symfony}/ContainerGetReturnTypeExtension.php (95%) rename src/ReturnTypeExtension/{ => Symfony}/SplFileInfoTolerantReturnTypeExtension.php (95%) diff --git a/config/phpstan-extensions.neon b/config/phpstan-extensions.neon index 3be053309..24b346ed3 100644 --- a/config/phpstan-extensions.neon +++ b/config/phpstan-extensions.neon @@ -7,17 +7,17 @@ services: # Symfony Container::get($1) => $1 type - - class: Symplify\PHPStanRules\ReturnTypeExtension\ContainerGetReturnTypeExtension + class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # Laravel Container::make($1) => $1 type - - class: Symplify\PHPStanRules\ReturnTypeExtension\LaravelContainerMakeTypeExtension + class: Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # Symfony Finder SplFileInfo::getRealPath() => string type - - class: Symplify\PHPStanRules\ReturnTypeExtension\SplFileInfoTolerantReturnTypeExtension + class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] # getcwd()/dirname()/realpath() => always "string" diff --git a/src/ReturnTypeExtension/LaravelContainerMakeTypeExtension.php b/src/ReturnTypeExtension/Laravel/LaravelContainerMakeTypeExtension.php similarity index 95% rename from src/ReturnTypeExtension/LaravelContainerMakeTypeExtension.php rename to src/ReturnTypeExtension/Laravel/LaravelContainerMakeTypeExtension.php index 0ab0ac38e..85e89c436 100644 --- a/src/ReturnTypeExtension/LaravelContainerMakeTypeExtension.php +++ b/src/ReturnTypeExtension/Laravel/LaravelContainerMakeTypeExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Symplify\PHPStanRules\ReturnTypeExtension; +namespace Symplify\PHPStanRules\ReturnTypeExtension\Laravel; use Illuminate\Container\Container; use PhpParser\Node\Expr\MethodCall; diff --git a/src/ReturnTypeExtension/ContainerGetReturnTypeExtension.php b/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php similarity index 95% rename from src/ReturnTypeExtension/ContainerGetReturnTypeExtension.php rename to src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php index e668ea82f..925263435 100644 --- a/src/ReturnTypeExtension/ContainerGetReturnTypeExtension.php +++ b/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Symplify\PHPStanRules\ReturnTypeExtension; +namespace Symplify\PHPStanRules\ReturnTypeExtension\Symfony; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; diff --git a/src/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension.php b/src/ReturnTypeExtension/Symfony/SplFileInfoTolerantReturnTypeExtension.php similarity index 95% rename from src/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension.php rename to src/ReturnTypeExtension/Symfony/SplFileInfoTolerantReturnTypeExtension.php index 15f43238e..62023d7d7 100644 --- a/src/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension.php +++ b/src/ReturnTypeExtension/Symfony/SplFileInfoTolerantReturnTypeExtension.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Symplify\PHPStanRules\ReturnTypeExtension; +namespace Symplify\PHPStanRules\ReturnTypeExtension\Symfony; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; diff --git a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php index 5fcf91af7..877253ba0 100644 --- a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php +++ b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/ContainerGetReturnTypeExtensionTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\Attributes\DataProvider; /** - * @see \Symplify\PHPStanRules\ReturnTypeExtension\ContainerGetReturnTypeExtension + * @see \Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension */ final class ContainerGetReturnTypeExtensionTest extends TypeInferenceTestCase { diff --git a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon index 97442bb7a..452adcc2d 100644 --- a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon +++ b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon @@ -3,5 +3,5 @@ services: # $container->get($1) => $1 type - - class: Symplify\PHPStanRules\ReturnTypeExtension\ContainerGetReturnTypeExtension + class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php index a1ada6228..539486dca 100644 --- a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php +++ b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/LaravelContainerMakeTypeExtensionTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\Attributes\DataProvider; /** - * @see \Symplify\PHPStanRules\ReturnTypeExtension\LaravelContainerMakeTypeExtension + * @see \Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension */ final class LaravelContainerMakeTypeExtensionTest extends TypeInferenceTestCase { diff --git a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon index 5b6debbb6..c505c63c4 100644 --- a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon +++ b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon @@ -3,5 +3,5 @@ services: # $container->make($1) => $1 type - - class: Symplify\PHPStanRules\ReturnTypeExtension\LaravelContainerMakeTypeExtension + class: Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] diff --git a/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/SplFileInfoTolerantReturnTypeExtensionTest.php b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/SplFileInfoTolerantReturnTypeExtensionTest.php index 8aac28152..e1cfe07db 100644 --- a/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/SplFileInfoTolerantReturnTypeExtensionTest.php +++ b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/SplFileInfoTolerantReturnTypeExtensionTest.php @@ -10,7 +10,7 @@ use PHPUnit\Framework\Attributes\DataProvider; /** - * @see \Symplify\PHPStanRules\ReturnTypeExtension\SplFileInfoTolerantReturnTypeExtension + * @see \Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension */ final class SplFileInfoTolerantReturnTypeExtensionTest extends TypeInferenceTestCase { diff --git a/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon index c9d0e2837..bb6fb3d49 100644 --- a/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon +++ b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon @@ -1,5 +1,5 @@ services: # $splFileInfo->getRealPath() => string type - - class: Symplify\PHPStanRules\ReturnTypeExtension\SplFileInfoTolerantReturnTypeExtension + class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension tags: [phpstan.broker.dynamicMethodReturnTypeExtension] From afd31455e7404f3eb32998864483ab8dd64a4315 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Jun 2026 06:36:10 +0100 Subject: [PATCH 5/7] make ClassConstFetchReturnTypeResolver::resolve static; gate Symfony/Laravel return type extensions behind symfonyReturnType/laravelReturnType params (off by default) --- config/phpstan-extensions.neon | 44 ++++++++++++------- .../LaravelContainerMakeTypeExtension.php | 9 +--- .../ContainerGetReturnTypeExtension.php | 9 +--- .../ClassConstFetchReturnTypeResolver.php | 2 +- .../config/type_extension.neon | 2 - .../config/type_extension.neon | 2 - 6 files changed, 33 insertions(+), 35 deletions(-) diff --git a/config/phpstan-extensions.neon b/config/phpstan-extensions.neon index 24b346ed3..e75aa6c3a 100644 --- a/config/phpstan-extensions.neon +++ b/config/phpstan-extensions.neon @@ -1,24 +1,36 @@ -services: - # use with "errorFormat: symplify" in CLI/config - errorFormatter.symplify: - class: Symplify\PHPStanRules\ErrorFormatter\SymplifyErrorFormatter +# Symfony/Laravel return type extensions are disabled by default; enable in your phpstan.neon with: +# +# parameters: +# symfonyReturnType: true +# laravelReturnType: true - - Symplify\PHPStanRules\TypeResolver\ClassConstFetchReturnTypeResolver +parameters: + symfonyReturnType: false + laravelReturnType: false - # Symfony Container::get($1) => $1 type - - - class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension - tags: [phpstan.broker.dynamicMethodReturnTypeExtension] +parametersSchema: + symfonyReturnType: bool() + laravelReturnType: bool() +conditionalTags: + # Symfony Container::get($1) => $1 type + Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension: + phpstan.broker.dynamicMethodReturnTypeExtension: %symfonyReturnType% + # Symfony Finder SplFileInfo::getRealPath() => string type + Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension: + phpstan.broker.dynamicMethodReturnTypeExtension: %symfonyReturnType% # Laravel Container::make($1) => $1 type - - - class: Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension - tags: [phpstan.broker.dynamicMethodReturnTypeExtension] + Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension: + phpstan.broker.dynamicMethodReturnTypeExtension: %laravelReturnType% - # Symfony Finder SplFileInfo::getRealPath() => string type - - - class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension - tags: [phpstan.broker.dynamicMethodReturnTypeExtension] +services: + # use with "errorFormat: symplify" in CLI/config + errorFormatter.symplify: + class: Symplify\PHPStanRules\ErrorFormatter\SymplifyErrorFormatter + + - Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension + - Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension + - Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension # getcwd()/dirname()/realpath() => always "string" - diff --git a/src/ReturnTypeExtension/Laravel/LaravelContainerMakeTypeExtension.php b/src/ReturnTypeExtension/Laravel/LaravelContainerMakeTypeExtension.php index 85e89c436..7cb009940 100644 --- a/src/ReturnTypeExtension/Laravel/LaravelContainerMakeTypeExtension.php +++ b/src/ReturnTypeExtension/Laravel/LaravelContainerMakeTypeExtension.php @@ -17,13 +17,8 @@ * * @see \Symplify\PHPStanRules\Tests\ReturnTypeExtension\LaravelContainerMakeTypeExtension\LaravelContainerMakeTypeExtensionTest */ -final readonly class LaravelContainerMakeTypeExtension implements DynamicMethodReturnTypeExtension +final class LaravelContainerMakeTypeExtension implements DynamicMethodReturnTypeExtension { - public function __construct( - private ClassConstFetchReturnTypeResolver $classConstFetchReturnTypeResolver - ) { - } - public function getClass(): string { return Container::class; @@ -39,6 +34,6 @@ public function getTypeFromMethodCall( MethodCall $methodCall, Scope $scope ): ?Type { - return $this->classConstFetchReturnTypeResolver->resolve($methodReflection, $methodCall); + return ClassConstFetchReturnTypeResolver::resolve($methodReflection, $methodCall); } } diff --git a/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php b/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php index 925263435..7f9698075 100644 --- a/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php +++ b/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php @@ -17,13 +17,8 @@ * * @see \Symplify\PHPStanRules\Tests\ReturnTypeExtension\ContainerGetReturnTypeExtension\ContainerGetReturnTypeExtensionTest */ -final readonly class ContainerGetReturnTypeExtension implements DynamicMethodReturnTypeExtension +final class ContainerGetReturnTypeExtension implements DynamicMethodReturnTypeExtension { - public function __construct( - private ClassConstFetchReturnTypeResolver $classConstFetchReturnTypeResolver - ) { - } - public function getClass(): string { return ContainerInterface::class; @@ -39,6 +34,6 @@ public function getTypeFromMethodCall( MethodCall $methodCall, Scope $scope ): ?Type { - return $this->classConstFetchReturnTypeResolver->resolve($methodReflection, $methodCall); + return ClassConstFetchReturnTypeResolver::resolve($methodReflection, $methodCall); } } diff --git a/src/TypeResolver/ClassConstFetchReturnTypeResolver.php b/src/TypeResolver/ClassConstFetchReturnTypeResolver.php index d72a93b4b..b3c4e6f06 100644 --- a/src/TypeResolver/ClassConstFetchReturnTypeResolver.php +++ b/src/TypeResolver/ClassConstFetchReturnTypeResolver.php @@ -16,7 +16,7 @@ final class ClassConstFetchReturnTypeResolver { - public function resolve(MethodReflection $methodReflection, MethodCall $methodCall): ?Type + public static function resolve(MethodReflection $methodReflection, MethodCall $methodCall): ?Type { if (! isset($methodCall->args[0])) { return null; diff --git a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon index 452adcc2d..a67483497 100644 --- a/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon +++ b/tests/ReturnTypeExtension/ContainerGetReturnTypeExtension/config/type_extension.neon @@ -1,6 +1,4 @@ services: - - Symplify\PHPStanRules\TypeResolver\ClassConstFetchReturnTypeResolver - # $container->get($1) => $1 type - class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension diff --git a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon index c505c63c4..5ca441017 100644 --- a/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon +++ b/tests/ReturnTypeExtension/LaravelContainerMakeTypeExtension/config/type_extension.neon @@ -1,6 +1,4 @@ services: - - Symplify\PHPStanRules\TypeResolver\ClassConstFetchReturnTypeResolver - # $container->make($1) => $1 type - class: Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension From 1640c9cf978d08816b561cfcd1d82a05defbc773 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Jun 2026 06:44:11 +0100 Subject: [PATCH 6/7] docs: document symfonyReturnType/laravelReturnType return type extensions --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index eba0e72ce..cc4eca21a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,23 @@ parameters:
+Want sharper type inference for service containers? The Symfony and Laravel return type extensions are **disabled by default** — enable the ones that fit your stack: + +```yaml +parameters: + symfonyReturnType: true + laravelReturnType: true +``` + +`symfonyReturnType` resolves `$container->get(SomeService::class)` to `SomeService` and Symfony Finder's `$splFileInfo->getRealPath()` to `string`. `laravelReturnType` does the same for Laravel's `$container->make(SomeService::class)`: + +```php +$service = $container->get(SomeService::class); +// $service is now known as SomeService, instead of plain object +``` + +
+ But at start, make baby steps with one rule at a time: Jump to: [Symfony-specific rules](#3-symfony-specific-rules), [Doctrine-specific rules](#2-doctrine-specific-rules), [PHPUnit-specific rules](#4-phpunit-specific-rules) or [PHPUnit mock rules](#5-phpunit-mock-rules). From ddc4033364ab6d306a90afc8a6d2b938413c2238 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Jun 2026 07:55:43 +0100 Subject: [PATCH 7/7] gate NativeFunctionReturnTypeExtension behind pathStrings param (off by default) --- README.md | 5 +++-- config/phpstan-extensions.neon | 14 ++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cc4eca21a..2eb65cdb1 100644 --- a/README.md +++ b/README.md @@ -58,15 +58,16 @@ parameters:
-Want sharper type inference for service containers? The Symfony and Laravel return type extensions are **disabled by default** — enable the ones that fit your stack: +Want sharper type inference? The return type extensions are **disabled by default** — enable the ones that fit your stack: ```yaml parameters: symfonyReturnType: true laravelReturnType: true + pathStrings: true ``` -`symfonyReturnType` resolves `$container->get(SomeService::class)` to `SomeService` and Symfony Finder's `$splFileInfo->getRealPath()` to `string`. `laravelReturnType` does the same for Laravel's `$container->make(SomeService::class)`: +`symfonyReturnType` resolves `$container->get(SomeService::class)` to `SomeService` and Symfony Finder's `$splFileInfo->getRealPath()` to `string`. `laravelReturnType` does the same for Laravel's `$container->make(SomeService::class)`. `pathStrings` narrows `getcwd()`, `dirname()` and `realpath()` to `string`: ```php $service = $container->get(SomeService::class); diff --git a/config/phpstan-extensions.neon b/config/phpstan-extensions.neon index e75aa6c3a..5a9deab49 100644 --- a/config/phpstan-extensions.neon +++ b/config/phpstan-extensions.neon @@ -1,16 +1,19 @@ -# Symfony/Laravel return type extensions are disabled by default; enable in your phpstan.neon with: +# return type extensions are disabled by default; enable in your phpstan.neon with: # # parameters: # symfonyReturnType: true # laravelReturnType: true +# pathStrings: true parameters: symfonyReturnType: false laravelReturnType: false + pathStrings: false parametersSchema: symfonyReturnType: bool() laravelReturnType: bool() + pathStrings: bool() conditionalTags: # Symfony Container::get($1) => $1 type @@ -22,6 +25,9 @@ conditionalTags: # Laravel Container::make($1) => $1 type Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension: phpstan.broker.dynamicMethodReturnTypeExtension: %laravelReturnType% + # getcwd()/dirname()/realpath() => always "string" + Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension: + phpstan.broker.dynamicFunctionReturnTypeExtension: %pathStrings% services: # use with "errorFormat: symplify" in CLI/config @@ -31,8 +37,4 @@ services: - Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension - Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension - Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension - - # getcwd()/dirname()/realpath() => always "string" - - - class: Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension - tags: [phpstan.broker.dynamicFunctionReturnTypeExtension] + - Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension