Skip to content

Commit e9b7022

Browse files
Fix ClassName::beginsWith() (#26)
* Add test that fails * Fix for PSR-4 * Update phpstan-baseline * Distinguish separators * Allow using also default namespace separator for PSR0 * Drop support for PHP 7.3 * Trailing comma, property type * fixup! Trailing comma, property type
1 parent d65e4fd commit e9b7022

File tree

10 files changed

+97
-68
lines changed

10 files changed

+97
-68
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
strategy:
2020
matrix:
2121
php-version:
22-
- '7.3'
22+
- '7.4'
2323

2424
steps:
2525
-
@@ -52,7 +52,7 @@ jobs:
5252
strategy:
5353
matrix:
5454
php-version:
55-
- '7.3'
55+
- '7.4'
5656

5757
steps:
5858
-
@@ -85,7 +85,6 @@ jobs:
8585
strategy:
8686
matrix:
8787
php-version:
88-
- '7.3'
8988
- '7.4'
9089
- '8.0'
9190

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
}
1010
],
1111
"require": {
12-
"php": "^7.3 || ^8.0",
12+
"php": "^7.4 || ^8.0",
1313
"psr/log": "^1.0",
1414
"webmozart/path-util": "^2.3"
1515
},

lib/Adapter/Composer/ComposerClassToFile.php

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,9 @@
1111

1212
class ComposerClassToFile implements ClassToFile
1313
{
14-
/**
15-
* @var ClassLoader
16-
*/
17-
private $classLoader;
14+
private ClassLoader $classLoader;
1815

19-
/**
20-
* @var LoggerInterface
21-
*/
22-
private $logger;
16+
private LoggerInterface $logger;
2317

2418
public function __construct(ClassLoader $classLoader, LoggerInterface $logger = null)
2519
{
@@ -31,8 +25,8 @@ public function classToFileCandidates(ClassName $className): FilePathCandidates
3125
{
3226
$candidates = [];
3327
foreach ($this->getStrategies() as $strategy) {
34-
list($prefixes, $inflector) = $strategy;
35-
$this->resolveFile($candidates, $prefixes, $inflector, $className);
28+
list($prefixes, $inflector, $separator) = $strategy;
29+
$this->resolveFile($candidates, $prefixes, $inflector, $className, $separator);
3630
}
3731

3832
// order with the longest prefixes first
@@ -54,30 +48,40 @@ private function getStrategies(): array
5448
[
5549
$this->classLoader->getPrefixesPsr4(),
5650
new Psr4NameInflector(),
51+
Psr4NameInflector::NAMESPACE_SEPARATOR,
5752
],
5853
[
5954
$this->classLoader->getPrefixes(),
6055
new Psr0NameInflector(),
56+
Psr0NameInflector::NAMESPACE_SEPARATOR,
6157
],
6258
[
6359
$this->classLoader->getClassMap(),
6460
new ClassmapNameInflector(),
61+
Psr4NameInflector::NAMESPACE_SEPARATOR,
6562
],
6663
[
6764
$this->classLoader->getFallbackDirs(),
6865
new Psr0NameInflector(),
66+
Psr0NameInflector::NAMESPACE_SEPARATOR,
6967
],
7068
[
7169
$this->classLoader->getFallbackDirsPsr4(),
7270
// PSR0 name inflector works here as there is no prefix
7371
new Psr0NameInflector(),
72+
Psr0NameInflector::NAMESPACE_SEPARATOR,
7473
],
7574
];
7675
}
7776

78-
private function resolveFile(&$candidates, array $prefixes, NameInflector $inflector, ClassName $className): void
79-
{
80-
$fileCandidates = $this->getFileCandidates($className, $prefixes);
77+
private function resolveFile(
78+
&$candidates,
79+
array $prefixes,
80+
NameInflector $inflector,
81+
ClassName $className,
82+
string $separator
83+
): void {
84+
$fileCandidates = $this->getFileCandidates($className, $prefixes, $separator);
8185

8286
foreach ($fileCandidates as $prefix => $files) {
8387
$prefixCandidates = [];
@@ -93,7 +97,7 @@ private function resolveFile(&$candidates, array $prefixes, NameInflector $infle
9397
}
9498
}
9599

96-
private function getFileCandidates(ClassName $className, array $prefixes)
100+
private function getFileCandidates(ClassName $className, array $prefixes, string $separator)
97101
{
98102
$candidates = [];
99103

@@ -102,7 +106,7 @@ private function getFileCandidates(ClassName $className, array $prefixes)
102106
$prefix = '';
103107
}
104108

105-
if ($prefix && false === $className->beginsWith($prefix)) {
109+
if ($prefix && false === $className->beginsWith($prefix, $separator)) {
106110
continue;
107111
}
108112

lib/Adapter/Composer/Psr0NameInflector.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77

88
final class Psr0NameInflector implements NameInflector
99
{
10+
public const NAMESPACE_SEPARATOR = '_';
11+
1012
public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath
1113
{
12-
if (substr($prefix, -1) === '_' && $className->beginsWith($prefix)) {
13-
$elements = explode('_', $className);
14+
if (
15+
in_array(substr($prefix, -1), [self::NAMESPACE_SEPARATOR, ClassName::DEFAULT_NAMESPACE_SEPARATOR])
16+
&& $className->beginsWith($prefix, self::NAMESPACE_SEPARATOR)) {
17+
$elements = explode(self::NAMESPACE_SEPARATOR, $className);
1418
$className = implode('\\', $elements);
1519
}
1620

lib/Adapter/Composer/Psr4NameInflector.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77

88
final class Psr4NameInflector implements NameInflector
99
{
10+
public const NAMESPACE_SEPARATOR = ClassName::DEFAULT_NAMESPACE_SEPARATOR;
11+
1012
public function inflectToRelativePath(string $prefix, ClassName $className, string $mappedPath): FilePath
1113
{
12-
$relativePath = str_replace('\\', '/', substr($className, strlen($prefix))).'.php';
14+
$relativePath = str_replace(self::NAMESPACE_SEPARATOR, '/', substr($className, strlen($prefix))).'.php';
1315

1416
return FilePath::fromParts([$mappedPath, $relativePath]);
1517
}
@@ -22,7 +24,7 @@ public function inflectToClassName(FilePath $filePath, string $pathPrefix, strin
2224
}
2325

2426
$className = substr($filePath, strlen($pathPrefix) + 1);
25-
$className = str_replace('/', '\\', $className);
27+
$className = str_replace('/', self::NAMESPACE_SEPARATOR, $className);
2628
$className = $classPrefix.$className;
2729
$className = preg_replace('{\.(.+)$}', '', $className);
2830

lib/Domain/ClassName.php

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
namespace Phpactor\ClassFileConverter\Domain;
44

5+
use function in_array;
6+
57
final class ClassName
68
{
7-
private $fullyQualifiedName;
9+
public const DEFAULT_NAMESPACE_SEPARATOR = '\\';
10+
11+
private string $fullyQualifiedName;
812

913
private function __construct()
1014
{
@@ -15,30 +19,52 @@ public function __toString()
1519
return $this->fullyQualifiedName;
1620
}
1721

18-
public static function fromString($string)
22+
public static function fromString(string $string): self
1923
{
2024
$new = new self();
2125
$new->fullyQualifiedName = $string;
2226

2327
return $new;
2428
}
2529

26-
public function namespace()
30+
public function namespace(): string
2731
{
28-
return substr($this->fullyQualifiedName, 0, (int) strrpos($this->fullyQualifiedName, '\\'));
32+
return substr($this->fullyQualifiedName, 0, (int) strrpos(
33+
$this->fullyQualifiedName,
34+
self::DEFAULT_NAMESPACE_SEPARATOR,
35+
));
2936
}
3037

31-
public function name()
38+
public function name(): string
3239
{
33-
$pos = strrpos($this->fullyQualifiedName, '\\');
40+
$pos = strrpos($this->fullyQualifiedName, self::DEFAULT_NAMESPACE_SEPARATOR);
3441
if (false === $pos) {
3542
return $this->fullyQualifiedName;
3643
}
3744
return substr($this->fullyQualifiedName, $pos + 1);
3845
}
3946

40-
public function beginsWith($prefix)
47+
public function beginsWith(string $prefix, string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR): bool
4148
{
42-
return 0 === strpos($this->fullyQualifiedName, $prefix);
49+
if ($prefix === $this->fullyQualifiedName) {
50+
return true;
51+
}
52+
53+
if (0 !== strpos($this->fullyQualifiedName, $prefix)) {
54+
return false;
55+
}
56+
57+
if ($this->isNamespaceSeparator(mb_substr($prefix, -1, 1), $additionalNseparator)) {
58+
return true;
59+
}
60+
61+
return mb_substr($this->fullyQualifiedName, mb_strlen($prefix), 1) === $additionalNseparator;
62+
}
63+
64+
private function isNamespaceSeparator(
65+
string $character,
66+
string $additionalNseparator = self::DEFAULT_NAMESPACE_SEPARATOR
67+
): bool {
68+
return in_array($character, [self::DEFAULT_NAMESPACE_SEPARATOR, $additionalNseparator], true);
4369
}
4470
}

phpstan-baseline.neon

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -95,41 +95,6 @@ parameters:
9595
count: 1
9696
path: lib/Domain/ChainFileToClass.php
9797

98-
-
99-
message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:beginsWith\\(\\) has no return typehint specified\\.$#"
100-
count: 1
101-
path: lib/Domain/ClassName.php
102-
103-
-
104-
message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:beginsWith\\(\\) has parameter \\$prefix with no typehint specified\\.$#"
105-
count: 1
106-
path: lib/Domain/ClassName.php
107-
108-
-
109-
message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:fromString\\(\\) has no return typehint specified\\.$#"
110-
count: 1
111-
path: lib/Domain/ClassName.php
112-
113-
-
114-
message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:fromString\\(\\) has parameter \\$string with no typehint specified\\.$#"
115-
count: 1
116-
path: lib/Domain/ClassName.php
117-
118-
-
119-
message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:name\\(\\) has no return typehint specified\\.$#"
120-
count: 1
121-
path: lib/Domain/ClassName.php
122-
123-
-
124-
message: "#^Method Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:namespace\\(\\) has no return typehint specified\\.$#"
125-
count: 1
126-
path: lib/Domain/ClassName.php
127-
128-
-
129-
message: "#^Property Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassName\\:\\:\\$fullyQualifiedName has no typehint specified\\.$#"
130-
count: 1
131-
path: lib/Domain/ClassName.php
132-
13398
-
13499
message: "#^Class Phpactor\\\\ClassFileConverter\\\\Domain\\\\ClassNameCandidates implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#"
135100
count: 1

tests/Integration/Composer/ComposerClassToFileTest.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,17 @@ public function testPsr4(): void
4242
$this->assertClassNameToFilePath('Acme\\Test\\Foo\\Class', ['psr4/Foo/Class.php']);
4343
}
4444

45+
/**
46+
* @testdox PSR-4 class name to a file path.
47+
*/
48+
public function testPsr4WithClassmapAuthoritative(): void
49+
{
50+
$this->loadExample('psr4-classmap-authoritative.json');
51+
$this->getClassLoader()->addClassMap(['Acme\\Test\\Foo\\Bar' => $this->workspacePath() . '/psr4/Foo/Bar.php']);
52+
$this->assertClassNameToFilePath('Acme\\Test\\Foo\\Bar2', ['psr4/Foo/Bar2.php']);
53+
$this->assertClassNameToFilePath('Acme\\Test\\Foo\\Class2', ['psr4/Foo/Class2.php']);
54+
}
55+
4556
/**
4657
* @testdox PSR-4 class in dev namespace
4758
*/
@@ -118,7 +129,6 @@ public function testPsr0ShortNamePrefix2(): void
118129
$this->assertClassNameToFilePath('Twig_Tests_Extension', [ 'psr0/twig/Twig/Tests/Extension.php' ]);
119130
}
120131

121-
122132
/**
123133
* @testdox PSR-4 fallback
124134
*/

tests/Integration/Composer/ComposerTestCase.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Phpactor\ClassFileConverter\Tests\Integration\Composer;
44

5+
use Composer\Autoload\ClassLoader;
56
use Phpactor\ClassFileConverter\Tests\Integration\IntegrationTestCase;
67
use Symfony\Component\Filesystem\Filesystem;
78

@@ -26,7 +27,7 @@ protected function loadExample($composerFile): void
2627
exec('composer install 2> /dev/null');
2728
}
2829

29-
protected function getClassLoader()
30+
protected function getClassLoader(): ClassLoader
3031
{
3132
return require $this->workspacePath().'/vendor/autoload.php';
3233
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "dantleech/basic",
3+
"authors": [
4+
{
5+
"name": "dantleech",
6+
"email": "[email protected]"
7+
}
8+
],
9+
"require": {},
10+
"config": {
11+
"classmap-authoritative": true
12+
},
13+
"autoload": {
14+
"psr-4": {
15+
"Acme\\Test\\": "psr4/"
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)