diff --git a/.github/workflows/buildTest.yml b/.github/workflows/buildTest.yml index 59b6416..15081a1 100644 --- a/.github/workflows/buildTest.yml +++ b/.github/workflows/buildTest.yml @@ -12,9 +12,9 @@ jobs: name: Build and test on ${{ matrix.php }} steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Force PHP ${{ matrix.php }} - uses: nanasess/setup-php@master + uses: nanasess/setup-php@v4 with: php-version: ${{ matrix.php }} - name: Validate composer.json and composer.lock @@ -37,11 +37,10 @@ jobs: ./vendor/bin/phpstan --no-interaction --no-ansi analyse - name: Mess Detector Sources run: | - ./vendor/bin/phpmd src text codesize,controversial,design,naming,unusedcode,design - - name: Mess Detector Tests - run: | - ./vendor/bin/phpmd tests text codesize,controversial,design - ./vendor/bin/phpmd src text codesize,controversial,naming,unusedcode + ./vendor/bin/phpmd src text codesize,controversial,naming,unusedcode + - name: Mess Detector Tests + run: | + ./vendor/bin/phpmd tests text codesize,controversial,design - name: php-cs-fixer run: | ./tools/php-cs-fixer/vendor/bin/php-cs-fixer fix -v --config=.php-cs-fixer.dist.php --using-cache=no --dry-run diff --git a/CHANGELOG.md b/CHANGELOG.md index a34c8d0..fd9787c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,29 @@ Intended to follow [«Keep a Changelog»](https://keepachangelog.com/en/) ---- -## Upcoming +## Upcomming + +- Remove support for the interface +- Deprecate (abandon) the interface package +- create conflict with interface for version 2 + ```json + { + "conflict": { + "ebln/ebln/phpstan-factory-mark": "*" + } + } + ``` +---- + +## [1.0.0] + +### Added - Support for attributes ----- +### Removed +* Support for PHP < 7.4 +* Support for PHPStan < 1.11 ## [0.0.2] diff --git a/README.md b/README.md index 78a5b06..6034745 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,44 @@ ebln/phpstan-factory-rule Enforce that your classes get only instantiated by the factories you define! -## Usage +## Usage with support for attributes +Require this package: `composer require --dev ebln/phpstan-factory-rule` -Install this package and the marking package alongside with PHPStan. +Add the `ForceFactory` attribute to your class, and supply all class names as arguments, +which shall be allowed to instanciate your object. +```php + */ + private array $allowedFactories; + + /** @param class-string ...$factories */ + public function __construct(string ...$factories) + { + $allowedFactories = []; + foreach ($factories as $factory) { + $allowedFactories[$factory] = $factory; + } + + $this->allowedFactories = array_values($allowedFactories); + } + + /** @return array */ + public function getAllowedFactories(): array + { + return $this->allowedFactories; + } +} diff --git a/composer.json b/composer.json index 575d7aa..243c884 100644 --- a/composer.json +++ b/composer.json @@ -19,10 +19,11 @@ "phpmd/phpmd": "^2.10", "phpunit/phpunit": "^9.5", "roave/security-advisories": "dev-latest", - "vimeo/psalm": "^4.12" + "vimeo/psalm": "^5.24" }, "autoload": { "psr-4": { + "Ebln\\Attrib\\": "attrib/", "Ebln\\PHPStan\\EnforceFactory\\": "src/" } }, @@ -52,7 +53,7 @@ "psalm --find-unused-psalm-suppress", "phpstan analyse", "@style-check", - "phpmd src ansi codesize,controversial,design,naming,unusedcode,design", + "phpmd src ansi codesize,controversial,naming,unusedcode", "phpmd tests ansi codesize,controversial,design" ], "style-check": "php-cs-fixer fix -v --config=.php-cs-fixer.dist.php --using-cache=no --dry-run", diff --git a/composer.lock b/composer.lock index 012d1b9..46fff23 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d34c0cd3e1aa9b32f4c362580e4f4dd4", + "content-hash": "cb320edf79fd64c86777c993b2440def", "packages": [ { "name": "ebln/phpstan-factory-mark", @@ -270,79 +270,6 @@ ], "time": "2024-04-13T18:00:56+00:00" }, - { - "name": "composer/package-versions-deprecated", - "version": "1.11.99.5", - "source": { - "type": "git", - "url": "https://github.com/composer/package-versions-deprecated.git", - "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", - "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^1.1.0 || ^2.0", - "php": "^7 || ^8" - }, - "replace": { - "ocramius/package-versions": "1.11.99" - }, - "require-dev": { - "composer/composer": "^1.9.3 || ^2.0@dev", - "ext-zip": "^1.13", - "phpunit/phpunit": "^6.5 || ^7" - }, - "type": "composer-plugin", - "extra": { - "class": "PackageVersions\\Installer", - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "PackageVersions\\": "src/PackageVersions" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be" - } - ], - "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "support": { - "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-01-17T14:14:24+00:00" - }, { "name": "composer/pcre", "version": "3.1.4", @@ -1225,6 +1152,67 @@ }, "time": "2022-03-02T22:36:06+00:00" }, + { + "name": "fidry/cpu-core-counter", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-02-07T09:43:46+00:00" + }, { "name": "justinrainbow/json-schema", "version": "v5.2.13", @@ -1521,59 +1509,6 @@ }, "time": "2024-03-17T08:10:35+00:00" }, - { - "name": "openlss/lib-array2xml", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/nullivex/lib-array2xml.git", - "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", - "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "autoload": { - "psr-0": { - "LSS": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Bryan Tong", - "email": "bryan@nullivex.com", - "homepage": "https://www.nullivex.com" - }, - { - "name": "Tony Butler", - "email": "spudz76@gmail.com", - "homepage": "https://www.nullivex.com" - } - ], - "description": "Array2XML conversion library credit to lalit.org", - "homepage": "https://www.nullivex.com", - "keywords": [ - "array", - "array conversion", - "xml", - "xml conversion" - ], - "support": { - "issues": "https://github.com/nullivex/lib-array2xml/issues", - "source": "https://github.com/nullivex/lib-array2xml/tree/master" - }, - "time": "2019-03-29T20:06:56+00:00" - }, { "name": "pdepend/pdepend", "version": "2.16.2", @@ -4343,6 +4278,70 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "spatie/array-to-xml", + "version": "2.17.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", + "reference": "5cbec9c6ab17e320c58a259f0cebe88bde4a7c46", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^7.4|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "phpunit/phpunit": "^9.0", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" + } + ], + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], + "support": { + "source": "https://github.com/spatie/array-to-xml/tree/2.17.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2022-12-26T08:22:07+00:00" + }, { "name": "symfony/config", "version": "v5.4.40", @@ -5515,24 +5514,24 @@ }, { "name": "vimeo/psalm", - "version": "4.30.0", + "version": "5.24.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d0bc6e25d89f649e4f36a534f330f8bb4643dd69" + "reference": "462c80e31c34e58cc4f750c656be3927e80e550e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d0bc6e25d89f649e4f36a534f330f8bb4643dd69", - "reference": "d0bc6e25d89f649e4f36a534f330f8bb4643dd69", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/462c80e31c34e58cc4f750c656be3927e80e550e", + "reference": "462c80e31c34e58cc4f750c656be3927e80e550e", "shasum": "" }, "require": { "amphp/amp": "^2.4.2", "amphp/byte-stream": "^1.5", - "composer/package-versions-deprecated": "^1.8.0", + "composer-runtime-api": "^2", "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", "dnoegel/php-xdg-base-dir": "^0.1.1", "ext-ctype": "*", "ext-dom": "*", @@ -5541,35 +5540,38 @@ "ext-mbstring": "*", "ext-simplexml": "*", "ext-tokenizer": "*", - "felixfbecker/advanced-json-rpc": "^3.0.3", - "felixfbecker/language-server-protocol": "^1.5", + "felixfbecker/advanced-json-rpc": "^3.1", + "felixfbecker/language-server-protocol": "^1.5.2", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", - "nikic/php-parser": "^4.13", - "openlss/lib-array2xml": "^1.0", - "php": "^7.1|^8", - "sebastian/diff": "^3.0 || ^4.0", - "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0 || ^6.0", - "symfony/polyfill-php80": "^1.25", - "webmozart/path-util": "^2.3" + "nikic/php-parser": "^4.16", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" + }, + "conflict": { + "nikic/php-parser": "4.17.0" }, "provide": { "psalm/psalm": "self.version" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.2", - "brianium/paratest": "^4.0||^6.0", + "amphp/phpunit-util": "^2.0", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", "php-parallel-lint/php-parallel-lint": "^1.2", - "phpdocumentor/reflection-docblock": "^5", - "phpmyadmin/sql-parser": "5.1.0||dev-master", - "phpspec/prophecy": ">=1.9.0", - "phpstan/phpdoc-parser": "1.2.* || 1.6.4", - "phpunit/phpunit": "^9.0", - "psalm/plugin-phpunit": "^0.16", - "slevomat/coding-standard": "^7.0", - "squizlabs/php_codesniffer": "^3.5", - "symfony/process": "^4.3 || ^5.0 || ^6.0", - "weirdan/prophecy-shim": "^1.0 || ^2.0" + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { "ext-curl": "In order to send data to shepherd", @@ -5582,20 +5584,17 @@ "psalm-refactor", "psalter" ], - "type": "library", + "type": "project", "extra": { "branch-alias": { - "dev-master": "4.x-dev", + "dev-master": "5.x-dev", + "dev-4.x": "4.x-dev", "dev-3.x": "3.x-dev", "dev-2.x": "2.x-dev", "dev-1.x": "1.x-dev" } }, "autoload": { - "files": [ - "src/functions.php", - "src/spl_object_id.php" - ], "psr-4": { "Psalm\\": "src/Psalm/" } @@ -5613,13 +5612,15 @@ "keywords": [ "code", "inspection", - "php" + "php", + "static analysis" ], "support": { + "docs": "https://psalm.dev/docs", "issues": "https://github.com/vimeo/psalm/issues", - "source": "https://github.com/vimeo/psalm/tree/4.30.0" + "source": "https://github.com/vimeo/psalm" }, - "time": "2022-11-06T20:37:08+00:00" + "time": "2024-05-01T19:32:08+00:00" }, { "name": "webmozart/assert", @@ -5678,57 +5679,6 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" - }, - { - "name": "webmozart/path-util", - "version": "2.3.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/path-util.git", - "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725", - "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "webmozart/assert": "~1.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\PathUtil\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", - "support": { - "issues": "https://github.com/webmozart/path-util/issues", - "source": "https://github.com/webmozart/path-util/tree/2.3.0" - }, - "abandoned": "symfony/filesystem", - "time": "2015-12-17T08:42:14+00:00" } ], "aliases": [], diff --git a/phpstan-baseline-7.4.neon b/phpstan-baseline-7.4.neon new file mode 100644 index 0000000..e2a3c34 --- /dev/null +++ b/phpstan-baseline-7.4.neon @@ -0,0 +1,13 @@ +parameters: + ignoreErrors: + - + message: "#^Call to an undefined method ReflectionClass\\:\\:getAttributes\\(\\)\\.$#" + path: src/ForceFactoryRule.php + + - + message: "#^Method Ebln\\\\PHPStan\\\\EnforceFactory\\\\ForceFactoryRule\\:\\:getFactoriesFromAttributeByClass\\(\\) has parameter \\$reflection with generic class ReflectionClass but does not specify its types\\: T$#" + path: src/ForceFactoryRule.php + + - + message: "#^PHPDoc tag @param for parameter \\$reflection with type PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionClass\\|PHPStan\\\\BetterReflection\\\\Reflection\\\\Adapter\\\\ReflectionEnum is not subtype of native type ReflectionClass\\.$#" + path: src/ForceFactoryRule.php diff --git a/phpstan-ignore-by-version.neon.php b/phpstan-ignore-by-version.neon.php new file mode 100644 index 0000000..49d2af2 --- /dev/null +++ b/phpstan-ignore-by-version.neon.php @@ -0,0 +1,14 @@ +reflectionProvider = $reflectionProvider; + } + public function getNodeType(): string { return \PhpParser\Node\Expr\New_::class; @@ -26,17 +35,18 @@ public function processNode(Node $node, Scope $scope): array if (!$isName) { continue; // newly instantiated class name couldn't be infered } + /** @var class-string $class → sadly Psalm cannot be convinced to return class-string for getClassNames in reasonable amount of time */ $allowedFactories = $this->getAllowedFactories($class); if (null === $allowedFactories) { - continue; // newly instantiated class dowsn't implement ForceFactory interface + continue; // newly instantiated class doesn't implement ForceFactory interface } - if (empty($allowedFactories)) { + if ([] === $allowedFactories) { $errors[] = RuleErrorBuilder::message( - ltrim($class, '\\') . ' cannot be instantiated by other classes; see ' . ForceFactoryInterface::class + ltrim($class, '\\') . ' has either no factories defined or a conflict between interface and attribute!' )->build(); - continue; + continue; // bogus configuration } /** @psalm-suppress PossiblyNullReference | sad that even phpstan cannot infer that from isInClass */ @@ -57,17 +67,78 @@ public function processNode(Node $node, Scope $scope): array } /** + * @phpstan-param class-string $className + * * @return null|string[] List of FQCNs * * @phpstan-return null|class-string[] */ private function getAllowedFactories(string $className): ?array { - if (!is_a($className, ForceFactoryInterface::class, true)) { + $allowedFactories = $this->getFactoriesFromAttribute($className); + if (is_a($className, ForceFactoryInterface::class, true)) { + /* phpstan-var class-string $className */ + $interfaceFactories = $className::getFactories(); + sort($interfaceFactories); + if (null === $allowedFactories) { + $allowedFactories = $interfaceFactories; + } elseif ($allowedFactories !== $interfaceFactories) { + $allowedFactories = []; // Will result in a bogus definition error + } + } + + return $allowedFactories; + } + + /** + * @phpstan-param class-string $className + * + * @return array + */ + private function getFactoriesFromAttribute(string $className): ?array + { + if (\PHP_VERSION_ID < 80000 && $this->reflectionProvider->hasClass($className)) { return null; } - return $className::getFactories(); + $reflection = $this->reflectionProvider->getClass($className); + /* psalm-suppress UndefinedClass */ + $allowedFactories = []; + do { + /** @psalm-suppress UndefinedClass */ + $allowedFactories = [...$allowedFactories, ...$this->getFactoriesFromAttributeByClass($reflection->getNativeReflection())]; + } while ($reflection = $reflection->getParentClass()); + + if (empty($allowedFactories)) { + return null; + } + $allowedFactories = array_filter($allowedFactories); + sort($allowedFactories); + + return $allowedFactories; + } + + /** + * @psalm-suppress UndefinedDocblockClass,MismatchingDocblockParamType + * + * @psalm-param \PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass|\PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum $reflection + * + * @return array + */ + private function getFactoriesFromAttributeByClass(\ReflectionClass $reflection): array + { + /** @psalm-suppress UndefinedClass */ + foreach ($reflection->getAttributes() as $attribute) { + if (ForceFactory::class === $attribute->getName()) { + /** @var ForceFactory $forceFactory */ + $forceFactory = $attribute->newInstance(); + $allowedFactories = $forceFactory->getAllowedFactories(); + + return empty($allowedFactories) ? [null] : $allowedFactories; + } + } + + return []; } /** @@ -81,8 +152,8 @@ private function getAllowedFactories(string $className): ?array * * @psalm-return array * - * @license https://github.com/phpstan/phpstan/blob/1.1.2/LICENSE - * @author Ondřej Mirtes et al. https://github.com/phpstan/phpstan-src/blob/0.12.x/src/Rules/Classes/InstantiationRule.php#blob_contributors_box + * @license https://github.com/phpstan/phpstan-src/blob/1.11.x/LICENSE + * @author Ondřej Mirtes et al. https://github.com/phpstan/phpstan-src/blame/1.11.x/src/Rules/Classes/InstantiationRule.php * @author ebln * * @see \PHPStan\Rules\Classes\InstantiationRule::getClassNames @@ -93,8 +164,8 @@ private function getClassNames(\PhpParser\Node $node, \PHPStan\Analyser\Scope $s return [[(string)$node->class, \true]]; } if ($node->class instanceof \PhpParser\Node\Stmt\Class_) { - $anonymousClassType = $scope->getType($node); - if (!$anonymousClassType instanceof \PHPStan\Type\TypeWithClassName) { + $classNames = $scope->getType($node)->getObjectClassNames(); + if ([] === $classNames) { throw new \PHPStan\ShouldNotHappenException(); } // Report back extended class! @@ -102,8 +173,10 @@ private function getClassNames(\PhpParser\Node $node, \PHPStan\Analyser\Scope $s return [[$node->class->extends->toString(), \true]]; } - // we don't care about the anonymous class' name and abort processing early - return [[$anonymousClassType->getClassName(), \false]]; + return array_map( + static fn (string $className) => [$className, \false], + $classNames, + ); } $type = $scope->getType($node->class); diff --git a/tests/AttribForceFactoryRuleTest.php b/tests/AttribForceFactoryRuleTest.php new file mode 100644 index 0000000..d57f165 --- /dev/null +++ b/tests/AttribForceFactoryRuleTest.php @@ -0,0 +1,88 @@ += 8.0 + * @extends RuleTestCase + */ +class AttribForceFactoryRuleTest extends RuleTestCase +{ + private const ERROR_MESSAGE = 'Test\Ebln\PHPStan\EnforceFactory\dataAttrib\code\ForcedFactoryProduct must be instantiated by Test\Ebln\PHPStan\EnforceFactory\dataAttrib\ForcedFactory or Test\Ebln\PHPStan\EnforceFactory\dataAttrib\TraitFactory!'; + + // Sadly this remains a vector, as phpstan fails to infer the created class name + public function testLoopholeFactory(): void + { + $this->analyse([__DIR__ . '/dataAttrib/LoopholeFactory.php'], []); + } + + // Sadly this remains a vector, as phpstan fails to infer the created class name + public function testLoopholeInvoker(): void + { + $this->analyse([__DIR__ . '/dataAttrib/LoopholeFactory.php', __DIR__ . '/dataAttrib/LoopholeInvoker.php'], []); + } + + public function testEmptyAllowedClasses(): void + { + $this->analyse([__DIR__ . '/dataAttrib/EmptyFactory.php'], [ + [ + 'Test\Ebln\PHPStan\EnforceFactory\dataAttrib\code\EmptyProduct has either no factories defined or a conflict between interface and attribute!', + 13, + ], + ]); + } + + public function testRogueFactory(): void + { + $offset = 1; + $this->analyse([__DIR__ . '/dataAttrib/RogueFactory.php'], [ + [self::ERROR_MESSAGE, 15 + $offset], + [self::ERROR_MESSAGE, 22 + $offset], + [self::ERROR_MESSAGE, 29 + $offset], + [self::ERROR_MESSAGE, 40 + $offset], + [self::ERROR_MESSAGE, 40 + $offset], + [self::ERROR_MESSAGE, 51 + $offset], + [self::ERROR_MESSAGE, 56 + $offset], + ['Test\Ebln\PHPStan\EnforceFactory\dataAttrib\code\ExtendedProduct must be instantiated by Test\Ebln\PHPStan\EnforceFactory\dataAttrib\ForcedFactory or Test\Ebln\PHPStan\EnforceFactory\dataAttrib\TraitFactory!', 69 + $offset], + [self::ERROR_MESSAGE, 95 + $offset], + ['Test\Ebln\PHPStan\EnforceFactory\dataAttrib\code\IndependentForcedFactoryProduct must be instantiated by Test\Ebln\PHPStan\EnforceFactory\dataAttrib\IndependentFactory!', 100 + $offset], + ]); + } + + public function testRogueFactoryAndTrait(): void + { + $this->analyse([__DIR__ . '/dataAttrib/RogueTraitFactory.php', __DIR__ . '/dataAttrib/FactoryTrait.php'], [ + [self::ERROR_MESSAGE, 13], + [self::ERROR_MESSAGE, 20], + [self::ERROR_MESSAGE, 27], + ]); + } + + public function testTraitedFactory(): void + { + $this->analyse([__DIR__ . '/dataAttrib/TraitFactory.php', __DIR__ . '/dataAttrib/FactoryTrait.php'], []); + } + + public function testAllowedFactory(): void + { + $this->analyse([__DIR__ . '/dataAttrib/ForcedFactory.php'], []); + } + + public function testIndependentFactory(): void + { + $offset = 1; + $this->analyse([__DIR__ . '/dataAttrib/IndependentFactory.php'], [ + ]); + } + + protected function getRule(): Rule + { + return new ForceFactoryRule($this->createReflectionProvider()); + } +} diff --git a/tests/ForceFactoryRuleTest.php b/tests/ForceFactoryRuleTest.php index 5520bd8..3bfdb1f 100644 --- a/tests/ForceFactoryRuleTest.php +++ b/tests/ForceFactoryRuleTest.php @@ -25,7 +25,7 @@ public function testEmptyAllowedClasses(): void { $this->analyse([__DIR__ . '/data/EmptyFactory.php'], [ [ - 'Test\Ebln\PHPStan\EnforceFactory\data\code\EmptyProduct cannot be instantiated by other classes; see Ebln\PHPStan\EnforceFactory\ForceFactoryInterface', + 'Test\Ebln\PHPStan\EnforceFactory\data\code\EmptyProduct has either no factories defined or a conflict between interface and attribute!', 13, ], ]); @@ -67,6 +67,6 @@ public function testAllowedFactory(): void protected function getRule(): Rule { - return new ForceFactoryRule(); + return new ForceFactoryRule($this->createReflectionProvider()); } } diff --git a/tests/MixedForceFactoryRuleTest.php b/tests/MixedForceFactoryRuleTest.php new file mode 100644 index 0000000..a7709f2 --- /dev/null +++ b/tests/MixedForceFactoryRuleTest.php @@ -0,0 +1,42 @@ += 8.0 + * @extends RuleTestCase + */ +class MixedForceFactoryRuleTest extends RuleTestCase +{ + public function testAttributeFactory(): void + { + $this->analyse( + [__DIR__ . '/dataMixed/AttributeFactory.php'], + [ + ['Test\Ebln\PHPStan\EnforceFactory\dataMixed\code\MismatchedProduct has either no factories defined or a conflict between interface and attribute!', 25], + ] + ); + } + + public function testRogueAttributeFactory(): void + { + $this->analyse([__DIR__ . '/dataMixed/RogueAttributeFactory.php'], [ + ['Test\Ebln\PHPStan\EnforceFactory\dataMixed\code\AttributeProduct must be instantiated by Test\Ebln\PHPStan\EnforceFactory\dataMixed\AttributeFactory!', 14], + ['Test\Ebln\PHPStan\EnforceFactory\dataMixed\code\MixedProduct must be instantiated by Test\Ebln\PHPStan\EnforceFactory\dataMixed\AttributeFactory or Test\Ebln\PHPStan\EnforceFactory\dataMixed\ForcedFactory!', 19], + + ]); + } + + protected function getRule(): Rule + { + return new ForceFactoryRule(self::getContainer()->getByType(ReflectionProvider::class)); + } +} + diff --git a/tests/dataAttrib/EmptyFactory.php b/tests/dataAttrib/EmptyFactory.php new file mode 100644 index 0000000..1ccd65b --- /dev/null +++ b/tests/dataAttrib/EmptyFactory.php @@ -0,0 +1,15 @@ +foo(); + } + + public function anonymousExtendingSquare(): void + { + $x = new class() extends ExtendedProduct + { + public function foo(): string + { + return 'bar'; + } + }; + + $bar = $x->foo(); + } + + public function anonymousPassing(): void + { + $x = new class() + { + public function foo(): string + { + return 'bar'; + } + }; + + $bar = $x->foo(); + } + + public static function staticClass(): ForcedFactoryProduct + { + return new ForcedFactoryProduct(); + } +} diff --git a/tests/dataAttrib/IndependentFactory.php b/tests/dataAttrib/IndependentFactory.php new file mode 100644 index 0000000..ba6a3dd --- /dev/null +++ b/tests/dataAttrib/IndependentFactory.php @@ -0,0 +1,15 @@ +loopholeFactory = new LoopholeFactory(); + } + + public function expectedFailingLoophole(): object + { + $loophole = $this->loopholeFactory->variableUninferable(true); + + return $loophole; + } + + public function expectedMissingClass(): void + { + $this->loopholeFactory->variableUninferable(false); + } +} diff --git a/tests/dataAttrib/RogueFactory.php b/tests/dataAttrib/RogueFactory.php new file mode 100644 index 0000000..0f64b98 --- /dev/null +++ b/tests/dataAttrib/RogueFactory.php @@ -0,0 +1,104 @@ +foo(); + } + + public function anonymousExtendingSquare(): void + { + $x = new class() extends ExtendedProduct + { + public function foo(): string + { + return 'bar'; + } + }; + + $bar = $x->foo(); + } + + public function anonymousPassing(): void + { + $x = new class() + { + public function foo(): string + { + return 'bar'; + } + }; + + $bar = $x->foo(); + } + + public static function staticClass(): ForcedFactoryProduct + { + return new ForcedFactoryProduct(); + } + + public function independentClass(): IndependentForcedFactoryProduct + { + return new IndependentForcedFactoryProduct(); + } + +} diff --git a/tests/dataAttrib/RogueTraitFactory.php b/tests/dataAttrib/RogueTraitFactory.php new file mode 100644 index 0000000..083ea54 --- /dev/null +++ b/tests/dataAttrib/RogueTraitFactory.php @@ -0,0 +1,10 @@ +