Skip to content

Commit 98bbf67

Browse files
authored
Fix parsing of ambiguous classes to avoid ignoring real classes when test ones get scanned first (#13)
Refs composer/composer#12140
1 parent 465b1b0 commit 98bbf67

File tree

4 files changed

+64
-11
lines changed

4 files changed

+64
-11
lines changed

phpstan-baseline.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ parameters:
99
message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'A' and array\\<class\\-string, array\\<non\\-empty\\-string\\>\\> will always evaluate to false\\.$#"
1010
count: 1
1111
path: tests/ClassMapGeneratorTest.php
12+
13+
-
14+
message: "#^Offset 'A' does not exist on non\\-empty\\-array\\<class\\-string, array\\<non\\-empty\\-string\\>\\>\\.$#"
15+
count: 4
16+
path: tests/ClassMapGeneratorTest.php

src/ClassMap.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
namespace Composer\ClassMapGenerator;
1414

15+
use Composer\Pcre\Preg;
16+
1517
/**
1618
* @author Jordi Boggiano <j.boggiano@seld.be>
1719
*/
@@ -71,11 +73,36 @@ public function getPsrViolations(): array
7173
*
7274
* To get the path the class is being mapped to, call getClassPath
7375
*
76+
* By default, paths that contain test(s), fixture(s), example(s) or stub(s) are ignored
77+
* as those are typically not problematic when they're dummy classes in the tests folder.
78+
* If you want to get these back as well you can pass false to $duplicatesFilter. Or
79+
* you can pass your own pattern to exclude if you need to change the default.
80+
*
81+
* @param non-empty-string|false $duplicatesFilter
82+
*
7483
* @return array<class-string, array<non-empty-string>>
7584
*/
76-
public function getAmbiguousClasses(): array
85+
public function getAmbiguousClasses($duplicatesFilter = '{/(test|fixture|example|stub)s?/}i'): array
7786
{
78-
return $this->ambiguousClasses;
87+
if (false === $duplicatesFilter) {
88+
return $this->ambiguousClasses;
89+
}
90+
91+
if (true === $duplicatesFilter) {
92+
throw new \InvalidArgumentException('$duplicatesFilter should be false or a string with a valid regex, got true.');
93+
}
94+
95+
$ambiguousClasses = [];
96+
foreach ($this->ambiguousClasses as $class => $paths) {
97+
$paths = array_filter($paths, function ($path) use ($duplicatesFilter) {
98+
return !Preg::isMatch($duplicatesFilter, strtr($path, '\\', '/'));
99+
});
100+
if (\count($paths) > 0) {
101+
$ambiguousClasses[$class] = array_values($paths);
102+
}
103+
}
104+
105+
return $ambiguousClasses;
79106
}
80107

81108
/**

src/ClassMapGenerator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ public function scanPaths($path, ?string $excluded = null, string $autoloadType
191191
foreach ($classes as $class) {
192192
if (!$this->classMap->hasClass($class)) {
193193
$this->classMap->addClass($class, $filePath);
194-
} elseif ($filePath !== $this->classMap->getClassPath($class) && !Preg::isMatch('{/(test|fixture|example|stub)s?/}i', strtr($this->classMap->getClassPath($class).' '.$filePath, '\\', '/'))) {
194+
} elseif ($filePath !== $this->classMap->getClassPath($class)) {
195195
$this->classMap->addAmbiguousClass($class, $filePath);
196196
}
197197
}

tests/ClassMapGeneratorTest.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,11 @@ public function testUnambiguousReference(): void
186186
{
187187
$tempDir = self::getUniqueTmpDirectory();
188188

189-
file_put_contents($tempDir . '/A.php', "<?php\nclass A {}");
189+
mkdir($tempDir.'/src');
190+
mkdir($tempDir.'/ambiguous');
191+
file_put_contents($tempDir . '/src/A.php', "<?php\nclass A {}");
190192
file_put_contents(
191-
$tempDir . '/B.php',
193+
$tempDir . '/src/B.php',
192194
"<?php
193195
if (true) {
194196
interface B {}
@@ -199,17 +201,36 @@ interface B extends Iterator {}
199201
);
200202

201203
foreach (array('test', 'fixture', 'example') as $keyword) {
202-
if (!is_dir($tempDir . '/' . $keyword)) {
203-
mkdir($tempDir . '/' . $keyword, 0777, true);
204+
if (!is_dir($tempDir . '/ambiguous/' . $keyword)) {
205+
mkdir($tempDir . '/ambiguous/' . $keyword, 0777, true);
204206
}
205-
file_put_contents($tempDir . '/' . $keyword . '/A.php', "<?php\nclass A {}");
207+
file_put_contents($tempDir . '/ambiguous/' . $keyword . '/A.php', "<?php\nclass A {}");
206208
}
207209

208-
$this->generator->scanPaths($tempDir);
210+
// if we scan src first, then test ambiguous refs will be ignored correctly
211+
$this->generator->scanPaths($tempDir.'/src');
212+
$this->generator->scanPaths($tempDir.'/ambiguous');
209213
$classMap = $this->generator->getClassMap();
210-
211214
self::assertCount(0, $classMap->getAmbiguousClasses());
212215

216+
// but when retrieving without filtering, the ambiguous classes are there
217+
self::assertCount(1, $classMap->getAmbiguousClasses(false));
218+
self::assertCount(3, $classMap->getAmbiguousClasses(false)['A']);
219+
220+
// if we scan tests first however, then we always get ambiguous refs as the test one is overriding src
221+
$this->generator = new ClassMapGenerator(['php', 'inc', 'hh']);
222+
$this->generator->scanPaths($tempDir.'/ambiguous');
223+
$this->generator->scanPaths($tempDir.'/src');
224+
$classMap = $this->generator->getClassMap();
225+
226+
// when retrieving with filtering, only the one from src is seen as ambiguous
227+
self::assertCount(1, $classMap->getAmbiguousClasses());
228+
self::assertCount(1, $classMap->getAmbiguousClasses()['A']);
229+
self::assertSame($tempDir.'/src'.DIRECTORY_SEPARATOR.'A.php', $classMap->getAmbiguousClasses()['A'][0]);
230+
// when retrieving without filtering, all the ambiguous classes are there
231+
self::assertCount(1, $classMap->getAmbiguousClasses(false));
232+
self::assertCount(3, $classMap->getAmbiguousClasses(false)['A']);
233+
213234
$fs = new Filesystem();
214235
$fs->remove($tempDir);
215236
}
@@ -291,7 +312,7 @@ public static function getUniqueTmpDirectory(): string
291312
$root = sys_get_temp_dir();
292313

293314
do {
294-
$unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-' . random_int(1000, 9000));
315+
$unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-classmap-' . random_int(1000, 9000));
295316

296317
if (!file_exists($unique) && @mkdir($unique, 0777)) {
297318
return (string) realpath($unique);

0 commit comments

Comments
 (0)