Skip to content

Commit ff5635f

Browse files
authored
Ignored paths and relative to the composer.json file (#18)
1 parent 7e3f7b3 commit ff5635f

File tree

8 files changed

+235
-50
lines changed

8 files changed

+235
-50
lines changed

MIGRATION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Migration
22

3-
## v1.2 to v1.3
3+
## v1.2 to v2.0
44

55
### New Requirements
66

@@ -12,7 +12,7 @@ None
1212

1313
### Backward Incompatible Changes
1414

15-
None
15+
- The value of `ignore-paths` is no longer a pattern, it is now relative to the `composer.json` file. The `{vendor}` placeholder can be used as a placeholder for the absolute path to the vendor directory.
1616

1717
### Deprecated Features
1818

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,9 @@ You can require the attributes file as shown in the usage example, but it's pref
126126

127127
composer-attribute-collector inspects files that participate in the autoload process. This can cause
128128
issues with files that have side effects. For instance, `symfony/cache` is known to cause issues, so
129-
we're excluding paths matching `symfony/cache/Traits` from inspection. Additional paths can be
130-
specified using the `extra` section of `composer.json`:
129+
we're excluding paths matching `{vendor}/symfony/cache/Traits` from inspection. Additional paths can
130+
be specified using the `extra` section of `composer.json`. The specified paths are relative to the
131+
`composer.json` file, and the `{vendor}` placeholder is replaced with the path to the vendor folder.
131132

132133
```json
133134
{

src/Config.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
namespace olvlvl\ComposerAttributeCollector;
4+
5+
use Composer\Factory;
6+
use Composer\PartialComposer;
7+
use InvalidArgumentException;
8+
use RuntimeException;
9+
10+
use function array_merge;
11+
use function dirname;
12+
use function is_string;
13+
use function realpath;
14+
use function str_ends_with;
15+
use function str_starts_with;
16+
use function strlen;
17+
18+
use const DIRECTORY_SEPARATOR;
19+
20+
/**
21+
* @readonly
22+
* @internal
23+
*/
24+
final class Config
25+
{
26+
public const EXTRA = 'composer-attribute-collector';
27+
public const EXTRA_IGNORE_PATHS = 'ignore-paths';
28+
29+
public const BUILTIN_IGNORE_PATHS = [
30+
31+
// https://github.com/olvlvl/composer-attribute-collector/issues/4
32+
"{vendor}/symfony/cache/Traits"
33+
34+
];
35+
36+
/**
37+
* If a path starts with this placeholder, it is replaced with the absolute path to the vendor directory.
38+
*/
39+
public const VENDOR_PLACEHOLDER = '{vendor}';
40+
41+
public static function from(PartialComposer $composer): self
42+
{
43+
$vendorDir = $composer->getConfig()->get('vendor-dir');
44+
45+
if (!is_string($vendorDir) || !$vendorDir) {
46+
throw new RuntimeException("Unable to determine vendor directory");
47+
}
48+
49+
$composerFile = Factory::getComposerFile();
50+
$rootDir = realpath(dirname($composerFile));
51+
52+
if (!$rootDir) {
53+
throw new RuntimeException("Unable to determine root directory");
54+
}
55+
56+
$rootDir .= DIRECTORY_SEPARATOR;
57+
58+
/** @var array{ ignore-paths?: non-empty-string[] } $extra */
59+
$extra = $composer->getPackage()->getExtra()[self::EXTRA] ?? [];
60+
61+
$ignorePaths = self::expandPaths(
62+
array_merge($extra[self::EXTRA_IGNORE_PATHS] ?? [], self::BUILTIN_IGNORE_PATHS),
63+
$vendorDir,
64+
$rootDir
65+
);
66+
67+
return new self(
68+
attributesFile: "$vendorDir/attributes.php",
69+
ignorePaths: $ignorePaths,
70+
);
71+
}
72+
73+
/**
74+
* @param non-empty-string $attributesFile
75+
* Absolute path to the `attributes.php` file.
76+
* @param string[] $ignorePaths
77+
* Paths that should be ignored for attributes collection.
78+
*/
79+
public function __construct(
80+
public string $attributesFile,
81+
public array $ignorePaths,
82+
) {
83+
}
84+
85+
/**
86+
* @param non-empty-string[] $paths
87+
* @param non-empty-string $vendorDir
88+
* @param non-empty-string $rootDir
89+
*
90+
* @return non-empty-string[]
91+
*/
92+
private static function expandPaths(array $paths, string $vendorDir, string $rootDir): array
93+
{
94+
if (str_ends_with($vendorDir, DIRECTORY_SEPARATOR)) {
95+
throw new InvalidArgumentException("vendorDir must not end with a directory separator, given: $vendorDir");
96+
}
97+
98+
if (!str_ends_with($rootDir, DIRECTORY_SEPARATOR)) {
99+
throw new InvalidArgumentException("rootDir must end with a directory separator, given: $rootDir");
100+
}
101+
102+
$expanded = [];
103+
104+
foreach ($paths as $path) {
105+
if (str_starts_with($path, self::VENDOR_PLACEHOLDER)) {
106+
$path = $vendorDir . substr($path, strlen(self::VENDOR_PLACEHOLDER));
107+
} else {
108+
$path = $rootDir . $path;
109+
}
110+
111+
$expanded[] = $path;
112+
}
113+
114+
return $expanded;
115+
}
116+
}

src/Filter/PathFilter.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
use Composer\IO\IOInterface;
1313
use olvlvl\ComposerAttributeCollector\Filter;
1414

15-
use function str_contains;
15+
use function str_starts_with;
1616

17+
/**
18+
* @internal
19+
*/
1720
final class PathFilter implements Filter
1821
{
1922
/**
@@ -27,7 +30,7 @@ public function __construct(
2730
public function filter(string $filepath, string $class, IOInterface $io): bool
2831
{
2932
foreach ($this->matches as $match) {
30-
if (str_contains($filepath, $match)) {
33+
if (str_starts_with($filepath, $match)) {
3134
$io->debug("Discarding '$class' because its path matches '$match'");
3235

3336
return false;

src/Plugin.php

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,7 @@
3434
*/
3535
final class Plugin implements PluginInterface, EventSubscriberInterface
3636
{
37-
public const EXTRA = 'composer-attribute-collector';
38-
public const EXTRA_IGNORE_PATHS = 'ignore-paths';
3937
public const CACHE_DIR = '.composer-attribute-collector';
40-
private const PROBLEMATIC_PATHS = [
41-
// https://github.com/olvlvl/composer-attribute-collector/issues/4
42-
'symfony/cache/Traits'
43-
];
4438

4539
/**
4640
* @uses onPostAutoloadDump
@@ -78,14 +72,12 @@ public function uninstall(Composer $composer, IOInterface $io): void
7872
public static function onPostAutoloadDump(Event $event): void
7973
{
8074
$composer = $event->getComposer();
75+
$config = Config::from($composer);
8176
$io = $event->getIO();
82-
$vendorDir = $composer->getConfig()->get('vendor-dir');
83-
assert(is_string($vendorDir));
84-
$filepath = "$vendorDir/attributes.php";
8577

8678
$start = microtime(true);
8779
$io->write('<info>Generating attributes file</info>');
88-
self::dump($event->getComposer(), $io, $filepath);
80+
self::dump($event->getComposer(), $config, $io);
8981
$elapsed = self::renderElapsedTime($start);
9082
$io->write("<info>Generated attributes file in $elapsed</info>");
9183
}
@@ -95,8 +87,8 @@ public static function onPostAutoloadDump(Event $event): void
9587
*/
9688
public static function dump(
9789
Composer $composer,
90+
Config $config,
9891
IOInterface $io,
99-
string $filepath,
10092
AutoloadsBuilder $autoloadsBuilder = null,
10193
ClassMapBuilder $classMapBuilder = null
10294
): void {
@@ -120,7 +112,7 @@ public static function dump(
120112
self::setupAutoload($classMap);
121113

122114
$start = microtime(true);
123-
$filter = self::buildFileFilter($composer);
115+
$filter = self::buildFileFilter($config);
124116
$classMap = $classMapFilter->filter(
125117
$classMap,
126118
fn (string $class, string $filepath): bool => $filter->filter($filepath, $class, $io)
@@ -138,7 +130,7 @@ public static function dump(
138130
$elapsed = self::renderElapsedTime($start);
139131
$io->debug("Generating attributes file: rendered code in $elapsed");
140132

141-
file_put_contents($filepath, $code);
133+
file_put_contents($config->attributesFile, $code);
142134
}
143135

144136
private static function buildDefaultDatastore(IOInterface $io): Datastore
@@ -168,18 +160,10 @@ private static function setupAutoload(array $classMap): void
168160
});
169161
}
170162

171-
private static function buildFileFilter(Composer $composer): Filter
163+
private static function buildFileFilter(Config $config): Filter
172164
{
173-
$extra = $composer->getPackage()->getExtra()[self::EXTRA] ?? [];
174-
/** @var string[] $ignore_paths */
175-
$ignore_paths = array_merge(
176-
// @phpstan-ignore-next-line
177-
$extra[self::EXTRA_IGNORE_PATHS] ?? [],
178-
self::PROBLEMATIC_PATHS
179-
);
180-
181165
return new Filter\Chain([
182-
new PathFilter($ignore_paths),
166+
new PathFilter($config->ignorePaths),
183167
new ContentFilter(),
184168
new InterfaceFilter()
185169
]);

tests/ConfigTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace tests\olvlvl\ComposerAttributeCollector;
4+
5+
use Composer\Package\RootPackageInterface;
6+
use Composer\PartialComposer;
7+
use olvlvl\ComposerAttributeCollector\Config;
8+
use PHPUnit\Framework\TestCase;
9+
use RuntimeException;
10+
11+
use function assert;
12+
use function getcwd;
13+
use function is_string;
14+
15+
final class ConfigTest extends TestCase
16+
{
17+
public function testFrom(): void
18+
{
19+
$extra = [
20+
Config::EXTRA => [
21+
Config::EXTRA_IGNORE_PATHS => [
22+
'{vendor}/vendor1/package1',
23+
'tests/Acme/PSR4/IncompatibleSignature.php'
24+
]
25+
]
26+
];
27+
28+
$package = $this->getMockBuilder(RootPackageInterface::class)->getMock();
29+
$package
30+
->method('getExtra')
31+
->willReturn($extra);
32+
33+
$cwd = getcwd();
34+
assert(is_string($cwd));
35+
$config = $this->getMockBuilder(\Composer\Config::class)->getMock();
36+
$config
37+
->method('get')
38+
->with('vendor-dir')
39+
->willReturn("$cwd/vendor");
40+
41+
$composer = new PartialComposer();
42+
$composer->setConfig($config);
43+
$composer->setPackage($package);
44+
45+
$expected = new Config(
46+
attributesFile: "$cwd/vendor/attributes.php",
47+
ignorePaths: [
48+
"$cwd/vendor/vendor1/package1",
49+
"$cwd/tests/Acme/PSR4/IncompatibleSignature.php",
50+
"$cwd/vendor/symfony/cache/Traits",
51+
]
52+
);
53+
54+
$actual = Config::from($composer);
55+
56+
$this->assertEquals($expected, $actual);
57+
}
58+
59+
public function testFromFailsOnMissingVendorDir(): void
60+
{
61+
$config = $this->getMockBuilder(\Composer\Config::class)->getMock();
62+
$config
63+
->method('get')
64+
->with('vendor-dir')
65+
->willReturn("");
66+
67+
$composer = new PartialComposer();
68+
$composer->setConfig($config);
69+
70+
$this->expectException(RuntimeException::class);
71+
$this->expectExceptionMessage("Unable to determine vendor directory");
72+
73+
Config::from($composer);
74+
}
75+
}

tests/Filter/PathFilterTest.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ protected function setUp(): void
2222
parent::setUp();
2323

2424
$this->filter = new PathFilter([
25-
"symfony/cache/Traits"
25+
"/absolute/path/to/symfony/cache/Traits"
2626
]);
2727
}
2828

2929
/**
3030
* @dataProvider provideFilter
31+
*
32+
* @param class-string $class
3133
*/
3234
public function testFilter(string $filepath, string $class, bool $expected): void
3335
{
@@ -49,12 +51,17 @@ public function testFilter(string $filepath, string $class, bool $expected): voi
4951
$this->assertEquals($expected, $actual);
5052
}
5153

54+
/**
55+
* @return array<array{ non-empty-string, string, bool }>
56+
*/
5257
public function provideFilter(): array
5358
{
5459
return [
5560

56-
[ "vendor/symfony/cache/Traits/RedisCluster5Proxy.php", "RedisCluster5Proxy", false ],
57-
[ "vendor/symfony/routing/Route.php", "Route", true ],
61+
[ "/absolute/path/to/symfony/cache/Traits/RedisCluster5Proxy.php", "RedisCluster5Proxy", false ],
62+
[ "some/prefix/absolute/path/to/symfony/cache/Traits/RedisCluster5Proxy.php", "RedisCluster5Proxy", true ],
63+
[ "symfony/cache/Traits/RedisCluster5Proxy.php", "RedisCluster5Proxy", true ],
64+
[ "/absolute/path/to/symfony/routing/Route.php", "Route", true ],
5865

5966
];
6067
}

0 commit comments

Comments
 (0)