From eff90bcfc2c67ff07a37524f2058fc51e60b9134 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 13 Aug 2019 17:35:54 +0200 Subject: [PATCH] On Demand Symbol Loading --- composer.json | 5 +- .../Cli/Application.php | 1 + .../Cli/CheckCommand.php | 85 +++++--- ...erPackageDirectDependenciesSourceFiles.php | 65 ------ .../Cli/CheckCommandTest.php | 2 +- ...ckageDirectDependenciesSourceFilesTest.php | 192 ------------------ 6 files changed, 65 insertions(+), 285 deletions(-) delete mode 100644 src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php delete mode 100644 test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php diff --git a/composer.json b/composer.json index 81d368c1..0a665288 100644 --- a/composer.json +++ b/composer.json @@ -30,8 +30,9 @@ "ext-json": "*", "ext-phar": "*", "nikic/php-parser": "^4.0", - "symfony/console": "^3.4.17 || ^4.1.6", - "webmozart/glob": "^4.1" + "symfony/console": "^3.4.17 | ^4.1.6", + "webmozart/glob": "^4.1", + "roave/better-reflection": "^3.5" }, "require-dev": { "mikey179/vfsstream": "^1.6", diff --git a/src/ComposerRequireChecker/Cli/Application.php b/src/ComposerRequireChecker/Cli/Application.php index cc5032ed..2a7b39ea 100644 --- a/src/ComposerRequireChecker/Cli/Application.php +++ b/src/ComposerRequireChecker/Cli/Application.php @@ -2,6 +2,7 @@ namespace ComposerRequireChecker\Cli; +use Composer\Autoload\ClassLoader; use Symfony\Component\Console\Application as AbstractApplication; class Application extends AbstractApplication diff --git a/src/ComposerRequireChecker/Cli/CheckCommand.php b/src/ComposerRequireChecker/Cli/CheckCommand.php index b6e26c44..29222092 100644 --- a/src/ComposerRequireChecker/Cli/CheckCommand.php +++ b/src/ComposerRequireChecker/Cli/CheckCommand.php @@ -4,10 +4,9 @@ use ComposerRequireChecker\ASTLocator\LocateASTFromFiles; use ComposerRequireChecker\DefinedExtensionsResolver\DefinedExtensionsResolver; -use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromASTRoots; use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromExtensions; use ComposerRequireChecker\DependencyGuesser\DependencyGuesser; -use ComposerRequireChecker\FileLocator\LocateComposerPackageDirectDependenciesSourceFiles; +use ComposerRequireChecker\Exception\DependenciesNotInstalledException; use ComposerRequireChecker\FileLocator\LocateComposerPackageSourceFiles; use ComposerRequireChecker\FileLocator\LocateFilesByGlobPattern; use ComposerRequireChecker\GeneratorUtil\ComposeGenerators; @@ -15,6 +14,13 @@ use ComposerRequireChecker\UsedSymbolsLocator\LocateUsedSymbolsFromASTRoots; use PhpParser\ErrorHandler\Collecting as CollectingErrorHandler; use PhpParser\ParserFactory; +use Roave\BetterReflection\BetterReflection; +use Roave\BetterReflection\Reflector\ClassReflector; +use Roave\BetterReflection\Reflector\ConstantReflector; +use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound; +use Roave\BetterReflection\Reflector\FunctionReflector; +use Roave\BetterReflection\SourceLocator\Type\Composer\Factory\Exception\MissingComposerJson; +use Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForComposerJsonAndInstalledJson; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; @@ -70,15 +76,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $sourcesASTs = $this->getASTFromFilesLocator($input); - $this->verbose("Collecting defined vendor symbols... ", $output); - $definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs( + $this->verbose("Collecting used symbols... ", $output); + $usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke($sourcesASTs( (new ComposeGenerators())->__invoke( - $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)), $getPackageSourceFiles($composerData, dirname($composerJson)), - (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson) + $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)) ) )); - $this->verbose("found " . count($definedVendorSymbols) . " symbols.", $output, true); + $this->verbose("found " . count($usedSymbols) . " symbols.", $output, true); + $this->verbose("Collecting defined extension symbols... ", $output); $definedExtensionSymbols = (new LocateDefinedSymbolsFromExtensions())->__invoke( @@ -86,26 +92,55 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); $this->verbose("found " . count($definedExtensionSymbols) . " symbols.", $output, true); - $this->verbose("Collecting used symbols... ", $output); - $usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke($sourcesASTs( - (new ComposeGenerators())->__invoke( - $getPackageSourceFiles($composerData, dirname($composerJson)), - $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)) - ) - )); - $this->verbose("found " . count($usedSymbols) . " symbols.", $output, true); - - if (!count($usedSymbols)) { - throw new \LogicException('There were no symbols found, please check your configuration.'); + try { + $locator = new MakeLocatorForComposerJsonAndInstalledJson(); + $astLocator = (new BetterReflection())->astLocator(); + $composerLocator = $locator(dirname($composerJson), $astLocator); + $classReflector = new ClassReflector($composerLocator); + $functionReflector = new FunctionReflector($composerLocator, $classReflector); + $constantReflector = new ConstantReflector($composerLocator, $classReflector); + } catch (MissingComposerJson $missingComposerJson) { + $message = 'The composer dependencies have not been installed, run composer install/update first'; + throw new DependenciesNotInstalledException($message); } - $this->verbose("Checking for unknown symbols... ", $output, true); - $unknownSymbols = array_diff( - $usedSymbols, - $definedVendorSymbols, - $definedExtensionSymbols, - $options->getSymbolWhitelist() - ); + $whiteList = $options->getSymbolWhitelist(); + $unknownSymbols = []; + foreach ($usedSymbols as $usedSymbol) { + if (in_array($usedSymbol, $whiteList)) { + continue; + } + + if (in_array($usedSymbol, $definedExtensionSymbols)) { + continue; + } + + try { + $classReflector->reflect($usedSymbol); + + continue; + } catch (IdentifierNotFound $ignore) { + // void + } + + try { + $functionReflector->reflect($usedSymbol); + + continue; + } catch (IdentifierNotFound $ignore) { + // void + } + + try { + $constantReflector->reflect($usedSymbol); + + continue; + } catch (IdentifierNotFound $ignore) { + // void + } + + $unknownSymbols[] = $usedSymbol; + } if (!$unknownSymbols) { $output->writeln("There were no unknown symbols found."); diff --git a/src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php b/src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php deleted file mode 100644 index f318978f..00000000 --- a/src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php +++ /dev/null @@ -1,65 +0,0 @@ - $vendorRequiredVersion) { - $vendorDirs[$vendorName] = $packageDir . '/' . $configVendorDir . '/' . $vendorName; - }; - - $installedPackages = $this->getInstalledPackages($packageDir . '/' . $configVendorDir); - - foreach ($vendorDirs as $vendorName => $vendorDir) { - if (!array_key_exists($vendorName, $installedPackages)) { - continue; - } - - yield from (new LocateComposerPackageSourceFiles())->__invoke($installedPackages[$vendorName], $vendorDir); - } - } - - - /** - * Lookup each vendor package's composer.json info from installed.json - * - * @param string $vendorDir - * - * @return array Keys are the package name and value is the composer.json as an array - * @throws DependenciesNotInstalledException When composer install/update has not been run - */ - private function getInstalledPackages(string $vendorDir): array - { - try { - $installedData = (new JsonLoader($vendorDir . '/composer/installed.json'))->getData(); - } catch (NotReadableException $e) { - $message = 'The composer dependencies have not been installed, run composer install/update first'; - throw new DependenciesNotInstalledException($message); - } - - $installedPackages = []; - - $packages = $installedData['packages'] ?? $installedData; - foreach ($packages as $vendorJson) { - $vendorName = $vendorJson['name']; - $installedPackages[$vendorName] = $vendorJson; - } - - return $installedPackages; - } -} diff --git a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php index 37c032a6..82899d1a 100644 --- a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php +++ b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php @@ -57,7 +57,7 @@ public function testVerboseSelfCheckShowsCounts(): void 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, ]); - $this->assertRegExp('/Collecting defined vendor symbols... found \d+ symbols./', $this->commandTester->getDisplay()); +// $this->assertRegExp('/Collecting defined vendor symbols... found \d+ symbols./', $this->commandTester->getDisplay()); $this->assertRegExp('/Collecting defined extension symbols... found \d+ symbols./', $this->commandTester->getDisplay()); $this->assertRegExp('/Collecting used symbols... found \d+ symbols./', $this->commandTester->getDisplay()); } diff --git a/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php b/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php deleted file mode 100644 index 0c82ea99..00000000 --- a/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php +++ /dev/null @@ -1,192 +0,0 @@ -locator = new LocateComposerPackageDirectDependenciesSourceFiles(); - $this->root = vfsStream::setup(); - } - - public function testNoDependencies(): void - { - vfsStream::create([ - 'composer.json' => '{}', - 'vendor' => [ - 'composer' => [ - 'installed.json' => '{"packages":[]}', - ], - ], - ]); - - $files = $this->locate($this->root->getChild('composer.json')->url()); - - $this->assertCount(0, $files); - } - - public function testSingleDependency(): void - { - vfsStream::create([ - 'composer.json' => '{"require":{"foo/bar": "^1.0"}}', - 'vendor' => [ - 'composer' => [ - 'installed.json' => '{"packages":[{"name": "foo/bar", "autoload":{"psr-4":{"":"src"}}}]}', - ], - 'foo' => [ - 'bar' => [ - 'src' => [ - 'MyClass.php' => '', - ], - ], - ], - ], - ]); - - $files = $this->locate($this->root->getChild('composer.json')->url()); - - $this->assertCount(1, $files); - - $expectedFile = $this->root->getChild('vendor/foo/bar/src/MyClass.php')->url(); - $actualFile = str_replace('\\', '/', reset($files)); - $this->assertSame($expectedFile, $actualFile); - } - - public function testVendorDirsWithoutComposerFilesAreIgnored(): void - { - vfsStream::create([ - 'composer.json' => '{"require": {"foo/bar": "^1.0"}}', - 'vendor' => [ - 'composer' => [ - 'installed.json' => '{"packages":[]}', - ], - 'foo' => [ - 'bar' => [ - 'src' => [ - 'MyClass.php' => '', - ], - ], - ], - ], - ]); - - $files = $this->locate($this->root->getChild('composer.json')->url()); - - $this->assertCount(0, $files); - } - - public function testVendorConfigSettingIsBeingUsed(): void - { - vfsStream::create([ - 'composer.json' => '{"require":{"foo/bar": "^1.0"},"config":{"vendor-dir":"alternate-vendor"}}', - 'alternate-vendor' => [ - 'composer' => [ - 'installed.json' => '{"packages":[{"name": "foo/bar", "autoload":{"psr-4":{"":"src"}}}]}', - ], - 'foo' => [ - 'bar' => [ - 'src' => [ - 'MyClass.php' => '', - ], - ], - ], - ], - ]); - - $files = $this->locate($this->root->getChild('composer.json')->url()); - - $this->assertCount(1, $files); - - $expectedFile = $this->root->getChild('alternate-vendor/foo/bar/src/MyClass.php')->url(); - $actualFile = str_replace('\\', '/', reset($files)); - $this->assertSame($expectedFile, $actualFile); - } - - public function testInstalledJsonUsedAsFallback(): void - { - vfsStream::create([ - 'composer.json' => '{"require":{"foo/bar": "^1.0"}}', - 'vendor' => [ - 'composer' => [ - 'installed.json' => '{"packages": [{"name": "foo/bar", "autoload":{"psr-4":{"":"src"}}}]}', - ], - 'foo' => [ - 'bar' => [ - 'src' => [ - 'MyClass.php' => '', - ], - ], - ], - ], - ]); - - $files = $this->locate($this->root->getChild('composer.json')->url()); - - $this->assertCount(1, $files); - - $expectedFile = $this->root->getChild('vendor/foo/bar/src/MyClass.php')->url(); - $actualFile = str_replace('\\', '/', reset($files)); - $this->assertSame($expectedFile, $actualFile); - - # Ensure we didn't leave our temporary composer.json lying around - $this->assertFalse($this->root->hasChild('vendor/foo/bar/composer.json')); - } - - - /** - * https://github.com/composer/composer/pull/7999 - */ - public function testOldInstalledJsonUsedAsFallback(): void - { - vfsStream::create([ - 'composer.json' => '{"require":{"foo/bar": "^1.0"}}', - 'vendor' => [ - 'composer' => [ - 'installed.json' => '[{"name": "foo/bar", "autoload":{"psr-4":{"":"src"}}}]', - ], - 'foo' => [ - 'bar' => [ - 'src' => [ - 'MyClass.php' => '', - ], - ], - ], - ], - ]); - - $files = $this->locate($this->root->getChild('composer.json')->url()); - - $this->assertCount(1, $files); - - $expectedFile = $this->root->getChild('vendor/foo/bar/src/MyClass.php')->url(); - $actualFile = str_replace('\\', '/', reset($files)); - $this->assertSame($expectedFile, $actualFile); - - # Ensure we didn't leave our temporary composer.json lying around - $this->assertFalse($this->root->hasChild('vendor/foo/bar/composer.json')); - } - - /** - * @return string[] - */ - private function locate(string $composerJson): array - { - return iterator_to_array(($this->locator)($composerJson)); - } -}