1010use RecursiveIteratorIterator ;
1111use ReflectionClass ;
1212use ReflectionException ;
13+ use ReflectionFunction ;
1314use ShipMonk \ComposerDependencyAnalyser \Config \Configuration ;
1415use ShipMonk \ComposerDependencyAnalyser \Config \ErrorType ;
1516use ShipMonk \ComposerDependencyAnalyser \Exception \InvalidPathException ;
1617use ShipMonk \ComposerDependencyAnalyser \Result \AnalysisResult ;
1718use ShipMonk \ComposerDependencyAnalyser \Result \SymbolUsage ;
1819use UnexpectedValueException ;
19- use function array_change_key_case ;
2020use function array_diff ;
2121use function array_filter ;
2222use function array_key_exists ;
3030use function get_defined_functions ;
3131use function in_array ;
3232use function is_file ;
33+ use function is_string ;
3334use function str_replace ;
3435use function strlen ;
3536use function strpos ;
3637use function strtolower ;
3738use function substr ;
3839use function trim ;
39- use const CASE_LOWER ;
4040use const DIRECTORY_SEPARATOR ;
4141
4242class 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}
0 commit comments