From 5b1adcb4e73ffa2dc9ae19b5cc94c0c7f3b5ed6b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 2 Sep 2024 16:21:55 +0200 Subject: [PATCH] Nette entrypoint provider (#51) --- README.md | 2 +- composer.json | 5 +- composer.lock | 313 +++++++++++++++++- src/Collector/MethodDefinitionCollector.php | 44 ++- src/Provider/NetteEntrypointProvider.php | 175 ++++++++++ tests/Rule/DeadMethodRuleTest.php | 6 + .../data/DeadMethodRule/providers/nette.php | 60 ++++ 7 files changed, 578 insertions(+), 27 deletions(-) create mode 100644 src/Provider/NetteEntrypointProvider.php create mode 100644 tests/Rule/data/DeadMethodRule/providers/nette.php diff --git a/README.md b/README.md index 2538ae3..9571dac 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ includes: ## Configuration: - All entrypoints of your code (controllers, consumers, commands, ...) need to be known to the detector to get proper results - By default, all overridden methods which declaration originates inside `vendor` are considered entrypoints -- Also, there are some built-in providers for some magic calls that occur in `doctrine`, `symfony`, `phpstan` and `phpunit` +- Also, there are some built-in providers for some magic calls that occur in `doctrine`, `nette`, `symfony`, `phpstan` and `phpunit` - For everything else, you can implement your own entrypoint provider, just tag it with `shipmonk.deadCode.entrypointProvider` and implement `ShipMonk\PHPStan\DeadCode\Provider\EntrypointProvider` ```neon diff --git a/composer.json b/composer.json index 8f4d51d..ad7666d 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,9 @@ "doctrine/orm": "^2.19 || ^3.0", "editorconfig-checker/editorconfig-checker": "^10.3.0", "ergebnis/composer-normalize": "^2.28", + "nette/application": "^3.1", + "nette/component-model": "^3.0", + "nette/utils": "^3.0 || ^4.0", "phpstan/phpstan-phpunit": "^1.1.1", "phpstan/phpstan-strict-rules": "^1.2.3", "phpstan/phpstan-symfony": "^1.4", @@ -74,7 +77,7 @@ "check:dependencies": "composer-dependency-analyser", "check:ec": "ec src tests", "check:tests": "phpunit tests", - "check:types": "phpstan analyse -vvv --ansi", + "check:types": "phpstan analyse -vv --ansi", "fix:cs": "phpcbf" } } diff --git a/composer.lock b/composer.lock index ff987f7..710638f 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": "9e3d895e3a54f77f946ac3426bf00eae", + "content-hash": "73654267cbbb625ea912738f32d0a446", "packages": [ { "name": "phpstan/phpstan", @@ -1545,6 +1545,303 @@ ], "time": "2024-06-12T14:39:25+00:00" }, + { + "name": "nette/application", + "version": "v3.2.5", + "source": { + "type": "git", + "url": "https://github.com/nette/application.git", + "reference": "1e868966c3de55a087e5ec938189ec34a1648b04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/application/zipball/1e868966c3de55a087e5ec938189ec34a1648b04", + "reference": "1e868966c3de55a087e5ec938189ec34a1648b04", + "shasum": "" + }, + "require": { + "nette/component-model": "^3.1", + "nette/http": "^3.3", + "nette/routing": "^3.1", + "nette/utils": "^4.0", + "php": "8.1 - 8.3" + }, + "conflict": { + "latte/latte": "<2.7.1 || >=3.0.0 <3.0.12 || >=3.1", + "nette/caching": "<3.2", + "nette/di": "<3.2", + "nette/forms": "<3.2", + "nette/schema": "<1.3", + "tracy/tracy": "<2.9" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "latte/latte": "^2.10.2 || ^3.0.12", + "mockery/mockery": "^2.0", + "nette/di": "^3.2", + "nette/forms": "^3.2", + "nette/robot-loader": "^4.0", + "nette/security": "^3.2", + "nette/tester": "^2.5", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "latte/latte": "Allows using Latte in templates", + "nette/forms": "Allows to use Nette\\Application\\UI\\Form" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🏆 Nette Application: a full-stack component-based MVC kernel for PHP that helps you write powerful and modern web applications. Write less, have cleaner code and your work will bring you joy.", + "homepage": "https://nette.org", + "keywords": [ + "Forms", + "component-based", + "control", + "framework", + "mvc", + "mvp", + "nette", + "presenter", + "routing", + "seo" + ], + "support": { + "issues": "https://github.com/nette/application/issues", + "source": "https://github.com/nette/application/tree/v3.2.5" + }, + "time": "2024-05-13T09:10:31+00:00" + }, + { + "name": "nette/component-model", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/nette/component-model.git", + "reference": "4e0946a788b4ac42ea903b761c693ec7dd083a69" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/component-model/zipball/4e0946a788b4ac42ea903b761c693ec7dd083a69", + "reference": "4e0946a788b4ac42ea903b761c693ec7dd083a69", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "⚛ Nette Component Model", + "homepage": "https://nette.org", + "keywords": [ + "components", + "nette" + ], + "support": { + "issues": "https://github.com/nette/component-model/issues", + "source": "https://github.com/nette/component-model/tree/v3.1.0" + }, + "time": "2024-02-08T20:25:40+00:00" + }, + { + "name": "nette/http", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/nette/http.git", + "reference": "c779293fb79e6d2a16d474cd19dce866615f3b9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/http/zipball/c779293fb79e6d2a16d474cd19dce866615f3b9c", + "reference": "c779293fb79e6d2a16d474cd19dce866615f3b9c", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0.4", + "php": "8.1 - 8.3" + }, + "conflict": { + "nette/di": "<3.0.3", + "nette/schema": "<1.2" + }, + "require-dev": { + "nette/di": "^3.0", + "nette/security": "^3.0", + "nette/tester": "^2.4", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.8" + }, + "suggest": { + "ext-fileinfo": "to detect MIME type of uploaded files by Nette\\Http\\FileUpload", + "ext-gd": "to use image function in Nette\\Http\\FileUpload", + "ext-intl": "to support punycode by Nette\\Http\\Url", + "ext-session": "to use Nette\\Http\\Session" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🌐 Nette Http: abstraction for HTTP request, response and session. Provides careful data sanitization and utility for URL and cookies manipulation.", + "homepage": "https://nette.org", + "keywords": [ + "cookies", + "http", + "nette", + "proxy", + "request", + "response", + "security", + "session", + "url" + ], + "support": { + "issues": "https://github.com/nette/http/issues", + "source": "https://github.com/nette/http/tree/v3.3.0" + }, + "time": "2024-01-30T18:16:20+00:00" + }, + { + "name": "nette/routing", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/nette/routing.git", + "reference": "f7419bc147164106cb03b3d331c85aff6cb81fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/routing/zipball/f7419bc147164106cb03b3d331c85aff6cb81fc3", + "reference": "f7419bc147164106cb03b3d331c85aff6cb81fc3", + "shasum": "" + }, + "require": { + "nette/http": "^3.2 || ~4.0.0", + "nette/utils": "^4.0", + "php": "8.1 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.5", + "phpstan/phpstan": "^1", + "tracy/tracy": "^2.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "Nette Routing: two-ways URL conversion", + "homepage": "https://nette.org", + "keywords": [ + "nette" + ], + "support": { + "issues": "https://github.com/nette/routing/issues", + "source": "https://github.com/nette/routing/tree/v3.1.0" + }, + "time": "2024-01-21T21:13:45+00:00" + }, { "name": "nette/schema", "version": "v1.3.0", @@ -1609,20 +1906,20 @@ }, { "name": "nette/utils", - "version": "v4.0.4", + "version": "v4.0.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218" + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/d3ad0aa3b9f934602cb3e3902ebccf10be34d218", - "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", "shasum": "" }, "require": { - "php": ">=8.0 <8.4" + "php": "8.0 - 8.4" }, "conflict": { "nette/finder": "<3", @@ -1689,9 +1986,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.4" + "source": "https://github.com/nette/utils/tree/v4.0.5" }, - "time": "2024-01-17T16:50:36+00:00" + "time": "2024-08-07T15:39:19+00:00" }, { "name": "nikic/php-parser", diff --git a/src/Collector/MethodDefinitionCollector.php b/src/Collector/MethodDefinitionCollector.php index d5b3648..c632c51 100644 --- a/src/Collector/MethodDefinitionCollector.php +++ b/src/Collector/MethodDefinitionCollector.php @@ -10,6 +10,7 @@ use PHPStan\Node\InClassNode; use PHPStan\Reflection\ClassReflection; use ReflectionException; +use ReflectionMethod; use ShipMonk\PHPStan\DeadCode\Crate\MethodDefinition; use function array_map; use function strpos; @@ -45,6 +46,10 @@ public function processNode( // we need to collect even methods of traits that are always overridden foreach ($reflection->getTraits(true) as $trait) { foreach ($trait->getNativeReflection()->getMethods() as $traitMethod) { + if ($this->isUnsupportedMethod($traitMethod)) { + continue; + } + $traitLine = $traitMethod->getStartLine(); $traitName = $trait->getName(); $traitMethodName = $traitMethod->getName(); @@ -64,19 +69,7 @@ public function processNode( } foreach ($nativeReflection->getMethods() as $method) { - if ($method->isDestructor()) { - continue; - } - - if (!$method->isConstructor() && strpos($method->getName(), '__') === 0) { // magic methods like __toString, __clone, __get, __set etc - continue; - } - - if ($method->isConstructor() && $method->isPrivate()) { // e.g. classes used for storing static methods only - continue; - } - - if ($method->getFileName() === false) { // e.g. php core + if ($this->isUnsupportedMethod($method)) { continue; } @@ -84,10 +77,6 @@ public function processNode( continue; } - if (strpos($method->getFileName(), '/vendor/') !== false) { - continue; - } - $line = $method->getStartLine(); if ($line === false) { @@ -154,4 +143,25 @@ private function getDeclaringTraitDefinition( return null; } + private function isUnsupportedMethod(ReflectionMethod $method): bool + { + if ($method->isDestructor()) { + return true; + } + + if (!$method->isConstructor() && strpos($method->getName(), '__') === 0) { // magic methods like __toString, __clone, __get, __set etc + return true; + } + + if ($method->isConstructor() && $method->isPrivate()) { // e.g. classes with "denied" instantiation + return true; + } + + if ($method->getFileName() === false) { // e.g. php core + return true; + } + + return strpos($method->getFileName(), '/vendor/') !== false; + } + } diff --git a/src/Provider/NetteEntrypointProvider.php b/src/Provider/NetteEntrypointProvider.php new file mode 100644 index 0000000..c0f9f8f --- /dev/null +++ b/src/Provider/NetteEntrypointProvider.php @@ -0,0 +1,175 @@ +> + */ + private array $smartObjectCache = []; + + public function __construct( + ReflectionProvider $reflectionProvider, + ?bool $enabled + ) + { + $this->reflectionProvider = $reflectionProvider; + $this->enabled = $enabled ?? $this->isNetteInstalled(); + } + + public function isEntrypoint(ReflectionMethod $method): bool + { + if (!$this->enabled) { + return false; + } + + $methodName = $method->getName(); + $class = $method->getDeclaringClass(); + $className = $class->getName(); + $reflection = $this->reflectionProvider->getClass($className); + + return $this->isNetteMagic($reflection, $methodName); + } + + private function isNetteMagic(ClassReflection $reflection, string $methodName): bool + { + if ( + $reflection->is(SignalReceiver::class) + && strpos($methodName, 'handle') === 0 + ) { + return true; + } + + if ( + $reflection->is(Container::class) + && strpos($methodName, 'createComponent') === 0 + ) { + return true; + } + + if ( + $reflection->is(Control::class) + && strpos($methodName, 'render') === 0 + ) { + return true; + } + + if ( + $reflection->is(Presenter::class) + && ( + strpos($methodName, 'action') === 0 + || strpos($methodName, 'inject') === 0 + ) + ) { + return true; + } + + if ( + $reflection->hasTraitUse(SmartObject::class) + ) { + if (strpos($methodName, 'is') === 0) { + /** @var string $name cannot be false */ + $name = substr($methodName, 2); + + } elseif (strpos($methodName, 'get') === 0 || strpos($methodName, 'set') === 0) { + /** @var string $name cannot be false */ + $name = substr($methodName, 3); + + } else { + $name = null; + } + + if ($name !== null) { + $name = lcfirst($name); + $property = $this->getMagicProperties($reflection->getNativeReflection())[$name] ?? null; + + if ($property !== null) { + return true; + } + } + } + + return false; + } + + /** + * @param ReflectionClass $rc + * @return array + * @see ObjectHelpers::getMagicProperties() Modified to use static reflection + */ + private function getMagicProperties(ReflectionClass $rc): array + { + $class = $rc->getName(); + + if (isset($this->smartObjectCache[$class])) { + return $this->smartObjectCache[$class]; + } + + preg_match_all( + '~^ [ \t*]* @property(|-read|-write|-deprecated) [ \t]+ [^\s$]+ [ \t]+ \$ (\w+) ()~mx', + (string) $rc->getDocComment(), + $matches, + PREG_SET_ORDER, + ); + + $props = []; + + foreach ($matches as [, $type, $name]) { + $uname = ucfirst($name); + $write = $type !== '-read' + && $rc->hasMethod($nm = 'set' . $uname) + && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); // @phpstan-ignore missingType.checkedException + $read = $type !== '-write' + && ($rc->hasMethod($nm = 'get' . $uname) || $rc->hasMethod($nm = 'is' . $uname)) + && ($rm = $rc->getMethod($nm))->name === $nm && !$rm->isPrivate() && !$rm->isStatic(); // @phpstan-ignore missingType.checkedException + + if ($read || $write) { + $props[$name] = true; + } + } + + foreach ($rc->getTraits() as $trait) { + $props += $this->getMagicProperties($trait); + } + + $parent = $rc->getParentClass(); + + if ($parent !== false) { + $props += $this->getMagicProperties($parent); + } + + $this->smartObjectCache[$class] = $props; + return $props; + } + + private function isNetteInstalled(): bool + { + return InstalledVersions::isInstalled('nette/application') + || InstalledVersions::isInstalled('nette/component-model') + || InstalledVersions::isInstalled('nette/utils'); + } + +} diff --git a/tests/Rule/DeadMethodRuleTest.php b/tests/Rule/DeadMethodRuleTest.php index 3aabbc3..c5dbe50 100644 --- a/tests/Rule/DeadMethodRuleTest.php +++ b/tests/Rule/DeadMethodRuleTest.php @@ -17,6 +17,7 @@ use ShipMonk\PHPStan\DeadCode\Collector\MethodDefinitionCollector; use ShipMonk\PHPStan\DeadCode\Provider\DoctrineEntrypointProvider; use ShipMonk\PHPStan\DeadCode\Provider\EntrypointProvider; +use ShipMonk\PHPStan\DeadCode\Provider\NetteEntrypointProvider; use ShipMonk\PHPStan\DeadCode\Provider\PhpStanEntrypointProvider; use ShipMonk\PHPStan\DeadCode\Provider\PhpUnitEntrypointProvider; use ShipMonk\PHPStan\DeadCode\Provider\SymfonyEntrypointProvider; @@ -111,6 +112,7 @@ public static function provideFiles(): iterable yield 'provider-phpunit' => [__DIR__ . '/data/DeadMethodRule/providers/phpunit.php', 80_000]; yield 'provider-doctrine' => [__DIR__ . '/data/DeadMethodRule/providers/doctrine.php', 80_000]; yield 'provider-phpstan' => [__DIR__ . '/data/DeadMethodRule/providers/phpstan.php', 80_000]; + yield 'provider-nette' => [__DIR__ . '/data/DeadMethodRule/providers/nette.php']; } /** @@ -149,6 +151,10 @@ public function isEntrypoint(ReflectionMethod $method): bool true, $this->createPhpStanContainerMock(), ), + new NetteEntrypointProvider( + self::getContainer()->getByType(ReflectionProvider::class), + true, + ), ]; } diff --git a/tests/Rule/data/DeadMethodRule/providers/nette.php b/tests/Rule/data/DeadMethodRule/providers/nette.php new file mode 100644 index 0000000..76f8059 --- /dev/null +++ b/tests/Rule/data/DeadMethodRule/providers/nette.php @@ -0,0 +1,60 @@ +redirect('Login:'); + } + +} + +/** + * @property float $radius + * @property-read bool $visible + */ +class Circle +{ + use SmartObject; + + private float $radius = 0.0; // not public + + protected function getRadius(): float + { + return $this->radius; + } + + protected function setRadius(float $radius): void + { + $this->radius = max(0.0, $radius); + } + + protected function isVisible(): bool + { + return $this->radius > 0; + } +} + +$circle = new Circle; +$circle->radius = 10; // actually calls setRadius(10) +echo $circle->radius; // calls getRadius() +echo $circle->visible; // calls isVisible()