Skip to content

Commit 3bd1168

Browse files
authored
Ignore paths (Fixes #3, #4) (#5)
1 parent 42669a2 commit 3bd1168

21 files changed

+611
-24
lines changed

MIGRATION.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# Migration
22

3+
## v1.0 to v1.1
4+
5+
### New Requirements
6+
7+
None
8+
9+
### New features
10+
11+
- File paths matching `symfony/cache/Traits` are ignored.
12+
- The option `extra.composer-attribute-collection.ignore-paths` can be used to ignore paths.
13+
14+
### Backward Incompatible Changes
15+
16+
None
17+
18+
### Deprecated Features
19+
20+
None
21+
22+
### Other Changes
23+
24+
None
25+
326
<!--
427
528
## vX.x to vX.x

README.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,29 @@ could also use [spatie/file-system-watcher][], it only requires PHP.
8888

8989

9090

91+
## Configuration
92+
93+
### Ignoring paths ([root-only][])
94+
95+
composer-attribute-collector inspects files that participate in the autoload process. This can cause
96+
issues with files that have side effects. For instance, `symfony/cache` is known to cause issues, so
97+
we're excluding paths matching `symfony/cache/Traits` from inspection. Additional paths can be
98+
specified using the `extra` section of `composer.json`:
99+
100+
```json
101+
{
102+
"extra": {
103+
"composer-attribute-collector": {
104+
"ignore-paths": [
105+
"path/to/ignore"
106+
]
107+
}
108+
}
109+
}
110+
```
111+
112+
113+
91114
## Test drive with a Symfony app
92115

93116
You can try the plugin with a fresh installation of Symfony.
@@ -125,6 +148,8 @@ Attributes::with(fn () => new Collection(
125148
[ ['translation:pull', 'Pull translations from a given provider.'], \Symfony\Component\Translation\Command\TranslationPullCommand::class ],
126149
```
127150

151+
We also have [a repository to test the Symfony usecase](https://github.com/olvlvl/composer-attribute-collector-usecase-symfony).
152+
128153

129154

130155
## Use cases
@@ -270,7 +295,8 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
270295

271296

272297

273-
[Composer]: https://getcomposer.org/
298+
[Composer]: https://getcomposer.org/
299+
[root-only]: https://getcomposer.org/doc/04-schema.md#root-package
274300
[ICanBoogie/MessageBus]: https://github.com/ICanBoogie/MessageBus
275301
[spatie/file-system-watcher]: https://github.com/spatie/file-system-watcher
276302
[phpstorm-watchers]: https://www.jetbrains.com/help/phpstorm/using-file-watchers.html

composer.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@
3535
"phpunit/phpunit": "^9.5"
3636
},
3737
"extra": {
38-
"class": "olvlvl\\ComposerAttributeCollector\\Plugin"
38+
"class": "olvlvl\\ComposerAttributeCollector\\Plugin",
39+
"composer-attribute-collector": {
40+
"ignore-paths": [
41+
"IncompatibleSignature"
42+
]
43+
}
3944
}
4045
}

src/Filter.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace olvlvl\ComposerAttributeCollector;
4+
5+
use Composer\IO\IOInterface;
6+
7+
interface Filter
8+
{
9+
/**
10+
* @param class-string $class
11+
*/
12+
public function filter(string $filepath, string $class, IOInterface $io): bool;
13+
}

src/Filter/Chain.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* (c) Olivier Laviale <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace olvlvl\ComposerAttributeCollector\Filter;
11+
12+
use Composer\IO\IOInterface;
13+
use olvlvl\ComposerAttributeCollector\Filter;
14+
15+
final class Chain implements Filter
16+
{
17+
/**
18+
* @param iterable<Filter> $filters
19+
*/
20+
public function __construct(
21+
private iterable $filters
22+
) {
23+
}
24+
25+
public function filter(string $filepath, string $class, IOInterface $io): bool
26+
{
27+
foreach ($this->filters as $filter) {
28+
if ($filter->filter($filepath, $class, $io) === false) {
29+
return false;
30+
}
31+
}
32+
33+
return true;
34+
}
35+
}

src/Filter/ContentFilter.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* (c) Olivier Laviale <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace olvlvl\ComposerAttributeCollector\Filter;
11+
12+
use Composer\IO\IOInterface;
13+
use olvlvl\ComposerAttributeCollector\Filter;
14+
15+
use function assert;
16+
use function file_get_contents;
17+
use function is_string;
18+
use function preg_match;
19+
use function str_contains;
20+
21+
final class ContentFilter implements Filter
22+
{
23+
public function filter(string $filepath, string $class, IOInterface $io): bool
24+
{
25+
$content = file_get_contents($filepath);
26+
27+
assert(is_string($content));
28+
29+
// No hint of attribute usage.
30+
if (!str_contains($content, '#[')) {
31+
return false;
32+
}
33+
34+
// Hint of an attribute class.
35+
if (preg_match('/#\[\\\?Attribute[\]\(]/', $content)) {
36+
$io->debug("Discarding '$class' because it looks like an attribute");
37+
return false;
38+
}
39+
40+
return true;
41+
}
42+
}

src/Filter/InterfaceFilter.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* (c) Olivier Laviale <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace olvlvl\ComposerAttributeCollector\Filter;
11+
12+
use Composer\IO\IOInterface;
13+
use olvlvl\ComposerAttributeCollector\Filter;
14+
use Throwable;
15+
16+
use function interface_exists;
17+
18+
final class InterfaceFilter implements Filter
19+
{
20+
public function filter(string $filepath, string $class, IOInterface $io): bool
21+
{
22+
try {
23+
if (interface_exists($class)) {
24+
return false;
25+
}
26+
} catch (Throwable $e) {
27+
$io->warning("Discarding '$class' because an error occurred during loading: {$e->getMessage()}");
28+
29+
return false;
30+
}
31+
32+
return true;
33+
}
34+
}

src/Filter/PathFilter.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* (c) Olivier Laviale <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace olvlvl\ComposerAttributeCollector\Filter;
11+
12+
use Composer\IO\IOInterface;
13+
use olvlvl\ComposerAttributeCollector\Filter;
14+
15+
use function str_contains;
16+
17+
final class PathFilter implements Filter
18+
{
19+
/**
20+
* @param string[] $matches
21+
*/
22+
public function __construct(
23+
private array $matches
24+
) {
25+
}
26+
27+
public function filter(string $filepath, string $class, IOInterface $io): bool
28+
{
29+
foreach ($this->matches as $match) {
30+
if (str_contains($filepath, $match)) {
31+
$io->debug("Discarding '$class' because its path matches '$match'");
32+
33+
return false;
34+
}
35+
}
36+
37+
return true;
38+
}
39+
}

src/Plugin.php

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,38 @@
1515
use Composer\IO\IOInterface;
1616
use Composer\Plugin\PluginInterface;
1717
use Composer\Script\Event;
18+
use olvlvl\ComposerAttributeCollector\Filter\ContentFilter;
19+
use olvlvl\ComposerAttributeCollector\Filter\InterfaceFilter;
20+
use olvlvl\ComposerAttributeCollector\Filter\PathFilter;
1821
use ReflectionAttribute;
1922
use ReflectionClass;
2023
use ReflectionException;
2124
use ReflectionMethod;
22-
use Throwable;
2325

26+
use function array_filter;
2427
use function array_map;
25-
use function file_get_contents;
28+
use function array_merge;
2629
use function file_put_contents;
2730
use function implode;
28-
use function interface_exists;
2931
use function is_string;
3032
use function spl_autoload_register;
31-
use function str_contains;
3233
use function var_export;
3334

35+
use const ARRAY_FILTER_USE_BOTH;
36+
3437
/**
3538
* @internal
3639
*/
3740
final class Plugin implements PluginInterface, EventSubscriberInterface
3841
{
42+
public const EXTRA = 'composer-attribute-collector';
43+
public const EXTRA_IGNORE_PATHS = 'ignore-paths';
44+
45+
private const PROBLEMATIC_PATHS = [
46+
// https://github.com/olvlvl/composer-attribute-collector/issues/4
47+
'symfony/cache/Traits'
48+
];
49+
3950
/**
4051
* @uses onPostAutoloadDump
4152
*
@@ -97,6 +108,13 @@ public static function dump(
97108

98109
self::setupAutoload($classMap);
99110

111+
$filter = self::buildClassMapFilter($composer);
112+
$classMap = array_filter(
113+
$classMap,
114+
fn ($class, $filepath) => $filter->filter($class, $filepath, $io),
115+
ARRAY_FILTER_USE_BOTH
116+
);
117+
100118
$collection = self::collectAttributes($classMap, $io);
101119
$code = self::render($collection);
102120

@@ -116,6 +134,23 @@ private static function setupAutoload(array $classMap): void
116134
});
117135
}
118136

137+
private static function buildClassMapFilter(Composer $composer): Filter
138+
{
139+
$extra = $composer->getPackage()->getExtra()[self::EXTRA] ?? [];
140+
/** @var string[] $ignore_paths */
141+
$ignore_paths = array_merge(
142+
// @phpstan-ignore-next-line
143+
$extra[self::EXTRA_IGNORE_PATHS] ?? [],
144+
self::PROBLEMATIC_PATHS
145+
);
146+
147+
return new Filter\Chain([
148+
new PathFilter($ignore_paths),
149+
new ContentFilter(),
150+
new InterfaceFilter()
151+
]);
152+
}
153+
119154
/**
120155
* @param array<class-string, non-empty-string> $classMap
121156
*
@@ -126,24 +161,6 @@ private static function collectAttributes(array $classMap, IOInterface $io): Col
126161
$collector = new Collector();
127162

128163
foreach ($classMap as $class => $filepath) {
129-
$content = file_get_contents($filepath);
130-
131-
assert(is_string($content));
132-
133-
if (!str_contains($content, '#[')) {
134-
continue;
135-
}
136-
137-
try {
138-
if (interface_exists($class)) {
139-
continue;
140-
}
141-
} catch (Throwable $e) {
142-
$io->warning("Discarding '$class' because an error occurred during loading: {$e->getMessage()}");
143-
144-
continue;
145-
}
146-
147164
$classReflection = new ReflectionClass($class);
148165

149166
if (self::isAttribute($classReflection)) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Acme\PSR4;
4+
5+
use Acme\Attribute\Handler;
6+
use IteratorAggregate;
7+
8+
#[Handler]
9+
class IncompatibleSignature implements IteratorAggregate
10+
{
11+
public function getIterator(string $invalid): void
12+
{
13+
// TODO: Implement getIterator() method.
14+
}
15+
}

0 commit comments

Comments
 (0)