diff --git a/README.md b/README.md index 3535ac3..c089879 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,11 @@ $ vendor/bin/phpstan - annotations like `@test`, `@before`, `@afterClass` etc - attributes like `#[Test]`, `#[Before]`, `#[AfterClass]` etc +#### PhpBench: +- `benchXxx` methods +- `#[BeforeMethods]`, `#[AfterMethods]` attributes +- `#[ParamProviders]` attribute for param provider methods + #### PHPStan: - constructor calls for DIC services (rules, extensions, ...) diff --git a/composer.json b/composer.json index 83dab30..6da73d8 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "nette/component-model": "^3.0", "nette/utils": "^3.0 || ^4.0", "nikic/php-parser": "^5.4.0", + "phpbench/phpbench": "^1.2", "phpstan/phpstan-phpunit": "^2.0.4", "phpstan/phpstan-strict-rules": "^2.0.3", "phpstan/phpstan-symfony": "^2.0.2", diff --git a/composer.lock b/composer.lock index 4f69202..4532286 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": "423e34004649a4f9008c2632c052d09e", + "content-hash": "e42b7d573929d70a46d0ffdd48c6bfb6", "packages": [ { "name": "phpstan/phpstan", @@ -225,6 +225,83 @@ }, "time": "2023-01-05T11:28:13+00:00" }, + { + "name": "doctrine/annotations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "abandoned": true, + "time": "2024-09-05T10:17:24+00:00" + }, { "name": "doctrine/collections", "version": "2.3.0", @@ -2363,6 +2440,155 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpbench/container", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/phpbench/container.git", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196", + "shasum": "" + }, + "require": { + "psr/container": "^1.0|^2.0", + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.89", + "phpstan/phpstan": "^0.12.52", + "phpunit/phpunit": "^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\DependencyInjection\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Simple, configurable, service container.", + "support": { + "issues": "https://github.com/phpbench/container/issues", + "source": "https://github.com/phpbench/container/tree/2.2.3" + }, + "time": "2025-11-06T09:05:13+00:00" + }, + { + "name": "phpbench/phpbench", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/phpbench/phpbench.git", + "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/bb61ae6c54b3d58642be154eb09f4e73c3511018", + "reference": "bb61ae6c54b3d58642be154eb09f4e73c3511018", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^2.0", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "ext-tokenizer": "*", + "php": "^8.1", + "phpbench/container": "^2.2", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "seld/jsonlint": "^1.1", + "symfony/console": "^6.1 || ^7.0", + "symfony/filesystem": "^6.1 || ^7.0", + "symfony/finder": "^6.1 || ^7.0", + "symfony/options-resolver": "^6.1 || ^7.0", + "symfony/process": "^6.1 || ^7.0", + "webmozart/glob": "^4.6" + }, + "require-dev": { + "dantleech/invoke": "^2.0", + "ergebnis/composer-normalize": "^2.39", + "friendsofphp/php-cs-fixer": "^3.0", + "jangregor/phpstan-prophecy": "^1.0", + "phpspec/prophecy": "^1.22", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^10.4 || ^11.0", + "rector/rector": "^1.2", + "symfony/error-handler": "^6.1 || ^7.0", + "symfony/var-dumper": "^6.1 || ^7.0" + }, + "suggest": { + "ext-xdebug": "For Xdebug profiling extension." + }, + "bin": [ + "bin/phpbench" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "files": [ + "lib/Report/Func/functions.php" + ], + "psr-4": { + "PhpBench\\": "lib/", + "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "PHP Benchmarking Framework", + "keywords": [ + "benchmarking", + "optimization", + "performance", + "profiling", + "testing" + ], + "support": { + "issues": "https://github.com/phpbench/phpbench/issues", + "source": "https://github.com/phpbench/phpbench/tree/1.4.2" + }, + "funding": [ + { + "url": "https://github.com/dantleech", + "type": "github" + } + ], + "time": "2025-10-26T14:21:59+00:00" + }, { "name": "phpstan/phpdoc-parser", "version": "2.1.0", @@ -4239,6 +4465,70 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "seld/jsonlint", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2024-07-11T14:55:45+00:00" + }, { "name": "shipmonk/coding-standard", "version": "0.1.3", @@ -5208,6 +5498,144 @@ ], "time": "2025-04-22T09:11:45+00:00" }, + { + "name": "symfony/filesystem", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-07T08:17:47+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-15T18:45:57+00:00" + }, { "name": "symfony/http-foundation", "version": "v7.3.0", @@ -5401,6 +5829,77 @@ ], "time": "2025-05-29T07:47:32+00:00" }, + { + "name": "symfony/options-resolver", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-05T10:16:07+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.32.0", @@ -5872,6 +6371,71 @@ ], "time": "2025-02-20T12:04:08+00:00" }, + { + "name": "symfony/process", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T10:12:26+00:00" + }, { "name": "symfony/routing", "version": "v7.3.0", @@ -6427,11 +6991,60 @@ } ], "time": "2025-05-03T07:21:55+00:00" + }, + { + "name": "webmozart/glob", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/glob.git", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/glob/zipball/8a2842112d6916e61e0e15e316465b611f3abc17", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/filesystem": "^5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Glob\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A PHP implementation of Ant's glob.", + "support": { + "issues": "https://github.com/webmozarts/glob/issues", + "source": "https://github.com/webmozarts/glob/tree/4.7.0" + }, + "time": "2024-03-07T20:33:40+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/rules.neon b/rules.neon index 75744cd..fdc0873 100644 --- a/rules.neon +++ b/rules.neon @@ -56,6 +56,13 @@ services: arguments: enabled: %shipmonkDeadCode.usageProviders.phpunit.enabled% + - + class: ShipMonk\PHPStan\DeadCode\Provider\PhpBenchUsageProvider + tags: + - shipmonk.deadCode.memberUsageProvider + arguments: + enabled: %shipmonkDeadCode.usageProviders.phpbench.enabled% + - class: ShipMonk\PHPStan\DeadCode\Provider\SymfonyUsageProvider tags: @@ -181,6 +188,8 @@ parameters: enabled: true phpunit: enabled: null + phpbench: + enabled: null symfony: enabled: null configDir: null @@ -230,6 +239,9 @@ parametersSchema: phpunit: structure([ enabled: schema(bool(), nullable()) ]) + phpbench: structure([ + enabled: schema(bool(), nullable()) + ]) symfony: structure([ enabled: schema(bool(), nullable()) configDir: schema(string(), nullable()) diff --git a/src/Provider/PhpBenchUsageProvider.php b/src/Provider/PhpBenchUsageProvider.php new file mode 100644 index 0000000..bdf189b --- /dev/null +++ b/src/Provider/PhpBenchUsageProvider.php @@ -0,0 +1,279 @@ +enabled = $enabled ?? InstalledVersions::isInstalled('phpbench/phpbench'); + $this->phpDocParser = $phpDocParser; + $this->lexer = $lexer; + } + + public function getUsages( + Node $node, + Scope $scope + ): array + { + if (!$this->enabled || !$node instanceof InClassNode) { // @phpstan-ignore phpstanApi.instanceofAssumption + return []; + } + + $classReflection = $node->getClassReflection(); + $className = $classReflection->getName(); + + if (substr($className, -5) !== 'Bench') { + return []; + } + + $usages = []; + + foreach ($classReflection->getNativeReflection()->getMethods() as $method) { + $methodName = $method->getName(); + + $paramProviderMethods = array_merge( + $this->getMethodNamesFromAnnotation($method->getDocComment(), '@ParamProviders'), + $this->getParamProvidersFromAttributes($method), + ); + + foreach ($paramProviderMethods as $paramProvider) { + $usages[] = $this->createUsage( + $className, + $paramProvider, + sprintf('Param provider method, used by %s', $methodName), + ); + } + + $beforeAfterMethodsFromAttributes = array_merge( + $this->getMethodNamesFromAttribute($method, BeforeMethods::class), + $this->getMethodNamesFromAttribute($method, AfterMethods::class), + ); + + foreach ($beforeAfterMethodsFromAttributes as $beforeAfterMethod) { + $usages[] = $this->createUsage( + $className, + $beforeAfterMethod, + sprintf('Before/After method, used by %s', $methodName), + ); + } + + if ($this->isBenchmarkMethod($method)) { + $usages[] = $this->createUsage($className, $methodName, 'Benchmark method'); + } + + if ($this->isBeforeOrAfterMethod($method)) { + $usages[] = $this->createUsage($className, $methodName, 'Before/After method'); + } + } + + return $usages; + } + + private function isBenchmarkMethod(ReflectionMethod $method): bool + { + return strpos($method->getName(), 'bench') === 0; + } + + /** + * @return list + */ + private function getMethodNamesFromAttribute( + ReflectionMethod $method, + string $attributeClass + ): array + { + $result = []; + + foreach ($method->getAttributes($attributeClass) as $attribute) { + $methods = $attribute->getArguments()[0] ?? $attribute->getArguments()['methods'] ?? []; + if (!is_array($methods)) { + $methods = [$methods]; + } + + foreach ($methods as $methodName) { + if (is_string($methodName)) { + $result[] = $methodName; + } + } + } + + return $result; + } + + private function isBeforeOrAfterMethod(ReflectionMethod $method): bool + { + $classReflection = $method->getDeclaringClass(); + $methodName = $method->getName(); + + // Check class-level annotations + $docComment = $classReflection->getDocComment(); + if ($docComment !== false) { + $beforeMethodsFromAnnotations = $this->getMethodNamesFromAnnotation($docComment, '@BeforeMethods'); + $afterMethodsFromAnnotations = $this->getMethodNamesFromAnnotation($docComment, '@AfterMethods'); + + if (in_array($methodName, $beforeMethodsFromAnnotations, true) || in_array($methodName, $afterMethodsFromAnnotations, true)) { + return true; + } + } + + // Check class-level attributes + foreach ($classReflection->getAttributes(BeforeMethods::class) as $attribute) { + $methods = $attribute->getArguments()[0] ?? $attribute->getArguments()['methods'] ?? []; + if (!is_array($methods)) { + $methods = [$methods]; + } + + foreach ($methods as $beforeMethod) { + if ($beforeMethod === $methodName) { + return true; + } + } + } + + foreach ($classReflection->getAttributes(AfterMethods::class) as $attribute) { + $methods = $attribute->getArguments()[0] ?? $attribute->getArguments()['methods'] ?? []; + if (!is_array($methods)) { + $methods = [$methods]; + } + + foreach ($methods as $afterMethod) { + if ($afterMethod === $methodName) { + return true; + } + } + } + + return false; + } + + /** + * @param false|string $rawPhpDoc + * @return list + */ + private function getMethodNamesFromAnnotation( + $rawPhpDoc, + string $annotationName + ): array + { + if ($rawPhpDoc === false || strpos($rawPhpDoc, $annotationName) === false) { + return []; + } + + $tokens = new TokenIterator($this->lexer->tokenize($rawPhpDoc)); + $phpDoc = $this->phpDocParser->parse($tokens); + + $result = []; + + foreach ($phpDoc->getTagsByName($annotationName) as $tag) { + $value = (string) $tag->value; + + // Extract content from parentheses: @BeforeMethods("setUp") -> "setUp" + // or @BeforeMethods({"setUp", "tearDown"}) -> {"setUp", "tearDown"} + if (preg_match('~\((.+)\)\s*$~', $value, $matches) === 1) { + $value = $matches[1]; + } + + $value = trim($value); + $value = trim($value, '"\''); + + // If it's a single method name, add it directly + if (strpos($value, ',') === false && strpos($value, '{') === false) { + $result[] = $value; + continue; + } + + // Handle array format: {"method1", "method2"} + $value = trim($value, '{}'); + $methods = explode(',', $value); + foreach ($methods as $method) { + $method = trim($method); + $method = trim($method, '"\''); + if ($method !== '') { + $result[] = $method; + } + } + } + + return $result; + } + + /** @return list */ + private function getParamProvidersFromAttributes(ReflectionMethod $method): array + { + $result = []; + + foreach ($method->getAttributes(ParamProviders::class) as $providerAttributeReflection) { + $providers = $providerAttributeReflection->getArguments()[0] + ?? $providerAttributeReflection->getArguments()['providers'] + ?? null; + + if (!is_array($providers)) { + continue; + } + + foreach ($providers as $provider) { + if (!is_string($provider)) { + continue; + } + + $result[] = $provider; + } + } + + return $result; + } + + private function createUsage( + string $className, + string $methodName, + string $reason + ): ClassMethodUsage + { + return new ClassMethodUsage( + UsageOrigin::createVirtual($this, VirtualUsageData::withNote($reason)), + new ClassMethodRef( + $className, + $methodName, + false, + ), + ); + } + +} diff --git a/tests/Rule/DeadCodeRuleTest.php b/tests/Rule/DeadCodeRuleTest.php index b95b59e..5d30d25 100644 --- a/tests/Rule/DeadCodeRuleTest.php +++ b/tests/Rule/DeadCodeRuleTest.php @@ -42,6 +42,7 @@ use ShipMonk\PHPStan\DeadCode\Provider\EnumUsageProvider; use ShipMonk\PHPStan\DeadCode\Provider\MemberUsageProvider; use ShipMonk\PHPStan\DeadCode\Provider\NetteUsageProvider; +use ShipMonk\PHPStan\DeadCode\Provider\PhpBenchUsageProvider; use ShipMonk\PHPStan\DeadCode\Provider\PhpStanUsageProvider; use ShipMonk\PHPStan\DeadCode\Provider\PhpUnitUsageProvider; use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider; @@ -766,7 +767,7 @@ public static function provideGroupingFiles(): Traversable } /** - * @return Traversable, 1?: bool}> + * @return Traversable, 1?: bool}> */ public static function provideFiles(): Traversable { @@ -856,6 +857,7 @@ public static function provideFiles(): Traversable yield 'provider-symfony-7.1' => [__DIR__ . '/data/providers/symfony-gte71.php', self::requiresPhp(8_00_00) && self::requiresPackage('symfony/dependency-injection', '>= 7.1')]; yield 'provider-twig' => [__DIR__ . '/data/providers/twig.php', self::requiresPhp(8_00_00)]; yield 'provider-phpunit' => [__DIR__ . '/data/providers/phpunit.php', self::requiresPhp(8_00_00)]; + yield 'provider-phpbench' => [__DIR__ . '/data/providers/phpbench.php', self::requiresPhp(8_00_00)]; yield 'provider-doctrine' => [__DIR__ . '/data/providers/doctrine.php', self::requiresPhp(8_01_00)]; yield 'provider-phpstan' => [__DIR__ . '/data/providers/phpstan.php']; yield 'provider-nette' => [__DIR__ . '/data/providers/nette.php']; @@ -991,6 +993,11 @@ private function getMemberUsageProviders(): array self::getContainer()->getByType(PhpDocParser::class), self::getContainer()->getByType(Lexer::class), ), + new PhpBenchUsageProvider( + $this->providersEnabled, + self::getContainer()->getByType(PhpDocParser::class), + self::getContainer()->getByType(Lexer::class), + ), new DoctrineUsageProvider( $this->providersEnabled, ), diff --git a/tests/Rule/data/providers/phpbench.php b/tests/Rule/data/providers/phpbench.php new file mode 100644 index 0000000..647adc2 --- /dev/null +++ b/tests/Rule/data/providers/phpbench.php @@ -0,0 +1,97 @@ +