diff --git a/composer.json b/composer.json index 7c6269e2..b8d4598d 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "ext-phar": "*", "composer-runtime-api": "^2.0.0", "nikic/php-parser": "^4.13.0", + "roave/better-reflection": "^5.0", "symfony/console": "^6.0.0", "webmozart/assert": "^1.9.1", "webmozart/glob": "^4.4.0" diff --git a/composer.lock b/composer.lock index 00333595..87f06b93 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,56 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "96a447b8623496db4a1687c90d4438a5", + "content-hash": "43190f04ebd0bfc580949efb83c75bf7", "packages": [ + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2021.3", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "c790a8fa467ff5d3f11b0e7c1f3698abbe37b182" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/c790a8fa467ff5d3f11b0e7c1f3698abbe37b182", + "reference": "c790a8fa467ff5d3f11b0e7c1f3698abbe37b182", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "dev-master", + "nikic/php-parser": "@stable", + "php": "^8.0", + "phpdocumentor/reflection-docblock": "@stable", + "phpunit/phpunit": "@stable" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "support": { + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2021.3" + }, + "time": "2021-10-19T20:06:47+00:00" + }, { "name": "nikic/php-parser", "version": "v4.13.2", @@ -110,6 +158,119 @@ }, "time": "2021-11-05T16:50:12+00:00" }, + { + "name": "roave/better-reflection", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/Roave/BetterReflection.git", + "reference": "24a030165953af5e4193502de1b860e3e99633d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/BetterReflection/zipball/24a030165953af5e4193502de1b860e3e99633d3", + "reference": "24a030165953af5e4193502de1b860e3e99633d3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "2021.3", + "nikic/php-parser": "^4.13.2", + "php": "~8.0.12 || ~8.1.0", + "roave/signature": "^1.5" + }, + "conflict": { + "thecodingmachine/safe": "<1.1.3" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0.0", + "phpstan/phpstan": "^1.2.0", + "phpunit/phpunit": "^9.5.9", + "roave/infection-static-analysis-plugin": "^1.13.0", + "vimeo/psalm": "^4.15.0" + }, + "suggest": { + "composer/composer": "Required to use the ComposerSourceLocator" + }, + "type": "library", + "autoload": { + "psr-4": { + "Roave\\BetterReflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "James Titcumb", + "email": "james@asgrim.com", + "homepage": "https://github.com/asgrim" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Gary Hockin", + "email": "gary@roave.com", + "homepage": "https://github.com/geeh" + }, + { + "name": "Jaroslav HanslĂ­k", + "email": "kukulich@kukulich.cz", + "homepage": "https://github.com/kukulich" + } + ], + "description": "Better Reflection - an improved code reflection API", + "support": { + "issues": "https://github.com/Roave/BetterReflection/issues", + "source": "https://github.com/Roave/BetterReflection/tree/5.0.0" + }, + "time": "2021-12-25T17:47:19+00:00" + }, + { + "name": "roave/signature", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/Roave/Signature.git", + "reference": "b100e2c40e51f3c56a0b29faf3e7ca75c33df60b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/Signature/zipball/b100e2c40e51f3c56a0b29faf3e7ca75c33df60b", + "reference": "b100e2c40e51f3c56a0b29faf3e7ca75c33df60b", + "shasum": "" + }, + "require": { + "php": "7.4.*|8.0.*|8.1.*" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0", + "infection/infection": "^0.25.1", + "phpunit/phpunit": "^9.5.9", + "vimeo/psalm": "^4.10.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Roave\\Signature\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Sign and verify stuff", + "support": { + "issues": "https://github.com/Roave/Signature/issues", + "source": "https://github.com/Roave/Signature/tree/1.5.0" + }, + "time": "2021-09-18T13:37:44+00:00" + }, { "name": "symfony/console", "version": "v6.0.1", @@ -5296,5 +5457,5 @@ "platform-dev": { "ext-zend-opcache": "*" }, - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.2.0" } diff --git a/infection.json.dist b/infection.json.dist index c35e8541..6b3c4d2d 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -5,7 +5,7 @@ "src" ] }, - "timeout": 30, + "timeout": 60, "logs": { "text": "php://stderr" }, diff --git a/src/ComposerRequireChecker/Cli/CheckCommand.php b/src/ComposerRequireChecker/Cli/CheckCommand.php index 96caf6e4..35937171 100644 --- a/src/ComposerRequireChecker/Cli/CheckCommand.php +++ b/src/ComposerRequireChecker/Cli/CheckCommand.php @@ -12,6 +12,8 @@ use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromComposerRuntimeApi; use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromExtensions; use ComposerRequireChecker\DependencyGuesser\DependencyGuesser; +use ComposerRequireChecker\DependencyGuesser\GuessFromInstalledComposerPackages; +use ComposerRequireChecker\DependencyGuesser\GuessFromLoadedExtensions; use ComposerRequireChecker\Exception\InvalidJson; use ComposerRequireChecker\Exception\NotReadable; use ComposerRequireChecker\FileLocator\LocateComposerPackageDirectDependenciesSourceFiles; @@ -212,7 +214,11 @@ public function __invoke(string $string): void $resultsWriter = new CliText($output); } - $guesser = new DependencyGuesser($options); + $guesser = new DependencyGuesser([ + new GuessFromLoadedExtensions($options), + new GuessFromInstalledComposerPackages(dirname($composerJson)), + ]); + $resultsWriter->write( array_map( static function (string $unknownSymbol) use ($guesser): array { diff --git a/src/ComposerRequireChecker/DependencyGuesser/DependencyGuesser.php b/src/ComposerRequireChecker/DependencyGuesser/DependencyGuesser.php index a083a411..7598a8f6 100644 --- a/src/ComposerRequireChecker/DependencyGuesser/DependencyGuesser.php +++ b/src/ComposerRequireChecker/DependencyGuesser/DependencyGuesser.php @@ -4,17 +4,19 @@ namespace ComposerRequireChecker\DependencyGuesser; -use ComposerRequireChecker\Cli\Options; use Generator; class DependencyGuesser { - /** @var Guesser[] */ + /** @var array */ private array $guessers = []; - public function __construct(?Options $options = null) + /** + * @param array $guessers + */ + public function __construct(array $guessers) { - $this->guessers[] = new GuessFromLoadedExtensions($options); + $this->guessers = $guessers; } /** diff --git a/src/ComposerRequireChecker/DependencyGuesser/GuessFromInstalledComposerPackages.php b/src/ComposerRequireChecker/DependencyGuesser/GuessFromInstalledComposerPackages.php new file mode 100644 index 00000000..029f9ce7 --- /dev/null +++ b/src/ComposerRequireChecker/DependencyGuesser/GuessFromInstalledComposerPackages.php @@ -0,0 +1,93 @@ +sourceLocator = new MemoizingSourceLocator((new MakeLocatorForInstalledJson())( + $installationPath, + (new BetterReflection())->astLocator() + )); + + $cleanPath = preg_quote(sprintf('%s/vendor', str_replace(DIRECTORY_SEPARATOR, '/', $installationPath)), '@'); + $this->pathRegex = sprintf('@^%s/(?:composer/\.\./)?([^/]+/[^/]+)/@', $cleanPath); + } + + /** + * @return Generator + */ + public function __invoke(string $symbolName): Generator + { + foreach ($this->locateIdentifier($symbolName) as $reflection) { + $path = $reflection->getFileName(); + + if ($path === null) { + continue; + } + + $matched = preg_match($this->pathRegex, $path, $captures); + + if (! $matched) { + continue; + } + + yield JsonLoader::getData($captures[0] . 'composer.json')['name']; + } + } + + /** + * @return Generator + */ + private function locateIdentifier(string $symbolName): Generator + { + $locatedIndentifiers = [ + $this->sourceLocator->locateIdentifier( + new DefaultReflector($this->sourceLocator), + new Identifier($symbolName, new IdentifierType(IdentifierType::IDENTIFIER_CLASS)) + ), + $this->sourceLocator->locateIdentifier( + new DefaultReflector($this->sourceLocator), + new Identifier($symbolName, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION)) + ), + $this->sourceLocator->locateIdentifier( + new DefaultReflector($this->sourceLocator), + new Identifier($symbolName, new IdentifierType(IdentifierType::IDENTIFIER_CONSTANT)) + ), + ]; + + foreach ($locatedIndentifiers as $locatedIndentifier) { + if (! ($locatedIndentifier instanceof ReflectionFunction || $locatedIndentifier instanceof ReflectionClass || $locatedIndentifier instanceof ReflectionConstant)) { + continue; + } + + yield $locatedIndentifier; + } + } +} diff --git a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php index 34b55d2c..cf4953cf 100644 --- a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php +++ b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php @@ -206,12 +206,14 @@ public function testSourceFileThatUsesDevDependency(): void 'composer-json' => dirname(__DIR__, 3) . '/composer.json', '--config-file' => $root->getChild('config.json')->url(), ]); - $this->assertNotEquals(0, $exitCode); - $this->assertMatchesRegularExpression( - '/The following 2 unknown symbols were found.*PHPUnit\\\\Framework\\\\TestCase/s', - $this->commandTester->getDisplay() - ); + + $display = $this->commandTester->getDisplay(); + $this->assertStringContainsString('The following 2 unknown symbols were found', $display); + $this->assertStringContainsString('org\bovigo\vfs\vfsStream', $display); + $this->assertStringContainsString('mikey179/vfsstream', $display); + $this->assertStringContainsString('PHPUnit\Framework\TestCase', $display); + $this->assertStringContainsString('phpunit/phpunit', $display); } public function testNoUnknownSymbolsFound(): void diff --git a/test/ComposerRequireCheckerTest/DependencyGuesser/DependencyGuesserTest.php b/test/ComposerRequireCheckerTest/DependencyGuesser/DependencyGuesserTest.php index 066dabe8..e8be2ce6 100644 --- a/test/ComposerRequireCheckerTest/DependencyGuesser/DependencyGuesserTest.php +++ b/test/ComposerRequireCheckerTest/DependencyGuesser/DependencyGuesserTest.php @@ -6,6 +6,7 @@ use ComposerRequireChecker\Cli\Options; use ComposerRequireChecker\DependencyGuesser\DependencyGuesser; +use ComposerRequireChecker\DependencyGuesser\GuessFromLoadedExtensions; use PHPUnit\Framework\TestCase; use function extension_loaded; @@ -14,33 +15,32 @@ final class DependencyGuesserTest extends TestCase { private DependencyGuesser $guesser; - protected function setUp(): void - { - $this->guesser = new DependencyGuesser(); - } - public function testGuessExtJson(): void { if (! extension_loaded('json')) { $this->markTestSkipped('extension json is not available'); } - $result = $this->guesser->__invoke('json_decode'); + $guesser = new DependencyGuesser([new GuessFromLoadedExtensions()]); + + $result = $guesser->__invoke('json_decode'); $this->assertNotEmpty($result); $this->assertContains('ext-json', $result); } public function testDoesNotSuggestAnything(): void { - $result = $this->guesser->__invoke('an_hopefully_unique_unknown_symbol'); + $guesser = new DependencyGuesser([new GuessFromLoadedExtensions()]); + + $result = $guesser->__invoke('an_hopefully_unique_unknown_symbol'); $this->assertFalse($result->valid()); } public function testCoreExtensionsResolvesToPHP(): void { - $options = new Options(['php-core-extensions' => ['SPL', 'something-else']]); - $this->guesser = new DependencyGuesser($options); - $result = $this->guesser->__invoke('RecursiveDirectoryIterator'); + $guesser = new DependencyGuesser([new GuessFromLoadedExtensions(new Options(['php-core-extensions' => ['SPL', 'something-else']]))]); + + $result = $guesser->__invoke('RecursiveDirectoryIterator'); $this->assertNotEmpty($result); $this->assertContains('php', $result); } diff --git a/test/ComposerRequireCheckerTest/DependencyGuesser/GuessFromInstalledComposerPackagesTest.php b/test/ComposerRequireCheckerTest/DependencyGuesser/GuessFromInstalledComposerPackagesTest.php new file mode 100644 index 00000000..222a65f7 --- /dev/null +++ b/test/ComposerRequireCheckerTest/DependencyGuesser/GuessFromInstalledComposerPackagesTest.php @@ -0,0 +1,44 @@ +guesser = new GuessFromInstalledComposerPackages(dirname(__DIR__, 3)); + } + + public function testGuessVendorClass(): void + { + $result = iterator_to_array(($this->guesser)(TestCase::class)); + + self::assertNotEmpty($result); + self::assertContains('phpunit/phpunit', $result); + } + + public function testGuessVendorFunction(): void + { + $result = iterator_to_array(($this->guesser)('DeepCopy\deep_copy')); + + self::assertNotEmpty($result); + self::assertContains('myclabs/deep-copy', $result); + } + + public function testDoNotGuessClassFromProject(): void + { + $result = iterator_to_array(($this->guesser)(GuessFromInstalledComposerPackages::class)); + + self::assertEmpty($result); + } +}