From df0ca4be91d404bd2bab275212be269ccb1f50e3 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 10 Apr 2024 13:32:02 +0200 Subject: [PATCH 01/14] Support ext-* --- README.md | 2 +- src/Analyser.php | 174 +++++++++++++----- src/ComposerJson.php | 45 ++++- src/UsedSymbolExtractor.php | 35 +++- tests/AnalyserTest.php | 42 +++++ tests/UsedSymbolExtractorTest.php | 19 +- .../extensions/ext-dev-usages.php | 3 + .../extensions/ext-prod-usages.php | 6 + .../used-symbols/extensions.php | 13 ++ 9 files changed, 280 insertions(+), 59 deletions(-) create mode 100644 tests/data/not-autoloaded/extensions/ext-dev-usages.php create mode 100644 tests/data/not-autoloaded/extensions/ext-prod-usages.php create mode 100644 tests/data/not-autoloaded/used-symbols/extensions.php diff --git a/README.md b/README.md index 67eea35..fcb9270 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ NO_COLOR=1 vendor/bin/composer-dependency-analyser ``` ## Limitations: -- Extension dependencies are not analysed (e.g. `ext-json`) +- For precise `ext-x` analysis, your enabled extentions of your php runtime should be superset of those used in the scanned project ## Contributing: - Check your code by `composer check` diff --git a/src/Analyser.php b/src/Analyser.php index 2c984bd..d056541 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -45,6 +45,24 @@ class Analyser { + /** + * Those are core PHP extensions, that can never be disabled + * There are more PHP "core" extensions, that are bundled by default, but PHP can be compiled without them + * You can check which are added conditionally in https://github.com/php/php-src/tree/master/ext (see config.w32 files) + */ + private const CORE_EXTENSIONS = [ + 'ext-core', + 'ext-date', + 'ext-json', + 'ext-hash', + 'ext-pcre', + 'ext-phar', + 'ext-reflection', + 'ext-spl', + 'ext-random', + 'ext-standard', + ]; + /** * @var Stopwatch */ @@ -73,7 +91,7 @@ class Analyser private $classmap = []; /** - * package name => is dev dependency + * package or ext-* => is dev dependency * * @var array */ @@ -87,15 +105,22 @@ class Analyser private $ignoredSymbols; /** - * function name => path + * custom function name => path * * @var array */ private $definedFunctions = []; + /** + * kind => [symbol name => ext-*] + * + * @var array> + */ + private $extensionSymbols; + /** * @param array $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders()) - * @param array $composerJsonDependencies package name => is dev dependency + * @param array $composerJsonDependencies package or ext-* => is dev dependency */ public function __construct( Stopwatch $stopwatch, @@ -129,7 +154,7 @@ public function run(): AnalysisResult $prodOnlyInDevErrors = []; $unusedErrors = []; - $usedPackages = []; + $usedDependencies = []; $prodPackagesUsedInProdPath = []; $usages = []; @@ -149,60 +174,65 @@ public function run(): AnalysisResult continue; } - $symbolPath = $this->getSymbolPath($usedSymbol, $kind); + if (isset($this->extensionSymbols[$kind][$usedSymbol])) { + $dependencyName = $this->extensionSymbols[$kind][$usedSymbol]; - if ($symbolPath === null) { - if ($kind === SymbolKind::CLASSLIKE && !$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) { - foreach ($lineNumbers as $lineNumber) { - $unknownClassErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); + } else { + $symbolPath = $this->getSymbolPath($usedSymbol, $kind); + + if ($symbolPath === null) { + if ($kind === SymbolKind::CLASSLIKE && !$ignoreList->shouldIgnoreUnknownClass($usedSymbol, $filePath)) { + foreach ($lineNumbers as $lineNumber) { + $unknownClassErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); + } } - } - if ($kind === SymbolKind::FUNCTION && !$ignoreList->shouldIgnoreUnknownFunction($usedSymbol, $filePath)) { - foreach ($lineNumbers as $lineNumber) { - $unknownFunctionErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); + if ($kind === SymbolKind::FUNCTION && !$ignoreList->shouldIgnoreUnknownFunction($usedSymbol, $filePath)) { + foreach ($lineNumbers as $lineNumber) { + $unknownFunctionErrors[$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); + } } + + continue; } - continue; - } + if (!$this->isVendorPath($symbolPath)) { + continue; // local class + } - if (!$this->isVendorPath($symbolPath)) { - continue; // local class + $dependencyName = $this->getPackageNameFromVendorPath($symbolPath); } - $packageName = $this->getPackageNameFromVendorPath($symbolPath); - if ( - $this->isShadowDependency($packageName) - && !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $packageName) + $this->isShadowDependency($dependencyName) + && !$ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, $filePath, $dependencyName) ) { foreach ($lineNumbers as $lineNumber) { - $shadowErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); + $shadowErrors[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); } } if ( !$isDevFilePath - && $this->isDevDependency($packageName) - && !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $packageName) + && $this->isDevDependency($dependencyName) + && !$ignoreList->shouldIgnoreError(ErrorType::DEV_DEPENDENCY_IN_PROD, $filePath, $dependencyName) ) { foreach ($lineNumbers as $lineNumber) { - $devInProdErrors[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); + $devInProdErrors[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); } } if ( !$isDevFilePath - && !$this->isDevDependency($packageName) + && !$this->isDevDependency($dependencyName) ) { - $prodPackagesUsedInProdPath[$packageName] = true; + $prodPackagesUsedInProdPath[$dependencyName] = true; } - $usedPackages[$packageName] = true; + $usedDependencies[$dependencyName] = true; foreach ($lineNumbers as $lineNumber) { - $usages[$packageName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); + $usages[$dependencyName][$usedSymbol][] = new SymbolUsage($filePath, $lineNumber, $kind); } } } @@ -215,19 +245,31 @@ public function run(): AnalysisResult continue; } - $symbolPath = $this->getSymbolPath($forceUsedSymbol, null); + if ( + isset($this->extensionSymbols[SymbolKind::FUNCTION][$forceUsedSymbol]) + || isset($this->extensionSymbols[SymbolKind::CONSTANT][$forceUsedSymbol]) + || isset($this->extensionSymbols[SymbolKind::CLASSLIKE][$forceUsedSymbol]) + ) { + $forceUsedDependency = $this->extensionSymbols[SymbolKind::FUNCTION][$forceUsedSymbol] + ?? $this->extensionSymbols[SymbolKind::CONSTANT][$forceUsedSymbol] + ?? $this->extensionSymbols[SymbolKind::CLASSLIKE][$forceUsedSymbol]; + } else { + $symbolPath = $this->getSymbolPath($forceUsedSymbol, null); + + if ($symbolPath === null || !$this->isVendorPath($symbolPath)) { + continue; + } - if ($symbolPath === null || !$this->isVendorPath($symbolPath)) { - continue; + $forceUsedDependency = $this->getPackageNameFromVendorPath($symbolPath); } - $forceUsedPackage = $this->getPackageNameFromVendorPath($symbolPath); - $usedPackages[$forceUsedPackage] = true; - $forceUsedPackages[$forceUsedPackage] = true; + $usedDependencies[$forceUsedDependency] = true; + $forceUsedPackages[$forceUsedDependency] = true; } if ($this->config->shouldReportUnusedDevDependencies()) { $dependenciesForUnusedAnalysis = array_keys($this->composerJsonDependencies); + } else { $dependenciesForUnusedAnalysis = array_keys(array_filter($this->composerJsonDependencies, static function (bool $devDependency) { return !$devDependency; // dev deps are typically used only in CI @@ -236,7 +278,8 @@ public function run(): AnalysisResult $unusedDependencies = array_diff( $dependenciesForUnusedAnalysis, - array_keys($usedPackages) + array_keys($usedDependencies), + self::CORE_EXTENSIONS ); foreach ($unusedDependencies as $unusedDependency) { @@ -252,7 +295,8 @@ public function run(): AnalysisResult $prodDependencies, array_keys($prodPackagesUsedInProdPath), array_keys($forceUsedPackages), // we dont know where are those used, lets not report them - $unusedDependencies + $unusedDependencies, + self::CORE_EXTENSIONS ); foreach ($prodPackagesUsedOnlyInDev as $prodPackageUsedOnlyInDev) { @@ -340,7 +384,11 @@ private function getUsedSymbolsInFile(string $filePath): array throw new InvalidPathException("Unable to get contents of '$filePath'"); } - return (new UsedSymbolExtractor($code))->parseUsedSymbols(); + return (new UsedSymbolExtractor($code))->parseUsedSymbols( + array_keys($this->extensionSymbols[SymbolKind::CLASSLIKE]), + array_keys($this->extensionSymbols[SymbolKind::FUNCTION]), + array_keys($this->extensionSymbols[SymbolKind::CONSTANT]) + ); } /** @@ -485,9 +533,22 @@ private function initExistingSymbols(): void 'Composer\\Autoload\\ClassLoader' => true, ]; - /** @var string $constantName */ - foreach (get_defined_constants() as $constantName => $constantValue) { - $this->ignoredSymbols[$constantName] = true; + /** @var array> $definedConstants */ + $definedConstants = get_defined_constants(true); + foreach ($definedConstants as $constantExtension => $constants) { + foreach ($constants as $constantName => $_) { + if ($constantExtension === 'user') { + $this->ignoredSymbols[$constantName] = true; + } else { + $extensionName = $this->getNormalizedExtensionName($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) { @@ -495,10 +556,18 @@ private function initExistingSymbols(): void $reflectionFunction = new ReflectionFunction($functionName); $functionFilePath = $reflectionFunction->getFileName(); - if ($reflectionFunction->getExtension() === null && is_string($functionFilePath)) { - $this->definedFunctions[$functionName] = Path::normalize($functionFilePath); + if ($reflectionFunction->getExtension() === null) { + if (is_string($functionFilePath)) { + $this->definedFunctions[$functionName] = Path::normalize($functionFilePath); + } } else { - $this->ignoredSymbols[$functionName] = true; + $extensionName = $this->getNormalizedExtensionName($reflectionFunction->getExtension()->name); + + if (in_array($extensionName, self::CORE_EXTENSIONS, true)) { + $this->ignoredSymbols[$functionName] = true; + } else { + $this->extensionSymbols[SymbolKind::FUNCTION][$functionName] = $extensionName; + } } } } @@ -511,11 +580,24 @@ private function initExistingSymbols(): void foreach ($classLikes as $classLikeNames) { foreach ($classLikeNames as $classLikeName) { - if ((new ReflectionClass($classLikeName))->getExtension() !== null) { - $this->ignoredSymbols[$classLikeName] = true; + $classReflection = new ReflectionClass($classLikeName); + + if ($classReflection->getExtension() !== null) { + $extensionName = $this->getNormalizedExtensionName($classReflection->getExtension()->name); + + if (in_array($extensionName, self::CORE_EXTENSIONS, true)) { + $this->ignoredSymbols[$classLikeName] = true; + } else { + $this->extensionSymbols[SymbolKind::CLASSLIKE][$classLikeName] = $extensionName; + } } } } } + private function getNormalizedExtensionName(string $extension): string + { + return 'ext-' . ComposerJson::normalizeExtensionName($extension); + } + } diff --git a/src/ComposerJson.php b/src/ComposerJson.php index 3631304..a351499 100644 --- a/src/ComposerJson.php +++ b/src/ComposerJson.php @@ -23,6 +23,7 @@ use function realpath; use function str_replace; use function strpos; +use function strtolower; use function strtr; use function trim; use const ARRAY_FILTER_USE_KEY; @@ -44,7 +45,7 @@ class ComposerJson public $composerAutoloadPath; /** - * Package => isDev + * Package or ext-* => isDev * * @readonly * @var array @@ -99,18 +100,52 @@ public function __construct( $this->extractAutoloadExcludeRegexes($basePath, $composerJsonData['autoload-dev']['exclude-from-classmap'] ?? [], true) ); + $filterExtensions = static function (string $dependency): bool { + return strpos($dependency, 'ext-') === 0; + }; $filterPackages = static function (string $package): bool { return strpos($package, '/') !== false; }; - $this->dependencies = array_merge( + $this->dependencies = $this->normalizeNames(array_merge( array_fill_keys(array_keys(array_filter($requiredPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), false), - array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), true) - ); + array_fill_keys(array_keys(array_filter($requiredPackages, $filterExtensions, ARRAY_FILTER_USE_KEY)), false), + array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterPackages, ARRAY_FILTER_USE_KEY)), true), + array_fill_keys(array_keys(array_filter($requiredDevPackages, $filterExtensions, ARRAY_FILTER_USE_KEY)), true) + )); if (count($this->dependencies) === 0) { - throw new InvalidConfigException("No packages found in $composerJsonPath file."); + throw new InvalidConfigException("No dependencies found in $composerJsonPath file."); + } + } + + /** + * @param array $dependencies + * @return array + */ + private function normalizeNames(array $dependencies): array + { + $normalized = []; + + foreach ($dependencies as $dependency => $isDev) { + if (strpos($dependency, 'ext-') === 0) { + $key = self::normalizeExtensionName($dependency); + } else { + $key = $dependency; + } + + $normalized[$key] = $isDev; } + + return $normalized; + } + + /** + * Zend Opcache -> zend-opcache + */ + public static function normalizeExtensionName(string $extension): string + { + return str_replace(' ', '-', strtolower($extension)); } /** diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index 8f386c8..f330f8a 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -2,6 +2,9 @@ namespace ShipMonk\ComposerDependencyAnalyser; +use function array_combine; +use function array_fill_keys; +use function array_merge; use function count; use function explode; use function is_array; @@ -62,14 +65,30 @@ public function __construct(string $code) * It does not produce any local names in current namespace * - this results in very limited functionality in files without namespace * + * @param array $extClasses + * @param array $extFunctions + * @param array $extConstants + * * @return array>> * @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php */ - public function parseUsedSymbols(): array + public function parseUsedSymbols( + array $extClasses, + array $extFunctions, + array $extConstants + ): array { $usedSymbols = []; - $useStatements = []; - $useStatementKinds = []; + $useStatements = $initialUseStatements = array_merge( + array_combine($extClasses, $extClasses), + array_combine($extFunctions, $extFunctions), + array_combine($extConstants, $extConstants) + ); + $useStatementKinds = $initialUseStatementKinds = array_merge( + array_fill_keys($extClasses, SymbolKind::CLASSLIKE), + array_fill_keys($extFunctions, SymbolKind::FUNCTION), + array_fill_keys($extConstants, SymbolKind::CONSTANT) + ); $level = 0; // {, }, {$, ${ $squareLevel = 0; // [, ], #[ @@ -107,8 +126,10 @@ public function parseUsedSymbols(): array break; case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1: + // namespace change $inGlobalScope = false; - $useStatements = []; // reset use statements on namespace change + $useStatements = $initialUseStatements; + $useStatementKinds = $initialUseStatementKinds; break; case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1: @@ -149,8 +170,10 @@ public function parseUsedSymbols(): array $nextName = $this->parseNameForOldPhp(); if (substr($nextName, 0, 1) !== '\\') { // not a namespace-relative name, but a new namespace declaration - $useStatements = []; // reset use statements on namespace change + // namespace change $inGlobalScope = false; + $useStatements = $initialUseStatements; + $useStatementKinds = $initialUseStatementKinds; } break; @@ -216,7 +239,7 @@ public function parseUsedSymbols(): 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/AnalyserTest.php b/tests/AnalyserTest.php index 0f6ed15..7e09a2f 100644 --- a/tests/AnalyserTest.php +++ b/tests/AnalyserTest.php @@ -609,6 +609,48 @@ public function testOtherSymbols(): void self::assertEquals($this->createAnalysisResult(1, []), $result); } + public function testExtensions(): void + { + $vendorDir = realpath(__DIR__ . '/../vendor'); + $prodPath = realpath(__DIR__ . '/data/not-autoloaded/extensions/ext-prod-usages.php'); + $devPath = realpath(__DIR__ . '/data/not-autoloaded/extensions/ext-dev-usages.php'); + self::assertNotFalse($vendorDir); + self::assertNotFalse($prodPath); + self::assertNotFalse($devPath); + + $config = new Configuration(); + $config->addPathToScan($prodPath, false); + $config->addPathToScan($devPath, true); + + $detector = new Analyser( + $this->getStopwatchMock(), + $vendorDir, + [$vendorDir => $this->getClassLoaderMock()], + $config, + [ + 'ext-dom' => false, + 'ext-libxml' => true, + 'ext-mbstring' => false, + ] + ); + $result = $detector->run(); + + $this->assertResultsWithoutUsages($this->createAnalysisResult(2, [ + ErrorType::SHADOW_DEPENDENCY => [ + 'ext-pdo' => ['PDO' => [new SymbolUsage($prodPath, 5, SymbolKind::CLASSLIKE)]], + ], + ErrorType::DEV_DEPENDENCY_IN_PROD => [ + 'ext-libxml' => ['LIBXML_NOEMPTYTAG' => [new SymbolUsage($prodPath, 3, SymbolKind::CONSTANT)]], + ], + ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV => [ + 'ext-dom', + ], + ErrorType::UNUSED_DEPENDENCY => [ + 'ext-mbstring', + ], + ]), $result); + } + public function testPharSupport(): void { $canCreatePhar = ini_set('phar.readonly', '0'); diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index 8f35c8b..c28d71f 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -20,7 +20,7 @@ public function test(string $path, array $expectedUsages): void $extractor = new UsedSymbolExtractor($code); - self::assertSame($expectedUsages, $extractor->parseUsedSymbols()); + self::assertSame($expectedUsages, $extractor->parseUsedSymbols(['PDO'], ['json_encode'], ['LIBXML_ERR_FATAL'])); } /** @@ -118,6 +118,23 @@ public function provideVariants(): iterable [], ]; + yield 'extensions' => [ + __DIR__ . '/data/not-autoloaded/used-symbols/extensions.php', + [ + SymbolKind::FUNCTION => [ + 'json_encode' => [5], + 'json_decode' => [12], + ], + SymbolKind::CONSTANT => [ + 'LIBXML_ERR_FATAL' => [6], + ], + SymbolKind::CLASSLIKE => [ + 'PDO' => [7], + 'CURLOPT_SSL_VERIFYHOST' => [10], + ], + ], + ]; + if (PHP_VERSION_ID >= 80000) { yield 'attribute' => [ __DIR__ . '/data/not-autoloaded/used-symbols/attribute.php', diff --git a/tests/data/not-autoloaded/extensions/ext-dev-usages.php b/tests/data/not-autoloaded/extensions/ext-dev-usages.php new file mode 100644 index 0000000..51d8f4c --- /dev/null +++ b/tests/data/not-autoloaded/extensions/ext-dev-usages.php @@ -0,0 +1,3 @@ + Date: Fri, 4 Oct 2024 13:45:12 +0200 Subject: [PATCH 02/14] Adjust e2e --- .github/workflows/e2e.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3758f12..913cf18 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -119,6 +119,11 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: 8.3 + ini-file: development + + - + name: List enabled extensions + run: php -m - name: Install analyser dependencies From d4e0a59b13e87be96f825801249014b28643b892 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 15 Oct 2024 15:57:08 +0200 Subject: [PATCH 03/14] Extension symbols: track separatelly --- src/Analyser.php | 1 + src/UsedSymbolExtractor.php | 162 +++++++++++++----- tests/UsedSymbolExtractorTest.php | 50 +++++- .../used-symbols/extensions-global.php | 22 +++ .../used-symbols/extensions.php | 9 + .../used-symbols/t-string-issues.php | 24 +++ 6 files changed, 216 insertions(+), 52 deletions(-) create mode 100644 tests/data/not-autoloaded/used-symbols/extensions-global.php create mode 100644 tests/data/not-autoloaded/used-symbols/t-string-issues.php diff --git a/src/Analyser.php b/src/Analyser.php index d056541..fa024de 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -535,6 +535,7 @@ private function initExistingSymbols(): void /** @var array> $definedConstants */ $definedConstants = get_defined_constants(true); + foreach ($definedConstants as $constantExtension => $constants) { foreach ($constants as $constantName => $_) { if ($constantExtension === 'user') { diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index f330f8a..74d007c 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -2,8 +2,8 @@ namespace ShipMonk\ComposerDependencyAnalyser; -use function array_combine; use function array_fill_keys; +use function array_map; use function array_merge; use function count; use function explode; @@ -11,6 +11,7 @@ use function ltrim; use function strlen; use function strpos; +use function strtolower; use function substr; use function token_get_all; use const PHP_VERSION_ID; @@ -22,14 +23,18 @@ use const T_CURLY_OPEN; use const T_DOC_COMMENT; use const T_DOLLAR_OPEN_CURLY_BRACES; +use const T_DOUBLE_COLON; use const T_ENUM; use const T_FUNCTION; +use const T_INSTEADOF; use const T_INTERFACE; use const T_NAME_FULLY_QUALIFIED; use const T_NAME_QUALIFIED; use const T_NAMESPACE; use const T_NEW; use const T_NS_SEPARATOR; +use const T_NULLSAFE_OBJECT_OPERATOR; +use const T_OBJECT_OPERATOR; use const T_STRING; use const T_TRAIT; use const T_USE; @@ -65,10 +70,9 @@ public function __construct(string $code) * It does not produce any local names in current namespace * - this results in very limited functionality in files without namespace * - * @param array $extClasses - * @param array $extFunctions - * @param array $extConstants - * + * @param list $extClasses + * @param list $extFunctions + * @param list $extConstants * @return array>> * @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php */ @@ -79,16 +83,13 @@ public function parseUsedSymbols( ): array { $usedSymbols = []; - $useStatements = $initialUseStatements = array_merge( - array_combine($extClasses, $extClasses), - array_combine($extFunctions, $extFunctions), - array_combine($extConstants, $extConstants) - ); - $useStatementKinds = $initialUseStatementKinds = array_merge( - array_fill_keys($extClasses, SymbolKind::CLASSLIKE), - array_fill_keys($extFunctions, SymbolKind::FUNCTION), - array_fill_keys($extConstants, SymbolKind::CONSTANT) + $extensionSymbols = array_merge( + array_fill_keys(array_map('strtolower', $extClasses), SymbolKind::CLASSLIKE), + array_fill_keys(array_map('strtolower', $extFunctions), SymbolKind::FUNCTION), + array_fill_keys(array_map('strtolower', $extConstants), SymbolKind::CONSTANT) ); + $useStatements = []; + $useStatementKinds = []; $level = 0; // {, }, {$, ${ $squareLevel = 0; // [, ], #[ @@ -128,13 +129,14 @@ public function parseUsedSymbols( case PHP_VERSION_ID >= 80000 ? T_NAMESPACE : -1: // namespace change $inGlobalScope = false; - $useStatements = $initialUseStatements; - $useStatementKinds = $initialUseStatementKinds; + $useStatements = []; + $useStatementKinds = []; break; case PHP_VERSION_ID >= 80000 ? T_NAME_FULLY_QUALIFIED : -1: $symbolName = $this->normalizeBackslash($token[1]); - $kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null); + $lowerSymbolName = strtolower($symbolName); + $kind = $extensionSymbols[$lowerSymbolName] ?? $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null); $usedSymbols[$kind][$symbolName][] = $token[2]; break; @@ -143,21 +145,34 @@ public function parseUsedSymbols( if (isset($useStatements[$neededAlias])) { $symbolName = $useStatements[$neededAlias] . substr($token[1], strlen($neededAlias)); - $kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null); - $usedSymbols[$kind][$symbolName][] = $token[2]; - } elseif ($inGlobalScope) { $symbolName = $token[1]; - $kind = $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null); - $usedSymbols[$kind][$symbolName][] = $token[2]; + } else { + break; } + $lowerSymbolName = strtolower($symbolName); + $kind = $extensionSymbols[$lowerSymbolName] ?? $this->getFqnSymbolKind($this->pointer - 2, $this->pointer, $inAttributeSquareLevel !== null); + $usedSymbols[$kind][$symbolName][] = $token[2]; + break; case PHP_VERSION_ID >= 80000 ? T_STRING : -1: $name = $token[1]; + $lowerName = strtolower($name); + $pointerBeforeName = $this->pointer - 2; + $pointerAfterName = $this->pointer; + + if (!$this->canBeSymbolName($pointerBeforeName, $pointerAfterName)) { + break; + } - if (isset($useStatements[$name])) { + if (isset($extensionSymbols[$lowerName])) { + $symbolName = $name; + $kind = $extensionSymbols[$lowerName]; + $usedSymbols[$kind][$symbolName][] = $token[2]; + + } elseif (isset($useStatements[$name])) { $symbolName = $useStatements[$name]; $kind = $useStatementKinds[$name]; $usedSymbols[$kind][$symbolName][] = $token[2]; @@ -172,8 +187,8 @@ public function parseUsedSymbols( if (substr($nextName, 0, 1) !== '\\') { // not a namespace-relative name, but a new namespace declaration // namespace change $inGlobalScope = false; - $useStatements = $initialUseStatements; - $useStatementKinds = $initialUseStatementKinds; + $useStatements = []; + $useStatementKinds = []; } break; @@ -181,9 +196,10 @@ public function parseUsedSymbols( case PHP_VERSION_ID < 80000 ? T_NS_SEPARATOR : -1: $pointerBeforeName = $this->pointer - 2; $symbolName = $this->normalizeBackslash($this->parseNameForOldPhp()); + $lowerSymbolName = strtolower($symbolName); if ($symbolName !== '') { // e.g. \array (NS separator followed by not-a-name) - $kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false); + $kind = $extensionSymbols[$lowerSymbolName] ?? $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false); $usedSymbols[$kind][$symbolName][] = $token[2]; } @@ -192,23 +208,34 @@ public function parseUsedSymbols( case PHP_VERSION_ID < 80000 ? T_STRING : -1: $pointerBeforeName = $this->pointer - 2; $name = $this->parseNameForOldPhp(); + $lowerName = strtolower($name); + $pointerAfterName = $this->pointer - 1; + + if (!$this->canBeSymbolName($pointerBeforeName, $pointerAfterName)) { + break; + } if (isset($useStatements[$name])) { // unqualified name $symbolName = $useStatements[$name]; $kind = $useStatementKinds[$name]; $usedSymbols[$kind][$symbolName][] = $token[2]; + } elseif (isset($extensionSymbols[$lowerName])) { + $symbolName = $name; + $kind = $extensionSymbols[$lowerName]; + $usedSymbols[$kind][$symbolName][] = $token[2]; + } else { [$neededAlias] = explode('\\', $name, 2); if (isset($useStatements[$neededAlias])) { // qualified name $symbolName = $useStatements[$neededAlias] . substr($name, strlen($neededAlias)); - $kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false); + $kind = $this->getFqnSymbolKind($pointerBeforeName, $pointerAfterName, false); $usedSymbols[$kind][$symbolName][] = $token[2]; } elseif ($inGlobalScope && strpos($name, '\\') !== false) { $symbolName = $name; - $kind = $this->getFqnSymbolKind($pointerBeforeName, $this->pointer - 1, false); + $kind = $this->getFqnSymbolKind($pointerBeforeName, $pointerAfterName, false); $usedSymbols[$kind][$symbolName][] = $token[2]; } } @@ -369,44 +396,87 @@ private function getFqnSymbolKind( return SymbolKind::CLASSLIKE; } + $tokenBeforeName = $this->getTokenBefore($pointerBeforeName); + $tokenAfterName = $this->getTokenAfter($pointerAfterName); + + if ( + $tokenAfterName === '(' + && $tokenBeforeName[0] !== T_NEW // eliminate new \ClassName( + ) { + return SymbolKind::FUNCTION; + } + + return SymbolKind::CLASSLIKE; // constant may fall here, this is eliminated later + } + + private function canBeSymbolName( + int $pointerBeforeName, + int $pointerAfterName + ): bool + { + $tokenBeforeName = $this->getTokenBefore($pointerBeforeName); + $tokenAfterName = $this->getTokenAfter($pointerAfterName); + + if ( + $tokenBeforeName[0] === T_DOUBLE_COLON + || $tokenBeforeName[0] === T_INSTEADOF + || $tokenBeforeName[0] === T_AS + || $tokenBeforeName[0] === T_FUNCTION + || $tokenBeforeName[0] === T_OBJECT_OPERATOR + || $tokenBeforeName[0] === (PHP_VERSION_ID > 80000 ? T_NULLSAFE_OBJECT_OPERATOR : -1) + || $tokenAfterName[0] === T_INSTEADOF + || $tokenAfterName[0] === T_AS + ) { + return false; + } + + return true; + } + + /** + * @return array{int, string}|string + */ + private function getTokenBefore(int $pointer) + { do { - $tokenBeforeName = $this->tokens[$pointerBeforeName]; + $token = $this->tokens[$pointer]; - if (!is_array($tokenBeforeName)) { + if (!is_array($token)) { break; } - if ($tokenBeforeName[0] === T_WHITESPACE || $tokenBeforeName[0] === T_COMMENT || $tokenBeforeName[0] === T_DOC_COMMENT) { - $pointerBeforeName--; + if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT) { + $pointer--; continue; } break; - } while ($pointerBeforeName >= 0); + } while ($pointer >= 0); + return $token; + } + + /** + * @return array{int, string}|string + */ + private function getTokenAfter(int $pointer) + { do { - $tokenAfterName = $this->tokens[$pointerAfterName]; + $token = $this->tokens[$pointer]; - if (!is_array($tokenAfterName)) { + if (!is_array($token)) { break; } - if ($tokenAfterName[0] === T_WHITESPACE || $tokenAfterName[0] === T_COMMENT || $tokenAfterName[0] === T_DOC_COMMENT) { - $pointerAfterName++; + if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_DOC_COMMENT) { + $pointer++; continue; } break; - } while ($pointerAfterName < $this->numTokens); + } while ($pointer < $this->numTokens); - if ( - $tokenAfterName === '(' - && $tokenBeforeName[0] !== T_NEW // eliminate new \ClassName( - ) { - return SymbolKind::FUNCTION; - } - - return SymbolKind::CLASSLIKE; // constant may fall here, this is eliminated later + return $token; } } diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index c28d71f..74192a4 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -3,6 +3,7 @@ namespace ShipMonk\ComposerDependencyAnalyser; use PHPUnit\Framework\TestCase; +use function array_map; use function file_get_contents; use const PHP_VERSION_ID; @@ -20,7 +21,14 @@ public function test(string $path, array $expectedUsages): void $extractor = new UsedSymbolExtractor($code); - self::assertSame($expectedUsages, $extractor->parseUsedSymbols(['PDO'], ['json_encode'], ['LIBXML_ERR_FATAL'])); + self::assertSame( + $expectedUsages, + $extractor->parseUsedSymbols( + ['PDO'], + array_map('strtolower', ['json_encode', 'DDTrace\active_span', 'DDTrace\root_span']), + ['LIBXML_ERR_FATAL', 'LIBXML_ERR_ERROR', 'DDTrace\DBM_PROPAGATION_FULL'] + ) + ); } /** @@ -49,6 +57,11 @@ public function provideVariants(): iterable ], ]; + yield 'T_STRING issues' => [ + __DIR__ . '/data/not-autoloaded/used-symbols/t-string-issues.php', + [], + ]; + yield 'various usages' => [ __DIR__ . '/data/not-autoloaded/used-symbols/various-usages.php', [ @@ -122,15 +135,40 @@ public function provideVariants(): iterable __DIR__ . '/data/not-autoloaded/used-symbols/extensions.php', [ SymbolKind::FUNCTION => [ - 'json_encode' => [5], - 'json_decode' => [12], + 'json_encode' => [8], + 'DDTrace\active_span' => [12], + 'DDTrace\root_span' => [13], + 'json_decode' => [21], + ], + SymbolKind::CONSTANT => [ + 'LIBXML_ERR_FATAL' => [9], + 'LIBXML_ERR_ERROR' => [10], + 'DDTrace\DBM_PROPAGATION_FULL' => [14], + ], + SymbolKind::CLASSLIKE => [ + 'PDO' => [11], + 'CURLOPT_SSL_VERIFYHOST' => [19], + ], + ], + ]; + + yield 'extensions global' => [ + __DIR__ . '/data/not-autoloaded/used-symbols/extensions-global.php', + [ + SymbolKind::FUNCTION => [ + 'json_encode' => [8], + 'DDTrace\active_span' => [12], + 'DDTrace\root_span' => [13], + 'json_decode' => [21], ], SymbolKind::CONSTANT => [ - 'LIBXML_ERR_FATAL' => [6], + 'LIBXML_ERR_FATAL' => [9], + 'LIBXML_ERR_ERROR' => [10], + 'DDTrace\DBM_PROPAGATION_FULL' => [14], ], SymbolKind::CLASSLIKE => [ - 'PDO' => [7], - 'CURLOPT_SSL_VERIFYHOST' => [10], + 'PDO' => [11], + 'CURLOPT_SSL_VERIFYHOST' => [19], ], ], ]; diff --git a/tests/data/not-autoloaded/used-symbols/extensions-global.php b/tests/data/not-autoloaded/used-symbols/extensions-global.php new file mode 100644 index 0000000..52f72ec --- /dev/null +++ b/tests/data/not-autoloaded/used-symbols/extensions-global.php @@ -0,0 +1,22 @@ +array_filter(); + $this?->array_filter(); + self::array_filter(); + } + +} From d7a5b9f8c176089ea7bd00068e5c91fdf3bc9b8f Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 15 Oct 2024 16:51:17 +0200 Subject: [PATCH 04/14] Speedup #1: Reorganize ext symbol kinds only once --- src/Analyser.php | 14 +++++++++++--- src/UsedSymbolExtractor.php | 16 ++-------------- tests/UsedSymbolExtractorTest.php | 14 ++++++++++---- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/Analyser.php b/src/Analyser.php index fa024de..788cc8b 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -118,6 +118,13 @@ class Analyser */ private $extensionSymbols; + /** + * lowercase symbol name => kind + * + * @var array + */ + private $extensionSymbolKinds; + /** * @param array $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders()) * @param array $composerJsonDependencies package or ext-* => is dev dependency @@ -385,9 +392,7 @@ private function getUsedSymbolsInFile(string $filePath): array } return (new UsedSymbolExtractor($code))->parseUsedSymbols( - array_keys($this->extensionSymbols[SymbolKind::CLASSLIKE]), - array_keys($this->extensionSymbols[SymbolKind::FUNCTION]), - array_keys($this->extensionSymbols[SymbolKind::CONSTANT]) + $this->extensionSymbolKinds ); } @@ -547,6 +552,7 @@ private function initExistingSymbols(): void $this->ignoredSymbols[$constantName] = true; } else { $this->extensionSymbols[SymbolKind::CONSTANT][$constantName] = $extensionName; + $this->extensionSymbolKinds[strtolower($constantName)] = SymbolKind::CONSTANT; } } } @@ -568,6 +574,7 @@ private function initExistingSymbols(): void $this->ignoredSymbols[$functionName] = true; } else { $this->extensionSymbols[SymbolKind::FUNCTION][$functionName] = $extensionName; + $this->extensionSymbolKinds[$functionName] = SymbolKind::FUNCTION; } } } @@ -590,6 +597,7 @@ private function initExistingSymbols(): void $this->ignoredSymbols[$classLikeName] = true; } else { $this->extensionSymbols[SymbolKind::CLASSLIKE][$classLikeName] = $extensionName; + $this->extensionSymbolKinds[strtolower($classLikeName)] = SymbolKind::CLASSLIKE; } } } diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index 74d007c..b16a27f 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -2,9 +2,6 @@ namespace ShipMonk\ComposerDependencyAnalyser; -use function array_fill_keys; -use function array_map; -use function array_merge; use function count; use function explode; use function is_array; @@ -70,24 +67,15 @@ public function __construct(string $code) * It does not produce any local names in current namespace * - this results in very limited functionality in files without namespace * - * @param list $extClasses - * @param list $extFunctions - * @param list $extConstants + * @param array $extensionSymbols * @return array>> * @license Inspired by https://github.com/doctrine/annotations/blob/2.0.0/lib/Doctrine/Common/Annotations/TokenParser.php */ public function parseUsedSymbols( - array $extClasses, - array $extFunctions, - array $extConstants + array $extensionSymbols ): array { $usedSymbols = []; - $extensionSymbols = array_merge( - array_fill_keys(array_map('strtolower', $extClasses), SymbolKind::CLASSLIKE), - array_fill_keys(array_map('strtolower', $extFunctions), SymbolKind::FUNCTION), - array_fill_keys(array_map('strtolower', $extConstants), SymbolKind::CONSTANT) - ); $useStatements = []; $useStatementKinds = []; diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index 74192a4..016b605 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -3,8 +3,8 @@ namespace ShipMonk\ComposerDependencyAnalyser; use PHPUnit\Framework\TestCase; -use function array_map; use function file_get_contents; +use function strtolower; use const PHP_VERSION_ID; class UsedSymbolExtractorTest extends TestCase @@ -24,9 +24,15 @@ public function test(string $path, array $expectedUsages): void self::assertSame( $expectedUsages, $extractor->parseUsedSymbols( - ['PDO'], - array_map('strtolower', ['json_encode', 'DDTrace\active_span', 'DDTrace\root_span']), - ['LIBXML_ERR_FATAL', 'LIBXML_ERR_ERROR', 'DDTrace\DBM_PROPAGATION_FULL'] + [ + strtolower('PDO') => SymbolKind::CLASSLIKE, + strtolower('json_encode') => SymbolKind::FUNCTION, + strtolower('DDTrace\active_span') => SymbolKind::FUNCTION, + strtolower('DDTrace\root_span') => SymbolKind::FUNCTION, + strtolower('LIBXML_ERR_FATAL') => SymbolKind::CONSTANT, + strtolower('LIBXML_ERR_ERROR') => SymbolKind::CONSTANT, + strtolower('DDTrace\DBM_PROPAGATION_FULL') => SymbolKind::CONSTANT, + ] ) ); } From 50a30aa299d24ae3065c4106a4b8dcdd930a32a4 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 16 Oct 2024 13:50:43 +0200 Subject: [PATCH 05/14] Ability to disable ext-* analysis --- README.md | 2 ++ composer-dependency-analyser.php | 1 + src/Analyser.php | 36 +++++++++++++++++++++++++------- src/Cli.php | 5 +++++ src/CliOptions.php | 5 +++++ src/Config/Configuration.php | 24 +++++++++++++++++++++ src/Initializer.php | 7 +++++++ tests/AnalyserTest.php | 30 ++++++++++++++++++++++++++ tests/BinTest.php | 2 +- tests/CliTest.php | 3 ++- tests/InitializerTest.php | 1 + 11 files changed, 106 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fcb9270..8776ee2 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ This tool reads your `composer.json` and scans all paths listed in `autoload` & - `--verbose` to see more example classes & usages - `--show-all-usages` to see all usages - `--format` to use different output format, available are: `console` (default), `junit` +- `--disable-ext-analysis` to disable php extensions analysis (e.g. `ext-xml`) - `--ignore-unknown-classes` to globally ignore unknown classes - `--ignore-unknown-functions` to globally ignore unknown functions - `--ignore-shadow-deps` to globally ignore shadow dependencies @@ -128,6 +129,7 @@ return $config //// Adjust analysis ->enableAnalysisOfUnusedDevDependencies() // dev packages are often used only in CI, so this is not enabled by default ->disableReportingUnmatchedIgnores() // do not report ignores that never matched any error + ->disableExtensionsAnalysis() // do not analyse ext-* dependencies //// Use symbols from yaml/xml/neon files // - designed for DIC config files (see below) diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index bb1ce73..2902924 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -3,6 +3,7 @@ use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; return (new Configuration()) + ->disableExtensionsAnalysis() ->addPathToScan(__FILE__, true) ->addPathToScan(__DIR__ . '/bin', false) ->addPathToExclude(__DIR__ . '/tests/data'); diff --git a/src/Analyser.php b/src/Analyser.php index 788cc8b..cd66a1e 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -116,14 +116,14 @@ class Analyser * * @var array> */ - private $extensionSymbols; + private $extensionSymbols = []; /** * lowercase symbol name => kind * * @var array */ - private $extensionSymbolKinds; + private $extensionSymbolKinds = []; /** * @param array $classLoaders vendorDir => ClassLoader (e.g. result of \Composer\Autoload\ClassLoader::getRegisteredLoaders()) @@ -139,11 +139,11 @@ public function __construct( { $this->stopwatch = $stopwatch; $this->config = $config; - $this->composerJsonDependencies = $composerJsonDependencies; + $this->composerJsonDependencies = $this->filterDependencies($composerJsonDependencies, $config); $this->vendorDirs = array_keys($classLoaders + [$defaultVendorDir => null]); $this->classLoaders = array_values($classLoaders); - $this->initExistingSymbols(); + $this->initExistingSymbols($config); } /** @@ -503,7 +503,7 @@ private function normalizePath(string $filePath): string return Path::normalize($filePath); } - private function initExistingSymbols(): void + private function initExistingSymbols(Configuration $config): void { $this->ignoredSymbols = [ // built-in types @@ -543,8 +543,9 @@ private function initExistingSymbols(): void foreach ($definedConstants as $constantExtension => $constants) { foreach ($constants as $constantName => $_) { - if ($constantExtension === 'user') { + if ($constantExtension === 'user' || !$config->shouldAnalyseExtensions()) { $this->ignoredSymbols[$constantName] = true; + } else { $extensionName = $this->getNormalizedExtensionName($constantExtension); @@ -570,7 +571,7 @@ private function initExistingSymbols(): void } else { $extensionName = $this->getNormalizedExtensionName($reflectionFunction->getExtension()->name); - if (in_array($extensionName, self::CORE_EXTENSIONS, true)) { + if (in_array($extensionName, self::CORE_EXTENSIONS, true) || !$config->shouldAnalyseExtensions()) { $this->ignoredSymbols[$functionName] = true; } else { $this->extensionSymbols[SymbolKind::FUNCTION][$functionName] = $extensionName; @@ -593,7 +594,7 @@ private function initExistingSymbols(): void if ($classReflection->getExtension() !== null) { $extensionName = $this->getNormalizedExtensionName($classReflection->getExtension()->name); - if (in_array($extensionName, self::CORE_EXTENSIONS, true)) { + if (in_array($extensionName, self::CORE_EXTENSIONS, true) || !$config->shouldAnalyseExtensions()) { $this->ignoredSymbols[$classLikeName] = true; } else { $this->extensionSymbols[SymbolKind::CLASSLIKE][$classLikeName] = $extensionName; @@ -609,4 +610,23 @@ private function getNormalizedExtensionName(string $extension): string return 'ext-' . ComposerJson::normalizeExtensionName($extension); } + /** + * @param array $dependencies + * @return array + */ + private function filterDependencies(array $dependencies, Configuration $config): array + { + $filtered = []; + + foreach ($dependencies as $dependency => $isDevDependency) { + if (!$config->shouldAnalyseExtensions() && strpos($dependency, 'ext-') === 0) { + continue; + } + + $filtered[$dependency] = $isDevDependency; + } + + return $filtered; + } + } diff --git a/src/Cli.php b/src/Cli.php index 64df045..ceb7f26 100644 --- a/src/Cli.php +++ b/src/Cli.php @@ -20,6 +20,7 @@ class Cli 'version' => false, 'help' => false, 'verbose' => false, + 'disable-ext-analysis' => false, 'ignore-shadow-deps' => false, 'ignore-unused-deps' => false, 'ignore-dev-in-prod-deps' => false, @@ -153,6 +154,10 @@ public function getProvidedOptions(): CliOptions $options->verbose = true; } + if (isset($this->providedOptions['disable-ext-analysis'])) { + $options->disableExtAnalysis = true; + } + if (isset($this->providedOptions['ignore-shadow-deps'])) { $options->ignoreShadowDeps = true; } diff --git a/src/CliOptions.php b/src/CliOptions.php index 3656225..3eeaf15 100644 --- a/src/CliOptions.php +++ b/src/CliOptions.php @@ -20,6 +20,11 @@ class CliOptions */ public $verbose = null; + /** + * @var true|null + */ + public $disableExtAnalysis = null; + /** * @var true|null */ diff --git a/src/Config/Configuration.php b/src/Config/Configuration.php index 7fb7291..2486b1f 100644 --- a/src/Config/Configuration.php +++ b/src/Config/Configuration.php @@ -14,6 +14,11 @@ class Configuration { + /** + * @var bool + */ + private $extensionsAnalysis = true; + /** * @var bool */ @@ -94,6 +99,17 @@ class Configuration */ private $ignoredUnknownFunctionsRegexes = []; + /** + * Disable analysis of ext-* dependencies + * + * @return $this + */ + public function disableExtensionsAnalysis(): self + { + $this->extensionsAnalysis = false; + return $this; + } + /** * @return $this */ @@ -103,6 +119,9 @@ public function disableComposerAutoloadPathScan(): self return $this; } + /** + * @return $this + */ public function disableReportingUnmatchedIgnores(): self { $this->reportUnmatchedIgnores = false; @@ -439,6 +458,11 @@ public function getPathsToScan(): array return $this->pathsToScan; } + public function shouldAnalyseExtensions(): bool + { + return $this->extensionsAnalysis; + } + public function shouldScanComposerAutoloadPaths(): bool { return $this->scanComposerAutoloadPaths; diff --git a/src/Initializer.php b/src/Initializer.php index 8379b86..3c3e36c 100644 --- a/src/Initializer.php +++ b/src/Initializer.php @@ -52,6 +52,8 @@ class Initializer --ignore-shadow-deps Ignore all shadow dependency issues --ignore-dev-in-prod-deps Ignore all dev dependency in production code issues --ignore-prod-only-in-dev-deps Ignore all prod dependency used only in dev paths issues + + --disable-ext-analysis Disable analysis of php extensions (e.g. ext-xml) EOD; /** @@ -116,6 +118,7 @@ public function initConfiguration( $config = new Configuration(); } + $disableExtAnalysis = $options->disableExtAnalysis === true; $ignoreUnknownClasses = $options->ignoreUnknownClasses === true; $ignoreUnknownFunctions = $options->ignoreUnknownFunctions === true; $ignoreUnused = $options->ignoreUnusedDeps === true; @@ -123,6 +126,10 @@ public function initConfiguration( $ignoreDevInProd = $options->ignoreDevInProdDeps === true; $ignoreProdOnlyInDev = $options->ignoreProdOnlyInDevDeps === true; + if ($disableExtAnalysis) { + $config->disableExtensionsAnalysis(); + } + if ($ignoreUnknownClasses) { $config->ignoreErrors([ErrorType::UNKNOWN_CLASS]); } diff --git a/tests/AnalyserTest.php b/tests/AnalyserTest.php index 7e09a2f..1d8c101 100644 --- a/tests/AnalyserTest.php +++ b/tests/AnalyserTest.php @@ -651,6 +651,36 @@ public function testExtensions(): void ]), $result); } + public function testDisabledExtensionAnalysis(): void + { + $vendorDir = realpath(__DIR__ . '/../vendor'); + $prodPath = realpath(__DIR__ . '/data/not-autoloaded/extensions/ext-prod-usages.php'); + $devPath = realpath(__DIR__ . '/data/not-autoloaded/extensions/ext-dev-usages.php'); + self::assertNotFalse($vendorDir); + self::assertNotFalse($prodPath); + self::assertNotFalse($devPath); + + $config = new Configuration(); + $config->disableExtensionsAnalysis(); + $config->addPathToScan($prodPath, false); + $config->addPathToScan($devPath, true); + + $detector = new Analyser( + $this->getStopwatchMock(), + $vendorDir, + [$vendorDir => $this->getClassLoaderMock()], + $config, + [ + 'ext-dom' => false, + 'ext-libxml' => true, + 'ext-mbstring' => false, + ] + ); + $result = $detector->run(); + + $this->assertResultsWithoutUsages($this->createAnalysisResult(2, []), $result); + } + public function testPharSupport(): void { $canCreatePhar = ini_set('phar.readonly', '0'); diff --git a/tests/BinTest.php b/tests/BinTest.php index d95b3ed..daf16a6 100644 --- a/tests/BinTest.php +++ b/tests/BinTest.php @@ -17,7 +17,7 @@ public function test(): void $testsDir = __DIR__; $noComposerJsonError = 'File composer.json not found'; - $noPackagesError = 'No packages found'; + $noPackagesError = 'No dependencies found'; $parseError = 'Failure while parsing'; $junitDumpError = "Cannot use 'junit' format with '--dump-usages' option"; diff --git a/tests/CliTest.php b/tests/CliTest.php index 8bcee01..72f7148 100644 --- a/tests/CliTest.php +++ b/tests/CliTest.php @@ -53,11 +53,12 @@ public function validationDataProvider(): iterable yield 'valid bool options' => [ null, - ['bin/script.php', '--help', '--verbose', '--ignore-shadow-deps', '--ignore-unused-deps', '--ignore-dev-in-prod-deps', '--ignore-unknown-classes', '--ignore-unknown-functions'], + ['bin/script.php', '--help', '--verbose', '--ignore-shadow-deps', '--ignore-unused-deps', '--ignore-dev-in-prod-deps', '--ignore-unknown-classes', '--ignore-unknown-functions', '--disable-ext-analysis'], (static function (): CliOptions { $options = new CliOptions(); $options->help = true; $options->verbose = true; + $options->disableExtAnalysis = true; $options->ignoreShadowDeps = true; $options->ignoreUnusedDeps = true; $options->ignoreDevInProdDeps = true; diff --git a/tests/InitializerTest.php b/tests/InitializerTest.php index 5bd4f7f..f8f7113 100644 --- a/tests/InitializerTest.php +++ b/tests/InitializerTest.php @@ -99,6 +99,7 @@ public function testInitCliOptions(): void self::assertNull($options->showAllUsages); self::assertNull($options->composerJson); + self::assertNull($options->disableExtAnalysis); self::assertNull($options->ignoreProdOnlyInDevDeps); self::assertNull($options->ignoreUnknownClasses); self::assertNull($options->ignoreUnknownFunctions); From c552ca01de0e44377d720c3fa8c7103ced36a807 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 16 Oct 2024 15:20:59 +0200 Subject: [PATCH 06/14] Support config->ignoreOnExtension() --- composer-dependency-analyser.php | 9 ++- src/Config/Configuration.php | 122 ++++++++++++++++++++++++++++--- src/Config/Ignore/IgnoreList.php | 44 +++++------ tests/ConfigurationTest.php | 8 ++ 4 files changed, 149 insertions(+), 34 deletions(-) diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 2902924..108ca2f 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -1,9 +1,14 @@ disableExtensionsAnalysis() ->addPathToScan(__FILE__, true) ->addPathToScan(__DIR__ . '/bin', false) - ->addPathToExclude(__DIR__ . '/tests/data'); + ->addPathToExclude(__DIR__ . '/tests/data') + ->ignoreErrorsOnExtensionsAndPaths( + ['ext-dom', 'ext-libxml'], + [__DIR__ . '/src/Result/JunitFormatter.php'], // optional usages guarded with extension_loaded() + [ErrorType::DEV_DEPENDENCY_IN_PROD] + ); diff --git a/src/Config/Configuration.php b/src/Config/Configuration.php index 2486b1f..8883145 100644 --- a/src/Config/Configuration.php +++ b/src/Config/Configuration.php @@ -72,12 +72,12 @@ class Configuration /** * @var array> */ - private $ignoredErrorsOnPackage = []; + private $ignoredErrorsOnDependency = []; /** * @var array>> */ - private $ignoredErrorsOnPackageAndPath = []; + private $ignoredErrorsOnDependencyAndPath = []; /** * @var list @@ -296,13 +296,34 @@ public function ignoreErrorsOnPaths(array $paths, array $errorTypes): self public function ignoreErrorsOnPackage(string $packageName, array $errorTypes): self { $this->checkPackageName($packageName); - $this->checkAllowedErrorTypeForPackageIgnore($errorTypes); + $this->ignoreErrorsOnDependency($packageName, $errorTypes); + return $this; + } - $previousErrorTypes = $this->ignoredErrorsOnPackage[$packageName] ?? []; - $this->ignoredErrorsOnPackage[$packageName] = array_merge($previousErrorTypes, $errorTypes); + /** + * @param list $errorTypes + * @return $this + * @throws InvalidConfigException + */ + public function ignoreErrorsOnExtension(string $extension, array $errorTypes): self + { + $this->checkExtensionName($extension); + $this->ignoreErrorsOnDependency($extension, $errorTypes); return $this; } + /** + * @param list $errorTypes + * @throws InvalidConfigException + */ + private function ignoreErrorsOnDependency(string $dependency, array $errorTypes): void + { + $this->checkAllowedErrorTypeForPackageIgnore($errorTypes); + + $previousErrorTypes = $this->ignoredErrorsOnDependency[$dependency] ?? []; + $this->ignoredErrorsOnDependency[$dependency] = array_merge($previousErrorTypes, $errorTypes); + } + /** * @param list $packageNames * @param list $errorTypes @@ -318,6 +339,21 @@ public function ignoreErrorsOnPackages(array $packageNames, array $errorTypes): return $this; } + /** + * @param list $extensions + * @param list $errorTypes + * @return $this + * @throws InvalidConfigException + */ + public function ignoreErrorsOnExtensions(array $extensions, array $errorTypes): self + { + foreach ($extensions as $extension) { + $this->ignoreErrorsOnExtension($extension, $errorTypes); + } + + return $this; + } + /** * @param list $errorTypes * @return $this @@ -327,14 +363,37 @@ public function ignoreErrorsOnPackages(array $packageNames, array $errorTypes): public function ignoreErrorsOnPackageAndPath(string $packageName, string $path, array $errorTypes): self { $this->checkPackageName($packageName); + $this->ignoreErrorsOnDependencyAndPath($packageName, $path, $errorTypes); + return $this; + } + + /** + * @param list $errorTypes + * @return $this + * @throws InvalidPathException + * @throws InvalidConfigException + */ + public function ignoreErrorsOnExtensionAndPath(string $extension, string $path, array $errorTypes): self + { + $this->checkExtensionName($extension); + $this->ignoreErrorsOnDependencyAndPath($extension, $path, $errorTypes); + return $this; + } + + /** + * @param list $errorTypes + * @throws InvalidPathException + * @throws InvalidConfigException + */ + private function ignoreErrorsOnDependencyAndPath(string $dependency, string $path, array $errorTypes): void + { $this->checkAllowedErrorTypeForPathIgnore($errorTypes); $this->checkAllowedErrorTypeForPackageIgnore($errorTypes); $realpath = Path::realpath($path); - $previousErrorTypes = $this->ignoredErrorsOnPackageAndPath[$packageName][$realpath] ?? []; - $this->ignoredErrorsOnPackageAndPath[$packageName][$realpath] = array_merge($previousErrorTypes, $errorTypes); - return $this; + $previousErrorTypes = $this->ignoredErrorsOnDependencyAndPath[$dependency][$realpath] ?? []; + $this->ignoredErrorsOnDependencyAndPath[$dependency][$realpath] = array_merge($previousErrorTypes, $errorTypes); } /** @@ -353,6 +412,22 @@ public function ignoreErrorsOnPackageAndPaths(string $packageName, array $paths, return $this; } + /** + * @param list $paths + * @param list $errorTypes + * @return $this + * @throws InvalidPathException + * @throws InvalidConfigException + */ + public function ignoreErrorsOnExtensionAndPaths(string $extension, array $paths, array $errorTypes): self + { + foreach ($paths as $path) { + $this->ignoreErrorsOnExtensionAndPath($extension, $path, $errorTypes); + } + + return $this; + } + /** * @param list $packages * @param list $paths @@ -370,6 +445,23 @@ public function ignoreErrorsOnPackagesAndPaths(array $packages, array $paths, ar return $this; } + /** + * @param list $extensions + * @param list $paths + * @param list $errorTypes + * @return $this + * @throws InvalidPathException + * @throws InvalidConfigException + */ + public function ignoreErrorsOnExtensionsAndPaths(array $extensions, array $paths, array $errorTypes): self + { + foreach ($extensions as $extension) { + $this->ignoreErrorsOnExtensionAndPaths($extension, $paths, $errorTypes); + } + + return $this; + } + /** * @param list $classNames * @return $this @@ -425,8 +517,8 @@ public function getIgnoreList(): IgnoreList return new IgnoreList( $this->ignoredErrors, $this->ignoredErrorsOnPath, - $this->ignoredErrorsOnPackage, - $this->ignoredErrorsOnPackageAndPath, + $this->ignoredErrorsOnDependency, + $this->ignoredErrorsOnDependencyAndPath, $this->ignoredUnknownClasses, $this->ignoredUnknownClassesRegexes, $this->ignoredUnknownFunctions, @@ -500,6 +592,16 @@ private function isFilepathWithinPath(string $filePath, string $path): bool return strpos($filePath, $path) === 0; } + /** + * @throws InvalidConfigException + */ + private function checkExtensionName(string $extension): void + { + if (strpos($extension, 'ext-') !== 0) { + throw new InvalidConfigException("Invalid php extension dependency name '$extension', it is expected to start with ext-"); + } + } + /** * @throws InvalidConfigException */ diff --git a/src/Config/Ignore/IgnoreList.php b/src/Config/Ignore/IgnoreList.php index 59da5a5..92e3e8a 100644 --- a/src/Config/Ignore/IgnoreList.php +++ b/src/Config/Ignore/IgnoreList.php @@ -25,12 +25,12 @@ class IgnoreList /** * @var array> */ - private $ignoredErrorsOnPackage = []; + private $ignoredErrorsOnDependency = []; /** * @var array>> */ - private $ignoredErrorsOnPackageAndPath = []; + private $ignoredErrorsOnDependencyAndPath = []; /** * @var array @@ -55,8 +55,8 @@ class IgnoreList /** * @param list $ignoredErrors * @param array> $ignoredErrorsOnPath - * @param array> $ignoredErrorsOnPackage - * @param array>> $ignoredErrorsOnPackageAndPath + * @param array> $ignoredErrorsOnDependency + * @param array>> $ignoredErrorsOnDependencyAndPath * @param list $ignoredUnknownClasses * @param list $ignoredUnknownClassesRegexes * @param list $ignoredUnknownFunctions @@ -65,8 +65,8 @@ class IgnoreList public function __construct( array $ignoredErrors, array $ignoredErrorsOnPath, - array $ignoredErrorsOnPackage, - array $ignoredErrorsOnPackageAndPath, + array $ignoredErrorsOnDependency, + array $ignoredErrorsOnDependencyAndPath, array $ignoredUnknownClasses, array $ignoredUnknownClassesRegexes, array $ignoredUnknownFunctions, @@ -79,13 +79,13 @@ public function __construct( $this->ignoredErrorsOnPath[$path] = array_fill_keys($errorTypes, false); } - foreach ($ignoredErrorsOnPackage as $packageName => $errorTypes) { - $this->ignoredErrorsOnPackage[$packageName] = array_fill_keys($errorTypes, false); + foreach ($ignoredErrorsOnDependency as $dependency => $errorTypes) { + $this->ignoredErrorsOnDependency[$dependency] = array_fill_keys($errorTypes, false); } - foreach ($ignoredErrorsOnPackageAndPath as $packageName => $paths) { + foreach ($ignoredErrorsOnDependencyAndPath as $dependency => $paths) { foreach ($paths as $path => $errorTypes) { - $this->ignoredErrorsOnPackageAndPath[$packageName][$path] = array_fill_keys($errorTypes, false); + $this->ignoredErrorsOnDependencyAndPath[$dependency][$path] = array_fill_keys($errorTypes, false); } } @@ -116,7 +116,7 @@ public function getUnusedIgnores(): array } } - foreach ($this->ignoredErrorsOnPackage as $packageName => $errorTypes) { + foreach ($this->ignoredErrorsOnDependency as $packageName => $errorTypes) { foreach ($errorTypes as $errorType => $ignored) { if (!$ignored) { $unused[] = new UnusedErrorIgnore($errorType, null, $packageName); @@ -124,7 +124,7 @@ public function getUnusedIgnores(): array } } - foreach ($this->ignoredErrorsOnPackageAndPath as $packageName => $paths) { + foreach ($this->ignoredErrorsOnDependencyAndPath as $packageName => $paths) { foreach ($paths as $path => $errorTypes) { foreach ($errorTypes as $errorType => $ignored) { if (!$ignored) { @@ -240,12 +240,12 @@ private function shouldIgnoreUnknownFunctionByRegex(string $function): bool /** * @param ErrorType::SHADOW_DEPENDENCY|ErrorType::UNUSED_DEPENDENCY|ErrorType::DEV_DEPENDENCY_IN_PROD|ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV $errorType */ - public function shouldIgnoreError(string $errorType, ?string $realPath, ?string $packageName): bool + public function shouldIgnoreError(string $errorType, ?string $realPath, ?string $dependency): bool { $ignoredGlobally = $this->shouldIgnoreErrorGlobally($errorType); $ignoredByPath = $realPath !== null && $this->shouldIgnoreErrorOnPath($errorType, $realPath); - $ignoredByPackage = $packageName !== null && $this->shouldIgnoreErrorOnPackage($errorType, $packageName); - $ignoredByPackageAndPath = $realPath !== null && $packageName !== null && $this->shouldIgnoreErrorOnPackageAndPath($errorType, $packageName, $realPath); + $ignoredByPackage = $dependency !== null && $this->shouldIgnoreErrorOnDependency($errorType, $dependency); + $ignoredByPackageAndPath = $realPath !== null && $dependency !== null && $this->shouldIgnoreErrorOnDependencyAndPath($errorType, $dependency, $realPath); return $ignoredGlobally || $ignoredByPackageAndPath || $ignoredByPath || $ignoredByPackage; } @@ -281,10 +281,10 @@ private function shouldIgnoreErrorOnPath(string $errorType, string $filePath): b /** * @param ErrorType::* $errorType */ - private function shouldIgnoreErrorOnPackage(string $errorType, string $packageName): bool + private function shouldIgnoreErrorOnDependency(string $errorType, string $dependency): bool { - if (isset($this->ignoredErrorsOnPackage[$packageName][$errorType])) { - $this->ignoredErrorsOnPackage[$packageName][$errorType] = true; + if (isset($this->ignoredErrorsOnDependency[$dependency][$errorType])) { + $this->ignoredErrorsOnDependency[$dependency][$errorType] = true; return true; } @@ -294,12 +294,12 @@ private function shouldIgnoreErrorOnPackage(string $errorType, string $packageNa /** * @param ErrorType::* $errorType */ - private function shouldIgnoreErrorOnPackageAndPath(string $errorType, string $packageName, string $filePath): bool + private function shouldIgnoreErrorOnDependencyAndPath(string $errorType, string $packageName, string $filePath): bool { - if (isset($this->ignoredErrorsOnPackageAndPath[$packageName])) { - foreach ($this->ignoredErrorsOnPackageAndPath[$packageName] as $path => $errorTypes) { + if (isset($this->ignoredErrorsOnDependencyAndPath[$packageName])) { + foreach ($this->ignoredErrorsOnDependencyAndPath[$packageName] as $path => $errorTypes) { if ($this->isFilepathWithinPath($filePath, $path) && isset($errorTypes[$errorType])) { - $this->ignoredErrorsOnPackageAndPath[$packageName][$path][$errorType] = true; + $this->ignoredErrorsOnDependencyAndPath[$packageName][$path][$errorType] = true; return true; } } diff --git a/tests/ConfigurationTest.php b/tests/ConfigurationTest.php index 9edf75d..27c2a26 100644 --- a/tests/ConfigurationTest.php +++ b/tests/ConfigurationTest.php @@ -21,6 +21,7 @@ public function testShouldIgnore(): void $configuration->ignoreUnknownClasses(['Unknown\Clazz']); $configuration->ignoreErrors([ErrorType::UNUSED_DEPENDENCY, ErrorType::UNKNOWN_CLASS]); $configuration->ignoreErrorsOnPath(__DIR__ . '/data/../', [ErrorType::SHADOW_DEPENDENCY]); + $configuration->ignoreErrorsOnExtension('ext-xml', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]); $configuration->ignoreErrorsOnPackage('my/package', [ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV]); $configuration->ignoreErrorsOnPackageAndPath('vendor/package', __DIR__ . '/../tests/data', [ErrorType::DEV_DEPENDENCY_IN_PROD]); @@ -42,6 +43,13 @@ public function testShouldIgnore(): void self::assertTrue($ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, __DIR__ . DIRECTORY_SEPARATOR . 'app', null)); self::assertTrue($ignoreList->shouldIgnoreError(ErrorType::SHADOW_DEPENDENCY, __DIR__ . DIRECTORY_SEPARATOR . 'app', 'some/package')); + self::assertFalse($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, null, null)); + self::assertFalse($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, null, 'ext-simplexml')); + self::assertTrue($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, null, 'ext-xml')); + self::assertFalse($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, __DIR__, null)); + self::assertFalse($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, __DIR__, 'ext-simplexml')); + self::assertTrue($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, __DIR__, 'ext-xml')); + self::assertFalse($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, null, null)); self::assertFalse($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, null, 'some/package')); self::assertTrue($ignoreList->shouldIgnoreError(ErrorType::PROD_DEPENDENCY_ONLY_IN_DEV, null, 'my/package')); From e5eaaece6623101048a33b6ed30476d6ad8f8055 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Wed, 16 Oct 2024 15:52:31 +0200 Subject: [PATCH 07/14] Fix for functions used with invalid case --- src/Analyser.php | 8 ++++---- tests/AnalyserTest.php | 1 + tests/data/not-autoloaded/extensions/ext-dev-usages.php | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Analyser.php b/src/Analyser.php index cd66a1e..30bddd7 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -175,14 +175,14 @@ public function run(): AnalysisResult foreach ($usedSymbolsByKind as $kind => $usedSymbols) { foreach ($usedSymbols as $usedSymbol => $lineNumbers) { - $usedSymbolNameForIgnoreCheck = $kind === SymbolKind::FUNCTION ? strtolower($usedSymbol) : $usedSymbol; + $normalizedUsedSymbolName = $kind === SymbolKind::FUNCTION ? strtolower($usedSymbol) : $usedSymbol; - if (isset($this->ignoredSymbols[$usedSymbolNameForIgnoreCheck])) { + if (isset($this->ignoredSymbols[$normalizedUsedSymbolName])) { continue; } - if (isset($this->extensionSymbols[$kind][$usedSymbol])) { - $dependencyName = $this->extensionSymbols[$kind][$usedSymbol]; + if (isset($this->extensionSymbols[$kind][$normalizedUsedSymbolName])) { + $dependencyName = $this->extensionSymbols[$kind][$normalizedUsedSymbolName]; } else { $symbolPath = $this->getSymbolPath($usedSymbol, $kind); diff --git a/tests/AnalyserTest.php b/tests/AnalyserTest.php index 1d8c101..aff3d20 100644 --- a/tests/AnalyserTest.php +++ b/tests/AnalyserTest.php @@ -834,6 +834,7 @@ private function assertResultsWithoutUsages(AnalysisResult $expectedResult, Anal self::assertSame($expectedResult->getScannedFilesCount(), $result->getScannedFilesCount(), 'Scanned files count mismatch'); self::assertEquals($expectedResult->getUnusedIgnores(), $result->getUnusedIgnores(), 'Unused ignores mismatch'); self::assertEquals($expectedResult->getUnknownClassErrors(), $result->getUnknownClassErrors(), 'Unknown class mismatch'); + self::assertEquals($expectedResult->getUnknownFunctionErrors(), $result->getUnknownFunctionErrors(), 'Unknown functions mismatch'); self::assertEquals($expectedResult->getShadowDependencyErrors(), $result->getShadowDependencyErrors(), 'Shadow dependency mismatch'); self::assertEquals($expectedResult->getDevDependencyInProductionErrors(), $result->getDevDependencyInProductionErrors(), 'Dev dependency in production mismatch'); self::assertEquals($expectedResult->getProdDependencyOnlyInDevErrors(), $result->getProdDependencyOnlyInDevErrors(), 'Prod dependency only in dev mismatch'); diff --git a/tests/data/not-autoloaded/extensions/ext-dev-usages.php b/tests/data/not-autoloaded/extensions/ext-dev-usages.php index 51d8f4c..8eb727f 100644 --- a/tests/data/not-autoloaded/extensions/ext-dev-usages.php +++ b/tests/data/not-autoloaded/extensions/ext-dev-usages.php @@ -1,3 +1,4 @@ Date: Wed, 16 Oct 2024 15:53:08 +0200 Subject: [PATCH 08/14] Fix variable name --- src/Analyser.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Analyser.php b/src/Analyser.php index 30bddd7..d77416c 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -162,7 +162,7 @@ public function run(): AnalysisResult $unusedErrors = []; $usedDependencies = []; - $prodPackagesUsedInProdPath = []; + $prodDependenciesUsedInProdPath = []; $usages = []; @@ -233,7 +233,7 @@ public function run(): AnalysisResult !$isDevFilePath && !$this->isDevDependency($dependencyName) ) { - $prodPackagesUsedInProdPath[$dependencyName] = true; + $prodDependenciesUsedInProdPath[$dependencyName] = true; } $usedDependencies[$dependencyName] = true; @@ -300,7 +300,7 @@ public function run(): AnalysisResult })); $prodPackagesUsedOnlyInDev = array_diff( $prodDependencies, - array_keys($prodPackagesUsedInProdPath), + array_keys($prodDependenciesUsedInProdPath), array_keys($forceUsedPackages), // we dont know where are those used, lets not report them $unusedDependencies, self::CORE_EXTENSIONS From e99300ec06a35c1c4e7fc18b33ae03f263750dec Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 17 Oct 2024 12:09:10 +0200 Subject: [PATCH 09/14] Adjust readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 8776ee2..3f19236 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Found unused dependencies! ``` ## Detected issues: -This tool reads your `composer.json` and scans all paths listed in `autoload` & `autoload-dev` sections while analysing: +This tool reads your `composer.json` and scans all paths listed in `autoload` & `autoload-dev` sections while analysing you dependencies (both **packages and PHP extensions**). ### Shadowed dependencies - Those are dependencies of your dependencies, which are not listed in `composer.json` @@ -168,8 +168,8 @@ Another approach for DIC-only usages is to scan the generated php file, but that NO_COLOR=1 vendor/bin/composer-dependency-analyser ``` -## Limitations: -- For precise `ext-x` analysis, your enabled extentions of your php runtime should be superset of those used in the scanned project +## Recommendations: +- For precise `ext-*` analysis, your enabled extensions of your php runtime should be superset of those used in the scanned project ## Contributing: - Check your code by `composer check` From afb9410716ed80b832f8b0f598789d4f5c962ca5 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 17 Oct 2024 12:42:00 +0200 Subject: [PATCH 10/14] Fix ext priority --- src/UsedSymbolExtractor.php | 12 ++++++------ tests/UsedSymbolExtractorTest.php | 3 +++ .../used-symbols/extensions-global.php | 4 ++-- .../data/not-autoloaded/used-symbols/extensions.php | 4 ++-- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index b16a27f..cff49c6 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -155,15 +155,15 @@ public function parseUsedSymbols( break; } - if (isset($extensionSymbols[$lowerName])) { - $symbolName = $name; - $kind = $extensionSymbols[$lowerName]; - $usedSymbols[$kind][$symbolName][] = $token[2]; - - } elseif (isset($useStatements[$name])) { + if (isset($useStatements[$name])) { $symbolName = $useStatements[$name]; $kind = $useStatementKinds[$name]; $usedSymbols[$kind][$symbolName][] = $token[2]; + + } elseif (isset($extensionSymbols[$lowerName])) { + $symbolName = $name; + $kind = $extensionSymbols[$lowerName]; + $usedSymbols[$kind][$symbolName][] = $token[2]; } break; diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index 016b605..4a7e6f3 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -25,6 +25,7 @@ public function test(string $path, array $expectedUsages): void $expectedUsages, $extractor->parseUsedSymbols( [ + strtolower('XMLReader') => SymbolKind::CLASSLIKE, strtolower('PDO') => SymbolKind::CLASSLIKE, strtolower('json_encode') => SymbolKind::FUNCTION, strtolower('DDTrace\active_span') => SymbolKind::FUNCTION, @@ -153,6 +154,7 @@ public function provideVariants(): iterable ], SymbolKind::CLASSLIKE => [ 'PDO' => [11], + 'My\App\XMLReader' => [15], 'CURLOPT_SSL_VERIFYHOST' => [19], ], ], @@ -174,6 +176,7 @@ public function provideVariants(): iterable ], SymbolKind::CLASSLIKE => [ 'PDO' => [11], + 'My\App\XMLReader' => [15], 'CURLOPT_SSL_VERIFYHOST' => [19], ], ], diff --git a/tests/data/not-autoloaded/used-symbols/extensions-global.php b/tests/data/not-autoloaded/used-symbols/extensions-global.php index 52f72ec..11bf9fe 100644 --- a/tests/data/not-autoloaded/used-symbols/extensions-global.php +++ b/tests/data/not-autoloaded/used-symbols/extensions-global.php @@ -2,7 +2,7 @@ - +use My\App\XMLReader; use function DDTrace\active_span; json_encode(''); @@ -12,7 +12,7 @@ active_span(); DDTrace\root_span(); DDTrace\DBM_PROPAGATION_FULL; - +XMLReader::class; // those are not provided as known ext symbols in the test diff --git a/tests/data/not-autoloaded/used-symbols/extensions.php b/tests/data/not-autoloaded/used-symbols/extensions.php index bf68c5b..7396f1d 100644 --- a/tests/data/not-autoloaded/used-symbols/extensions.php +++ b/tests/data/not-autoloaded/used-symbols/extensions.php @@ -1,9 +1,9 @@ Date: Thu, 17 Oct 2024 12:43:28 +0200 Subject: [PATCH 11/14] e2e: --show-all-usages --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 913cf18..c3bc05f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -144,4 +144,4 @@ jobs: - name: Run analyser working-directory: ${{ matrix.repo }} - run: php ../../analyser/bin/composer-dependency-analyser ${{ matrix.cdaArgs }} + run: php ../../analyser/bin/composer-dependency-analyser --show-all-usages ${{ matrix.cdaArgs }} From 35fc53b6c4ef44ad7c7f7d2d3c36658d79c226ed Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 17 Oct 2024 12:52:01 +0200 Subject: [PATCH 12/14] e2e: run both ext & no-ext analysis --- .github/workflows/e2e.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c3bc05f..696102a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -141,6 +141,11 @@ jobs: working-directory: ${{ matrix.repo }} run: composer install --no-progress --no-interaction --ignore-platform-reqs ${{ matrix.composerArgs }} + - + name: Run analyser (--disable-ext-analysis) + working-directory: ${{ matrix.repo }} + run: php ../../analyser/bin/composer-dependency-analyser --show-all-usages --disable-ext-analysis ${{ matrix.cdaArgs }} + - name: Run analyser working-directory: ${{ matrix.repo }} From eaece67917fe5dc363099a0d903f2f83e1b5393b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 17 Oct 2024 13:11:42 +0200 Subject: [PATCH 13/14] Another testcase for T_NAME_QUALIFIED --- tests/UsedSymbolExtractorTest.php | 3 +++ tests/data/not-autoloaded/used-symbols/extensions-global.php | 4 ++-- tests/data/not-autoloaded/used-symbols/extensions.php | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index 4a7e6f3..16dc5a0 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -33,6 +33,7 @@ public function test(string $path, array $expectedUsages): void strtolower('LIBXML_ERR_FATAL') => SymbolKind::CONSTANT, strtolower('LIBXML_ERR_ERROR') => SymbolKind::CONSTANT, strtolower('DDTrace\DBM_PROPAGATION_FULL') => SymbolKind::CONSTANT, + strtolower('DDTrace\Integrations\Exec\proc_get_pid') => SymbolKind::FUNCTION, ] ) ); @@ -145,6 +146,7 @@ public function provideVariants(): iterable 'json_encode' => [8], 'DDTrace\active_span' => [12], 'DDTrace\root_span' => [13], + 'DDTrace\Integrations\Exec\proc_get_pid' => [16], 'json_decode' => [21], ], SymbolKind::CONSTANT => [ @@ -167,6 +169,7 @@ public function provideVariants(): iterable 'json_encode' => [8], 'DDTrace\active_span' => [12], 'DDTrace\root_span' => [13], + 'DDTrace\Integrations\Exec\proc_get_pid' => [16], 'json_decode' => [21], ], SymbolKind::CONSTANT => [ diff --git a/tests/data/not-autoloaded/used-symbols/extensions-global.php b/tests/data/not-autoloaded/used-symbols/extensions-global.php index 11bf9fe..46aab7e 100644 --- a/tests/data/not-autoloaded/used-symbols/extensions-global.php +++ b/tests/data/not-autoloaded/used-symbols/extensions-global.php @@ -1,7 +1,7 @@ Date: Thu, 17 Oct 2024 13:32:27 +0200 Subject: [PATCH 14/14] Fix another t_string issue --- src/UsedSymbolExtractor.php | 1 + tests/UsedSymbolExtractorTest.php | 42 ++++++++++++------- .../used-symbols/t-string-issues.php | 2 +- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/UsedSymbolExtractor.php b/src/UsedSymbolExtractor.php index cff49c6..371bc61 100644 --- a/src/UsedSymbolExtractor.php +++ b/src/UsedSymbolExtractor.php @@ -411,6 +411,7 @@ private function canBeSymbolName( || $tokenBeforeName[0] === T_AS || $tokenBeforeName[0] === T_FUNCTION || $tokenBeforeName[0] === T_OBJECT_OPERATOR + || $tokenBeforeName[0] === T_NAMESPACE || $tokenBeforeName[0] === (PHP_VERSION_ID > 80000 ? T_NULLSAFE_OBJECT_OPERATOR : -1) || $tokenAfterName[0] === T_INSTEADOF || $tokenAfterName[0] === T_AS diff --git a/tests/UsedSymbolExtractorTest.php b/tests/UsedSymbolExtractorTest.php index 16dc5a0..bc6e397 100644 --- a/tests/UsedSymbolExtractorTest.php +++ b/tests/UsedSymbolExtractorTest.php @@ -12,9 +12,10 @@ class UsedSymbolExtractorTest extends TestCase /** * @param array>> $expectedUsages + * @param array $extensionSymbols * @dataProvider provideVariants */ - public function test(string $path, array $expectedUsages): void + public function test(string $path, array $expectedUsages, array $extensionSymbols = []): void { $code = file_get_contents($path); self::assertNotFalse($code); @@ -23,24 +24,12 @@ public function test(string $path, array $expectedUsages): void self::assertSame( $expectedUsages, - $extractor->parseUsedSymbols( - [ - strtolower('XMLReader') => SymbolKind::CLASSLIKE, - strtolower('PDO') => SymbolKind::CLASSLIKE, - strtolower('json_encode') => SymbolKind::FUNCTION, - strtolower('DDTrace\active_span') => SymbolKind::FUNCTION, - strtolower('DDTrace\root_span') => SymbolKind::FUNCTION, - strtolower('LIBXML_ERR_FATAL') => SymbolKind::CONSTANT, - strtolower('LIBXML_ERR_ERROR') => SymbolKind::CONSTANT, - strtolower('DDTrace\DBM_PROPAGATION_FULL') => SymbolKind::CONSTANT, - strtolower('DDTrace\Integrations\Exec\proc_get_pid') => SymbolKind::FUNCTION, - ] - ) + $extractor->parseUsedSymbols($extensionSymbols) ); } /** - * @return iterable>>}> + * @return iterable>>, 2?: array}> */ public function provideVariants(): iterable { @@ -68,6 +57,9 @@ public function provideVariants(): iterable yield 'T_STRING issues' => [ __DIR__ . '/data/not-autoloaded/used-symbols/t-string-issues.php', [], + [ + strtolower('PDO') => SymbolKind::CLASSLIKE, + ], ]; yield 'various usages' => [ @@ -160,6 +152,7 @@ public function provideVariants(): iterable 'CURLOPT_SSL_VERIFYHOST' => [19], ], ], + self::extensionSymbolsForExtensionsTestCases(), ]; yield 'extensions global' => [ @@ -183,6 +176,7 @@ public function provideVariants(): iterable 'CURLOPT_SSL_VERIFYHOST' => [19], ], ], + self::extensionSymbolsForExtensionsTestCases(), ]; if (PHP_VERSION_ID >= 80000) { @@ -206,4 +200,22 @@ public function provideVariants(): iterable } } + /** + * @return array + */ + private static function extensionSymbolsForExtensionsTestCases(): array + { + return [ + strtolower('XMLReader') => SymbolKind::CLASSLIKE, + strtolower('PDO') => SymbolKind::CLASSLIKE, + strtolower('json_encode') => SymbolKind::FUNCTION, + strtolower('DDTrace\active_span') => SymbolKind::FUNCTION, + strtolower('DDTrace\root_span') => SymbolKind::FUNCTION, + strtolower('LIBXML_ERR_FATAL') => SymbolKind::CONSTANT, + strtolower('LIBXML_ERR_ERROR') => SymbolKind::CONSTANT, + strtolower('DDTrace\DBM_PROPAGATION_FULL') => SymbolKind::CONSTANT, + strtolower('DDTrace\Integrations\Exec\proc_get_pid') => SymbolKind::FUNCTION, + ]; + } + } diff --git a/tests/data/not-autoloaded/used-symbols/t-string-issues.php b/tests/data/not-autoloaded/used-symbols/t-string-issues.php index 69c8705..68c1595 100644 --- a/tests/data/not-autoloaded/used-symbols/t-string-issues.php +++ b/tests/data/not-autoloaded/used-symbols/t-string-issues.php @@ -1,6 +1,6 @@