Skip to content

Commit 41d19aa

Browse files
authored
Configuration: allow extra paths, granual ignores etc. (#8)
1 parent 3188b66 commit 41d19aa

19 files changed

+879
-109
lines changed

README.md

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@ Example output:
3636
Found shadow dependencies!
3737
(those are used, but not listed as dependency in composer.json)
3838
39-
• Nette\Utils\Json (nette/utils)
40-
• Nette\Utils\JsonException (nette/utils)
41-
• Symfony\Contracts\Service\Attribute\Required (symfony/service-contracts)
39+
• symfony/service-contracts
40+
e.g. Symfony\Contracts\Service\Attribute\Required in app/Controller/ProductController.php:24
4241
43-
```
42+
Found unused dependencies!
43+
(those are listed in composer.json, but no usage was found in scanned paths)
44+
45+
• nette/utils
4446
45-
You can add `--verbose` flag to see first usage (file & line) of each class.
47+
```
4648

4749
## Detected issues:
4850
This tool reads your `composer.json` and scans all paths listed in both `autoload` sections while analysing:
@@ -51,20 +53,76 @@ This tool reads your `composer.json` and scans all paths listed in both `autoloa
5153
- Those are dependencies of your dependencies, which are not listed in `composer.json`
5254
- Your code can break when your direct dependency gets updated to newer version which does not require that shadowed dependency anymore
5355
- You should list all those classes within your dependencies
56+
5457
- **Unused dependencies**
5558
- Any non-dev dependency is expected to have at least single usage within the scanned paths
59+
- To avoid false positives here, you might need to adjust scanned paths or ignore some packages by `--config`
60+
5661
- **Dev dependencies in production code**
57-
- Your code can break once you run your application with `composer install --no-dev`
58-
- You should move those to `require` from `require-dev`
62+
- For libraries, this is risky as your users might not have those installed
63+
- For applications, it can break once you run it with `composer install --no-dev`
64+
- You should move those from `require-dev` to `require`
65+
- If you want to ignore some packages here, use `--config`
66+
5967
- **Unknown classes**
6068
- Any class missing in composer classmap gets reported as we cannot say if that one is shadowed or not
61-
- This might be expected in some cases, so you can disable this behaviour by `--ignore-unknown-classes`
69+
- This might be expected in some cases, so you can disable this behaviour by `--ignore-unknown-classes` or more granularly by `--config`
6270

6371
It is expected to run this tool in root of your project, where the `composer.json` is located.
6472
If you want to run it elsewhere, you can use `--composer-json=path/to/composer.json` option.
6573

6674
Currently, it only supports those autoload sections: `psr-4`, `psr-0`, `files`.
6775

76+
## Configuration:
77+
You can provide custom path to config file by `--config=path/to/config.php` where the config file is PHP file returning `ShipMonk\Composer\Config\Configuration` object.
78+
Here is example of what you can do:
79+
80+
```php
81+
<?php
82+
83+
use ShipMonk\Composer\Config\Configuration;
84+
use ShipMonk\Composer\Config\ErrorType;
85+
86+
$config = new Configuration();
87+
88+
return $config
89+
// disable scanning autoload & autoload-dev paths from composer.json
90+
// with such option, you should add custom paths by addPathToScan() or addPathsToScan()
91+
->disableComposerAutoloadPathScan()
92+
93+
// disable detection of dev dependencies in production code globally
94+
->ignoreErrors([ErrorType::DEV_DEPENDENCY_IN_PROD])
95+
96+
// overwrite file extensions to scan, defaults to 'php'
97+
->setFileExtensions(['php'])
98+
99+
// add extra path to scan
100+
// for multiple paths at once, use addPathsToScan()
101+
->addPathToScan(__DIR__ . '/build', isDev: false)
102+
103+
// ignore errors on specific paths
104+
// this can be handy when DIC container file was passed as extra path, but you want to ignore shadow dependencies there
105+
// for multiple paths at once, use ignoreErrorsOnPaths()
106+
->ignoreErrorsOnPath(__DIR__ . '/cache/DIC.php', [ErrorType::SHADOW_DEPENDENCY])
107+
108+
// ignore errors on specific packages
109+
// you might have various reasons to ignore certain errors
110+
// e.g. polyfills are often used in libraries, but those are obviously unused when running with latest PHP
111+
// for multiple packages at once, use ignoreErrorsOnPackages()
112+
->ignoreErrorsOnPackage('symfony/polyfill-php73', [ErrorType::UNUSED_DEPENDENCY])
113+
114+
// allow using classes not present in composer's autoloader
115+
// e.g. a library may conditionally support some feature only when Memcached is available
116+
->ignoreUnknownClasses(['Memcached'])
117+
118+
// allow using classes not present in composer's autoloader by regex
119+
// e.g. when you want to ignore whole namespace of classes
120+
->ignoreUnknownClassesRegex('~^PHPStan\\.*?~')
121+
;
122+
```
123+
124+
All paths are expected to exist. If you need some glob functionality, you can do it in your config file and pass the expanded list to e.g. `ignoreErrorsOnPaths`.
125+
68126
## Limitations:
69127
- Files without namespace has limited support
70128
- Only classes with use statements and FQNs are detected

bin/composer-dependency-analyser

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@
44
use Composer\Autoload\ClassLoader;
55
use ShipMonk\Composer\ComposerDependencyAnalyser;
66
use ShipMonk\Composer\ComposerJson;
7+
use ShipMonk\Composer\Config\Configuration;
8+
use ShipMonk\Composer\Config\ErrorType;
79
use ShipMonk\Composer\Printer;
810

911
$usage = <<<EOD
1012
1113
Usage:
12-
vendor/bin/composer-analyser dir-to-scan
14+
vendor/bin/composer-analyser
1315
1416
Options:
1517
--help Print this help text and exit.
16-
--verbose Print verbose output
1718
--ignore-unknown-classes Ignore when class is not found in classmap
1819
--composer-json <path> Provide custom path to composer.json
20+
--config <path> Provide path to php configuration file
21+
(must return ShipMonk\Composer\Config\Configuration instance)
1922
2023
EOD;
2124

@@ -41,24 +44,44 @@ $exit = static function (string $message) use ($printer): void {
4144
exit(255);
4245
};
4346

44-
/** @var int $restIndex */
45-
$providedOptions = getopt('', ['help', 'verbose', 'ignore-unknown-classes', 'composer-json:'], $restIndex);
47+
/** @var string[] $providedOptions */
48+
$providedOptions = getopt('', ['help', 'ignore-unknown-classes', 'composer-json:', 'config:'], $restIndex);
4649

47-
$cwd = getcwd();
50+
/** @var int $restIndex */
4851
$providedPaths = array_slice($argv, $restIndex);
52+
$cwd = getcwd();
4953

5054
if (isset($providedOptions['help'])) {
5155
echo $usage;
5256
exit;
5357
}
5458

55-
$verbose = isset($providedOptions['verbose']);
59+
if (isset($providedOptions['config'])) {
60+
$printer->printLine('Using config ' . $providedOptions['config'] . PHP_EOL);
61+
62+
$configPath = $cwd . "/" . $providedOptions['config'];
63+
if (!is_file($configPath)) {
64+
$exit("Invalid config path given, {$providedOptions['config']} is not a file.");
65+
}
66+
67+
$config = require $configPath;
68+
69+
if (!$config instanceof Configuration) {
70+
$exit("Invalid config file, must return instance of " . Configuration::class);
71+
}
72+
} else {
73+
$config = new Configuration();
74+
}
75+
5676
$ignoreUnknown = isset($providedOptions['ignore-unknown-classes']);
5777

78+
if ($ignoreUnknown) {
79+
$config->ignoreErrors([ErrorType::UNKNOWN_CLASS]);
80+
}
81+
5882
/** @var non-empty-string $cwd */
5983
$cwd = getcwd();
6084

61-
/** @var string[] $providedOptions */
6285
$composerJsonPath = isset($providedOptions['composer-json'])
6386
? ($cwd . "/" . $providedOptions['composer-json'])
6487
: ($cwd . "/composer.json");
@@ -97,19 +120,43 @@ if (!$loaders[$vendorDir]->isClassMapAuthoritative()) {
97120
$exit('Run \'composer dump-autoload --classmap-authoritative\' first');
98121
}
99122

100-
$absolutePaths = [];
101-
foreach ($composerJson->autoloadPaths as $relativePath => $isDevPath) {
102-
$absolutePath = dirname($composerJsonPath) . '/' . $relativePath;
103-
if (!is_dir($absolutePath) && !is_file($absolutePath)) {
104-
$exit("Unexpected path detected, $absolutePath is not a file nor directory.");
123+
if ($config->shouldScanComposerAutoloadPaths()) {
124+
foreach ($composerJson->autoloadPaths as $relativePath => $isDevPath) {
125+
$absolutePath = dirname($composerJsonPath) . '/' . $relativePath;
126+
$config->addPathToScan($absolutePath, $isDevPath);
127+
}
128+
129+
if ($config->getPathsToScan() === []) {
130+
$exit('No paths to scan! There is no composer autoload section and no extra path to scan configured.');
131+
}
132+
} else {
133+
if ($config->getPathsToScan() === []) {
134+
$exit('No paths to scan! Scanning composer\'s \'autoload\' sections is disabled and no extra path to scan was configured.');
135+
}
136+
}
137+
138+
foreach ($config->getPathsToScan() as $pathToScan) {
139+
if (!is_dir($pathToScan->getPath()) && !is_file($pathToScan->getPath())) {
140+
$exit("Invalid scan path given, {$pathToScan->getPath()} is not a file nor directory.");
141+
}
142+
}
143+
144+
foreach ($config->getPathsToExclude() as $pathToExclude) {
145+
if (!is_dir($pathToExclude) && !is_file($pathToExclude)) {
146+
$exit("Invalid exclude path given, {$pathToExclude} is not a file nor directory.");
147+
}
148+
}
149+
150+
foreach ($config->getPathsWithIgnore() as $pathWithIgnore) {
151+
if (!is_dir($pathWithIgnore) && !is_file($pathWithIgnore)) {
152+
$exit("Invalid ignore path given, {$pathWithIgnore} is not a file nor directory.");
105153
}
106-
$absolutePaths[$absolutePath] = $isDevPath;
107154
}
108155

109-
$detector = new ComposerDependencyAnalyser($vendorDir, $loaders[$vendorDir]->getClassMap(), $composerJson->dependencies, ['php']);
110-
$errors = $detector->scan($absolutePaths);
156+
$analyser = new ComposerDependencyAnalyser($config, $vendorDir, $loaders[$vendorDir]->getClassMap(), $composerJson->dependencies);
157+
$errors = $analyser->run();
111158

112-
$exitCode = $printer->printResult(array_values($errors), $ignoreUnknown, $verbose);
159+
$exitCode = $printer->printResult($errors);
113160
exit($exitCode);
114161

115162

composer.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpcs.xml.dist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,6 @@
407407
<rule ref="SlevomatCodingStandard.ControlStructures.AssignmentInCondition"/>
408408
<rule ref="SlevomatCodingStandard.ControlStructures.BlockControlStructureSpacing"/>
409409
<rule ref="SlevomatCodingStandard.ControlStructures.DisallowContinueWithoutIntegerOperandInSwitch"/>
410-
<rule ref="SlevomatCodingStandard.ControlStructures.UselessIfConditionWithReturn"/>
411410
<rule ref="SlevomatCodingStandard.ControlStructures.UselessTernaryOperator"/>
412411
<rule ref="SlevomatCodingStandard.ControlStructures.EarlyExit">
413412
<exclude name="SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed"/> <!-- leads to less readable code in some cases -->

0 commit comments

Comments
 (0)