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/.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/README.md b/README.md index a663510d1..2eb65cdb1 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,24 @@ parameters:
+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)`. `pathStrings` narrows `getcwd()`, `dirname()` and `realpath()` to `string`: + +```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). @@ -2966,4 +2984,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-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]) diff --git a/composer.json b/composer.json index 1f1ef2994..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" @@ -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..5a9deab49 --- /dev/null +++ b/config/phpstan-extensions.neon @@ -0,0 +1,40 @@ +# 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 + 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 + 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 + errorFormatter.symplify: + class: Symplify\PHPStanRules\ErrorFormatter\SymplifyErrorFormatter + + - Symplify\PHPStanRules\ReturnTypeExtension\Symfony\ContainerGetReturnTypeExtension + - Symplify\PHPStanRules\ReturnTypeExtension\Symfony\SplFileInfoTolerantReturnTypeExtension + - Symplify\PHPStanRules\ReturnTypeExtension\Laravel\LaravelContainerMakeTypeExtension + - Symplify\PHPStanRules\ReturnTypeExtension\NativeFunctionReturnTypeExtension 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 @@ +make() return type + * + * @see \Symplify\PHPStanRules\Tests\ReturnTypeExtension\LaravelContainerMakeTypeExtension\LaravelContainerMakeTypeExtensionTest + */ +final class LaravelContainerMakeTypeExtension implements DynamicMethodReturnTypeExtension +{ + 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 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/Symfony/ContainerGetReturnTypeExtension.php b/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php new file mode 100644 index 000000000..7f9698075 --- /dev/null +++ b/src/ReturnTypeExtension/Symfony/ContainerGetReturnTypeExtension.php @@ -0,0 +1,39 @@ +getName() === 'get'; + } + + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { + return ClassConstFetchReturnTypeResolver::resolve($methodReflection, $methodCall); + } +} diff --git a/src/ReturnTypeExtension/Symfony/SplFileInfoTolerantReturnTypeExtension.php b/src/ReturnTypeExtension/Symfony/SplFileInfoTolerantReturnTypeExtension.php new file mode 100644 index 000000000..62023d7d7 --- /dev/null +++ b/src/ReturnTypeExtension/Symfony/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..b3c4e6f06 --- /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..877253ba0 --- /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\Symfony\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..539486dca --- /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\Laravel\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..bb6fb3d49 --- /dev/null +++ b/tests/ReturnTypeExtension/SplFileInfoTolerantReturnTypeExtension/config/type_extension.neon @@ -0,0 +1,5 @@ +services: + # $splFileInfo->getRealPath() => string type + - + class: Symplify\PHPStanRules\ReturnTypeExtension\Symfony\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); + } +}