From 9b646e5e8c01b6cbe11b100490d27c59c45457da Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 10 Dec 2025 18:41:47 +0200 Subject: [PATCH 01/11] initial extension creation --- .gitattributes | 9 ++ .github/workflows/phpstan.yml | 29 +++++++ .github/workflows/tests.yml | 39 +++++++++ .gitignore | 24 ++++++ .phpunit.cache/test-results | 1 + CLAUDE.md | 52 ++++++++++++ README.md | 68 +++++++++++++++ composer.json | 53 ++++++++++++ extension.neon | 9 ++ phpstan.neon | 7 ++ phpunit.xml | 22 +++++ ...PsrContainerDynamicReturnTypeExtension.php | 83 +++++++++++++++++++ ...rWpContainerDynamicReturnTypeExtension.php | 83 +++++++++++++++++++ ...ontainerDynamicReturnTypeExtensionTest.php | 37 +++++++++ ...ontainerDynamicReturnTypeExtensionTest.php | 37 +++++++++ tests/data/psr-container-types.php | 56 +++++++++++++ tests/data/stellarwp-container-types.php | 56 +++++++++++++ tests/phpstan-test.neon | 6 ++ 18 files changed, 671 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/phpstan.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .phpunit.cache/test-results create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 extension.neon create mode 100644 phpstan.neon create mode 100644 phpunit.xml create mode 100644 src/PsrContainerDynamicReturnTypeExtension.php create mode 100644 src/StellarWpContainerDynamicReturnTypeExtension.php create mode 100644 tests/PsrContainerDynamicReturnTypeExtensionTest.php create mode 100644 tests/StellarWpContainerDynamicReturnTypeExtensionTest.php create mode 100644 tests/data/psr-container-types.php create mode 100644 tests/data/stellarwp-container-types.php create mode 100644 tests/phpstan-test.neon diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..227f633 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +* text +.* export-ignore +composer.lock text -diff +phpunit.* export-ignore +tests export-ignore +codeception.* export-ignore +phpcs.xml export-ignore +docs export-ignore +CLAUDE.md export-ignore \ No newline at end of file diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..b20e539 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,29 @@ +name: PHPStan + +on: + pull_request: + push: + branches: + - main + - master + +jobs: + phpstan: + name: Static Analysis + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run PHPStan + run: composer test:phpstan diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c534b7f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,39 @@ +name: Tests + +on: + pull_request: + push: + branches: + - main + - master + +jobs: + tests: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + - '8.3' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run tests + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c87dbed --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +composer.lock +vendor/ +tests/_support/_generated + +# Dev tools +.buildpath +*.iml +.project +.idea/ +.vscode/ + +# Tests +codeception.yml +tests/_output/* +tests/*.suite.yml +tests/*.pem +tests/*.cert +tests/*.key + +.DS_Store +DOCKER_ENV +Dockerfile-* +output.log +docker_tag \ No newline at end of file diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results new file mode 100644 index 0000000..c7aae5e --- /dev/null +++ b/.phpunit.cache/test-results @@ -0,0 +1 @@ +{"version":2,"defects":{"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:37":7,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:46":7,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:55":7,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:37":7,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:46":7,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:55":7},"times":{"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:37":0.003,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:46":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:55":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:65":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:76":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:37":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:46":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:55":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:65":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:76":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:16":0.001,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:25":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:34":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:44":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:16":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:25":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:34":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:44":0}} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..206cd4e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,52 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a PHPStan extension (`dpanta94/phpstan-containers`) that provides dynamic return type resolution for dependency injection containers. When code calls `$container->get(Foo::class)`, PHPStan will understand that the return type is `Foo`. + +Supports two container interfaces: +- `StellarWP\ContainerContract\ContainerInterface` +- `Psr\Container\ContainerInterface` (PSR-11) + +## Commands + +```bash +# Run tests +composer test + +# Run PHPStan analysis +composer test:phpstan + +# Install dependencies +composer install + +# Run PHP syntax linting +vendor/bin/parallel-lint src/ +``` + +## Architecture + +- **`src/StellarWpContainerDynamicReturnTypeExtension.php`**: PHPStan extension for StellarWP container contract. Intercepts `get()` method calls on `StellarWP\ContainerContract\ContainerInterface` and resolves the return type based on the class name string argument. + +- **`src/PsrContainerDynamicReturnTypeExtension.php`**: PHPStan extension for PSR-11 container contract. Intercepts `get()` method calls on `Psr\Container\ContainerInterface` and resolves the return type based on the class name string argument. + +- **`extension.neon`**: PHPStan configuration file that registers both extensions. + +- **`composer.json`**: Defines the package as type `phpstan-extension` with auto-registration via the `extra.phpstan.includes` config. + +## Testing + +Tests use PHPStan's `TypeInferenceTestCase` to verify type resolution: +- **`tests/StellarWpContainerDynamicReturnTypeExtensionTest.php`**: Tests for StellarWP extension +- **`tests/PsrContainerDynamicReturnTypeExtensionTest.php`**: Tests for PSR-11 extension +- **`tests/data/`**: Test fixture files containing `assertType()` calls + +## Key Details + +- Namespace: `DPanta\PHPStan\Containers` +- PSR-4 autoloading from `src/` +- Requires PHP 7.4+ and PHPStan 2.0+ +- Both extensions implement `DynamicMethodReturnTypeExtension` interface from PHPStan +- Uses `ReflectionProvider` to check class existence (not PHP's native `class_exists`) diff --git a/README.md b/README.md new file mode 100644 index 0000000..5970d81 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# PHPStan Container Extensions + +[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-239922)](https://github.com/phpstan/phpstan) + +PHPStan extensions for dependency injection containers. Provides dynamic return type resolution so PHPStan understands that `$container->get(Foo::class)` returns `Foo`. + +## Supported Containers + +- [StellarWP Container Contract](https://github.com/stellarwp/container-contract) (`StellarWP\ContainerContract\ContainerInterface`) +- [PSR-11 Container](https://www.php-fig.org/psr/psr-11/) (`Psr\Container\ContainerInterface`) + +## Requirements + +- PHP 7.4 or higher +- PHPStan 2.0 or higher + +## Installation + +```bash +composer require --dev dpanta94/phpstan-containers +``` + +If you use [phpstan/extension-installer](https://github.com/phpstan/extension-installer), you're all set! + +### Manual Installation + +If you don't use the extension installer, add the extension to your `phpstan.neon`: + +```neon +includes: + - vendor/dpanta94/phpstan-containers/extension.neon +``` + +## Usage + +Once installed, PHPStan will automatically understand container return types: + +```php +use Psr\Container\ContainerInterface; + +class MyService { + public function __construct(private ContainerInterface $container) {} + + public function doSomething(): void { + // PHPStan knows $logger is an instance of Logger + $logger = $this->container->get(Logger::class); + $logger->info('Hello world'); + + // PHPStan knows $mailer is an instance of MailerInterface + $mailer = $this->container->get(MailerInterface::class); + $mailer->send($message); + } +} +``` + +The extension resolves types when: +- The argument to `get()` is a class-string constant (e.g., `Foo::class`) +- The class or interface exists in the codebase + +When using string service IDs (e.g., `$container->get('mailer')`), the extension falls back to the default `mixed` return type. + +## How It Works + +The extension implements PHPStan's `DynamicMethodReturnTypeExtension` interface to intercept calls to `ContainerInterface::get()`. When it detects a class-string argument, it returns an `ObjectType` for that class, giving PHPStan full knowledge of the returned object's type. + +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7778144 --- /dev/null +++ b/composer.json @@ -0,0 +1,53 @@ +{ + "name": "dpanta94/phpstan-containers", + "description": "PHPStan extensions for Containers", + "license": "MIT", + "type": "phpstan-extension", + "keywords": [ + "static analysis", + "code analysis", + "code analyze", + "container", + "dependency injection", + "phpstan", + "psr-11" + ], + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "^2.0" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.5 || ^10.0", + "psr/container": "^1.0 || ^2.0", + "stellarwp/container-contract": "^1.0" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "DPanta\\PHPStan\\Containers\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "DPanta\\PHPStan\\Containers\\Tests\\": "tests/", + "DPanta\\PHPStan\\Containers\\Tests\\Data\\": "tests/data/" + } + }, + "config": { + "sort-packages": true + }, + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "scripts": { + "test": "phpunit", + "test:phpstan": "phpstan analyze" + } +} diff --git a/extension.neon b/extension.neon new file mode 100644 index 0000000..fc0a394 --- /dev/null +++ b/extension.neon @@ -0,0 +1,9 @@ +services: + - + class: DPanta\PHPStan\Containers\StellarWpContainerDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: DPanta\PHPStan\Containers\PsrContainerDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..176a929 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,7 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + +parameters: + level: max + paths: + - src/ diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..888af67 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,22 @@ + + + + + tests + + + + + + src + + + diff --git a/src/PsrContainerDynamicReturnTypeExtension.php b/src/PsrContainerDynamicReturnTypeExtension.php new file mode 100644 index 0000000..b759a7d --- /dev/null +++ b/src/PsrContainerDynamicReturnTypeExtension.php @@ -0,0 +1,83 @@ +reflectionProvider = $reflectionProvider; + } + + /** + * Get the class this extension applies to. + * + * @return class-string + */ + public function getClass(): string { + return ContainerInterface::class; + } + + /** + * Check if this extension handles the given method. + * + * @param MethodReflection $methodReflection The method reflection. + * + * @return bool + */ + public function isMethodSupported( MethodReflection $methodReflection ): bool { + return $methodReflection->getName() === 'get'; + } + + /** + * Get the return type for the method call. + * + * @param MethodReflection $methodReflection The method reflection. + * @param MethodCall $methodCall The method call node. + * @param Scope $scope The analysis scope. + * + * @return Type|null + */ + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { + if ( count( $methodCall->getArgs() ) === 0 ) { + return null; + } + + $arg = $methodCall->getArgs()[0]->value; + $type = $scope->getType( $arg ); + + $classNames = $type->getConstantStrings(); + if ( count( $classNames ) === 1 ) { + $className = $classNames[0]->getValue(); + if ( $this->reflectionProvider->hasClass( $className ) ) { + return new ObjectType( $className ); + } + } + + return null; + } +} diff --git a/src/StellarWpContainerDynamicReturnTypeExtension.php b/src/StellarWpContainerDynamicReturnTypeExtension.php new file mode 100644 index 0000000..1fa4e9d --- /dev/null +++ b/src/StellarWpContainerDynamicReturnTypeExtension.php @@ -0,0 +1,83 @@ +reflectionProvider = $reflectionProvider; + } + + /** + * Get the class this extension applies to. + * + * @return class-string + */ + public function getClass(): string { + return ContainerInterface::class; + } + + /** + * Check if this extension handles the given method. + * + * @param MethodReflection $methodReflection The method reflection. + * + * @return bool + */ + public function isMethodSupported( MethodReflection $methodReflection ): bool { + return $methodReflection->getName() === 'get'; + } + + /** + * Get the return type for the method call. + * + * @param MethodReflection $methodReflection The method reflection. + * @param MethodCall $methodCall The method call node. + * @param Scope $scope The analysis scope. + * + * @return Type|null + */ + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { + if ( count( $methodCall->getArgs() ) === 0 ) { + return null; + } + + $arg = $methodCall->getArgs()[0]->value; + $type = $scope->getType( $arg ); + + $classNames = $type->getConstantStrings(); + if ( count( $classNames ) === 1 ) { + $className = $classNames[0]->getValue(); + if ( $this->reflectionProvider->hasClass( $className ) ) { + return new ObjectType( $className ); + } + } + + return null; + } +} diff --git a/tests/PsrContainerDynamicReturnTypeExtensionTest.php b/tests/PsrContainerDynamicReturnTypeExtensionTest.php new file mode 100644 index 0000000..d79f7bc --- /dev/null +++ b/tests/PsrContainerDynamicReturnTypeExtensionTest.php @@ -0,0 +1,37 @@ + + */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/psr-container-types.php'); + } + + /** + * @dataProvider dataFileAsserts + */ + public function testFileAsserts( + string $assertType, + string $file, + mixed ...$args + ): void { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/phpstan-test.neon']; + } +} diff --git a/tests/StellarWpContainerDynamicReturnTypeExtensionTest.php b/tests/StellarWpContainerDynamicReturnTypeExtensionTest.php new file mode 100644 index 0000000..fa0702b --- /dev/null +++ b/tests/StellarWpContainerDynamicReturnTypeExtensionTest.php @@ -0,0 +1,37 @@ + + */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/stellarwp-container-types.php'); + } + + /** + * @dataProvider dataFileAsserts + */ + public function testFileAsserts( + string $assertType, + string $file, + mixed ...$args + ): void { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/phpstan-test.neon']; + } +} diff --git a/tests/data/psr-container-types.php b/tests/data/psr-container-types.php new file mode 100644 index 0000000..70784b8 --- /dev/null +++ b/tests/data/psr-container-types.php @@ -0,0 +1,56 @@ +get(\stdClass::class); + assertType('stdClass', $service); +} + +/** + * @param ContainerInterface $container + */ +function testPsrContainerGetWithDateTimeInterface(ContainerInterface $container): void +{ + $service = $container->get(\DateTimeInterface::class); + assertType('DateTimeInterface', $service); +} + +/** + * @param ContainerInterface $container + */ +function testPsrContainerGetWithDateTime(ContainerInterface $container): void +{ + $service = $container->get(\DateTime::class); + assertType('DateTime', $service); +} + +/** + * @param ContainerInterface $container + */ +function testPsrContainerGetWithStringId(ContainerInterface $container): void +{ + // When using a non-class string, the extension should return null and fall back to default behavior + $service = $container->get('some.service.id'); + assertType('mixed', $service); +} + +/** + * @param ContainerInterface $container + * @param string $dynamicId + */ +function testPsrContainerGetWithDynamicId(ContainerInterface $container, string $dynamicId): void +{ + // When using a dynamic string, the extension cannot determine the type + $service = $container->get($dynamicId); + assertType('mixed', $service); +} diff --git a/tests/data/stellarwp-container-types.php b/tests/data/stellarwp-container-types.php new file mode 100644 index 0000000..232fbe1 --- /dev/null +++ b/tests/data/stellarwp-container-types.php @@ -0,0 +1,56 @@ +get(\stdClass::class); + assertType('stdClass', $service); +} + +/** + * @param ContainerInterface $container + */ +function testStellarWpContainerGetWithDateTimeInterface(ContainerInterface $container): void +{ + $service = $container->get(\DateTimeInterface::class); + assertType('DateTimeInterface', $service); +} + +/** + * @param ContainerInterface $container + */ +function testStellarWpContainerGetWithDateTime(ContainerInterface $container): void +{ + $service = $container->get(\DateTime::class); + assertType('DateTime', $service); +} + +/** + * @param ContainerInterface $container + */ +function testStellarWpContainerGetWithStringId(ContainerInterface $container): void +{ + // When using a non-class string, the extension should return null and fall back to default behavior + $service = $container->get('some.service.id'); + assertType('mixed', $service); +} + +/** + * @param ContainerInterface $container + * @param string $dynamicId + */ +function testStellarWpContainerGetWithDynamicId(ContainerInterface $container, string $dynamicId): void +{ + // When using a dynamic string, the extension cannot determine the type + $service = $container->get($dynamicId); + assertType('mixed', $service); +} diff --git a/tests/phpstan-test.neon b/tests/phpstan-test.neon new file mode 100644 index 0000000..02cbca4 --- /dev/null +++ b/tests/phpstan-test.neon @@ -0,0 +1,6 @@ +includes: + - ../extension.neon + +parameters: + scanDirectories: + - %currentWorkingDirectory%/tests/data/ From 287a2170c0bee2fa061f8c3ff8d5592f9f16cc7f Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 10 Dec 2025 18:45:44 +0200 Subject: [PATCH 02/11] php 7.4 compat --- tests/PsrContainerDynamicReturnTypeExtensionTest.php | 3 ++- tests/StellarWpContainerDynamicReturnTypeExtensionTest.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/PsrContainerDynamicReturnTypeExtensionTest.php b/tests/PsrContainerDynamicReturnTypeExtensionTest.php index d79f7bc..6a56b46 100644 --- a/tests/PsrContainerDynamicReturnTypeExtensionTest.php +++ b/tests/PsrContainerDynamicReturnTypeExtensionTest.php @@ -18,11 +18,12 @@ public static function dataFileAsserts(): iterable /** * @dataProvider dataFileAsserts + * @param mixed ...$args */ public function testFileAsserts( string $assertType, string $file, - mixed ...$args + ...$args ): void { $this->assertFileAsserts($assertType, $file, ...$args); } diff --git a/tests/StellarWpContainerDynamicReturnTypeExtensionTest.php b/tests/StellarWpContainerDynamicReturnTypeExtensionTest.php index fa0702b..accc0d3 100644 --- a/tests/StellarWpContainerDynamicReturnTypeExtensionTest.php +++ b/tests/StellarWpContainerDynamicReturnTypeExtensionTest.php @@ -18,11 +18,12 @@ public static function dataFileAsserts(): iterable /** * @dataProvider dataFileAsserts + * @param mixed ...$args */ public function testFileAsserts( string $assertType, string $file, - mixed ...$args + ...$args ): void { $this->assertFileAsserts($assertType, $file, ...$args); } From 027500f375f04492ea6ffa86a9787d2024c7aca6 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Wed, 10 Dec 2025 19:59:10 +0200 Subject: [PATCH 03/11] chore: code improvements --- ...actContainerDynamicReturnTypeExtension.php | 80 +++++++++++++++++++ ...PsrContainerDynamicReturnTypeExtension.php | 58 +------------- ...rWpContainerDynamicReturnTypeExtension.php | 58 +------------- 3 files changed, 82 insertions(+), 114 deletions(-) create mode 100644 src/AbstractContainerDynamicReturnTypeExtension.php diff --git a/src/AbstractContainerDynamicReturnTypeExtension.php b/src/AbstractContainerDynamicReturnTypeExtension.php new file mode 100644 index 0000000..d198a4a --- /dev/null +++ b/src/AbstractContainerDynamicReturnTypeExtension.php @@ -0,0 +1,80 @@ +reflectionProvider = $reflectionProvider; + } + + /** + * Check if this extension handles the given method. + * + * @param MethodReflection $methodReflection The method reflection. + * + * @return bool + */ + public function isMethodSupported( MethodReflection $methodReflection ): bool { + return $methodReflection->getName() === 'get'; + } + + /** + * Get the return type for the method call. + * + * If the first argument is a class-string constant (e.g., Foo::class), + * this returns an ObjectType for that class. Otherwise, returns null + * to fall back to PHPStan's default behavior. + * + * @param MethodReflection $methodReflection The method reflection. + * @param MethodCall $methodCall The method call node. + * @param Scope $scope The analysis scope. + * + * @return Type|null + */ + public function getTypeFromMethodCall( + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): ?Type { + if ( count( $methodCall->getArgs() ) === 0 ) { + return null; + } + + $arg = $methodCall->getArgs()[0]->value; + $type = $scope->getType( $arg ); + + $classNames = $type->getConstantStrings(); + if ( count( $classNames ) === 1 ) { + $className = $classNames[0]->getValue(); + if ( $this->reflectionProvider->hasClass( $className ) ) { + return new ObjectType( $className ); + } + } + + return null; + } +} diff --git a/src/PsrContainerDynamicReturnTypeExtension.php b/src/PsrContainerDynamicReturnTypeExtension.php index b759a7d..6c03a8c 100644 --- a/src/PsrContainerDynamicReturnTypeExtension.php +++ b/src/PsrContainerDynamicReturnTypeExtension.php @@ -9,25 +9,12 @@ namespace DPanta\PHPStan\Containers; -use PhpParser\Node\Expr\MethodCall; -use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; use Psr\Container\ContainerInterface; /** * PHPStan extension that teaches PHPStan that PSR-11 Container::get(Foo::class) returns Foo. */ -class PsrContainerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - - private ReflectionProvider $reflectionProvider; - - public function __construct( ReflectionProvider $reflectionProvider ) { - $this->reflectionProvider = $reflectionProvider; - } +class PsrContainerDynamicReturnTypeExtension extends AbstractContainerDynamicReturnTypeExtension { /** * Get the class this extension applies to. @@ -37,47 +24,4 @@ public function __construct( ReflectionProvider $reflectionProvider ) { public function getClass(): string { return ContainerInterface::class; } - - /** - * Check if this extension handles the given method. - * - * @param MethodReflection $methodReflection The method reflection. - * - * @return bool - */ - public function isMethodSupported( MethodReflection $methodReflection ): bool { - return $methodReflection->getName() === 'get'; - } - - /** - * Get the return type for the method call. - * - * @param MethodReflection $methodReflection The method reflection. - * @param MethodCall $methodCall The method call node. - * @param Scope $scope The analysis scope. - * - * @return Type|null - */ - public function getTypeFromMethodCall( - MethodReflection $methodReflection, - MethodCall $methodCall, - Scope $scope - ): ?Type { - if ( count( $methodCall->getArgs() ) === 0 ) { - return null; - } - - $arg = $methodCall->getArgs()[0]->value; - $type = $scope->getType( $arg ); - - $classNames = $type->getConstantStrings(); - if ( count( $classNames ) === 1 ) { - $className = $classNames[0]->getValue(); - if ( $this->reflectionProvider->hasClass( $className ) ) { - return new ObjectType( $className ); - } - } - - return null; - } } diff --git a/src/StellarWpContainerDynamicReturnTypeExtension.php b/src/StellarWpContainerDynamicReturnTypeExtension.php index 1fa4e9d..219113e 100644 --- a/src/StellarWpContainerDynamicReturnTypeExtension.php +++ b/src/StellarWpContainerDynamicReturnTypeExtension.php @@ -9,25 +9,12 @@ namespace DPanta\PHPStan\Containers; -use PhpParser\Node\Expr\MethodCall; -use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\DynamicMethodReturnTypeExtension; -use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; use StellarWP\ContainerContract\ContainerInterface; /** * PHPStan extension that teaches PHPStan that StellarWP Container::get(Foo::class) returns Foo. */ -class StellarWpContainerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - - private ReflectionProvider $reflectionProvider; - - public function __construct( ReflectionProvider $reflectionProvider ) { - $this->reflectionProvider = $reflectionProvider; - } +class StellarWpContainerDynamicReturnTypeExtension extends AbstractContainerDynamicReturnTypeExtension { /** * Get the class this extension applies to. @@ -37,47 +24,4 @@ public function __construct( ReflectionProvider $reflectionProvider ) { public function getClass(): string { return ContainerInterface::class; } - - /** - * Check if this extension handles the given method. - * - * @param MethodReflection $methodReflection The method reflection. - * - * @return bool - */ - public function isMethodSupported( MethodReflection $methodReflection ): bool { - return $methodReflection->getName() === 'get'; - } - - /** - * Get the return type for the method call. - * - * @param MethodReflection $methodReflection The method reflection. - * @param MethodCall $methodCall The method call node. - * @param Scope $scope The analysis scope. - * - * @return Type|null - */ - public function getTypeFromMethodCall( - MethodReflection $methodReflection, - MethodCall $methodCall, - Scope $scope - ): ?Type { - if ( count( $methodCall->getArgs() ) === 0 ) { - return null; - } - - $arg = $methodCall->getArgs()[0]->value; - $type = $scope->getType( $arg ); - - $classNames = $type->getConstantStrings(); - if ( count( $classNames ) === 1 ) { - $className = $classNames[0]->getValue(); - if ( $this->reflectionProvider->hasClass( $className ) ) { - return new ObjectType( $className ); - } - } - - return null; - } } From 67b8927af6d71758161952bc89cf431a885f546a Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Thu, 11 Dec 2025 13:58:37 +0200 Subject: [PATCH 04/11] feat: better return type --- .gitignore | 7 +-- .phpunit.cache/test-results | 1 - ...actContainerDynamicReturnTypeExtension.php | 43 ++++++++----------- 3 files changed, 19 insertions(+), 32 deletions(-) delete mode 100644 .phpunit.cache/test-results diff --git a/.gitignore b/.gitignore index c87dbed..d15b606 100644 --- a/.gitignore +++ b/.gitignore @@ -10,12 +10,7 @@ tests/_support/_generated .vscode/ # Tests -codeception.yml -tests/_output/* -tests/*.suite.yml -tests/*.pem -tests/*.cert -tests/*.key +.phpunit.cache .DS_Store DOCKER_ENV diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results deleted file mode 100644 index c7aae5e..0000000 --- a/.phpunit.cache/test-results +++ /dev/null @@ -1 +0,0 @@ -{"version":2,"defects":{"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:37":7,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:46":7,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:55":7,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:37":7,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:46":7,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:55":7},"times":{"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:37":0.003,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:46":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:55":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:65":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:76":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:37":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:46":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:55":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:65":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:76":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:16":0.001,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:25":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:34":0,"DPanta\\PHPStan\\Containers\\Tests\\PsrContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/psr-container-types.php:44":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:16":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:25":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:34":0,"DPanta\\PHPStan\\Containers\\Tests\\StellarWpContainerDynamicReturnTypeExtensionTest::testFileAsserts#\/Users\/dpan\/repos\/pantadev\/container-phpstan\/tests\/data\/stellarwp-container-types.php:44":0}} \ No newline at end of file diff --git a/src/AbstractContainerDynamicReturnTypeExtension.php b/src/AbstractContainerDynamicReturnTypeExtension.php index d198a4a..f4fe276 100644 --- a/src/AbstractContainerDynamicReturnTypeExtension.php +++ b/src/AbstractContainerDynamicReturnTypeExtension.php @@ -12,7 +12,7 @@ use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -25,12 +25,6 @@ */ abstract class AbstractContainerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension { - private ReflectionProvider $reflectionProvider; - - public function __construct( ReflectionProvider $reflectionProvider ) { - $this->reflectionProvider = $reflectionProvider; - } - /** * Check if this extension handles the given method. * @@ -56,25 +50,24 @@ public function isMethodSupported( MethodReflection $methodReflection ): bool { * @return Type|null */ public function getTypeFromMethodCall( - MethodReflection $methodReflection, - MethodCall $methodCall, - Scope $scope - ): ?Type { - if ( count( $methodCall->getArgs() ) === 0 ) { - return null; - } + MethodReflection $methodReflection, + MethodCall $methodCall, + Scope $scope + ): Type { + $args = $methodCall->getArgs(); + if (count($args) !== 1) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + } - $arg = $methodCall->getArgs()[0]->value; - $type = $scope->getType( $arg ); + $argType = $scope->getType($args[0]->value); + if (count($argType->getConstantStrings()) === 0) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + } - $classNames = $type->getConstantStrings(); - if ( count( $classNames ) === 1 ) { - $className = $classNames[0]->getValue(); - if ( $this->reflectionProvider->hasClass( $className ) ) { - return new ObjectType( $className ); - } - } + if ($argType->isClassString()->no()) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + } - return null; - } + return new ObjectType($argType->getValue()); + } } From 8de46c6fd9f17b33dc757589a91dd84a8313d11c Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Thu, 11 Dec 2025 14:04:26 +0200 Subject: [PATCH 05/11] docs: Updates and give credits --- .gitattributes | 5 +---- .gitignore | 4 +++- CLAUDE.md | 52 -------------------------------------------------- README.md | 9 +++++---- 4 files changed, 9 insertions(+), 61 deletions(-) delete mode 100644 CLAUDE.md diff --git a/.gitattributes b/.gitattributes index 227f633..8d6b961 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,7 +3,4 @@ composer.lock text -diff phpunit.* export-ignore tests export-ignore -codeception.* export-ignore -phpcs.xml export-ignore -docs export-ignore -CLAUDE.md export-ignore \ No newline at end of file +phpunit.xml export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index d15b606..4f51263 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ tests/_support/_generated DOCKER_ENV Dockerfile-* output.log -docker_tag \ No newline at end of file +docker_tag + +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 206cd4e..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,52 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is a PHPStan extension (`dpanta94/phpstan-containers`) that provides dynamic return type resolution for dependency injection containers. When code calls `$container->get(Foo::class)`, PHPStan will understand that the return type is `Foo`. - -Supports two container interfaces: -- `StellarWP\ContainerContract\ContainerInterface` -- `Psr\Container\ContainerInterface` (PSR-11) - -## Commands - -```bash -# Run tests -composer test - -# Run PHPStan analysis -composer test:phpstan - -# Install dependencies -composer install - -# Run PHP syntax linting -vendor/bin/parallel-lint src/ -``` - -## Architecture - -- **`src/StellarWpContainerDynamicReturnTypeExtension.php`**: PHPStan extension for StellarWP container contract. Intercepts `get()` method calls on `StellarWP\ContainerContract\ContainerInterface` and resolves the return type based on the class name string argument. - -- **`src/PsrContainerDynamicReturnTypeExtension.php`**: PHPStan extension for PSR-11 container contract. Intercepts `get()` method calls on `Psr\Container\ContainerInterface` and resolves the return type based on the class name string argument. - -- **`extension.neon`**: PHPStan configuration file that registers both extensions. - -- **`composer.json`**: Defines the package as type `phpstan-extension` with auto-registration via the `extra.phpstan.includes` config. - -## Testing - -Tests use PHPStan's `TypeInferenceTestCase` to verify type resolution: -- **`tests/StellarWpContainerDynamicReturnTypeExtensionTest.php`**: Tests for StellarWP extension -- **`tests/PsrContainerDynamicReturnTypeExtensionTest.php`**: Tests for PSR-11 extension -- **`tests/data/`**: Test fixture files containing `assertType()` calls - -## Key Details - -- Namespace: `DPanta\PHPStan\Containers` -- PSR-4 autoloading from `src/` -- Requires PHP 7.4+ and PHPStan 2.0+ -- Both extensions implement `DynamicMethodReturnTypeExtension` interface from PHPStan -- Uses `ReflectionProvider` to check class existence (not PHP's native `class_exists`) diff --git a/README.md b/README.md index 5970d81..99b585a 100644 --- a/README.md +++ b/README.md @@ -54,15 +54,16 @@ class MyService { ``` The extension resolves types when: + - The argument to `get()` is a class-string constant (e.g., `Foo::class`) - The class or interface exists in the codebase When using string service IDs (e.g., `$container->get('mailer')`), the extension falls back to the default `mixed` return type. -## How It Works - -The extension implements PHPStan's `DynamicMethodReturnTypeExtension` interface to intercept calls to `ContainerInterface::get()`. When it detects a class-string argument, it returns an `ObjectType` for that class, giving PHPStan full knowledge of the returned object's type. - ## License MIT + +## Credits + +This package is inspired by [Phil Nelson](https://github.com/phil-nelson)'s [phpstan-container-extension](https://packagist.org/packages/phil-nelson/phpstan-container-extension). From 00700d33c1ea87290de0ab4cd6324b0e8311a111 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Thu, 11 Dec 2025 14:30:44 +0200 Subject: [PATCH 06/11] Fix static analysis --- src/AbstractContainerDynamicReturnTypeExtension.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/AbstractContainerDynamicReturnTypeExtension.php b/src/AbstractContainerDynamicReturnTypeExtension.php index f4fe276..b88eafe 100644 --- a/src/AbstractContainerDynamicReturnTypeExtension.php +++ b/src/AbstractContainerDynamicReturnTypeExtension.php @@ -47,7 +47,7 @@ public function isMethodSupported( MethodReflection $methodReflection ): bool { * @param MethodCall $methodCall The method call node. * @param Scope $scope The analysis scope. * - * @return Type|null + * @return Type */ public function getTypeFromMethodCall( MethodReflection $methodReflection, @@ -68,6 +68,15 @@ public function getTypeFromMethodCall( return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); } - return new ObjectType($argType->getValue()); + if (!method_exists($argType, 'getValue')) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + } + + $value = $argType->getValue(); + if (!is_string($value)) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + } + + return new ObjectType($value); } } From 54d5f5adeee0dc52a496e0b767033e0160aed68f Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Thu, 11 Dec 2025 14:42:42 +0200 Subject: [PATCH 07/11] chore: style fixes --- ...actContainerDynamicReturnTypeExtension.php | 68 ++++++++++--------- ...PsrContainerDynamicReturnTypeExtension.php | 22 +++--- ...rWpContainerDynamicReturnTypeExtension.php | 22 +++--- 3 files changed, 59 insertions(+), 53 deletions(-) diff --git a/src/AbstractContainerDynamicReturnTypeExtension.php b/src/AbstractContainerDynamicReturnTypeExtension.php index b88eafe..9d49129 100644 --- a/src/AbstractContainerDynamicReturnTypeExtension.php +++ b/src/AbstractContainerDynamicReturnTypeExtension.php @@ -1,4 +1,5 @@ getName() === 'get'; - } +abstract class AbstractContainerDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension +{ + /** + * Check if this extension handles the given method. + * + * @param MethodReflection $methodReflection The method reflection. + * + * @return bool + */ + public function isMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'get'; + } - /** - * Get the return type for the method call. - * - * If the first argument is a class-string constant (e.g., Foo::class), - * this returns an ObjectType for that class. Otherwise, returns null - * to fall back to PHPStan's default behavior. - * - * @param MethodReflection $methodReflection The method reflection. - * @param MethodCall $methodCall The method call node. - * @param Scope $scope The analysis scope. - * - * @return Type - */ - public function getTypeFromMethodCall( + /** + * Get the return type for the method call. + * + * If the first argument is a class-string constant (e.g., Foo::class), + * this returns an ObjectType for that class. Otherwise, returns null + * to fall back to PHPStan's default behavior. + * + * @param MethodReflection $methodReflection The method reflection. + * @param MethodCall $methodCall The method call node. + * @param Scope $scope The analysis scope. + * + * @return Type + */ + public function getTypeFromMethodCall( MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope @@ -68,14 +70,14 @@ public function getTypeFromMethodCall( return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); } - if (!method_exists($argType, 'getValue')) { - return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - } + if (!method_exists($argType, 'getValue')) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + } - $value = $argType->getValue(); - if (!is_string($value)) { - return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); - } + $value = $argType->getValue(); + if (!is_string($value)) { + return ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType(); + } return new ObjectType($value); } diff --git a/src/PsrContainerDynamicReturnTypeExtension.php b/src/PsrContainerDynamicReturnTypeExtension.php index 6c03a8c..e898bee 100644 --- a/src/PsrContainerDynamicReturnTypeExtension.php +++ b/src/PsrContainerDynamicReturnTypeExtension.php @@ -1,4 +1,5 @@ Date: Thu, 11 Dec 2025 14:43:20 +0200 Subject: [PATCH 08/11] chore: add pint as a workflow --- .gitattributes | 3 ++- .github/workflows/phpstan.yml | 1 - .github/workflows/pint.yml | 31 +++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 1 - composer.json | 1 + pint.json | 7 +++++++ 6 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/pint.yml create mode 100644 pint.json diff --git a/.gitattributes b/.gitattributes index 8d6b961..eea346a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,4 +3,5 @@ composer.lock text -diff phpunit.* export-ignore tests export-ignore -phpunit.xml export-ignore \ No newline at end of file +phpunit.xml export-ignore +pint.json export-ignore \ No newline at end of file diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index b20e539..977c1d9 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -5,7 +5,6 @@ on: push: branches: - main - - master jobs: phpstan: diff --git a/.github/workflows/pint.yml b/.github/workflows/pint.yml new file mode 100644 index 0000000..f326c7b --- /dev/null +++ b/.github/workflows/pint.yml @@ -0,0 +1,31 @@ +name: fix code styling + +on: + pull_request: + push: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer:v2, laravel/pint + extensions: json, dom, curl, libxml, mbstring + coverage: none + + - name: Run Pint + run: pint + + - name: Commit linted files + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: 'chore: fix code styling' \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c534b7f..1083160 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,7 +5,6 @@ on: push: branches: - main - - master jobs: tests: diff --git a/composer.json b/composer.json index 7778144..5f0d336 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "phpstan/phpstan": "^2.0" }, "require-dev": { + "laravel/pint": "^1.26", "php-parallel-lint/php-parallel-lint": "^1.1", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.5 || ^10.0", diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..f255aa6 --- /dev/null +++ b/pint.json @@ -0,0 +1,7 @@ +{ + "preset": "psr12", + "exclude": [ + "vendor", + "tests" + ] +} \ No newline at end of file From 174e59d3bad72fe8991ff62368ac6feeea321c49 Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Thu, 11 Dec 2025 14:46:41 +0200 Subject: [PATCH 09/11] added changelog file --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ffd982b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard. + +## [0.1.0] 2025-12-11 + +* Feature - Initial release of PHPStan Container Extensions. + +[0.1.0]: https://github.com/dpanta94/phpstan-containers/releases/tag/0.1.0 From 4fd4b70e8cb6b90d860ee88c25a80bebe7c6e31d Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Thu, 11 Dec 2025 14:48:26 +0200 Subject: [PATCH 10/11] add PR template --- .github/pull_request_template.md | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b5f48ec --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,39 @@ +## Summary + + + +## Type of Change + +- [ ] ๐Ÿ› Bug fix (non-breaking change which fixes an issue) +- [ ] โœจ New feature (non-breaking change which adds functionality) +- [ ] ๐Ÿ’ฅ Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] ๐Ÿ“š Documentation update +- [ ] โšก Performance improvement +- [ ] โ™ป๏ธ Code refactoring +- [ ] ๐Ÿงช Test improvements +- [ ] ๐Ÿ”ง Chore (build process, dependencies, etc.) + +## Related Issues + + + + + +Closes # + +## Pre-Submission Checklist + + + +### Code Quality + +- [ ] **PHPStan analysis passes** +- [ ] **Coding standards pass** +- [ ] **All tests pass** +- [ ] **No debug code** (var_dump, error_log, etc.) left in production code +- [ ] **No commented-out code** unless specifically needed for reference +- [ ] **Documentation updated** for any new features or changed behavior +- [ ] **New tests** have been added for new functionality +- [ ] **All existing tests** continue to pass + +**๐Ÿ“– Read the full contributing guidelines: [CONTRIBUTING.md](/.github/CONTRIBUTING.md)** From 28d0835da361a59ed6f40ab6dbe123d18dc8972e Mon Sep 17 00:00:00 2001 From: Dimitrios Pantazis Date: Thu, 11 Dec 2025 14:50:50 +0200 Subject: [PATCH 11/11] remove laravel pint as a dependency --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 5f0d336..7778144 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,6 @@ "phpstan/phpstan": "^2.0" }, "require-dev": { - "laravel/pint": "^1.26", "php-parallel-lint/php-parallel-lint": "^1.1", "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^9.5 || ^10.0",