Skip to content

Commit da33ae7

Browse files
authored
Add ability to dump usages of a package, simplify bin script (#88)
1 parent dc5a994 commit da33ae7

15 files changed

+1232
-728
lines changed

README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- ⚙️ **Configurable:** Fine-grained ignores via PHP config
66
- 🕸️ **Lightweight:** No composer dependencies
77
- 🍰 **Easy-to-use:** No config needed for first try
8-
-**Compatible:** PHP 7.2 - 8.3
8+
-**Compatible:** PHP 7.2 - 8.3
99

1010
## Comparison:
1111

@@ -48,43 +48,49 @@ Found unused dependencies!
4848
(scanned 13970 files in 2.297 s)
4949
```
5050

51-
You can add `--verbose` to see more example classes & usages.
52-
5351
## Detected issues:
5452
This tool reads your `composer.json` and scans all paths listed in `autoload` & `autoload-dev` sections while analysing:
5553

5654
### Shadowed dependencies
5755
- Those are dependencies of your dependencies, which are not listed in `composer.json`
5856
- Your code can break when your direct dependency gets updated to newer version which does not require that shadowed dependency anymore
5957
- You should list all those packages within your dependencies
60-
- Ignorable by `--ignore-shadow-deps` or more granularly by `--config`
6158

6259
### Unused dependencies
6360
- Any non-dev dependency is expected to have at least single usage within the scanned paths
6461
- To avoid false positives here, you might need to adjust scanned paths or ignore some packages by `--config`
65-
- Ignorable by `--ignore-unused-deps` or more granularly by `--config`
6662

6763
### Dev dependencies in production code
6864
- For libraries, this is risky as your users might not have those installed
6965
- For applications, it can break once you run it with `composer install --no-dev`
7066
- You should move those from `require-dev` to `require`
71-
- Ignorable by `--ignore-dev-in-prod-deps` or more granularly by `--config`
7267

7368
### Prod dependencies used only in dev paths
7469
- For libraries, this miscategorization can lead to uselessly required dependencies for your users
7570
- You should move those from `require` to `require-dev`
76-
- Ignorable by `--ignore-prod-only-in-dev-deps` or more granularly by `--config`
7771

7872
### Unknown classes
7973
- Any class that cannot be autoloaded gets reported as we cannot say if that one is shadowed or not
80-
- Ignorable by `--ignore-unknown-classes` or more granularly by `--config`
8174

82-
It is expected to run this tool in root of your project, where the `composer.json` is located.
83-
If you want to run it elsewhere, you can use `--composer-json=path/to/composer.json` option.
75+
76+
## Cli options:
77+
- `--composer-json path/to/composer.json` for custom path to composer.json
78+
- `--dump-usages symfony/console` to show usages of certain package(s), `*` placeholder is supported
79+
- `--config path/to/config.php` for custom path to config file
80+
- `--help` display usage & cli options
81+
- `--verbose` to see more example classes & usages
82+
- `--show-all-usages` to see all usages
83+
- `--ignore-unknown-classes` to globally ignore unknown classes
84+
- `--ignore-shadow-deps` to globally ignore shadow dependencies
85+
- `--ignore-unused-deps` to globally ignore unused dependencies
86+
- `--ignore-dev-in-prod-deps` to globally ignore dev dependencies in prod code
87+
- `--ignore-prod-only-in-dev-deps` to globally ignore prod dependencies used only in dev paths
88+
8489

8590
## Configuration:
86-
You can provide custom path to config file by `--config=path/to/config.php` where the config file is PHP file returning `ShipMonk\ComposerDependencyAnalyser\Config\Configuration` object.
87-
It gets loaded automatically if it is located in cwd as `composer-dependency-analyser.php`.
91+
When a file named `composer-dependency-analyser.php` is located in cwd, it gets loaded automatically.
92+
The file must return `ShipMonk\ComposerDependencyAnalyser\Config\Configuration` object.
93+
You can use custom path and filename via `--config` cli option.
8894
Here is example of what you can do:
8995

9096
```php

bin/composer-dependency-analyser

Lines changed: 24 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,18 @@
11
#!/usr/bin/env php
22
<?php declare(strict_types=1);
33

4-
use Composer\Autoload\ClassLoader;
5-
use ShipMonk\ComposerDependencyAnalyser\Cli;
64
use ShipMonk\ComposerDependencyAnalyser\Analyser;
7-
use ShipMonk\ComposerDependencyAnalyser\ComposerJson;
8-
use ShipMonk\ComposerDependencyAnalyser\Config\Configuration;
9-
use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType;
105
use ShipMonk\ComposerDependencyAnalyser\Exception\InvalidCliException;
11-
use ShipMonk\ComposerDependencyAnalyser\Exception\RuntimeException as OurRuntimeException;
6+
use ShipMonk\ComposerDependencyAnalyser\Exception\InvalidConfigException;
7+
use ShipMonk\ComposerDependencyAnalyser\Exception\InvalidPathException;
8+
use ShipMonk\ComposerDependencyAnalyser\Initializer;
129
use ShipMonk\ComposerDependencyAnalyser\Printer;
10+
use ShipMonk\ComposerDependencyAnalyser\Result\ResultFormatter;
1311
use ShipMonk\ComposerDependencyAnalyser\Stopwatch;
1412

1513
error_reporting(E_ALL);
1614
ini_set('display_errors', 'stderr');
1715

18-
$usage = <<<EOD
19-
20-
Usage:
21-
vendor/bin/composer-analyser
22-
23-
Options:
24-
--help Print this help text and exit.
25-
--verbose Print more usage examples
26-
--composer-json <path> Provide custom path to composer.json
27-
--config <path> Provide path to php configuration file
28-
(must return ShipMonk\ComposerDependencyAnalyser\Config\Configuration instance)
29-
30-
Ignore options:
31-
(or use --config for better granularity)
32-
33-
--ignore-unknown-classes Ignore when class is not found in classmap
34-
--ignore-unused-deps Ignore all unused dependency issues
35-
--ignore-shadow-deps Ignore all shadow dependency issues
36-
--ignore-dev-in-prod-deps Ignore all dev dependency in production code issues
37-
--ignore-prod-only-in-dev-deps Ignore all prod dependency used only in dev paths issues
38-
39-
40-
EOD;
41-
4216
$psr4Prefix = 'ShipMonk\\ComposerDependencyAnalyser\\';
4317

4418
// autoloader for own classes (do not rely on presence in composer's autoloader)
@@ -51,141 +25,35 @@ spl_autoload_register(static function (string $class) use ($psr4Prefix): void {
5125
}
5226
});
5327

28+
/** @var non-empty-string $cwd */
5429
$cwd = getcwd();
55-
$printer = new Printer($cwd === false ? '' : $cwd);
56-
57-
/**
58-
* @return never
59-
*/
60-
$exit = static function (string $message) use ($printer): void {
61-
$printer->printLine("\n<red>$message</red>" . PHP_EOL);
62-
exit(255);
63-
};
64-
65-
if ($cwd === false) {
66-
$exit('Cannot get current working directory');
67-
}
68-
if (!isset($argv)) {
69-
$exit('No $argv available, possibly disabled register_argc_argv?');
70-
}
7130

72-
try {
73-
$cli = new Cli();
74-
$cli->validateArgv($cwd, $argv);
75-
$providedOptions = $cli->getProvidedOptions();
76-
} catch (InvalidCliException $e) {
77-
$exit($e->getMessage());
78-
}
79-
80-
if (isset($providedOptions['help'])) {
81-
echo $usage;
82-
exit;
83-
}
84-
85-
$composerJsonPath = isset($providedOptions['composer-json'])
86-
? ($cwd . "/" . $providedOptions['composer-json'])
87-
: ($cwd . "/composer.json");
31+
$printer = new Printer();
32+
$initializer = new Initializer($cwd, $printer);
33+
$stopwatch = new Stopwatch();
8834

8935
try {
90-
$composerJson = new ComposerJson($composerJsonPath);
91-
} catch (OurRuntimeException $e) {
92-
$exit($e->getMessage());
93-
}
94-
95-
// load vendor that belongs to given composer.json
96-
$autoloadFile = $composerJson->composerAutoloadPath;
97-
if (is_file($autoloadFile)) {
98-
require_once $autoloadFile;
99-
} else {
100-
$exit("Cannot find composer's autoload file, expected at '$autoloadFile'");
101-
}
102-
103-
if (isset($providedOptions['config'])) {
104-
$configPath = $cwd . "/" . $providedOptions['config'];
105-
106-
if (!is_file($configPath)) {
107-
$exit("Invalid config path given, {$configPath} is not a file.");
108-
}
109-
} else {
110-
$configPath = $cwd . "/composer-dependency-analyser.php";
111-
}
36+
$options = $initializer->initCliOptions($cwd, $argv);
37+
$composerJson = $initializer->initComposerJson($cwd, $options);
38+
$initializer->initComposerAutoloader($composerJson);
39+
$configuration = $initializer->initConfiguration($options, $composerJson);
40+
$classLoaders = $initializer->initComposerClassLoaders();
11241

113-
if (is_file($configPath)) {
114-
$printer->printLine('<gray>Using config</gray> ' . $configPath);
115-
116-
try {
117-
$config = require $configPath;
118-
} catch (OurRuntimeException $e) {
119-
$exit($e->getMessage());
120-
} catch (Throwable $e) {
121-
$exit(get_class($e) . " in {$e->getFile()}:{$e->getLine()}\n > " . $e->getMessage());
122-
}
123-
124-
if (!$config instanceof Configuration) {
125-
$exit("Invalid config file, it must return instance of " . Configuration::class);
126-
}
127-
} else {
128-
$config = new Configuration();
129-
}
130-
131-
$ignoreUnknown = isset($providedOptions['ignore-unknown-classes']);
132-
$ignoreUnused = isset($providedOptions['ignore-unused-deps']);
133-
$ignoreShadow = isset($providedOptions['ignore-shadow-deps']);
134-
$ignoreDevInProd = isset($providedOptions['ignore-dev-in-prod-deps']);
135-
$ignoreProdOnlyInDev = isset($providedOptions['ignore-prod-only-in-dev-deps']);
136-
137-
if ($ignoreUnknown) {
138-
$config->ignoreErrors([ErrorType::UNKNOWN_CLASS]);
139-
}
140-
if ($ignoreUnused) {
141-
$config->ignoreErrors([ErrorType::UNUSED_DEPENDENCY]);
142-
}
143-
if ($ignoreShadow) {
144-
$config->ignoreErrors([ErrorType::SHADOW_DEPENDENCY]);
145-
}
146-
if ($ignoreDevInProd) {
147-
$config->ignoreErrors([ErrorType::DEV_DEPENDENCY_IN_PROD]);
148-
}
149-
if ($ignoreProdOnlyInDev) {
150-
$config->ignoreErrors([ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]);
151-
}
152-
153-
$loaders = ClassLoader::getRegisteredLoaders();
154-
if (count($loaders) > 1) {
155-
$printer->printLine("\nDetected multiple class loaders:");
156-
foreach ($loaders as $vendorDir => $_) {
157-
$printer->printLine(" • <gray>$vendorDir</gray>");
158-
}
159-
$printer->printLine('');
160-
}
161-
162-
if (count($loaders) === 0) {
163-
$printer->printLine("\nNo composer class loader detected!\n");
164-
}
165-
166-
try {
167-
if ($config->shouldScanComposerAutoloadPaths()) {
168-
foreach ($composerJson->autoloadPaths as $absolutePath => $isDevPath) {
169-
$config->addPathToScan($absolutePath, $isDevPath);
170-
}
42+
$analyser = new Analyser($stopwatch, $classLoaders, $configuration, $composerJson->dependencies);
43+
$result = $analyser->run();
17144

172-
if ($config->getPathsToScan() === []) {
173-
$exit('No paths to scan! There is no composer autoload section and no extra path to scan configured.');
174-
}
175-
} else {
176-
if ($config->getPathsToScan() === []) {
177-
$exit('No paths to scan! Scanning composer\'s \'autoload\' sections is disabled and no extra path to scan was configured.');
178-
}
179-
}
45+
$formatter = new ResultFormatter($cwd, $printer);
46+
$exitCode = $formatter->format($result, $options, $configuration);
18047

181-
$stopwatch = new Stopwatch();
182-
$analyser = new Analyser($stopwatch, $loaders, $config, $composerJson->dependencies);
183-
$result = $analyser->run();
184-
} catch (OurRuntimeException $e) {
185-
$exit($e->getMessage());
48+
} catch (
49+
InvalidPathException |
50+
InvalidConfigException |
51+
InvalidCliException $e
52+
) {
53+
$printer->printLine("\n<red>{$e->getMessage()}</red>" . PHP_EOL);
54+
exit(255);
18655
}
18756

188-
$exitCode = $printer->printResult($result, isset($providedOptions['verbose']), $config->shouldReportUnmatchedIgnoredErrors());
18957
exit($exitCode);
19058

19159

src/Analyser.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@
3333
use function implode;
3434
use function in_array;
3535
use function is_file;
36-
use function ksort;
3736
use function preg_split;
38-
use function sort;
3937
use function str_replace;
4038
use function strlen;
4139
use function strpos;
@@ -124,6 +122,8 @@ public function run(): AnalysisResult
124122
$usedPackages = [];
125123
$prodPackagesUsedInProdPath = [];
126124

125+
$usages = [];
126+
127127
$ignoreList = $this->config->getIgnoreList();
128128

129129
foreach ($this->getUniqueFilePathsToScan() as $filePath => $isDevFilePath) {
@@ -179,6 +179,10 @@ public function run(): AnalysisResult
179179
}
180180

181181
$usedPackages[$packageName] = true;
182+
183+
foreach ($lineNumbers as $lineNumber) {
184+
$usages[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber);
185+
}
182186
}
183187
}
184188

@@ -235,15 +239,10 @@ public function run(): AnalysisResult
235239
}
236240
}
237241

238-
ksort($classmapErrors);
239-
ksort($shadowErrors);
240-
ksort($devInProdErrors);
241-
sort($prodOnlyInDevErrors);
242-
sort($unusedErrors);
243-
244242
return new AnalysisResult(
245243
$scannedFilesCount,
246244
$this->stopwatch->stop(),
245+
$usages,
247246
$classmapErrors,
248247
$shadowErrors,
249248
$devInProdErrors,

0 commit comments

Comments
 (0)