4545class 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}
0 commit comments