Skip to content

Commit e459d63

Browse files
committed
Add support for glob paths to exclude files
Moved file exclusion matching to value object classes `Path` and `Paths`. Provides support for wildcard exclusions using an asterisk: ```json "extra": { "exclude-from-files": [ "laravel/framework/src/*/helpers.php" ] } ``` Changed: - Refactored tests to decouple set-up and tear-down, sort methods by visibility and alphabetically, and improve static analysis. - Updated tests to support meta-packages and immutable packages. - Fixed support for `InstallationManager` before and after Composer v2.5.6. Resolves #16
1 parent 33f3b3d commit e459d63

File tree

9 files changed

+523
-283
lines changed

9 files changed

+523
-283
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Directories
2+
/report/
23
/vendor/
34

45
# Files

CHANGELOG.md

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

3+
## [Unreleased]
4+
5+
* Added support for glob (wildcard) paths to exclude files via new value object classes for paths.
6+
* Updated tests to support meta-packages and immutable packages.
7+
* Refactored tests to decouple set-up and tear-down, sort methods by visibility and alphabetically, and improve static analysis.
8+
39
## [3.0.1] — 2023-05-24
410

511
* Fixed support for changes to metapackages

README.md

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,20 @@ composer config allow-plugins.mcaskill/composer-exclude-files true
3333
> File exclusions of dependencies' `composer.json` are ignored.
3434
3535
From the root `composer.json`, add the `exclude-from-files` property to the
36-
`extra` section. The list of paths must be relative to this composer manifest's
37-
vendor directory.
36+
`extra` section. The list of paths must be relative to this Composer manifest's
37+
vendor directory: `<vendor-name>/<project-name>/<file-path>`.
38+
39+
This plugin supports a subset of special characters used by
40+
the [`glob()` function][php-function-glob] to match exclude paths
41+
matching a pattern:
42+
43+
* `*` — Matches zero or more characters.
44+
* `?` — Matches exactly one character (any character).
3845

3946
This plugin is invoked before the autoloader is dumped, such as with the
4047
commands `install`, `update`, and `dump-autoload`.
4148

42-
###### Example 1: Using illuminate/support
49+
###### Example 1: Excluding one file from illuminate/support
4350

4451
```json
4552
{
@@ -59,7 +66,7 @@ commands `install`, `update`, and `dump-autoload`.
5966
}
6067
```
6168

62-
###### Example 2: Using laravel/framework
69+
###### Example 2: Excluding many files from laravel/framework
6370

6471
```json
6572
{
@@ -68,14 +75,24 @@ commands `install`, `update`, and `dump-autoload`.
6875
},
6976
"extra": {
7077
"exclude-from-files": [
71-
"laravel/framework/src/Illuminate/Foundation/helpers.php"
78+
"laravel/framework/src/*/helpers.php"
7279
]
7380
},
74-
"config": {
75-
"allow-plugins": {
76-
"mcaskill/composer-exclude-files": true
77-
}
78-
}
81+
"config": {}
82+
}
83+
```
84+
85+
###### Example 3: Excluding all files
86+
87+
```json
88+
{
89+
"require": {},
90+
"extra": {
91+
"exclude-from-files": [
92+
"*"
93+
]
94+
},
95+
"config": {}
7996
}
8097
```
8198

@@ -91,6 +108,7 @@ The resulting effect is the specified files are never included in
91108
This is licensed under MIT.
92109

93110
[composer-allow-plugins]: https://getcomposer.org/allow-plugins
111+
[php-function-glob]: https://php.net/function.glob
94112

95113
[github-badge]: https://img.shields.io/github/actions/workflow/status/mcaskill/composer-plugin-exclude-files/test.yml?branch=main
96114
[license-badge]: https://poser.pugx.org/mcaskill/composer-exclude-files/license

phpstan-baseline.neon

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
parameters:
22
ignoreErrors:
3+
-
4+
message: "#^Parameter \\#1 \\$path of function realpath expects string, mixed given\\.$#"
5+
count: 1
6+
path: src/Path.php
37
-
48
message: "#^Parameter \\#1 \\$path of function realpath expects string, string\\|false given\\.$#"
59
count: 1
6-
path: src/ExcludeFilePlugin.php
10+
path: src/Path.php

phpstan.neon.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ includes:
22
- ./phpstan-baseline.neon
33

44
parameters:
5-
level: 8
5+
level: 9
66
treatPhpDocTypesAsCertain: false
77

88
excludePaths:

src/ExcludeFilePlugin.php

Lines changed: 59 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,15 @@
1414
use Composer\Composer;
1515
use Composer\EventDispatcher\EventSubscriberInterface;
1616
use Composer\IO\IOInterface;
17-
use Composer\Package\Package;
1817
use Composer\Package\PackageInterface;
1918
use Composer\Package\RootPackageInterface;
2019
use Composer\Plugin\PluginInterface;
2120
use Composer\Script\ScriptEvents;
2221
use Composer\Util\Filesystem;
23-
use RuntimeException;
2422

23+
/**
24+
* @phpstan-import-type AutoloadRules from PackageInterface
25+
*/
2526
class ExcludeFilePlugin implements
2627
PluginInterface,
2728
EventSubscriberInterface
@@ -93,53 +94,46 @@ public static function getSubscribedEvents(): array
9394
*/
9495
public function parseAutoloads(): void
9596
{
96-
$composer = $this->composer;
97-
98-
$package = $composer->getPackage();
97+
$rootPackage = $this->composer->getPackage();
9998

100-
$excludedFiles = $this->parseExcludedFiles($this->getExcludedFiles($package));
101-
if (!$excludedFiles) {
99+
$excludedFiles = $this->getExcludedFiles($rootPackage);
100+
if ($excludedFiles->isEmpty()) {
102101
return;
103102
}
104103

105-
$excludedFiles = \array_fill_keys($excludedFiles, true);
106-
107-
$generator = $composer->getAutoloadGenerator();
108-
$packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
109-
$packageMap = $generator->buildPackageMap($composer->getInstallationManager(), $package, $packages);
104+
$generator = $this->composer->getAutoloadGenerator();
105+
$packageMap = $generator->buildPackageMap(
106+
$this->composer->getInstallationManager(),
107+
$rootPackage,
108+
$this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages()
109+
);
110110

111-
$this->filterAutoloads($packageMap, $package, $excludedFiles);
111+
$this->filterPackageMapAutoloads($packageMap, $rootPackage, $excludedFiles);
112112
}
113113

114114
/**
115-
* Alters packages to exclude files required in "autoload.files" by
116-
* "extra.exclude-from-files".
115+
* Alters packages to exclude files required in "autoload.files"
116+
* by "extra.exclude-from-files".
117117
*
118-
* @param array<int, array{PackageInterface, ?string}> $packageMap
119-
* List of packages and their installation paths.
120-
* @param RootPackageInterface $rootPackage
121-
* Root package instance.
122-
* @param array<string, true> $excludedFiles
123-
* Map of files to exclude from the "files" autoload mechanism.
118+
* @param array{PackageInterface, ?string}[] $packageMap List of packages
119+
* and their installation paths.
120+
* @param RootPackageInterface $rootPackage Root package instance.
121+
* @param Paths $excludedFiles Collection of Path instances
122+
* to exclude from the "files" autoload mechanism.
124123
* @return void
125124
*/
126-
private function filterAutoloads(
125+
private function filterPackageMapAutoloads(
127126
array $packageMap,
128127
RootPackageInterface $rootPackage,
129-
array $excludedFiles
128+
Paths $excludedFiles
130129
): void {
131130
foreach ($packageMap as [ $package, $installPath ]) {
132-
// Skip root package
131+
// Skip root package.
133132
if ($package === $rootPackage) {
134133
continue;
135134
}
136135

137-
// Skip immutable package
138-
if (!($package instanceof Package)) {
139-
continue;
140-
}
141-
142-
// Skip packages that are not installed
136+
// Skip package if nothing is installed.
143137
if (null === $installPath) {
144138
continue;
145139
}
@@ -152,23 +146,29 @@ private function filterAutoloads(
152146
* Alters a package to exclude files required in "autoload.files" by
153147
* "extra.exclude-from-files".
154148
*
155-
* @param Package $package The package to filter.
156-
* @param string $installPath The installation path of $package.
157-
* @param array<string, true> $excludedFiles Map of files to exclude from
158-
* the "files" autoload mechanism.
149+
* @param PackageInterface $package The package to filter.
150+
* @param string $installPath The installation path of $package.
151+
* @param Paths $excludedFiles Collection of Path instances to exclude
152+
* from the "files" autoload mechanism.
159153
* @return void
160154
*/
161155
private function filterPackageAutoloads(
162-
Package $package,
156+
PackageInterface $package,
163157
string $installPath,
164-
array $excludedFiles
158+
Paths $excludedFiles
165159
): void {
160+
// Skip package if immutable.
161+
if (!\method_exists($package, 'setAutoload')) {
162+
return;
163+
}
164+
166165
$type = self::INCLUDE_FILES_PROPERTY;
167166

167+
/** @var array<string, string[]> */
168168
$autoload = $package->getAutoload();
169169

170170
// Skip misconfigured packages
171-
if (!isset($autoload[$type]) || !\is_array($autoload[$type])) {
171+
if (empty($autoload[$type]) || !\is_array($autoload[$type])) {
172172
return;
173173
}
174174

@@ -178,79 +178,50 @@ private function filterPackageAutoloads(
178178

179179
$filtered = false;
180180

181-
foreach ($autoload[$type] as $key => $path) {
182-
if ($package->getTargetDir() && !\is_readable($installPath.'/'.$path)) {
183-
// add target-dir from file paths that don't have it
184-
$path = $package->getTargetDir() . '/' . $path;
181+
foreach ($autoload[$type] as $index => $localPath) {
182+
if ($package->getTargetDir() && !\is_readable($installPath.'/'.$localPath)) {
183+
// Add 'target-dir' from file paths that don't have it
184+
$localPath = $package->getTargetDir() . '/' . $localPath;
185185
}
186186

187-
$resolvedPath = $installPath . '/' . $path;
188-
$resolvedPath = \strtr($resolvedPath, '\\', '/');
187+
$absolutePath = $installPath . '/' . $localPath;
188+
$absolutePath = \strtr($absolutePath, '\\', '/');
189189

190-
if (isset($excludedFiles[$resolvedPath])) {
190+
if ($excludedFiles->isMatch($absolutePath)) {
191191
$filtered = true;
192-
unset($autoload[$type][$key]);
192+
unset($autoload[$type][$index]);
193193
}
194194
}
195195

196196
if ($filtered) {
197+
/**
198+
* @disregard P1013 Package method existance validated earlier.
199+
* {@see https://github.com/bmewburn/vscode-intelephense/issues/952}.
200+
*/
197201
$package->setAutoload($autoload);
198202
}
199203
}
200204

201205
/**
202-
* Gets a list files the root package wants to exclude.
206+
* Gets a parsed list of files the given package wants to exclude.
203207
*
204208
* @param PackageInterface $package Root package instance.
205-
* @return string[] Retuns the list of excluded files.
209+
* @return Paths Retuns a collection of Path instances.
206210
*/
207-
private function getExcludedFiles(PackageInterface $package): array
211+
private function getExcludedFiles(PackageInterface $package): Paths
208212
{
209213
$type = self::EXCLUDE_FILES_PROPERTY;
210214

211215
$extra = $package->getExtra();
212216

213-
if (isset($extra[$type]) && \is_array($extra[$type])) {
214-
return $extra[$type];
215-
}
216-
217-
return [];
218-
}
219-
220-
/**
221-
* Prepends the vendor directory to each path in "extra.exclude-from-files".
222-
*
223-
* @param string[] $paths Array of paths relative to the composer manifest.
224-
* @throws RuntimeException If the 'vendor-dir' path is unavailable.
225-
* @return string[] Retuns the array of paths, prepended with the vendor directory.
226-
*/
227-
private function parseExcludedFiles(array $paths): array
228-
{
229-
if (!$paths) {
230-
return $paths;
231-
}
232-
233-
$config = $this->composer->getConfig();
234-
$vendorDir = $config->get('vendor-dir');
235-
if (!$vendorDir) {
236-
throw new RuntimeException(
237-
'Invalid value for \'vendor-dir\'. Expected string'
238-
);
239-
}
240-
241-
$filesystem = new Filesystem();
242-
// Do not remove double realpath() calls.
243-
// Fixes failing Windows realpath() implementation.
244-
// See https://bugs.php.net/bug.php?id=72738
245-
/** @var string */
246-
$vendorPath = \realpath(\realpath($vendorDir));
247-
$vendorPath = $filesystem->normalizePath($vendorPath);
248-
249-
foreach ($paths as &$path) {
250-
$path = \preg_replace('{/+}', '/', \trim(\strtr($path, '\\', '/'), '/'));
251-
$path = $vendorPath . '/' . $path;
217+
if (empty($extra[$type]) || !\is_array($extra[$type])) {
218+
return new Paths;
252219
}
253220

254-
return $paths;
221+
return Paths::create(
222+
new Filesystem(),
223+
$this->composer->getConfig(),
224+
$extra[$type]
225+
);
255226
}
256227
}

0 commit comments

Comments
 (0)