Skip to content

Commit ae5d04e

Browse files
committed
Support ext-*
1 parent 77f3be3 commit ae5d04e

File tree

9 files changed

+280
-59
lines changed

9 files changed

+280
-59
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ NO_COLOR=1 vendor/bin/composer-dependency-analyser
167167
```
168168

169169
## Limitations:
170-
- Extension dependencies are not analysed (e.g. `ext-json`)
170+
- For precise `ext-x` analysis, your enabled extentions of your php runtime should be superset of those used in the scanned project
171171

172172
## Contributing:
173173
- Check your code by `composer check`

src/Analyser.php

+128-46
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@
4545
class Analyser
4646
{
4747

48+
/**
49+
* Those are core PHP extensions, that can never be disabled
50+
* There are more PHP "core" extensions, that are bundled by default, but PHP can be compiled without them
51+
* You can check which are added conditionally in https://github.com/php/php-src/tree/master/ext (see config.w32 files)
52+
*/
53+
private const CORE_EXTENSIONS = [
54+
'ext-core',
55+
'ext-date',
56+
'ext-json',
57+
'ext-hash',
58+
'ext-pcre',
59+
'ext-phar',
60+
'ext-reflection',
61+
'ext-spl',
62+
'ext-random',
63+
'ext-standard',
64+
];
65+
4866
/**
4967
* @var Stopwatch
5068
*/
@@ -73,7 +91,7 @@ class Analyser
7391
private $classmap = [];
7492

7593
/**
76-
* package name => is dev dependency
94+
* package or ext-* => is dev dependency
7795
*
7896
* @var array<string, bool>
7997
*/
@@ -87,15 +105,22 @@ class Analyser
87105
private $ignoredSymbols;
88106

89107
/**
90-
* function name => path
108+
* custom function name => path
91109
*
92110
* @var array<string, string>
93111
*/
94112
private $definedFunctions = [];
95113

114+
/**
115+
* kind => [symbol name => ext-*]
116+
*
117+
* @var array<SymbolKind::*, array<string, string>>
118+
*/
119+
private $extensionSymbols;
120+
96121
/**
97122
* @param array<string, ClassLoader> $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders())
98-
* @param array<string, bool> $composerJsonDependencies package name => is dev dependency
123+
* @param array<string, bool> $composerJsonDependencies package or ext-* => is dev dependency
99124
*/
100125
public function __construct(
101126
Stopwatch $stopwatch,
@@ -129,7 +154,7 @@ public function run(): AnalysisResult
129154
$prodOnlyInDevErrors = [];
130155
$unusedErrors = [];
131156

132-
$usedPackages = [];
157+
$usedDependencies = [];
133158
$prodPackagesUsedInProdPath = [];
134159

135160
$usages = [];
@@ -149,60 +174,65 @@ public function run(): AnalysisResult
149174
continue;
150175
}
151176

152-
$symbolPath = $this->getSymbolPath($usedSymbol, $kind);
177+
if (isset($this->extensionSymbols[$kind][$usedSymbol])) {
178+
$dependencyName = $this->extensionSymbols[$kind][$usedSymbol];
153179

154-
if ($symbolPath === null) {
155-
if ($kind === SymbolKind::CLASSLIKE && !$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) {
156-
foreach ($lineNumbers as $lineNumber) {
157-
$unknownClassErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
180+
} else {
181+
$symbolPath = $this->getSymbolPath($usedSymbol, $kind);
182+
183+
if ($symbolPath === null) {
184+
if ($kind === SymbolKind::CLASSLIKE && !$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) {
185+
foreach ($lineNumbers as $lineNumber) {
186+
$unknownClassErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
187+
}
158188
}
159-
}
160189

161-
if ($kind === SymbolKind::FUNCTION && !$ignoreList->shouldIgnoreUnknownFunction($usedSymbol, $filePath)) {
162-
foreach ($lineNumbers as $lineNumber) {
163-
$unknownFunctionErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
190+
if ($kind === SymbolKind::FUNCTION && !$ignoreList->shouldIgnoreUnknownFunction($usedSymbol, $filePath)) {
191+
foreach ($lineNumbers as $lineNumber) {
192+
$unknownFunctionErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
193+
}
164194
}
195+
196+
continue;
165197
}
166198

167-
continue;
168-
}
199+
if (!$this->isVendorPath($symbolPath)) {
200+
continue; // local class
201+
}
169202

170-
if (!$this->isVendorPath($symbolPath)) {
171-
continue; // local class
203+
$dependencyName = $this->getPackageNameFromVendorPath($symbolPath);
172204
}
173205

174-
$packageName = $this->getPackageNameFromVendorPath($symbolPath);
175-
176206
if (
177-
$this->isShadowDependency($packageName)
178-
&& !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $packageName)
207+
$this->isShadowDependency($dependencyName)
208+
&& !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $dependencyName)
179209
) {
180210
foreach ($lineNumbers as $lineNumber) {
181-
$shadowErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
211+
$shadowErrors[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
182212
}
183213
}
184214

185215
if (
186216
!$isDevFilePath
187-
&& $this->isDevDependency($packageName)
188-
&& !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $packageName)
217+
&& $this->isDevDependency($dependencyName)
218+
&& !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $dependencyName)
189219
) {
190220
foreach ($lineNumbers as $lineNumber) {
191-
$devInProdErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
221+
$devInProdErrors[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
192222
}
193223
}
194224

195225
if (
196226
!$isDevFilePath
197-
&& !$this->isDevDependency($packageName)
227+
&& !$this->isDevDependency($dependencyName)
198228
) {
199-
$prodPackagesUsedInProdPath[$packageName] = true;
229+
$prodPackagesUsedInProdPath[$dependencyName] = true;
200230
}
201231

202-
$usedPackages[$packageName] = true;
232+
$usedDependencies[$dependencyName] = true;
203233

204234
foreach ($lineNumbers as $lineNumber) {
205-
$usages[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
235+
$usages[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind);
206236
}
207237
}
208238
}
@@ -215,19 +245,31 @@ public function run(): AnalysisResult
215245
continue;
216246
}
217247

218-
$symbolPath = $this->getSymbolPath($forceUsedSymbol, null);
248+
if (
249+
isset($this->extensionSymbols[SymbolKind::FUNCTION][$forceUsedSymbol])
250+
|| isset($this->extensionSymbols[SymbolKind::CONSTANT][$forceUsedSymbol])
251+
|| isset($this->extensionSymbols[SymbolKind::CLASSLIKE][$forceUsedSymbol])
252+
) {
253+
$forceUsedDependency = $this->extensionSymbols[SymbolKind::FUNCTION][$forceUsedSymbol]
254+
?? $this->extensionSymbols[SymbolKind::CONSTANT][$forceUsedSymbol]
255+
?? $this->extensionSymbols[SymbolKind::CLASSLIKE][$forceUsedSymbol];
256+
} else {
257+
$symbolPath = $this->getSymbolPath($forceUsedSymbol, null);
258+
259+
if ($symbolPath === null || !$this->isVendorPath($symbolPath)) {
260+
continue;
261+
}
219262

220-
if ($symbolPath === null || !$this->isVendorPath($symbolPath)) {
221-
continue;
263+
$forceUsedDependency = $this->getPackageNameFromVendorPath($symbolPath);
222264
}
223265

224-
$forceUsedPackage = $this->getPackageNameFromVendorPath($symbolPath);
225-
$usedPackages[$forceUsedPackage] = true;
226-
$forceUsedPackages[$forceUsedPackage] = true;
266+
$usedDependencies[$forceUsedDependency] = true;
267+
$forceUsedPackages[$forceUsedDependency] = true;
227268
}
228269

229270
if ($this->config->shouldReportUnusedDevDependencies()) {
230271
$dependenciesForUnusedAnalysis = array_keys($this->composerJsonDependencies);
272+
231273
} else {
232274
$dependenciesForUnusedAnalysis = array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) {
233275
return !$devDependency; // dev deps are typically used only in CI
@@ -236,7 +278,8 @@ public function run(): AnalysisResult
236278

237279
$unusedDependencies = array_diff(
238280
$dependenciesForUnusedAnalysis,
239-
array_keys($usedPackages)
281+
array_keys($usedDependencies),
282+
self::CORE_EXTENSIONS
240283
);
241284

242285
foreach ($unusedDependencies as $unusedDependency) {
@@ -252,7 +295,8 @@ public function run(): AnalysisResult
252295
$prodDependencies,
253296
array_keys($prodPackagesUsedInProdPath),
254297
array_keys($forceUsedPackages), // we dont know where are those used, lets not report them
255-
$unusedDependencies
298+
$unusedDependencies,
299+
self::CORE_EXTENSIONS
256300
);
257301

258302
foreach ($prodPackagesUsedOnlyInDev as $prodPackageUsedOnlyInDev) {
@@ -340,7 +384,11 @@ private function getUsedSymbolsInFile(string $filePath): array
340384
throw new InvalidPathException("Unable to get contents of '$filePath'");
341385
}
342386

343-
return (new UsedSymbolExtractor($code))->parseUsedSymbols();
387+
return (new UsedSymbolExtractor($code))->parseUsedSymbols(
388+
array_keys($this->extensionSymbols[SymbolKind::CLASSLIKE]),
389+
array_keys($this->extensionSymbols[SymbolKind::FUNCTION]),
390+
array_keys($this->extensionSymbols[SymbolKind::CONSTANT])
391+
);
344392
}
345393

346394
/**
@@ -485,20 +533,41 @@ private function initExistingSymbols(): void
485533
'Composer\\Autoload\\ClassLoader' => true,
486534
];
487535

488-
/** @var string $constantName */
489-
foreach (get_defined_constants() as $constantName => $constantValue) {
490-
$this->ignoredSymbols[$constantName] = true;
536+
/** @var array<string, array<string, mixed>> $definedConstants */
537+
$definedConstants = get_defined_constants(true);
538+
foreach ($definedConstants as $constantExtension => $constants) {
539+
foreach ($constants as $constantName => $_) {
540+
if ($constantExtension === 'user') {
541+
$this->ignoredSymbols[$constantName] = true;
542+
} else {
543+
$extensionName = $this->getNormalizedExtensionName($constantExtension);
544+
545+
if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
546+
$this->ignoredSymbols[$constantName] = true;
547+
} else {
548+
$this->extensionSymbols[SymbolKind::CONSTANT][$constantName] = $extensionName;
549+
}
550+
}
551+
}
491552
}
492553

493554
foreach (get_defined_functions() as $functionNames) {
494555
foreach ($functionNames as $functionName) {
495556
$reflectionFunction = new ReflectionFunction($functionName);
496557
$functionFilePath = $reflectionFunction->getFileName();
497558

498-
if ($reflectionFunction->getExtension() === null && is_string($functionFilePath)) {
499-
$this->definedFunctions[$functionName] = Path::normalize($functionFilePath);
559+
if ($reflectionFunction->getExtension() === null) {
560+
if (is_string($functionFilePath)) {
561+
$this->definedFunctions[$functionName] = Path::normalize($functionFilePath);
562+
}
500563
} else {
501-
$this->ignoredSymbols[$functionName] = true;
564+
$extensionName = $this->getNormalizedExtensionName($reflectionFunction->getExtension()->name);
565+
566+
if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
567+
$this->ignoredSymbols[$functionName] = true;
568+
} else {
569+
$this->extensionSymbols[SymbolKind::FUNCTION][$functionName] = $extensionName;
570+
}
502571
}
503572
}
504573
}
@@ -511,11 +580,24 @@ private function initExistingSymbols(): void
511580

512581
foreach ($classLikes as $classLikeNames) {
513582
foreach ($classLikeNames as $classLikeName) {
514-
if ((new ReflectionClass($classLikeName))->getExtension() !== null) {
515-
$this->ignoredSymbols[$classLikeName] = true;
583+
$classReflection = new ReflectionClass($classLikeName);
584+
585+
if ($classReflection->getExtension() !== null) {
586+
$extensionName = $this->getNormalizedExtensionName($classReflection->getExtension()->name);
587+
588+
if (in_array($extensionName, self::CORE_EXTENSIONS, true)) {
589+
$this->ignoredSymbols[$classLikeName] = true;
590+
} else {
591+
$this->extensionSymbols[SymbolKind::CLASSLIKE][$classLikeName] = $extensionName;
592+
}
516593
}
517594
}
518595
}
519596
}
520597

598+
private function getNormalizedExtensionName(string $extension): string
599+
{
600+
return 'ext-' . ComposerJson::normalizeExtensionName($extension);
601+
}
602+
521603
}

src/ComposerJson.php

+40-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use function realpath;
2424
use function str_replace;
2525
use function strpos;
26+
use function strtolower;
2627
use function strtr;
2728
use function trim;
2829
use const ARRAY_FILTER_USE_KEY;
@@ -44,7 +45,7 @@ class ComposerJson
4445
public $composerAutoloadPath;
4546

4647
/**
47-
* Package => isDev
48+
* Package or ext-* => isDev
4849
*
4950
* @readonly
5051
* @var array<string, bool>
@@ -99,18 +100,52 @@ public function __construct(
99100
$this->extractAutoloadExcludeRegexes($basePath, $composerJsonData['autoload-dev']['exclude-from-classmap'] ?? [], true)
100101
);
101102

103+
$filterExtensions = static function (string $dependency): bool {
104+
return strpos($dependency, 'ext-') === 0;
105+
};
102106
$filterPackages = static function (string $package): bool {
103107
return strpos($package, '/') !== false;
104108
};
105109

106-
$this->dependencies = array_merge(
110+
$this->dependencies = $this->normalizeNames(array_merge(
107111
array_fill_keys(array_keys(array_filter($requiredPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), false),
108-
array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), true)
109-
);
112+
array_fill_keys(array_keys(array_filter($requiredPackages, $filterExtensions, ARRAY_FILTER_USE_KEY)), false),
113+
array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), true),
114+
array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterExtensions, ARRAY_FILTER_USE_KEY)), true)
115+
));
110116

111117
if (count($this->dependencies) === 0) {
112-
throw new InvalidConfigException("No packages found in $composerJsonPath file.");
118+
throw new InvalidConfigException("No dependencies found in $composerJsonPath file.");
119+
}
120+
}
121+
122+
/**
123+
* @param array<string, bool> $dependencies
124+
* @return array<string, bool>
125+
*/
126+
private function normalizeNames(array $dependencies): array
127+
{
128+
$normalized = [];
129+
130+
foreach ($dependencies as $dependency => $isDev) {
131+
if (strpos($dependency, 'ext-') === 0) {
132+
$key = self::normalizeExtensionName($dependency);
133+
} else {
134+
$key = $dependency;
135+
}
136+
137+
$normalized[$key] = $isDev;
113138
}
139+
140+
return $normalized;
141+
}
142+
143+
/**
144+
* Zend Opcache -> zend-opcache
145+
*/
146+
public static function normalizeExtensionName(string $extension): string
147+
{
148+
return str_replace(' ', '-', strtolower($extension));
114149
}
115150

116151
/**

0 commit comments

Comments
 (0)