diff --git a/src/Analyser.php b/src/Analyser.php index 24d20cc..9795ba1 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -21,6 +21,7 @@ use function array_filter; use function array_key_exists; use function array_keys; +use function array_merge; use function explode; use function file_get_contents; use function get_declared_classes; @@ -43,16 +44,16 @@ class Analyser { private const CORE_EXTENSIONS = [ - 'Core', - 'date', - 'json', - 'hash', - 'pcre', - 'Phar', - 'Reflection', - 'SPL', - 'random', - 'standard', + 'ext-Core', + 'ext-date', + 'ext-json', + 'ext-hash', + 'ext-pcre', + 'ext-Phar', + 'ext-Reflection', + 'ext-SPL', + 'ext-random', + 'ext-standard', ]; /** @@ -108,18 +109,11 @@ class Analyser private $definedFunctions = []; /** - * class name => ext-* + * kind => [symbol name => ext-*] * - * @var array + * @var array> */ - private $extensionClasses; - - /** - * function name => ext-* - * - * @var array - */ - private $extensionFunctions; + private $extensionSymbols; /** * @param array $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders()) @@ -163,6 +157,7 @@ public function run(): AnalysisResult $unusedErrors = []; $usedPackages = []; + $usedExtensions = []; $prodPackagesUsedInProdPath = []; $usages = []; @@ -180,20 +175,9 @@ public function run(): AnalysisResult continue; } - if ($kind === SymbolKind::FUNCTION && isset($this->extensionFunctions[$usedSymbol])) { - $neededExtension = $this->extensionFunctions[$usedSymbol]; - - if (!isset($this->composerJsonExtensions[$neededExtension])) { - foreach ($lineNumbers as $lineNumber) { - $missingExtensions[$neededExtension][] = new SymbolUsage($filePath, $lineNumber, $kind); - } - } - - continue; - } - - if ($kind === SymbolKind::CLASSLIKE && isset($this->extensionClasses[$usedSymbol])) { - $neededExtension = $this->extensionClasses[$usedSymbol]; + if (isset($this->extensionSymbols[$kind][$usedSymbol])) { + $neededExtension = $this->extensionSymbols[$kind][$usedSymbol]; + $usedExtensions[$neededExtension] = true; if (!isset($this->composerJsonExtensions[$neededExtension])) { foreach ($lineNumbers as $lineNumber) { @@ -282,16 +266,25 @@ public function run(): AnalysisResult } if ($this->config->shouldReportUnusedDevDependencies()) { - $dependenciesForUnusedAnalysis = array_keys($this->composerJsonDependencies); + $dependenciesForUnusedAnalysis = array_merge( + array_keys($this->composerJsonDependencies), + array_keys($this->composerJsonExtensions) + ); + } else { - $dependenciesForUnusedAnalysis = array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) { - return !$devDependency; // dev deps are typically used only in CI - })); + $dependenciesForUnusedAnalysis = array_merge( + array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) { + return !$devDependency; // dev deps are typically used only in CI + })), + array_keys($this->composerJsonExtensions) + ); } $unusedDependencies = array_diff( $dependenciesForUnusedAnalysis, - array_keys($usedPackages) + array_keys($usedPackages), + array_keys($usedExtensions), + self::CORE_EXTENSIONS ); foreach ($unusedDependencies as $unusedDependency) { @@ -391,7 +384,10 @@ private function getUsedSymbolsInFile(string $filePath): array throw new InvalidPathException("Unable to get contents of '$filePath'"); } - return (new UsedSymbolExtractor($code))->parseUsedSymbols($this->extensionFunctions); + return (new UsedSymbolExtractor($code))->parseUsedSymbols( + $this->extensionSymbols[SymbolKind::FUNCTION], + $this->extensionSymbols[SymbolKind::CONSTANT] + ); } /** @@ -536,9 +532,21 @@ private function initExistingSymbols(): void 'Composer\\Autoload\\ClassLoader' => true, ]; - /** @var string $constantName */ - foreach (get_defined_constants() as $constantName => $constantValue) { - $this->ignoredSymbols[$constantName] = true; + /** @var array $constants */ + foreach (get_defined_constants(true) as $constantExtension => $constants) { + foreach ($constants as $constantName => $_) { + if ($constantExtension === 'user') { + $this->ignoredSymbols[$constantName] = true; + } else { + $extensionName = 'ext-' . $constantExtension; + + if (in_array($extensionName, self::CORE_EXTENSIONS, true)) { + $this->ignoredSymbols[$constantName] = true; + } else { + $this->extensionSymbols[SymbolKind::CONSTANT][$constantName] = $extensionName; + } + } + } } foreach (get_defined_functions() as $functionNames) { @@ -551,12 +559,12 @@ private function initExistingSymbols(): void $this->definedFunctions[$functionName] = Path::normalize($functionFilePath); } } else { - $extensionName = $reflectionFunction->getExtension()->name; + $extensionName = 'ext-' . $reflectionFunction->getExtension()->name; if (in_array($extensionName, self::CORE_EXTENSIONS, true)) { $this->ignoredSymbols[$functionName] = true; } else { - $this->extensionFunctions[$functionName] = 'ext-' . $extensionName; + $this->extensionSymbols[SymbolKind::FUNCTION][$functionName] = $extensionName; } } } @@ -573,12 +581,12 @@ private function initExistingSymbols(): void $classReflection = new ReflectionClass($classLikeName); if ($classReflection->getExtension() !== null) { - $extensionName = $classReflection->getExtension()->name; + $extensionName = 'ext-' . $classReflection->getExtension()->name; if (in_array($extensionName, self::CORE_EXTENSIONS, true)) { $this->ignoredSymbols[$classLikeName] = true; } else { - $this->extensionClasses[$classLikeName] = 'ext-' . $extensionName; + $this->extensionSymbols[SymbolKind::CLASSLIKE][$classLikeName] = $extensionName; } } } diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index ac833df..a35e908 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -4,6 +4,7 @@ use function array_combine; use function array_fill_keys; +use function array_merge; use function count; use function explode; use function is_array; @@ -61,14 +62,24 @@ public function __construct(string $code) * - this results in very limited functionality in files without namespace * * @param array $definedFunctions + * @param array $definedConstants * @return array>> * @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php */ - public function parseUsedSymbols(array $definedFunctions): array + public function parseUsedSymbols( + array $definedFunctions, + array $definedConstants + ): array { $usedSymbols = []; - $useStatements = array_combine($definedFunctions, $definedFunctions); - $useStatementKinds = array_fill_keys($definedFunctions, SymbolKind::FUNCTION); + $useStatements = $initialSseStatements = array_merge( + array_combine($definedFunctions, $definedFunctions), + array_combine($definedConstants, $definedConstants) + ); + $useStatementKinds = $initialUseStatementKinds = array_merge( + array_fill_keys($definedFunctions, SymbolKind::FUNCTION), + array_fill_keys($definedConstants, SymbolKind::CONSTANT) + ); $level = 0; $inClassLevel = null; @@ -100,8 +111,8 @@ public function parseUsedSymbols(array $definedFunctions): array case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1: // reset use statements on namespace change - $useStatements = array_combine($definedFunctions, $definedFunctions); - $useStatementKinds = array_fill_keys($definedFunctions, SymbolKind::FUNCTION); + $useStatements = $initialSseStatements; + $useStatementKinds = $initialUseStatementKinds; break; case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1: @@ -138,8 +149,8 @@ public function parseUsedSymbols(array $definedFunctions): array if (substr($nextName, 0, 1) !== '\\') { // not a namespace-relative name, but a new namespace declaration // reset use statements on namespace change - $useStatements = array_combine($definedFunctions, $definedFunctions); - $useStatementKinds = array_fill_keys($definedFunctions, SymbolKind::FUNCTION); + $useStatements = $initialSseStatements; + $useStatementKinds = $initialUseStatementKinds; } break; @@ -192,7 +203,7 @@ public function parseUsedSymbols(array $definedFunctions): array } } - return $usedSymbols; // @phpstan-ignore-line Not enough precise analysis "Offset 'kind' (1|2|3) does not accept type int<1, max>" + return $usedSymbols; } /** diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index ff38f27..220ece4 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -19,7 +19,7 @@ public function test(string $path, array $expectedUsages): void $extractor = new UsedSymbolExtractor($code); - self::assertSame($expectedUsages, $extractor->parseUsedSymbols(['json_encode'])); + self::assertSame($expectedUsages, $extractor->parseUsedSymbols(['json_encode'], [])); } /**