diff --git a/README.md b/README.md index 8fc1e86f..9adee8e4 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,18 @@ If this is already done, run it like this: composer-require-checker check /path/to/your/project/composer.json ``` +### Check development code and dependencies (optional) + +By default, Composer require checker only checks the source code listed in the `autoload` section of `composer.json` +against the dependencies listed in the `require` section. This checks that there are no indirect dependencies in the +*production* code. + +To check the *development* code, use the `--dev` option. This will scan the source code from the `autoload-dev` section and will search for symbols in dependencies from both the `require` and `require-dev` +section. + +Please note these two checks are really different: a successful `--dev` check does not guarantee there are no +no indirect dependencies in the *production* code. You probably want to do both. + ## Configuration Composer require checker is configured to whitelist some symbols per default. Have a look at the diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..bf116cd1 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,30 @@ +build: false +platform: + - x64 + +environment: + COMPOSER_NO_INTERACTION: "1" + ANSICON: '121x90 (121x90)' + matrix: + - PHP_VERSION: '7.2' + COMPOSER_FLAGS: '--prefer-stable --prefer-lowest' + - PHP_VERSION: '7.3' + - PHP_VERSION: '7.4' + +matrix: + fast_finish: true + +init: + - SET PATH=C:\Program Files\OpenSSL;C:\tools\php;%PATH% + +install: + - ps: Invoke-WebRequest "https://raw.githubusercontent.com/ChadSikorra/ps-install-php/master/Install-PHP.ps1" -OutFile "Install-PHP.ps1" + - ps: .\Install-PHP.ps1 -Version $Env:PHP_VERSION -Highest -Arch x64 -Extensions mbstring,fileinfo,openssl + - echo zend_extension=php_opcache.dll >> C:\tools\php\php.ini + - refreshenv + - php -r "readfile('https://getcomposer.org/installer');" | php + - php composer.phar update %COMPOSER_FLAGS% --no-interaction --no-progress -vvv + - php composer.phar info -D | sort + +test_script: + - vendor/bin/phpunit -c phpunit.xml.dist diff --git a/bin/composer-require-checker.bat b/bin/composer-require-checker.bat new file mode 100644 index 00000000..b8a2fe07 --- /dev/null +++ b/bin/composer-require-checker.bat @@ -0,0 +1 @@ +@php %~dp0\composer-require-checker.php %* diff --git a/composer.json b/composer.json index 0bd4872f..002032f6 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "nikic/php-parser": "^4.3", "ocramius/package-versions": "^1.4.2", "symfony/console": "^4.4.0", + "webmozart/assert": "^1.6", "webmozart/glob": "^4.1" }, "require-dev": { diff --git a/src/ComposerRequireChecker/Cli/CheckCommand.php b/src/ComposerRequireChecker/Cli/CheckCommand.php index b6e26c44..532aec5f 100644 --- a/src/ComposerRequireChecker/Cli/CheckCommand.php +++ b/src/ComposerRequireChecker/Cli/CheckCommand.php @@ -48,6 +48,12 @@ protected function configure() InputOption::VALUE_NONE, 'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise' . ' errors will be thrown' + ) + ->addOption( + 'dev', + null, + InputOption::VALUE_NONE, + 'check that the development sources (i.e. tests) have not indirect dependencies' ); } @@ -63,6 +69,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $composerData = $this->getComposerData($composerJson); + $checkDevSources = (bool)$input->getOption('dev'); + $options = $this->getCheckOptions($input); $getPackageSourceFiles = new LocateComposerPackageSourceFiles(); @@ -70,12 +78,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $sourcesASTs = $this->getASTFromFilesLocator($input); + $additionalLocators = $checkDevSources ? [ + $getPackageSourceFiles($composerData, dirname($composerJson), 'autoload-dev'), + (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson, 'require-dev') + ] : []; + $this->verbose("Collecting defined vendor symbols... ", $output); $definedVendorSymbols = (new LocateDefinedSymbolsFromASTRoots())->__invoke($sourcesASTs( (new ComposeGenerators())->__invoke( $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)), - $getPackageSourceFiles($composerData, dirname($composerJson)), - (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson) + $getPackageSourceFiles($composerData, dirname($composerJson), 'autoload'), + (new LocateComposerPackageDirectDependenciesSourceFiles())->__invoke($composerJson, 'require'), + ...$additionalLocators ) )); $this->verbose("found " . count($definedVendorSymbols) . " symbols.", $output, true); @@ -89,7 +103,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->verbose("Collecting used symbols... ", $output); $usedSymbols = (new LocateUsedSymbolsFromASTRoots())->__invoke($sourcesASTs( (new ComposeGenerators())->__invoke( - $getPackageSourceFiles($composerData, dirname($composerJson)), + $getPackageSourceFiles($composerData, dirname($composerJson), $checkDevSources ? 'autoload-dev' : 'autoload'), $getAdditionalSourceFiles($options->getScanFiles(), dirname($composerJson)) ) )); diff --git a/src/ComposerRequireChecker/FileLocator/LocateAllFilesByExtension.php b/src/ComposerRequireChecker/FileLocator/LocateAllFilesByExtension.php index 8e07316c..133fd08f 100644 --- a/src/ComposerRequireChecker/FileLocator/LocateAllFilesByExtension.php +++ b/src/ComposerRequireChecker/FileLocator/LocateAllFilesByExtension.php @@ -24,11 +24,11 @@ private function filterFilesByExtension(Traversable $files, string $fileExtensio { $extensionMatcher = '/.*' . preg_quote($fileExtension) . '$/'; - $blacklist = $this->prepareBlacklistPatterns($blacklist); + $blacklistMatcher = '{('.implode('|', $this->prepareBlacklistPatterns($blacklist)).')}'; /* @var $file \SplFileInfo */ foreach ($files as $file) { - if ($blacklist && preg_match('{('.implode('|', $blacklist).')}', $file->getPathname())) { + if ($blacklist && preg_match($blacklistMatcher, $file->getPathname())) { continue; } @@ -40,16 +40,18 @@ private function filterFilesByExtension(Traversable $files, string $fileExtensio } } - private function prepareBlacklistPatterns(?array $blacklistPaths) + private function prepareBlacklistPatterns(?array $blacklistPaths): array { if ($blacklistPaths === null) { - return $blacklistPaths; + return []; } + $dirSep = \preg_quote(DIRECTORY_SEPARATOR, '{}'); + foreach ($blacklistPaths as &$path) { - $path = preg_replace('{/+}', '/', preg_quote(trim(strtr($path, '\\', '/'), '/'))); + $path = preg_replace('{' . $dirSep . '+}', DIRECTORY_SEPARATOR, \preg_quote(trim(str_replace('/', DIRECTORY_SEPARATOR, $path), DIRECTORY_SEPARATOR), '{}')); $path = str_replace('\\*\\*', '.+?', $path); - $path = str_replace('\\*', '[^/]+?', $path); + $path = str_replace('\\*', '[^' . $dirSep . ']+?', $path); } return $blacklistPaths; diff --git a/src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php b/src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php index f318978f..2dcc0054 100644 --- a/src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php +++ b/src/ComposerRequireChecker/FileLocator/LocateComposerPackageDirectDependenciesSourceFiles.php @@ -6,22 +6,25 @@ use ComposerRequireChecker\Exception\NotReadableException; use ComposerRequireChecker\JsonLoader; use Generator; +use Webmozart\Assert\Assert; use function array_key_exists; use function file_get_contents; use function json_decode; final class LocateComposerPackageDirectDependenciesSourceFiles { - public function __invoke(string $composerJsonPath): Generator + public function __invoke(string $composerJsonPath, string $requireKey): Generator { + Assert::oneOf($requireKey, ['require', 'require-dev']); + $packageDir = dirname($composerJsonPath); $composerJson = json_decode(file_get_contents($composerJsonPath), true); $configVendorDir = $composerJson['config']['vendor-dir'] ?? 'vendor'; $vendorDirs = []; - foreach ($composerJson['require'] ?? [] as $vendorName => $vendorRequiredVersion) { + foreach ($composerJson[$requireKey] ?? [] as $vendorName => $vendorRequiredVersion) { $vendorDirs[$vendorName] = $packageDir . '/' . $configVendorDir . '/' . $vendorName; - }; + } $installedPackages = $this->getInstalledPackages($packageDir . '/' . $configVendorDir); @@ -30,7 +33,7 @@ public function __invoke(string $composerJsonPath): Generator continue; } - yield from (new LocateComposerPackageSourceFiles())->__invoke($installedPackages[$vendorName], $vendorDir); + yield from (new LocateComposerPackageSourceFiles())->__invoke($installedPackages[$vendorName], $vendorDir, 'autoload'); } } diff --git a/src/ComposerRequireChecker/FileLocator/LocateComposerPackageSourceFiles.php b/src/ComposerRequireChecker/FileLocator/LocateComposerPackageSourceFiles.php index d9c33e36..5e7df4d6 100644 --- a/src/ComposerRequireChecker/FileLocator/LocateComposerPackageSourceFiles.php +++ b/src/ComposerRequireChecker/FileLocator/LocateComposerPackageSourceFiles.php @@ -3,33 +3,39 @@ namespace ComposerRequireChecker\FileLocator; use Generator; +use Webmozart\Assert\Assert; final class LocateComposerPackageSourceFiles { /** * @param mixed[] $composerData The contents of composer.json for a package * @param string $packageDir The path to package + * @param string $autoloadKey The key of autoload section from composer.json * * @return Generator */ - public function __invoke(array $composerData, string $packageDir): Generator + public function __invoke(array $composerData, string $packageDir, string $autoloadKey): Generator { - $blacklist = $composerData['autoload']['exclude-from-classmap'] ?? null; + Assert::oneOf($autoloadKey, ['autoload', 'autoload-dev']); + + $autoloadData = $composerData[$autoloadKey] ?? []; + + $blacklist = $autoloadData['exclude-from-classmap'] ?? null; yield from $this->locateFilesInClassmapDefinitions( - $this->getFilePaths($composerData['autoload']['classmap'] ?? [], $packageDir), + $this->getFilePaths($autoloadData['classmap'] ?? [], $packageDir), $blacklist ); yield from $this->locateFilesInFilesInFilesDefinitions( - $this->getFilePaths($composerData['autoload']['files'] ?? [], $packageDir), + $this->getFilePaths($autoloadData['files'] ?? [], $packageDir), $blacklist ); yield from $this->locateFilesInPsr0Definitions( - $this->getFilePaths($composerData['autoload']['psr-0'] ?? [], $packageDir), + $this->getFilePaths($autoloadData['psr-0'] ?? [], $packageDir), $blacklist ); yield from $this->locateFilesInPsr4Definitions( - $this->getFilePaths($composerData['autoload']['psr-4'] ?? [], $packageDir), + $this->getFilePaths($autoloadData['psr-4'] ?? [], $packageDir), $blacklist ); } diff --git a/test/ComposerRequireCheckerTest/BinaryTest.php b/test/ComposerRequireCheckerTest/BinaryTest.php index 85dd415f..01be44d2 100644 --- a/test/ComposerRequireCheckerTest/BinaryTest.php +++ b/test/ComposerRequireCheckerTest/BinaryTest.php @@ -12,7 +12,11 @@ final class BinaryTest extends TestCase protected function setUp(): void { - $this->bin = __DIR__ . "/../../bin/composer-require-checker"; + if (strpos(\PHP_OS, "WIN") === 0) { + $this->bin = __DIR__ . "\\..\\..\\bin\\composer-require-checker.bat"; + } else { + $this->bin = __DIR__ . "/../../bin/composer-require-checker"; + } } public function testSuccess(): void diff --git a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php index 478042e9..e22ff928 100644 --- a/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php +++ b/test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php @@ -48,6 +48,23 @@ public function testSelfCheckShowsNoErrors(): void $this->assertNotRegExp('/Collecting used symbols... found \d+ symbols./', $this->commandTester->getDisplay()); } + public function testSelfCheckIncludingDevShowsNoErrors(): void + { + $this->commandTester->execute([ + // that's our own composer.json, lets be sure our self check does not throw errors + 'composer-json' => dirname(__DIR__, 3) . '/composer.json' + ], [ + 'include-dev' => true + ]); + + $this->assertSame(0, $this->commandTester->getStatusCode()); + $this->assertStringContainsString('no unknown symbols found', $this->commandTester->getDisplay()); + + // verbose output should not be shown + $this->assertNotRegExp('/Collecting defined (vendor|extension) symbols... found \d+ symbols./', $this->commandTester->getDisplay()); + $this->assertNotRegExp('/Collecting used symbols... found \d+ symbols./', $this->commandTester->getDisplay()); + } + public function testVerboseSelfCheckShowsCounts(): void { $this->commandTester->execute([ diff --git a/test/ComposerRequireCheckerTest/FileLocator/LocateAllFilesByExtensionTest.php b/test/ComposerRequireCheckerTest/FileLocator/LocateAllFilesByExtensionTest.php index ae2a2640..808b71ae 100644 --- a/test/ComposerRequireCheckerTest/FileLocator/LocateAllFilesByExtensionTest.php +++ b/test/ComposerRequireCheckerTest/FileLocator/LocateAllFilesByExtensionTest.php @@ -49,6 +49,99 @@ public function testLocateFromASingleDirectory(): void } } + public function testLocateWithFilenameBlackList(): void + { + $dir = vfsStream::newDirectory('MyNamespaceA')->at($this->root); + for ($i = 0; $i < 10; $i++) { + vfsStream::newFile("MyClass$i.php")->at($dir); + } + + $foundFiles = $this->locate([$dir->url()], '.php', ['MyClass6']); + + $this->assertCount(9, $foundFiles); + $this->assertNotContains(vfsStream::url('MyClass6.php'), $foundFiles); + } + + public function testLocateWithDirectoryBlackList(): void + { + $dir = vfsStream::newDirectory('MyNamespaceA')->at($this->root); + for ($i = 0; $i < 10; $i++) { + vfsStream::newFile("Directory$i/MyClass.php")->at($dir); + } + + $foundFiles = $this->locate([$dir->url()], '.php', ['Directory5/']); + + $this->assertCount(9, $foundFiles); + $this->assertNotContains(vfsStream::url('Directory5/MyClass.php'), $foundFiles); + } + + /** + * @param array $blacklist + * @param array $expectedFiles + * + * @dataProvider provideBlacklists + */ + public function testLocateWithBlackList(array $blacklist, array $expectedFiles): void + { + $this->root = vfsStream::create([ + 'MyNamespaceA' => [ + 'MyClass.php' => ' [ + 'FooClass.php' => ' [ + 'BarClass.php' => ' [ + 'AnotherBarClass.php' => 'locate([$this->root->url()], '.php', $blacklist); + + $this->assertCount(count($expectedFiles), $foundFiles); + foreach ($expectedFiles as $file) { + $this->assertContains($this->root->getChild($file)->url(), $foundFiles); + } + $this->assertContains($this->root->getChild('MyNamespaceA/Foo/FooClass.php')->url(), $foundFiles); + } + + public function provideBlacklists(): array { + return [ + 'No blacklist' => [ + [], + [ + 'MyNamespaceA/MyClass.php', + 'MyNamespaceA/Foo/FooClass.php', + 'MyNamespaceA/Foo/Bar/BarClass.php', + 'MyNamespaceA/Bar/AnotherBarClass.php', + ] + ], + '* wildcard' => [ + ['Another*.php'], + [ + 'MyNamespaceA/MyClass.php', + 'MyNamespaceA/Foo/FooClass.php', + 'MyNamespaceA/Foo/Bar/BarClass.php', + ] + ], + '** wildcard' => [ + ['**/Bar'], + [ + 'MyNamespaceA/MyClass.php', + 'MyNamespaceA/Foo/FooClass.php', + ] + ], + 'Combined patterns' => [ + ['My*.php', 'Bar/'], + [ + 'MyNamespaceA/Foo/FooClass.php', + ] + ], + ]; + } + /** * @param string[] $directories * @param string $fileExtension @@ -57,6 +150,10 @@ public function testLocateFromASingleDirectory(): void */ private function locate(array $directories, string $fileExtension, ?array $blacklist): array { - return iterator_to_array(($this->locator)(new ArrayObject($directories), $fileExtension, $blacklist)); + $files = []; + foreach (($this->locator)(new ArrayObject($directories), $fileExtension, $blacklist) as $file) { + $files[] = str_replace('\\', '/', $file); + } + return $files; } } diff --git a/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php b/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php index 0c82ea99..e9b97a93 100644 --- a/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php +++ b/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageDirectDependenciesSourceFilesTest.php @@ -36,7 +36,7 @@ public function testNoDependencies(): void ], ]); - $files = $this->locate($this->root->getChild('composer.json')->url()); + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require'); $this->assertCount(0, $files); } @@ -59,7 +59,7 @@ public function testSingleDependency(): void ], ]); - $files = $this->locate($this->root->getChild('composer.json')->url()); + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require'); $this->assertCount(1, $files); @@ -86,7 +86,7 @@ public function testVendorDirsWithoutComposerFilesAreIgnored(): void ], ]); - $files = $this->locate($this->root->getChild('composer.json')->url()); + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require'); $this->assertCount(0, $files); } @@ -109,7 +109,7 @@ public function testVendorConfigSettingIsBeingUsed(): void ], ]); - $files = $this->locate($this->root->getChild('composer.json')->url()); + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require'); $this->assertCount(1, $files); @@ -136,7 +136,7 @@ public function testInstalledJsonUsedAsFallback(): void ], ]); - $files = $this->locate($this->root->getChild('composer.json')->url()); + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require'); $this->assertCount(1, $files); @@ -170,7 +170,7 @@ public function testOldInstalledJsonUsedAsFallback(): void ], ]); - $files = $this->locate($this->root->getChild('composer.json')->url()); + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require'); $this->assertCount(1, $files); @@ -182,11 +182,90 @@ public function testOldInstalledJsonUsedAsFallback(): void $this->assertFalse($this->root->hasChild('vendor/foo/bar/composer.json')); } + public function testWithoutDevDependencies(): void + { + vfsStream::create([ + 'composer.json' => '{"require":{"foo/bar": "^1.0"},"require-dev":{"foo/baz": "^1.0"}}', + 'vendor' => [ + 'composer' => [ + 'installed.json' => + '{"packages":[' . + '{"name": "foo/bar", "autoload":{"psr-4":{"":"src"}}},' . + '{"name": "foo/baz", "autoload":{"psr-4":{"":"src"}}}' . + ']}', + ], + 'foo' => [ + 'bar' => [ + 'src' => [ + 'MyClass.php' => '', + ], + ], + 'baz' => [ + 'src' => [ + 'BazClass.php' => '', + ], + ], + ], + ], + ]); + + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require'); + + $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 testWithDevDependencies(): void + { + vfsStream::create([ + 'composer.json' => '{"require":{"foo/bar": "^1.0"},"require-dev":{"foo/baz": "^1.0"}}', + 'vendor' => [ + 'composer' => [ + 'installed.json' => <<<'INSTALLEDJSON' +{ + "packages": [ + {"name": "foo/bar", "autoload": {"psr-4":{"":"src"}}}, + {"name": "foo/baz", "autoload": {"psr-4":{"":"src"}}} + ] +} +INSTALLEDJSON + ], + 'foo' => [ + 'bar' => [ + 'src' => [ + 'MyClass.php' => '', + ], + ], + 'baz' => [ + 'src' => [ + 'BazClass.php' => '', + ], + ], + ], + ], + ]); + + $files = $this->locate($this->root->getChild('composer.json')->url(), 'require-dev'); + + $this->assertCount(1, $files); + $this->assertContains($this->root->getChild('vendor/foo/baz/src/BazClass.php')->url(), $files); + } + /** + * @param string $composerJson + * @param string $requireKey * @return string[] */ - private function locate(string $composerJson): array + private function locate(string $composerJson, string $requireKey): array { - return iterator_to_array(($this->locator)($composerJson)); + $files = []; + $generator = ($this->locator)($composerJson, $requireKey); + foreach ($generator as $file) { + $files[] = str_replace(DIRECTORY_SEPARATOR, '/', $file); + } + return $files; } } diff --git a/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageSourceFilesTest.php b/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageSourceFilesTest.php index 0b3a2969..80555dd0 100644 --- a/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageSourceFilesTest.php +++ b/test/ComposerRequireCheckerTest/FileLocator/LocateComposerPackageSourceFilesTest.php @@ -36,7 +36,7 @@ public function testFromClassmap(): void 'MyClassB.php' => 'files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(2, $files); $this->assertContains($this->root->getChild('src/MyClassA.php')->url(), $files); @@ -50,7 +50,7 @@ public function testFromFiles(): void 'foo.php' => 'files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(1, $files); $this->assertContains($this->root->getChild('foo.php')->url(), $files); @@ -67,9 +67,10 @@ public function testFromPsr0(): void ], ]); - $files = $this->files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(1, $files); + $this->assertContains($this->root->getChild('src/MyNamespace/MyClass.php')->url(), $files); } public function testFromPsr4(): void @@ -81,9 +82,10 @@ public function testFromPsr4(): void ], ]); - $files = $this->files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(1, $files); + $this->assertContains($this->root->getChild('src/MyClass.php')->url(), $files); } public function testFromPsr0WithMultipleDirectories(): void @@ -94,7 +96,7 @@ public function testFromPsr0WithMultipleDirectories(): void 'lib' => ['MyNamespace' => ['MyClassB.php' => 'files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(2, $files); $this->assertContains($this->root->getChild('src/MyNamespace/MyClassA.php')->url(), $files); @@ -109,7 +111,7 @@ public function testFromPsr4WithMultipleDirectories(): void 'lib' => ['MyClassB.php' => 'files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(2, $files); $this->assertContains($this->root->getChild('src/MyClassA.php')->url(), $files); @@ -121,11 +123,11 @@ public function testFromPsr4WithNestedDirectory(): void vfsStream::create([ 'composer.json' => '{"autoload": {"psr-4": {"MyNamespace\\\\": ["src/MyNamespace"]}}}', 'src' => [ - 'MyNamespace' => ['MyClassA.php' => ' ['MyClassA.php' => 'files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(1, $files); $this->assertContains($this->root->getChild('src/MyNamespace/MyClassA.php')->url(), $files); @@ -136,11 +138,11 @@ public function testFromPsr4WithNestedDirectoryAlternativeDirectorySeparator(): vfsStream::create([ 'composer.json' => '{"autoload": {"psr-4": {"MyNamespace\\\\": ["src\\\\MyNamespace"]}}}', 'src' => [ - 'MyNamespace' => ['MyClassA.php' => ' ['MyClassA.php' => 'files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); $this->assertCount(1, $files); $this->assertContains($this->root->getChild('src/MyNamespace/MyClassA.php')->url(), $files); @@ -160,24 +162,49 @@ public function testFromPsr4WithExcludeFromClassmap(array $excludedPattern, arra 'ATest.php' => ' [ - 'Tests' => [ + 'Bar' => [ 'BTest.php' => ' [ 'ClassB.php' => ' [ + 'Bar' => [ 'CTest.php' => 'files($this->root->getChild('composer.json')->url()); + $files = $this->files($this->root->getChild('composer.json')->url(), 'autoload'); - $this->assertCount(count($expectedFiles), $files); foreach ($expectedFiles as $expectedFile) { $this->assertContains($this->root->getChild($expectedFile)->url(), $files); } + $this->assertCount(count($expectedFiles), $files); + } + + public function testFromDevClassmap(): void + { + vfsStream::create([ + 'composer.json' => <<<'COMPOSERJSON' +{ + "autoload": {"classmap": ["src/MyClassA.php"]}, + "autoload-dev": {"classmap": ["tests/MyClassATest.php"]} +} +COMPOSERJSON + , + 'src' => [ + 'MyClassA.php' => ' [ + 'MyClassATest.php' => 'files($this->root->getChild('composer.json')->url(), 'autoload-dev'); + + $this->assertCount(1, $files); + $this->assertNotContains($this->root->getChild('src/MyClassA.php')->url(), $files); + $this->assertContains($this->root->getChild('tests/MyClassATest.php')->url(), $files); } /** @@ -191,23 +218,22 @@ public function provideExcludePattern(): array [ 'ClassA.php', 'tests/ATest.php', - 'foo/Tests/BTest.php', + 'foo/Bar/BTest.php', 'foo/src/ClassB.php', - 'foo/src/Tests/CTest.php', - + 'foo/src/Bar/CTest.php', ], ], 'Exclude single directory by pattern' => [ ['/tests/'], [ 'ClassA.php', - 'foo/Tests/BTest.php', + 'foo/Bar/BTest.php', 'foo/src/ClassB.php', - 'foo/src/Tests/CTest.php', + 'foo/src/Bar/CTest.php', ], ], 'Exclude all subdirectories by pattern' => [ - ['**/Tests/'], + ['**/Bar/'], [ 'ClassA.php', 'tests/ATest.php', @@ -215,7 +241,7 @@ public function provideExcludePattern(): array ], ], 'Combine multiple patterns' => [ - ['/tests/', '**/Tests/'], + ['/tests/', '**/Bar/'], [ 'ClassA.php', 'foo/src/ClassB.php', @@ -227,13 +253,13 @@ public function provideExcludePattern(): array /** * @return string[] */ - private function files(string $composerJson): array + private function files(string $composerJson, string $autoloadKey): array { $composerData = (new JsonLoader($composerJson))->getData(); $files = []; - $filesGenerator = ($this->locator)($composerData, dirname($composerJson)); + $filesGenerator = ($this->locator)($composerData, dirname($composerJson), $autoloadKey); foreach ($filesGenerator as $file) { - $files[] = $file; + $files[] = str_replace('\\', '/', $file); } return $files; }