From d7636669243fd0763d35a3c9cd31ae3236ea2191 Mon Sep 17 00:00:00 2001 From: przepompownia Date: Wed, 6 Apr 2022 03:04:35 +0200 Subject: [PATCH 1/8] Add test that fails --- .../Composer/ComposerClassToFileTest.php | 11 +++++++++++ .../Integration/Composer/ComposerTestCase.php | 3 ++- .../composers/psr4-classmap-authoritative.json | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Composer/composers/psr4-classmap-authoritative.json diff --git a/tests/Integration/Composer/ComposerClassToFileTest.php b/tests/Integration/Composer/ComposerClassToFileTest.php index 004dc20f..f7534d43 100644 --- a/tests/Integration/Composer/ComposerClassToFileTest.php +++ b/tests/Integration/Composer/ComposerClassToFileTest.php @@ -42,6 +42,17 @@ public function testPsr4(): void $this->assertClassNameToFilePath('Acme\\Test\\Foo\\Class', ['psr4/Foo/Class.php']); } + /** + * @testdox PSR-4 class name to a file path. + */ + public function testPsr4WithClassmapAuthoritative(): void + { + $this->loadExample('psr4-classmap-authoritative.json'); + $this->getClassLoader()->addClassMap(['Acme\\Test\\Foo\\Bar' => $this->workspacePath() . '/psr4/Foo/Bar.php']); + $this->assertClassNameToFilePath('Acme\\Test\\Foo\\Bar2', ['psr4/Foo/Bar2.php']); + $this->assertClassNameToFilePath('Acme\\Test\\Foo\\Class2', ['psr4/Foo/Class2.php']); + } + /** * @testdox PSR-4 class in dev namespace */ diff --git a/tests/Integration/Composer/ComposerTestCase.php b/tests/Integration/Composer/ComposerTestCase.php index 0bebea57..a9e9e7dc 100644 --- a/tests/Integration/Composer/ComposerTestCase.php +++ b/tests/Integration/Composer/ComposerTestCase.php @@ -2,6 +2,7 @@ namespace Phpactor\ClassFileConverter\Tests\Integration\Composer; +use Composer\Autoload\ClassLoader; use Phpactor\ClassFileConverter\Tests\Integration\IntegrationTestCase; use Symfony\Component\Filesystem\Filesystem; @@ -26,7 +27,7 @@ protected function loadExample($composerFile): void exec('composer install 2> /dev/null'); } - protected function getClassLoader() + protected function getClassLoader(): ClassLoader { return require $this->workspacePath().'/vendor/autoload.php'; } diff --git a/tests/Integration/Composer/composers/psr4-classmap-authoritative.json b/tests/Integration/Composer/composers/psr4-classmap-authoritative.json new file mode 100644 index 00000000..f4554d88 --- /dev/null +++ b/tests/Integration/Composer/composers/psr4-classmap-authoritative.json @@ -0,0 +1,18 @@ +{ + "name": "dantleech/basic", + "authors": [ + { + "name": "dantleech", + "email": "dan.t.leech@gmail.com" + } + ], + "require": {}, + "config": { + "classmap-authoritative": true + }, + "autoload": { + "psr-4": { + "Acme\\Test\\": "psr4/" + } + } +} From dc17cac342811a1df7861d37b716f743d2989193 Mon Sep 17 00:00:00 2001 From: przepompownia Date: Wed, 6 Apr 2022 03:09:02 +0200 Subject: [PATCH 2/8] Fix for PSR-4 --- lib/Domain/ClassName.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/Domain/ClassName.php b/lib/Domain/ClassName.php index 485e10c1..0280d0b7 100644 --- a/lib/Domain/ClassName.php +++ b/lib/Domain/ClassName.php @@ -37,8 +37,20 @@ public function name() return substr($this->fullyQualifiedName, $pos + 1); } - public function beginsWith($prefix) + public function beginsWith(string $prefix, string $separator = '\\'): bool { - return 0 === strpos($this->fullyQualifiedName, $prefix); + if ($prefix === $this->fullyQualifiedName) { + return true; + } + + if (0 !== strpos($this->fullyQualifiedName, $prefix)) { + return false; + } + + if (mb_substr($prefix, -1, 1) === $separator) { + return true; + } + + return mb_substr($this->fullyQualifiedName, mb_strlen($prefix), 1) === $separator; } } From 292e0a7fbebb2c8a34d64266fc472f7dcc2396d4 Mon Sep 17 00:00:00 2001 From: przepompownia Date: Wed, 6 Apr 2022 10:44:15 +0200 Subject: [PATCH 3/8] Update phpstan-baseline --- phpstan-baseline.neon | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3952583e..191159d6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -95,16 +95,6 @@ parameters: count: 1 path: lib/Domain/ChainFileToClass.php - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:beginsWith\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:beginsWith\\(\\) has parameter \\$prefix with no typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:fromString\\(\\) has no return typehint specified\\.$#" count: 1 From b2edd41e23c990df1a474668a13f2bc0a6dadf7c Mon Sep 17 00:00:00 2001 From: przepompownia Date: Wed, 6 Apr 2022 13:49:53 +0200 Subject: [PATCH 4/8] Distinguish separators --- lib/Adapter/Composer/ComposerClassToFile.php | 24 +++++++++++++------ lib/Adapter/Composer/Psr0NameInflector.php | 6 +++-- lib/Adapter/Composer/Psr4NameInflector.php | 6 +++-- .../Composer/ComposerClassToFileTest.php | 1 - 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/lib/Adapter/Composer/ComposerClassToFile.php b/lib/Adapter/Composer/ComposerClassToFile.php index ded37869..34d348e4 100644 --- a/lib/Adapter/Composer/ComposerClassToFile.php +++ b/lib/Adapter/Composer/ComposerClassToFile.php @@ -31,8 +31,8 @@ public function classToFileCandidates(ClassName $className): FilePathCandidates { $candidates = []; foreach ($this->getStrategies() as $strategy) { - list($prefixes, $inflector) = $strategy; - $this->resolveFile($candidates, $prefixes, $inflector, $className); + list($prefixes, $inflector, $separator) = $strategy; + $this->resolveFile($candidates, $prefixes, $inflector, $className, $separator); } // order with the longest prefixes first @@ -54,30 +54,40 @@ private function getStrategies(): array [ $this->classLoader->getPrefixesPsr4(), new Psr4NameInflector(), + Psr4NameInflector::SEPARATOR, ], [ $this->classLoader->getPrefixes(), new Psr0NameInflector(), + Psr0NameInflector::SEPARATOR, ], [ $this->classLoader->getClassMap(), new ClassmapNameInflector(), + Psr4NameInflector::SEPARATOR, ], [ $this->classLoader->getFallbackDirs(), new Psr0NameInflector(), + Psr0NameInflector::SEPARATOR, ], [ $this->classLoader->getFallbackDirsPsr4(), // PSR0 name inflector works here as there is no prefix new Psr0NameInflector(), + Psr0NameInflector::SEPARATOR, ], ]; } - private function resolveFile(&$candidates, array $prefixes, NameInflector $inflector, ClassName $className): void - { - $fileCandidates = $this->getFileCandidates($className, $prefixes); + private function resolveFile( + &$candidates, + array $prefixes, + NameInflector $inflector, + ClassName $className, + string $separator, + ): void { + $fileCandidates = $this->getFileCandidates($className, $prefixes, $separator); foreach ($fileCandidates as $prefix => $files) { $prefixCandidates = []; @@ -93,7 +103,7 @@ private function resolveFile(&$candidates, array $prefixes, NameInflector $infle } } - private function getFileCandidates(ClassName $className, array $prefixes) + private function getFileCandidates(ClassName $className, array $prefixes, string $separator) { $candidates = []; @@ -102,7 +112,7 @@ private function getFileCandidates(ClassName $className, array $prefixes) $prefix = ''; } - if ($prefix && false === $className->beginsWith($prefix)) { + if ($prefix && false === $className->beginsWith($prefix, $separator)) { continue; } diff --git a/lib/Adapter/Composer/Psr0NameInflector.php b/lib/Adapter/Composer/Psr0NameInflector.php index bf3669ba..908aaecb 100644 --- a/lib/Adapter/Composer/Psr0NameInflector.php +++ b/lib/Adapter/Composer/Psr0NameInflector.php @@ -7,10 +7,12 @@ final class Psr0NameInflector implements NameInflector { + public const SEPARATOR = '_'; + public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath { - if (substr($prefix, -1) === '_' && $className->beginsWith($prefix)) { - $elements = explode('_', $className); + if (substr($prefix, -1) === self::SEPARATOR && $className->beginsWith($prefix, self::SEPARATOR)) { + $elements = explode(self::SEPARATOR, $className); $className = implode('\\', $elements); } diff --git a/lib/Adapter/Composer/Psr4NameInflector.php b/lib/Adapter/Composer/Psr4NameInflector.php index f2d92ff7..e9e093e5 100644 --- a/lib/Adapter/Composer/Psr4NameInflector.php +++ b/lib/Adapter/Composer/Psr4NameInflector.php @@ -7,9 +7,11 @@ final class Psr4NameInflector implements NameInflector { + public const SEPARATOR = '\\'; + public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath { - $relativePath = str_replace('\\', '/', substr($className, strlen($prefix))).'.php'; + $relativePath = str_replace(self::SEPARATOR, '/', substr($className, strlen($prefix))).'.php'; return FilePath::fromParts([$mappedPath, $relativePath]); } @@ -22,7 +24,7 @@ public function inflectToClassName(FilePath $filePath, string $pathPrefix, strin } $className = substr($filePath, strlen($pathPrefix) + 1); - $className = str_replace('/', '\\', $className); + $className = str_replace('/', self::SEPARATOR, $className); $className = $classPrefix.$className; $className = preg_replace('{\.(.+)$}', '', $className); diff --git a/tests/Integration/Composer/ComposerClassToFileTest.php b/tests/Integration/Composer/ComposerClassToFileTest.php index f7534d43..7f381800 100644 --- a/tests/Integration/Composer/ComposerClassToFileTest.php +++ b/tests/Integration/Composer/ComposerClassToFileTest.php @@ -129,7 +129,6 @@ public function testPsr0ShortNamePrefix2(): void $this->assertClassNameToFilePath('Twig_Tests_Extension', [ 'psr0/twig/Twig/Tests/Extension.php' ]); } - /** * @testdox PSR-4 fallback */ From 6474d4434abe66a3b267407cbbc8df2c9704936c Mon Sep 17 00:00:00 2001 From: przepompownia Date: Thu, 7 Apr 2022 14:16:59 +0200 Subject: [PATCH 5/8] Allow using also default namespace separator for PSR0 --- lib/Adapter/Composer/ComposerClassToFile.php | 10 ++++---- lib/Adapter/Composer/Psr0NameInflector.php | 8 ++++--- lib/Adapter/Composer/Psr4NameInflector.php | 6 ++--- lib/Domain/ClassName.php | 24 ++++++++++++++++---- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/lib/Adapter/Composer/ComposerClassToFile.php b/lib/Adapter/Composer/ComposerClassToFile.php index 34d348e4..ce505c38 100644 --- a/lib/Adapter/Composer/ComposerClassToFile.php +++ b/lib/Adapter/Composer/ComposerClassToFile.php @@ -54,28 +54,28 @@ private function getStrategies(): array [ $this->classLoader->getPrefixesPsr4(), new Psr4NameInflector(), - Psr4NameInflector::SEPARATOR, + Psr4NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getPrefixes(), new Psr0NameInflector(), - Psr0NameInflector::SEPARATOR, + Psr0NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getClassMap(), new ClassmapNameInflector(), - Psr4NameInflector::SEPARATOR, + Psr4NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getFallbackDirs(), new Psr0NameInflector(), - Psr0NameInflector::SEPARATOR, + Psr0NameInflector::NAMESPACE_SEPARATOR, ], [ $this->classLoader->getFallbackDirsPsr4(), // PSR0 name inflector works here as there is no prefix new Psr0NameInflector(), - Psr0NameInflector::SEPARATOR, + Psr0NameInflector::NAMESPACE_SEPARATOR, ], ]; } diff --git a/lib/Adapter/Composer/Psr0NameInflector.php b/lib/Adapter/Composer/Psr0NameInflector.php index 908aaecb..e6162ebd 100644 --- a/lib/Adapter/Composer/Psr0NameInflector.php +++ b/lib/Adapter/Composer/Psr0NameInflector.php @@ -7,12 +7,14 @@ final class Psr0NameInflector implements NameInflector { - public const SEPARATOR = '_'; + public const NAMESPACE_SEPARATOR = '_'; public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath { - if (substr($prefix, -1) === self::SEPARATOR && $className->beginsWith($prefix, self::SEPARATOR)) { - $elements = explode(self::SEPARATOR, $className); + if ( + in_array(substr($prefix, -1), [self::NAMESPACE_SEPARATOR, ClassName::DEFAULT_NAMESPACE_SEPARATOR]) + && $className->beginsWith($prefix, self::NAMESPACE_SEPARATOR)) { + $elements = explode(self::NAMESPACE_SEPARATOR, $className); $className = implode('\\', $elements); } diff --git a/lib/Adapter/Composer/Psr4NameInflector.php b/lib/Adapter/Composer/Psr4NameInflector.php index e9e093e5..02721881 100644 --- a/lib/Adapter/Composer/Psr4NameInflector.php +++ b/lib/Adapter/Composer/Psr4NameInflector.php @@ -7,11 +7,11 @@ final class Psr4NameInflector implements NameInflector { - public const SEPARATOR = '\\'; + public const NAMESPACE_SEPARATOR = ClassName::DEFAULT_NAMESPACE_SEPARATOR; public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath { - $relativePath = str_replace(self::SEPARATOR, '/', substr($className, strlen($prefix))).'.php'; + $relativePath = str_replace(self::NAMESPACE_SEPARATOR, '/', substr($className, strlen($prefix))).'.php'; return FilePath::fromParts([$mappedPath, $relativePath]); } @@ -24,7 +24,7 @@ public function inflectToClassName(FilePath $filePath, string $pathPrefix, strin } $className = substr($filePath, strlen($pathPrefix) + 1); - $className = str_replace('/', self::SEPARATOR, $className); + $className = str_replace('/', self::NAMESPACE_SEPARATOR, $className); $className = $classPrefix.$className; $className = preg_replace('{\.(.+)$}', '', $className); diff --git a/lib/Domain/ClassName.php b/lib/Domain/ClassName.php index 0280d0b7..12d405ab 100644 --- a/lib/Domain/ClassName.php +++ b/lib/Domain/ClassName.php @@ -2,8 +2,12 @@ namespace Phpactor\ClassFileConverter\Domain; +use function in_array; + final class ClassName { + public const DEFAULT_NAMESPACE_SEPARATOR = '\\'; + private $fullyQualifiedName; private function __construct() @@ -25,19 +29,22 @@ public static function fromString($string) public function namespace() { - return substr($this->fullyQualifiedName, 0, (int) strrpos($this->fullyQualifiedName, '\\')); + return substr($this->fullyQualifiedName, 0, (int) strrpos( + $this->fullyQualifiedName, + self::DEFAULT_NAMESPACE_SEPARATOR, + )); } public function name() { - $pos = strrpos($this->fullyQualifiedName, '\\'); + $pos = strrpos($this->fullyQualifiedName, self::DEFAULT_NAMESPACE_SEPARATOR); if (false === $pos) { return $this->fullyQualifiedName; } return substr($this->fullyQualifiedName, $pos + 1); } - public function beginsWith(string $prefix, string $separator = '\\'): bool + public function beginsWith(string $prefix, string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR): bool { if ($prefix === $this->fullyQualifiedName) { return true; @@ -47,10 +54,17 @@ public function beginsWith(string $prefix, string $separator = '\\'): bool return false; } - if (mb_substr($prefix, -1, 1) === $separator) { + if ($this->isNamespaceSeparator(mb_substr($prefix, -1, 1), $additionalNseparator)) { return true; } - return mb_substr($this->fullyQualifiedName, mb_strlen($prefix), 1) === $separator; + return mb_substr($this->fullyQualifiedName, mb_strlen($prefix), 1) === $additionalNseparator; + } + + private function isNamespaceSeparator( + string $character, + string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR, + ): bool { + return in_array($character, [self::DEFAULT_NAMESPACE_SEPARATOR, $additionalNseparator], true); } } From cbb6ae6511b1bc3a2769ff03a07b537a8f367399 Mon Sep 17 00:00:00 2001 From: przepompownia Date: Thu, 7 Apr 2022 16:17:24 +0200 Subject: [PATCH 6/8] Drop support for PHP 7.3 --- .github/workflows/ci.yml | 5 ++--- composer.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b5084f4..6594e5d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: php-version: - - '7.3' + - '7.4' steps: - @@ -52,7 +52,7 @@ jobs: strategy: matrix: php-version: - - '7.3' + - '7.4' steps: - @@ -85,7 +85,6 @@ jobs: strategy: matrix: php-version: - - '7.3' - '7.4' - '8.0' diff --git a/composer.json b/composer.json index aaaa1357..4a542870 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,7 @@ } ], "require": { - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "psr/log": "^1.0", "webmozart/path-util": "^2.3" }, From 6df9bf467dd501c60f12c220e93de8033415f55b Mon Sep 17 00:00:00 2001 From: przepompownia Date: Thu, 7 Apr 2022 19:57:11 +0200 Subject: [PATCH 7/8] Trailing comma, property type --- lib/Adapter/Composer/ComposerClassToFile.php | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/Adapter/Composer/ComposerClassToFile.php b/lib/Adapter/Composer/ComposerClassToFile.php index ce505c38..96ca8333 100644 --- a/lib/Adapter/Composer/ComposerClassToFile.php +++ b/lib/Adapter/Composer/ComposerClassToFile.php @@ -11,15 +11,9 @@ class ComposerClassToFile implements ClassToFile { - /** - * @var ClassLoader - */ - private $classLoader; + private ClassLoader $classLoader; - /** - * @var LoggerInterface - */ - private $logger; + private LoggerInterface $logger; public function __construct(ClassLoader $classLoader, LoggerInterface $logger = null) { @@ -85,7 +79,7 @@ private function resolveFile( array $prefixes, NameInflector $inflector, ClassName $className, - string $separator, + string $separator ): void { $fileCandidates = $this->getFileCandidates($className, $prefixes, $separator); From de88012ac06210ab93f01b17bead780b1e636930 Mon Sep 17 00:00:00 2001 From: przepompownia Date: Thu, 7 Apr 2022 20:13:49 +0200 Subject: [PATCH 8/8] fixup! Trailing comma, property type --- lib/Domain/ClassName.php | 10 +++++----- phpstan-baseline.neon | 25 ------------------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/lib/Domain/ClassName.php b/lib/Domain/ClassName.php index 12d405ab..b1e37aee 100644 --- a/lib/Domain/ClassName.php +++ b/lib/Domain/ClassName.php @@ -8,7 +8,7 @@ final class ClassName { public const DEFAULT_NAMESPACE_SEPARATOR = '\\'; - private $fullyQualifiedName; + private string $fullyQualifiedName; private function __construct() { @@ -19,7 +19,7 @@ public function __toString() return $this->fullyQualifiedName; } - public static function fromString($string) + public static function fromString(string $string): self { $new = new self(); $new->fullyQualifiedName = $string; @@ -27,7 +27,7 @@ public static function fromString($string) return $new; } - public function namespace() + public function namespace(): string { return substr($this->fullyQualifiedName, 0, (int) strrpos( $this->fullyQualifiedName, @@ -35,7 +35,7 @@ public function namespace() )); } - public function name() + public function name(): string { $pos = strrpos($this->fullyQualifiedName, self::DEFAULT_NAMESPACE_SEPARATOR); if (false === $pos) { @@ -63,7 +63,7 @@ public function beginsWith(string $prefix, string $additionalNseparator = self:: private function isNamespaceSeparator( string $character, - string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR, + string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR ): bool { return in_array($character, [self::DEFAULT_NAMESPACE_SEPARATOR, $additionalNseparator], true); } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 191159d6..8c78cbce 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -95,31 +95,6 @@ parameters: count: 1 path: lib/Domain/ChainFileToClass.php - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:fromString\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:fromString\\(\\) has parameter \\$string with no typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:name\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:namespace\\(\\) has no return typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - - - message: "#^Property Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:\\$fullyQualifiedName has no typehint specified\\.$#" - count: 1 - path: lib/Domain/ClassName.php - - message: "#^Class Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassNameCandidates implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#" count: 1