Skip to content

Commit

Permalink
Add support for glob paths to exclude files
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mcaskill committed May 19, 2024
1 parent 33f3b3d commit e459d63
Show file tree
Hide file tree
Showing 9 changed files with 523 additions and 283 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Directories
/report/
/vendor/

# Files
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [Unreleased]

* Added support for glob (wildcard) paths to exclude files via new value object classes for paths.
* Updated tests to support meta-packages and immutable packages.
* Refactored tests to decouple set-up and tear-down, sort methods by visibility and alphabetically, and improve static analysis.

## [3.0.1] — 2023-05-24

* Fixed support for changes to metapackages
Expand Down
38 changes: 28 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,20 @@ composer config allow-plugins.mcaskill/composer-exclude-files true
> File exclusions of dependencies' `composer.json` are ignored.
From the root `composer.json`, add the `exclude-from-files` property to the
`extra` section. The list of paths must be relative to this composer manifest's
vendor directory.
`extra` section. The list of paths must be relative to this Composer manifest's
vendor directory: `<vendor-name>/<project-name>/<file-path>`.

This plugin supports a subset of special characters used by
the [`glob()` function][php-function-glob] to match exclude paths
matching a pattern:

* `*` — Matches zero or more characters.
* `?` — Matches exactly one character (any character).

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

###### Example 1: Using illuminate/support
###### Example 1: Excluding one file from illuminate/support

```json
{
Expand All @@ -59,7 +66,7 @@ commands `install`, `update`, and `dump-autoload`.
}
```

###### Example 2: Using laravel/framework
###### Example 2: Excluding many files from laravel/framework

```json
{
Expand All @@ -68,14 +75,24 @@ commands `install`, `update`, and `dump-autoload`.
},
"extra": {
"exclude-from-files": [
"laravel/framework/src/Illuminate/Foundation/helpers.php"
"laravel/framework/src/*/helpers.php"
]
},
"config": {
"allow-plugins": {
"mcaskill/composer-exclude-files": true
}
}
"config": {}
}
```

###### Example 3: Excluding all files

```json
{
"require": {},
"extra": {
"exclude-from-files": [
"*"
]
},
"config": {}
}
```

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

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

[github-badge]: https://img.shields.io/github/actions/workflow/status/mcaskill/composer-plugin-exclude-files/test.yml?branch=main
[license-badge]: https://poser.pugx.org/mcaskill/composer-exclude-files/license
Expand Down
6 changes: 5 additions & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#1 \\$path of function realpath expects string, mixed given\\.$#"
count: 1
path: src/Path.php
-
message: "#^Parameter \\#1 \\$path of function realpath expects string, string\\|false given\\.$#"
count: 1
path: src/ExcludeFilePlugin.php
path: src/Path.php
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ includes:
- ./phpstan-baseline.neon

parameters:
level: 8
level: 9
treatPhpDocTypesAsCertain: false

excludePaths:
Expand Down
147 changes: 59 additions & 88 deletions src/ExcludeFilePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@
use Composer\Composer;
use Composer\EventDispatcher\EventSubscriberInterface;
use Composer\IO\IOInterface;
use Composer\Package\Package;
use Composer\Package\PackageInterface;
use Composer\Package\RootPackageInterface;
use Composer\Plugin\PluginInterface;
use Composer\Script\ScriptEvents;
use Composer\Util\Filesystem;
use RuntimeException;

/**
* @phpstan-import-type AutoloadRules from PackageInterface
*/
class ExcludeFilePlugin implements
PluginInterface,
EventSubscriberInterface
Expand Down Expand Up @@ -93,53 +94,46 @@ public static function getSubscribedEvents(): array
*/
public function parseAutoloads(): void
{
$composer = $this->composer;

$package = $composer->getPackage();
$rootPackage = $this->composer->getPackage();

$excludedFiles = $this->parseExcludedFiles($this->getExcludedFiles($package));
if (!$excludedFiles) {
$excludedFiles = $this->getExcludedFiles($rootPackage);
if ($excludedFiles->isEmpty()) {
return;
}

$excludedFiles = \array_fill_keys($excludedFiles, true);

$generator = $composer->getAutoloadGenerator();
$packages = $composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages();
$packageMap = $generator->buildPackageMap($composer->getInstallationManager(), $package, $packages);
$generator = $this->composer->getAutoloadGenerator();
$packageMap = $generator->buildPackageMap(
$this->composer->getInstallationManager(),
$rootPackage,
$this->composer->getRepositoryManager()->getLocalRepository()->getCanonicalPackages()
);

$this->filterAutoloads($packageMap, $package, $excludedFiles);
$this->filterPackageMapAutoloads($packageMap, $rootPackage, $excludedFiles);
}

/**
* Alters packages to exclude files required in "autoload.files" by
* "extra.exclude-from-files".
* Alters packages to exclude files required in "autoload.files"
* by "extra.exclude-from-files".
*
* @param array<int, array{PackageInterface, ?string}> $packageMap
* List of packages and their installation paths.
* @param RootPackageInterface $rootPackage
* Root package instance.
* @param array<string, true> $excludedFiles
* Map of files to exclude from the "files" autoload mechanism.
* @param array{PackageInterface, ?string}[] $packageMap List of packages
* and their installation paths.
* @param RootPackageInterface $rootPackage Root package instance.
* @param Paths $excludedFiles Collection of Path instances
* to exclude from the "files" autoload mechanism.
* @return void
*/
private function filterAutoloads(
private function filterPackageMapAutoloads(
array $packageMap,
RootPackageInterface $rootPackage,
array $excludedFiles
Paths $excludedFiles
): void {
foreach ($packageMap as [ $package, $installPath ]) {
// Skip root package
// Skip root package.
if ($package === $rootPackage) {
continue;
}

// Skip immutable package
if (!($package instanceof Package)) {
continue;
}

// Skip packages that are not installed
// Skip package if nothing is installed.
if (null === $installPath) {
continue;
}
Expand All @@ -152,23 +146,29 @@ private function filterAutoloads(
* Alters a package to exclude files required in "autoload.files" by
* "extra.exclude-from-files".
*
* @param Package $package The package to filter.
* @param string $installPath The installation path of $package.
* @param array<string, true> $excludedFiles Map of files to exclude from
* the "files" autoload mechanism.
* @param PackageInterface $package The package to filter.
* @param string $installPath The installation path of $package.
* @param Paths $excludedFiles Collection of Path instances to exclude
* from the "files" autoload mechanism.
* @return void
*/
private function filterPackageAutoloads(
Package $package,
PackageInterface $package,
string $installPath,
array $excludedFiles
Paths $excludedFiles
): void {
// Skip package if immutable.
if (!\method_exists($package, 'setAutoload')) {
return;
}

$type = self::INCLUDE_FILES_PROPERTY;

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

// Skip misconfigured packages
if (!isset($autoload[$type]) || !\is_array($autoload[$type])) {
if (empty($autoload[$type]) || !\is_array($autoload[$type])) {
return;
}

Expand All @@ -178,79 +178,50 @@ private function filterPackageAutoloads(

$filtered = false;

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

$resolvedPath = $installPath . '/' . $path;
$resolvedPath = \strtr($resolvedPath, '\\', '/');
$absolutePath = $installPath . '/' . $localPath;
$absolutePath = \strtr($absolutePath, '\\', '/');

if (isset($excludedFiles[$resolvedPath])) {
if ($excludedFiles->isMatch($absolutePath)) {
$filtered = true;
unset($autoload[$type][$key]);
unset($autoload[$type][$index]);
}
}

if ($filtered) {
/**
* @disregard P1013 Package method existance validated earlier.
* {@see https://github.com/bmewburn/vscode-intelephense/issues/952}.
*/
$package->setAutoload($autoload);
}
}

/**
* Gets a list files the root package wants to exclude.
* Gets a parsed list of files the given package wants to exclude.
*
* @param PackageInterface $package Root package instance.
* @return string[] Retuns the list of excluded files.
* @return Paths Retuns a collection of Path instances.
*/
private function getExcludedFiles(PackageInterface $package): array
private function getExcludedFiles(PackageInterface $package): Paths
{
$type = self::EXCLUDE_FILES_PROPERTY;

$extra = $package->getExtra();

if (isset($extra[$type]) && \is_array($extra[$type])) {
return $extra[$type];
}

return [];
}

/**
* Prepends the vendor directory to each path in "extra.exclude-from-files".
*
* @param string[] $paths Array of paths relative to the composer manifest.
* @throws RuntimeException If the 'vendor-dir' path is unavailable.
* @return string[] Retuns the array of paths, prepended with the vendor directory.
*/
private function parseExcludedFiles(array $paths): array
{
if (!$paths) {
return $paths;
}

$config = $this->composer->getConfig();
$vendorDir = $config->get('vendor-dir');
if (!$vendorDir) {
throw new RuntimeException(
'Invalid value for \'vendor-dir\'. Expected string'
);
}

$filesystem = new Filesystem();
// Do not remove double realpath() calls.
// Fixes failing Windows realpath() implementation.
// See https://bugs.php.net/bug.php?id=72738
/** @var string */
$vendorPath = \realpath(\realpath($vendorDir));
$vendorPath = $filesystem->normalizePath($vendorPath);

foreach ($paths as &$path) {
$path = \preg_replace('{/+}', '/', \trim(\strtr($path, '\\', '/'), '/'));
$path = $vendorPath . '/' . $path;
if (empty($extra[$type]) || !\is_array($extra[$type])) {
return new Paths;
}

return $paths;
return Paths::create(
new Filesystem(),
$this->composer->getConfig(),
$extra[$type]
);
}
}
Loading

0 comments on commit e459d63

Please sign in to comment.