Skip to content

Commit 755c94f

Browse files
authored
Detect function usages (#71)
1 parent 1721e2a commit 755c94f

27 files changed

+736
-333
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ This tool reads your `composer.json` and scans all paths listed in `autoload` &
7272
### Unknown classes
7373
- Any class that cannot be autoloaded gets reported as we cannot say if that one is shadowed or not
7474

75+
### Unknown functions
76+
- Any function that is used, but not defined gets reported as we cannot say if that one is shadowed or not
7577

7678
## Cli options:
7779
- `--composer-json path/to/composer.json` for custom path to composer.json
@@ -82,6 +84,7 @@ This tool reads your `composer.json` and scans all paths listed in `autoload` &
8284
- `--show-all-usages` to see all usages
8385
- `--format` to use different output format, available are: console (default), junit
8486
- `--ignore-unknown-classes` to globally ignore unknown classes
87+
- `--ignore-unknown-functions` to globally ignore unknown functions
8588
- `--ignore-shadow-deps` to globally ignore shadow dependencies
8689
- `--ignore-unused-deps` to globally ignore unused dependencies
8790
- `--ignore-dev-in-prod-deps` to globally ignore dev dependencies in prod code
@@ -185,9 +188,7 @@ Another approach for DIC-only usages is to scan the generated php file, but that
185188
## Limitations:
186189
- Extension dependencies are not analysed (e.g. `ext-json`)
187190
- Files without namespace has limited support
188-
- Only classes with use statements and FQNs are detected
189-
- Function and constant usages are not analysed
190-
- Therefore, if some package contains only functions, it will be reported as unused
191+
- Only symbols with use statements and FQNs are detected
191192

192193
-----
193194

src/Analyser.php

Lines changed: 93 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
use RecursiveIteratorIterator;
1111
use ReflectionClass;
1212
use ReflectionException;
13+
use ReflectionFunction;
1314
use ShipMonk\ComposerDependencyAnalyser\Config\Configuration;
1415
use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType;
1516
use ShipMonk\ComposerDependencyAnalyser\Exception\InvalidPathException;
1617
use ShipMonk\ComposerDependencyAnalyser\Result\AnalysisResult;
1718
use ShipMonk\ComposerDependencyAnalyser\Result\SymbolUsage;
1819
use UnexpectedValueException;
19-
use function array_change_key_case;
2020
use function array_diff;
2121
use function array_filter;
2222
use function array_key_exists;
@@ -30,13 +30,13 @@
3030
use function get_defined_functions;
3131
use function in_array;
3232
use function is_file;
33+
use function is_string;
3334
use function str_replace;
3435
use function strlen;
3536
use function strpos;
3637
use function strtolower;
3738
use function substr;
3839
use function trim;
39-
use const CASE_LOWER;
4040
use const DIRECTORY_SEPARATOR;
4141

4242
class Analyser
@@ -80,6 +80,13 @@ class Analyser
8080
*/
8181
private $ignoredSymbols;
8282

83+
/**
84+
* function name => path
85+
*
86+
* @var array<string, string>
87+
*/
88+
private $definedFunctions = [];
89+
8390
/**
8491
* @param array<string, ClassLoader> $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders())
8592
* @param array<string, bool> $composerJsonDependencies package name => is dev dependency
@@ -94,7 +101,8 @@ public function __construct(
94101
$this->stopwatch = $stopwatch;
95102
$this->config = $config;
96103
$this->composerJsonDependencies = $composerJsonDependencies;
97-
$this->ignoredSymbols = $this->getIgnoredSymbols();
104+
105+
$this->initExistingSymbols();
98106

99107
foreach ($classLoaders as $vendorDir => $classLoader) {
100108
$this->classLoaders[$vendorDir] = $classLoader;
@@ -109,7 +117,8 @@ public function run(): AnalysisResult
109117
$this->stopwatch->start();
110118

111119
$scannedFilesCount = 0;
112-
$classmapErrors = [];
120+
$unknownClassErrors = [];
121+
$unknownFunctionErrors = [];
113122
$shadowErrors = [];
114123
$devInProdErrors = [];
115124
$prodOnlyInDevErrors = [];
@@ -125,59 +134,69 @@ public function run(): AnalysisResult
125134
foreach ($this->getUniqueFilePathsToScan() as $filePath => $isDevFilePath) {
126135
$scannedFilesCount++;
127136

128-
foreach ($this->getUsedSymbolsInFile($filePath) as $usedSymbol => $lineNumbers) {
129-
if (isset($this->ignoredSymbols[strtolower($usedSymbol)])) {
130-
continue;
131-
}
137+
$usedSymbolsByKind = $this->getUsedSymbolsInFile($filePath);
132138

133-
$symbolPath = $this->getSymbolPath($usedSymbol);
139+
foreach ($usedSymbolsByKind as $kind => $usedSymbols) {
140+
foreach ($usedSymbols as $usedSymbol => $lineNumbers) {
141+
if (isset($this->ignoredSymbols[$usedSymbol])) {
142+
continue;
143+
}
134144

135-
if ($symbolPath === null) {
136-
if (!$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) {
137-
foreach ($lineNumbers as $lineNumber) {
138-
$classmapErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber);
145+
$symbolPath = $this->getSymbolPath($usedSymbol, $kind);
146+
147+
if ($symbolPath === null) {
148+
if ($kind === SymbolKind::CLASSLIKE && !$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) {
149+
foreach ($lineNumbers as $lineNumber) {
150+
$unknownClassErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
151+
}
139152
}
140-
}
141153

142-
continue;
143-
}
154+
if ($kind === SymbolKind::FUNCTION && !$ignoreList->shouldIgnoreUnknownFunction($usedSymbol, $filePath)) {
155+
foreach ($lineNumbers as $lineNumber) {
156+
$unknownFunctionErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
157+
}
158+
}
144159

145-
if (!$this->isVendorPath($symbolPath)) {
146-
continue; // local class
147-
}
160+
continue;
161+
}
148162

149-
$packageName = $this->getPackageNameFromVendorPath($symbolPath);
163+
if (!$this->isVendorPath($symbolPath)) {
164+
continue; // local class
165+
}
150166

151-
if (
152-
$this->isShadowDependency($packageName)
153-
&& !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $packageName)
154-
) {
155-
foreach ($lineNumbers as $lineNumber) {
156-
$shadowErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber);
167+
$packageName = $this->getPackageNameFromVendorPath($symbolPath);
168+
169+
if (
170+
$this->isShadowDependency($packageName)
171+
&& !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $packageName)
172+
) {
173+
foreach ($lineNumbers as $lineNumber) {
174+
$shadowErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
175+
}
157176
}
158-
}
159177

160-
if (
161-
!$isDevFilePath
162-
&& $this->isDevDependency($packageName)
163-
&& !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $packageName)
164-
) {
165-
foreach ($lineNumbers as $lineNumber) {
166-
$devInProdErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber);
178+
if (
179+
!$isDevFilePath
180+
&& $this->isDevDependency($packageName)
181+
&& !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $packageName)
182+
) {
183+
foreach ($lineNumbers as $lineNumber) {
184+
$devInProdErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
185+
}
167186
}
168-
}
169187

170-
if (
171-
!$isDevFilePath
172-
&& !$this->isDevDependency($packageName)
173-
) {
174-
$prodPackagesUsedInProdPath[$packageName] = true;
175-
}
188+
if (
189+
!$isDevFilePath
190+
&& !$this->isDevDependency($packageName)
191+
) {
192+
$prodPackagesUsedInProdPath[$packageName] = true;
193+
}
176194

177-
$usedPackages[$packageName] = true;
195+
$usedPackages[$packageName] = true;
178196

179-
foreach ($lineNumbers as $lineNumber) {
180-
$usages[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber);
197+
foreach ($lineNumbers as $lineNumber) {
198+
$usages[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
199+
}
181200
}
182201
}
183202
}
@@ -189,7 +208,7 @@ public function run(): AnalysisResult
189208
continue;
190209
}
191210

192-
$symbolPath = $this->getSymbolPath($forceUsedSymbol);
211+
$symbolPath = $this->getSymbolPath($forceUsedSymbol, null);
193212

194213
if ($symbolPath === null || !$this->isVendorPath($symbolPath)) {
195214
continue;
@@ -239,7 +258,8 @@ public function run(): AnalysisResult
239258
$scannedFilesCount,
240259
$this->stopwatch->stop(),
241260
$usages,
242-
$classmapErrors,
261+
$unknownClassErrors,
262+
$unknownFunctionErrors,
243263
$shadowErrors,
244264
$devInProdErrors,
245265
$prodOnlyInDevErrors,
@@ -297,7 +317,7 @@ private function getPackageNameFromVendorPath(string $realPath): string
297317
}
298318

299319
/**
300-
* @return array<string, list<int>>
320+
* @return array<SymbolKind::*, array<string, list<int>>>
301321
* @throws InvalidPathException
302322
*/
303323
private function getUsedSymbolsInFile(string $filePath): array
@@ -308,7 +328,7 @@ private function getUsedSymbolsInFile(string $filePath): array
308328
throw new InvalidPathException("Unable to get contents of '$filePath'");
309329
}
310330

311-
return (new UsedSymbolExtractor($code))->parseUsedClasses();
331+
return (new UsedSymbolExtractor($code))->parseUsedSymbols();
312332
}
313333

314334
/**
@@ -349,8 +369,20 @@ private function isVendorPath(string $realPath): bool
349369
return false;
350370
}
351371

352-
private function getSymbolPath(string $symbol): ?string
372+
private function getSymbolPath(string $symbol, ?int $kind): ?string
353373
{
374+
if ($kind === SymbolKind::FUNCTION || $kind === null) {
375+
$lowerSymbol = strtolower($symbol);
376+
377+
if (isset($this->definedFunctions[$lowerSymbol])) {
378+
return $this->definedFunctions[$lowerSymbol];
379+
}
380+
381+
if ($kind === SymbolKind::FUNCTION) {
382+
return null;
383+
}
384+
}
385+
354386
if (!array_key_exists($symbol, $this->classmap)) {
355387
$path = $this->detectFileByClassLoader($symbol) ?? $this->detectFileByReflection($symbol);
356388
$this->classmap[$symbol] = $path === null
@@ -406,12 +438,9 @@ private function normalizePath(string $filePath): string
406438
return Path::normalize($filePath);
407439
}
408440

409-
/**
410-
* @return array<string, true>
411-
*/
412-
private function getIgnoredSymbols(): array
441+
private function initExistingSymbols(): void
413442
{
414-
$ignoredSymbols = [
443+
$this->ignoredSymbols = [
415444
// built-in types
416445
'bool' => true,
417446
'int' => true,
@@ -446,12 +475,19 @@ private function getIgnoredSymbols(): array
446475

447476
/** @var string $constantName */
448477
foreach (get_defined_constants() as $constantName => $constantValue) {
449-
$ignoredSymbols[$constantName] = true;
478+
$this->ignoredSymbols[$constantName] = true;
450479
}
451480

452481
foreach (get_defined_functions() as $functionNames) {
453482
foreach ($functionNames as $functionName) {
454-
$ignoredSymbols[$functionName] = true;
483+
$reflectionFunction = new ReflectionFunction($functionName);
484+
$functionFilePath = $reflectionFunction->getFileName();
485+
486+
if ($reflectionFunction->getExtension() === null && is_string($functionFilePath)) {
487+
$this->definedFunctions[$functionName] = Path::normalize($functionFilePath);
488+
} else {
489+
$this->ignoredSymbols[$functionName] = true;
490+
}
455491
}
456492
}
457493

@@ -464,12 +500,10 @@ private function getIgnoredSymbols(): array
464500
foreach ($classLikes as $classLikeNames) {
465501
foreach ($classLikeNames as $classLikeName) {
466502
if ((new ReflectionClass($classLikeName))->getExtension() !== null) {
467-
$ignoredSymbols[$classLikeName] = true;
503+
$this->ignoredSymbols[$classLikeName] = true;
468504
}
469505
}
470506
}
471-
472-
return array_change_key_case($ignoredSymbols, CASE_LOWER); // get_defined_functions returns lowercase functions
473507
}
474508

475509
}

src/Cli.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class Cli
2020
'ignore-dev-in-prod-deps' => false,
2121
'ignore-prod-only-in-dev-deps' => false,
2222
'ignore-unknown-classes' => false,
23+
'ignore-unknown-functions' => false,
24+
'ignore-unknown-symbols' => false,
2325
'composer-json' => true,
2426
'config' => true,
2527
'dump-usages' => true,
@@ -152,6 +154,10 @@ public function getProvidedOptions(): CliOptions
152154
$options->ignoreUnknownClasses = true;
153155
}
154156

157+
if (isset($this->providedOptions['ignore-unknown-functions'])) {
158+
$options->ignoreUnknownFunctions = true;
159+
}
160+
155161
if (isset($this->providedOptions['composer-json'])) {
156162
$options->composerJson = $this->providedOptions['composer-json']; // @phpstan-ignore-line type is ensured
157163
}

src/CliOptions.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class CliOptions
4040
*/
4141
public $ignoreUnknownClasses = null;
4242

43+
/**
44+
* @var true|null
45+
*/
46+
public $ignoreUnknownFunctions = null;
47+
4348
/**
4449
* @var string|null
4550
*/

0 commit comments

Comments
 (0)