diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 59dfefa..293846e 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -11,32 +11,47 @@ jobs: - repo: phpstan/phpstan-src name: phpstan - args: --config=build/composer-dependency-analyser.php + cdaArgs: --config=build/composer-dependency-analyser.php + composerArgs: '' - repo: shipmonk-rnd/phpstan-rules name: shipmonk-rules + cdaArgs: '' + composerArgs: '' + - + repo: vincentlanglet/twig-cs-fixer + name: twig-cs-fixer args: '' - repo: qossmic/deptrac-src name: deptrac - args: '' + cdaArgs: '' + composerArgs: '' - repo: kreait/firebase-php name: firebase - args: '' + cdaArgs: '' + composerArgs: '' + - + repo: oveleon/contao-cookiebar + name: contao-cookiebar + cdaArgs: --config=depcheck.php + composerArgs: --no-plugins - repo: rectorphp/rector-src name: rector - args: '' + cdaArgs: '' + composerArgs: '' - repo: inspirum/balikobot-php name: balikobot - args: '' - + cdaArgs: '' + composerArgs: '' - repo: tomasvotruba/unused-public name: unused-public - args: '' + cdaArgs: '' + composerArgs: '' fail-fast: false steps: - @@ -65,7 +80,7 @@ jobs: - name: Install analyser dependencies working-directory: analyser - run: composer install --no-progress --prefer-dist --no-interaction + run: composer install --no-progress --no-interaction - name: Disable autoloader prepend @@ -76,9 +91,9 @@ jobs: - name: Install ${{ matrix.name }} dependencies working-directory: ${{ matrix.name }} - run: composer install --no-progress --prefer-dist --no-interaction --ignore-platform-reqs + run: composer install --no-progress --no-interaction --ignore-platform-reqs ${{ matrix.composerArgs }} - name: Run analyser working-directory: ${{ matrix.name }} - run: php ../analyser/bin/composer-dependency-analyser ${{ matrix.args }} + run: php ../analyser/bin/composer-dependency-analyser ${{ matrix.cdaArgs }} diff --git a/README.md b/README.md index 1b54c4d..33c79ae 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ This tool reads your `composer.json` and scans all paths listed in `autoload` & - `--composer-json path/to/composer.json` for custom path to composer.json - `--dump-usages symfony/console` to show usages of certain package(s), `*` placeholder is supported - `--config path/to/config.php` for custom path to config file +- `--version` display version - `--help` display usage & cli options - `--verbose` to see more example classes & usages - `--show-all-usages` to see all usages diff --git a/bin/composer-dependency-analyser b/bin/composer-dependency-analyser index 459fba9..2e39d81 100755 --- a/bin/composer-dependency-analyser +++ b/bin/composer-dependency-analyser @@ -2,6 +2,7 @@ initConfiguration($options, $composerJson); $classLoaders = $initializer->initComposerClassLoaders(); - $analyser = new Analyser($stopwatch, $classLoaders, $configuration, $composerJson->dependencies); + $analyser = new Analyser($stopwatch, $composerJson->composerVendorDir, $classLoaders, $configuration, $composerJson->dependencies); $result = $analyser->run(); $formatter = $initializer->initFormatter($options); @@ -52,6 +53,9 @@ try { ) { $stdErrPrinter->printLine("\n{$e->getMessage()}" . PHP_EOL); exit(255); + +} catch (AbortException $e) { + exit(0); } exit($exitCode); diff --git a/composer.lock b/composer.lock index 6fedb8d..fcec371 100644 --- a/composer.lock +++ b/composer.lock @@ -209,16 +209,16 @@ }, { "name": "ergebnis/composer-normalize", - "version": "2.42.0", + "version": "2.43.0", "source": { "type": "git", "url": "https://github.com/ergebnis/composer-normalize.git", - "reference": "02cf2b69ad2a74c6f11a8c3f5f054b8f949df910" + "reference": "4b46330c84bb8f43fac79f5c5a05162fc7c80d75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/02cf2b69ad2a74c6f11a8c3f5f054b8f949df910", - "reference": "02cf2b69ad2a74c6f11a8c3f5f054b8f949df910", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/4b46330c84bb8f43fac79f5c5a05162fc7c80d75", + "reference": "4b46330c84bb8f43fac79f5c5a05162fc7c80d75", "shasum": "" }, "require": { @@ -232,17 +232,17 @@ "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0" }, "require-dev": { - "composer/composer": "^2.6.6", + "composer/composer": "^2.7.7", "ergebnis/license": "^2.4.0", - "ergebnis/php-cs-fixer-config": "^6.20.0", - "ergebnis/phpunit-slow-test-detector": "^2.9.0", + "ergebnis/php-cs-fixer-config": "^6.30.1", + "ergebnis/phpunit-slow-test-detector": "^2.14.0", "fakerphp/faker": "^1.23.1", "infection/infection": "~0.26.6", - "phpunit/phpunit": "^9.6.16", - "psalm/plugin-phpunit": "~0.18.4", - "rector/rector": "~0.19.2", - "symfony/filesystem": "^5.4.25", - "vimeo/psalm": "^5.20.0" + "phpunit/phpunit": "^9.6.19", + "psalm/plugin-phpunit": "~0.19.0", + "rector/rector": "^1.1.0", + "symfony/filesystem": "^5.4.40", + "vimeo/psalm": "^5.24.0" }, "type": "composer-plugin", "extra": { @@ -282,7 +282,7 @@ "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/composer-normalize" }, - "time": "2024-01-30T11:54:02+00:00" + "time": "2024-06-16T13:22:18+00:00" }, { "name": "ergebnis/json", @@ -618,20 +618,20 @@ }, { "name": "justinrainbow/json-schema", - "version": "v5.2.13", + "version": "5.3.0", "source": { "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", - "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "require-dev": { "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", @@ -642,11 +642,6 @@ "bin/validate-json" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -681,10 +676,10 @@ "schema" ], "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" }, - "time": "2023-09-26T02:20:38+00:00" + "time": "2024-07-06T21:00:26+00:00" }, { "name": "localheinz/diff", @@ -1240,16 +1235,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.3", + "version": "1.11.9", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5" + "reference": "e370bcddadaede0c1716338b262346f40d296f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e64220a05c1209fc856d58e789c3b7a32c0bb9a5", - "reference": "e64220a05c1209fc856d58e789c3b7a32c0bb9a5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e370bcddadaede0c1716338b262346f40d296f82", + "reference": "e370bcddadaede0c1716338b262346f40d296f82", "shasum": "" }, "require": { @@ -1294,7 +1289,7 @@ "type": "github" } ], - "time": "2024-05-31T13:53:37+00:00" + "time": "2024-08-01T16:25:18+00:00" }, { "name": "phpstan/phpstan-phpunit", diff --git a/src/Analyser.php b/src/Analyser.php index 811d9a8..2c984bd 100644 --- a/src/Analyser.php +++ b/src/Analyser.php @@ -22,6 +22,7 @@ use function array_filter; use function array_key_exists; use function array_keys; +use function array_values; use function explode; use function file_get_contents; use function get_declared_classes; @@ -50,9 +51,12 @@ class Analyser private $stopwatch; /** - * vendorDir => ClassLoader - * - * @var array + * @var list + */ + private $vendorDirs; + + /** + * @var list */ private $classLoaders; @@ -95,6 +99,7 @@ class Analyser */ public function __construct( Stopwatch $stopwatch, + string $defaultVendorDir, array $classLoaders, Configuration $config, array $composerJsonDependencies @@ -103,7 +108,8 @@ public function __construct( $this->stopwatch = $stopwatch; $this->config = $config; $this->composerJsonDependencies = $composerJsonDependencies; - $this->classLoaders = $classLoaders; + $this->vendorDirs = array_keys($classLoaders + [$defaultVendorDir => null]); + $this->classLoaders = array_values($classLoaders); $this->initExistingSymbols(); } @@ -311,7 +317,7 @@ private function isDevDependency(string $packageName): bool private function getPackageNameFromVendorPath(string $realPath): string { - foreach ($this->classLoaders as $vendorDir => $_) { + foreach ($this->vendorDirs as $vendorDir) { if (strpos($realPath, $vendorDir) === 0) { $filePathInVendor = trim(str_replace($vendorDir, '', $realPath), DIRECTORY_SEPARATOR); [$vendor, $package] = explode(DIRECTORY_SEPARATOR, $filePathInVendor, 3); @@ -366,7 +372,7 @@ private function listPhpFilesIn(string $path): Generator private function isVendorPath(string $realPath): bool { - foreach ($this->classLoaders as $vendorDir => $_) { + foreach ($this->vendorDirs as $vendorDir) { if (strpos($realPath, $vendorDir) === 0) { return true; } diff --git a/src/Cli.php b/src/Cli.php index a10115b..f4d90ed 100644 --- a/src/Cli.php +++ b/src/Cli.php @@ -13,6 +13,7 @@ class Cli { private const OPTIONS = [ + 'version' => false, 'help' => false, 'verbose' => false, 'ignore-shadow-deps' => false, @@ -126,6 +127,10 @@ public function getProvidedOptions(): CliOptions { $options = new CliOptions(); + if (isset($this->providedOptions['version'])) { + $options->version = true; + } + if (isset($this->providedOptions['help'])) { $options->help = true; } diff --git a/src/CliOptions.php b/src/CliOptions.php index a0c06b7..3656225 100644 --- a/src/CliOptions.php +++ b/src/CliOptions.php @@ -5,6 +5,11 @@ class CliOptions { + /** + * @var true|null + */ + public $version = null; + /** * @var true|null */ diff --git a/src/ComposerJson.php b/src/ComposerJson.php index c43dc27..4e1d48f 100644 --- a/src/ComposerJson.php +++ b/src/ComposerJson.php @@ -31,6 +31,12 @@ class ComposerJson { + /** + * @readonly + * @var string + */ + public $composerVendorDir; + /** * @readonly * @var string @@ -72,7 +78,8 @@ public function __construct( $basePath = dirname($composerJsonPath); $composerJsonData = $this->parseComposerJson($composerJsonPath); - $this->composerAutoloadPath = $this->resolveComposerAutoloadPath($basePath, $composerJsonData['config']['vendor-dir'] ?? 'vendor'); + $this->composerVendorDir = $this->resolveComposerVendorDir($basePath, $composerJsonData['config']['vendor-dir'] ?? 'vendor'); + $this->composerAutoloadPath = Path::normalize($this->composerVendorDir . '/autoload.php'); $requiredPackages = $composerJsonData['require'] ?? []; $requiredDevPackages = $composerJsonData['require-dev'] ?? []; @@ -121,7 +128,11 @@ private function extractAutoloadPaths(string $basePath, array $autoload, bool $i } foreach ($paths as $path) { - $absolutePath = $basePath . '/' . $path; + if (Path::isAbsolute($path)) { + $absolutePath = $path; + } else { + $absolutePath = $basePath . '/' . $path; + } if (strpos($path, '*') !== false) { // https://getcomposer.org/doc/04-schema.md#classmap $globPaths = glob($absolutePath); @@ -261,13 +272,13 @@ private function parseComposerJson(string $composerJsonPath): array return $composerJsonData; // @phpstan-ignore-line ignore mixed returned } - private function resolveComposerAutoloadPath(string $basePath, string $vendorDir): string + private function resolveComposerVendorDir(string $basePath, string $vendorDir): string { if (Path::isAbsolute($vendorDir)) { - return Path::normalize($vendorDir . '/autoload.php'); + return Path::normalize($vendorDir); } - return Path::normalize($basePath . '/' . $vendorDir . '/autoload.php'); + return Path::normalize($basePath . '/' . $vendorDir); } } diff --git a/src/Exception/AbortException.php b/src/Exception/AbortException.php new file mode 100644 index 0000000..00835b6 --- /dev/null +++ b/src/Exception/AbortException.php @@ -0,0 +1,13 @@ + $argv + * @throws AbortException * @throws InvalidCliException */ public function initCliOptions(string $cwd, array $argv): CliOptions @@ -227,7 +235,12 @@ public function initCliOptions(string $cwd, array $argv): CliOptions if ($cliOptions->help !== null) { $this->stdOutPrinter->printLine(self::$help); - throw new InvalidCliException(''); // just exit + throw new AbortException(); + } + + if ($cliOptions->version !== null) { + $this->stdOutPrinter->printLine('Composer Dependency Analyser ' . $this->deduceVersion()); + throw new AbortException(); } return $cliOptions; @@ -255,4 +268,29 @@ public function initFormatter(CliOptions $options): ResultFormatter throw new InvalidConfigException("Invalid format option provided, allowed are 'console' or 'junit'."); } + private function deduceVersion(): string + { + try { + if (isset($GLOBALS['_composer_autoload_path'])) { + require $GLOBALS['_composer_autoload_path']; + } + + /** @throws OutOfBoundsException */ + if (!class_exists(InstalledVersions::class)) { + return 'unknown'; + } + + $package = 'shipmonk/composer-dependency-analyser'; + + return sprintf( + '%s (%s)', + InstalledVersions::getPrettyVersion($package), + InstalledVersions::getReference($package) + ); + + } catch (OutOfBoundsException $e) { + return 'not found'; + } + } + } diff --git a/src/Result/ConsoleFormatter.php b/src/Result/ConsoleFormatter.php index 7de0cea..67402dc 100644 --- a/src/Result/ConsoleFormatter.php +++ b/src/Result/ConsoleFormatter.php @@ -12,6 +12,7 @@ use function array_reduce; use function count; use function fnmatch; +use function in_array; use function round; use function strlen; use function strpos; @@ -53,14 +54,14 @@ public function format( private function getMaxUsagesShownForErrors(CliOptions $options): int { - if ($options->verbose === true) { - return self::VERBOSE_SHOWN_USAGES; - } - if ($options->showAllUsages === true) { return PHP_INT_MAX; } + if ($options->verbose === true) { + return self::VERBOSE_SHOWN_USAGES; + } + return 1; } @@ -123,60 +124,73 @@ private function printResultErrors( $prodDependencyOnlyInDevErrors = $result->getProdDependencyOnlyInDevErrors(); $unusedDependencyErrors = $result->getUnusedDependencyErrors(); - if (count($unknownClassErrors) > 0) { + $unknownClassErrorsCount = count($unknownClassErrors); + $unknownFunctionErrorsCount = count($unknownFunctionErrors); + $shadowDependencyErrorsCount = count($shadowDependencyErrors); + $devDependencyInProductionErrorsCount = count($devDependencyInProductionErrors); + $prodDependencyOnlyInDevErrorsCount = count($prodDependencyOnlyInDevErrors); + $unusedDependencyErrorsCount = count($unusedDependencyErrors); + + if ($unknownClassErrorsCount > 0) { $hasError = true; + $classes = $this->pluralize($unknownClassErrorsCount, 'class'); $this->printSymbolBasedErrors( - 'Unknown classes!', + "Found $unknownClassErrorsCount unknown $classes!", 'unable to autoload those, so we cannot check them', $unknownClassErrors, $maxShownUsages ); } - if (count($unknownFunctionErrors) > 0) { + if ($unknownFunctionErrorsCount > 0) { $hasError = true; + $functions = $this->pluralize($unknownFunctionErrorsCount, 'function'); $this->printSymbolBasedErrors( - 'Unknown functions!', + "Found $unknownFunctionErrorsCount unknown $functions!", 'those are not declared, so we cannot check them', $unknownFunctionErrors, $maxShownUsages ); } - if (count($shadowDependencyErrors) > 0) { + if ($shadowDependencyErrorsCount > 0) { $hasError = true; + $dependencies = $this->pluralize($shadowDependencyErrorsCount, 'dependency'); $this->printPackageBasedErrors( - 'Found shadow dependencies!', + "Found $shadowDependencyErrorsCount shadow $dependencies!", 'those are used, but not listed as dependency in composer.json', $shadowDependencyErrors, $maxShownUsages ); } - if (count($devDependencyInProductionErrors) > 0) { + if ($devDependencyInProductionErrorsCount > 0) { $hasError = true; + $dependencies = $this->pluralize($devDependencyInProductionErrorsCount, 'dependency'); $this->printPackageBasedErrors( - 'Found dev dependencies in production code!', + "Found $devDependencyInProductionErrorsCount dev $dependencies in production code!", 'those should probably be moved to "require" section in composer.json', $devDependencyInProductionErrors, $maxShownUsages ); } - if (count($prodDependencyOnlyInDevErrors) > 0) { + if ($prodDependencyOnlyInDevErrorsCount > 0) { $hasError = true; + $dependencies = $this->pluralize($prodDependencyOnlyInDevErrorsCount, 'dependency'); $this->printPackageBasedErrors( - 'Found prod dependencies used only in dev paths!', + "Found $prodDependencyOnlyInDevErrorsCount prod $dependencies used only in dev paths!", 'those should probably be moved to "require-dev" section in composer.json', array_fill_keys($prodDependencyOnlyInDevErrors, []), $maxShownUsages ); } - if (count($unusedDependencyErrors) > 0) { + if ($unusedDependencyErrorsCount > 0) { $hasError = true; + $dependencies = $this->pluralize($unusedDependencyErrorsCount, 'dependency'); $this->printPackageBasedErrors( - 'Found unused dependencies!', + "Found $unusedDependencyErrorsCount unused $dependencies!", 'those are listed in composer.json, but no usage was found in scanned paths', array_fill_keys($unusedDependencyErrors, []), $maxShownUsages @@ -433,4 +447,21 @@ private function willLimitUsages(array $usages, int $limit): bool return false; } + private function pluralize(int $count, string $singular): string + { + if ($count === 1) { + return $singular; + } + + if (substr($singular, -1) === 's' || substr($singular, -1) === 'x' || substr($singular, -2) === 'sh' || substr($singular, -2) === 'ch') { + return $singular . 'es'; + } + + if (substr($singular, -1) === 'y' && !in_array($singular[strlen($singular) - 2], ['a', 'e', 'i', 'o', 'u'], true)) { + return substr($singular, 0, -1) . 'ies'; + } + + return $singular . 's'; + } + } diff --git a/src/Result/JunitFormatter.php b/src/Result/JunitFormatter.php index f206518..bf94816 100644 --- a/src/Result/JunitFormatter.php +++ b/src/Result/JunitFormatter.php @@ -2,6 +2,7 @@ namespace ShipMonk\ComposerDependencyAnalyser\Result; +use DOMDocument; use ShipMonk\ComposerDependencyAnalyser\CliOptions; use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\Ignore\UnusedErrorIgnore; @@ -9,16 +10,17 @@ use ShipMonk\ComposerDependencyAnalyser\Printer; use ShipMonk\ComposerDependencyAnalyser\SymbolKind; use function array_fill_keys; -use function array_reduce; use function count; +use function extension_loaded; use function htmlspecialchars; -use function implode; use function sprintf; use function strlen; use function strpos; use function substr; +use function trim; use const ENT_COMPAT; use const ENT_XML1; +use const LIBXML_NOEMPTYTAG; use const PHP_INT_MAX; class JunitFormatter implements ResultFormatter @@ -120,9 +122,13 @@ public function format( $xml .= $this->createUnusedIgnoresTestSuite($unusedIgnores); } + if ($hasError) { + $xml .= sprintf('', $this->getUsagesComment($maxShownUsages)); + } + $xml .= ''; - $this->printer->print($xml); + $this->printer->print($this->prettyPrintXml($xml)); if ($hasError) { return 255; @@ -133,14 +139,14 @@ public function format( private function getMaxUsagesShownForErrors(CliOptions $options): int { - if ($options->verbose === true) { - return self::VERBOSE_SHOWN_USAGES; - } - if ($options->showAllUsages === true) { return PHP_INT_MAX; } + if ($options->verbose === true) { + return self::VERBOSE_SHOWN_USAGES; + } + return 1; } @@ -154,28 +160,12 @@ private function createSymbolBasedTestSuite(string $title, array $errors, int $m foreach ($errors as $symbol => $usages) { $xml .= sprintf('', $this->escape($symbol)); - if ($maxShownUsages > 1) { - $failureUsage = []; + foreach ($usages as $index => $usage) { + $xml .= sprintf('%s', $this->escape($this->relativizeUsage($usage))); - foreach ($usages as $index => $usage) { - $failureUsage[] = $this->relativizeUsage($usage); - - if ($index === $maxShownUsages - 1) { - $restUsagesCount = count($usages) - $index - 1; - - if ($restUsagesCount > 0) { - $failureUsage[] = "+ {$restUsagesCount} more"; - break; - } - } + if ($index === $maxShownUsages - 1) { + break; } - - $xml .= sprintf('%s', $this->escape(implode('\n', $failureUsage))); - } else { - $firstUsage = $usages[0]; - $restUsagesCount = count($usages) - 1; - $rest = $restUsagesCount > 0 ? " (+ {$restUsagesCount} more)" : ''; - $xml .= sprintf('in %s%s', $this->escape($this->relativizeUsage($firstUsage)), $rest); } $xml .= ''; @@ -195,7 +185,24 @@ private function createPackageBasedTestSuite(string $title, array $errors, int $ foreach ($errors as $packageName => $usagesPerClassname) { $xml .= sprintf('', $this->escape($packageName)); - $xml .= sprintf('%s', $this->escape(implode('\n', $this->createUsages($usagesPerClassname, $maxShownUsages)))); + + $printedSymbols = 0; + + foreach ($usagesPerClassname as $symbol => $usages) { + foreach ($this->createUsages($usages, $maxShownUsages) as $usage) { + $printedSymbols++; + $xml .= sprintf( + '%s', + $symbol, + $this->escape($usage) + ); + + if ($printedSymbols === $maxShownUsages) { + break 2; + } + } + } + $xml .= ''; } @@ -205,59 +212,19 @@ private function createPackageBasedTestSuite(string $title, array $errors, int $ } /** - * @param array> $usagesPerSymbol + * @param list $usages * @return list */ - private function createUsages(array $usagesPerSymbol, int $maxShownUsages): array + private function createUsages(array $usages, int $maxShownUsages): array { $usageMessages = []; - if ($maxShownUsages === 1) { - $countOfAllUsages = array_reduce( - $usagesPerSymbol, - static function (int $carry, array $usages): int { - return $carry + count($usages); - }, - 0 - ); + foreach ($usages as $index => $usage) { + $usageMessages[] = $this->relativizeUsage($usage); - foreach ($usagesPerSymbol as $symbol => $usages) { - $firstUsage = $usages[0]; - $restUsagesCount = $countOfAllUsages - 1; - $rest = $countOfAllUsages > 1 ? " (+ {$restUsagesCount} more)" : ''; - $usageMessages[] = "e.g. {$symbol} in {$this->relativizeUsage($firstUsage)}$rest"; + if ($index === $maxShownUsages - 1) { break; } - } else { - $classnamesPrinted = 0; - - foreach ($usagesPerSymbol as $symbol => $usages) { - $classnamesPrinted++; - - $usageMessages[] = $symbol; - - foreach ($usages as $index => $usage) { - $usageMessages[] = " {$this->relativizeUsage($usage)}"; - - if ($index === $maxShownUsages - 1) { - $restUsagesCount = count($usages) - $index - 1; - - if ($restUsagesCount > 0) { - $usageMessages[] = " + {$restUsagesCount} more"; - break; - } - } - } - - if ($classnamesPrinted === $maxShownUsages) { - $restSymbolsCount = count($usagesPerSymbol) - $classnamesPrinted; - - if ($restSymbolsCount > 0) { - $usageMessages[] = " + {$restSymbolsCount} more symbol" . ($restSymbolsCount > 1 ? 's' : ''); - break; - } - } - } } return $usageMessages; @@ -323,4 +290,37 @@ private function escape(string $string): string return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8'); } + private function prettyPrintXml(string $inputXml): string + { + if (!extension_loaded('dom') || !extension_loaded('libxml')) { + return $inputXml; + } + + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + $dom->loadXML($inputXml); + + $outputXml = $dom->saveXML(null, LIBXML_NOEMPTYTAG); + + if ($outputXml === false) { + return $inputXml; + } + + return trim($outputXml); + } + + private function getUsagesComment(int $maxShownUsages): string + { + if ($maxShownUsages === PHP_INT_MAX) { + return 'showing all failure usages'; + } + + if ($maxShownUsages === 1) { + return 'showing only first example failure usage'; + } + + return sprintf('showing only first %d example failure usages', $maxShownUsages); + } + } diff --git a/tests/AnalyserTest.php b/tests/AnalyserTest.php index 720de96..0f6ed15 100644 --- a/tests/AnalyserTest.php +++ b/tests/AnalyserTest.php @@ -45,6 +45,7 @@ public function test(callable $editConfig, AnalysisResult $expectedResult): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, [$vendorDir => $this->getClassLoaderMock()], $config, $dependencies @@ -477,12 +478,14 @@ public function testNativeTypesNotReported(): void $path = realpath(__DIR__ . '/data/not-autoloaded/builtin/native-symbols.php'); self::assertNotFalse($path); + $vendorDir = __DIR__; $config = new Configuration(); $config->addPathToScan($path, false); $detector = new Analyser( $this->getStopwatchMock(), - [__DIR__ => $this->getClassLoaderMock()], + $vendorDir, + [$vendorDir => $this->getClassLoaderMock()], $config, [] ); @@ -503,13 +506,16 @@ public function testNoMultipleScansOfTheSameFile(): void $path = realpath(__DIR__ . '/data/not-autoloaded/analysis/unknown-classes.php'); self::assertNotFalse($path); + $vendorDir = __DIR__ . '/data/autoloaded/vendor'; + $config = new Configuration(); $config->addPathToScan($path, true); $config->addPathToScan($path, true); $detector = new Analyser( $this->getStopwatchMock(), - [__DIR__ . '/data/autoloaded/vendor' => $this->getClassLoaderMock()], + $vendorDir, + [$vendorDir => $this->getClassLoaderMock()], $config, [] ); @@ -538,6 +544,7 @@ public function testDevPathInsideProdPath(): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, [$vendorDir => $this->getClassLoaderMock()], $config, [ @@ -565,6 +572,7 @@ public function testProdPathInsideDevPath(): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, [$vendorDir => $this->getClassLoaderMock()], $config, [ @@ -591,6 +599,7 @@ public function testOtherSymbols(): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, [$vendorDir => $this->getClassLoaderMock()], $config, [] @@ -626,6 +635,7 @@ public function testPharSupport(): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, [$vendorDir => $this->getClassLoaderMock()], $config, [ @@ -665,6 +675,7 @@ public function testMultipleClassloaders(): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, $classLoaders, $config, [ @@ -692,6 +703,7 @@ public function testFunctions(): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, [$vendorDir => $this->getClassLoaderMock()], $config, ['org/package' => false] @@ -713,6 +725,7 @@ public function testExplicitFileWithoutExtension(): void $detector = new Analyser( $this->getStopwatchMock(), + $vendorDir, [$vendorDir => $this->getClassLoaderMock()], $config, [ diff --git a/tests/BinTest.php b/tests/BinTest.php index c4dd187..d95b3ed 100644 --- a/tests/BinTest.php +++ b/tests/BinTest.php @@ -24,16 +24,18 @@ public function test(): void $okOutput = 'No composer issues found'; $dumpingOutput = 'Dumping sample usages of'; $helpOutput = 'Usage:'; + $versionOutput = 'Composer Dependency Analyser'; $usingConfig = 'Using config'; - $junitOutput = ''; + $junitOutput = "\n"; $this->runCommand('php bin/composer-dependency-analyser', $rootDir, 0, $okOutput, $usingConfig); $this->runCommand('php bin/composer-dependency-analyser --verbose', $rootDir, 0, $okOutput, $usingConfig); $this->runCommand('php ../bin/composer-dependency-analyser', $testsDir, 255, null, $noComposerJsonError); - $this->runCommand('php bin/composer-dependency-analyser --help', $rootDir, 255, $helpOutput); - $this->runCommand('php ../bin/composer-dependency-analyser --help', $testsDir, 255, $helpOutput); + $this->runCommand('php bin/composer-dependency-analyser --help', $rootDir, 0, $helpOutput); + $this->runCommand('php ../bin/composer-dependency-analyser --help', $testsDir, 0, $helpOutput); + $this->runCommand('php ../bin/composer-dependency-analyser --version', $testsDir, 0, $versionOutput); $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.json', $rootDir, 0, $okOutput, $usingConfig); $this->runCommand('php bin/composer-dependency-analyser --composer-json=composer.lock', $rootDir, 255, null, $noPackagesError); $this->runCommand('php bin/composer-dependency-analyser --composer-json=README.md', $rootDir, 255, null, $parseError); diff --git a/tests/ComposerJsonTest.php b/tests/ComposerJsonTest.php index 96bf1b4..401e0c0 100644 --- a/tests/ComposerJsonTest.php +++ b/tests/ComposerJsonTest.php @@ -41,6 +41,7 @@ public function testComposerJson(): void realpath(__DIR__ . '/data/not-autoloaded/composer/dir2/file1.php') => false, realpath(__DIR__ . '/data/not-autoloaded/composer/dir1') => false, realpath(__DIR__ . '/data/not-autoloaded/composer/dir2') => false, + DIRECTORY_SEPARATOR . 'absolute' . DIRECTORY_SEPARATOR . 'dir' => false, ], $composerJson->autoloadPaths ); diff --git a/tests/ConsoleFormatterTest.php b/tests/ConsoleFormatterTest.php index 0f200be..19bbea2 100644 --- a/tests/ConsoleFormatterTest.php +++ b/tests/ConsoleFormatterTest.php @@ -83,7 +83,7 @@ public function testPrintResult(): void $expectedRegularOutput = <<<'OUT' -Unknown classes! +Found 1 unknown class! (unable to autoload those, so we cannot check them) • Unknown\Thing @@ -91,7 +91,7 @@ public function testPrintResult(): void -Unknown functions! +Found 1 unknown function! (those are not declared, so we cannot check them) • Unknown\function @@ -99,7 +99,7 @@ public function testPrintResult(): void -Found shadow dependencies! +Found 2 shadow dependencies! (those are used, but not listed as dependency in composer.json) • shadow/another @@ -110,7 +110,7 @@ public function testPrintResult(): void -Found dev dependencies in production code! +Found 1 dev dependency in production code! (those should probably be moved to "require" section in composer.json) • some/package @@ -118,13 +118,13 @@ public function testPrintResult(): void -Found prod dependencies used only in dev paths! +Found 1 prod dependency used only in dev paths! (those should probably be moved to "require-dev" section in composer.json) • misplaced/package -Found unused dependencies! +Found 1 unused dependency! (those are listed in composer.json, but no usage was found in scanned paths) • dead/package @@ -135,7 +135,7 @@ public function testPrintResult(): void OUT; $expectedVerboseOutput = <<<'OUT' -Unknown classes! +Found 1 unknown class! (unable to autoload those, so we cannot check them) • Unknown\Thing @@ -143,7 +143,7 @@ public function testPrintResult(): void -Unknown functions! +Found 1 unknown function! (those are not declared, so we cannot check them) • Unknown\function @@ -151,7 +151,7 @@ public function testPrintResult(): void -Found shadow dependencies! +Found 2 shadow dependencies! (those are used, but not listed as dependency in composer.json) • shadow/another @@ -170,7 +170,7 @@ public function testPrintResult(): void + 1 more symbol -Found dev dependencies in production code! +Found 1 dev dependency in production code! (those should probably be moved to "require" section in composer.json) • some/package @@ -178,13 +178,13 @@ public function testPrintResult(): void src/ProductGenerator.php:28 -Found prod dependencies used only in dev paths! +Found 1 prod dependency used only in dev paths! (those should probably be moved to "require-dev" section in composer.json) • misplaced/package -Found unused dependencies! +Found 1 unused dependency! (those are listed in composer.json, but no usage was found in scanned paths) • dead/package diff --git a/tests/InitializerTest.php b/tests/InitializerTest.php index 9729f41..5bd4f7f 100644 --- a/tests/InitializerTest.php +++ b/tests/InitializerTest.php @@ -5,7 +5,7 @@ use PHPUnit\Framework\TestCase; use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; use ShipMonk\ComposerDependencyAnalyser\Config\PathToScan; -use ShipMonk\ComposerDependencyAnalyser\Exception\InvalidCliException; +use ShipMonk\ComposerDependencyAnalyser\Exception\AbortException; use ShipMonk\ComposerDependencyAnalyser\Exception\InvalidConfigException; use ShipMonk\ComposerDependencyAnalyser\Result\ConsoleFormatter; use ShipMonk\ComposerDependencyAnalyser\Result\JunitFormatter; @@ -114,7 +114,7 @@ public function testInitCliOptionsHelp(): void $initializer = new Initializer(__DIR__, $printer, $printer); - $this->expectException(InvalidCliException::class); + $this->expectException(AbortException::class); $initializer->initCliOptions(__DIR__, ['script.php', '--help']); } diff --git a/tests/JunitFormatterTest.php b/tests/JunitFormatterTest.php index 995e2f0..5d479a2 100644 --- a/tests/JunitFormatterTest.php +++ b/tests/JunitFormatterTest.php @@ -2,7 +2,6 @@ namespace ShipMonk\ComposerDependencyAnalyser; -use DOMDocument; use ShipMonk\ComposerDependencyAnalyser\Config\Configuration; use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType; use ShipMonk\ComposerDependencyAnalyser\Config\Ignore\UnusedErrorIgnore; @@ -10,8 +9,6 @@ use ShipMonk\ComposerDependencyAnalyser\Result\JunitFormatter; use ShipMonk\ComposerDependencyAnalyser\Result\ResultFormatter; use ShipMonk\ComposerDependencyAnalyser\Result\SymbolUsage; -use function trim; -use const LIBXML_NOEMPTYTAG; class JunitFormatterTest extends FormatterTest { @@ -39,17 +36,21 @@ public function testPrintResult(): void 'shadow-dependency' was globally ignored, but it was never applied. + OUT; - self::assertSame($this->normalizeEol($expectedNoIssuesOutput), $this->prettyPrintXml($noIssuesOutput)); - self::assertSame($this->normalizeEol($expectedNoIssuesButWarningsOutput), $this->prettyPrintXml($noIssuesButUnusedIgnores)); + self::assertSame($this->normalizeEol($expectedNoIssuesOutput), $noIssuesOutput); + self::assertSame($this->normalizeEol($expectedNoIssuesButWarningsOutput), $noIssuesButUnusedIgnores); $analysisResult = new AnalysisResult( 10, 0.123, [], - ['Unknown\\Thing' => [new SymbolUsage('/app/app/init.php', 1093, SymbolKind::CLASSLIKE)]], + ['Unknown\\Thing' => [ + new SymbolUsage('/app/app/init.php', 1091, SymbolKind::CLASSLIKE), + new SymbolUsage('/app/app/init.php', 1093, SymbolKind::CLASSLIKE), + ]], ['Unknown\\function' => [new SymbolUsage('/app/app/foo.php', 51, SymbolKind::FUNCTION)]], [ 'shadow/package' => [ @@ -78,7 +79,7 @@ public function testPrintResult(): void }); $verboseOutput = $this->getFormatterNormalizedOutput(static function ($formatter) use ($analysisResult): void { $options = new CliOptions(); - $options->verbose = true; + $options->showAllUsages = true; $formatter->format($analysisResult, $options, new Configuration()); }); @@ -87,37 +88,34 @@ public function testPrintResult(): void - in app/init.php:1093 + app/init.php:1091 - in app/foo.php:51 + app/foo.php:51 - e.g. Another\Controller in src/bootstrap.php:173 + src/bootstrap.php:173 - e.g. Forth\Provider in src/bootstrap.php:873 (+ 6 more) + src/bootstrap.php:873 - e.g. Another\Command in src/ProductGenerator.php:28 + src/ProductGenerator.php:28 - - - + - - - + + OUT; $expectedVerboseOutput = <<<'OUT' @@ -125,6 +123,7 @@ public function testPrintResult(): void + app/init.php:1091 app/init.php:1093 @@ -135,48 +134,38 @@ public function testPrintResult(): void - Another\Controller\n src/bootstrap.php:173 + src/bootstrap.php:173 - Forth\Provider\n src/bootstrap.php:873\nShadow\Comparator\n src/Printer.php:25\nShadow\Utils\n src/Utils.php:19\n src/Utils.php:22\n src/Application.php:128\n + 1 more\n + 1 more symbol + src/bootstrap.php:873 + src/Printer.php:25 + src/Utils.php:19 + src/Utils.php:22 + src/Application.php:128 + src/Controller.php:229 + src/bootstrap.php:317 - Another\Command\n src/ProductGenerator.php:28 + src/ProductGenerator.php:28 - - - + - - - + + OUT; - self::assertSame($this->normalizeEol($expectedRegularOutput), $this->prettyPrintXml($regularOutput)); - self::assertSame($this->normalizeEol($expectedVerboseOutput), $this->prettyPrintXml($verboseOutput)); + self::assertSame($this->normalizeEol($expectedRegularOutput), $regularOutput); + self::assertSame($this->normalizeEol($expectedVerboseOutput), $verboseOutput); // editorconfig-checker-enable } - private function prettyPrintXml(string $inputXml): string - { - $dom = new DOMDocument(); - $dom->preserveWhiteSpace = false; - $dom->formatOutput = true; - $dom->loadXML($inputXml); - - $outputXml = $dom->saveXML(null, LIBXML_NOEMPTYTAG); - self::assertNotFalse($outputXml); - - return trim($outputXml); - } - protected function createFormatter(Printer $printer): ResultFormatter { return new JunitFormatter('/app', $printer); diff --git a/tests/data/not-autoloaded/composer/sample.json b/tests/data/not-autoloaded/composer/sample.json index 56a307f..ef9ce40 100644 --- a/tests/data/not-autoloaded/composer/sample.json +++ b/tests/data/not-autoloaded/composer/sample.json @@ -8,7 +8,8 @@ }, "autoload": { "classmap": [ - "dir*" + "dir*", + "/absolute/dir" ], "files": [ "dir2/file1.php"