diff --git a/.gitignore b/.gitignore index 3bef9a5b1..91928691b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /.phpcs-cache /.phpunit.result.cache /phpcs.xml -/vendor +./vendor diff --git a/composer.json b/composer.json index 1f68e07bb..c269be8cb 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "license": "MIT", "require": { "php": ">=7.2.0,<7.4.0", + "ext-json": "*", "jetbrains/phpstorm-stubs": "2019.1", "nikic/php-parser": "^4.0.4", "phpdocumentor/reflection-docblock": "^4.1.1", diff --git a/composer.lock b/composer.lock index f1317e851..0fdca5097 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": "03bf49a514a13fde0a6e5ab048048915", + "content-hash": "2227819d7c5d9b4044210f18c1ed1866", "packages": [ { "name": "jetbrains/phpstorm-stubs", @@ -1875,7 +1875,8 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.1.0,<7.4.0" + "php": ">=7.2.0,<7.4.0", + "ext-json": "*" }, "platform-dev": [] } diff --git a/docs/usage.md b/docs/usage.md index 1190cadd3..b8b238a0e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -118,6 +118,39 @@ $classInfo = ReflectionClass::createFromName('MyClass'); $functionInfo = ReflectionFunction::createFromName('foo'); ``` +### Inspecting code and dependencies of a composer-based project + +If you need to inspect code from a project that has a `composer.json` and +its associated `vendor/` directory populated, this package offers some +factories that ease the setup of the source locator. These are: + + * `Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForComposerJsonAndInstalledJson` - if + you need to inspect project and dependencies + * `Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForComposerJson` - if you only want to + inspect project sources + * `Roave\BetterReflection\SourceLocator\Type\Composer\Factory\MakeLocatorForInstalledJson` - if you only want + to inspect project dependencies + +Here's an example of `MakeLocatorForComposerJsonAndInstalledJson` usage: + +```php +astLocator(); +$reflector = new ClassReflector(new AggregateSourceLocator([ + (new MakeLocatorForComposerJsonAndInstalledJson)('path/to/the/project', $astLocator), + new PhpInternalSourceLocator($astLocator) +])); + +$classes = $reflector->getAllClasses(); +``` + ### Using the Composer autoloader directly ```php diff --git a/src/SourceLocator/Type/Composer/Factory/Exception/Exception.php b/src/SourceLocator/Type/Composer/Factory/Exception/Exception.php new file mode 100644 index 000000000..ffc7e63bc --- /dev/null +++ b/src/SourceLocator/Type/Composer/Factory/Exception/Exception.php @@ -0,0 +1,11 @@ +prefixPaths($this->packageToClassMapPaths($composer), $pathPrefix); + $classMapFiles = array_filter($classMapPaths, 'is_file'); + $classMapDirectories = array_filter($classMapPaths, 'is_dir'); + $filePaths = $this->prefixPaths($this->packageToFilePaths($composer), $pathPrefix); + + return new AggregateSourceLocator(array_merge( + [ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings( + $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $pathPrefix) + ), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings( + $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $pathPrefix) + ), + $astLocator + ), + new DirectoriesSourceLocator($classMapDirectories, $astLocator), + ], + ...array_map(static function (string $file) use ($astLocator) : array { + return [new SingleFileSourceLocator($file, $astLocator)]; + }, array_merge($classMapFiles, $filePaths)) + )); + } + + /** + * @param mixed[] $package + * + * @return array> + */ + private function packageToPsr4AutoloadNamespaces(array $package) : array + { + return array_map(static function ($namespacePaths) : array { + return (array) $namespacePaths; + }, $package['autoload']['psr-4'] ?? []); + } + + /** + * @param mixed[] $package + * + * @return array> + */ + private function packageToPsr0AutoloadNamespaces(array $package) : array + { + return array_map(static function ($namespacePaths) : array { + return (array) $namespacePaths; + }, $package['autoload']['psr-0'] ?? []); + } + + /** + * @param mixed[] $package + * + * @return array + */ + private function packageToClassMapPaths(array $package) : array + { + return $package['autoload']['classmap'] ?? []; + } + + /** + * @param mixed[] $package + * + * @return array + */ + private function packageToFilePaths(array $package) : array + { + return $package['autoload']['files'] ?? []; + } + + /** + * @param array> $paths + * + * @return array> + */ + private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath) : array + { + return array_map(function (array $paths) use ($trimmedInstallationPath) : array { + return $this->prefixPaths($paths, $trimmedInstallationPath); + }, $paths); + } + + /** + * @param array $paths + * + * @return array + */ + private function prefixPaths(array $paths, string $prefix) : array + { + return array_map(static function (string $path) use ($prefix) { + return $prefix . $path; + }, $paths); + } +} diff --git a/src/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonAndInstalledJson.php b/src/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonAndInstalledJson.php new file mode 100644 index 000000000..4896b9ae8 --- /dev/null +++ b/src/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonAndInstalledJson.php @@ -0,0 +1,213 @@ +prefixPaths($this->packageToClassMapPaths($composer), $realInstallationPath . '/'), + ...array_map(function (array $package) use ($realInstallationPath) : array { + return $this->prefixPaths( + $this->packageToClassMapPaths($package), + $this->packagePrefixPath($realInstallationPath, $package) + ); + }, $installed) + ); + $classMapFiles = array_filter($classMapPaths, 'is_file'); + $classMapDirectories = array_filter($classMapPaths, 'is_dir'); + $filePaths = array_merge( + $this->prefixPaths($this->packageToFilePaths($composer), $realInstallationPath . '/'), + ...array_map(function (array $package) use ($realInstallationPath) : array { + return $this->prefixPaths( + $this->packageToFilePaths($package), + $this->packagePrefixPath($realInstallationPath, $package) + ); + }, $installed) + ); + + return new AggregateSourceLocator(array_merge( + [ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings(array_merge_recursive( + $this->prefixWithInstallationPath($this->packageToPsr4AutoloadNamespaces($composer), $realInstallationPath), + ...array_map(function (array $package) use ($realInstallationPath) : array { + return $this->prefixWithPackagePath( + $this->packageToPsr4AutoloadNamespaces($package), + $realInstallationPath, + $package + ); + }, $installed) + )), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings(array_merge_recursive( + $this->prefixWithInstallationPath($this->packageToPsr0AutoloadNamespaces($composer), $realInstallationPath), + ...array_map(function (array $package) use ($realInstallationPath) : array { + return $this->prefixWithPackagePath( + $this->packageToPsr0AutoloadNamespaces($package), + $realInstallationPath, + $package + ); + }, $installed) + )), + $astLocator + ), + new DirectoriesSourceLocator($classMapDirectories, $astLocator), + ], + ...array_map(static function (string $file) use ($astLocator) : array { + return [new SingleFileSourceLocator($file, $astLocator)]; + }, array_merge($classMapFiles, $filePaths)) + )); + } + + /** + * @param mixed[] $package + * + * @return array> + */ + private function packageToPsr4AutoloadNamespaces(array $package) : array + { + return array_map(static function ($namespacePaths) : array { + return (array) $namespacePaths; + }, $package['autoload']['psr-4'] ?? []); + } + + /** + * @param mixed[] $package + * + * @return array> + */ + private function packageToPsr0AutoloadNamespaces(array $package) : array + { + return array_map(static function ($namespacePaths) : array { + return (array) $namespacePaths; + }, $package['autoload']['psr-0'] ?? []); + } + + /** + * @param mixed[] $package + * + * @return array + */ + private function packageToClassMapPaths(array $package) : array + { + return $package['autoload']['classmap'] ?? []; + } + + /** + * @param mixed[] $package + * + * @return array + */ + private function packageToFilePaths(array $package) : array + { + return $package['autoload']['files'] ?? []; + } + + /** + * @param mixed[] $package + * + * @psalm-param array{name: string} $package + */ + private function packagePrefixPath(string $trimmedInstallationPath, array $package) : string + { + return $trimmedInstallationPath . '/vendor/' . $package['name'] . '/'; + } + + /** + * @param array> $paths + * @param array> $package + * + * @return array> + * + * @psalm-param array{name: string} $package + */ + private function prefixWithPackagePath(array $paths, string $trimmedInstallationPath, array $package) : array + { + $prefix = $this->packagePrefixPath($trimmedInstallationPath, $package); + + return array_map(function (array $paths) use ($prefix) : array { + return $this->prefixPaths($paths, $prefix); + }, $paths); + } + + /** + * @param array> $paths + * + * @return array> + */ + private function prefixWithInstallationPath(array $paths, string $trimmedInstallationPath) : array + { + return array_map(function (array $paths) use ($trimmedInstallationPath) : array { + return $this->prefixPaths($paths, $trimmedInstallationPath . '/'); + }, $paths); + } + + /** + * @param array $paths + * + * @return array + */ + private function prefixPaths(array $paths, string $prefix) : array + { + return array_map(static function (string $path) use ($prefix) { + return $prefix . $path; + }, $paths); + } +} diff --git a/src/SourceLocator/Type/Composer/Factory/MakeLocatorForInstalledJson.php b/src/SourceLocator/Type/Composer/Factory/MakeLocatorForInstalledJson.php new file mode 100644 index 000000000..aa36593ed --- /dev/null +++ b/src/SourceLocator/Type/Composer/Factory/MakeLocatorForInstalledJson.php @@ -0,0 +1,190 @@ +prefixPaths( + $this->packageToClassMapPaths($package), + $this->packagePrefixPath($realInstallationPath, $package) + ); + }, $installed) + ); + $classMapFiles = array_filter($classMapPaths, 'is_file'); + $classMapDirectories = array_filter($classMapPaths, 'is_dir'); + $filePaths = array_merge( + [], + ...array_map(function (array $package) use ($realInstallationPath) : array { + return $this->prefixPaths( + $this->packageToFilePaths($package), + $this->packagePrefixPath($realInstallationPath, $package) + ); + }, $installed) + ); + + return new AggregateSourceLocator(array_merge( + [ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings(array_merge_recursive( + [], + ...array_map(function (array $package) use ($realInstallationPath) : array { + return $this->prefixWithPackagePath( + $this->packageToPsr4AutoloadNamespaces($package), + $realInstallationPath, + $package + ); + }, $installed) + )), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings(array_merge_recursive( + [], + ...array_map(function (array $package) use ($realInstallationPath) : array { + return $this->prefixWithPackagePath( + $this->packageToPsr0AutoloadNamespaces($package), + $realInstallationPath, + $package + ); + }, $installed) + )), + $astLocator + ), + new DirectoriesSourceLocator($classMapDirectories, $astLocator), + ], + ...array_map(static function (string $file) use ($astLocator) : array { + return [new SingleFileSourceLocator($file, $astLocator)]; + }, array_merge($classMapFiles, $filePaths)) + )); + } + + /** + * @param mixed[] $package + * + * @return array> + */ + private function packageToPsr4AutoloadNamespaces(array $package) : array + { + return array_map(static function ($namespacePaths) : array { + return (array) $namespacePaths; + }, $package['autoload']['psr-4'] ?? []); + } + + /** + * @param mixed[] $package + * + * @return array> + */ + private function packageToPsr0AutoloadNamespaces(array $package) : array + { + return array_map(static function ($namespacePaths) : array { + return (array) $namespacePaths; + }, $package['autoload']['psr-0'] ?? []); + } + + /** + * @param mixed[] $package + * + * @return array + */ + private function packageToClassMapPaths(array $package) : array + { + return $package['autoload']['classmap'] ?? []; + } + + /** + * @param mixed[] $package + * + * @return array + */ + private function packageToFilePaths(array $package) : array + { + return $package['autoload']['files'] ?? []; + } + + /** + * @param mixed[] $package + * + * @psalm-param array{name: string} $package + */ + private function packagePrefixPath(string $trimmedInstallationPath, array $package) : string + { + return $trimmedInstallationPath . '/vendor/' . $package['name'] . '/'; + } + + /** + * @param array> $paths + * @param array> $package + * + * @return array> + * + * @psalm-param array{name: string} $package + */ + private function prefixWithPackagePath(array $paths, string $trimmedInstallationPath, array $package) : array + { + $prefix = $this->packagePrefixPath($trimmedInstallationPath, $package); + + return array_map(function (array $paths) use ($prefix) : array { + return $this->prefixPaths($paths, $prefix); + }, $paths); + } + + /** + * @param array $paths + * + * @return array + */ + private function prefixPaths(array $paths, string $prefix) : array + { + return array_map(static function (string $path) use ($prefix) { + return $prefix . $path; + }, $paths); + } +} diff --git a/src/SourceLocator/Type/Composer/Psr/Exception/Exception.php b/src/SourceLocator/Type/Composer/Psr/Exception/Exception.php new file mode 100644 index 000000000..581ab648d --- /dev/null +++ b/src/SourceLocator/Type/Composer/Psr/Exception/Exception.php @@ -0,0 +1,11 @@ +> */ + private $mappings = []; + + private function __construct() + { + } + + /** @param array> $mappings */ + public static function fromArrayMappings(array $mappings) : self + { + self::assertValidMapping($mappings); + + $instance = new self(); + + $instance->mappings = array_map( + static function (array $directories) : array { + return array_map(static function (string $directory) : string { + return rtrim($directory, '/'); + }, $directories); + }, + $mappings + ); + + return $instance; + } + + /** {@inheritDoc} */ + public function resolvePossibleFilePaths(Identifier $identifier) : array + { + if (! $identifier->isClass()) { + return []; + } + + $className = $identifier->getName(); + + foreach ($this->mappings as $prefix => $paths) { + if (strpos($className, $prefix) === 0) { + return array_map( + static function (string $path) use ($className) : string { + return rtrim($path, '/') . '/' . str_replace(['\\', '_'], '/', $className) . '.php'; + }, + $paths + ); + } + } + + return []; + } + + /** {@inheritDoc} */ + public function directories() : array + { + return array_values(array_unique(array_merge([], ...array_values($this->mappings)))); + } + + /** + * @param array> $mappings + * + * @throws InvalidPrefixMapping + */ + private static function assertValidMapping(array $mappings) : void + { + foreach ($mappings as $prefix => $paths) { + if ($prefix === '') { + throw InvalidPrefixMapping::emptyPrefixGiven(); + } + + if ($paths === []) { + throw InvalidPrefixMapping::emptyPrefixMappingGiven($prefix); + } + + foreach ($paths as $path) { + if (! is_dir($path)) { + throw InvalidPrefixMapping::prefixMappingIsNotADirectory($prefix, $path); + } + } + } + } +} diff --git a/src/SourceLocator/Type/Composer/Psr/Psr4Mapping.php b/src/SourceLocator/Type/Composer/Psr/Psr4Mapping.php new file mode 100644 index 000000000..ce9a2edb8 --- /dev/null +++ b/src/SourceLocator/Type/Composer/Psr/Psr4Mapping.php @@ -0,0 +1,119 @@ +> */ + private $mappings = []; + + private function __construct() + { + } + + /** @param array> $mappings */ + public static function fromArrayMappings(array $mappings) : self + { + self::assertValidMapping($mappings); + + $instance = new self(); + + $instance->mappings = array_map( + static function (array $directories) : array { + return array_map(static function (string $directory) : string { + return rtrim($directory, '/'); + }, $directories); + }, + $mappings + ); + + return $instance; + } + + /** {@inheritDoc} */ + public function resolvePossibleFilePaths(Identifier $identifier) : array + { + if (! $identifier->isClass()) { + return []; + } + + $className = $identifier->getName(); + $matchingPrefixes = $this->matchingPrefixes($className); + + return array_values(array_filter(array_merge( + [], + ...array_map(static function (array $paths, string $prefix) use ($className) : array { + $subPath = ltrim(str_replace('\\', '/', substr($className, strlen($prefix))), '/'); + + if ($subPath === '') { + return []; + } + + return array_map(static function (string $path) use ($subPath) : string { + return rtrim($path, '/') . '/' . $subPath . '.php'; + }, $paths); + }, $matchingPrefixes, array_keys($matchingPrefixes)) + ))); + } + + /** @return array> */ + private function matchingPrefixes(string $className) : array + { + return array_filter( + $this->mappings, + static function (string $prefix) use ($className) : bool { + return strpos($className, $prefix) === 0; + }, + ARRAY_FILTER_USE_KEY + ); + } + + /** {@inheritDoc} */ + public function directories() : array + { + return array_values(array_unique(array_merge([], ...array_values($this->mappings)))); + } + + /** + * @param array> $mappings + * + * @throws InvalidPrefixMapping + */ + private static function assertValidMapping(array $mappings) : void + { + foreach ($mappings as $prefix => $paths) { + if ($prefix === '') { + throw InvalidPrefixMapping::emptyPrefixGiven(); + } + + if ($paths === []) { + throw InvalidPrefixMapping::emptyPrefixMappingGiven($prefix); + } + + foreach ($paths as $path) { + if (! is_dir($path)) { + throw InvalidPrefixMapping::prefixMappingIsNotADirectory($prefix, $path); + } + } + } + } +} diff --git a/src/SourceLocator/Type/Composer/Psr/PsrAutoloaderMapping.php b/src/SourceLocator/Type/Composer/Psr/PsrAutoloaderMapping.php new file mode 100644 index 000000000..6ddf0e8e1 --- /dev/null +++ b/src/SourceLocator/Type/Composer/Psr/PsrAutoloaderMapping.php @@ -0,0 +1,16 @@ + */ + public function resolvePossibleFilePaths(Identifier $identifier) : array; + + /** @return array */ + public function directories() : array; +} diff --git a/src/SourceLocator/Type/Composer/PsrAutoloaderLocator.php b/src/SourceLocator/Type/Composer/PsrAutoloaderLocator.php new file mode 100644 index 000000000..9864da72d --- /dev/null +++ b/src/SourceLocator/Type/Composer/PsrAutoloaderLocator.php @@ -0,0 +1,73 @@ +mapping = $mapping; + $this->astLocator = $astLocator; + } + + /** + * {@inheritDoc} + */ + public function locateIdentifier(Reflector $reflector, Identifier $identifier) : ?Reflection + { + foreach ($this->mapping->resolvePossibleFilePaths($identifier) as $file) { + if (! file_exists($file)) { + continue; + } + + try { + return $this->astLocator->findReflection( + $reflector, + new LocatedSource( + file_get_contents($file), + $file + ), + $identifier + ); + } catch (IdentifierNotFound $exception) { + // on purpose - autoloading is allowed to fail, and silently-failing autoloaders are normal/endorsed + } + } + + return null; + } + + /** + * Find all identifiers of a type + * + * @return Reflection[] + */ + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType) : array + { + return (new DirectoriesSourceLocator( + $this->mapping->directories(), + $this->astLocator + ))->locateIdentifiersByType($reflector, $identifierType); + } +} diff --git a/test/unit/Assets/ComposerLocators/empty-project/.gitignore b/test/unit/Assets/ComposerLocators/empty-project/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/empty-project/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/composer.json b/test/unit/Assets/ComposerLocators/project-a/composer.json new file mode 100644 index 000000000..cc90907d5 --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/composer.json @@ -0,0 +1,20 @@ +{ + "autoload": { + "psr-4": { + "ProjectA\\": "src/root_PSR-4_Sources", + "ProjectA\\B\\": "src/root_PSR-4_Sources" + }, + "psr-0": { + "ProjectA_A_": "src/root_PSR-0_Sources", + "ProjectA_B_": "src/root_PSR-0_Sources" + }, + "classmap": [ + "src/root_ClassmapDir", + "src/root_ClassmapFile" + ], + "files": [ + "src/root_File1.php", + "src/root_File2.php" + ] + } +} diff --git a/test/unit/Assets/ComposerLocators/project-a/src/root_ClassmapDir/.gitignore b/test/unit/Assets/ComposerLocators/project-a/src/root_ClassmapDir/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/src/root_ClassmapDir/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/src/root_ClassmapFile b/test/unit/Assets/ComposerLocators/project-a/src/root_ClassmapFile new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/src/root_File1.php b/test/unit/Assets/ComposerLocators/project-a/src/root_File1.php new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/src/root_File2.php b/test/unit/Assets/ComposerLocators/project-a/src/root_File2.php new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/src/root_PSR-0_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-a/src/root_PSR-0_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/src/root_PSR-0_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/src/root_PSR-4_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-a/src/root_PSR-4_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/src/root_PSR-4_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_ClassmapDir/.gitignore b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_ClassmapDir/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_ClassmapDir/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_ClassmapFile b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_ClassmapFile new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_File1.php b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_File1.php new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_File2.php b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_File2.php new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_PSR-0_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_PSR-0_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_PSR-0_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_PSR-4_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_PSR-4_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/vendor/a/b/src/ab_PSR-4_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/composer/installed.json b/test/unit/Assets/ComposerLocators/project-a/vendor/composer/installed.json new file mode 100644 index 000000000..e9cd7722e --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/vendor/composer/installed.json @@ -0,0 +1,66 @@ +[ + { + "name": "a/b", + "autoload": { + "psr-4": { + "A\\B\\": "src/ab_PSR-4_Sources", + "C\\D\\": [ + "src/ab_PSR-4_Sources" + ] + }, + "psr-0": { + "A_B_": "src/ab_PSR-0_Sources", + "C_D_": [ + "src/ab_PSR-0_Sources" + ] + }, + "classmap": [ + "src/ab_ClassmapDir", + "src/ab_ClassmapFile" + ], + "files": [ + "src/ab_File1.php", + "src/ab_File2.php" + ] + } + }, + { + "name": "e/f", + "autoload": { + "psr-4": { + "E\\F\\": [ + "src/ef_PSR-4_Sources" + ] + }, + "psr-0": { + "E_F_": [ + "src/ef_PSR-0_Sources" + ] + }, + "classmap": [ + "src/ef_ClassmapDir", + "src/ef_ClassmapFile" + ], + "files": [ + "src/ef_File1.php", + "src/ef_File2.php" + ] + } + }, + { + "name": "this-has/no-autoload-definitions" + }, + { + "name": "this-has/empty-autoload-definitions", + "autoload": {} + }, + { + "name": "this-has/empty-autoload-definition-blocks", + "autoload": { + "psr-0": {}, + "psr-4": {}, + "classmap": [], + "files": [] + } + } +] diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_ClassmapDir/.gitignore b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_ClassmapDir/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_ClassmapDir/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_ClassmapFile b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_ClassmapFile new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_File1.php b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_File1.php new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_File2.php b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_File2.php new file mode 100644 index 000000000..e69de29bb diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_PSR-0_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_PSR-0_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_PSR-0_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_PSR-4_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_PSR-4_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-a/vendor/e/f/src/ef_PSR-4_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-with-invalid-composer-json/composer.json b/test/unit/Assets/ComposerLocators/project-with-invalid-composer-json/composer.json new file mode 100644 index 000000000..72943a16f --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-invalid-composer-json/composer.json @@ -0,0 +1 @@ +aaa diff --git a/test/unit/Assets/ComposerLocators/project-with-invalid-composer-json/vendor/composer/installed.json b/test/unit/Assets/ComposerLocators/project-with-invalid-composer-json/vendor/composer/installed.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-invalid-composer-json/vendor/composer/installed.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/unit/Assets/ComposerLocators/project-with-invalid-installed-json/composer.json b/test/unit/Assets/ComposerLocators/project-with-invalid-installed-json/composer.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-invalid-installed-json/composer.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/unit/Assets/ComposerLocators/project-with-invalid-installed-json/vendor/composer/installed.json b/test/unit/Assets/ComposerLocators/project-with-invalid-installed-json/vendor/composer/installed.json new file mode 100644 index 000000000..72943a16f --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-invalid-installed-json/vendor/composer/installed.json @@ -0,0 +1 @@ +aaa diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/composer.json b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/composer.json new file mode 100644 index 000000000..e45ac6f6b --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/composer.json @@ -0,0 +1,10 @@ +{ + "autoload": { + "psr-4": { + "A\\": "src/root_PSR-4_Sources" + }, + "psr-0": { + "A_": "src/root_PSR-0_Sources" + } + } +} diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/src/root_PSR-0_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/src/root_PSR-0_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/src/root_PSR-0_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/src/root_PSR-4_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/src/root_PSR-4_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/src/root_PSR-4_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/a/b/src/ab_PSR-0_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/a/b/src/ab_PSR-0_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/a/b/src/ab_PSR-0_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/a/b/src/ab_PSR-4_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/a/b/src/ab_PSR-4_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/a/b/src/ab_PSR-4_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/composer/installed.json b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/composer/installed.json new file mode 100644 index 000000000..d9410688e --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/composer/installed.json @@ -0,0 +1,34 @@ +[ + { + "name": "a/b", + "autoload": { + "psr-4": { + "A\\": "src/ab_PSR-4_Sources", + "B\\": [ + "src/ab_PSR-4_Sources" + ] + }, + "psr-0": { + "A_": "src/ab_PSR-0_Sources", + "B_": [ + "src/ab_PSR-0_Sources" + ] + } + } + }, + { + "name": "e/f", + "autoload": { + "psr-4": { + "A\\": [ + "src/ef_PSR-4_Sources" + ] + }, + "psr-0": { + "A_": [ + "src/ef_PSR-0_Sources" + ] + } + } + } +] diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/e/f/src/ef_PSR-0_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/e/f/src/ef_PSR-0_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/e/f/src/ef_PSR-0_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/e/f/src/ef_PSR-4_Sources/.gitignore b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/e/f/src/ef_PSR-4_Sources/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-with-psr-collisions/vendor/e/f/src/ef_PSR-4_Sources/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-without-autoload-definitions/composer.json b/test/unit/Assets/ComposerLocators/project-without-autoload-definitions/composer.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-without-autoload-definitions/composer.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/unit/Assets/ComposerLocators/project-without-autoload-definitions/vendor/composer/installed.json b/test/unit/Assets/ComposerLocators/project-without-autoload-definitions/vendor/composer/installed.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-without-autoload-definitions/vendor/composer/installed.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/unit/Assets/ComposerLocators/project-without-installed.json/.gitignore b/test/unit/Assets/ComposerLocators/project-without-installed.json/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-without-installed.json/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/test/unit/Assets/ComposerLocators/project-without-installed.json/composer.json b/test/unit/Assets/ComposerLocators/project-without-installed.json/composer.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/test/unit/Assets/ComposerLocators/project-without-installed.json/composer.json @@ -0,0 +1,2 @@ +{ +} diff --git a/test/unit/SourceLocator/Type/Composer/Factory/Exception/FailedToParseJsonTest.php b/test/unit/SourceLocator/Type/Composer/Factory/Exception/FailedToParseJsonTest.php new file mode 100644 index 000000000..979a20f35 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Factory/Exception/FailedToParseJsonTest.php @@ -0,0 +1,23 @@ +getMessage() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Factory/Exception/InvalidProjectDirectoryTest.php b/test/unit/SourceLocator/Type/Composer/Factory/Exception/InvalidProjectDirectoryTest.php new file mode 100644 index 000000000..a2f8717b3 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Factory/Exception/InvalidProjectDirectoryTest.php @@ -0,0 +1,23 @@ +getMessage() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Factory/Exception/MissingComposerJsonTest.php b/test/unit/SourceLocator/Type/Composer/Factory/Exception/MissingComposerJsonTest.php new file mode 100644 index 000000000..601ee7d70 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Factory/Exception/MissingComposerJsonTest.php @@ -0,0 +1,23 @@ +getMessage() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Factory/Exception/MissingInstalledJsonTest.php b/test/unit/SourceLocator/Type/Composer/Factory/Exception/MissingInstalledJsonTest.php new file mode 100644 index 000000000..73ee35a64 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Factory/Exception/MissingInstalledJsonTest.php @@ -0,0 +1,23 @@ +getMessage() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerInstalledJsonTest.php b/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerInstalledJsonTest.php new file mode 100644 index 000000000..c55f6bab5 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerInstalledJsonTest.php @@ -0,0 +1,189 @@ +__invoke($projectDirectory, BetterReflectionSingleton::instance()->astLocator()) + ); + } + + /** + * @return array> + * + * @psalm-return array + */ + public function expectedLocators() : array + { + $astLocator = BetterReflectionSingleton::instance()->astLocator(); + + $projectA = realpath(__DIR__ . '/../../../../Assets/ComposerLocators/project-a'); + $projectWithPsrCollisions = realpath(__DIR__ . '/../../../../Assets/ComposerLocators/project-with-psr-collisions'); + $projectALocator = new AggregateSourceLocator([ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings([ + 'A\\B\\' => [ + $projectA . '/vendor/a/b/src/ab_PSR-4_Sources', + ], + 'C\\D\\' => [ + $projectA . '/vendor/a/b/src/ab_PSR-4_Sources', + ], + 'E\\F\\' => [ + $projectA . '/vendor/e/f/src/ef_PSR-4_Sources', + ], + ]), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings([ + 'A_B_' => [ + $projectA . '/vendor/a/b/src/ab_PSR-0_Sources', + ], + 'C_D_' => [ + $projectA . '/vendor/a/b/src/ab_PSR-0_Sources', + ], + 'E_F_' => [ + $projectA . '/vendor/e/f/src/ef_PSR-0_Sources', + ], + ]), + $astLocator + ), + new DirectoriesSourceLocator( + [ + $projectA . '/vendor/a/b/src/ab_ClassmapDir', + $projectA . '/vendor/e/f/src/ef_ClassmapDir', + ], + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/a/b/src/ab_ClassmapFile', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/e/f/src/ef_ClassmapFile', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/a/b/src/ab_File1.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/a/b/src/ab_File2.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/e/f/src/ef_File1.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/e/f/src/ef_File2.php', + $astLocator + ), + ]); + + $expectedLocators = [ + [ + $projectA, + $projectALocator, + ], + [ + $projectWithPsrCollisions, + new AggregateSourceLocator([ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings([ + 'A\\' => [ + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-4_Sources', + $projectWithPsrCollisions . '/vendor/e/f/src/ef_PSR-4_Sources', + ], + 'B\\' => [ + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-4_Sources', + ], + ]), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings([ + 'A_' => [ + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-0_Sources', + $projectWithPsrCollisions . '/vendor/e/f/src/ef_PSR-0_Sources', + ], + 'B_' => [ + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-0_Sources', + ], + ]), + $astLocator + ), + new DirectoriesSourceLocator([], $astLocator), + ]), + ], + [ + // Relative paths are turned into absolute paths too + __DIR__ . '/../../../../Assets/ComposerLocators/project-a', + $projectALocator, + ], + ]; + + return array_combine(array_column($expectedLocators, 0), $expectedLocators); + } + + public function testWillFailToProduceLocatorForProjectWithoutInstalledJson() : void + { + $this->expectException(MissingInstalledJson::class); + + (new MakeLocatorForInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/project-without-installed.json', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForProjectWithInvalidInstalledJson() : void + { + $this->expectException(FailedToParseJson::class); + + (new MakeLocatorForInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/project-with-invalid-installed-json', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForInvalidProjectDirectory() : void + { + $this->expectException(InvalidProjectDirectory::class); + + (new MakeLocatorForInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/non-existing', + BetterReflectionSingleton::instance()->astLocator() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonAndInstalledJsonTest.php b/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonAndInstalledJsonTest.php new file mode 100644 index 000000000..443ad1fd4 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonAndInstalledJsonTest.php @@ -0,0 +1,239 @@ +__invoke($projectDirectory, BetterReflectionSingleton::instance()->astLocator()) + ); + } + + /** + * @return array> + * + * @psalm-return array + */ + public function expectedLocators() : array + { + $astLocator = BetterReflectionSingleton::instance()->astLocator(); + + $projectA = realpath(__DIR__ . '/../../../../Assets/ComposerLocators/project-a'); + $projectWithPsrCollisions = realpath(__DIR__ . '/../../../../Assets/ComposerLocators/project-with-psr-collisions'); + $projectALocator = new AggregateSourceLocator([ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings([ + 'ProjectA\\' => [ + $projectA . '/src/root_PSR-4_Sources', + ], + 'ProjectA\\B\\' => [ + $projectA . '/src/root_PSR-4_Sources', + ], + 'A\\B\\' => [ + $projectA . '/vendor/a/b/src/ab_PSR-4_Sources', + ], + 'C\\D\\' => [ + $projectA . '/vendor/a/b/src/ab_PSR-4_Sources', + ], + 'E\\F\\' => [ + $projectA . '/vendor/e/f/src/ef_PSR-4_Sources', + ], + ]), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings([ + 'ProjectA_A_' => [ + $projectA . '/src/root_PSR-0_Sources', + ], + 'ProjectA_B_' => [ + $projectA . '/src/root_PSR-0_Sources', + ], + 'A_B_' => [ + $projectA . '/vendor/a/b/src/ab_PSR-0_Sources', + ], + 'C_D_' => [ + $projectA . '/vendor/a/b/src/ab_PSR-0_Sources', + ], + 'E_F_' => [ + $projectA . '/vendor/e/f/src/ef_PSR-0_Sources', + ], + ]), + $astLocator + ), + new DirectoriesSourceLocator( + [ + $projectA . '/src/root_ClassmapDir', + $projectA . '/vendor/a/b/src/ab_ClassmapDir', + $projectA . '/vendor/e/f/src/ef_ClassmapDir', + ], + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/src/root_ClassmapFile', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/a/b/src/ab_ClassmapFile', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/e/f/src/ef_ClassmapFile', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/src/root_File1.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/src/root_File2.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/a/b/src/ab_File1.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/a/b/src/ab_File2.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/e/f/src/ef_File1.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/vendor/e/f/src/ef_File2.php', + $astLocator + ), + ]); + + $expectedLocators = [ + [ + $projectA, + $projectALocator, + ], + [ + $projectWithPsrCollisions, + new AggregateSourceLocator([ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings([ + 'A\\' => [ + $projectWithPsrCollisions . '/src/root_PSR-4_Sources', + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-4_Sources', + $projectWithPsrCollisions . '/vendor/e/f/src/ef_PSR-4_Sources', + ], + 'B\\' => [ + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-4_Sources', + ], + ]), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings([ + 'A_' => [ + $projectWithPsrCollisions . '/src/root_PSR-0_Sources', + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-0_Sources', + $projectWithPsrCollisions . '/vendor/e/f/src/ef_PSR-0_Sources', + ], + 'B_' => [ + $projectWithPsrCollisions . '/vendor/a/b/src/ab_PSR-0_Sources', + ], + ]), + $astLocator + ), + new DirectoriesSourceLocator([], $astLocator), + ]), + ], + [ + // Relative paths are turned into absolute paths too + __DIR__ . '/../../../../Assets/ComposerLocators/project-a', + $projectALocator, + ], + ]; + + return array_combine(array_column($expectedLocators, 0), $expectedLocators); + } + + public function testWillFailToProduceLocatorForProjectWithoutComposerJson() : void + { + $this->expectException(MissingComposerJson::class); + + (new MakeLocatorForComposerJsonAndInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/empty-project', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForProjectWithoutInstalledJson() : void + { + $this->expectException(MissingInstalledJson::class); + + (new MakeLocatorForComposerJsonAndInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/project-without-installed.json', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForProjectWithInvalidComposerJson() : void + { + $this->expectException(FailedToParseJson::class); + + (new MakeLocatorForComposerJsonAndInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/project-with-invalid-composer-json', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForProjectWithInvalidInstalledJson() : void + { + $this->expectException(FailedToParseJson::class); + + (new MakeLocatorForComposerJsonAndInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/project-with-invalid-installed-json', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForInvalidProjectDirectory() : void + { + $this->expectException(InvalidProjectDirectory::class); + + (new MakeLocatorForComposerJsonAndInstalledJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/non-existing', + BetterReflectionSingleton::instance()->astLocator() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonTest.php b/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonTest.php new file mode 100644 index 000000000..3f50c32c2 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Factory/MakeLocatorForComposerJsonTest.php @@ -0,0 +1,162 @@ +__invoke($projectDirectory, BetterReflectionSingleton::instance()->astLocator()) + ); + } + + /** + * @return array> + * + * @psalm-return array + */ + public function expectedLocators() : array + { + $astLocator = BetterReflectionSingleton::instance()->astLocator(); + + $projectA = realpath(__DIR__ . '/../../../../Assets/ComposerLocators/project-a'); + $projectWithPsrCollisions = realpath(__DIR__ . '/../../../../Assets/ComposerLocators/project-with-psr-collisions'); + $projectALocator = new AggregateSourceLocator([ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings([ + 'ProjectA\\' => [ + $projectA . '/src/root_PSR-4_Sources', + ], + 'ProjectA\\B\\' => [ + $projectA . '/src/root_PSR-4_Sources', + ], + ]), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings([ + 'ProjectA_A_' => [ + $projectA . '/src/root_PSR-0_Sources', + ], + 'ProjectA_B_' => [ + $projectA . '/src/root_PSR-0_Sources', + ], + ]), + $astLocator + ), + new DirectoriesSourceLocator( + [ + $projectA . '/src/root_ClassmapDir', + ], + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/src/root_ClassmapFile', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/src/root_File1.php', + $astLocator + ), + new SingleFileSourceLocator( + $projectA . '/src/root_File2.php', + $astLocator + ), + ]); + + $expectedLocators = [ + [ + $projectA, + $projectALocator, + ], + [ + $projectWithPsrCollisions, + new AggregateSourceLocator([ + new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings([ + 'A\\' => [ + $projectWithPsrCollisions . '/src/root_PSR-4_Sources', + ], + ]), + $astLocator + ), + new PsrAutoloaderLocator( + Psr0Mapping::fromArrayMappings([ + 'A_' => [ + $projectWithPsrCollisions . '/src/root_PSR-0_Sources', + ], + ]), + $astLocator + ), + new DirectoriesSourceLocator([], $astLocator), + ]), + ], + [ + // Relative paths are turned into absolute paths too + __DIR__ . '/../../../../Assets/ComposerLocators/project-a', + $projectALocator, + ], + ]; + + return array_combine(array_column($expectedLocators, 0), $expectedLocators); + } + + public function testWillFailToProduceLocatorForProjectWithoutComposerJson() : void + { + $this->expectException(MissingComposerJson::class); + + (new MakeLocatorForComposerJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/empty-project', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForProjectWithInvalidComposerJson() : void + { + $this->expectException(FailedToParseJson::class); + + (new MakeLocatorForComposerJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/project-with-invalid-composer-json', + BetterReflectionSingleton::instance()->astLocator() + ); + } + + public function testWillFailToProduceLocatorForInvalidProjectDirectory() : void + { + $this->expectException(InvalidProjectDirectory::class); + + (new MakeLocatorForComposerJson()) + ->__invoke( + __DIR__ . '/../../../../Assets/ComposerLocators/non-existing', + BetterReflectionSingleton::instance()->astLocator() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Psr/Excetion/InvalidPrefixMappingTest.php b/test/unit/SourceLocator/Type/Composer/Psr/Excetion/InvalidPrefixMappingTest.php new file mode 100644 index 000000000..66441c3fe --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Psr/Excetion/InvalidPrefixMappingTest.php @@ -0,0 +1,41 @@ +getMessage() + ); + } + + public function testPrefixMappingIsNotADirectory() : void + { + self::assertSame( + 'Provided path "A\" for prefix "a/b" is not a directory', + InvalidPrefixMapping::prefixMappingIsNotADirectory('A\\', 'a/b') + ->getMessage() + ); + } + + public function testEmptyPrefixMappingGiven() : void + { + self::assertSame( + 'An invalid empty list of paths was provided for PSR mapping prefix "A\"', + InvalidPrefixMapping::emptyPrefixMappingGiven('A\\') + ->getMessage() + ); + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Psr/Psr0MappingTest.php b/test/unit/SourceLocator/Type/Composer/Psr/Psr0MappingTest.php new file mode 100644 index 000000000..35ae93eae --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Psr/Psr0MappingTest.php @@ -0,0 +1,159 @@ +>> $mappings + * @param string[] $expectedDirectories + * + * @dataProvider mappings + */ + public function testExpectedDirectories(array $mappings, array $expectedDirectories) : void + { + self::assertEquals($expectedDirectories, Psr0Mapping::fromArrayMappings($mappings)->directories()); + } + + /** + * @param array>> $mappings + * + * @dataProvider mappings + */ + public function testIdempotentConstructor(array $mappings) : void + { + self::assertEquals(Psr0Mapping::fromArrayMappings($mappings), Psr0Mapping::fromArrayMappings($mappings)); + } + + /** @return array>>> */ + public function mappings() : array + { + return [ + 'one directory, one prefix' => [ + ['foo' => [__DIR__]], + [__DIR__], + ], + 'two directories, one prefix' => [ + ['foo' => [__DIR__, __DIR__ . '/../..']], + [__DIR__, __DIR__ . '/../..'], + ], + 'two directories, one duplicate, one prefix' => [ + ['foo' => [__DIR__, __DIR__, __DIR__ . '/../..']], + [__DIR__, __DIR__ . '/../..'], + ], + 'two directories, two prefixes' => [ + [ + 'foo' => [__DIR__], + 'bar' => [__DIR__ . '/../..'], + ], + [__DIR__, __DIR__ . '/../..'], + ], + 'trailing slash in directory is trimmed' => [ + ['foo' => [__DIR__ . '/']], + [__DIR__], + ], + ]; + } + + /** + * @param array>> $mappings + * @param string[] $expectedFiles + * + * @dataProvider classLookupMappings + */ + public function testClassLookups(array $mappings, Identifier $identifier, array $expectedFiles) : void + { + self::assertEquals( + $expectedFiles, + Psr0Mapping::fromArrayMappings($mappings)->resolvePossibleFilePaths($identifier) + ); + } + + /** @return array>>> */ + public function classLookupMappings() : array + { + return [ + 'empty mappings, no match' => [ + [], + new Identifier('Foo', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [], + ], + 'one mapping, no match for function identifier' => [ + ['Foo\\' => [__DIR__]], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION)), + [], + ], + 'one mapping, match' => [ + ['Foo\\' => [__DIR__]], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/Foo/Bar.php'], + ], + 'one mapping, no match with underscore prefix and namespaced class' => [ + ['Foo_' => [__DIR__]], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [], + ], + 'one mapping, match with underscore replacement' => [ + ['Foo_' => [__DIR__]], + new Identifier('Foo_Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/Foo/Bar.php'], + ], + 'trailing and leading slash in mapping is trimmed' => [ + ['Foo' => [__DIR__ . '/']], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/Foo/Bar.php'], + ], + 'one mapping, match if class === prefix' => [ + ['Foo' => [__DIR__]], + new Identifier('Foo', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/Foo.php'], + ], + 'multiple mappings, match when class !== prefix' => [ + [ + 'Foo_Baz' => [__DIR__ . '/../..'], + 'Foo' => [__DIR__ . '/..'], + ], + new Identifier('Foo_Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/../Foo/Bar.php'], + ], + ]; + } + + /** + * @param array>> $invalidMappings + * + * @dataProvider invalidMappings + */ + public function testRejectsInvalidMappings(array $invalidMappings) : void + { + $this->expectException(InvalidPrefixMapping::class); + + Psr0Mapping::fromArrayMappings($invalidMappings); + } + + /** @return array>>> */ + public function invalidMappings() : array + { + return [ + 'array contains empty prefixes' => [['' => 'bar']], + 'array contains empty paths' => [['foo' => ['']]], + 'array contains empty path list' => [['foo' => []]], + 'array contains path pointing to a file' => [['foo' => [tempnam(sys_get_temp_dir(), 'non_existing')]]], + 'array contains path pointing to a non-existing directory' => [['foo' => [sys_get_temp_dir() . '/' . uniqid('not_existing', true)]]], + ]; + } +} diff --git a/test/unit/SourceLocator/Type/Composer/Psr/Psr4MappingTest.php b/test/unit/SourceLocator/Type/Composer/Psr/Psr4MappingTest.php new file mode 100644 index 000000000..4975a41bb --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/Psr/Psr4MappingTest.php @@ -0,0 +1,149 @@ +>> $mappings + * @param string[] $expectedDirectories + * + * @dataProvider mappings + */ + public function testExpectedDirectories(array $mappings, array $expectedDirectories) : void + { + self::assertEquals($expectedDirectories, Psr4Mapping::fromArrayMappings($mappings)->directories()); + } + + /** + * @param array>> $mappings + * + * @dataProvider mappings + */ + public function testIdempotentConstructor(array $mappings) : void + { + self::assertEquals(Psr4Mapping::fromArrayMappings($mappings), Psr4Mapping::fromArrayMappings($mappings)); + } + + /** @return array>|array>> */ + public function mappings() : array + { + return [ + 'one directory, one prefix' => [ + ['foo' => [__DIR__]], + [__DIR__], + ], + 'two directories, one prefix' => [ + ['foo' => [__DIR__, __DIR__ . '/../..']], + [__DIR__, __DIR__ . '/../..'], + ], + 'two directories, one duplicate, one prefix' => [ + ['foo' => [__DIR__, __DIR__, __DIR__ . '/../..']], + [__DIR__, __DIR__ . '/../..'], + ], + 'two directories, two prefixes' => [ + [ + 'foo' => [__DIR__], + 'bar' => [__DIR__ . '/../..'], + ], + [__DIR__, __DIR__ . '/../..'], + ], + 'trailing slash in directory is trimmed' => [ + ['foo' => [__DIR__ . '/']], + [__DIR__], + ], + ]; + } + + /** + * @param array>> $mappings + * @param string[] $expectedFiles + * + * @dataProvider classLookupMappings + */ + public function testClassLookups(array $mappings, Identifier $identifier, array $expectedFiles) : void + { + self::assertEquals( + $expectedFiles, + Psr4Mapping::fromArrayMappings($mappings)->resolvePossibleFilePaths($identifier) + ); + } + + /** @return array>|array>> */ + public function classLookupMappings() : array + { + return [ + 'empty mappings, no match' => [ + [], + new Identifier('Foo', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [], + ], + 'one mapping, no match for function identifier' => [ + ['Foo\\' => [__DIR__]], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION)), + [], + ], + 'one mapping, match' => [ + ['Foo\\' => [__DIR__]], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/Bar.php'], + ], + 'trailing and leading slash in mapping is trimmed' => [ + ['Foo' => [__DIR__ . '/']], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/Bar.php'], + ], + 'one mapping, no match if class === prefix' => [ + ['Foo' => [__DIR__]], + new Identifier('Foo', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [], + ], + 'multiple mappings, match when class !== prefix' => [ + [ + 'Foo\\Bar' => [__DIR__ . '/../..'], + 'Foo' => [__DIR__ . '/..'], + ], + new Identifier('Foo\\Bar', new IdentifierType(IdentifierType::IDENTIFIER_CLASS)), + [__DIR__ . '/../Bar.php'], + ], + ]; + } + + /** + * @param array>> $invalidMappings + * + * @dataProvider invalidMappings + */ + public function testRejectsInvalidMappings(array $invalidMappings) : void + { + $this->expectException(InvalidPrefixMapping::class); + + Psr4Mapping::fromArrayMappings($invalidMappings); + } + + /** @return array>>> */ + public function invalidMappings() : array + { + return [ + 'array contains empty prefixes' => [['' => 'bar']], + 'array contains empty paths' => [['foo' => ['']]], + 'array contains empty path list' => [['foo' => []]], + 'array contains path pointing to a file' => [['foo' => [tempnam(sys_get_temp_dir(), 'non_existing')]]], + 'array contains path pointing to a non-existing directory' => [['foo' => [sys_get_temp_dir() . '/' . uniqid('not_existing', true)]]], + ]; + } +} diff --git a/test/unit/SourceLocator/Type/Composer/PsrAutoloaderLocatorTest.php b/test/unit/SourceLocator/Type/Composer/PsrAutoloaderLocatorTest.php new file mode 100644 index 000000000..0f17492d9 --- /dev/null +++ b/test/unit/SourceLocator/Type/Composer/PsrAutoloaderLocatorTest.php @@ -0,0 +1,167 @@ +astLocator(); + + $this->psrMapping = $this->createMock(PsrAutoloaderMapping::class); + $this->psrLocator = new PsrAutoloaderLocator( + $this->psrMapping, + BetterReflectionSingleton::instance() + ->astLocator() + ); + $this->reflector = new ClassReflector($this->psrLocator); + $this + ->psrMapping + ->method('directories') + ->willReturn([ + __DIR__ . '/../../../Assets/DirectoryScannerAssets', + __DIR__ . '/../../../Assets/DirectoryScannerAssetsFoo', + ]); + + $this + ->psrMapping + ->method('resolvePossibleFilePaths') + ->willReturnCallback(static function (Identifier $identifier) : array { + if ($identifier->getName() === Foo::class) { + return [__DIR__ . '/../../../Assets/DirectoryScannerAssets/Foo.php']; + } + + if ($identifier->getName() === (Foo::class . 'potato')) { + return [__DIR__ . '/../../../Assets/DirectoryScannerAssets/Foopotato.php']; + } + + if ($identifier->getName() === 'Roave\\BetterReflectionTest\\Assets\\DirectoryScannerAssets\\Bar\\Empty') { + return [__DIR__ . '/../../../Assets/DirectoryScannerAssets/Bar/Empty.php']; + } + + return []; + }); + } + + public function testWillLocateExistingFileWithMatchingClass() : void + { + $located = $this->psrLocator->locateIdentifier( + $this->reflector, + new Identifier( + Foo::class, + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ) + ); + + self::assertNotNull($located); + self::assertSame(Foo::class, $located->getName()); + } + + public function testWillNotLocateNonExistingFileWithMatchingPsr4Class() : void + { + self::assertNull($this->psrLocator->locateIdentifier( + $this->reflector, + new Identifier( + Foo::class . 'potato', + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ) + )); + } + + public function testWillNotLocateExistingFileWithMatchingPsr4ClassAndNoContents() : void + { + self::assertNull($this->psrLocator->locateIdentifier( + $this->reflector, + new Identifier( + 'Roave\\BetterReflectionTest\\Assets\\DirectoryScannerAssets\\Bar\\Empty', + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ) + )); + } + + public function testWillNotLocateClassNotMatchingPsr4Mappings() : void + { + self::assertNull($this->psrLocator->locateIdentifier( + $this->reflector, + new Identifier( + 'Blah', + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ) + )); + } + + public function testWillLocateAllClassesInMappedPsr4Paths() : void + { + $astLocator = BetterReflectionSingleton::instance() + ->astLocator(); + + $locator = new PsrAutoloaderLocator( + Psr4Mapping::fromArrayMappings([ + 'Roave\\BetterReflectionTest\\Assets\\DirectoryScannerAssets\\' => [ + __DIR__ . '/../../../Assets/DirectoryScannerAssets', + ], + 'Roave\\BetterReflectionTest\\Assets\\DirectoryScannerAssetsFoo\\' => [ + __DIR__ . '/../../../Assets/DirectoryScannerAssetsFoo', + ], + ]), + $astLocator + ); + + $expected = [ + FooBar::class, + Foo::class, + FooBar1::class, + Foo1::class, + ]; + + $actual = array_map( + static function (Reflection $reflection) : string { + return $reflection->getName(); + }, + $locator->locateIdentifiersByType( + new ClassReflector($locator), + new IdentifierType(IdentifierType::IDENTIFIER_CLASS) + ) + ); + + // Sorting may depend on filesystem here + sort($expected); + sort($actual); + + self::assertSame($expected, $actual); + } +}