diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 0fddd07d6931..e6cbb0695ccb 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -80,7 +80,9 @@ jobs: - name: "Handle lowest dependencies update" if: "contains(matrix.dependencies, 'lowest')" - run: "echo \"COMPOSER_UPDATE_FLAGS=$COMPOSER_UPDATE_FLAGS --prefer-lowest\" >> $GITHUB_ENV" + run: | + echo "COMPOSER_UPDATE_FLAGS=$COMPOSER_UPDATE_FLAGS --prefer-lowest" >> $GITHUB_ENV + echo "COMPOSER_LOWEST_DEPS_TEST=1" >> $GITHUB_ENV - name: "Handle ignore-platform-reqs dependencies update" if: "contains(matrix.dependencies, 'ignore')" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 479fe09c2036..bff8ead33cb6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,19 +21,31 @@ jobs: matrix: php-version: - "7.2" - - "latest" + - "nightly" steps: - - name: "Checkout" - uses: "actions/checkout@v4" + - uses: actions/checkout@v4 - - name: "Install PHP" - uses: "shivammathur/setup-php@v2" + - uses: shivammathur/setup-php@v2 with: - coverage: "none" - extensions: "intl" - ini-values: "memory_limit=-1, error_reporting=E_ALL, display_errors=On" php-version: "${{ matrix.php-version }}" + coverage: none + + - uses: ramsey/composer-install@v3 + with: + dependency-versions: highest - name: "Lint PHP files" - run: "find src/ -type f -name '*.php' -print0 | xargs -0 -L1 -P4 -- php -l -f" + run: | + hasErrors=0 + for f in $(find src/ tests/ -type f -name '*.php' ! -path '*/vendor/*') + do + { error="$(php -derror_reporting=-1 -ddisplay_errors=1 -l -f $f 2>&1 1>&3 3>&-)"; } 3>&1; + if [ "$error" != "" ]; then + while IFS= read -r line; do echo "::error file=$f::$line"; done <<< "$error" + hasErrors=1 + fi + done + if [ $hasErrors -eq 1 ]; then + exit 1 + fi diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index a4c25379a3ba..501b14840954 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -27,7 +27,7 @@ jobs: include: - php-version: "7.2" experimental: false - - php-version: "8.1" + - php-version: "8.3" experimental: true fail-fast: false diff --git a/CHANGELOG.md b/CHANGELOG.md index b53992a5580d..ebd97e8ce8df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,134 @@ +### [2.8.3] 2024-11-17 + + * Fixed windows handling of process discovery (#12180) + * Fixed react/promise requirement to allow 2.x installs again (#12188) + * Fixed some issues when lock:false is set in require and bump commands + +### [2.8.2] 2024-10-29 + + * Fixed crash while suggesting providers if they have no description (#12152) + * Fixed issues creating lock files violating the schema in some circumstances (#12149) + * Fixed `create-project` regression in 2.8.1 when using path repos with relative paths (#12150) + * Fixed ctrl-C aborts not working inside text prompts (#12106) + * Fixed git failing silently when git cannot read a repo due to ownership violations (#12178) + * Fixed handling of signals in non-PHP binaries run via proxies (#12176) + +### [2.8.1] 2024-10-04 + + * Fixed `init` command regression when no license is provided (#12145) + * Fixed `--strict-ambiguous` flag handling whereas it sometimes did not report all issues (#12148) + * Fixed `create-project` to inherit the target folder's permissions for installed project files (#12146) + * Fixed a few cases where the prompt for using a parent dir's composer.json fails to work correctly (#8023) + +### [2.8.0] 2024-10-02 + + * BC Warning: Fixed `https_proxy` env var falling back to `http_proxy`'s value. The fallback and warning have now been removed per the 2.7.3 release notes (#11938, #11915) + * Added `--patch-only` flag to the `update` command to restrict updates to patch versions and make an update of all deps safer (#12122) + * Added `--abandoned` flag to the `audit` command to configure how abandoned packages should be treated, overriding the `audit.abandoned` config setting (#12091) + * Added `--ignore-severity` flag to the `audit` command to ignore one or more advisory severities (#12132) + * Added `--bump-after-update` flag to the `update` command to run bump after the update is done (#11942) + * Added a way to control which `scripts` receive additional CLI arguments and where they appear in the command, see [the docs](https://getcomposer.org/doc/articles/scripts.md#controlling-additional-arguments) (#12086) + * Added `allow-missing-requirements` config setting to skip the error when the lock file is not fulfilling the composer.json's dependencies (#11966) + * Added a JSON schema for the composer.lock file (#12123) + * Added better support for Bitbucket app passwords when cloning repos / installing from source (#12103) + * Added `--type` flag to filter packages by type(s) in the `reinstall` command (#12114) + * Added `--strict-ambiguous` flag to the `dump-autoload` command to make it return with an error code if duplicate classes are found (#12119) + * Added warning in `dump-autoload` when vendor files have been deleted (#12139) + * Added warnings for each missing platform package when running `create-project` to avoid having to run it again and again (#12120) + * Added sorting of packages in allow-plugins when `sort-packages` is enabled (#11348) + * Added suggestion of provider packages / polyfills when an ext or lib package is missing (#12113) + * Improved interactive package update selection by first outputting all packages and their possible updates (#11990) + * Improved dependency resolution failure output by sorting the output in a deterministic and (often) more logical way (#12111) + * Fixed PHP 8.4 deprecation warnings about `E_STRICT` (#12116) + * Fixed `init` command to validate the given license identifier (#12115) + * Fixed version guessing to be more deterministic on feature branches if it appears that it could come from either of two mainline branches (#12129) + * Fixed COMPOSER_ROOT_VERSION env var handling to treat 1.2 the same as 1.2.x-dev and not 1.2.0 (#12109) + * Fixed require command skipping new stability flags from the lock file, causing invalid lock file diffs (#12112) + * Fixed php://stdin potentially being open several times when running Composer programmatically (#12107) + * Fixed handling of platform packages in why-not command and partial updates (#12110) + * Reverted "Fixed transport-options.ssl for local cert authorization being stored in lock file making them less portable (#12019)" from 2.7.8 as it was broken + +### [2.7.9] 2024-09-04 + + * Fixed Docker detection breaking on constrained environments (#12095) + * Fixed upstream issue in bash completion script, it is recommended to update it using the `completion` command (#12015) + +### [2.7.8] 2024-08-22 + + * Added `release-age`, `release-date` and `latest-release-date` in the JSON output of `outdated` (#12053) + * Fixed PHP 8.4 deprecation warnings + * Fixed addressability of branches containing `#` signs (#12042) + * Fixed `bump` command not handling some `~` constraints correctly (#12038) + * Fixed COMPOSER_AUTH not taking precedence over ./auth.json (#12084) + * Fixed `relative: true` sometimes not being respected in path repo symlinks (#12092) + * Fixed copy from cache sometimes failing on VirtualBox shared folders (#12057) + * Fixed PSR-4 autoloading order regression in some edge case (#12063) + * Fixed duplicate lib-* packages causing issues when having pecl + core versions of the same PHP extension (#12093) + * Fixed transport-options.ssl for local cert authorization being stored in lock file making them less portable (#12019) + * Fixed memory issues when installing large binaries (#12032) + * Fixed `archive` command crashing when a path cannot be realpath'd on windows (#11544) + * API: Deprecated BasePackage::$stabilities in favor of BasePackage::STABILITIES (685add70ec) + * Improved Docker detection (#12062) + +### [2.7.7] 2024-06-10 + + * Security: Fixed command injection via malicious git branch name (GHSA-47f6-5gq3-vx9c / CVE-2024-35241) + * Security: Fixed multiple command injections via malicious git/hg branch names (GHSA-v9qv-c7wm-wgmf / CVE-2024-35242) + * Security: Fixed secure-http checks that could be bypassed by using malformed URL formats (fa3b9582c) + * Security: Fixed Filesystem::isLocalPath including windows-specific checks on linux (3c37a67c) + * Security: Fixed perforce argument escaping (3773f775) + * Security: Fixed handling of zip bombs when extracting archives (de5f7e32) + * Security: Fixed Windows command parameter escaping to prevent abuse of unicode characters with best fit encoding conversion (3130a7455, 04a63b324) + * Fixed PSR violations for classes not matching the namespace of a rule being hidden, this may lead to new violations being shown (#11957) + * Fixed UX when a plugin is still in vendor dir but is not required nor allowed anymore after changing branches (#12000) + * Fixed new platform requirements from composer.json not being checked if the lock file is outdated (#12001) + * Fixed ability for `config` command to remove autoload keys (#11967) + * Fixed empty `type` support in `init` command (#11999) + * Fixed git clone errors when `safe.bareRepository` is set to `strict` in the git config (#11969) + * Fixed regression showing network errors on PHP <8.1 (#11974) + * Fixed some color bleed from a few warnings (#11972) + +### [2.7.6] 2024-05-04 + + * Fixed regression when script handlers add an autoloader which uses a private callback (#11960) + +### [2.7.5] 2024-05-03 + + * Added `uninstall` alias to `remove` command (#11951) + * Added workaround for broken curl versions 8.7.0/8.7.1 causing transport exceptions (#11913) + * Fixed root usage warnings showing up within Podman containers (#11946) + * Fixed config command not handling objects correctly in some conditions (#11945) + * Fixed binary proxies not containing the correct path if the project dir is a symlink (#11947) + * Fixed Composer autoloader being overruled by project autoloaders when they are loaded by event handlers (scripts/plugins) (#11955) + * Fixed TransportException (http failures) not having a distinct exit code, should now exit with `100` as code (#11954) + +### [2.7.4] 2024-04-22 + + * Fixed regression (`Call to undefined method ProxyManager::needsTransitionWarning()`) with projects requiring composer/composer in an pre-2.7.3 version (#11943, #11940) + +### [2.7.3] 2024-04-19 + + * BC Warning: Fixed `https_proxy` env var falling back to `http_proxy`'s value, this is still in place but with a warning for now, and https_proxy can now be set empty to remove the fallback. Composer 2.8.0 will remove the fallback so make sure you heed the warnings (#11915) + * Fixed `show` and `outdated` commands to remove leading `v` in e.g. `v1.2.3` when showing lists of packages (#11925) + * Fixed `audit` command not showing any id when no CVE is present, the advisory ID is now shown (#11892) + * Fixed the warning about a missing default version showing for packages with `project` type as those are typically not versioned and do not have cyclic dependencies (#11885) + * Fixed PHP 8.4 deprecation warnings + * Fixed `clear-cache` command to respect the config.cache-dir setting from the local composer.json (#11921) + * Fixed `status` command not handling failed download/install promises correctly (#11889) + * Added support for `buy_me_a_coffee` in GitHub funding files (#11902) + * Added `hg` support for SSH urls (#11878) + * Fixed some env vars with an integer value causing a crash (#11908) + * Fixed context data not being output when using IOInterface as a PSR-3 logger (#11882) + +### [2.7.2] 2024-03-11 + + * Added info about the PHP version when running `composer --version` (#11866) + * Added warning when the root version cannot be detected (#11858) + * Fixed plugins still being enabled in a few contexts when running as root (c3efff91f) + * Fixed `outdated --ignore ...` still attempting to load the latest version of the ignored packages (#11863) + * Fixed handling of broken symlinks in the middle of an install path (#11864) + * Fixed `update --lock` still incorrectly updating some metadata (#11850, #11787) + ### [2.7.1] 2024-02-09 * Added several warnings when plugins are disabled to hint at common problems people had with 2.7.0 (#11842) @@ -640,7 +771,7 @@ * UX Change: Packages from `path` repositories which are symlinked in the vendor dir will always be updated in partial updates to avoid mistakes when the original composer.json changes but the symlinked package is not explicitly updated (#9765) * Added `reinstall` command that takes one or more package names, including wildcard (`*`) support, and removes then reinstalls them in the exact same version they had (#9915) * Added support for parallel package installs on Windows via [7-Zip](https://www.7-zip.org/) if it is installed (#9875) - * Added detection of invalid composer.lock files that do not fullfil the composer.json requirements to `validate` command (#9899) + * Added detection of invalid composer.lock files that do not fulfill the composer.json requirements to `validate` command (#9899) * Added `InstalledVersions::getInstalledPackagesByType(string $type)` to retrieve installed plugins for example, [read more](https://getcomposer.org/doc/07-runtime.md#knowing-which-packages-of-a-given-type-are-installed) (#9699) * Added `InstalledVersions::getInstalledPath(string $packageName)` to retrieve the install path of a given package, [read more](https://getcomposer.org/doc/07-runtime.md#knowing-the-path-in-which-a-package-is-installed) (#9699) * Added flag to `InstalledVersions::isInstalled()` to allow excluding dev requirements from that check (#9682) @@ -1069,7 +1200,7 @@ * Added a `--no-cache` flag available on all commands to run with the cache disabled * Added PHP_BINARY as env var pointing to the PHP process when executing Composer scripts as shell scripts * Added a `use-github-api` config option which can set the `no-api` flag on all GitHub VCS repositories declared - * Added a static helper you can preprend to a script to avoid process timeouts, `"Composer\\Config::disableProcessTimeout"` + * Added a static helper you can prepend to a script to avoid process timeouts, `"Composer\\Config::disableProcessTimeout"` * Added Event::getOriginatingEvent to retrieve an event's original event when a script handler forwards to another one * Added support for autoloading directly from a phar file * Fixed loading order of plugins to always initialize them in order of dependencies @@ -1673,7 +1804,7 @@ * Break: Removed support for lock files created before 2012-09-15 due to their outdated unusable format * Added `prefer-stable` flag to pick stable packages over unstable ones when possible * Added `preferred-install` config option to always enable --prefer-source or --prefer-dist - * Added `diagnose` command to to system/network checks and find common problems + * Added `diagnose` command to system/network checks and find common problems * Added wildcard support in the update whitelist, e.g. to update all packages of a vendor do `composer update vendor/*` * Added `archive` command to archive the current directory or a given package * Added `run-script` command to manually trigger scripts @@ -1829,6 +1960,18 @@ * Initial release +[2.8.3]: https://github.com/composer/composer/compare/2.8.2...2.8.3 +[2.8.2]: https://github.com/composer/composer/compare/2.8.1...2.8.2 +[2.8.1]: https://github.com/composer/composer/compare/2.8.0...2.8.1 +[2.8.0]: https://github.com/composer/composer/compare/2.7.9...2.8.0 +[2.7.9]: https://github.com/composer/composer/compare/2.7.8...2.7.9 +[2.7.8]: https://github.com/composer/composer/compare/2.7.7...2.7.8 +[2.7.7]: https://github.com/composer/composer/compare/2.7.6...2.7.7 +[2.7.6]: https://github.com/composer/composer/compare/2.7.5...2.7.6 +[2.7.5]: https://github.com/composer/composer/compare/2.7.4...2.7.5 +[2.7.4]: https://github.com/composer/composer/compare/2.7.3...2.7.4 +[2.7.3]: https://github.com/composer/composer/compare/2.7.2...2.7.3 +[2.7.2]: https://github.com/composer/composer/compare/2.7.1...2.7.2 [2.7.1]: https://github.com/composer/composer/compare/2.7.0...2.7.1 [2.7.0]: https://github.com/composer/composer/compare/2.6.6...2.7.0 [2.6.6]: https://github.com/composer/composer/compare/2.6.5...2.6.6 diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 3e5878ea38b5..99c2876cf62f 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -100,7 +100,7 @@ If your repository only has a small number of packages, and you want to avoid th `"providers-api": "https://packagist.org/providers/%package%.json",` -The providers-api is optional, but if you implement it it should return packages which provide a given package name, but not the package which has that name. For example https://packagist.org/providers/monolog/monolog.json lists some package which have a "provide" rule for monolog/monolog, but it does not list monolog/monolog itself. +The providers-api is optional, but if you implement it, it should return packages which provide a given package name, but not the package which has that name. For example https://packagist.org/providers/monolog/monolog.json lists some package which have a "provide" rule for monolog/monolog, but it does not list monolog/monolog itself. ### list diff --git a/composer.json b/composer.json index 2cbcfa73d7a8..743ca0bba96d 100644 --- a/composer.json +++ b/composer.json @@ -23,34 +23,34 @@ ], "require": { "php": "^7.2.5 || ^8.0", - "composer/ca-bundle": "^1.0", - "composer/class-map-generator": "^1.0", + "composer/ca-bundle": "^1.5", + "composer/class-map-generator": "^1.4.0", "composer/metadata-minifier": "^1.0", - "composer/semver": "^3.2.5", + "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", - "justinrainbow/json-schema": "^5.2.11", + "justinrainbow/json-schema": "^5.3", "psr/log": "^1.0 || ^2.0 || ^3.0", "seld/jsonlint": "^1.4", "seld/phar-utils": "^1.2", - "symfony/console": "^5.4.11 || ^6.0.11 || ^7", - "symfony/filesystem": "^5.4 || ^6.0 || ^7", - "symfony/finder": "^5.4 || ^6.0 || ^7", - "symfony/process": "^5.4 || ^6.0 || ^7", - "react/promise": "^2.8 || ^3", - "composer/pcre": "^2.1 || ^3.1", + "symfony/console": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/filesystem": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/finder": "^5.4.35 || ^6.3.12 || ^7.0.3", + "symfony/process": "^5.4.35 || ^6.3.12 || ^7.0.3", + "react/promise": "^2.11 || ^3.2", + "composer/pcre": "^2.2 || ^3.2", "symfony/polyfill-php73": "^1.24", "symfony/polyfill-php80": "^1.24", "symfony/polyfill-php81": "^1.24", "seld/signal-handler": "^2.0" }, "require-dev": { - "symfony/phpunit-bridge": "^6.4.1 || ^7.0.1", - "phpstan/phpstan": "^1.9.3", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-deprecation-rules": "^1", - "phpstan/phpstan-strict-rules": "^1", - "phpstan/phpstan-symfony": "^1.2.10" + "symfony/phpunit-bridge": "^6.4.3 || ^7.0.1", + "phpstan/phpstan": "^1.11.8", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpstan/phpstan-symfony": "^1.4.0" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -65,7 +65,7 @@ }, "extra": { "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.8-dev" }, "phpstan": { "includes": [ @@ -81,7 +81,13 @@ "autoload-dev": { "psr-4": { "Composer\\Test\\": "tests/Composer/Test/" - } + }, + "exclude-from-classmap": [ + "tests/Composer/Test/Fixtures/", + "tests/Composer/Test/Autoload/Fixtures", + "tests/Composer/Test/Autoload/MinimumVersionSupport", + "tests/Composer/Test/Plugin/Fixtures" + ] }, "bin": [ "bin/composer" diff --git a/composer.lock b/composer.lock index 04c822b48fea..b49b1e1c2332 100644 --- a/composer.lock +++ b/composer.lock @@ -4,32 +4,32 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "bbb9ffc97dcec54f38fdc5b4bf9a287d", + "content-hash": "d6c1c91b79d7140594e249343184ce6f", "packages": [ { "name": "composer/ca-bundle", - "version": "1.4.0", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "b66d11b7479109ab547f9405b97205640b17d385" + "reference": "3b1fc3f0be055baa7c6258b1467849c3e8204eb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/b66d11b7479109ab547f9405b97205640b17d385", - "reference": "b66d11b7479109ab547f9405b97205640b17d385", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/3b1fc3f0be055baa7c6258b1467849c3e8204eb2", + "reference": "3b1fc3f0be055baa7c6258b1467849c3e8204eb2", "shasum": "" }, "require": { "ext-openssl": "*", "ext-pcre": "*", - "php": "^5.3.2 || ^7.0 || ^8.0" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^0.12.55", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", - "symfony/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", "extra": { @@ -64,7 +64,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.4.0" + "source": "https://github.com/composer/ca-bundle/tree/1.5.3" }, "funding": [ { @@ -80,20 +80,20 @@ "type": "tidelift" } ], - "time": "2023-12-18T12:05:55+00:00" + "time": "2024-11-04T10:15:26+00:00" }, { "name": "composer/class-map-generator", - "version": "1.1.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" + "reference": "98bbf6780e56e0fd2404fe4b82eb665a0f93b783" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", - "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/98bbf6780e56e0fd2404fe4b82eb665a0f93b783", + "reference": "98bbf6780e56e0fd2404fe4b82eb665a0f93b783", "shasum": "" }, "require": { @@ -106,8 +106,8 @@ "phpstan/phpstan-deprecation-rules": "^1", "phpstan/phpstan-phpunit": "^1", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/filesystem": "^5.4 || ^6", - "symfony/phpunit-bridge": "^5" + "phpunit/phpunit": "^8", + "symfony/filesystem": "^5.4 || ^6" }, "type": "library", "extra": { @@ -137,7 +137,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.1.0" + "source": "https://github.com/composer/class-map-generator/tree/1.4.0" }, "funding": [ { @@ -153,7 +153,7 @@ "type": "tidelift" } ], - "time": "2023-06-30T13:58:57+00:00" + "time": "2024-10-03T18:14:00+00:00" }, { "name": "composer/metadata-minifier", @@ -226,30 +226,38 @@ }, { "name": "composer/pcre", - "version": "2.1.1", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b439557066cd445732fa57cbc8d905394b4db8a0" + "reference": "ebb81df8f52b40172d14062ae96a06939d80a069" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b439557066cd445732fa57cbc8d905394b4db8a0", - "reference": "b439557066cd445732fa57cbc8d905394b4db8a0", + "url": "https://api.github.com/repos/composer/pcre/zipball/ebb81df8f52b40172d14062ae96a06939d80a069", + "reference": "ebb81df8f52b40172d14062ae96a06939d80a069", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^5" + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" }, "type": "library", "extra": { "branch-alias": { "dev-main": "2.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] } }, "autoload": { @@ -277,7 +285,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/2.1.1" + "source": "https://github.com/composer/pcre/tree/2.3.2" }, "funding": [ { @@ -293,28 +301,28 @@ "type": "tidelift" } ], - "time": "2023-10-11T07:10:55+00:00" + "time": "2024-11-12T16:24:47+00:00" }, { "name": "composer/semver", - "version": "3.4.0", + "version": "3.4.3", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", - "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", "shasum": "" }, "require": { "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" }, "type": "library", "extra": { @@ -358,7 +366,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.0" + "source": "https://github.com/composer/semver/tree/3.4.3" }, "funding": [ { @@ -374,7 +382,7 @@ "type": "tidelift" } ], - "time": "2023-08-31T09:50:34+00:00" + "time": "2024-09-19T14:15:21+00:00" }, { "name": "composer/spdx-licenses", @@ -458,16 +466,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.3", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "ced299686f41dce890debac69273b47ffe98a40c" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c", - "reference": "ced299686f41dce890debac69273b47ffe98a40c", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -478,7 +486,7 @@ "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^6.0" + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" }, "type": "library", "autoload": { @@ -502,9 +510,9 @@ "performance" ], "support": { - "irc": "irc://irc.freenode.org/composer", + "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.3" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -520,24 +528,24 @@ "type": "tidelift" } ], - "time": "2022-02-25T21:32:43+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "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", @@ -548,11 +556,6 @@ "bin/validate-json" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -587,10 +590,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": "psr/container", @@ -692,16 +695,16 @@ }, { "name": "react/promise", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { @@ -753,7 +756,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.1.0" + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { @@ -761,27 +764,27 @@ "type": "open_collective" } ], - "time": "2023-11-16T16:21:57+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { "name": "seld/jsonlint", - "version": "1.10.2", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259" + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9bb7db07b5d66d90f6ebf542f09fc67d800e5259", - "reference": "9bb7db07b5d66d90f6ebf542f09fc67d800e5259", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", "shasum": "" }, "require": { "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.5", + "phpstan/phpstan": "^1.11", "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" }, "bin": [ @@ -813,7 +816,7 @@ ], "support": { "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/1.10.2" + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" }, "funding": [ { @@ -825,7 +828,7 @@ "type": "tidelift" } ], - "time": "2024-02-07T12:57:50+00:00" + "time": "2024-07-11T14:55:45+00:00" }, { "name": "seld/phar-utils", @@ -938,16 +941,16 @@ }, { "name": "symfony/console", - "version": "v5.4.35", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931" + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/dbdf6adcb88d5f83790e1efb57ef4074309d3931", - "reference": "dbdf6adcb88d5f83790e1efb57ef4074309d3931", + "url": "https://api.github.com/repos/symfony/console/zipball/c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", + "reference": "c4ba980ca61a9eb18ee6bcc73f28e475852bb1ed", "shasum": "" }, "require": { @@ -1017,7 +1020,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.35" + "source": "https://github.com/symfony/console/tree/v5.4.47" }, "funding": [ { @@ -1033,20 +1036,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T14:28:09+00:00" + "time": "2024-11-06T11:30:55+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" + "reference": "80d075412b557d41002320b96a096ca65aa2c98d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d", "shasum": "" }, "require": { @@ -1084,7 +1087,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" }, "funding": [ { @@ -1100,20 +1103,20 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:53:40+00:00" + "time": "2023-01-24T14:02:46+00:00" }, { "name": "symfony/filesystem", - "version": "v5.4.35", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "5a553607d4ffbfa9c0ab62facadea296c9db7086" + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/5a553607d4ffbfa9c0ab62facadea296c9db7086", - "reference": "5a553607d4ffbfa9c0ab62facadea296c9db7086", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/57c8294ed37d4a055b77057827c67f9558c95c54", + "reference": "57c8294ed37d4a055b77057827c67f9558c95c54", "shasum": "" }, "require": { @@ -1122,6 +1125,9 @@ "symfony/polyfill-mbstring": "~1.8", "symfony/polyfill-php80": "^1.16" }, + "require-dev": { + "symfony/process": "^5.4|^6.4" + }, "type": "library", "autoload": { "psr-4": { @@ -1148,7 +1154,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.35" + "source": "https://github.com/symfony/filesystem/tree/v5.4.45" }, "funding": [ { @@ -1164,20 +1170,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-10-22T13:05:35+00:00" }, { "name": "symfony/finder", - "version": "v5.4.35", + "version": "v5.4.45", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435" + "reference": "63741784cd7b9967975eec610b256eed3ede022b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/abe6d6f77d9465fed3cd2d029b29d03b56b56435", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435", + "url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b", + "reference": "63741784cd7b9967975eec610b256eed3ede022b", "shasum": "" }, "require": { @@ -1211,7 +1217,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.35" + "source": "https://github.com/symfony/finder/tree/v5.4.45" }, "funding": [ { @@ -1227,24 +1233,24 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-09-28T13:32:08+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-ctype": "*" @@ -1290,7 +1296,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -1306,24 +1312,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -1368,7 +1374,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" }, "funding": [ { @@ -1384,24 +1390,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "suggest": { "ext-intl": "For best performance" @@ -1449,7 +1455,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" }, "funding": [ { @@ -1465,24 +1471,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "provide": { "ext-mbstring": "*" @@ -1529,7 +1535,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -1545,24 +1551,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2" + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/21bd091060673a1177ae842c0ef8fe30893114d2", - "reference": "21bd091060673a1177ae842c0ef8fe30893114d2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", + "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1605,7 +1611,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" }, "funding": [ { @@ -1621,24 +1627,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1685,7 +1691,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -1701,24 +1707,24 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/polyfill-php81", - "version": "v1.29.0", + "version": "v1.31.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { @@ -1761,7 +1767,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" }, "funding": [ { @@ -1777,20 +1783,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "symfony/process", - "version": "v5.4.35", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "cbc28e34015ad50166fc2f9c8962d28d0fe861eb" + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/cbc28e34015ad50166fc2f9c8962d28d0fe861eb", - "reference": "cbc28e34015ad50166fc2f9c8962d28d0fe861eb", + "url": "https://api.github.com/repos/symfony/process/zipball/5d1662fb32ebc94f17ddb8d635454a776066733d", + "reference": "5d1662fb32ebc94f17ddb8d635454a776066733d", "shasum": "" }, "require": { @@ -1823,7 +1829,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.35" + "source": "https://github.com/symfony/process/tree/v5.4.47" }, "funding": [ { @@ -1839,20 +1845,20 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-11-06T11:36:42+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.5.2", + "version": "v2.5.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", + "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", "shasum": "" }, "require": { @@ -1906,7 +1912,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" + "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" }, "funding": [ { @@ -1922,20 +1928,20 @@ "type": "tidelift" } ], - "time": "2022-05-30T19:17:29+00:00" + "time": "2023-04-21T15:04:16+00:00" }, { "name": "symfony/string", - "version": "v5.4.35", + "version": "v5.4.47", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2" + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c209c4d0559acce1c9a2067612cfb5d35756edc2", - "reference": "c209c4d0559acce1c9a2067612cfb5d35756edc2", + "url": "https://api.github.com/repos/symfony/string/zipball/136ca7d72f72b599f2631aca474a4f8e26719799", + "reference": "136ca7d72f72b599f2631aca474a4f8e26719799", "shasum": "" }, "require": { @@ -1992,7 +1998,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.35" + "source": "https://github.com/symfony/string/tree/v5.4.47" }, "funding": [ { @@ -2008,22 +2014,22 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-11-10T20:33:58+00:00" } ], "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.57", + "version": "1.12.10", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e" + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1627b1d03446904aaa77593f370c5201d2ecc34e", - "reference": "1627b1d03446904aaa77593f370c5201d2ecc34e", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fc463b5d0fe906dcf19689be692c65c50406a071", + "reference": "fc463b5d0fe906dcf19689be692c65c50406a071", "shasum": "" }, "require": { @@ -2066,35 +2072,30 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-01-24T11:51:34+00:00" + "time": "2024-11-11T15:37:09+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", - "version": "1.1.4", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-deprecation-rules.git", - "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa" + "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", - "reference": "089d8a8258ed0aeefdc7b68b6c3d25572ebfdbaa", + "url": "https://api.github.com/repos/phpstan/phpstan-deprecation-rules/zipball/f94d246cc143ec5a23da868f8f7e1393b50eaa82", + "reference": "f94d246cc143ec5a23da868f8f7e1393b50eaa82", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.3" + "phpstan/phpstan": "^1.12" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan-php-parser": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^9.5" }, @@ -2118,27 +2119,27 @@ "description": "PHPStan rules for detecting usage of deprecated classes, methods, properties, constants and traits.", "support": { "issues": "https://github.com/phpstan/phpstan-deprecation-rules/issues", - "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.1.4" + "source": "https://github.com/phpstan/phpstan-deprecation-rules/tree/1.2.1" }, - "time": "2023-08-05T09:02:04+00:00" + "time": "2024-09-11T15:52:35+00:00" }, { "name": "phpstan/phpstan-phpunit", - "version": "1.3.15", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-phpunit.git", - "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a" + "reference": "11d4235fbc6313ecbf93708606edfd3222e44949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", - "reference": "70ecacc64fe8090d8d2a33db5a51fe8e88acd93a", + "url": "https://api.github.com/repos/phpstan/phpstan-phpunit/zipball/11d4235fbc6313ecbf93708606edfd3222e44949", + "reference": "11d4235fbc6313ecbf93708606edfd3222e44949", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.12" }, "conflict": { "phpunit/phpunit": "<7.0" @@ -2170,27 +2171,27 @@ "description": "PHPUnit extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-phpunit/issues", - "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.3.15" + "source": "https://github.com/phpstan/phpstan-phpunit/tree/1.4.1" }, - "time": "2023-10-09T18:58:39+00:00" + "time": "2024-11-12T12:43:59+00:00" }, { "name": "phpstan/phpstan-strict-rules", - "version": "1.5.2", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-strict-rules.git", - "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542" + "reference": "daeec748b53de80a97498462513066834ec28f8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/7a50e9662ee9f3942e4aaaf3d603653f60282542", - "reference": "7a50e9662ee9f3942e4aaaf3d603653f60282542", + "url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/daeec748b53de80a97498462513066834ec28f8b", + "reference": "daeec748b53de80a97498462513066834ec28f8b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.34" + "phpstan/phpstan": "^1.12.4" }, "require-dev": { "nikic/php-parser": "^4.13.0", @@ -2219,28 +2220,28 @@ "description": "Extra strict and opinionated rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.5.2" + "source": "https://github.com/phpstan/phpstan-strict-rules/tree/1.6.1" }, - "time": "2023-10-30T14:35:06+00:00" + "time": "2024-09-20T14:04:44+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.3.7", + "version": "1.4.12", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "ef7db637be9b85fa00278fc3477ac66abe8eb7d1" + "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/ef7db637be9b85fa00278fc3477ac66abe8eb7d1", - "reference": "ef7db637be9b85fa00278fc3477ac66abe8eb7d1", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/c7b7e7f520893621558bfbfdb2694d4364565c1d", + "reference": "c7b7e7f520893621558bfbfdb2694d4364565c1d", "shasum": "" }, "require": { "ext-simplexml": "*", "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.36" + "phpstan/phpstan": "^1.12" }, "conflict": { "symfony/framework-bundle": "<3.0" @@ -2291,22 +2292,22 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.7" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.4.12" }, - "time": "2024-01-10T21:54:42+00:00" + "time": "2024-11-06T10:13:18+00:00" }, { "name": "symfony/phpunit-bridge", - "version": "v7.0.3", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "0a2eeb0d9e68bf01660e3e903f8113508bb46132" + "reference": "c6b9d8f52d3e276bedb49612aa4a2a046171287f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/0a2eeb0d9e68bf01660e3e903f8113508bb46132", - "reference": "0a2eeb0d9e68bf01660e3e903f8113508bb46132", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/c6b9d8f52d3e276bedb49612aa4a2a046171287f", + "reference": "c6b9d8f52d3e276bedb49612aa4a2a046171287f", "shasum": "" }, "require": { @@ -2338,7 +2339,8 @@ "Symfony\\Bridge\\PhpUnit\\": "" }, "exclude-from-classmap": [ - "/Tests/" + "/Tests/", + "/bin/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2358,7 +2360,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v7.0.3" + "source": "https://github.com/symfony/phpunit-bridge/tree/v7.1.6" }, "funding": [ { @@ -2374,18 +2376,18 @@ "type": "tidelift" } ], - "time": "2024-01-23T15:02:46+00:00" + "time": "2024-09-25T14:20:29+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^7.2.5 || ^8.0" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.2.5" }, diff --git a/doc/02-libraries.md b/doc/02-libraries.md index c731f105d956..7b498593719a 100644 --- a/doc/02-libraries.md +++ b/doc/02-libraries.md @@ -150,4 +150,34 @@ allows you to submit the URL to your VCS repository, at which point Packagist will start crawling it. Once it is done, your package will be available to anyone! +## Light-weight distribution packages + +Some useless information like the `.github` directory, or large examples, test +data, etc. should typically not be included in distributed packages. + +The `.gitattributes` file is a git specific file like `.gitignore` also living +at the root directory of your library. It overrides local and global +configuration (`.git/config` and `~/.gitconfig` respectively) when present and +tracked by git. + +Use `.gitattributes` to prevent unwanted files from bloating the zip +distribution packages. + +```text +// .gitattributes +/demo export-ignore +phpunit.xml.dist export-ignore +/.github/ export-ignore +``` + +Test it by inspecting the zip file generated manually: + +```shell +git archive branchName --format zip -o file.zip +``` + +> **Note:** Files would be still tracked by git just not included in the +> zip distribution. This only works for packages installed from +> dist (i.e. tagged releases) coming from GitHub, GitLab or Bitbucket. + ← [Basic usage](01-basic-usage.md) | [Command-line interface](03-cli.md) → diff --git a/doc/03-cli.md b/doc/03-cli.md index d7d1fd136638..cc193e1620e1 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -232,8 +232,10 @@ php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.* COMPOSER_PREFER_LOWEST=1 env var. * **--minimal-changes (-m):** During a partial update with `-w`/`-W`, only perform absolutely necessary changes to transitive dependencies. Can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var. +* **--patch-only:** Only allow patch version updates for currently installed dependencies. * **--interactive:** Interactive interface with autocompletion to select the packages to update. * **--root-reqs:** Restricts the update to your first degree dependencies. +* **--bump-after-update:** Runs `bump` after performing the update. Set to `dev` or `no-dev` to only bump those dependencies. Specifying one of the words `mirrors`, `lock`, or `nothing` as an argument has the same effect as specifying the option `--lock`, for example `composer update mirrors` is exactly the same as `composer update --lock`. @@ -313,7 +315,7 @@ If you do not want to install the new dependencies immediately you can call it w * **--apcu-autoloader-prefix:** Use a custom prefix for the APCu autoloader cache. Implicitly enables `--apcu-autoloader`. -## remove / rm +## remove / rm / uninstall The `remove` command removes packages from the `composer.json` file from the current directory. @@ -727,8 +729,10 @@ packages depending on the packages that cause the conflict. ## validate You should always run the `validate` command before you commit your -`composer.json` file, and before you tag a release. It will check if your -`composer.json` is valid. +`composer.json` file (and `composer.lock` [if applicable](01-basic-usage.md#commit-your-composer-lock-file-to-version-control)), and before you tag a release. + +It will check if your +`composer.json` is valid. If a `composer.lock` exists, it will also check if it is up to date with the `composer.json`. ```shell php composer.phar validate @@ -738,7 +742,9 @@ php composer.phar validate * **--no-check-all:** Do not emit a warning if requirements in `composer.json` use unbound or overly strict version constraints. * **--no-check-lock:** Do not emit an error if `composer.lock` exists and is not up to date. +* **--check-lock** Check if lock file is up to date (even when [config.lock](06-config.md#lock) is false) * **--no-check-publish:** Do not emit an error if `composer.json` is unsuitable for publishing as a package on Packagist but is otherwise valid. +* **--no-check-version:** Do not emit an error if the version field is present. * **--with-dependencies:** Also validate the composer.json of all installed dependencies. * **--strict:** Return a non-zero exit code for warnings as well as errors. @@ -977,6 +983,7 @@ performance. * **--apcu:** Use APCu to cache found/not-found classes. * **--apcu-prefix:** Use a custom prefix for the APCu autoloader cache. Implicitly enables `--apcu`. +* **--dry-run:** Outputs the operations but will not execute anything. * **--no-dev:** Disables autoload-dev rules. Composer will by default infer this automatically according to the last `install` or `update` `--no-dev` state. * **--dev:** Enables autoload-dev rules. Composer will by default infer this @@ -989,6 +996,8 @@ performance. Multiple requirements can be ignored via wildcard. * **--strict-psr:** Return a failed exit code (1) if PSR-4 or PSR-0 mapping errors are present. Requires --optimize to work. +* **--strict-ambiguous:** Return a failed exit code (2) if the same class is found + in multiple files. Requires --optimize to work. ## clear-cache / clearcache / cc @@ -1060,9 +1069,10 @@ php composer.phar archive vendor/package 2.0.21 --format=zip ## audit This command is used to audit the packages you have installed -for possible security issues. It checks for and -lists security vulnerability advisories according to the -[Packagist.org api](https://packagist.org/apidoc#list-security-advisories). +for potential security issues. It checks for and +lists security vulnerability advisories using the +[Packagist.org api](https://packagist.org/apidoc#list-security-advisories) +by default or other repositories if specified in the `repositories` section of `composer.json`. The audit command returns the amount of vulnerabilities found. `0` if successful, and up to `255` otherwise. @@ -1075,6 +1085,11 @@ php composer.phar audit * **--no-dev:** Disables auditing of require-dev packages. * **--format (-f):** Audit output format. Must be "table" (default), "plain", "json", or "summary". * **--locked:** Audit packages from the lock file, regardless of what is currently in vendor dir. +* **--abandoned:** Behavior on abandoned packages. Must be "ignore", "report", + or "fail". See also [audit.abandoned](06-config.md#abandoned). Passing this + flag will override the config value and the environment variable. +* **--ignore-severity:** Ignore advisories of a certain severity level. Can be passed one or more + time to ignore multiple severities. ## help @@ -1086,8 +1101,8 @@ php composer.phar help install ## Command-line completion -Command-line completion can be enabled by following instructions -[on this page](https://github.com/bamarni/symfony-console-autocomplete). +Command-line completion can be enabled by running the `composer completion --help` command and +following the instructions. ## Environment variables @@ -1232,20 +1247,12 @@ environment variable if you use Vagrant or VirtualBox and experience issues with being found during installation even though they should be present. ### http_proxy or HTTP_PROXY +### HTTP_PROXY_REQUEST_FULLURI +### HTTPS_PROXY_REQUEST_FULLURI +### no_proxy or NO_PROXY -If you are using Composer from behind an HTTP proxy, you can use the standard -`http_proxy` or `HTTP_PROXY` env vars. Set it to the URL of your proxy. -Many operating systems already set this variable for you. - -Using `http_proxy` (lowercased) or even defining both might be preferable since -some tools like git or curl will only use the lower-cased `http_proxy` version. -Alternatively you can also define the git proxy using -`git config --global http.proxy `. - -If you are using Composer in a non-CLI context (i.e. integration into a CMS or -similar use case), and need to support proxies, please provide the `CGI_HTTP_PROXY` -environment variable instead. See [httpoxy.org](https://httpoxy.org/) for further -details. +See the [proxy documentation](faqs/how-to-use-composer-behind-a-proxy.md) for more details +on how to use proxy env vars. ### COMPOSER_AUDIT_ABANDONED @@ -1264,32 +1271,10 @@ in performance gains. Set to `4` or `6` to force IPv4 or IPv6 DNS resolution. This only works when the curl extension is used for downloads. -### HTTP_PROXY_REQUEST_FULLURI - -If you use a proxy, but it does not support the request_fulluri flag, then you -should set this env var to `false` or `0` to prevent Composer from setting the -request_fulluri option. - -### HTTPS_PROXY_REQUEST_FULLURI - -If you use a proxy, but it does not support the request_fulluri flag for HTTPS -requests, then you should set this env var to `false` or `0` to prevent Composer -from setting the request_fulluri option. - ### COMPOSER_SELF_UPDATE_TARGET If set, makes the self-update command write the new Composer phar file into that path instead of overwriting itself. Useful for updating Composer on a read-only filesystem. -### no_proxy or NO_PROXY - -If you are behind a proxy and would like to disable it for certain domains, you -can use the `no_proxy` or `NO_PROXY` env var. Set it to a comma separated list of -domains the proxy should *not* be used for. - -The env var accepts domains, IP addresses, and IP address blocks in CIDR -notation. You can restrict the filter to a particular port (e.g. `:80`). You -can also set it to `*` to ignore the proxy for all HTTP requests. - ### COMPOSER_DISABLE_NETWORK If set to `1`, disables network access (best effort). This can be used for debugging or diff --git a/doc/04-schema.md b/doc/04-schema.md index e36ec21115b9..bae038fc09a4 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -102,6 +102,9 @@ Out of the box, Composer supports four types: - **composer-plugin:** A package of type `composer-plugin` may provide an installer for other packages that have a custom type. Read more in the [dedicated article](articles/custom-installers.md). +- **php-ext** and **php-ext-zend**: These names are reserved for PHP extension + packages which are written in C. Do not use these types for packages written + in PHP. Only use a custom type if you need custom logic during installation. It is recommended to omit this field and have it default to `library`. @@ -123,6 +126,11 @@ Examples: > `--dev` option to prompt users if they would like to add these packages to > `require-dev` instead of `require`. These are: `dev`, `testing`, `static analysis`. +> **Note**: The range of characters allowed inside the string is restricted to +> unicode letters or numbers, space `" "`, dot `.`, underscore `_` and dash `-`. (Regex: `'{^[\p{N}\p{L} ._-]+$}u'`) +> Using other characters will emit a warning when running `composer validate` and +> will cause the package to fail updating on Packagist.org. + Optional. ### homepage diff --git a/doc/06-config.md b/doc/06-config.md index c6aa47491db1..85a138b6a445 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -6,10 +6,22 @@ This chapter will describe the `config` section of the `composer.json` ## process-timeout The timeout in seconds for process executions, defaults to 300 (5mins). -The duration processes like git clones can run before +The duration processes like `git clone`s can run before Composer assumes they died out. You may need to make this higher if you have a slow connection or huge vendors. +Example: + +```json +{ + "config": { + "process-timeout": 900 + } +} +``` + +### Disabling timeouts for an individual script command + To disable the process timeout on a custom command under `scripts`, a static helper is available: @@ -153,7 +165,12 @@ Defaults to `report` in Composer 2.6, and defaults to `fail` from Composer 2.7 o } ``` -Since Composer 2.7 the option can be overriden via the [`COMPOSER_AUDIT_ABANDONED`](03-cli.md#composer-audit-abandoned) environment variable. +Since Composer 2.7, the option can be overridden via the [`COMPOSER_AUDIT_ABANDONED`](03-cli.md#composer-audit-abandoned) environment variable. + +Since Composer 2.8, the option can be overridden via the +[`--abandoned`](03-cli.md#audit) command line option, which overrides both the +config value and the environment variable. + ## use-parent-dir @@ -476,4 +493,16 @@ throw, but you can set this config option to `["example.org"]` to allow using sv URLs on that hostname. This is a better/safer alternative to disabling `secure-http` altogether. +## bump-after-update + +Defaults to `false` and can be any of `true`, `false`, `"dev"` or `"no-dev"`. If +set to true, Composer will run the `bump` command after running the `update` command. +If set to `"dev"` or `"no-dev"` then only the corresponding dependencies will be bumped. + +## allow-missing-requirements + +Defaults to `false`. Ignores error during `install` if there are any missing +requirements - the lock file is not up to date with the latest changes in +`composer.json`. + ← [Repositories](05-repositories.md) | [Runtime](07-runtime.md) → diff --git a/doc/articles/aliases.md b/doc/articles/aliases.md index ce5a5b71d3d4..e7a4b3189ecd 100644 --- a/doc/articles/aliases.md +++ b/doc/articles/aliases.md @@ -7,32 +7,32 @@ ## Why aliases? When you are using a VCS repository, you will only get comparable versions for -branches that look like versions, such as `2.0` or `2.0.x`. For your `master` branch, you -will get a `dev-master` version. For your `bugfix` branch, you will get a +branches that look like versions, such as `2.0` or `2.0.x`. For your `main` branch, you +will get a `dev-main` version. For your `bugfix` branch, you will get a `dev-bugfix` version. -If your `master` branch is used to tag releases of the `1.0` development line, +If your `main` branch is used to tag releases of the `1.0` development line, i.e. `1.0.1`, `1.0.2`, `1.0.3`, etc., any package depending on it will probably require version `1.0.*`. -If anyone wants to require the latest `dev-master`, they have a problem: Other +If anyone wants to require the latest `dev-main`, they have a problem: Other packages may require `1.0.*`, so requiring that dev version will lead to -conflicts, since `dev-master` does not match the `1.0.*` constraint. +conflicts, since `dev-main` does not match the `1.0.*` constraint. Enter aliases. ## Branch alias -The `dev-master` branch is one in your main VCS repo. It is rather common that -someone will want the latest master dev version. Thus, Composer allows you to -alias your `dev-master` branch to a `1.0.x-dev` version. It is done by +The `dev-main` branch is one in your main VCS repo. It is rather common that +someone will want the latest main dev version. Thus, Composer allows you to +alias your `dev-main` branch to a `1.0.x-dev` version. It is done by specifying a `branch-alias` field under `extra` in `composer.json`: ```json { "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-main": "1.0.x-dev" } } } @@ -41,14 +41,16 @@ specifying a `branch-alias` field under `extra` in `composer.json`: If you alias a non-comparable version (such as dev-develop) `dev-` must prefix the branch name. You may also alias a comparable version (i.e. start with numbers, and end with `.x-dev`), but only as a more specific version. -For example, 1.x-dev could be aliased as 1.2.x-dev. +For example, a `1.x` or `1.x-dev` branch could be aliased from `1.x-dev` to +`1.2.x-dev` as that is more specific. -The alias must be a comparable dev version, and the `branch-alias` must be present on -the branch that it references. For `dev-master`, you need to commit it on the -`master` branch. +The alias must be a comparable dev version (you cannot alias `dev-main` +to `dev-master` for example), and the `branch-alias` must be present on +the branch that it references. To alias `dev-main`, you need to define and +commit it on the `main` branch. As a result, anyone can now require `1.0.*` and it will happily install -`dev-master`. +`dev-main`. In order to use branch aliasing, you must own the repository of the package being aliased. If you want to alias a third party package without maintaining diff --git a/doc/articles/authentication-for-private-packages.md b/doc/articles/authentication-for-private-packages.md index c1c186b3c013..fb86c5c23391 100644 --- a/doc/articles/authentication-for-private-packages.md +++ b/doc/articles/authentication-for-private-packages.md @@ -155,7 +155,7 @@ If the username e.g. is an email address it needs to be passed as `name%40exampl ### Command line inline http-basic ```shell -php composer.phar config [--global] repositories composer.unique-name https://username:password@repo.example.org +php composer.phar config [--global] repositories.unique-name composer https://username:password@repo.example.org ``` ### Manual inline http-basic @@ -264,7 +264,7 @@ php composer.phar config [--global] --editor --auth > **Note:** For the gitlab authentication to work on private gitlab instances, the > [`gitlab-domains`](../06-config.md#gitlab-domains) section should also contain the URL. -To create a new access token, go to your [access tokens section on GitLab](https://gitlab.com/-/profile/personal_access_tokens) +To create a new access token, go to your [access tokens section on GitLab](https://gitlab.com/-/user_settings/personal_access_tokens) (or the equivalent URL on your private instance) and create a new token. See also [the GitLab access token documentation](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#creating-a-personal-access-token) for more information. When creating a gitlab token manually, make sure it has either the `read_api` or `api` scope. @@ -296,7 +296,7 @@ php composer.phar config [--global] --editor --auth ## github-oauth -To create a new access token, head to your [token settings section on Github](https://github.com/settings/tokens) and [generate a new token](https://github.com/settings/tokens/new). +To create a new access token, head to your [token settings section on GitHub](https://github.com/settings/tokens) and [generate a new token](https://github.com/settings/tokens/new). For public repositories when rate limited, a token *without* any particular scope is sufficient (see `(no scope)` in the [scopes documentation](https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps)). Such tokens grant read-only access to public information. diff --git a/doc/articles/handling-private-packages.md b/doc/articles/handling-private-packages.md index 9548809f96a5..eb71b75ce03f 100644 --- a/doc/articles/handling-private-packages.md +++ b/doc/articles/handling-private-packages.md @@ -25,10 +25,8 @@ set up your own package archive on [Packagist.com](https://packagist.com). Satis on the other hand is open source but only a static `composer` repository generator. It is a bit like an ultra-lightweight, static file-based version of packagist and can be used to host the metadata of your company's private -packages, or your own. You can get it from -[GitHub](https://github.com/composer/satis) or install via CLI: - - php composer.phar create-project composer/satis --stability=dev --keep-vcs +packages, or your own. You can install it using [Composer](https://github.com/composer/satis?tab=readme-ov-file#run-from-source) +or [Docker](https://github.com/composer/satis?tab=readme-ov-file#run-as-docker-container). ## Setup diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index ec4004ebc018..90dfa3672ce7 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -273,7 +273,7 @@ class MyCommand extends Command public function execute(InputInterface $input, OutputInterface $output): int { if ($input->getOption('arbitrary-flag')) { - $output->writeln('The flag was used') + $output->writeln('The flag was used'); } return 0; @@ -410,6 +410,40 @@ JSON array of commands. You can also call a shell/bash script, which will have the path to the PHP executable available in it as a `PHP_BINARY` env var. +## Controlling additional arguments + +As of Composer 2.8, you can control how additional arguments are passed to script commands. + +When running scripts like `composer script-name arg arg2` or `composer script-name -- --option`, +Composer will by default append `arg`, `arg2` and `--option` to the script's command. + +If you do not want these args in a given command, you can put `@no_additional_args` +anywhere in it, that will remove the default behavior and that flag will be removed +as well before running the command. + +If you want the args to be added somewhere else than at the very end, then you can put +`@additional_args` to be able to choose exactly where they go. + +For example running `composer run-commands ARG` with the below config: + +```json +{ + "scripts": { + "run-commands": [ + "echo hello @no_additional_args", + "command-with-args @additional_args && do-something-without-args --here" + ] + } +} +``` + +Would end up executing these commands: + +``` +echo hello +command-with-args ARG && do-something-without-args --here +``` + ## Setting environment variables To set an environment variable in a cross-platform way, you can use `@putenv`: @@ -419,7 +453,7 @@ To set an environment variable in a cross-platform way, you can use `@putenv`: "scripts": { "install-phpstan": [ "@putenv COMPOSER=phpstan-composer.json", - "composer install --prefer-dist" + "@composer install --prefer-dist" ] } } diff --git a/doc/articles/troubleshooting.md b/doc/articles/troubleshooting.md index eda335453780..fbb6356d6e44 100644 --- a/doc/articles/troubleshooting.md +++ b/doc/articles/troubleshooting.md @@ -247,6 +247,19 @@ please report this [issue](https://github.com/composer/composer/issues). 3. Check if it contains any path to a non-existent file, if it's the case, remove them. +## SSL certificate problem: unable to get local issuer certificate + +1. Check that your root certificate store / CA bundle is up to date. Run `composer diagnose -vvv` + and look for `Checked CA file ...` or `Checked directory ...` lines in the first lines of output. + This will show you where Composer is looking for a CA bundle. You can get a + [new cacert.pem from cURL](https://curl.se/docs/caextract.html) and store it there. +2. If this did not help despite Composer finding a valid CA bundle, try disabling your antivirus and + firewall software to see if that helps. We have seen issues where Avast on Windows for example would + prevent Composer from functioning correctly. To disable the HTTPS scanning in Avast you can go in + "Protection > Core Shields > Web Shield > **uncheck** Enable HTTPS scanning". If this helps you + should report it to the software vendor so they can hopefully improve things. + + ## API rate limit and OAuth tokens Because of GitHub's rate limits on their API it can happen that Composer prompts diff --git a/doc/faqs/how-to-install-untrusted-packages-safely.md b/doc/faqs/how-to-install-untrusted-packages-safely.md index 27a8b7efd5c8..6048d15237f7 100644 --- a/doc/faqs/how-to-install-untrusted-packages-safely.md +++ b/doc/faqs/how-to-install-untrusted-packages-safely.md @@ -39,3 +39,10 @@ Also note that the `exec` command will always run third party code as the user w See the [COMPOSER_ALLOW_SUPERUSER](../03-cli.md#composer-allow-superuser) environment variable for more info on how to disable the warnings. + +## Running Composer inside Docker/Podman containers + +Composer makes a best effort attempt to detect that it runs inside a container and if so it will +allow running as root without any further issues. If that detection fails however you will +see warnings and plugins will be disabled unless you set the [COMPOSER_ALLOW_SUPERUSER](../03-cli.md#composer-allow-superuser) +environment variable. diff --git a/doc/faqs/how-to-use-composer-behind-a-proxy.md b/doc/faqs/how-to-use-composer-behind-a-proxy.md new file mode 100644 index 000000000000..ebefaced0878 --- /dev/null +++ b/doc/faqs/how-to-use-composer-behind-a-proxy.md @@ -0,0 +1,106 @@ +# How to use Composer behind a proxy + +Composer, like many other tools, uses environment variables to control the use of a proxy server and +supports: + +- `http_proxy` - the proxy to use for HTTP requests +- `https_proxy` - the proxy to use for HTTPS requests +- `CGI_HTTP_PROXY` - the proxy to use for HTTP requests in a non-CLI context +- `no_proxy` - domains that do not require a proxy + +These named variables are a convention, rather than an official standard, and their evolution and +usage across different operating systems and tools is complex. Composer prefers the use of lowercase +names, but accepts uppercase names where appropriate. + +## Usage + +Composer requires specific environment variables for HTTP and HTTPS requests. For example: + +``` +http_proxy=http://proxy.com:80 +https_proxy=http://proxy.com:80 +``` + +Uppercase names can also be used. + +### Non-CLI usage + +Composer does not look for `http_proxy` or `HTTP_PROXY` in a non-CLI context. If you are running it +this way (i.e. integration into a CMS or similar use case) you must use `CGI_HTTP_PROXY` for HTTP +requests: + +``` +CGI_HTTP_PROXY=http://proxy.com:80 +https_proxy=http://proxy.com:80 + +# cgi_http_proxy can also be used +``` + +> **Note:** CGI_HTTP_PROXY was introduced by Perl in 2001 to prevent request header manipulation and +was popularized in 2016 when this vulnerability was widely reported: https://httpoxy.org + +## Syntax + +Use `scheme://host:port` as in the examples above. Although a missing scheme defaults to http and a +missing port defaults to 80/443 for http/https schemes, other tools might require these values. + +The host can be specified as an IP address using dotted quad notation for IPv4, or enclosed in +square brackets for IPv6. + +### Authorization + +Composer supports Basic authorization, using the `scheme://user:pass@host:port` syntax. Reserved url +characters in either the user name or password must be percent-encoded. For example: + +``` +user: me@company +pass: p@ssw$rd +proxy: http://proxy.com:80 + +# percent-encoded authorization +me%40company:p%40ssw%24rd + +scheme://me%40company:p%40ssw%24rd@proxy.com:80 +``` + +> **Note:** The user name and password components must be percent-encoded individually and then +combined with the colon separator. The user name cannot contain a colon (even if percent-encoded), +because the proxy will split the components on the first colon it finds. + +## HTTPS proxy servers + +Composer supports HTTPS proxy servers, where HTTPS is the scheme used to connect to the proxy, but +only from PHP 7.3 with curl version 7.52.0 and above. + +``` +http_proxy=https://proxy.com:443 +https_proxy=https://proxy.com:443 +``` + +## Bypassing the proxy for specific domains + +Use the `no_proxy` (or `NO_PROXY`) environment variable to set a comma-separated list of domains +that the proxy should **not** be used for. + +``` +no_proxy=example.com +# Bypasses the proxy for example.com and its sub-domains + +no_proxy=www.example.com +# Bypasses the proxy for www.example.com and its sub-domains, but not for example.com +``` + +A domain can be restricted to a particular port (e.g. `:80`) and can also be specified as an IP +address or an IP address block in CIDR notation. + +IPv6 addresses do not need to be enclosed in square brackets, like they are for +http_proxy/https_proxy values, although this format is accepted. + +Setting the value to `*` will bypass the proxy for all requests. + +> **Note:** A leading dot in the domain name has no significance and is removed prior to processing. + +## Deprecated environment variables + +Composer originally provided `HTTP_PROXY_REQUEST_FULLURI` and `HTTPS_PROXY_REQUEST_FULLURI` to help +mitigate issues with misbehaving proxies. These are no longer required or used. diff --git a/phpstan/baseline-8.1.neon b/phpstan/baseline-8.3.neon similarity index 78% rename from phpstan/baseline-8.1.neon rename to phpstan/baseline-8.3.neon index 5a0aabf76033..9a3a36933f70 100644 --- a/phpstan/baseline-8.1.neon +++ b/phpstan/baseline-8.3.neon @@ -15,11 +15,6 @@ parameters: count: 1 path: ../src/Composer/Command/ArchiveCommand.php - - - message: "#^Only booleans are allowed in an if condition, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Command/ConfigCommand.php - - message: "#^Parameter \\#1 \\$callback of function call_user_func expects callable\\(\\)\\: mixed, array\\{Composer\\\\Config\\\\JsonConfigSource, string\\} given\\.$#" count: 2 @@ -60,6 +55,26 @@ parameters: count: 2 path: ../src/Composer/Console/Application.php + - + message: "#^Parameter \\#2 \\$mode of method Symfony\\\\Component\\\\Console\\\\Input\\\\InputArgument\\:\\:__construct\\(\\) expects int\\<0, 7\\>\\|null, int\\|null given\\.$#" + count: 1 + path: ../src/Composer/Console/Input/InputArgument.php + + - + message: "#^Parameter \\#3 \\$mode of method Symfony\\\\Component\\\\Console\\\\Input\\\\InputOption\\:\\:__construct\\(\\) expects int\\<0, 31\\>\\|null, int\\|null given\\.$#" + count: 1 + path: ../src/Composer/Console/Input/InputOption.php + + - + message: "#^Casting to string something that's already string\\.$#" + count: 1 + path: ../src/Composer/DependencyResolver/GenericRule.php + + - + message: "#^Casting to string something that's already string\\.$#" + count: 1 + path: ../src/Composer/DependencyResolver/MultiConflictRule.php + - message: "#^Parameter \\#2 \\$callback of function uksort expects callable\\(string, string\\)\\: int, 'version_compare' given\\.$#" count: 2 @@ -100,16 +115,6 @@ parameters: count: 1 path: ../src/Composer/EventDispatcher/EventDispatcher.php - - - message: "#^Parameter \\#1 \\$stream of function fwrite expects resource, resource\\|false given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^Parameter \\#1 \\$stream of function rewind expects resource, resource\\|false given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - message: "#^Parameter \\#1 \\$stream of function fclose expects resource, resource\\|false given\\.$#" count: 1 @@ -131,7 +136,7 @@ parameters: path: ../src/Composer/Package/Archiver/PharArchiver.php - - message: "#^Parameter \\#1 \\$array of function ksort expects array, array\\\\|string given\\.$#" + message: "#^Parameter \\#1 \\$array of function ksort expects array, array\\|string given\\.$#" count: 1 path: ../src/Composer/Package/Dumper/ArrayDumper.php @@ -172,64 +177,19 @@ parameters: - message: "#^Casting to string something that's already string\\.$#" - count: 4 + count: 3 path: ../src/Composer/Util/Filesystem.php - message: "#^Parameter \\#1 \\$string of function rawurlencode expects string, string\\|null given\\.$#" - count: 15 + count: 10 path: ../src/Composer/Util/Git.php - - - message: "#^Parameter \\#1 \\$string of function rawurlencode expects string, string\\|null given\\.$#" - count: 2 - path: ../src/Composer/Util/Hg.php - - - - message: "#^Only booleans are allowed in &&, int\\<0, 2097152\\> given on the right side\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Parameter \\#1 \\$multi_handle of function curl_multi_add_handle expects CurlMultiHandle, resource\\|null given\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Parameter \\#1 \\$multi_handle of function curl_multi_exec expects CurlMultiHandle, resource\\|null given\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Parameter \\#1 \\$multi_handle of function curl_multi_info_read expects CurlMultiHandle, resource\\|null given\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Parameter \\#1 \\$multi_handle of function curl_multi_remove_handle expects CurlMultiHandle, resource\\|null given\\.$#" - count: 2 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Parameter \\#1 \\$multi_handle of function curl_multi_select expects CurlMultiHandle, resource\\|null given\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|false given\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - - message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$multiHandle \\(resource\\|null\\) does not accept CurlMultiHandle\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$shareHandle \\(resource\\|null\\) does not accept CurlShareHandle\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#" count: 1 @@ -270,16 +230,16 @@ parameters: count: 1 path: ../src/Composer/Util/RemoteFilesystem.php - - - message: "#^Parameter \\#1 \\$include_path of function set_include_path expects string, string\\|false given\\.$#" - count: 2 - path: ../tests/Composer/Test/Autoload/AutoloadGeneratorTest.php - - message: "#^Parameter \\#1 \\$string of function rtrim expects string, string\\|false given\\.$#" count: 2 path: ../tests/Composer/Test/ConfigTest.php + - + message: "#^Casting to string something that's already string\\.$#" + count: 1 + path: ../tests/Composer/Test/DependencyResolver/RuleTest.php + - message: "#^Call to function method_exists\\(\\) with Composer\\\\Console\\\\Application and 'setCatchErrors' will always evaluate to true\\.$#" count: 1 diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index b17166a49727..7a89f1d8301e 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -85,11 +85,6 @@ parameters: count: 1 path: ../src/Composer/Autoload/AutoloadGenerator.php - - - message: "#^Parameter \\#2 \\$subject of static method Composer\\\\Pcre\\\\Preg\\:\\:isMatch\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Autoload/AutoloadGenerator.php - - message: "#^Parameter \\#2 \\$to of method Composer\\\\Util\\\\Filesystem\\:\\:findShortestPathCode\\(\\) expects string, string\\|false given\\.$#" count: 1 @@ -130,11 +125,6 @@ parameters: count: 1 path: ../src/Composer/Autoload/ClassLoader.php - - - message: "#^Casting to bool something that's already bool\\.$#" - count: 2 - path: ../src/Composer/Cache.php - - message: "#^Only booleans are allowed in a negated boolean, int\\<0, 50\\> given\\.$#" count: 1 @@ -271,7 +261,7 @@ parameters: path: ../src/Composer/Command/ConfigCommand.php - - message: "#^Only booleans are allowed in an if condition, string given\\.$#" + message: "#^Only booleans are allowed in an if condition, string\\|false given\\.$#" count: 1 path: ../src/Composer/Command/ConfigCommand.php @@ -310,11 +300,6 @@ parameters: count: 1 path: ../src/Composer/Command/CreateProjectCommand.php - - - message: "#^Parameter \\#3 \\$existingRepos of static method Composer\\\\Repository\\\\RepositoryFactory\\:\\:generateRepositoryName\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Command/CreateProjectCommand.php - - message: "#^Variable method call on Composer\\\\Package\\\\RootPackageInterface\\.$#" count: 1 @@ -332,7 +317,7 @@ parameters: - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 4 + count: 3 path: ../src/Composer/Command/DiagnoseCommand.php - @@ -497,7 +482,7 @@ parameters: - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" - count: 7 + count: 6 path: ../src/Composer/Command/InitCommand.php - @@ -505,11 +490,6 @@ parameters: count: 2 path: ../src/Composer/Command/LicensesCommand.php - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Command/ReinstallCommand.php - - message: "#^Foreach overwrites \\$type with its key variable\\.$#" count: 1 @@ -676,17 +656,7 @@ parameters: path: ../src/Composer/Command/ShowCommand.php - - message: "#^Only booleans are allowed in &&, array\\ given on the right side\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - - - message: "#^Only booleans are allowed in &&, array\\ given on the right side\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - - - message: "#^Only booleans are allowed in &&, array\\\\>\\> given on the right side\\.$#" + message: "#^Only booleans are allowed in &&, array given on the right side\\.$#" count: 1 path: ../src/Composer/Command/ShowCommand.php @@ -765,6 +735,11 @@ parameters: count: 1 path: ../src/Composer/Command/ShowCommand.php + - + message: "#^Parameter \\#1 \\$array \\(array\\<'available'\\|'installed'\\|'locked'\\|'platform', list\\\\>\\>\\) to function array_filter contains falsy values only, the result will always be an empty array\\.$#" + count: 1 + path: ../src/Composer/Command/ShowCommand.php + - message: "#^Parameter \\#1 \\$arrayTree of method Composer\\\\Command\\\\ShowCommand\\:\\:displayPackageTree\\(\\) expects array\\\\>, array\\\\>\\|string\\|null\\>\\> given\\.$#" count: 2 @@ -805,11 +780,6 @@ parameters: count: 2 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Parameter \\#2 \\$literals of method Composer\\\\DependencyResolver\\\\DefaultPolicy\\:\\:selectPreferredPackages\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Parameter \\#2 \\.\\.\\.\\$values of function sprintf expects bool\\|float\\|int\\|string\\|null, array\\|string given\\.$#" count: 1 @@ -910,19 +880,9 @@ parameters: count: 1 path: ../src/Composer/Command/UpdateCommand.php - - - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" - count: 1 - path: ../src/Composer/Command/UpdateCommand.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 3 - path: ../src/Composer/Command/UpdateCommand.php - - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 + count: 2 path: ../src/Composer/Command/UpdateCommand.php - @@ -975,11 +935,6 @@ parameters: count: 2 path: ../src/Composer/Config.php - - - message: "#^Casting to bool something that's already bool\\.$#" - count: 1 - path: ../src/Composer/Config.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 2 @@ -990,11 +945,6 @@ parameters: count: 1 path: ../src/Composer/Config.php - - - message: "#^Only booleans are allowed in a ternary operator condition, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Config.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 1 @@ -1057,7 +1007,7 @@ parameters: - message: "#^Only booleans are allowed in \\|\\|, string\\|false given on the left side\\.$#" - count: 2 + count: 1 path: ../src/Composer/Console/Application.php - @@ -1105,421 +1055,31 @@ parameters: count: 1 path: ../src/Composer/Console/GithubActionError.php - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Decisions.php - - - - message: "#^Method Composer\\\\DependencyResolver\\\\Decisions\\:\\:key\\(\\) should return int\\|null but returns int\\|string\\|null\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Decisions.php - - - - message: "#^Only booleans are allowed in a ternary operator condition, Composer\\\\DependencyResolver\\\\Pool\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Decisions.php - - message: "#^Return type \\(array\\{int, Composer\\\\DependencyResolver\\\\Rule\\}\\|false\\) of method Composer\\\\DependencyResolver\\\\Decisions\\:\\:current\\(\\) should be covariant with return type \\(array\\{int, Composer\\\\DependencyResolver\\\\Rule\\}\\) of method Iterator\\\\>\\:\\:current\\(\\)$#" count: 1 path: ../src/Composer/DependencyResolver/Decisions.php - - message: "#^Only booleans are allowed in &&, string\\|null given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/DefaultPolicy.php - - - - message: "#^Cannot access offset 'hash' on array\\|false\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/GenericRule.php - - - - message: "#^Only booleans are allowed in &&, string\\|null given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/LockTransaction.php - - - - message: "#^Cannot access offset 'hash' on array\\|false\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/MultiConflictRule.php - - - - message: "#^Only booleans are allowed in \\|\\|, mixed given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Pool.php - - - - message: "#^Cannot call method getPackages\\(\\) on Composer\\\\Repository\\\\LockArrayRepository\\|null\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/PoolBuilder.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/PoolBuilder.php - - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/PoolBuilder.php - - - - message: "#^Only booleans are allowed in an if condition, Composer\\\\EventDispatcher\\\\EventDispatcher\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/PoolBuilder.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/PoolBuilder.php - - - - message: "#^Only booleans are allowed in an if condition, mixed given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/PoolBuilder.php - - - - message: "#^Parameter \\#4 \\$index of method Composer\\\\DependencyResolver\\\\PoolBuilder\\:\\:removeLoadedPackage\\(\\) expects int, int\\|string given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/PoolBuilder.php - - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/PoolOptimizer.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/PoolOptimizer.php - - - - message: "#^Only booleans are allowed in an if condition, mixed given\\.$#" - count: 4 - path: ../src/Composer/DependencyResolver/PoolOptimizer.php - - - - message: "#^Cannot call method getRepoName\\(\\) on Composer\\\\Repository\\\\RepositoryInterface\\|null\\.$#" - count: 3 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Cannot call method getRepository\\(\\) on Composer\\\\Package\\\\PackageInterface\\|false\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\DependencyResolver\\\\Pool\\|null given on the left side\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\|null given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in a ternary operator condition, Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in a ternary operator condition, string given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in an if condition, Composer\\\\Package\\\\BasePackage\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 5 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Only booleans are allowed in an if condition, array\\\\> given\\.$#" + message: "#^Cannot call method getPrettyString\\(\\) on array\\\\|Composer\\\\Package\\\\BasePackage\\|Composer\\\\Package\\\\Link\\|int\\|string\\.$#" count: 1 path: ../src/Composer/DependencyResolver/Problem.php - - message: "#^Only booleans are allowed in an if condition, int\\<0, max\\> given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Problem.php - - - - message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\|string, string\\|null given\\.$#" + message: "#^Cannot call method getRepoName\\(\\) on Composer\\\\Repository\\\\RepositoryInterface\\|null\\.$#" count: 1 path: ../src/Composer/DependencyResolver/Problem.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: "#^Cannot cast array\\{package\\: Composer\\\\Package\\\\BasePackage\\}\\|array\\{packageName\\: string, constraint\\: Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\}\\|Composer\\\\Package\\\\BasePackage\\|Composer\\\\Package\\\\Link\\|int\\|non\\-empty\\-string to string\\.$#" count: 1 path: ../src/Composer/DependencyResolver/Problem.php - - message: "#^Only booleans are allowed in an if condition, Composer\\\\Repository\\\\LockArrayRepository\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Request.php - - - - message: "#^Foreach overwrites \\$literal with its value variable\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Method Composer\\\\DependencyResolver\\\\Rule\\:\\:getReason\\(\\) should return 2\\|3\\|6\\|7\\|10\\|12\\|13\\|14 but returns int\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Only booleans are allowed in &&, array\\ given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Only booleans are allowed in &&, array\\ given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Only booleans are allowed in a negated boolean, int given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Only booleans are allowed in an if condition, Composer\\\\Repository\\\\LockArrayRepository\\|null given\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Parameter \\#1 \\$literal of method Composer\\\\DependencyResolver\\\\Pool\\:\\:literalToPackage\\(\\) expects int, int\\|null given\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Rule.php - - - - message: "#^Parameter \\#1 \\$packages of static method Composer\\\\DependencyResolver\\\\Problem\\:\\:getPackageList\\(\\) expects array\\, array\\ given\\.$#" + message: "#^Method Composer\\\\DependencyResolver\\\\Rule\\:\\:getReason\\(\\) should return 2\\|3\\|6\\|7\\|10\\|12\\|13\\|14 but returns int\\<0, 255\\>\\.$#" count: 1 path: ../src/Composer/DependencyResolver/Rule.php - - - message: "#^Only booleans are allowed in &&, Composer\\\\DependencyResolver\\\\Pool\\|null given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSet.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\DependencyResolver\\\\Request\\|null given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSet.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\Repository\\\\RepositorySet\\|null given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSet.php - - - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\DependencyResolver\\\\Rule\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSetGenerator.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSetGenerator.php - - - - message: "#^Parameter \\#3 \\$reasonData of class Composer\\\\DependencyResolver\\\\GenericRule constructor expects array\\{package\\: Composer\\\\Package\\\\BasePackage\\}\\|array\\{packageName\\: string, constraint\\: Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\}\\|Composer\\\\Package\\\\BasePackage\\|Composer\\\\Package\\\\Link\\|int\\|string, array\\{package\\: Composer\\\\Package\\\\BasePackage\\}\\|array\\{packageName\\: string, constraint\\: Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\}\\|Composer\\\\Package\\\\BasePackage\\|Composer\\\\Package\\\\Link\\|int\\|string\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSetGenerator.php - - - - message: "#^Parameter \\#4 \\$reasonData of class Composer\\\\DependencyResolver\\\\Rule2Literals constructor expects array\\{package\\: Composer\\\\Package\\\\BasePackage\\}\\|array\\{packageName\\: string, constraint\\: Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\}\\|Composer\\\\Package\\\\BasePackage\\|Composer\\\\Package\\\\Link\\|int\\|string, array\\{package\\: Composer\\\\Package\\\\BasePackage\\}\\|array\\{packageName\\: string, constraint\\: Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\}\\|Composer\\\\Package\\\\BasePackage\\|Composer\\\\Package\\\\Link\\|int\\|string\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSetGenerator.php - - - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSetGenerator.php - - - - message: "#^Return type \\(\\-1\\|0\\|1\\|4\\) of method Composer\\\\DependencyResolver\\\\RuleSetIterator\\:\\:key\\(\\) should be covariant with return type \\(0\\|1\\|4\\) of method Iterator\\\\:\\:key\\(\\)$#" - count: 1 - path: ../src/Composer/DependencyResolver/RuleSetIterator.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Foreach overwrites \\$literal with its value variable\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\DependencyResolver\\\\Rule\\|null given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in &&, int\\<0, max\\> given on the right side\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in &&, mixed given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in a negated boolean, int given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in an if condition, Composer\\\\DependencyResolver\\\\Rule\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in an if condition, int given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in an if condition, int\\<0, max\\> given\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Only booleans are allowed in an if condition, mixed given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Parameter \\#1 \\$literal of method Composer\\\\DependencyResolver\\\\Decisions\\:\\:decide\\(\\) expects int, int\\|string given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Parameter \\#1 \\$literals of class Composer\\\\DependencyResolver\\\\GenericRule constructor expects list\\, non\\-empty\\-list\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Parameter \\#1 \\$number of function abs expects int, int\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Parameter \\#1 \\$rule of method Composer\\\\DependencyResolver\\\\Problem\\:\\:addRule\\(\\) expects Composer\\\\DependencyResolver\\\\Rule, Composer\\\\DependencyResolver\\\\Rule\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Parameter \\#2 \\$literal of method Composer\\\\DependencyResolver\\\\Solver\\:\\:setPropagateLearn\\(\\) expects int\\|string, int\\|null given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Parameter \\#2 \\$presentMap of class Composer\\\\DependencyResolver\\\\LockTransaction constructor expects array\\, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Solver.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/SolverProblemsException.php - - - - message: "#^Only booleans are allowed in &&, int\\<0, max\\>\\|false given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/SolverProblemsException.php - - - - message: "#^Only booleans are allowed in &&, int\\<0, max\\>\\|false given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/SolverProblemsException.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/SolverProblemsException.php - - - - message: "#^Only booleans are allowed in an if condition, int\\<0, max\\>\\|false given\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/SolverProblemsException.php - - - - message: "#^Only booleans are allowed in \\|\\|, int\\<0, max\\>\\|false given on the left side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/SolverProblemsException.php - - - - message: "#^Only booleans are allowed in \\|\\|, int\\<0, max\\>\\|false given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/SolverProblemsException.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Transaction.php - - - - message: "#^Only booleans are allowed in &&, array given on the right side\\.$#" - count: 1 - path: ../src/Composer/DependencyResolver/Transaction.php - - - - message: "#^Only booleans are allowed in a negated boolean, int\\<0, max\\> given\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Transaction.php - - - - message: "#^Only booleans are allowed in \\|\\|, int\\<0, max\\> given on the right side\\.$#" - count: 2 - path: ../src/Composer/DependencyResolver/Transaction.php - - message: "#^Call to function array_search\\(\\) requires parameter \\#3 to be set\\.$#" count: 1 @@ -1537,133 +1097,53 @@ parameters: - message: "#^Method Composer\\\\Downloader\\\\DownloadManager\\:\\:getDownloaderType\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\Package\\\\PackageInterface\\|null given on the left side\\.$#" - count: 1 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Downloader\\\\DownloaderInterface\\|null given\\.$#" - count: 4 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Only booleans are allowed in an if condition, Composer\\\\Downloader\\\\DownloaderInterface\\|null given\\.$#" - count: 4 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" - count: 2 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Parameter \\#1 \\$downloader of method Composer\\\\Downloader\\\\DownloadManager\\:\\:getDownloaderType\\(\\) expects Composer\\\\Downloader\\\\DownloaderInterface, Composer\\\\Downloader\\\\DownloaderInterface\\|null given\\.$#" - count: 1 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Parameter \\#1 \\$type of method Composer\\\\Downloader\\\\DownloadManager\\:\\:getDownloader\\(\\) expects string, string\\|null given\\.$#" - count: 2 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" - count: 1 - path: ../src/Composer/Downloader/DownloadManager.php - - - - message: "#^Call to function array_search\\(\\) requires parameter \\#3 to be set\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Foreach overwrites \\$path with its value variable\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Method Composer\\\\Downloader\\\\FileDownloader\\:\\:getDistPath\\(\\) should return string but returns array\\\\|string\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\Cache\\|null given on the left side\\.$#" - count: 4 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Only booleans are allowed in &&, string\\|null given on the left side\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Only booleans are allowed in a negated boolean, int given\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Only booleans are allowed in a negated boolean, string\\|null given\\.$#" - count: 2 - path: ../src/Composer/Downloader/FileDownloader.php - - - - message: "#^Only booleans are allowed in an if condition, Composer\\\\EventDispatcher\\\\EventDispatcher\\|null given\\.$#" - count: 2 - path: ../src/Composer/Downloader/FileDownloader.php + count: 1 + path: ../src/Composer/Downloader/DownloadManager.php - - message: "#^Only booleans are allowed in an if condition, Exception\\|null given\\.$#" + message: "#^Only booleans are allowed in &&, Composer\\\\Package\\\\PackageInterface\\|null given on the left side\\.$#" count: 1 - path: ../src/Composer/Downloader/FileDownloader.php + path: ../src/Composer/Downloader/DownloadManager.php - - message: "#^Only booleans are allowed in an if condition, array\\\\> given\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php + message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Downloader\\\\DownloaderInterface\\|null given\\.$#" + count: 4 + path: ../src/Composer/Downloader/DownloadManager.php - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" + message: "#^Only booleans are allowed in a negated boolean, array\\ given\\.$#" count: 1 - path: ../src/Composer/Downloader/FileDownloader.php + path: ../src/Composer/Downloader/DownloadManager.php - - message: "#^Only booleans are allowed in an if condition, int given\\.$#" - count: 1 - path: ../src/Composer/Downloader/FileDownloader.php + message: "#^Only booleans are allowed in an if condition, Composer\\\\Downloader\\\\DownloaderInterface\\|null given\\.$#" + count: 4 + path: ../src/Composer/Downloader/DownloadManager.php - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" + count: 2 + path: ../src/Composer/Downloader/DownloadManager.php + + - + message: "#^Parameter \\#1 \\$downloader of method Composer\\\\Downloader\\\\DownloadManager\\:\\:getDownloaderType\\(\\) expects Composer\\\\Downloader\\\\DownloaderInterface, Composer\\\\Downloader\\\\DownloaderInterface\\|null given\\.$#" count: 1 - path: ../src/Composer/Downloader/FileDownloader.php + path: ../src/Composer/Downloader/DownloadManager.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" - count: 3 - path: ../src/Composer/Downloader/FileDownloader.php + message: "#^Parameter \\#1 \\$type of method Composer\\\\Downloader\\\\DownloadManager\\:\\:getDownloader\\(\\) expects string, string\\|null given\\.$#" + count: 2 + path: ../src/Composer/Downloader/DownloadManager.php - - message: "#^Strict comparison using \\=\\=\\= between null and Composer\\\\Util\\\\Http\\\\Response will always evaluate to false\\.$#" + message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 1 - path: ../src/Composer/Downloader/FileDownloader.php + path: ../src/Composer/Downloader/DownloadManager.php - - message: "#^Parameter \\#3 \\$cwd of method Composer\\\\Util\\\\ProcessExecutor\\:\\:execute\\(\\) expects string\\|null, string\\|false given\\.$#" - count: 5 - path: ../src/Composer/Downloader/FossilDownloader.php + message: "#^Strict comparison using \\=\\=\\= between null and Composer\\\\Util\\\\Http\\\\Response\\|string will always evaluate to false\\.$#" + count: 1 + path: ../src/Composer/Downloader/FileDownloader.php - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" @@ -1750,51 +1230,6 @@ parameters: count: 3 path: ../src/Composer/Downloader/HgDownloader.php - - - message: "#^Only booleans are allowed in an if condition, array\\\\|null given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Only booleans are allowed in an if condition, mixed given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Only booleans are allowed in an if condition, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Parameter \\#1 \\$path of function realpath expects string, string\\|null given\\.$#" - count: 3 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Parameter \\#1 \\$path of method Composer\\\\Util\\\\Filesystem\\:\\:isAbsolutePath\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Parameter \\#1 \\$path of method Composer\\\\Util\\\\Filesystem\\:\\:normalizePath\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Parameter \\#1 \\$path of method Composer\\\\Util\\\\Filesystem\\:\\:normalizePath\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Parameter \\#1 \\$target of method Composer\\\\Util\\\\Filesystem\\:\\:junction\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - - - message: "#^Parameter \\#2 \\$to of method Composer\\\\Util\\\\Filesystem\\:\\:findShortestPath\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/PathDownloader.php - - message: "#^Cannot call method cleanupClientSpec\\(\\) on Composer\\\\Util\\\\Perforce\\|null\\.$#" count: 1 @@ -1912,7 +1347,7 @@ parameters: - message: "#^Only booleans are allowed in a negated boolean, array\\\\> given\\.$#" - count: 3 + count: 2 path: ../src/Composer/Downloader/ZipDownloader.php - @@ -1926,17 +1361,17 @@ parameters: path: ../src/Composer/Downloader/ZipDownloader.php - - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" - count: 1 + message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" + count: 2 path: ../src/Composer/EventDispatcher/EventDispatcher.php - - message: "#^Cannot access offset 0 on array\\{0\\: string, 1\\?\\: int\\}\\|int\\|string\\.$#" + message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" count: 1 path: ../src/Composer/EventDispatcher/EventDispatcher.php - - message: "#^Casting to bool something that's already bool\\.$#" + message: "#^Cannot access offset 0 on array\\{0\\: string, 1\\?\\: int\\}\\|int\\|string\\.$#" count: 1 path: ../src/Composer/EventDispatcher/EventDispatcher.php @@ -1975,11 +1410,6 @@ parameters: count: 1 path: ../src/Composer/EventDispatcher/EventDispatcher.php - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/EventDispatcher/EventDispatcher.php - - message: "#^Only booleans are allowed in an if condition, string\\|false given\\.$#" count: 2 @@ -2047,7 +1477,7 @@ parameters: - message: "#^Only booleans are allowed in an if condition, string\\|false given\\.$#" - count: 6 + count: 5 path: ../src/Composer/Factory.php - @@ -2067,7 +1497,7 @@ parameters: - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" - count: 3 + count: 2 path: ../src/Composer/Factory.php - @@ -2075,46 +1505,6 @@ parameters: count: 1 path: ../src/Composer/IO/BaseIO.php - - - message: "#^Only booleans are allowed in a ternary operator condition, Symfony\\\\Component\\\\Console\\\\Formatter\\\\OutputFormatterInterface\\|null given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^PHPDoc type Symfony\\\\Component\\\\Console\\\\Input\\\\StringInput of property Composer\\\\IO\\\\BufferIO\\:\\:\\$input is not the same as PHPDoc type Symfony\\\\Component\\\\Console\\\\Input\\\\InputInterface of overridden property Composer\\\\IO\\\\ConsoleIO\\:\\:\\$input\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^PHPDoc type Symfony\\\\Component\\\\Console\\\\Output\\\\StreamOutput of property Composer\\\\IO\\\\BufferIO\\:\\:\\$output is not the same as PHPDoc type Symfony\\\\Component\\\\Console\\\\Output\\\\OutputInterface of overridden property Composer\\\\IO\\\\ConsoleIO\\:\\:\\$output\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^Parameter \\#1 \\$fp of function fwrite expects resource, resource\\|false given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^Parameter \\#1 \\$fp of function rewind expects resource, resource\\|false given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^Parameter \\#1 \\$stream of class Symfony\\\\Component\\\\Console\\\\Output\\\\StreamOutput constructor expects resource, resource\\|false given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^Parameter \\#1 \\$stream of method Symfony\\\\Component\\\\Console\\\\Input\\\\Input\\:\\:setStream\\(\\) expects resource, resource\\|false given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - - - message: "#^Parameter \\#3 \\$subject of static method Composer\\\\Pcre\\\\Preg\\:\\:replaceCallback\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/IO/BufferIO.php - - message: "#^Parameter \\#1 \\$attempts of method Symfony\\\\Component\\\\Console\\\\Question\\\\Question\\:\\:setMaxAttempts\\(\\) expects int\\|null, int\\\\|int\\<1, max\\>\\|true\\|null given\\.$#" count: 1 @@ -2140,11 +1530,6 @@ parameters: count: 1 path: ../src/Composer/Installer.php - - - message: "#^Casting to bool something that's already bool\\.$#" - count: 11 - path: ../src/Composer/Installer.php - - message: "#^Only booleans are allowed in &&, array\\\\> given on the right side\\.$#" count: 1 @@ -2205,11 +1590,6 @@ parameters: count: 2 path: ../src/Composer/Installer/BinaryInstaller.php - - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Installer/BinaryInstaller.php - - message: "#^Parameter \\#1 \\$binPath of method Composer\\\\Installer\\\\BinaryInstaller\\:\\:installFullBinaries\\(\\) expects string, string\\|false given\\.$#" count: 1 @@ -2235,11 +1615,6 @@ parameters: count: 1 path: ../src/Composer/Installer/BinaryInstaller.php - - - message: "#^Parameter \\#2 \\$subject of static method Composer\\\\Pcre\\\\Preg\\:\\:isMatch\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Installer/BinaryInstaller.php - - message: "#^Property Composer\\\\Installer\\\\BinaryInstaller\\:\\:\\$binDir \\(string\\) does not accept string\\|false\\.$#" count: 1 @@ -2250,71 +1625,6 @@ parameters: count: 1 path: ../src/Composer/Installer/BinaryInstaller.php - - - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" - count: 2 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Foreach overwrites \\$batch with its value variable\\.$#" - count: 2 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\EventDispatcher\\\\EventDispatcher\\|null given on the right side\\.$#" - count: 2 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in &&, array given on the right side\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in a negated boolean, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in a negated boolean, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in an if condition, Symfony\\\\Component\\\\Console\\\\Helper\\\\ProgressBar\\|null given\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 4 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in an if condition, int\\<0, max\\> given\\.$#" - count: 2 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in an if condition, int\\<0, max\\>\\|false given\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - - - message: "#^Parameter \\#2 \\$offset of function array_splice expects int, int\\|string given\\.$#" - count: 1 - path: ../src/Composer/Installer/InstallationManager.php - - message: "#^Variable method call on \\$this\\(Composer\\\\Installer\\\\InstallationManager\\)\\.$#" count: 2 @@ -2385,11 +1695,6 @@ parameters: count: 1 path: ../src/Composer/Installer/SuggestedPackagesReporter.php - - - message: "#^Only booleans are allowed in a ternary operator condition, mixed given\\.$#" - count: 2 - path: ../src/Composer/Installer/SuggestedPackagesReporter.php - - message: "#^Only booleans are allowed in an if condition, Composer\\\\Package\\\\PackageInterface\\|null given\\.$#" count: 2 @@ -2490,16 +1795,6 @@ parameters: count: 1 path: ../src/Composer/Package/AliasPackage.php - - - message: "#^Call to function method_exists\\(\\) with Closure\\(SplFileInfo\\)\\: bool and 'bindTo' will always evaluate to true\\.$#" - count: 1 - path: ../src/Composer/Package/Archiver/ArchivableFilesFinder.php - - - - message: "#^Parameter \\#1 \\$path of method Composer\\\\Util\\\\Filesystem\\:\\:normalizePath\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Package/Archiver/ArchivableFilesFinder.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 3 @@ -2560,6 +1855,16 @@ parameters: count: 1 path: ../src/Composer/Package/BasePackage.php + - + message: "#^Method Composer\\\\Package\\\\BasePackage\\:\\:packageNameToRegexp\\(\\) should return non\\-empty\\-string but returns string\\.$#" + count: 1 + path: ../src/Composer/Package/BasePackage.php + + - + message: "#^Method Composer\\\\Package\\\\BasePackage\\:\\:packageNamesToRegexp\\(\\) should return non\\-empty\\-string but returns string\\.$#" + count: 1 + path: ../src/Composer/Package/BasePackage.php + - message: "#^Only booleans are allowed in &&, Composer\\\\Repository\\\\RepositoryInterface\\|null given on the left side\\.$#" count: 1 @@ -2600,28 +1905,18 @@ parameters: count: 2 path: ../src/Composer/Package/Dumper/ArrayDumper.php - - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Package/Dumper/ArrayDumper.php - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" count: 1 path: ../src/Composer/Package/Dumper/ArrayDumper.php - - - message: "#^Only booleans are allowed in an if condition, string given\\.$#" - count: 1 - path: ../src/Composer/Package/Dumper/ArrayDumper.php - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" - count: 4 + count: 1 path: ../src/Composer/Package/Dumper/ArrayDumper.php - - message: "#^Parameter \\#1 \\$array_arg of function ksort expects array, array\\\\|string given\\.$#" + message: "#^Parameter \\#1 \\$array_arg of function ksort expects array, array\\|string given\\.$#" count: 1 path: ../src/Composer/Package/Dumper/ArrayDumper.php @@ -2705,21 +2000,6 @@ parameters: count: 2 path: ../src/Composer/Package/Loader/RootPackageLoader.php - - - message: "#^Parameter \\#1 \\$requires of method Composer\\\\Package\\\\Loader\\\\RootPackageLoader\\:\\:extractAliases\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Package/Loader/RootPackageLoader.php - - - - message: "#^Parameter \\#1 \\$requires of static method Composer\\\\Package\\\\Loader\\\\RootPackageLoader\\:\\:extractReferences\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Package/Loader/RootPackageLoader.php - - - - message: "#^Parameter \\#1 \\$requires of static method Composer\\\\Package\\\\Loader\\\\RootPackageLoader\\:\\:extractStabilityFlags\\(\\) expects array\\, array\\ given\\.$#" - count: 1 - path: ../src/Composer/Package/Loader/RootPackageLoader.php - - message: "#^Parameter \\#2 \\$class \\(class\\-string\\\\) of method Composer\\\\Package\\\\Loader\\\\RootPackageLoader\\:\\:load\\(\\) should be contravariant with parameter \\$class \\(class\\-string\\\\) of method Composer\\\\Package\\\\Loader\\\\ArrayLoader\\:\\:load\\(\\)$#" count: 1 @@ -3025,21 +2305,11 @@ parameters: count: 1 path: ../src/Composer/Plugin/PostFileDownloadEvent.php - - - message: "#^Casting to bool something that's already bool\\.$#" - count: 1 - path: ../src/Composer/Question/StrictConfirmationQuestion.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 2 path: ../src/Composer/Question/StrictConfirmationQuestion.php - - - message: "#^Method Composer\\\\Repository\\\\ArrayRepository\\:\\:getProviders\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: ../src/Composer/Repository/ArrayRepository.php - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Semver\\\\Constraint\\\\ConstraintInterface\\|null given\\.$#" count: 1 @@ -3086,17 +2356,7 @@ parameters: path: ../src/Composer/Repository/ComposerRepository.php - - message: "#^Method Composer\\\\Repository\\\\ComposerRepository\\:\\:getProviders\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: ../src/Composer/Repository/ComposerRepository.php - - - - message: "#^Method Composer\\\\Repository\\\\ComposerRepository\\:\\:getProviders\\(\\) should return array\\ but returns array\\\\.$#" - count: 1 - path: ../src/Composer/Repository/ComposerRepository.php - - - - message: "#^Method Composer\\\\Repository\\\\ComposerRepository\\:\\:whatProvides\\(\\) should return array\\ but returns array\\\\.$#" + message: "#^Method Composer\\\\Repository\\\\ComposerRepository\\:\\:getProviders\\(\\) should return array\\ but returns array\\\\.$#" count: 1 path: ../src/Composer/Repository/ComposerRepository.php @@ -3157,7 +2417,7 @@ parameters: - message: "#^Only booleans are allowed in a negated boolean, mixed given\\.$#" - count: 2 + count: 1 path: ../src/Composer/Repository/ComposerRepository.php - @@ -3211,7 +2471,7 @@ parameters: path: ../src/Composer/Repository/CompositeRepository.php - - message: "#^Only booleans are allowed in a ternary operator condition, array\\\\>\\> given\\.$#" + message: "#^Only booleans are allowed in a ternary operator condition, array\\\\>\\> given\\.$#" count: 1 path: ../src/Composer/Repository/CompositeRepository.php @@ -3341,14 +2601,9 @@ parameters: path: ../src/Composer/Repository/PathRepository.php - - message: "#^Composer\\\\Repository\\\\PearRepository\\:\\:__construct\\(\\) does not call parent constructor from Composer\\\\Repository\\\\ArrayRepository\\.$#" - count: 1 - path: ../src/Composer/Repository/PearRepository.php - - - - message: "#^Call to an undefined method object\\:\\:getVersion\\(\\)\\.$#" + message: "#^Composer\\\\Repository\\\\PearRepository\\:\\:__construct\\(\\) does not call parent constructor from Composer\\\\Repository\\\\ArrayRepository\\.$#" count: 1 - path: ../src/Composer/Repository/PlatformRepository.php + path: ../src/Composer/Repository/PearRepository.php - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" @@ -3434,7 +2689,7 @@ parameters: path: ../src/Composer/Repository/RepositorySet.php - - message: "#^Only booleans are allowed in an if condition, array\\\\> given\\.$#" + message: "#^Only booleans are allowed in an if condition, array\\\\> given\\.$#" count: 1 path: ../src/Composer/Repository/RepositorySet.php @@ -3443,21 +2698,6 @@ parameters: count: 1 path: ../src/Composer/Repository/RepositorySet.php - - - message: "#^Only booleans are allowed in a negated boolean, string given\\.$#" - count: 1 - path: ../src/Composer/Repository/Vcs/FossilDriver.php - - - - message: "#^Parameter \\#1 \\$file of method Composer\\\\Util\\\\Filesystem\\:\\:remove\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Repository/Vcs/FossilDriver.php - - - - message: "#^Parameter \\#1 \\$filename of function is_file expects string, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Repository/Vcs/FossilDriver.php - - message: "#^Call to function array_search\\(\\) requires parameter \\#3 to be set\\.$#" count: 2 @@ -3510,11 +2750,6 @@ parameters: - message: "#^Only booleans are allowed in &&, string given on the left side\\.$#" - count: 3 - path: ../src/Composer/Repository/Vcs/GitDriver.php - - - - message: "#^Only booleans are allowed in a negated boolean, string given\\.$#" count: 1 path: ../src/Composer/Repository/Vcs/GitDriver.php @@ -3630,7 +2865,7 @@ parameters: - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" - count: 5 + count: 4 path: ../src/Composer/Repository/Vcs/GitLabDriver.php - @@ -3753,11 +2988,6 @@ parameters: count: 2 path: ../src/Composer/Repository/Vcs/SvnDriver.php - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: ../src/Composer/Repository/Vcs/SvnDriver.php - - message: "#^Method Composer\\\\Repository\\\\Vcs\\\\SvnDriver\\:\\:getRootIdentifier\\(\\) should return string but returns string\\|false\\.$#" count: 1 @@ -3768,26 +2998,11 @@ parameters: count: 1 path: ../src/Composer/Repository/Vcs/SvnDriver.php - - - message: "#^Only booleans are allowed in &&, string given on the left side\\.$#" - count: 4 - path: ../src/Composer/Repository/Vcs/SvnDriver.php - - message: "#^Only booleans are allowed in &&, string\\|false given on the right side\\.$#" count: 1 path: ../src/Composer/Repository/Vcs/SvnDriver.php - - - message: "#^Only booleans are allowed in a negated boolean, string given\\.$#" - count: 1 - path: ../src/Composer/Repository/Vcs/SvnDriver.php - - - - message: "#^Only booleans are allowed in an if condition, string given\\.$#" - count: 3 - path: ../src/Composer/Repository/Vcs/SvnDriver.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 1 @@ -4009,14 +3224,9 @@ parameters: path: ../src/Composer/Util/ConfigValidator.php - - message: "#^Only booleans are allowed in a negated boolean, int given\\.$#" - count: 1 - path: ../src/Composer/Util/ErrorHandler.php - - - - message: "#^Only booleans are allowed in an if condition, Composer\\\\IO\\\\IOInterface\\|null given\\.$#" + message: "#^Casting to string something that's already string\\.$#" count: 1 - path: ../src/Composer/Util/ErrorHandler.php + path: ../src/Composer/Util/Filesystem.php - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" @@ -4028,26 +3238,11 @@ parameters: count: 1 path: ../src/Composer/Util/Git.php - - - message: "#^Only booleans are allowed in &&, string given on the right side\\.$#" - count: 1 - path: ../src/Composer/Util/Git.php - - - - message: "#^Only booleans are allowed in &&, string\\|false given on the left side\\.$#" - count: 1 - path: ../src/Composer/Util/Git.php - - message: "#^Only booleans are allowed in &&, string\\|null given on the left side\\.$#" count: 1 path: ../src/Composer/Util/Git.php - - - message: "#^Only booleans are allowed in an if condition, int\\<0, max\\>\\|false given\\.$#" - count: 1 - path: ../src/Composer/Util/Git.php - - message: "#^Only booleans are allowed in an if condition, string\\|false given\\.$#" count: 2 @@ -4055,7 +3250,7 @@ parameters: - message: "#^Parameter \\#1 \\$str of function rawurlencode expects string, string\\|null given\\.$#" - count: 15 + count: 10 path: ../src/Composer/Util/Git.php - @@ -4103,11 +3298,6 @@ parameters: count: 2 path: ../src/Composer/Util/GitLab.php - - - message: "#^Parameter \\#1 \\$str of function rawurlencode expects string, string\\|null given\\.$#" - count: 2 - path: ../src/Composer/Util/Hg.php - - message: "#^Call to function in_array\\(\\) requires parameter \\#3 to be set\\.$#" count: 1 @@ -4115,36 +3305,16 @@ parameters: - message: "#^Cannot access offset 'features' on array\\|false\\.$#" - count: 2 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Casting to string something that's already string\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Constant CURLOPT_PROXY_CAINFO not found\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Constant CURLOPT_PROXY_CAPATH not found\\.$#" + message: "#^Casting to string something that's already string\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 3 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Only booleans are allowed in &&, int given on the right side\\.$#" - count: 1 - path: ../src/Composer/Util/Http/CurlDownloader.php - - - - message: "#^Only booleans are allowed in &&, int\\<0, 65536\\> given on the right side\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php @@ -4204,35 +3374,30 @@ parameters: path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#1 \\$mh of function curl_multi_add_handle expects resource, resource\\|null given\\.$#" + message: "#^Parameter \\#1 \\$mh of function curl_multi_add_handle expects resource, CurlMultiHandle given\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#1 \\$mh of function curl_multi_exec expects resource, resource\\|null given\\.$#" + message: "#^Parameter \\#1 \\$mh of function curl_multi_exec expects resource, CurlMultiHandle given\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#1 \\$mh of function curl_multi_info_read expects resource, resource\\|null given\\.$#" + message: "#^Parameter \\#1 \\$mh of function curl_multi_info_read expects resource, CurlMultiHandle given\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#1 \\$mh of function curl_multi_remove_handle expects resource, resource\\|null given\\.$#" + message: "#^Parameter \\#1 \\$mh of function curl_multi_remove_handle expects resource, CurlMultiHandle given\\.$#" count: 2 path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#1 \\$mh of function curl_multi_select expects resource, resource\\|null given\\.$#" + message: "#^Parameter \\#1 \\$mh of function curl_multi_select expects resource, CurlMultiHandle given\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - - message: "#^Parameter \\#1 \\$mh of function curl_multi_setopt expects resource, resource\\|false given\\.$#" - count: 2 - path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#1 \\$str of function preg_quote expects string, string\\|false\\|null given\\.$#" count: 1 @@ -4304,7 +3469,7 @@ parameters: path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#" + message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$jobs \\(array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\) does not accept non\\-empty\\-array\\, retries\\: int\\<0, max\\>, storeAuth\\: 'prompt'\\|bool, ipResolve\\: 4\\|6\\|null\\}, options\\: array, progress\\: array, curlHandle\\: CurlHandle\\|resource\\|false, filename\\: string\\|null, headerHandle\\: resource, \\.\\.\\.\\}\\>\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php @@ -4314,82 +3479,47 @@ parameters: path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$multiHandle \\(resource\\|null\\) does not accept resource\\|false\\.$#" + message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$multiHandle \\(CurlMultiHandle\\) does not accept resource\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$multiHandle has unknown class CurlMultiHandle as its type\\.$#" count: 1 path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: ../src/Composer/Util/Http/ProxyHelper.php - - - - message: "#^Foreach overwrites \\$name with its value variable\\.$#" - count: 1 - path: ../src/Composer/Util/Http/ProxyHelper.php - - - - message: "#^Implicit array creation is not allowed \\- variable \\$options does not exist\\.$#" - count: 1 - path: ../src/Composer/Util/Http/ProxyHelper.php - - - - message: "#^Only booleans are allowed in a negated boolean, int\\<0, 65535\\>\\|false\\|null given\\.$#" - count: 1 - path: ../src/Composer/Util/Http/ProxyHelper.php - - - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" - count: 3 - path: ../src/Composer/Util/Http/ProxyHelper.php - - - - message: "#^Parameter \\#1 \\$proxy of static method Composer\\\\Util\\\\Http\\\\ProxyHelper\\:\\:formatParsedUrl\\(\\) expects array\\{scheme\\?\\: string, host\\: string, port\\?\\: int, user\\?\\: string, pass\\?\\: string\\}, array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false given\\.$#" - count: 1 - path: ../src/Composer/Util/Http/ProxyHelper.php - - - - message: "#^Only booleans are allowed in &&, Composer\\\\Util\\\\NoProxyPattern\\|null given on the left side\\.$#" + message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$shareHandle \\(CurlShareHandle\\) does not accept resource\\.$#" count: 1 - path: ../src/Composer/Util/Http/ProxyManager.php + path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Only booleans are allowed in &&, string\\|null given on the right side\\.$#" + message: "#^Property Composer\\\\Util\\\\Http\\\\CurlDownloader\\:\\:\\$shareHandle has unknown class CurlShareHandle as its type\\.$#" count: 1 - path: ../src/Composer/Util/Http/ProxyManager.php + path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Util\\\\Http\\\\ProxyManager\\|null given\\.$#" + message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 1 - path: ../src/Composer/Util/Http/ProxyManager.php - - - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" - count: 4 - path: ../src/Composer/Util/Http/ProxyManager.php + path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Parameter \\#3 \\$formattedUrl of class Composer\\\\Util\\\\Http\\\\RequestProxy constructor expects string, string\\|null given\\.$#" + message: "#^Type alias Job contains unknown class CurlHandle\\.$#" count: 1 - path: ../src/Composer/Util/Http/ProxyManager.php + path: ../src/Composer/Util/Http/CurlDownloader.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: "#^Constant CURLOPT_PROXY_CAINFO not found\\.$#" count: 1 - path: ../src/Composer/Util/Http/ProxyManager.php + path: ../src/Composer/Util/Http/RequestProxy.php - - message: "#^Only booleans are allowed in an if condition, string given\\.$#" + message: "#^Constant CURLOPT_PROXY_CAPATH not found\\.$#" count: 1 path: ../src/Composer/Util/Http/RequestProxy.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" + message: "#^Method Composer\\\\Util\\\\Http\\\\RequestProxy\\:\\:getCurlOptions\\(\\) should return array\\ but returns array\\\\.$#" count: 1 path: ../src/Composer/Util/Http/RequestProxy.php @@ -4598,41 +3728,6 @@ parameters: count: 1 path: ../src/Composer/Util/Perforce.php - - - message: "#^Cannot access offset 'dir' on array\\|false\\.$#" - count: 1 - path: ../src/Composer/Util/Platform.php - - - - message: "#^Casting to string something that's already string\\.$#" - count: 1 - path: ../src/Composer/Util/Platform.php - - - - message: "#^Only booleans are allowed in &&, array\\|false given on the left side\\.$#" - count: 1 - path: ../src/Composer/Util/Platform.php - - - - message: "#^Only booleans are allowed in &&, string\\|false given on the right side\\.$#" - count: 1 - path: ../src/Composer/Util/Platform.php - - - - message: "#^Only booleans are allowed in a negated boolean, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Util/Platform.php - - - - message: "#^Only booleans are allowed in a ternary operator condition, array\\\\|false given\\.$#" - count: 1 - path: ../src/Composer/Util/Platform.php - - - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" - count: 2 - path: ../src/Composer/Util/Platform.php - - message: "#^Method Composer\\\\Util\\\\ProcessExecutor\\:\\:doExecute\\(\\) should return int but returns int\\|null\\.$#" count: 1 @@ -4658,6 +3753,16 @@ parameters: count: 8 path: ../src/Composer/Util/RemoteFilesystem.php + - + message: "#^Function http_clear_last_response_headers not found\\.$#" + count: 1 + path: ../src/Composer/Util/RemoteFilesystem.php + + - + message: "#^Function http_get_last_response_headers not found\\.$#" + count: 1 + path: ../src/Composer/Util/RemoteFilesystem.php + - message: "#^Method Composer\\\\Util\\\\RemoteFilesystem\\:\\:copy\\(\\) should return bool but returns bool\\|string\\.$#" count: 1 @@ -4803,11 +3908,6 @@ parameters: count: 1 path: ../src/Composer/Util/StreamContextFactory.php - - - message: "#^Only booleans are allowed in an if condition, array given\\.$#" - count: 1 - path: ../src/Composer/Util/StreamContextFactory.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 2 @@ -4828,11 +3928,6 @@ parameters: count: 1 path: ../src/Composer/Util/Svn.php - - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" - count: 1 - path: ../src/Composer/Util/Svn.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 1 @@ -4908,11 +4003,6 @@ parameters: count: 2 path: ../src/bootstrap.php - - - message: "#^Call to method RecursiveDirectoryIterator\\:\\:getSubPathname\\(\\) with incorrect case\\: getSubPathName$#" - count: 1 - path: ../tests/Composer/Test/AllFunctionalTest.php - - message: "#^Method Composer\\\\Test\\\\AllFunctionalTest\\:\\:cleanOutput\\(\\) should return string but returns string\\|false\\.$#" count: 1 @@ -4928,31 +4018,11 @@ parameters: count: 1 path: ../tests/Composer/Test/AllFunctionalTest.php - - - message: "#^Only booleans are allowed in \\|\\|, string given on the right side\\.$#" - count: 1 - path: ../tests/Composer/Test/AllFunctionalTest.php - - - - message: "#^Only numeric types are allowed in \\-, int\\<0, max\\>\\|false given on the left side\\.$#" - count: 3 - path: ../tests/Composer/Test/AllFunctionalTest.php - - message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#" count: 1 path: ../tests/Composer/Test/AllFunctionalTest.php - - - message: "#^Parameter \\#2 \\$subject of static method Composer\\\\Pcre\\\\Preg\\:\\:split\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../tests/Composer/Test/AllFunctionalTest.php - - - - message: "#^Dynamic call to static method Composer\\\\Test\\\\Autoload\\\\AutoloadGeneratorTest\\:\\:assertFileContentEquals\\(\\)\\.$#" - count: 28 - path: ../tests/Composer/Test/Autoload/AutoloadGeneratorTest.php - - message: "#^Dynamic call to static method Composer\\\\Test\\\\TestCase\\:\\:ensureDirectoryExistsAndClear\\(\\)\\.$#" count: 1 @@ -4963,11 +4033,6 @@ parameters: count: 2 path: ../tests/Composer/Test/Autoload/AutoloadGeneratorTest.php - - - message: "#^Parameter \\#1 \\$new_include_path of function set_include_path expects string, string\\|false given\\.$#" - count: 2 - path: ../tests/Composer/Test/Autoload/AutoloadGeneratorTest.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 1 @@ -5136,16 +4201,6 @@ parameters: count: 3 path: ../tests/Composer/Test/Installer/BinaryInstallerTest.php - - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with 'Composer\\\\\\\\Composer' and Composer\\\\Composer will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Installer/InstallerEventTest.php - - - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with 'Composer\\\\\\\\IO…' and Composer\\\\IO\\\\IOInterface will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Installer/InstallerEventTest.php - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" count: 9 @@ -5216,11 +4271,6 @@ parameters: count: 1 path: ../tests/Composer/Test/Json/JsonManipulatorTest.php - - - message: "#^Parameter \\#1 \\$expectedJson of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertJsonStringEqualsJsonString\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../tests/Composer/Test/Json/JsonManipulatorTest.php - - message: "#^Offset 'ask' might not exist on array\\{ask\\: string, reply\\?\\: string\\}\\|array\\{auth\\: array\\{string, string, string\\|null\\}\\}\\|array\\{text\\: string, verbosity\\?\\: 1\\|2\\|4\\|8\\|16\\}\\.$#" count: 2 @@ -5246,6 +4296,11 @@ parameters: count: 2 path: ../tests/Composer/Test/Mock/ProcessExecutorMock.php + - + message: "#^Property Composer\\\\Test\\\\Mock\\\\ProcessExecutorMock\\:\\:\\$expectations \\(array\\\\|string, return\\: int, stdout\\: string, stderr\\: string, callback\\: \\(callable\\(\\)\\: mixed\\)\\|null\\}\\>\\|null\\) does not accept array\\, non\\-empty\\-list\\\\|\\(callable\\(\\)\\: mixed\\)\\|int\\|string\\>\\|\\(callable\\(\\)\\: mixed\\)\\|int\\|string\\|null\\>\\>\\.$#" + count: 1 + path: ../tests/Composer/Test/Mock/ProcessExecutorMock.php + - message: "#^Composer\\\\Test\\\\Mock\\\\VersionGuesserMock\\:\\:__construct\\(\\) does not call parent constructor from Composer\\\\Package\\\\Version\\\\VersionGuesser\\.$#" count: 1 @@ -5286,11 +4341,6 @@ parameters: count: 1 path: ../tests/Composer/Test/Package/Dumper/ArrayDumperTest.php - - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertTrue\\(\\) with true will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php - - message: "#^Only booleans are allowed in an if condition, string\\|null given\\.$#" count: 1 @@ -5321,11 +4371,6 @@ parameters: count: 1 path: ../tests/Composer/Test/Question/StrictConfirmationQuestionTest.php - - - message: "#^Cannot call method getDistType\\(\\) on Composer\\\\Package\\\\BasePackage\\|null\\.$#" - count: 1 - path: ../tests/Composer/Test/Repository/ArtifactRepositoryTest.php - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|null given\\.$#" count: 2 @@ -5356,11 +4401,6 @@ parameters: count: 1 path: ../tests/Composer/Test/Repository/PathRepositoryTest.php - - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with 'Composer\\\\\\\\Repository…' and Composer\\\\Repository\\\\RepositoryInterface will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Repository/RepositoryManagerTest.php - - message: "#^Parameter \\#1 \\$objectOrValue of method ReflectionProperty\\:\\:setValue\\(\\) expects object\\|null, object\\|string given\\.$#" count: 1 @@ -5407,29 +4447,14 @@ parameters: path: ../tests/Composer/Test/TestCase.php - - message: "#^Cannot access an offset on array\\\\>\\|false\\.$#" - count: 1 + message: "#^Cannot access an offset on array\\\\|int\\|string\\>\\>\\|false\\.$#" + count: 2 path: ../tests/Composer/Test/Util/GitTest.php - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with 'Composer\\\\\\\\Util\\\\\\\\Http…' and Composer\\\\Util\\\\Http\\\\ProxyManager will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php - - - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertInstanceOf\\(\\) with 'Composer\\\\\\\\Util\\\\\\\\Http…' and Composer\\\\Util\\\\Http\\\\RequestProxy will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php - - - - message: "#^Only booleans are allowed in an if condition, string given\\.$#" - count: 1 - path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php - - - - message: "#^Parameter \\#1 \\$haystack of function stripos expects string, string\\|null given\\.$#" + message: "#^Cannot access an offset on array\\\\>\\|false\\.$#" count: 1 - path: ../tests/Composer/Test/Util/Http/ProxyManagerTest.php + path: ../tests/Composer/Test/Util/GitTest.php - message: "#^Only booleans are allowed in an if condition, string\\|false\\|null given\\.$#" @@ -5451,31 +4476,16 @@ parameters: count: 2 path: ../tests/Composer/Test/Util/PerforceTest.php - - - message: "#^Parameter \\#2 \\$string of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringStartsWith\\(\\) expects string, string\\|false given\\.$#" - count: 2 - path: ../tests/Composer/Test/Util/PerforceTest.php - - message: "#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#" count: 1 path: ../tests/Composer/Test/Util/PlatformTest.php - - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertNotNull\\(\\) with string will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Util/ProcessExecutorTest.php - - message: "#^Dynamic call to static method Composer\\\\Util\\\\ProcessExecutor\\:\\:getTimeout\\(\\)\\.$#" count: 1 path: ../tests/Composer/Test/Util/ProcessExecutorTest.php - - - message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertTrue\\(\\) with true and 'callbackGet must…' will always evaluate to true\\.$#" - count: 1 - path: ../tests/Composer/Test/Util/RemoteFilesystemTest.php - - message: "#^Parameter \\#1 \\$object of method ReflectionProperty\\:\\:getValue\\(\\) expects object, object\\|string given\\.$#" count: 1 @@ -5491,16 +4501,6 @@ parameters: count: 2 path: ../tests/Composer/Test/Util/RemoteFilesystemTest.php - - - message: "#^Parameter \\#2 \\$haystack of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, bool\\|string given\\.$#" - count: 1 - path: ../tests/Composer/Test/Util/RemoteFilesystemTest.php - - - - message: "#^Parameter \\#2 \\$haystack of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertStringContainsString\\(\\) expects string, string\\|false given\\.$#" - count: 2 - path: ../tests/Composer/Test/Util/RemoteFilesystemTest.php - - message: "#^Implicit array creation is not allowed \\- variable \\$certificate does not exist\\.$#" count: 2 @@ -5510,4 +4510,3 @@ parameters: message: "#^Only booleans are allowed in a ternary operator condition, array\\ given\\.$#" count: 1 path: ../tests/Composer/Test/Util/TlsHelperTest.php - diff --git a/phpstan/config.neon b/phpstan/config.neon index 6ffdbe68e3bd..5581b24deceb 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -4,7 +4,10 @@ includes: - ../vendor/phpstan/phpstan-deprecation-rules/rules.neon - ../vendor/phpstan/phpstan-strict-rules/rules.neon - ../vendor/phpstan/phpstan-symfony/extension.neon + - ../vendor/composer/pcre/extension.neon - ../vendor/phpstan/phpstan-symfony/rules.neon + # TODO when requiring php 7.4+ we can use this + #- ../vendor/staabm/phpstan-todo-by/extension.neon - ./rules.neon # Composer-specific PHPStan extensions, can be reused by third party packages by including 'vendor/composer/composer/phpstan/rules.neon' in your phpstan config - ./baseline.neon - ./ignore-by-php-version.neon.php @@ -15,12 +18,12 @@ parameters: excludePaths: - '../tests/Composer/Test/Fixtures/*' - '../tests/Composer/Test/Autoload/Fixtures/*' - - '../tests/Composer/Test/Autoload/MinimumVersionSupport/vendor/' + - '../tests/Composer/Test/Autoload/MinimumVersionSupport/vendor/*' - '../tests/Composer/Test/Plugin/Fixtures/*' - - '../tests/Composer/Test/PolyfillTestCase.php' reportUnmatchedIgnoredErrors: false treatPhpDocTypesAsCertain: false + reportPossiblyNonexistentConstantArrayOffset: true ignoreErrors: # unused parameters diff --git a/phpstan/ignore-by-php-version.neon.php b/phpstan/ignore-by-php-version.neon.php index 21212110c723..14a209ea3739 100644 --- a/phpstan/ignore-by-php-version.neon.php +++ b/phpstan/ignore-by-php-version.neon.php @@ -3,7 +3,7 @@ // more inspiration at https://github.com/phpstan/phpstan-src/blob/master/build/ignore-by-php-version.neon.php $includes = []; if (PHP_VERSION_ID >= 80000) { - $includes[] = __DIR__ . '/baseline-8.1.neon'; + $includes[] = __DIR__ . '/baseline-8.3.neon'; } $config['includes'] = $includes; diff --git a/res/composer-lock-schema.json b/res/composer-lock-schema.json new file mode 100644 index 000000000000..b1ef31c2bfce --- /dev/null +++ b/res/composer-lock-schema.json @@ -0,0 +1,101 @@ +{ + "$schema": "https://json-schema.org/draft-04/schema#", + "title": "Composer Lock File", + "type": "object", + "required": [ "content-hash", "packages", "packages-dev" ], + "additionalProperties": true, + "properties": { + "_readme": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Informational text for humans reading the file" + }, + "content-hash": { + "type": "string", + "description": "Hash of all relevant properties of the composer.json that was used to create this lock file." + }, + "packages": { + "type": "array", + "description": "An array of packages that are required.", + "items": { + "$ref": "./composer-schema.json", + "required": ["name", "version"] + } + }, + "packages-dev": { + "type": "array", + "description": "An array of packages that are required in require-dev.", + "items": { + "$ref": "./composer-schema.json" + } + }, + "aliases": { + "type": "array", + "description": "Inline aliases defined in the root package.", + "items": { + "type": "object", + "required": [ "package", "version", "alias", "alias_normalized" ], + "properties": { + "package": { + "type": "string" + }, + "version": { + "type": "string" + }, + "alias": { + "type": "string" + }, + "alias_normalized": { + "type": "string" + } + } + } + }, + "minimum-stability": { + "type": "string", + "description": "The minimum-stability used to generate this lock file." + }, + "stability-flags": { + "type": "object", + "description": "Root package stability flags changing the minimum-stability for specific packages.", + "additionalProperties": { + "type": "integer" + } + }, + "prefer-stable": { + "type": "boolean", + "description": "Whether the --prefer-stable flag was used when building this lock file." + }, + "prefer-lowest": { + "type": "boolean", + "description": "Whether the --prefer-lowest flag was used when building this lock file." + }, + "platform": { + "type": "object", + "description": "Platform requirements of the root package.", + "additionalProperties": { + "type": "string" + } + }, + "platform-dev": { + "type": "object", + "description": "Platform dev-requirements of the root package.", + "additionalProperties": { + "type": "string" + } + }, + "platform-overrides": { + "type": "object", + "description": "Platform config overrides of the root package.", + "additionalProperties": { + "type": "string" + } + }, + "plugin-api-version": { + "type": "string", + "description": "The composer-plugin-api version that was used to generate this lock file." + } + } +} diff --git a/res/composer-repository-schema.json b/res/composer-repository-schema.json index adcc299d662a..223f63abf52a 100644 --- a/res/composer-repository-schema.json +++ b/res/composer-repository-schema.json @@ -1,11 +1,12 @@ { "$schema": "https://json-schema.org/draft-04/schema#", - "description": "A representation of packages metadata.", + "title": "Composer Package Repository", "type": "object", "oneOf": [ { "required": [ "packages" ] }, { "required": [ "providers" ] }, - { "required": [ "provider-includes", "providers-url" ] } + { "required": [ "provider-includes", "providers-url" ] }, + { "required": [ "metadata-url" ] } ], "properties": { "packages": { @@ -13,31 +14,124 @@ "description": "A hashmap of package names in the form of /.", "additionalProperties": { "$ref": "#/definitions/versions" } }, + "metadata-url": { + "type": "string", + "description": "Endpoint to retrieve package metadata data from, in Composer v2 format, e.g. '/p2/%package%.json'." + }, + "available-packages": { + "type": "array", + "items": { + "type": "string" + }, + "description": "If your repository only has a small number of packages, and you want to avoid serving many 404s, specify all the package names that your repository contains here." + }, + "available-package-patterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "If your repository only has a small number of packages, and you want to avoid serving many 404s, specify package name patterns containing wildcards (*) that your repository contains here." + }, + "security-advisories": { + "type": "array", + "items": { + "type": "object", + "required": ["metadata", "api-url"], + "properties": { + "metadata": { + "type": "boolean", + "description": "Whether metadata files contain security advisory data or whether it should always be queried using the API URL." + }, + "api-url": { + "type": "string", + "description": "Endpoint to call to retrieve security advisories data." + } + } + } + }, + "metadata-changes-url": { + "type": "string", + "description": "Endpoint to retrieve package metadata updates from. This should receive a timestamp since last call to be able to return new changes. e.g. '/metadata/changes.json'." + }, + "providers-api": { + "type": "string", + "description": "Endpoint to retrieve package names providing a given name from, e.g. '/providers/%package%.json'." + }, + "notify-batch": { + "type": "string", + "description": "Endpoint to call after multiple packages have been installed, e.g. '/downloads/'." + }, + "search": { + "type": "string", + "description": "Endpoint that provides search capabilities, e.g. '/search.json?q=%query%&type=%type%'." + }, + "list": { + "type": "string", + "description": "Endpoint that provides a full list of packages present in the repository. It should accept an optional `?filter=xx` query param, which can contain `*` as wildcards matching any substring. e.g. '/list.json'." + }, + "warnings": { + "type": "array", + "items": { + "type": "object", + "required": ["message", "versions"], + "properties": { + "message": { + "type": "string", + "description": "A message that will be output by Composer as a warning when this source is consulted." + }, + "versions": { + "type": "string", + "description": "A version constraint to limit to which Composer versions the warning should be shown." + } + } + } + }, + "infos": { + "type": "array", + "items": { + "type": "object", + "required": ["message", "versions"], + "properties": { + "message": { + "type": "string", + "description": "A message that will be output by Composer as info when this source is consulted." + }, + "versions": { + "type": "string", + "description": "A version constraint to limit to which Composer versions the info should be shown." + } + } + } + }, "providers-url": { "type": "string", - "description": "Endpoint to retrieve provider data from, e.g. '/p/%package%$%hash%.json'." + "description": "DEPRECATED: Endpoint to retrieve provider data from, e.g. '/p/%package%$%hash%.json'." }, "provider-includes": { "type": "object", - "description": "A hashmap of provider listings.", + "description": "DEPRECATED: A hashmap of provider listings.", "additionalProperties": { "$ref": "#/definitions/provider" } }, "providers": { "type": "object", - "description": "A hashmap of package names in the form of /.", + "description": "DEPRECATED: A hashmap of package names in the form of /.", "additionalProperties": { "$ref": "#/definitions/provider" } }, - "notify-batch": { + "warning": { "type": "string", - "description": "Endpoint to call after multiple packages have been installed, e.g. '/downloads/'." + "description": "DEPRECATED: A message that will be output by Composer as a warning when this source is consulted." }, - "search": { + "warning-versions": { "type": "string", - "description": "Endpoint that provides search capabilities, e.g. '/search.json?q=%query%&type=%type%'." + "description": "DEPRECATED: A version constraint to limit to which Composer versions the warning should be shown." }, - "warning": { + "info": { + "type": "string", + "description": "DEPRECATED: A message that will be output by Composer as a info when this source is consulted." + }, + "info-versions": { "type": "string", - "description": "A message that will be output by Composer as a warning when this source is consulted." + "description": "DEPRECATED: A version constraint to limit to which Composer versions the info should be shown." } }, "definitions": { diff --git a/res/composer-schema.json b/res/composer-schema.json index a77becb4b3e1..9df390e06e2b 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft-04/schema#", - "title": "Package", + "title": "Composer Package", "type": "object", "properties": { "name": { @@ -288,6 +288,70 @@ } } }, + "php-ext": { + "type": "object", + "description": "Settings for PHP extension packages.", + "properties": { + "extension-name": { + "type": "string", + "description": "If specified, this will be used as the name of the extension, where needed by tooling. If this is not specified, the extension name will be derived from the Composer package name (e.g. `vendor/name` would become `ext-name`). The extension name may be specified with or without the `ext-` prefix, and tools that use this must normalise this appropriately.", + "example": "ext-xdebug" + }, + "priority": { + "type": "integer", + "description": "This is used to add a prefix to the INI file, e.g. `90-xdebug.ini` which affects the loading order. The priority is a number in the range 10-99 inclusive, with 10 being the highest priority (i.e. will be processed first), and 99 being the lowest priority (i.e. will be processed last). There are two digits so that the files sort correctly on any platform, whether the sorting is natural or not.", + "minimum": 10, + "maximum": 99, + "example": 80, + "default": 80 + }, + "support-zts": { + "type": "boolean", + "description": "Does this package support Zend Thread Safety", + "example": false, + "default": true + }, + "support-nts": { + "type": "boolean", + "description": "Does this package support non-Thread Safe mode", + "example": false, + "default": true + }, + "build-path": { + "type": ["string", "null"], + "description": "If specified, this is the subdirectory that will be used to build the extension instead of the root of the project.", + "example": "my-extension-source", + "default": null + }, + "configure-options": { + "type": "array", + "description": "These configure options make up the flags that can be passed to ./configure when installing the extension.", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "The name of the flag, this would typically be prefixed with `--`, for example, the value 'the-flag' would be passed as `./configure --the-flag`.", + "example": "without-xdebug-compression", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]*$" + }, + "needs-value": { + "type": "boolean", + "description": "If this is set to true, the flag needs a value (e.g. --with-somelib=), otherwise it is a flag without a value (e.g. --enable-some-feature).", + "example": false, + "default": false + }, + "description": { + "type": "string", + "description": "The description of what the flag does or means.", + "example": "Disable compression through zlib" + } + } + } + } + } + }, "config": { "type": "object", "description": "Composer options.", @@ -610,6 +674,14 @@ "platform-check": { "type": ["boolean", "string"], "description": "Defaults to \"php-only\" which checks only the PHP version. Setting to true will also check the presence of required PHP extensions. If set to false, Composer will not create and require a platform_check.php file as part of the autoloader bootstrap." + }, + "bump-after-update": { + "type": ["string", "boolean"], + "description": "Defaults to false and can be any of true, false, \"dev\"` or \"no-dev\"`. If set to true, Composer will run the bump command after running the update command. If set to \"dev\" or \"no-dev\" then only the corresponding dependencies will be bumped." + }, + "allow-missing-requirements": { + "type": ["boolean"], + "description": "Defaults to false. If set to true, Composer will allow install when lock file is not up to date with the latest changes in composer.json." } } }, diff --git a/src/Composer/Advisory/Auditor.php b/src/Composer/Advisory/Auditor.php index f0dc76ae5ad3..485b3326787f 100644 --- a/src/Composer/Advisory/Auditor.php +++ b/src/Composer/Advisory/Auditor.php @@ -19,7 +19,6 @@ use Composer\Package\PackageInterface; use Composer\Repository\RepositorySet; use Composer\Util\PackageInfo; -use Composer\Util\Platform; use InvalidArgumentException; use Symfony\Component\Console\Formatter\OutputFormatter; @@ -47,17 +46,30 @@ class Auditor public const ABANDONED_REPORT = 'report'; public const ABANDONED_FAIL = 'fail'; + /** @internal */ + public const ABANDONEDS = [ + self::ABANDONED_IGNORE, + self::ABANDONED_REPORT, + self::ABANDONED_FAIL, + ]; + + /** Values to determine the audit result. */ + public const STATUS_OK = 0; + public const STATUS_VULNERABLE = 1; + public const STATUS_ABANDONED = 2; + /** * @param PackageInterface[] $packages * @param self::FORMAT_* $format The format that will be used to output audit results. * @param bool $warningOnly If true, outputs a warning. If false, outputs an error. * @param string[] $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities. * @param self::ABANDONED_* $abandoned + * @param array $ignoredSeverities List of ignored severity levels * - * @return int Amount of packages with vulnerabilities found + * @return int-mask A bitmask of STATUS_* constants or 0 on success * @throws InvalidArgumentException If no packages are passed in */ - public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoreList = [], string $abandoned = self::ABANDONED_FAIL): int + public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, string $format, bool $warningOnly = true, array $ignoreList = [], string $abandoned = self::ABANDONED_FAIL, array $ignoredSeverities = []): int { $allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, $format === self::FORMAT_SUMMARY); // we need the CVE & remote IDs set to filter ignores correctly so if we have any matches using the optimized codepath above @@ -65,10 +77,10 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, if (count($allAdvisories) > 0 && $ignoreList !== [] && $format === self::FORMAT_SUMMARY) { $allAdvisories = $repoSet->getMatchingSecurityAdvisories($packages, false); } - ['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList); + ['advisories' => $advisories, 'ignoredAdvisories' => $ignoredAdvisories] = $this->processAdvisories($allAdvisories, $ignoreList, $ignoredSeverities); $abandonedCount = 0; - $affectedPackagesCount = 0; + $affectedPackagesCount = count($advisories); if ($abandoned === self::ABANDONED_IGNORE) { $abandonedPackages = []; } else { @@ -78,35 +90,37 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, } } + $auditBitmask = $this->calculateBitmask(0 < $affectedPackagesCount, 0 < $abandonedCount); + if (self::FORMAT_JSON === $format) { $json = ['advisories' => $advisories]; if ($ignoredAdvisories !== []) { $json['ignored-advisories'] = $ignoredAdvisories; } - $json['abandoned'] = array_reduce($abandonedPackages, static function(array $carry, CompletePackageInterface $package): array { + $json['abandoned'] = array_reduce($abandonedPackages, static function (array $carry, CompletePackageInterface $package): array { $carry[$package->getPrettyName()] = $package->getReplacementPackage(); + return $carry; }, []); $io->write(JsonFile::encode($json)); - return count($advisories) + $abandonedCount; + return $auditBitmask; } $errorOrWarn = $warningOnly ? 'warning' : 'error'; - if (count($advisories) > 0 || count($ignoredAdvisories) > 0) { + if ($affectedPackagesCount > 0 || count($ignoredAdvisories) > 0) { $passes = [ [$ignoredAdvisories, "Found %d ignored security vulnerability advisor%s affecting %d package%s%s"], - // this has to run last to allow $affectedPackagesCount in the return statement to be correct [$advisories, "<$errorOrWarn>Found %d security vulnerability advisor%s affecting %d package%s%s"], ]; foreach ($passes as [$advisoriesToOutput, $message]) { - [$affectedPackagesCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput); - if ($affectedPackagesCount > 0) { + [$pkgCount, $totalAdvisoryCount] = $this->countAdvisories($advisoriesToOutput); + if ($pkgCount > 0) { $plurality = $totalAdvisoryCount === 1 ? 'y' : 'ies'; - $pkgPlurality = $affectedPackagesCount === 1 ? '' : 's'; + $pkgPlurality = $pkgCount === 1 ? '' : 's'; $punctuation = $format === 'summary' ? '.' : ':'; - $io->writeError(sprintf($message, $totalAdvisoryCount, $plurality, $affectedPackagesCount, $pkgPlurality, $punctuation)); + $io->writeError(sprintf($message, $totalAdvisoryCount, $plurality, $pkgCount, $pkgPlurality, $punctuation)); $this->outputAdvisories($io, $advisoriesToOutput, $format); } } @@ -122,7 +136,7 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, $this->outputAbandonedPackages($io, $abandonedPackages, $format); } - return $affectedPackagesCount + $abandonedCount; + return $auditBitmask; } /** @@ -131,7 +145,7 @@ public function audit(IOInterface $io, RepositorySet $repoSet, array $packages, */ private function filterAbandonedPackages(array $packages): array { - return array_filter($packages, static function (PackageInterface $pkg) { + return array_filter($packages, static function (PackageInterface $pkg): bool { return $pkg instanceof CompletePackageInterface && $pkg->isAbandoned(); }); } @@ -139,11 +153,12 @@ private function filterAbandonedPackages(array $packages): array /** * @phpstan-param array> $allAdvisories * @param array|array $ignoreList List of advisory IDs, remote IDs or CVE IDs that reported but not listed as vulnerabilities. + * @param array $ignoredSeverities List of ignored severity levels * @phpstan-return array{advisories: array>, ignoredAdvisories: array>} */ - private function processAdvisories(array $allAdvisories, array $ignoreList): array + private function processAdvisories(array $allAdvisories, array $ignoreList, array $ignoredSeverities): array { - if ($ignoreList === []) { + if ($ignoreList === [] && $ignoredSeverities === []) { return ['advisories' => $allAdvisories, 'ignoredAdvisories' => []]; } @@ -167,6 +182,11 @@ private function processAdvisories(array $allAdvisories, array $ignoreList): arr } if ($advisory instanceof SecurityAdvisory) { + if (in_array($advisory->severity, $ignoredSeverities, true)) { + $isActive = false; + $ignoreReason = "Ignored via --ignore-severity={$advisory->severity}"; + } + if (in_array($advisory->cve, $ignoredIds, true)) { $isActive = false; $ignoreReason = $ignoreList[$advisory->cve] ?? null; @@ -264,6 +284,10 @@ private function outputAdvisoriesTable(ConsoleIO $io, array $advisories): void $advisory->affectedVersions->getPrettyString(), $advisory->reportedAt->format(DATE_ATOM), ]; + if ($advisory->cve === null) { + $headers[] = 'Advisory ID'; + $row[] = $advisory->advisoryId; + } if ($advisory instanceof IgnoredSecurityAdvisory) { $headers[] = 'Ignore reason'; $row[] = $advisory->ignoreReason ?? 'None specified'; @@ -294,6 +318,9 @@ private function outputAdvisoriesPlain(IOInterface $io, array $advisories): void $error[] = "Package: ".$advisory->packageName; $error[] = "Severity: ".$this->getSeverity($advisory); $error[] = "CVE: ".$this->getCVE($advisory); + if ($advisory->cve === null) { + $error[] = "Advisory ID: ".$advisory->advisoryId; + } $error[] = "Title: ".OutputFormatter::escape($advisory->title); $error[] = "URL: ".$this->getURL($advisory); $error[] = "Affected versions: ".OutputFormatter::escape($advisory->affectedVersions->getPrettyString()); @@ -381,4 +408,21 @@ private function getURL(SecurityAdvisory $advisory): string return 'link).'>'.OutputFormatter::escape($advisory->link).''; } + /** + * @return int-mask + */ + private function calculateBitmask(bool $hasVulnerablePackages, bool $hasAbandonedPackages): int + { + $bitmask = self::STATUS_OK; + + if ($hasVulnerablePackages) { + $bitmask |= self::STATUS_VULNERABLE; + } + + if ($hasAbandonedPackages) { + $bitmask |= self::STATUS_ABANDONED; + } + + return $bitmask; + } } diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 844b802073e5..6db30b8fdf4a 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -34,6 +34,7 @@ use Composer\Util\PackageSorter; use Composer\Json\JsonFile; use Composer\Package\Locker; +use Symfony\Component\Console\Formatter\OutputFormatter; /** * @author Igor Wiedler @@ -173,7 +174,7 @@ public function setPlatformRequirementFilter(PlatformRequirementFilterInterface * @throws \Seld\JsonLint\ParsingException * @throws \RuntimeException */ - public function dump(Config $config, InstalledRepositoryInterface $localRepo, RootPackageInterface $rootPackage, InstallationManager $installationManager, string $targetDir, bool $scanPsrPackages = false, ?string $suffix = null, ?Locker $locker = null) + public function dump(Config $config, InstalledRepositoryInterface $localRepo, RootPackageInterface $rootPackage, InstallationManager $installationManager, string $targetDir, bool $scanPsrPackages = false, ?string $suffix = null, ?Locker $locker = null, bool $strictAmbiguous = false) { if ($this->classMapAuthoritative) { // Force scanPsrPackages when classmap is authoritative @@ -319,7 +320,7 @@ public static function autoload(\$class) EOF; } - $excluded = null; + $excluded = []; if (!empty($autoloads['exclude-from-classmap'])) { $excluded = $autoloads['exclude-from-classmap']; } @@ -348,14 +349,26 @@ public static function autoload(\$class) continue; } - $classMapGenerator->scanPaths($dir, $this->buildExclusionRegex($dir, $excluded), $group['type'], $namespace); + // if the vendor dir is contained within a psr-0/psr-4 dir being scanned we exclude it + if (str_contains($vendorPath, $dir.'/')) { + $exclusionRegex = $this->buildExclusionRegex($dir, array_merge($excluded, [$vendorPath.'/'])); + } else { + $exclusionRegex = $this->buildExclusionRegex($dir, $excluded); + } + + $classMapGenerator->scanPaths($dir, $exclusionRegex, $group['type'], $namespace); } } } } $classMap = $classMapGenerator->getClassMap(); - foreach ($classMap->getAmbiguousClasses() as $className => $ambiguousPaths) { + if ($strictAmbiguous) { + $ambiguousClasses = $classMap->getAmbiguousClasses(false); + } else { + $ambiguousClasses = $classMap->getAmbiguousClasses(); + } + foreach ($ambiguousClasses as $className => $ambiguousPaths) { if (count($ambiguousPaths) > 1) { $this->io->writeError( 'Warning: Ambiguous class resolution, "'.$className.'"'. @@ -368,6 +381,12 @@ public static function autoload(\$class) ); } } + if (\count($ambiguousClasses) > 0) { + $this->io->writeError('To resolve ambiguity in classes not under your control you can ignore them by path using exclude-files-from-classmap'); + } + + // output PSR violations which are not coming from the vendor dir + $classMap->clearPsrViolationsByPath($vendorPath); foreach ($classMap->getPsrViolations() as $msg) { $this->io->writeError("$msg"); } @@ -400,14 +419,14 @@ public static function autoload(\$class) // carry over existing autoload.php's suffix if possible and none is configured if (null === $suffix && Filesystem::isReadable($vendorPath.'/autoload.php')) { - $content = file_get_contents($vendorPath.'/autoload.php'); + $content = (string) file_get_contents($vendorPath.'/autoload.php'); if (Preg::isMatch('{ComposerAutoloaderInit([^:\s]+)::}', $content, $match)) { $suffix = $match[1]; } } if (null === $suffix) { - $suffix = $locker !== null && $locker->isLocked() ? $locker->getLockData()['content-hash'] : md5(uniqid('', true)); + $suffix = $locker !== null && $locker->isLocked() ? $locker->getLockData()['content-hash'] : bin2hex(random_bytes(16)); } } @@ -460,12 +479,12 @@ public static function autoload(\$class) } /** - * @param array|null $excluded + * @param array $excluded * @return non-empty-string|null */ - private function buildExclusionRegex(string $dir, ?array $excluded): ?string + private function buildExclusionRegex(string $dir, array $excluded): ?string { - if (null === $excluded) { + if ([] === $excluded) { return null; } @@ -602,7 +621,7 @@ public function createLoader(array $autoloads, ?string $vendorDir = null) } if (isset($autoloads['classmap'])) { - $excluded = null; + $excluded = []; if (!empty($autoloads['exclude-from-classmap'])) { $excluded = $autoloads['exclude-from-classmap']; } @@ -646,11 +665,11 @@ protected function getIncludePathsFile(array $packageMap, Filesystem $filesystem foreach ($package->getIncludePaths() as $includePath) { $includePath = trim($includePath, '/'); - $includePaths[] = empty($installPath) ? $includePath : $installPath.'/'.$includePath; + $includePaths[] = $installPath === '' ? $includePath : $installPath.'/'.$includePath; } } - if (!$includePaths) { + if (\count($includePaths) === 0) { return null; } @@ -1042,7 +1061,7 @@ public static function getLoader() } if ($this->apcu) { - $apcuPrefix = var_export(($this->apcuPrefix !== null ? $this->apcuPrefix : substr(base64_encode(md5(uniqid('', true), true)), 0, -3)), true); + $apcuPrefix = var_export(($this->apcuPrefix !== null ? $this->apcuPrefix : bin2hex(random_bytes(10))), true); $file .= <<setApcuPrefix($apcuPrefix); @@ -1231,6 +1250,10 @@ protected function parseAutoloadsType(array $packageMap, string $type, RootPacka } foreach ($autoload[$type] as $namespace => $paths) { + if (in_array($type, ['psr-4', 'psr-0'], true)) { + // normalize namespaces to ensure "\" becomes "" and others do not have leading separators as they are not needed + $namespace = ltrim($namespace, '\\'); + } foreach ((array) $paths as $path) { if (($type === 'files' || $type === 'classmap' || $type === 'exclude-from-classmap') && $package->getTargetDir() && !Filesystem::isReadable($installPath.'/'.$path)) { // remove target-dir from file paths of the root package @@ -1255,10 +1278,8 @@ protected function parseAutoloadsType(array $packageMap, string $type, RootPacka $path = Preg::replaceCallback( '{^((?:(?:\\\\\\.){1,2}+/)+)}', static function ($matches) use (&$updir): string { - if (isset($matches[1])) { - // undo preg_quote for the matched string - $updir = str_replace('\\.', '.', $matches[1]); - } + // undo preg_quote for the matched string + $updir = str_replace('\\.', '.', $matches[1]); return ''; }, @@ -1300,7 +1321,8 @@ static function ($matches) use (&$updir): string { */ protected function getFileIdentifier(PackageInterface $package, string $path) { - return md5($package->getName() . ':' . $path); + // TODO composer v3 change this to sha1 or xxh3? Possibly not worth the potential breakage though + return hash('md5', $package->getName() . ':' . $path); } /** diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index 1a216c59231f..e18715f478b1 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -53,7 +53,7 @@ public function __construct(IOInterface $io, string $cacheDir, string $allowlist $this->root = rtrim($cacheDir, '/\\') . '/'; $this->allowlist = $allowlist; $this->filesystem = $filesystem ?: new Filesystem(); - $this->readOnly = (bool) $readOnly; + $this->readOnly = $readOnly; if (!self::isUsable($cacheDir)) { $this->enabled = false; @@ -65,7 +65,7 @@ public function __construct(IOInterface $io, string $cacheDir, string $allowlist */ public function setReadOnly(bool $readOnly) { - $this->readOnly = (bool) $readOnly; + $this->readOnly = $readOnly; } /** @@ -144,7 +144,7 @@ public function write(string $file, string $contents) $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); - $tempFileName = $this->root . $file . uniqid('.', true) . '.tmp'; + $tempFileName = $this->root . $file . bin2hex(random_bytes(5)) . '.tmp'; try { return file_put_contents($tempFileName, $contents) !== false && rename($tempFileName, $this->root . $file); } catch (\ErrorException $e) { @@ -198,7 +198,7 @@ public function copyFrom(string $file, string $source) $this->io->writeError('Writing '.$this->root . $file.' into cache from '.$source); } - return copy($source, $this->root . $file); + return $this->filesystem->copy($source, $this->root . $file); } return false; @@ -224,7 +224,7 @@ public function copyTo(string $file, string $target) $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); - return copy($this->root . $file, $target); + return $this->filesystem->copy($this->root . $file, $target); } } @@ -357,7 +357,7 @@ public function sha1(string $file) if ($this->isEnabled()) { $file = Preg::replace('{[^'.$this->allowlist.']}i', '-', $file); if (file_exists($this->root . $file)) { - return sha1_file($this->root . $file); + return hash_file('sha1', $this->root . $file); } } diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index e0189109b652..b71f4e241fad 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -169,7 +169,7 @@ protected function selectPackage(IOInterface $io, string $packageName, ?string $ } if ($version !== null && Preg::isMatchStrictGroups('{@(stable|RC|beta|alpha|dev)$}i', $version, $match)) { - $minStability = $match[1]; + $minStability = VersionParser::normalizeStability($match[1]); $version = (string) substr($version, 0, -strlen($match[0])); } diff --git a/src/Composer/Command/AuditCommand.php b/src/Composer/Command/AuditCommand.php index 1097bb7af3f8..e4a2094b8b50 100644 --- a/src/Composer/Command/AuditCommand.php +++ b/src/Composer/Command/AuditCommand.php @@ -33,6 +33,8 @@ protected function configure(): void new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables auditing of require-dev packages.'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format. Must be "table", "plain", "json", or "summary".', Auditor::FORMAT_TABLE, Auditor::FORMATS), new InputOption('locked', null, InputOption::VALUE_NONE, 'Audit based on the lock file instead of the installed packages.'), + new InputOption('abandoned', null, InputOption::VALUE_REQUIRED, 'Behavior on abandoned packages. Must be "ignore", "report", or "fail".', null, Auditor::ABANDONEDS), + new InputOption('ignore-severity', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Ignore advisories of a certain severity level.', [], ['low', 'medium', 'high', 'critical']), ]) ->setHelp( <<getConfig()->get('audit'); - return min(255, $auditor->audit($this->getIO(), $repoSet, $packages, $this->getAuditFormat($input, 'format'), false, $auditConfig['ignore'] ?? [], $auditConfig['abandoned'] ?? Auditor::ABANDONED_FAIL)); + $abandoned = $input->getOption('abandoned'); + if ($abandoned !== null && !in_array($abandoned, Auditor::ABANDONEDS, true)) { + throw new \InvalidArgumentException('--audit must be one of '.implode(', ', Auditor::ABANDONEDS).'.'); + } + + $abandoned = $abandoned ?? $auditConfig['abandoned'] ?? Auditor::ABANDONED_FAIL; + + $ignoreSeverities = $input->getOption('ignore-severity') ?? []; + + return min(255, $auditor->audit( + $this->getIO(), + $repoSet, + $packages, + $this->getAuditFormat($input, 'format'), + false, + $auditConfig['ignore'] ?? [], + $abandoned, + $ignoreSeverities + )); + } /** diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index b6b1439de5af..bdfbb0d05996 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -289,6 +289,29 @@ protected function initialize(InputInterface $input, OutputInterface $output) parent::initialize($input, $output); } + /** + * Calls {@see Factory::create()} with the given arguments, taking into account flags and default states for disabling scripts and plugins + * + * @param mixed $config either a configuration array or a filename to read from, if null it will read from + * the default filename + * @return Composer + */ + protected function createComposerInstance(InputInterface $input, IOInterface $io, $config = null, ?bool $disablePlugins = null, ?bool $disableScripts = null): Composer + { + $disablePlugins = $disablePlugins === true || $input->hasParameterOption('--no-plugins'); + $disableScripts = $disableScripts === true || $input->hasParameterOption('--no-scripts'); + + $application = parent::getApplication(); + if ($application instanceof Application && $application->getDisablePluginsByDefault()) { + $disablePlugins = true; + } + if ($application instanceof Application && $application->getDisableScriptsByDefault()) { + $disableScripts = true; + } + + return Factory::create($io, $config, $disablePlugins, $disableScripts); + } + /** * Returns preferSource and preferDist values based on the configuration. * diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 2fb363979c10..55b502e03cc8 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -13,6 +13,7 @@ namespace Composer\Command; use Composer\Package\Link; +use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\RootPackage; @@ -24,6 +25,8 @@ use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Composer\Semver\Constraint\Bound; +use Composer\Util\Platform; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Package\Version\VersionParser; @@ -102,13 +105,27 @@ protected function doExecute(InputInterface $input, OutputInterface $output, boo // If the version we ask for is not installed then we need to locate it in remote repos and add it. // This is needed for why-not to resolve conflicts from an uninstalled version against installed packages. - if (!$installedRepo->findPackage($needle, $textConstraint)) { + $matchedPackage = $installedRepo->findPackage($needle, $textConstraint); + if (!$matchedPackage) { $defaultRepos = new CompositeRepository(RepositoryFactory::defaultRepos($this->getIO(), $composer->getConfig(), $composer->getRepositoryManager())); if ($match = $defaultRepos->findPackage($needle, $textConstraint)) { $installedRepo->addRepository(new InstalledArrayRepository([clone $match])); + } elseif (PlatformRepository::isPlatformPackage($needle)) { + $parser = new VersionParser(); + $constraint = $parser->parseConstraints($textConstraint); + if ($constraint->getLowerBound() !== Bound::zero()) { + $tempPlatformPkg = new Package($needle, $constraint->getLowerBound()->getVersion(), $constraint->getLowerBound()->getVersion()); + $installedRepo->addRepository(new InstalledArrayRepository([$tempPlatformPkg])); + } } else { $this->getIO()->writeError('Package "'.$needle.'" could not be found with constraint "'.$textConstraint.'", results below will most likely be incomplete.'); } + } elseif (PlatformRepository::isPlatformPackage($needle)) { + $extraNotice = ''; + if (($matchedPackage->getExtra()['config.platform'] ?? false) === true) { + $extraNotice = ' (version provided by config.platform)'; + } + $this->getIO()->writeError('Package "'.$needle.' '.$textConstraint.'" found in version "'.$matchedPackage->getPrettyVersion().'"'.$extraNotice.'.'); } // Include replaced packages for inverted lookups as they are then the actual starting point to consider @@ -154,7 +171,7 @@ protected function doExecute(InputInterface $input, OutputInterface $output, boo $this->printTable($output, $results); } - if ($inverted && $input->hasArgument(self::ARGUMENT_CONSTRAINT)) { + if ($inverted && $input->hasArgument(self::ARGUMENT_CONSTRAINT) && !PlatformRepository::isPlatformPackage($needle)) { $composerCommand = 'update'; foreach ($composer->getPackage()->getRequires() as $rootRequirement) { @@ -180,9 +197,9 @@ protected function doExecute(InputInterface $input, OutputInterface $output, boo /** * Assembles and prints a bottom-up table of the dependencies. * - * @param array{PackageInterface, Link, mixed}[] $results + * @param array{PackageInterface, Link, array|false}[] $results */ - protected function printTable(OutputInterface $output, $results): void + protected function printTable(OutputInterface $output, array $results): void { $table = []; $doubles = []; @@ -204,13 +221,13 @@ protected function printTable(OutputInterface $output, $results): void $packageUrl = PackageInfo::getViewSourceOrHomepageUrl($package); $nameWithLink = $packageUrl !== null ? '' . $package->getPrettyName() . '' : $package->getPrettyName(); $rows[] = [$nameWithLink, $version, $link->getDescription(), sprintf('%s (%s)', $link->getTarget(), $link->getPrettyConstraint())]; - if ($children) { + if (is_array($children)) { $queue = array_merge($queue, $children); } } $results = $queue; $table = array_merge($rows, $table); - } while (!empty($results)); + } while (\count($results) > 0); $this->renderTable($table, $output); } @@ -237,7 +254,7 @@ protected function initStyles(OutputInterface $output): void /** * Recursively prints a tree of the selected results. * - * @param array{PackageInterface, Link, mixed[]|bool}[] $results Results to be printed at this level. + * @param array{PackageInterface, Link, array|false}[] $results Results to be printed at this level. * @param string $prefix Prefix of the current tree level. * @param int $level Current level of recursion. */ @@ -258,7 +275,7 @@ protected function printTree(array $results, string $prefix = '', int $level = 1 $linkText = sprintf('%s <%s>%s %s', $link->getDescription(), $prevColor, $link->getTarget(), $link->getPrettyConstraint()); $circularWarn = $children === false ? '(circular dependency aborted here)' : ''; $this->writeTreeLine(rtrim(sprintf("%s%s%s (%s) %s", $prefix, $isLast ? '└──' : '├──', $packageText, $linkText, $circularWarn))); - if ($children) { + if (is_array($children)) { $this->printTree($children, $prefix . ($isLast ? ' ' : '│ '), $level + 1); } } diff --git a/src/Composer/Command/BumpCommand.php b/src/Composer/Command/BumpCommand.php index db5b9464a0dc..a87a65e1c8e7 100644 --- a/src/Composer/Command/BumpCommand.php +++ b/src/Composer/Command/BumpCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\Locker; @@ -72,9 +73,28 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { + return $this->doBump( + $this->getIO(), + $input->getOption('dev-only'), + $input->getOption('no-dev-only'), + $input->getOption('dry-run'), + $input->getArgument('packages') + ); + } + + /** + * @param string[] $packagesFilter + * @throws \Seld\JsonLint\ParsingException + */ + public function doBump( + IOInterface $io, + bool $devOnly, + bool $noDevOnly, + bool $dryRun, + array $packagesFilter + ): int { /** @readonly */ $composerJsonPath = Factory::getComposerFile(); - $io = $this->getIO(); if (!Filesystem::isReadable($composerJsonPath)) { $io->writeError(''.$composerJsonPath.' is not readable.'); @@ -112,7 +132,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $repo = $composer->getRepositoryManager()->getLocalRepository(); } - if ($composer->getPackage()->getType() !== 'project' && !$input->getOption('dev-only')) { + if ($composer->getPackage()->getType() !== 'project' && !$devOnly) { $io->writeError('Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users.'); $contents = $composerJson->read(); @@ -125,14 +145,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $bumper = new VersionBumper(); $tasks = []; - if (!$input->getOption('dev-only')) { + if (!$devOnly) { $tasks['require'] = $composer->getPackage()->getRequires(); } - if (!$input->getOption('no-dev-only')) { + if (!$noDevOnly) { $tasks['require-dev'] = $composer->getPackage()->getDevRequires(); } - $packagesFilter = $input->getArgument('packages'); if (count($packagesFilter) > 0) { $pattern = BasePackage::packageNamesToRegexp(array_unique(array_map('strtolower', $packagesFilter))); foreach ($tasks as $key => $reqs) { @@ -171,8 +190,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - $dryRun = $input->getOption('dry-run'); - if (!$dryRun && !$this->updateFileCleanly($composerJson, $updates)) { $composerDefinition = $composerJson->read(); foreach ($updates as $key => $packages) { @@ -199,15 +216,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->write('No requirements to update in '.$composerJsonPath.'.'); } - if (!$dryRun && $composer->getLocker()->isLocked() && $changeCount > 0) { - $contents = file_get_contents($composerJson->getPath()); - if (false === $contents) { - throw new \RuntimeException('Unable to read '.$composerJson->getPath().' contents to update the lock file hash.'); - } - $lock = new JsonFile(Factory::getLockFile($composerJsonPath)); - $lockData = $lock->read(); - $lockData['content-hash'] = Locker::getContentHash($contents); - $lock->write($lockData); + if (!$dryRun && $composer->getLocker()->isLocked() && $composer->getConfig()->get('lock') && $changeCount > 0) { + $composer->getLocker()->updateHash($composerJson); } if ($dryRun && $changeCount > 0) { diff --git a/src/Composer/Command/ClearCacheCommand.php b/src/Composer/Command/ClearCacheCommand.php index 028aa4212639..77ed517a801c 100644 --- a/src/Composer/Command/ClearCacheCommand.php +++ b/src/Composer/Command/ClearCacheCommand.php @@ -45,7 +45,13 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $config = Factory::createConfig(); + $composer = $this->tryComposer(); + if ($composer !== null) { + $config = $composer->getConfig(); + } else { + $config = Factory::createConfig(); + } + $io = $this->getIO(); $cachePaths = [ diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php index 89c6b68e55b0..444d69554f77 100644 --- a/src/Composer/Command/CompletionTrait.php +++ b/src/Composer/Command/CompletionTrait.php @@ -117,6 +117,36 @@ private function suggestInstalledPackage(bool $includeRootPackage = true, bool $ }; } + /** + * Suggest package names from installed. + */ + private function suggestInstalledPackageTypes(bool $includeRootPackage = true): \Closure + { + return function (CompletionInput $input) use ($includeRootPackage): array { + $composer = $this->requireComposer(); + $installedRepos = []; + + if ($includeRootPackage) { + $installedRepos[] = new RootPackageRepository(clone $composer->getPackage()); + } + + $locker = $composer->getLocker(); + if ($locker->isLocked()) { + $installedRepos[] = $locker->getLockedRepository(true); + } else { + $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); + } + + $installedRepo = new InstalledRepository($installedRepos); + + return array_values(array_unique( + array_map(static function (PackageInterface $package) { + return $package->getType(); + }, $installedRepo->getPackages()) + )); + }; + } + /** * Suggest package names available on all configured repositories. */ diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index cbdc174bce86..de3bd367ef77 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -291,7 +291,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $source = $this->config->getSourceOfValue($settingKey); if (Preg::isMatch('/^repos?(?:itories)?(?:\.(.+))?/', $settingKey, $matches)) { - if (!isset($matches[1]) || $matches[1] === '') { + if (!isset($matches[1])) { $value = $data['repositories'] ?? []; } else { if (!isset($data['repositories'][$matches[1]])) { @@ -469,6 +469,18 @@ static function ($val) { 'prepend-autoloader' => [$booleanValidator, $booleanNormalizer], 'disable-tls' => [$booleanValidator, $booleanNormalizer], 'secure-http' => [$booleanValidator, $booleanNormalizer], + 'bump-after-update' => [ + static function ($val): bool { + return in_array($val, ['dev', 'no-dev', 'true', 'false', '1', '0'], true); + }, + static function ($val) { + if ('dev' === $val || 'no-dev' === $val) { + return $val; + } + + return $val !== 'false' && (bool) $val; + }, + ], 'cafile' => [ static function ($val): bool { return file_exists($val) && Filesystem::isReadable($val); @@ -664,7 +676,7 @@ static function ($vals) { }], 'minimum-stability' => [ static function ($val): bool { - return isset(BasePackage::$stabilities[VersionParser::normalizeStability($val)]); + return isset(BasePackage::STABILITIES[VersionParser::normalizeStability($val)]); }, static function ($val): string { return VersionParser::normalizeStability($val); @@ -771,8 +783,12 @@ static function ($vals) { foreach ($bits as $bit) { $currentValue = $currentValue[$bit] ?? null; } - if (is_array($currentValue)) { - $value = array_merge($currentValue, $value); + if (is_array($currentValue) && is_array($value)) { + if (array_is_list($currentValue) && array_is_list($value)) { + $value = array_merge($currentValue, $value); + } else { + $value = $value + $currentValue; + } } } } @@ -1009,7 +1025,7 @@ private function getAuthConfigFile(InputInterface $input, Config $config): strin } /** - * Suggest setting-keys, while taking given options in acount. + * Suggest setting-keys, while taking given options in account. */ private function suggestSettingKeys(): \Closure { diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 1a9cbf4bdf2e..93354458ffc2 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -193,7 +193,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ $this->suggestedPackagesReporter = new SuggestedPackagesReporter($io); if ($packageName !== null) { - $installedFromVcs = $this->installRootPackage($io, $config, $packageName, $platformRequirementFilter, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositories, $disablePlugins, $disableScripts, $noProgress, $secureHttp); + $installedFromVcs = $this->installRootPackage($input, $io, $config, $packageName, $platformRequirementFilter, $directory, $packageVersion, $stability, $preferSource, $preferDist, $installDevPackages, $repositories, $disablePlugins, $disableScripts, $noProgress, $secureHttp); } else { $installedFromVcs = false; } @@ -202,7 +202,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ unlink('composer.lock'); } - $composer = Factory::create($io, null, $disablePlugins, $disableScripts); + $composer = $this->createComposerInstance($input, $io, null, $disablePlugins, $disableScripts); // add the repository to the composer.json and use it for the install run later if ($repositories !== null && $addRepository) { @@ -221,7 +221,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ $configSource->addRepository($name, $repoConfig, false); } - $composer = Factory::create($io, null, $disablePlugins); + $composer = $this->createComposerInstance($input, $io, null, $disablePlugins); } } @@ -319,14 +319,6 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ $composer->getEventDispatcher()->dispatchScript(ScriptEvents::POST_CREATE_PROJECT_CMD, $installDevPackages); chdir($oldCwd); - $vendorComposerDir = $config->get('vendor-dir').'/composer'; - if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) { - Silencer::call('rmdir', $vendorComposerDir); - $vendorDir = $config->get('vendor-dir'); - if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) { - Silencer::call('rmdir', $vendorDir); - } - } return 0; } @@ -336,12 +328,8 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ * * @throws \Exception */ - protected function installRootPackage(IOInterface $io, Config $config, string $packageName, PlatformRequirementFilterInterface $platformRequirementFilter, ?string $directory = null, ?string $packageVersion = null, ?string $stability = 'stable', bool $preferSource = false, bool $preferDist = false, bool $installDevPackages = false, ?array $repositories = null, bool $disablePlugins = false, bool $disableScripts = false, bool $noProgress = false, bool $secureHttp = true): bool + protected function installRootPackage(InputInterface $input, IOInterface $io, Config $config, string $packageName, PlatformRequirementFilterInterface $platformRequirementFilter, ?string $directory = null, ?string $packageVersion = null, ?string $stability = 'stable', bool $preferSource = false, bool $preferDist = false, bool $installDevPackages = false, ?array $repositories = null, bool $disablePlugins = false, bool $disableScripts = false, bool $noProgress = false, bool $secureHttp = true): bool { - if (!$secureHttp) { - $config->merge(['config' => ['secure-http' => false]], Config::SOURCE_COMMAND); - } - $parser = new VersionParser(); $requirements = $parser->parseNameVersionPairs([$packageName]); $name = strtolower($requirements[0]['name']); @@ -354,12 +342,22 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p $parts = explode("/", $name, 2); $directory = Platform::getCwd() . DIRECTORY_SEPARATOR . array_pop($parts); } + $directory = rtrim($directory, '/\\'); $process = new ProcessExecutor($io); $fs = new Filesystem($process); if (!$fs->isAbsolutePath($directory)) { $directory = Platform::getCwd() . DIRECTORY_SEPARATOR . $directory; } + if ('' === $directory) { + throw new \UnexpectedValueException('Got an empty target directory, something went wrong'); + } + + // set the base dir to ensure $config->all() below resolves the correct absolute paths to vendor-dir etc + $config->setBaseDir($directory); + if (!$secureHttp) { + $config->merge(['config' => ['secure-http' => false]], Config::SOURCE_COMMAND); + } $io->writeError('Creating a "' . $packageName . '" project at "' . $fs->findShortestPath(Platform::getCwd(), $directory, true) . '"'); @@ -375,7 +373,7 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p if (null === $stability) { if (null === $packageVersion) { $stability = 'stable'; - } elseif (Preg::isMatchStrictGroups('{^[^,\s]*?@('.implode('|', array_keys(BasePackage::$stabilities)).')$}i', $packageVersion, $match)) { + } elseif (Preg::isMatchStrictGroups('{^[^,\s]*?@('.implode('|', array_keys(BasePackage::STABILITIES)).')$}i', $packageVersion, $match)) { $stability = $match[1]; } else { $stability = VersionParser::parseStability($packageVersion); @@ -384,12 +382,14 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p $stability = VersionParser::normalizeStability($stability); - if (!isset(BasePackage::$stabilities[$stability])) { - throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); + if (!isset(BasePackage::STABILITIES[$stability])) { + throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::STABILITIES))); } - $composer = Factory::create($io, $config->all(), $disablePlugins, $disableScripts); + $composer = $this->createComposerInstance($input, $io, $config->all(), $disablePlugins, $disableScripts); $config = $composer->getConfig(); + // set the base dir here again on the new config instance, as otherwise in case the vendor dir is defined in an env var for example it would still override the value set above by $config->all() + $config->setBaseDir($directory); $rm = $composer->getRepositoryManager(); $repositorySet = new RepositorySet($stability); diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index fc594f7d94f7..7dade55ef4da 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -19,6 +19,8 @@ use Composer\Downloader\TransportException; use Composer\IO\BufferIO; use Composer\Json\JsonFile; +use Composer\Json\JsonValidationException; +use Composer\Package\Locker; use Composer\Package\RootPackage; use Composer\Package\Version\VersionParser; use Composer\Pcre\Preg; @@ -44,6 +46,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\ExecutableFinder; +use Composer\Util\Http\ProxyManager; +use Composer\Util\Http\RequestProxy; /** * @author Jordi Boggiano @@ -87,6 +91,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->write('Checking composer.json: ', false); $this->outputResult($this->checkComposerSchema()); + + if ($composer->getLocker()->isLocked()) { + $io->write('Checking composer.lock: ', false); + $this->outputResult($this->checkComposerLockSchema($composer->getLocker())); + } + $this->process = $composer->getLoop()->getProcessExecutor() ?? new ProcessExecutor($io); } else { $this->process = new ProcessExecutor($io); @@ -115,10 +125,40 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->write('Checking https connectivity to packagist: ', false); $this->outputResult($this->checkHttp('https', $config)); - $opts = stream_context_get_options(StreamContextFactory::getContext('http://example.org')); - if (!empty($opts['http']['proxy'])) { + foreach ($config->getRepositories() as $repo) { + if (($repo['type'] ?? null) === 'composer' && isset($repo['url'])) { + $composerRepo = new ComposerRepository($repo, $this->getIO(), $config, $this->httpDownloader); + $reflMethod = new \ReflectionMethod($composerRepo, 'getPackagesJsonUrl'); + if (PHP_VERSION_ID < 80100) { + $reflMethod->setAccessible(true); + } + $url = $reflMethod->invoke($composerRepo); + if (!str_starts_with($url, 'http')) { + continue; + } + if (str_starts_with($url, 'https://repo.packagist.org')) { + continue; + } + $io->write('Checking connectivity to ' . $repo['url'].': ', false); + $this->outputResult($this->checkComposerRepo($url, $config)); + } + } + + $proxyManager = ProxyManager::getInstance(); + $protos = $config->get('disable-tls') === true ? ['http'] : ['http', 'https']; + try { + foreach ($protos as $proto) { + $proxy = $proxyManager->getProxyForRequest($proto.'://repo.packagist.org'); + if ($proxy->getStatus() !== '') { + $type = $proxy->isSecure() ? 'HTTPS' : 'HTTP'; + $io->write('Checking '.$type.' proxy with '.$proto.': ', false); + $this->outputResult($this->checkHttpProxy($proxy, $proto)); + } + } + } catch (TransportException $e) { $io->write('Checking HTTP proxy: ', false); - $this->outputResult($this->checkHttpProxy()); + $status = $this->checkConnectivityAndComposerNetworkHttpEnablement(); + $this->outputResult(is_string($status) ? $status : $e); } if (count($oauth = $config->get('github-oauth')) > 0) { @@ -135,7 +175,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } elseif (10 > $rate['remaining']) { $io->write('WARNING'); $io->write(sprintf( - 'Github has a rate limit on their API. ' + 'GitHub has a rate limit on their API. ' . 'You currently have %u ' . 'out of %u requests left.' . PHP_EOL . 'See https://developer.github.com/v3/#rate-limiting and also' . PHP_EOL @@ -186,7 +226,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $io->write('OpenSSL version: ' . (defined('OPENSSL_VERSION_TEXT') ? ''.OPENSSL_VERSION_TEXT.'' : 'missing')); - $io->write('cURL version: ' . $this->getCurlVersion()); + $io->write('curl version: ' . $this->getCurlVersion()); $finder = new ExecutableFinder; $hasSystemUnzip = (bool) $finder->find('unzip'); @@ -235,13 +275,34 @@ private function checkComposerSchema() return true; } + /** + * @return string|true + */ + private function checkComposerLockSchema(Locker $locker) + { + $json = $locker->getJsonFile(); + + try { + $json->validateSchema(JsonFile::LOCK_SCHEMA); + } catch (JsonValidationException $e) { + $output = ''; + foreach ($e->getErrors() as $error) { + $output .= ''.$error.''.PHP_EOL; + } + + return trim($output); + } + + return true; + } + private function checkGit(): string { if (!function_exists('proc_open')) { return 'proc_open is not available, git cannot be used'; } - $this->process->execute('git config color.ui', $output); + $this->process->execute(['git', 'config', 'color.ui'], $output); if (strtolower(trim($output)) === 'always') { return 'Your git color.ui setting is set to always, this is known to create issues. Use "git config --global color.ui true" to set it correctly.'; } @@ -298,31 +359,77 @@ private function checkHttp(string $proto, Config $config) } /** - * @return string|true|\Exception + * @return string|string[]|true + */ + private function checkComposerRepo(string $url, Config $config) + { + $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); + if ($result !== true) { + return $result; + } + + $result = []; + if (str_starts_with($url, 'https://') && $config->get('disable-tls') === true) { + $tlsWarning = 'Composer is configured to disable SSL/TLS protection. This will leave remote HTTPS requests vulnerable to Man-In-The-Middle attacks.'; + } + + try { + $this->httpDownloader->get($url); + } catch (TransportException $e) { + $hints = HttpDownloader::getExceptionHints($e); + if (null !== $hints && count($hints) > 0) { + foreach ($hints as $hint) { + $result[] = $hint; + } + } + + $result[] = '[' . get_class($e) . '] ' . $e->getMessage() . ''; + } + + if (isset($tlsWarning)) { + $result[] = $tlsWarning; + } + + if (count($result) > 0) { + return $result; + } + + return true; + } + + /** + * @return string|\Exception */ - private function checkHttpProxy() + private function checkHttpProxy(RequestProxy $proxy, string $protocol) { $result = $this->checkConnectivityAndComposerNetworkHttpEnablement(); if ($result !== true) { return $result; } - $protocol = extension_loaded('openssl') ? 'https' : 'http'; try { - $json = $this->httpDownloader->get($protocol . '://repo.packagist.org/packages.json')->decodeJson(); - $hash = reset($json['provider-includes']); - $hash = $hash['sha256']; - $path = str_replace('%hash%', $hash, key($json['provider-includes'])); - $provider = $this->httpDownloader->get($protocol . '://repo.packagist.org/'.$path)->getBody(); - - if (hash('sha256', $provider) !== $hash) { - return 'It seems that your proxy is modifying http traffic on the fly'; + $proxyStatus = $proxy->getStatus(); + + if ($proxy->isExcludedByNoProxy()) { + return 'SKIP Because repo.packagist.org is '.$proxyStatus.''; } + + $json = $this->httpDownloader->get($protocol.'://repo.packagist.org/packages.json')->decodeJson(); + if (isset($json['provider-includes'])) { + $hash = reset($json['provider-includes']); + $hash = $hash['sha256']; + $path = str_replace('%hash%', $hash, key($json['provider-includes'])); + $provider = $this->httpDownloader->get($protocol.'://repo.packagist.org/'.$path)->getBody(); + + if (hash('sha256', $provider) !== $hash) { + return 'It seems that your proxy ('.$proxyStatus.') is modifying '.$protocol.' traffic on the fly'; + } + } + + return 'OK '.$proxyStatus.''; } catch (\Exception $e) { return $e; } - - return true; } /** @@ -610,7 +717,7 @@ private function checkPlatform() $errors['ioncube'] = ioncube_loader_version(); } - if (PHP_VERSION_ID < 70205) { + if (\PHP_VERSION_ID < 70205) { $errors['php'] = PHP_VERSION; } @@ -658,6 +765,12 @@ private function checkPlatform() $warnings['onedrive'] = PHP_VERSION; } + if (extension_loaded('uopz') + && !(filter_var(ini_get('uopz.disable'), FILTER_VALIDATE_BOOLEAN) + || filter_var(ini_get('uopz.exit'), FILTER_VALIDATE_BOOLEAN))) { + $warnings['uopz'] = true; + } + if (!empty($errors)) { foreach ($errors as $error => $current) { switch ($error) { @@ -771,6 +884,11 @@ private function checkPlatform() $text .= "Upgrade your PHP ({$current}) to use this location with Composer.".PHP_EOL; break; + case 'uopz': + $text = "The uopz extension ignores exit calls and may not work with all Composer commands.".PHP_EOL; + $text .= "Disabling it when using Composer is recommended."; + break; + default: throw new \InvalidArgumentException(sprintf("DiagnoseCommand: Unknown warning type \"%s\". Please report at https://github.com/composer/composer/issues/new.", $warning)); } diff --git a/src/Composer/Command/DumpAutoloadCommand.php b/src/Composer/Command/DumpAutoloadCommand.php index c30fae7a82b2..cc0d7bf8079d 100644 --- a/src/Composer/Command/DumpAutoloadCommand.php +++ b/src/Composer/Command/DumpAutoloadCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\Package\AliasPackage; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Symfony\Component\Console\Input\InputInterface; @@ -43,6 +44,7 @@ protected function configure() new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), new InputOption('strict-psr', null, InputOption::VALUE_NONE, 'Return a failed status code (1) if PSR-4 or PSR-0 mapping errors are present. Requires --optimize to work.'), + new InputOption('strict-ambiguous', null, InputOption::VALUE_NONE, 'Return a failed status code (2) if the same class is found in multiple files. Requires --optimize to work.'), ]) ->setHelp( <<getPackage(); $config = $composer->getConfig(); + $missingDependencies = false; + foreach ($localRepo->getCanonicalPackages() as $localPkg) { + $installPath = $installationManager->getInstallPath($localPkg); + if ($installPath !== null && file_exists($installPath) === false) { + $missingDependencies = true; + $this->getIO()->write('Not all dependencies are installed. Make sure to run a "composer install" to install missing dependencies'); + + break; + } + } + $optimize = $input->getOption('optimize') || $config->get('optimize-autoloader'); $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); $apcuPrefix = $input->getOption('apcu-prefix'); @@ -74,6 +87,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->getOption('strict-psr') && !$optimize && !$authoritative) { throw new \InvalidArgumentException('--strict-psr mode only works with optimized autoloader, use --optimize or --classmap-authoritative if you want a strict return value.'); } + if ($input->getOption('strict-ambiguous') && !$optimize && !$authoritative) { + throw new \InvalidArgumentException('--strict-ambiguous mode only works with optimized autoloader, use --optimize or --classmap-authoritative if you want a strict return value.'); + } if ($authoritative) { $this->getIO()->write('Generating optimized autoload files (authoritative)'); @@ -108,7 +124,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'composer', $optimize, null, - $composer->getLocker() + $composer->getLocker(), + $input->getOption('strict-ambiguous') ); $numberOfClasses = count($classMap); @@ -120,10 +137,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->getIO()->write('Generated autoload files'); } - if ($input->getOption('strict-psr') && count($classMap->getPsrViolations()) > 0) { + if ($missingDependencies || ($input->getOption('strict-psr') && count($classMap->getPsrViolations()) > 0)) { return 1; } + if ($input->getOption('strict-ambiguous') && count($classMap->getAmbiguousClasses(false)) > 0) { + return 2; + } + return 0; } } diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php index c6fc7fe7a6ff..2841c2ae178a 100644 --- a/src/Composer/Command/GlobalCommand.php +++ b/src/Composer/Command/GlobalCommand.php @@ -33,9 +33,13 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti { $application = $this->getApplication(); if ($input->mustSuggestArgumentValuesFor('command-name')) { - $suggestions->suggestValues(array_values(array_filter(array_map(static function (Command $command) { - return $command->isHidden() ? null : $command->getName(); - }, $application->all())))); + $suggestions->suggestValues(array_values(array_filter( + array_map(static function (Command $command) { + return $command->isHidden() ? null : $command->getName(); + }, $application->all()), function (?string $cmd) { + return $cmd !== null; + } + ))); return; } diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php index 5aec6f4c181f..3547faec7799 100644 --- a/src/Composer/Command/HomeCommand.php +++ b/src/Composer/Command/HomeCommand.php @@ -122,22 +122,20 @@ private function handlePackage(CompletePackageInterface $package, bool $showHome */ private function openBrowser(string $url): void { - $url = ProcessExecutor::escape($url); - $process = new ProcessExecutor($this->getIO()); if (Platform::isWindows()) { - $process->execute('start "web" explorer ' . $url, $output); + $process->execute(['start', '"web"', 'explorer', $url], $output); return; } - $linux = $process->execute('which xdg-open', $output); - $osx = $process->execute('which open', $output); + $linux = $process->execute(['which', 'xdg-open'], $output); + $osx = $process->execute(['which', 'open'], $output); if (0 === $linux) { - $process->execute('xdg-open ' . $url, $output); + $process->execute(['xdg-open', $url], $output); } elseif (0 === $osx) { - $process->execute('open ' . $url, $output); + $process->execute(['open', $url], $output); } else { $this->getIO()->writeError('No suitable browser opening command found, open yourself: ' . $url); } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 606bff8207bd..8b01dda6fcde 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -21,11 +21,13 @@ use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; +use Composer\Spdx\SpdxLicenses; use Composer\Util\Filesystem; use Composer\Util\Silencer; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Composer\Console\Input\InputOption; +use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -57,11 +59,11 @@ protected function configure() new InputOption('name', null, InputOption::VALUE_REQUIRED, 'Name of the package'), new InputOption('description', null, InputOption::VALUE_REQUIRED, 'Description of package'), new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), - new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), + new InputOption('type', null, InputOption::VALUE_REQUIRED, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), - new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), + new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::STABILITIES)).')'), new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'), new InputOption('autoload', 'a', InputOption::VALUE_REQUIRED, 'Add PSR-4 autoload mapping. Maps your package\'s namespace to the provided directory. (Expects a relative path, e.g. src/)'), @@ -87,7 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io = $this->getIO(); $allowlist = ['name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload']; - $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist))); + $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowlist)), function ($val) { return $val !== null && $val !== []; }); if (isset($options['name']) && !Preg::isMatch('{^[a-z0-9_.-]+/[a-z0-9_.-]+$}D', $options['name'])) { throw new \InvalidArgumentException( @@ -364,10 +366,10 @@ static function ($value) use ($minimumStability) { return $minimumStability; } - if (!isset(BasePackage::$stabilities[$value])) { + if (!isset(BasePackage::STABILITIES[$value])) { throw new \InvalidArgumentException( 'Invalid minimum stability "'.$value.'". Must be empty or one of: '. - implode(', ', array_keys(BasePackage::$stabilities)) + implode(', ', array_keys(BasePackage::STABILITIES)) ); } @@ -378,11 +380,14 @@ static function ($value) use ($minimumStability) { ); $input->setOption('stability', $minimumStability); - $type = $input->getOption('type') ?: false; + $type = $input->getOption('type'); $type = $io->ask( 'Package Type (e.g. library, project, metapackage, composer-plugin) ['.$type.']: ', $type ); + if ($type === '' || $type === false) { + $type = null; + } $input->setOption('type', $type); if (null === $license = $input->getOption('license')) { @@ -395,6 +400,10 @@ static function ($value) use ($minimumStability) { 'License ['.$license.']: ', $license ); + $spdx = new SpdxLicenses(); + if (null !== $license && !$spdx->validate($license) && $license !== 'proprietary') { + throw new \InvalidArgumentException('Invalid license provided: '.$license.'. Only SPDX license identifiers (https://spdx.org/licenses/) or "proprietary" are accepted.'); + } $input->setOption('license', $license); $io->writeError(['', 'Define your dependencies.', '']); @@ -465,8 +474,6 @@ static function ($value) use ($autoload) { private function parseAuthorString(string $author): array { if (Preg::isMatch('/^(?P[- .,\p{L}\p{N}\p{Mn}\'’"()]+)(?:\s+<(?P.+?)>)?$/u', $author, $match)) { - assert(is_string($match['name'])); - if (null !== $match['email'] && !$this->isValidEmail($match['email'])) { throw new \InvalidArgumentException('Invalid email "'.$match['email'].'"'); } @@ -529,15 +536,11 @@ protected function getGitConfig(): array return $this->gitConfig; } - $finder = new ExecutableFinder(); - $gitBin = $finder->find('git'); - - $cmd = new Process([$gitBin, 'config', '-l']); - $cmd->run(); + $process = new ProcessExecutor($this->getIO()); - if ($cmd->isSuccessful()) { + if (0 === $process->execute(['git', 'config', '-l'], $output)) { $this->gitConfig = []; - Preg::matchAllStrictGroups('{^([^=]+)=(.*)$}m', $cmd->getOutput(), $matches); + Preg::matchAllStrictGroups('{^([^=]+)=(.*)$}m', $output, $matches); foreach ($matches[1] as $key => $match) { $this->gitConfig[$match] = $matches[2][$key]; } diff --git a/src/Composer/Command/OutdatedCommand.php b/src/Composer/Command/OutdatedCommand.php index 139fe45a85ec..0fea6dc092f1 100644 --- a/src/Composer/Command/OutdatedCommand.php +++ b/src/Composer/Command/OutdatedCommand.php @@ -71,6 +71,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'command' => 'show', '--latest' => true, ]; + if ($input->getOption('no-interaction')) { + $args['--no-interaction'] = true; + } + if ($input->getOption('no-plugins')) { + $args['--no-plugins'] = true; + } + if ($input->getOption('no-scripts')) { + $args['--no-scripts'] = true; + } + if ($input->getOption('no-cache')) { + $args['--no-cache'] = true; + } if (!$input->getOption('all')) { $args['--outdated'] = true; } diff --git a/src/Composer/Command/PackageDiscoveryTrait.php b/src/Composer/Command/PackageDiscoveryTrait.php index d95e06de8ead..0bbd2a48cd28 100644 --- a/src/Composer/Command/PackageDiscoveryTrait.php +++ b/src/Composer/Command/PackageDiscoveryTrait.php @@ -16,6 +16,7 @@ use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\IO\IOInterface; +use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; @@ -52,6 +53,9 @@ protected function getRepos(): CompositeRepository return $this->repos; } + /** + * @param key-of|null $minimumStability + */ private function getRepositorySet(InputInterface $input, ?string $minimumStability = null): RepositorySet { $key = $minimumStability ?? 'default'; @@ -64,6 +68,9 @@ private function getRepositorySet(InputInterface $input, ?string $minimumStabili return $this->repositorySets[$key]; } + /** + * @return key-of + */ private function getMinimumStability(InputInterface $input): string { if ($input->hasOption('stability')) { // @phpstan-ignore-line as InitCommand does have this option but not all classes using this trait do @@ -96,7 +103,7 @@ final protected function determineRequirements(InputInterface $input, OutputInte foreach ($requires as $requirement) { if (isset($requirement['version']) && Preg::isMatch('{^\d+(\.\d+)?$}', $requirement['version'])) { - $io->writeError('The "'.$requirement['version'].'" constraint for "'.$requirement['name'].'" appears too strict and will likely not match what you want. See https://getcomposer.org/constraints'); + $io->writeError('The "'.$requirement['version'].'" constraint for "'.$requirement['name'].'" appears too strict and will likely not match what you want. See https://getcomposer.org/constraints'); } if (!isset($requirement['version'])) { diff --git a/src/Composer/Command/ReinstallCommand.php b/src/Composer/Command/ReinstallCommand.php index 446d9eec14b3..cb7882a9cf49 100644 --- a/src/Composer/Command/ReinstallCommand.php +++ b/src/Composer/Command/ReinstallCommand.php @@ -51,7 +51,8 @@ protected function configure(): void new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.', null, $this->suggestInstalledPackage(false)), + new InputOption('type', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Filter packages to reinstall by type(s)', null, $this->suggestInstalledPackageTypes(false)), + new InputArgument('packages', InputArgument::IS_ARRAY, 'List of package names to reinstall, can include a wildcard (*) to match any substring.', null, $this->suggestInstalledPackage(false)), ]) ->setHelp( <<getRepositoryManager()->getLocalRepository(); $packagesToReinstall = []; $packageNamesToReinstall = []; - foreach ($input->getArgument('packages') as $pattern) { - $patternRegexp = BasePackage::packageNameToRegexp($pattern); - $matched = false; + if (\count($input->getOption('type')) > 0) { + if (\count($input->getArgument('packages')) > 0) { + throw new \InvalidArgumentException('You cannot specify package names and filter by type at the same time.'); + } foreach ($localRepo->getCanonicalPackages() as $package) { - if (Preg::isMatch($patternRegexp, $package->getName())) { - $matched = true; + if (in_array($package->getType(), $input->getOption('type'), true)) { $packagesToReinstall[] = $package; $packageNamesToReinstall[] = $package->getName(); } } + } else { + if (\count($input->getArgument('packages')) === 0) { + throw new \InvalidArgumentException('You must pass one or more package names to be reinstalled.'); + } + foreach ($input->getArgument('packages') as $pattern) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + $matched = false; + foreach ($localRepo->getCanonicalPackages() as $package) { + if (Preg::isMatch($patternRegexp, $package->getName())) { + $matched = true; + $packagesToReinstall[] = $package; + $packageNamesToReinstall[] = $package->getName(); + } + } - if (!$matched) { - $io->writeError('Pattern "' . $pattern . '" does not match any currently installed packages.'); + if (!$matched) { + $io->writeError('Pattern "' . $pattern . '" does not match any currently installed packages.'); + } } } - if (!$packagesToReinstall) { + if (0 === \count($packagesToReinstall)) { $io->writeError('Found no packages to reinstall, aborting.'); return 1; diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index b40fb774e45a..9803190a4760 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -43,7 +43,7 @@ protected function configure() { $this ->setName('remove') - ->setAliases(['rm']) + ->setAliases(['rm', 'uninstall']) ->setDescription('Removes a package from the require or require-dev') ->setDefinition([ new InputArgument('packages', InputArgument::IS_ARRAY, 'Packages that should be removed.', null, $this->suggestRootRequirement()), diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index b7ee15681f55..59d1e0585a6c 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -402,6 +402,8 @@ private function getPackagesByRequireKey(): array /** * @param array $requirements + * @param 'require'|'require-dev' $requireKey + * @param 'require'|'require-dev' $removeKey * @throws \Exception */ private function doUpdate(InputInterface $input, OutputInterface $output, IOInterface $io, array $requirements, string $requireKey, string $removeKey): int @@ -552,22 +554,15 @@ private function updateRequirementsAfterResolution(array $requirementsToUpdate, if (!$dryRun) { $this->updateFile($this->json, $requirements, $requireKey, $removeKey, $sortPackages); - if ($locker->isLocked()) { - $contents = file_get_contents($this->json->getPath()); - if (false === $contents) { - throw new \RuntimeException('Unable to read '.$this->json->getPath().' contents to update the lock file hash.'); - } - $lockFile = Factory::getLockFile($this->json->getPath()); - if (file_exists($lockFile)) { - $lockMtime = filemtime($lockFile); - $lock = new JsonFile($lockFile); - $lockData = $lock->read(); - $lockData['content-hash'] = Locker::getContentHash($contents); - $lock->write($lockData); - if (is_int($lockMtime)) { - @touch($lockFile, $lockMtime); + if ($locker->isLocked() && $composer->getConfig()->get('lock')) { + $stabilityFlags = RootPackageLoader::extractStabilityFlags($requirements, $composer->getPackage()->getMinimumStability(), []); + $locker->updateHash($this->json, function (array $lockData) use ($stabilityFlags) { + foreach ($stabilityFlags as $packageName => $flag) { + $lockData['stability-flags'][$packageName] = $flag; } - } + + return $lockData; + }); } } diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index 61f0f95eb5c5..d95c94168127 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if (!($composer = $this->tryComposer())) { - $composer = Factory::create($this->getIO(), [], $input->hasParameterOption('--no-plugins')); + $composer = $this->createComposerInstance($input, $this->getIO(), []); } $localRepo = $composer->getRepositoryManager()->getLocalRepository(); $installedRepo = new CompositeRepository([$localRepo, $platformRepo]); diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 26d9f75467d8..6ca01120c065 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -146,7 +146,7 @@ class_exists('Composer\Downloader\FilesystemException'); $homeDirOwnerId = fileowner($home); if (is_array($composerUser) && $homeDirOwnerId !== false) { $homeOwner = posix_getpwuid($homeDirOwnerId); - if (is_array($homeOwner) && isset($composerUser['name'], $homeOwner['name']) && $composerUser['name'] !== $homeOwner['name']) { + if (is_array($homeOwner) && $composerUser['name'] !== $homeOwner['name']) { $io->writeError('You are running Composer as "'.$composerUser['name'].'", while "'.$home.'" is owned by "'.$homeOwner['name'].'"'); } } @@ -328,8 +328,8 @@ class_exists('Composer\Downloader\FilesystemException'); $verified = 1 === openssl_verify((string) file_get_contents($tempFilename), $signatureSha384, $pubkeyid, $algo); // PHP 8 automatically frees the key instance and deprecates the function - if (PHP_VERSION_ID < 80000) { - // @phpstan-ignore-next-line + if (\PHP_VERSION_ID < 80000) { + // @phpstan-ignore function.deprecated openssl_free_key($pubkeyid); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 542482554b6b..14c9a4c32165 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -43,6 +43,7 @@ use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; use Composer\Util\PackageInfo; +use DateTimeInterface; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; @@ -478,7 +479,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($showLatest && $showVersion) { foreach ($packages[$type] as $package) { - if (is_object($package)) { + if (is_object($package) && !Preg::isMatch($ignoredPackagesRegex, $package->getPrettyName())) { $latestPackage = $this->findLatestPackage($package, $composer, $platformRepo, $showMajorOnly, $showMinorOnly, $showPatchOnly, $platformReqFilter); if ($latestPackage === null) { continue; @@ -493,7 +494,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion; $writeLatest = $writeVersion && $showLatest; $writeDescription = !$input->getOption('name-only') && !$input->getOption('path'); - $writeReleaseDate = $writeLatest && $input->getOption('sort-by-age'); + $writeReleaseDate = $writeLatest && ($input->getOption('sort-by-age') || $format === 'json'); $hasOutdatedPackages = false; @@ -535,10 +536,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $packageViewData['homepage'] = $package instanceof CompletePackageInterface ? $package->getHomepage() : null; $packageViewData['source'] = PackageInfo::getViewSourceUrl($package); } - $nameLength = max($nameLength, strlen($package->getPrettyName())); + $nameLength = max($nameLength, strlen($packageViewData['name'])); if ($writeVersion) { $packageViewData['version'] = $package->getFullPrettyVersion(); - $versionLength = max($versionLength, strlen($package->getFullPrettyVersion())); + if ($format === 'text') { + $packageViewData['version'] = ltrim($packageViewData['version'], 'v'); + } + $versionLength = max($versionLength, strlen($packageViewData['version'])); } if ($writeReleaseDate) { if ($package->getReleaseDate() !== null) { @@ -547,14 +551,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int $packageViewData['release-age'] = 'from '.$packageViewData['release-age']; } $releaseDateLength = max($releaseDateLength, strlen($packageViewData['release-age'])); + $packageViewData['release-date'] = $package->getReleaseDate()->format(DateTimeInterface::ATOM); } else { $packageViewData['release-age'] = ''; + $packageViewData['release-date'] = ''; } } if ($writeLatest && $latestPackage) { $packageViewData['latest'] = $latestPackage->getFullPrettyVersion(); + if ($format === 'text') { + $packageViewData['latest'] = ltrim($packageViewData['latest'], 'v'); + } $packageViewData['latest-status'] = $this->getUpdateStatus($latestPackage, $package); $latestLength = max($latestLength, strlen($packageViewData['latest'])); + + if ($latestPackage->getReleaseDate() !== null) { + $packageViewData['latest-release-date'] = $latestPackage->getReleaseDate()->format(DateTimeInterface::ATOM); + } else { + $packageViewData['latest-release-date'] = ''; + } } elseif ($writeLatest) { $packageViewData['latest'] = '[none matched]'; $packageViewData['latest-status'] = 'up-to-date'; @@ -802,7 +817,8 @@ protected function getPackage(InstalledRepository $installedRepo, RepositoryInte $pool = $repositorySet->createPoolForPackage($name); } $matches = $pool->whatProvides($name, $constraint); - foreach ($matches as $index => $package) { + $literals = []; + foreach ($matches as $package) { // avoid showing the 9999999-dev alias if the default branch has no branch-alias set if ($package instanceof AliasPackage && $package->getVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { $package = $package->getAliasOf(); @@ -814,11 +830,12 @@ protected function getPackage(InstalledRepository $installedRepo, RepositoryInte } $versions[$package->getPrettyVersion()] = $package->getVersion(); - $matches[$index] = $package->getId(); + $literals[] = $package->getId(); } // select preferred package according to policy rules - if (null === $matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { + if (null === $matchedPackage && \count($literals) > 0) { + $preferred = $policy->selectPreferredPackages($pool, $literals); $matchedPackage = $pool->literalToPackage($preferred[0]); } @@ -1448,7 +1465,7 @@ private function findLatestPackage(PackageInterface $package, Composer $composer $stability = $composer->getPackage()->getMinimumStability(); $flags = $composer->getPackage()->getStabilityFlags(); if (isset($flags[$name])) { - $stability = array_search($flags[$name], BasePackage::$stabilities, true); + $stability = array_search($flags[$name], BasePackage::STABILITIES, true); } $bestStability = $stability; diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index 780f70031a64..5d90b310506b 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -92,7 +92,7 @@ private function doExecute(InputInterface $input): int $vcsVersionChanges = []; $parser = new VersionParser; - $guesser = new VersionGuesser($composer->getConfig(), $composer->getLoop()->getProcessExecutor() ?? new ProcessExecutor($io), $parser); + $guesser = new VersionGuesser($composer->getConfig(), $composer->getLoop()->getProcessExecutor() ?? new ProcessExecutor($io), $parser, $io); $dumper = new ArrayDumper; // list packages diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 40e566136c4f..6747242da1e0 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -16,20 +16,28 @@ use Composer\DependencyResolver\Request; use Composer\Installer; use Composer\IO\IOInterface; +use Composer\Package\BasePackage; use Composer\Package\Loader\RootPackageLoader; +use Composer\Package\PackageInterface; +use Composer\Package\Version\VersionSelector; use Composer\Pcre\Preg; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Package\Version\VersionParser; +use Composer\Repository\CompositeRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Repository\RepositorySet; +use Composer\Semver\Constraint\MultiConstraint; use Composer\Semver\Intervals; use Composer\Util\HttpDownloader; use Composer\Advisory\Auditor; +use Composer\Util\Platform; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Composer\Console\Input\InputOption; use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; /** * @author Jordi Boggiano @@ -76,8 +84,10 @@ protected function configure() new InputOption('prefer-stable', null, InputOption::VALUE_NONE, 'Prefer stable versions of dependencies (can also be set via the COMPOSER_PREFER_STABLE=1 env var).'), new InputOption('prefer-lowest', null, InputOption::VALUE_NONE, 'Prefer lowest versions of dependencies (can also be set via the COMPOSER_PREFER_LOWEST=1 env var).'), new InputOption('minimal-changes', 'm', InputOption::VALUE_NONE, 'During a partial update with -w/-W, only perform absolutely necessary changes to transitive dependencies (can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var).'), + new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Only allow patch version updates for currently installed dependencies.'), new InputOption('interactive', 'i', InputOption::VALUE_NONE, 'Interactive interface with autocompletion to select the packages to update.'), new InputOption('root-reqs', null, InputOption::VALUE_NONE, 'Restricts the update to your first degree dependencies.'), + new InputOption('bump-after-update', null, InputOption::VALUE_OPTIONAL, 'Runs bump after performing the update.', false, ['dev', 'no-dev', 'all']), ]) ->setHelp( <<getOption('patch-only')) { + if (!$composer->getLocker()->isLocked()) { + throw new \InvalidArgumentException('patch-only can only be used with a lock file present'); + } + foreach ($composer->getLocker()->getLockedRepository(true)->getCanonicalPackages() as $package) { + if ($package->isDev()) { + continue; + } + if (!Preg::isMatch('{^(\d+\.\d+\.\d+)}', $package->getVersion(), $match)) { + continue; + } + $constraint = $parser->parseConstraints('~'.$match[1]); + if (isset($temporaryConstraints[$package->getName()])) { + $temporaryConstraints[$package->getName()] = MultiConstraint::create([$temporaryConstraints[$package->getName()], $constraint], true); + } else { + $temporaryConstraints[$package->getName()] = $constraint; + } + } + } + if ($input->getOption('interactive')) { $packages = $this->getPackagesInteractively($io, $input, $output, $composer, $packages); } @@ -248,7 +278,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int $install->disablePlugins(); } - return $install->run(); + $result = $install->run(); + + if ($result === 0) { + $bumpAfterUpdate = $input->getOption('bump-after-update'); + if (false === $bumpAfterUpdate) { + $bumpAfterUpdate = $composer->getConfig()->get('bump-after-update'); + } + + if (false !== $bumpAfterUpdate) { + $io->writeError('Bumping dependencies'); + $bumpCommand = new BumpCommand(); + $bumpCommand->setComposer($composer); + $result = $bumpCommand->doBump( + $io, + $bumpAfterUpdate === 'dev', + $bumpAfterUpdate === 'no-dev', + $input->getOption('dry-run'), + $input->getArgument('packages') + ); + } + } + return $result; } /** @@ -261,46 +312,53 @@ private function getPackagesInteractively(IOInterface $io, InputInterface $input throw new \InvalidArgumentException('--interactive cannot be used in non-interactive terminals.'); } + $platformReqFilter = $this->getPlatformRequirementFilter($input); + $stabilityFlags = $composer->getPackage()->getStabilityFlags(); $requires = array_merge( $composer->getPackage()->getRequires(), $composer->getPackage()->getDevRequires() ); - $autocompleterValues = []; - foreach ($requires as $require) { - $target = $require->getTarget(); - $autocompleterValues[strtolower($target)] = $target; - } - - $installedPackages = $composer->getRepositoryManager()->getLocalRepository()->getPackages(); - foreach ($installedPackages as $package) { - $autocompleterValues[$package->getName()] = $package->getPrettyName(); - } - $helper = $this->getHelper('question'); - $question = new Question('Enter package name: ', null); + $filter = \count($packages) > 0 ? BasePackage::packageNamesToRegexp($packages) : null; - $io->writeError('Press enter without value to end submission'); - - do { - $autocompleterValues = array_diff($autocompleterValues, $packages); - $question->setAutocompleterValues($autocompleterValues); - $addedPackage = $helper->ask($input, $output, $question); - - if (!is_string($addedPackage) || empty($addedPackage)) { - break; + $io->writeError('Loading packages that can be updated...'); + $autocompleterValues = []; + $installedPackages = $composer->getLocker()->isLocked() ? $composer->getLocker()->getLockedRepository(true)->getPackages() : $composer->getRepositoryManager()->getLocalRepository()->getPackages(); + $versionSelector = $this->createVersionSelector($composer); + foreach ($installedPackages as $package) { + if ($filter !== null && !Preg::isMatch($filter, $package->getName())) { + continue; } - - $addedPackage = strtolower($addedPackage); - if (!in_array($addedPackage, $packages)) { - $packages[] = $addedPackage; + $currentVersion = $package->getPrettyVersion(); + $constraint = isset($requires[$package->getName()]) ? $requires[$package->getName()]->getPrettyConstraint() : null; + $stability = isset($stabilityFlags[$package->getName()]) ? (string) array_search($stabilityFlags[$package->getName()], BasePackage::STABILITIES, true) : $composer->getPackage()->getMinimumStability(); + $latestVersion = $versionSelector->findBestCandidate($package->getName(), $constraint, $stability, $platformReqFilter); + if ($latestVersion !== false && ($package->getVersion() !== $latestVersion->getVersion() || $latestVersion->isDev())) { + $autocompleterValues[$package->getName()] = '' . $currentVersion . ' => ' . $latestVersion->getPrettyVersion() . ''; + } + } + if (0 === \count($installedPackages)) { + foreach ($requires as $req => $constraint) { + if (PlatformRepository::isPlatformPackage($req)) { + continue; + } + $autocompleterValues[$req] = ''; } - } while (true); + } - $packages = array_filter($packages); - if (!$packages) { - throw new \InvalidArgumentException('You must enter minimum one package.'); + if (0 === \count($autocompleterValues)) { + throw new \RuntimeException('Could not find any package with new versions available'); } + $packages = $io->select( + 'Select packages: (Select more than one value separated by comma) ', + $autocompleterValues, + false, + 1, + 'No package named "%s" is installed.', + true + ); + $table = new Table($output); $table->setHeaders(['Selected packages']); foreach ($packages as $package) { @@ -317,4 +375,14 @@ private function getPackagesInteractively(IOInterface $io, InputInterface $input throw new \RuntimeException('Installation aborted.'); } + + private function createVersionSelector(Composer $composer): VersionSelector + { + $repositorySet = new RepositorySet(); + $repositorySet->addRepository(new CompositeRepository(array_filter($composer->getRepositoryManager()->getRepositories(), function (RepositoryInterface $repository) { + return !$repository instanceof PlatformRepository; + }))); + + return new VersionSelector($repositorySet); + } } diff --git a/src/Composer/Command/ValidateCommand.php b/src/Composer/Command/ValidateCommand.php index a5d1e9c46092..d9d8c7510238 100644 --- a/src/Composer/Command/ValidateCommand.php +++ b/src/Composer/Command/ValidateCommand.php @@ -91,7 +91,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int [$errors, $publishErrors, $warnings] = $validator->validate($file, $checkAll, $checkVersion); $lockErrors = []; - $composer = Factory::create($io, $file, $input->hasParameterOption('--no-plugins')); + $composer = $this->createComposerInstance($input, $io, $file); // config.lock = false ~= implicit --no-check-lock; --check-lock overrides $checkLock = ($checkLock && $composer->getConfig()->get('lock')) || $input->getOption('check-lock'); $locker = $composer->getLocker(); @@ -106,7 +106,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->outputResult($io, $file, $errors, $warnings, $checkPublish, $publishErrors, $checkLock, $lockErrors, true); // $errors include publish and lock errors when exists - $exitCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); + $exitCode = count($errors) > 0 ? 2 : (($isStrict && count($warnings) > 0) ? 1 : 0); if ($input->getOption('with-dependencies')) { $localRepo = $composer->getRepositoryManager()->getLocalRepository(); @@ -122,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->outputResult($io, $package->getPrettyName(), $errors, $warnings, $checkPublish, $publishErrors); // $errors include publish errors when exists - $depCode = $errors ? 2 : ($isStrict && $warnings ? 1 : 0); + $depCode = count($errors) > 0 ? 2 : (($isStrict && count($warnings) > 0) ? 1 : 0); $exitCode = max($depCode, $exitCode); } } diff --git a/src/Composer/Compiler.php b/src/Composer/Compiler.php index 9a2e27f5b426..591132c7f104 100644 --- a/src/Composer/Compiler.php +++ b/src/Composer/Compiler.php @@ -15,6 +15,7 @@ use Composer\Json\JsonFile; use Composer\CaBundle\CaBundle; use Composer\Pcre\Preg; +use Composer\Util\ProcessExecutor; use Symfony\Component\Finder\Finder; use Symfony\Component\Process\Process; use Seld\PharUtils\Timestamps; @@ -48,23 +49,22 @@ public function compile(string $pharFile = 'composer.phar'): void unlink($pharFile); } - $process = new Process(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], __DIR__); - if ($process->run() !== 0) { + $process = new ProcessExecutor(); + + if (0 !== $process->execute(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], $output, dirname(dirname(__DIR__)))) { throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.'); } - $this->version = trim($process->getOutput()); + $this->version = trim($output); - $process = new Process(['git', 'log', '-n1', '--pretty=%ci', 'HEAD'], __DIR__); - if ($process->run() !== 0) { + if (0 !== $process->execute(['git', 'log', '-n1', '--pretty=%ci', 'HEAD'], $output, dirname(dirname(__DIR__)))) { throw new \RuntimeException('Can\'t run git log. You must ensure to run compile from composer git repository clone and that git binary is available.'); } - $this->versionDate = new \DateTime(trim($process->getOutput())); + $this->versionDate = new \DateTime(trim($output)); $this->versionDate->setTimezone(new \DateTimeZone('UTC')); - $process = new Process(['git', 'describe', '--tags', '--exact-match', 'HEAD'], __DIR__); - if ($process->run() === 0) { - $this->version = trim($process->getOutput()); + if (0 === $process->execute(['git', 'describe', '--tags', '--exact-match', 'HEAD'], $output, dirname(dirname(__DIR__)))) { + $this->version = trim($output); } else { // get branch-alias defined in composer.json for dev-main (if any) $localConfig = __DIR__.'/../../composer.json'; @@ -75,6 +75,10 @@ public function compile(string $pharFile = 'composer.phar'): void } } + if ('' === $this->version) { + throw new \UnexpectedValueException('Version detection failed'); + } + $phar = new \Phar($pharFile, 0, 'composer.phar'); $phar->setSignatureAlgorithm(\Phar::SHA512); @@ -120,6 +124,7 @@ public function compile(string $pharFile = 'composer.phar'): void ->notPath('/bin\/(jsonlint|validate-json|simple-phpunit|phpstan|phpstan\.phar)(\.bat)?$/') ->notPath('justinrainbow/json-schema/demo/') ->notPath('justinrainbow/json-schema/dist/') + ->notPath('composer/pcre/extension.neon') ->notPath('composer/LICENSE') ->exclude('Tests') ->exclude('tests') diff --git a/src/Composer/Composer.php b/src/Composer/Composer.php index 4bf1679f6ff6..07d6973334d4 100644 --- a/src/Composer/Composer.php +++ b/src/Composer/Composer.php @@ -54,7 +54,7 @@ class Composer extends PartialComposer public const VERSION = '@package_version@'; public const BRANCH_ALIAS_VERSION = '@package_branch_alias_version@'; public const RELEASE_DATE = '@release_date@'; - public const SOURCE_VERSION = '2.7.999-dev+source'; + public const SOURCE_VERSION = '2.8.999-dev+source'; /** * Version number of the internal composer-runtime-api package diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 8d2885a3cc1c..f39579e06b7a 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -84,6 +84,8 @@ class Config 'gitlab-token' => [], 'http-basic' => [], 'bearer' => [], + 'bump-after-update' => false, + 'allow-missing-requirements' => false, ]; /** @var array */ @@ -96,7 +98,7 @@ class Config /** @var array */ private $config; - /** @var ?string */ + /** @var ?non-empty-string */ private $baseDir; /** @var array */ private $repositories; @@ -125,7 +127,7 @@ public function __construct(bool $useEnvironment = true, ?string $baseDir = null $this->config = static::$defaultConfig; $this->repositories = static::$defaultRepositories; - $this->useEnvironment = (bool) $useEnvironment; + $this->useEnvironment = $useEnvironment; $this->baseDir = is_string($baseDir) && '' !== $baseDir ? $baseDir : null; foreach ($this->config as $configKey => $configValue) { @@ -137,6 +139,18 @@ public function __construct(bool $useEnvironment = true, ?string $baseDir = null } } + /** + * Changing this can break path resolution for relative config paths so do not call this without knowing what you are doing + * + * The $baseDir should be an absolute path and without trailing slash + * + * @param non-empty-string|null $baseDir + */ + public function setBaseDir(?string $baseDir): void + { + $this->baseDir = $baseDir; + } + public function setConfigSource(ConfigSourceInterface $source): void { $this->configSource = $source; @@ -440,9 +454,9 @@ public function get(string $key, int $flags = 0) $result = $this->config[$key]; $abandonedEnv = $this->getComposerEnv('COMPOSER_AUDIT_ABANDONED'); if (false !== $abandonedEnv) { - if (!in_array($abandonedEnv, $validChoices = [Auditor::ABANDONED_IGNORE, Auditor::ABANDONED_REPORT, Auditor::ABANDONED_FAIL], true)) { + if (!in_array($abandonedEnv, $validChoices = Auditor::ABANDONEDS, true)) { throw new \RuntimeException( - "Invalid value for COMPOSER_AUDIT_ABANDONED: {$abandonedEnv}. Expected ".Auditor::ABANDONED_IGNORE.", ".Auditor::ABANDONED_REPORT." or ".Auditor::ABANDONED_FAIL + "Invalid value for COMPOSER_AUDIT_ABANDONED: {$abandonedEnv}. Expected one of ".implode(', ', Auditor::ABANDONEDS)."." ); } $result['abandoned'] = $abandonedEnv; @@ -529,7 +543,6 @@ private function process($value, int $flags) } return Preg::replaceCallback('#\{\$(.+)\}#', function ($match) use ($flags) { - assert(is_string($match[1])); return $this->get($match[1], $flags); }, $value); } @@ -545,7 +558,7 @@ private function realpath(string $path): string return $path; } - return $this->baseDir ? $this->baseDir . '/' . $path : $path; + return $this->baseDir !== null ? $this->baseDir . '/' . $path : $path; } /** @@ -584,8 +597,8 @@ private function disableRepoByName(string $name): void */ public function prohibitUrlByConfig(string $url, ?IOInterface $io = null, array $repoOptions = []): void { - // Return right away if the URL is malformed or custom (see issue #5173) - if (false === filter_var($url, FILTER_VALIDATE_URL)) { + // Return right away if the URL is malformed or custom (see issue #5173), but only for non-HTTP(S) URLs + if (false === filter_var($url, FILTER_VALIDATE_URL) && !Preg::isMatch('{^https?://}', $url)) { return; } diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index db3d36dc4806..596d14f52848 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -162,7 +162,7 @@ public function addProperty(string $name, $value): void public function removeProperty(string $name): void { $this->manipulateJson('removeProperty', static function (&$config, $key): void { - if (strpos($key, 'extra.') === 0 || strpos($key, 'scripts.') === 0) { + if (strpos($key, 'extra.') === 0 || strpos($key, 'scripts.') === 0 || stripos($key, 'autoload.') === 0 || stripos($key, 'autoload-dev.') === 0) { $bits = explode('.', $key); $last = array_pop($bits); $arr = &$config[reset($bits)]; diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 709ce6ef8a13..2b9922337070 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -12,13 +12,13 @@ namespace Composer\Console; +use Composer\Installer; use Composer\IO\NullIO; use Composer\Util\Filesystem; use Composer\Util\Platform; use Composer\Util\Silencer; use LogicException; use RuntimeException; -use Seld\Signal\SignalHandler; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Helper\HelperSet; @@ -32,6 +32,7 @@ use Composer\Command; use Composer\Composer; use Composer\Factory; +use Composer\Downloader\TransportException; use Composer\IO\IOInterface; use Composer\IO\ConsoleIO; use Composer\Json\JsonValidationException; @@ -82,9 +83,6 @@ class Application extends BaseApplication */ private $initialWorkingDirectory; - /** @var SignalHandler */ - private $signalHandler; - public function __construct(string $name = 'Composer', string $version = '') { if (method_exists($this, 'setCatchErrors')) { @@ -106,12 +104,6 @@ public function __construct(string $name = 'Composer', string $version = '') $this->io = new NullIO(); - $this->signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) { - $this->io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); - - $handler->exitWithLastSignal(); - }); - if (!$shutdownRegistered) { $shutdownRegistered = true; @@ -133,7 +125,6 @@ public function __construct(string $name = 'Composer', string $version = '') public function __destruct() { - $this->signalHandler->unregister(); } public function run(?InputInterface $input = null, ?OutputInterface $output = null): int @@ -150,7 +141,10 @@ public function doRun(InputInterface $input, OutputInterface $output): int $this->disablePluginsByDefault = $input->hasParameterOption('--no-plugins'); $this->disableScriptsByDefault = $input->hasParameterOption('--no-scripts'); - $stdin = defined('STDIN') ? STDIN : fopen('php://stdin', 'r'); + static $stdin = null; + if (null === $stdin) { + $stdin = defined('STDIN') ? STDIN : fopen('php://stdin', 'r'); + } if (Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING') !== '1' && (Platform::getEnv('COMPOSER_NO_INTERACTION') || $stdin === false || !Platform::isTty($stdin))) { $input->setInteractive(false); } @@ -190,13 +184,29 @@ public function doRun(InputInterface $input, OutputInterface $output): int } // prompt user for dir change if no composer.json is present in current dir - if ($io->isInteractive() && null === $newWorkDir && !in_array($commandName, ['', 'list', 'init', 'about', 'help', 'diagnose', 'self-update', 'global', 'create-project', 'outdated'], true) && !file_exists(Factory::getComposerFile()) && ($useParentDirIfNoJsonAvailable = $this->getUseParentDirConfigValue()) !== false) { + if ( + null === $newWorkDir + // do not prompt for commands that can function without composer.json + && !in_array($commandName, ['', 'list', 'init', 'about', 'help', 'diagnose', 'self-update', 'global', 'create-project', 'outdated'], true) + && !file_exists(Factory::getComposerFile()) + // if use-parent-dir is disabled we should not prompt + && ($useParentDirIfNoJsonAvailable = $this->getUseParentDirConfigValue()) !== false + // config --file ... should not prompt + && ($commandName !== 'config' || ($input->hasParameterOption('--file', true) === false && $input->hasParameterOption('-f', true) === false)) + // calling a command's help should not prompt + && $input->hasParameterOption('--help', true) === false + && $input->hasParameterOption('-h', true) === false + ) { $dir = dirname(Platform::getCwd(true)); $home = realpath(Platform::getEnv('HOME') ?: Platform::getEnv('USERPROFILE') ?: '/'); // abort when we reach the home dir or top of the filesystem while (dirname($dir) !== $dir && $dir !== $home) { if (file_exists($dir.'/'.Factory::getComposerFile())) { + if ($useParentDirIfNoJsonAvailable !== true && !$io->isInteractive()) { + $io->writeError('No composer.json in current directory, to use the one at '.$dir.' run interactively or set config.use-parent-dir to true'); + break; + } if ($useParentDirIfNoJsonAvailable === true || $io->askConfirmation('No composer.json in current directory, do you want to use the one at '.$dir.'? [Y,n]? ')) { if ($useParentDirIfNoJsonAvailable === true) { $io->writeError('No composer.json in current directory, changing working directory to '.$dir.''); @@ -210,12 +220,13 @@ public function doRun(InputInterface $input, OutputInterface $output): int } $dir = dirname($dir); } + unset($dir, $home); } $needsSudoCheck = !Platform::isWindows() && function_exists('exec') && !Platform::getEnv('COMPOSER_ALLOW_SUPERUSER') - && (ini_get('open_basedir') || !file_exists('/.dockerenv')); + && !Platform::isDocker(); $isNonAllowedRoot = false; // Clobber sudo credentials if COMPOSER_ALLOW_SUPERUSER is not set before loading plugins @@ -293,7 +304,8 @@ public function doRun(InputInterface $input, OutputInterface $output): int } if (!$this->disablePluginsByDefault && $isNonAllowedRoot && !$io->isInteractive()) { - $io->writeError('Composer plugins have been disabled for safety in this non-interactive session. Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.'); + $io->writeError('Composer plugins have been disabled for safety in this non-interactive session.'); + $io->writeError('Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.'); $this->disablePluginsByDefault = true; } @@ -317,7 +329,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknown OS' ), true, IOInterface::DEBUG); - if (PHP_VERSION_ID < 70205) { + if (\PHP_VERSION_ID < 70205) { $io->writeError('Composer supports PHP 7.2.5 and above, you will most likely encounter problems with your PHP '.PHP_VERSION.'. Upgrading is strongly recommended but you can use Composer 2.2.x LTS as a fallback.'); } @@ -344,7 +356,7 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow // Check system temp folder for usability as it can cause weird runtime issues otherwise Silencer::call(static function () use ($io): void { $pid = function_exists('getmypid') ? getmypid() . '-' : ''; - $tempfile = sys_get_temp_dir() . '/temp-' . $pid . md5(microtime()); + $tempfile = sys_get_temp_dir() . '/temp-' . $pid . bin2hex(random_bytes(5)); if (!(file_put_contents($tempfile, __FILE__) && (file_get_contents($tempfile) === __FILE__) && unlink($tempfile) && !file_exists($tempfile))) { $io->writeError(sprintf('PHP temp directory (%s) does not exist or is not writable to Composer. Set sys_temp_dir in your php.ini', sys_get_temp_dir())); } @@ -383,13 +395,18 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow $result = parent::doRun($input, $output); + if (true === $input->hasParameterOption(['--version', '-V'], true)) { + $io->writeError(sprintf('PHP version %s (%s)', \PHP_VERSION, \PHP_BINARY)); + $io->writeError('Run the "diagnose" command to get more detailed diagnostics output.'); + } + // chdir back to $oldWorkingDir if set if (isset($oldWorkingDir) && '' !== $oldWorkingDir) { Silencer::call('chdir', $oldWorkingDir); } if (isset($startTime)) { - $io->writeError('Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MiB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MiB), time: '.round(microtime(true) - $startTime, 2).'s'); + $io->writeError('Memory usage: '.round(memory_get_usage() / 1024 / 1024, 2).'MiB (peak: '.round(memory_get_peak_usage() / 1024 / 1024, 2).'MiB), time: '.round(microtime(true) - $startTime, 2).'s'); } return $result; @@ -419,6 +436,14 @@ function_exists('php_uname') ? php_uname('s') . ' / ' . php_uname('r') : 'Unknow return max(1, $e->getCode()); } + // override TransportException's code for the purpose of parent::run() using it as process exit code + // as http error codes are all beyond the 255 range of permitted exit codes + if ($e instanceof TransportException) { + $reflProp = new \ReflectionProperty($e, 'code'); + $reflProp->setAccessible(true); + $reflProp->setValue($e, Installer::ERROR_TRANSPORT_EXCEPTION); + } + throw $e; } finally { restore_error_handler(); @@ -466,6 +491,11 @@ private function hintCommonErrors(\Throwable $exception, OutputInterface $output } Silencer::restore(); + if ($exception instanceof TransportException && str_contains($exception->getMessage(), 'Unable to use a proxy')) { + $io->writeError('The following exception indicates your proxy is misconfigured', true, IOInterface::QUIET); + $io->writeError('Check https://getcomposer.org/doc/faqs/how-to-use-composer-behind-a-proxy.md for details', true, IOInterface::QUIET); + } + if (Platform::isWindows() && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) { $io->writeError('The following exception may be caused by a stale entry in your cmd.exe AutoRun', true, IOInterface::QUIET); $io->writeError('Check https://getcomposer.org/doc/articles/troubleshooting.md#-the-system-cannot-find-the-path-specified-windows- for details', true, IOInterface::QUIET); diff --git a/src/Composer/Console/Input/InputArgument.php b/src/Composer/Console/Input/InputArgument.php index b6d064fc82f8..19aff8c33b5b 100644 --- a/src/Composer/Console/Input/InputArgument.php +++ b/src/Composer/Console/Input/InputArgument.php @@ -26,7 +26,7 @@ * * @internal * - * TODO drop when PHP 8.1 / symfony 6.1+ can be required + * TODO symfony/console:6.1 drop when PHP 8.1 / symfony 6.1+ can be required */ class InputArgument extends BaseInputArgument { @@ -59,7 +59,7 @@ public function __construct(string $name, ?int $mode = null, string $description public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $values = $this->suggestedValues; - if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore-line + if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore function.impossibleType throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); } if ([] !== $values) { diff --git a/src/Composer/Console/Input/InputOption.php b/src/Composer/Console/Input/InputOption.php index 75bfe90d94a9..b5ff333cd672 100644 --- a/src/Composer/Console/Input/InputOption.php +++ b/src/Composer/Console/Input/InputOption.php @@ -26,7 +26,7 @@ * * @internal * - * TODO drop when PHP 8.1 / symfony 6.1+ can be required + * TODO symfony/console:6.1 drop when PHP 8.1 / symfony 6.1+ can be required */ class InputOption extends BaseInputOption { @@ -62,7 +62,7 @@ public function __construct(string $name, $shortcut = null, ?int $mode = null, s public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $values = $this->suggestedValues; - if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore-line + if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore function.impossibleType throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); } if ([] !== $values) { diff --git a/src/Composer/DependencyResolver/Decisions.php b/src/Composer/DependencyResolver/Decisions.php index 9a5c9c2357f9..599110750fff 100644 --- a/src/Composer/DependencyResolver/Decisions.php +++ b/src/Composer/DependencyResolver/Decisions.php @@ -28,7 +28,7 @@ class Decisions implements \Iterator, \Countable /** @var array */ protected $decisionMap; /** - * @var array + * @var array */ protected $decisionQueue = []; @@ -69,12 +69,12 @@ public function conflict(int $literal): bool public function decided(int $literalOrPackageId): bool { - return !empty($this->decisionMap[abs($literalOrPackageId)]); + return ($this->decisionMap[abs($literalOrPackageId)] ?? 0) !== 0; } public function undecided(int $literalOrPackageId): bool { - return empty($this->decisionMap[abs($literalOrPackageId)]); + return ($this->decisionMap[abs($literalOrPackageId)] ?? 0) === 0; } public function decidedInstall(int $literalOrPackageId): bool @@ -94,7 +94,7 @@ public function decisionLevel(int $literalOrPackageId): int return 0; } - public function decisionRule(int $literalOrPackageId): ?Rule + public function decisionRule(int $literalOrPackageId): Rule { $packageId = abs($literalOrPackageId); @@ -104,7 +104,7 @@ public function decisionRule(int $literalOrPackageId): ?Rule } } - return null; + throw new \LogicException('Did not find a decision rule using '.$literalOrPackageId); } /** @@ -219,7 +219,7 @@ public function toString(?Pool $pool = null): string ksort($decisionMap); $str = '['; foreach ($decisionMap as $packageId => $level) { - $str .= (($pool) ? $pool->literalToPackage($packageId) : $packageId).':'.$level.','; + $str .= ($pool !== null ? $pool->literalToPackage($packageId) : $packageId).':'.$level.','; } $str .= ']'; diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index f8176ae7288b..590187026181 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -30,7 +30,7 @@ class DefaultPolicy implements PolicyInterface private $preferLowest; /** @var array|null */ private $preferredVersions; - /** @var array>> */ + /** @var array>> */ private $preferredPackageResultCachePerPool; /** @var array> */ private $sortingCachePerPool; @@ -53,11 +53,11 @@ public function __construct(bool $preferStable = false, bool $preferLowest = fal public function versionCompare(PackageInterface $a, PackageInterface $b, string $operator): bool { if ($this->preferStable && ($stabA = $a->getStability()) !== ($stabB = $b->getStability())) { - return BasePackage::$stabilities[$stabA] < BasePackage::$stabilities[$stabB]; + return BasePackage::STABILITIES[$stabA] < BasePackage::STABILITIES[$stabB]; } // dev versions need to be compared as branches via matchSpecific's special treatment, the rest can be optimized with compiling matcher - if (strpos($a->getVersion(), 'dev-') === 0 || strpos($b->getVersion(), 'dev-') === 0) { + if (($a->isDev() && str_starts_with($a->getVersion(), 'dev-')) || ($b->isDev() && str_starts_with($b->getVersion(), 'dev-'))) { $constraint = new Constraint($operator, $b->getVersion()); $version = new Constraint('==', $a->getVersion()); @@ -68,9 +68,8 @@ public function versionCompare(PackageInterface $a, PackageInterface $b, string } /** - * @param int[] $literals - * @param string $requiredPackage - * @return int[] + * @param non-empty-list $literals + * @return non-empty-list */ public function selectPreferredPackages(Pool $pool, array $literals, ?string $requiredPackage = null): array { @@ -118,8 +117,8 @@ public function selectPreferredPackages(Pool $pool, array $literals, ?string $re } /** - * @param int[] $literals - * @return array + * @param non-empty-list $literals + * @return non-empty-array> */ protected function groupLiteralsByName(Pool $pool, array $literals): array { @@ -164,7 +163,7 @@ public function compareByPriority(Pool $pool, BasePackage $a, BasePackage $b, ?s // for replacers not replacing each other, put a higher prio on replacing // packages with the same vendor as the required package - if ($requiredPackage && false !== ($pos = strpos($requiredPackage, '/'))) { + if ($requiredPackage !== null && false !== ($pos = strpos($requiredPackage, '/'))) { $requiredVendor = substr($requiredPackage, 0, $pos); $aIsSameVendor = strpos($a->getName(), $requiredVendor) === 0; @@ -205,8 +204,8 @@ protected function replaces(BasePackage $source, BasePackage $target): bool } /** - * @param int[] $literals - * @return int[] + * @param list $literals + * @return list */ protected function pruneToBestVersion(Pool $pool, array $literals): array { @@ -252,8 +251,8 @@ protected function pruneToBestVersion(Pool $pool, array $literals): array * * If no package is a local alias, nothing happens * - * @param int[] $literals - * @return int[] + * @param list $literals + * @return list */ protected function pruneRemoteAliases(Pool $pool, array $literals): array { diff --git a/src/Composer/DependencyResolver/GenericRule.php b/src/Composer/DependencyResolver/GenericRule.php index f7cf7f23c32d..64dd7a21507e 100644 --- a/src/Composer/DependencyResolver/GenericRule.php +++ b/src/Composer/DependencyResolver/GenericRule.php @@ -46,7 +46,10 @@ public function getLiterals(): array */ public function getHash() { - $data = unpack('ihash', md5(implode(',', $this->literals), true)); + $data = unpack('ihash', (string) hash(\PHP_VERSION_ID > 80100 ? 'xxh3' : 'sha1', implode(',', $this->literals), true)); + if (false === $data) { + throw new \RuntimeException('Failed unpacking: '.implode(', ', $this->literals)); + } return $data['hash']; } diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 70603b867a26..d77a211396b8 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -104,36 +104,71 @@ public function getNewLockPackages(bool $devMode, bool $updateMirrors = false): { $packages = []; foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { - if (!$package instanceof AliasPackage) { - // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is - // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed - if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { - foreach ($this->presentMap as $presentPackage) { - if ($package->getName() === $presentPackage->getName() && $package->getVersion() === $presentPackage->getVersion()) { - if ($presentPackage->getSourceReference() && $presentPackage->getSourceType() === $package->getSourceType()) { - $package->setSourceDistReferences($presentPackage->getSourceReference()); - // if the dist url is not one of those handled gracefully by setSourceDistReferences then we should overwrite it with the old one - if ($package->getDistUrl() !== null && !Preg::isMatch('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { - $package->setDistUrl($presentPackage->getDistUrl()); - } - $package->setDistType($presentPackage->getDistType()); - if ($package instanceof Package) { - $package->setDistSha1Checksum($presentPackage->getDistSha1Checksum()); - } - } - if ($presentPackage->getReleaseDate() !== null && $package instanceof Package) { - $package->setReleaseDate($presentPackage->getReleaseDate()); - } - } - } - } - $packages[] = $package; + if ($package instanceof AliasPackage) { + continue; } + + // if we're just updating mirrors we need to reset everything to the same as currently "present" packages' references to keep the lock file as-is + if ($updateMirrors === true && !array_key_exists(spl_object_hash($package), $this->presentMap)) { + $package = $this->updateMirrorAndUrls($package); + } + + $packages[] = $package; } return $packages; } + /** + * Try to return the original package from presentMap with updated URLs/mirrors + * + * If the type of source/dist changed, then we do not update those and keep them as they were + */ + private function updateMirrorAndUrls(BasePackage $package): BasePackage + { + foreach ($this->presentMap as $presentPackage) { + if ($package->getName() !== $presentPackage->getName()) { + continue; + } + + if ($package->getVersion() !== $presentPackage->getVersion()) { + continue; + } + + if ($presentPackage->getSourceReference() === null) { + continue; + } + + if ($presentPackage->getSourceType() !== $package->getSourceType()) { + continue; + } + + if ($presentPackage instanceof Package) { + $presentPackage->setSourceUrl($package->getSourceUrl()); + $presentPackage->setSourceMirrors($package->getSourceMirrors()); + } + + // if the dist type changed, we only update the source url/mirrors + if ($presentPackage->getDistType() !== $package->getDistType()) { + return $presentPackage; + } + + // update dist url if it is in a known format + if ( + $package->getDistUrl() !== null + && $presentPackage->getDistReference() !== null + && Preg::isMatch('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl()) + ) { + $presentPackage->setDistUrl(Preg::replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $presentPackage->getDistReference(), $package->getDistUrl())); + } + $presentPackage->setDistMirrors($package->getDistMirrors()); + + return $presentPackage; + } + + return $package; + } + /** * Checks which of the given aliases from composer.json are actually in use for the lock file * @param list $aliases diff --git a/src/Composer/DependencyResolver/MultiConflictRule.php b/src/Composer/DependencyResolver/MultiConflictRule.php index 4826489d258a..a6109475503d 100644 --- a/src/Composer/DependencyResolver/MultiConflictRule.php +++ b/src/Composer/DependencyResolver/MultiConflictRule.php @@ -19,11 +19,11 @@ */ class MultiConflictRule extends Rule { - /** @var list */ + /** @var non-empty-list */ protected $literals; /** - * @param list $literals + * @param non-empty-list $literals */ public function __construct(array $literals, $reason, $reasonData) { @@ -40,7 +40,7 @@ public function __construct(array $literals, $reason, $reasonData) } /** - * @return list + * @return non-empty-list */ public function getLiterals(): array { @@ -52,7 +52,10 @@ public function getLiterals(): array */ public function getHash() { - $data = unpack('ihash', md5('c:'.implode(',', $this->literals), true)); + $data = unpack('ihash', (string) hash(\PHP_VERSION_ID > 80100 ? 'xxh3' : 'sha1', 'c:'.implode(',', $this->literals), true)); + if (false === $data) { + throw new \RuntimeException('Failed unpacking: '.implode(', ', $this->literals)); + } return $data['hash']; } diff --git a/src/Composer/DependencyResolver/PolicyInterface.php b/src/Composer/DependencyResolver/PolicyInterface.php index 928e7e0a9733..b4511d091c74 100644 --- a/src/Composer/DependencyResolver/PolicyInterface.php +++ b/src/Composer/DependencyResolver/PolicyInterface.php @@ -26,8 +26,8 @@ interface PolicyInterface public function versionCompare(PackageInterface $a, PackageInterface $b, string $operator): bool; /** - * @param int[] $literals - * @return int[] + * @param non-empty-list $literals + * @return non-empty-list */ public function selectPreferredPackages(Pool $pool, array $literals, ?string $requiredPackage = null): array; } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index d84d40906671..241acf97b8ce 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -40,7 +40,7 @@ class PoolBuilder { /** * @var int[] - * @phpstan-var array + * @phpstan-var array, BasePackage::STABILITY_*> */ private $acceptableStabilities; /** @@ -95,7 +95,7 @@ class PoolBuilder */ private $loadedPerRepo = []; /** - * @var BasePackage[] + * @var array */ private $packages = []; /** @@ -106,6 +106,10 @@ class PoolBuilder private $updateAllowList = []; /** @var array> */ private $skippedLoad = []; + /** @var list */ + private $ignoredTypes = []; + /** @var list|null */ + private $allowedTypes = null; /** * If provided, only these package names are loaded @@ -149,7 +153,7 @@ class PoolBuilder /** * @param int[] $acceptableStabilities array of stability => BasePackage::STABILITY_* value - * @phpstan-param array $acceptableStabilities + * @phpstan-param array, BasePackage::STABILITY_*> $acceptableStabilities * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array $stabilityFlags * @param array[] $rootAliases @@ -170,6 +174,26 @@ public function __construct(array $acceptableStabilities, array $stabilityFlags, $this->temporaryConstraints = $temporaryConstraints; } + /** + * Packages of those types are ignored + * + * @param list $types + */ + public function setIgnoredTypes(array $types): void + { + $this->ignoredTypes = $types; + } + + /** + * Only packages of those types are allowed if set to non-null + * + * @param list|null $types + */ + public function setAllowedTypes(?array $types): void + { + $this->allowedTypes = $types; + } + /** * @param RepositoryInterface[] $repositories */ @@ -177,10 +201,14 @@ public function buildPool(array $repositories, Request $request): Pool { $this->restrictedPackagesList = $request->getRestrictedPackages() !== null ? array_flip($request->getRestrictedPackages()) : null; - if ($request->getUpdateAllowList()) { + if (\count($request->getUpdateAllowList()) > 0) { $this->updateAllowList = $request->getUpdateAllowList(); $this->warnAboutNonMatchingUpdateAllowList($request); + if (null === $request->getLockedRepository()) { + throw new \LogicException('No lock repo present and yet a partial update was requested.'); + } + foreach ($request->getLockedRepository()->getPackages() as $lockedPackage) { if (!$this->isUpdateAllowed($lockedPackage)) { // remember which packages we skipped loading remote content for in this partial update @@ -247,7 +275,7 @@ public function buildPool(array $repositories, Request $request): Pool } } - while (!empty($this->packagesToLoad)) { + while (\count($this->packagesToLoad) > 0) { $this->loadPackagesMarkedForLoading($request, $repositories); } @@ -279,7 +307,7 @@ public function buildPool(array $repositories, Request $request): Pool } } - if ($this->eventDispatcher) { + if ($this->eventDispatcher !== null) { $prePoolCreateEvent = new PrePoolCreateEvent( PluginEvents::PRE_POOL_CREATE, $repositories, @@ -389,7 +417,7 @@ private function loadPackagesMarkedForLoading(Request $request, array $repositor $this->packagesToLoad = []; foreach ($repositories as $repoIndex => $repository) { - if (empty($packageBatch)) { + if (0 === \count($packageBatch)) { break; } @@ -406,6 +434,10 @@ private function loadPackagesMarkedForLoading(Request $request, array $repositor } foreach ($result['packages'] as $package) { $this->loadedPerRepo[$repoIndex][$package->getName()][$package->getVersion()] = $package; + + if (in_array($package->getType(), $this->ignoredTypes, true) || ($this->allowedTypes !== null && !in_array($package->getType(), $this->allowedTypes, true))) { + continue; + } $this->loadPackage($request, $repositories, $package, !isset($this->pathRepoUnlocked[$package->getName()])); } } @@ -471,7 +503,7 @@ private function loadPackage(Request $request, array $repositories, BasePackage if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { $skippedRootRequires = $this->getSkippedRootRequires($request, $require); - if ($request->getUpdateAllowTransitiveRootDependencies() || !$skippedRootRequires) { + if ($request->getUpdateAllowTransitiveRootDependencies() || 0 === \count($skippedRootRequires)) { $this->unlockPackage($request, $repositories, $require); $this->markPackageNameForLoading($request, $require, $linkConstraint); } else { @@ -500,7 +532,7 @@ private function loadPackage(Request $request, array $repositories, BasePackage if (isset($this->loadedPackages[$replace], $this->skippedLoad[$replace])) { $skippedRootRequires = $this->getSkippedRootRequires($request, $replace); - if ($request->getUpdateAllowTransitiveRootDependencies() || !$skippedRootRequires) { + if ($request->getUpdateAllowTransitiveRootDependencies() || 0 === \count($skippedRootRequires)) { $this->unlockPackage($request, $repositories, $replace); // the replaced package only needs to be loaded if something else requires it $this->markPackageNameForLoadingIfRequired($request, $replace); @@ -587,7 +619,13 @@ private function isUpdateAllowed(BasePackage $package): bool private function warnAboutNonMatchingUpdateAllowList(Request $request): void { + if (null === $request->getLockedRepository()) { + throw new \LogicException('No lock repo present and yet a partial update was requested.'); + } + foreach ($this->updateAllowList as $pattern) { + $matchedPlatformPackage = false; + $patternRegexp = BasePackage::packageNameToRegexp($pattern); // update pattern matches a locked package? => all good foreach ($request->getLockedRepository()->getPackages() as $package) { @@ -598,10 +636,16 @@ private function warnAboutNonMatchingUpdateAllowList(Request $request): void // update pattern matches a root require? => all good, probably a new package foreach ($request->getRequires() as $packageName => $constraint) { if (Preg::isMatch($patternRegexp, $packageName)) { + if (PlatformRepository::isPlatformPackage($packageName)) { + $matchedPlatformPackage = true; + continue; + } continue 2; } } - if (strpos($pattern, '*') !== false) { + if ($matchedPlatformPackage) { + $this->io->writeError('Pattern "' . $pattern . '" listed for update matches platform packages, but these cannot be updated by Composer.'); + } elseif (strpos($pattern, '*') !== false) { $this->io->writeError('Pattern "' . $pattern . '" listed for update does not match any locked packages.'); } else { $this->io->writeError('Package "' . $pattern . '" listed for update is not locked.'); diff --git a/src/Composer/DependencyResolver/PoolOptimizer.php b/src/Composer/DependencyResolver/PoolOptimizer.php index c53891fb63e8..3de9e037b409 100644 --- a/src/Composer/DependencyResolver/PoolOptimizer.php +++ b/src/Composer/DependencyResolver/PoolOptimizer.php @@ -207,7 +207,7 @@ private function optimizeByIdenticalDependencies(Request $request, Pool $pool): $groupHashParts[] = 'require:' . (string) $requireConstraint; } - if ($package->getReplaces()) { + if (\count($package->getReplaces()) > 0) { foreach ($package->getReplaces() as $link) { if (CompilingMatcher::match($link->getConstraint(), Constraint::OP_EQ, $package->getVersion())) { // Use the same hash part as the regular require hash because that's what the replacement does @@ -224,7 +224,7 @@ private function optimizeByIdenticalDependencies(Request $request, Pool $pool): } } - if (!$groupHashParts) { + if (0 === \count($groupHashParts)) { continue; } @@ -371,7 +371,7 @@ private function keepPackage(BasePackage $package, array $identicalDefinitionsPe */ private function optimizeImpossiblePackagesAway(Request $request, Pool $pool): void { - if (count($request->getLockedPackages()) === 0) { + if (\count($request->getLockedPackages()) === 0) { return; } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index cf2cb381e327..fa84ae0c015e 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -25,6 +25,7 @@ use Composer\Semver\Constraint\ConstraintInterface; use Composer\Package\Version\VersionParser; use Composer\Repository\PlatformRepository; +use Composer\Semver\Constraint\MultiConstraint; /** * Represents a problem detected while solving dependencies @@ -79,7 +80,7 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = array_merge(...array_reverse($this->reasons)); - if (count($reasons) === 1) { + if (\count($reasons) === 1) { reset($reasons); $rule = current($reasons); @@ -92,14 +93,65 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, $constraint = $reasonData['constraint']; $packages = $pool->whatProvides($packageName, $constraint); - if (count($packages) === 0) { + if (\count($packages) === 0) { return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $isVerbose, $packageName, $constraint)); } } + usort($reasons, function (Rule $rule1, Rule $rule2) use ($pool) { + $rule1Prio = $this->getRulePriority($rule1); + $rule2Prio = $this->getRulePriority($rule2); + if ($rule1Prio !== $rule2Prio) { + return $rule2Prio - $rule1Prio; + } + + return $this->getSortableString($pool, $rule1) <=> $this->getSortableString($pool, $rule2); + }); + return self::formatDeduplicatedRules($reasons, ' ', $repositorySet, $request, $pool, $isVerbose, $installedMap, $learnedPool); } + private function getSortableString(Pool $pool, Rule $rule): string + { + switch ($rule->getReason()) { + case Rule::RULE_ROOT_REQUIRE: + return $rule->getReasonData()['packageName']; + case Rule::RULE_FIXED: + return (string) $rule->getReasonData()['package']; + case Rule::RULE_PACKAGE_CONFLICT: + case Rule::RULE_PACKAGE_REQUIRES: + return $rule->getSourcePackage($pool) . '//' . $rule->getReasonData()->getPrettyString($rule->getSourcePackage($pool)); + case Rule::RULE_PACKAGE_SAME_NAME: + case Rule::RULE_PACKAGE_ALIAS: + case Rule::RULE_PACKAGE_INVERSE_ALIAS: + return (string) $rule->getReasonData(); + case Rule::RULE_LEARNED: + return implode('-', $rule->getLiterals()); + } + + throw new \LogicException('Unknown rule type: '.$rule->getReason()); + } + + private function getRulePriority(Rule $rule): int + { + switch ($rule->getReason()) { + case Rule::RULE_FIXED: + return 3; + case Rule::RULE_ROOT_REQUIRE: + return 2; + case Rule::RULE_PACKAGE_CONFLICT: + case Rule::RULE_PACKAGE_REQUIRES: + return 1; + case Rule::RULE_PACKAGE_SAME_NAME: + case Rule::RULE_LEARNED: + case Rule::RULE_PACKAGE_ALIAS: + case Rule::RULE_PACKAGE_INVERSE_ALIAS: + return 0; + } + + throw new \LogicException('Unknown rule type: '.$rule->getReason()); + } + /** * @param Rule[] $rules * @param array $installedMap A map of all present packages @@ -136,7 +188,7 @@ public static function formatDeduplicatedRules(array $rules, string $indent, Rep if (!$isVerbose) { $versions = self::condenseVersionList($versions, 1); } - if (count($versions) > 1) { + if (\count($versions) > 1) { // remove the s from requires/conflicts to correct grammar $message = Preg::replace('{^(%s%s (?:require|conflict))s}', '$1', $message); $result[] = sprintf($message, $package, '['.implode(', ', $versions).']'); @@ -224,16 +276,21 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req $ext = substr($packageName, 4); $msg = "- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but '; - $version = self::getPlatformPackageVersion($pool, $packageName, phpversion($ext) ?: '0'); + $version = self::getPlatformPackageVersion($pool, $packageName, phpversion($ext) === false ? '0' : phpversion($ext)); if (null === $version) { + $providersStr = self::getProvidersList($repositorySet, $packageName, 5); + if ($providersStr !== null) { + $providersStr = "\n\n Alternatively you can require one of these packages that provide the extension (or parts of it):\n$providersStr"; + } + if (extension_loaded($ext)) { return [ $msg, - 'the '.$packageName.' package is disabled by your platform config. Enable it again with "composer config platform.'.$packageName.' --unset".', + 'the '.$packageName.' package is disabled by your platform config. Enable it again with "composer config platform.'.$packageName.' --unset".' . $providersStr, ]; } - return [$msg, 'it is missing from your system. Install or enable PHP\'s '.$ext.' extension.']; + return [$msg, 'it is missing from your system. Install or enable PHP\'s '.$ext.' extension.' . $providersStr]; } return [$msg, 'it has the wrong version installed ('.$version.').']; @@ -247,7 +304,12 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req return ["- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error]; } - return ["- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.']; + $providersStr = self::getProvidersList($repositorySet, $packageName, 5); + if ($providersStr !== null) { + $providersStr = "\n\n Alternatively you can require one of these packages that provide the library (or parts of it):\n$providersStr"; + } + + return ["- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.'.$providersStr]; } } @@ -262,9 +324,21 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req } } + if ($constraint instanceof Constraint && $constraint->getOperator() === Constraint::STR_OP_EQ && Preg::isMatch('{^dev-.*#.*}', $constraint->getPrettyString())) { + $newConstraint = Preg::replace('{ +as +([^,\s|]+)$}', '', $constraint->getPrettyString()); + $packages = $repositorySet->findPackages($packageName, new MultiConstraint([ + new Constraint(Constraint::STR_OP_EQ, $newConstraint), + new Constraint(Constraint::STR_OP_EQ, str_replace('#', '+', $newConstraint)) + ], false)); + if (\count($packages) > 0) { + return ["- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).'. The # character in branch names is replaced by a + character. Make sure to require it as "'.str_replace('#', '+', $constraint->getPrettyString()).'".']; + } + } + // first check if the actual requested package is found in normal conditions // if so it must mean it is rejected by another constraint than the one given here - if ($packages = $repositorySet->findPackages($packageName, $constraint)) { + $packages = $repositorySet->findPackages($packageName, $constraint); + if (\count($packages) > 0) { $rootReqs = $repositorySet->getRootRequires(); if (isset($rootReqs[$packageName])) { $filtered = array_filter($packages, static function ($p) use ($rootReqs, $packageName): bool { @@ -285,7 +359,7 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req } } - if ($lockedPackage) { + if ($lockedPackage !== null) { $fixedConstraint = new Constraint('==', $lockedPackage->getVersion()); $filtered = array_filter($packages, static function ($p) use ($fixedConstraint): bool { return $fixedConstraint->matches(new Constraint('==', $p->getVersion())); @@ -299,7 +373,7 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req return !$p->getRepository() instanceof LockArrayRepository; }); - if (!$nonLockedPackages) { + if (0 === \count($nonLockedPackages)) { return ["- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages, $isVerbose, $pool, $constraint).' in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file.']; } @@ -307,9 +381,11 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req } // check if the package is found when bypassing stability checks - if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { + $packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES); + if (\count($packages) > 0) { // we must first verify if a valid package would be found in a lower priority repository - if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { + $allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES); + if (\count($allReposPackages) > 0) { return self::computeCheckForLowerPrioRepo($pool, $isVerbose, $packageName, $packages, $allReposPackages, 'minimum-stability', $constraint); } @@ -317,9 +393,11 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req } // check if the package is found when bypassing the constraint and stability checks - if ($packages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { + $packages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES); + if (\count($packages) > 0) { // we must first verify if a valid package would be found in a lower priority repository - if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { + $allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES); + if (\count($allReposPackages) > 0) { return self::computeCheckForLowerPrioRepo($pool, $isVerbose, $packageName, $packages, $allReposPackages, 'constraint', $constraint); } @@ -349,17 +427,8 @@ public static function getMissingPackageReason(RepositorySet $repositorySet, Req return ["- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.']; } - if ($providers = $repositorySet->getProviders($packageName)) { - $maxProviders = 20; - $providersStr = implode(array_map(static function ($p): string { - $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : ''; - - return ' - '.$p['name'].$description."\n"; - }, count($providers) > $maxProviders + 1 ? array_slice($providers, 0, $maxProviders) : $providers)); - if (count($providers) > $maxProviders + 1) { - $providersStr .= ' ... and '.(count($providers) - $maxProviders).' more.'."\n"; - } - + $providersStr = self::getProvidersList($repositorySet, $packageName, 15); + if ($providersStr !== null) { return ["- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it:\n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement."]; } @@ -377,12 +446,12 @@ public static function getPackageList(array $packages, bool $isVerbose, ?Pool $p foreach ($packages as $package) { $prepared[$package->getName()]['name'] = $package->getPrettyName(); $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion().($package instanceof AliasPackage ? ' (alias of '.$package->getAliasOf()->getPrettyVersion().')' : ''); - if ($pool && $constraint) { + if ($pool !== null && $constraint !== null) { foreach ($pool->getRemovedVersions($package->getName(), $constraint) as $version => $prettyVersion) { $prepared[$package->getName()]['versions'][$version] = $prettyVersion; } } - if ($pool && $useRemovedVersionGroup) { + if ($pool !== null && $useRemovedVersionGroup) { foreach ($pool->getRemovedVersionsByPackage(spl_object_hash($package)) as $version => $prettyVersion) { $prepared[$package->getName()]['versions'][$version] = $prettyVersion; } @@ -418,7 +487,7 @@ private static function getPlatformPackageVersion(Pool $pool, string $packageNam { $available = $pool->whatProvides($packageName); - if (count($available)) { + if (\count($available) > 0) { $selected = null; foreach ($available as $pkg) { if ($pkg->getRepository() instanceof PlatformRepository) { @@ -443,7 +512,7 @@ private static function getPlatformPackageVersion(Pool $pool, string $packageNam $version = $selected->getPrettyVersion(); $extra = $selected->getExtra(); if ($selected instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { - $version .= '; ' . str_replace('Package ', '', $selected->getDescription()); + $version .= '; ' . str_replace('Package ', '', (string) $selected->getDescription()); } } else { return null; @@ -504,8 +573,8 @@ private static function hasMultipleNames(array $packages): bool } /** - * @param PackageInterface[] $higherRepoPackages - * @param PackageInterface[] $allReposPackages + * @param non-empty-array $higherRepoPackages + * @param non-empty-array $allReposPackages * @return array{0: string, 1: string} */ private static function computeCheckForLowerPrioRepo(Pool $pool, bool $isVerbose, string $packageName, array $higherRepoPackages, array $allReposPackages, string $reason, ?ConstraintInterface $constraint = null): array @@ -522,12 +591,14 @@ private static function computeCheckForLowerPrioRepo(Pool $pool, bool $isVerbose } } - if ($higherRepoPackages) { + assert(null !== $nextRepo); + + if (\count($higherRepoPackages) > 0) { $topPackage = reset($higherRepoPackages); if ($topPackage instanceof RootPackageInterface) { return [ "- Root composer.json requires $packageName".self::constraintToText($constraint).', it is ', - 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' from '.$nextRepo->getRepoName().' but '.$topPackage->getPrettyName().' is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance.', + 'satisfiable by '.self::getPackageList($nextRepoPackages, $isVerbose, $pool, $constraint).' from '.$nextRepo->getRepoName().' but '.$topPackage->getPrettyName().' '.$topPackage->getPrettyVersion().' is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance.', ]; } } @@ -570,6 +641,25 @@ protected static function constraintToText(?ConstraintInterface $constraint = nu return ' ' . $constraint->getPrettyString() . ' (exact version match: ' . (count($versions) > 1 ? implode(', ', array_slice($versions, 0, -1)) . ' or ' . end($versions) : $versions[0]) . ')'; } - return $constraint ? ' '.$constraint->getPrettyString() : ''; + return $constraint !== null ? ' '.$constraint->getPrettyString() : ''; + } + + private static function getProvidersList(RepositorySet $repositorySet, string $packageName, int $maxProviders): ?string + { + $providers = $repositorySet->getProviders($packageName); + if (\count($providers) > 0) { + $providersStr = implode(array_map(static function ($p): string { + $description = $p['description'] !== '' && $p['description'] !== null ? ' '.substr($p['description'], 0, 100) : ''; + + return ' - '.$p['name'].$description."\n"; + }, count($providers) > $maxProviders + 1 ? array_slice($providers, 0, $maxProviders) : $providers)); + if (count($providers) > $maxProviders + 1) { + $providersStr .= ' ... and '.(count($providers) - $maxProviders).' more.'."\n"; + } + + return $providersStr; + } + + return null; } } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 300093f043fc..b11f4e1f2eb0 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -190,7 +190,7 @@ public function getFixedOrLockedPackages(): array } /** - * @return array + * @return ($packageIds is true ? array : array) * * @TODO look into removing the packageIds option, the only place true is used * is for the installed map in the solver problems. @@ -201,7 +201,7 @@ public function getPresentMap(bool $packageIds = false): array { $presentMap = []; - if ($this->lockedRepository) { + if ($this->lockedRepository !== null) { foreach ($this->lockedRepository->getPackages() as $package) { $presentMap[$packageIds ? $package->getId() : spl_object_hash($package)] = $package; } @@ -215,7 +215,7 @@ public function getPresentMap(bool $packageIds = false): array } /** - * @return BasePackage[] + * @return array */ public function getFixedPackagesMap(): array { diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 1b2f1aa951e6..8dde02b37af0 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -137,12 +137,12 @@ public function enable(): void public function isDisabled(): bool { - return (bool) (($this->bitfield & (255 << self::BITFIELD_DISABLED)) >> self::BITFIELD_DISABLED); + return 0 !== (($this->bitfield & (255 << self::BITFIELD_DISABLED)) >> self::BITFIELD_DISABLED); } public function isEnabled(): bool { - return !(($this->bitfield & (255 << self::BITFIELD_DISABLED)) >> self::BITFIELD_DISABLED); + return 0 === (($this->bitfield & (255 << self::BITFIELD_DISABLED)) >> self::BITFIELD_DISABLED); } abstract public function isAssertion(): bool; @@ -153,7 +153,7 @@ public function isCausedByLock(RepositorySet $repositorySet, Request $request, P if (PlatformRepository::isPlatformPackage($this->getReasonData()->getTarget())) { return false; } - if ($request->getLockedRepository()) { + if ($request->getLockedRepository() !== null) { foreach ($request->getLockedRepository()->getPackages() as $package) { if ($package->getName() === $this->getReasonData()->getTarget()) { if ($pool->isUnacceptableFixedOrLockedPackage($package)) { @@ -176,7 +176,7 @@ public function isCausedByLock(RepositorySet $repositorySet, Request $request, P if (PlatformRepository::isPlatformPackage($this->getReasonData()['packageName'])) { return false; } - if ($request->getLockedRepository()) { + if ($request->getLockedRepository() !== null) { foreach ($request->getLockedRepository()->getPackages() as $package) { if ($package->getName() === $this->getReasonData()['packageName']) { if ($pool->isUnacceptableFixedOrLockedPackage($package)) { @@ -215,7 +215,7 @@ public function getSourcePackage(Pool $pool): BasePackage return $package2; case self::RULE_PACKAGE_REQUIRES: - $sourceLiteral = array_shift($literals); + $sourceLiteral = $literals[0]; $sourcePackage = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($sourceLiteral)); return $sourcePackage; @@ -240,14 +240,14 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, $constraint = $reasonData['constraint']; $packages = $pool->whatProvides($packageName, $constraint); - if (!$packages) { + if (0 === \count($packages)) { return 'No package found to satisfy root composer.json require '.$packageName.' '.$constraint->getPrettyString(); } $packagesNonAlias = array_values(array_filter($packages, static function ($p): bool { return !($p instanceof AliasPackage); })); - if (count($packagesNonAlias) === 1) { + if (\count($packagesNonAlias) === 1) { $package = $packagesNonAlias[0]; if ($request->isLockedPackage($package)) { return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion()." and an update of this package was not requested."; @@ -305,6 +305,7 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, return $package2->getPrettyString().' conflicts with '.$conflictTarget.'.'; case self::RULE_PACKAGE_REQUIRES: + assert(\count($literals) > 0); $sourceLiteral = array_shift($literals); $sourcePackage = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($sourceLiteral)); $reasonData = $this->getReasonData(); @@ -315,7 +316,7 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, } $text = $reasonData->getPrettyString($sourcePackage); - if ($requires) { + if (\count($requires) > 0) { $text .= ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $requires, $isVerbose, $reasonData->getConstraint()) . '.'; } else { $targetName = $reasonData->getTarget(); @@ -333,19 +334,18 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, $package = $pool->literalToPackage($literal); $packageNames[$package->getName()] = true; } + unset($literal); $replacedName = $this->getReasonData(); - if (count($packageNames) > 1) { - $reason = null; - + if (\count($packageNames) > 1) { if (!isset($packageNames[$replacedName])) { - $reason = 'They '.(count($literals) === 2 ? 'both' : 'all').' replace '.$replacedName.' and thus cannot coexist.'; + $reason = 'They '.(\count($literals) === 2 ? 'both' : 'all').' replace '.$replacedName.' and thus cannot coexist.'; } else { $replacerNames = $packageNames; unset($replacerNames[$replacedName]); $replacerNames = array_keys($replacerNames); - if (count($replacerNames) === 1) { + if (\count($replacerNames) === 1) { $reason = $replacerNames[0] . ' replaces '; } else { $reason = '['.implode(', ', $replacerNames).'] replace '; @@ -363,7 +363,7 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, } } - if ($installedPackages && $removablePackages) { + if (\count($installedPackages) > 0 && \count($removablePackages) > 0) { return $this->formatPackagesUnique($pool, $removablePackages, $isVerbose, null, true).' cannot be installed as that would require removing '.$this->formatPackagesUnique($pool, $installedPackages, $isVerbose, null, true).'. '.$reason; } @@ -381,7 +381,7 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, // } $learnedString = ' (conflict analysis result)'; - if (count($literals) === 1) { + if (\count($literals) === 1) { $ruleText = $pool->literalToPrettyString($literals[0], $installedMap); } else { $groups = []; @@ -397,7 +397,7 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, } $ruleTexts = []; foreach ($groups as $group => $packages) { - $ruleTexts[] = $group . (count($packages) > 1 ? ' one of' : '').' ' . $this->formatPackagesUnique($pool, $packages, $isVerbose); + $ruleTexts[] = $group . (\count($packages) > 1 ? ' one of' : '').' ' . $this->formatPackagesUnique($pool, $packages, $isVerbose); } $ruleText = implode(' | ', $ruleTexts); @@ -439,14 +439,13 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, } /** - * @param array $packages An array containing packages or literals + * @param array $literalsOrPackages An array containing packages or literals */ - protected function formatPackagesUnique(Pool $pool, array $packages, bool $isVerbose, ?ConstraintInterface $constraint = null, bool $useRemovedVersionGroup = false): string + protected function formatPackagesUnique(Pool $pool, array $literalsOrPackages, bool $isVerbose, ?ConstraintInterface $constraint = null, bool $useRemovedVersionGroup = false): string { - foreach ($packages as $index => $package) { - if (!\is_object($package)) { - $packages[$index] = $pool->literalToPackage($package); - } + $packages = []; + foreach ($literalsOrPackages as $package) { + $packages[] = \is_object($package) ? $package : $pool->literalToPackage($package); } return Problem::getPackageList($packages, $isVerbose, $pool, $constraint, $useRemovedVersionGroup); diff --git a/src/Composer/DependencyResolver/Rule2Literals.php b/src/Composer/DependencyResolver/Rule2Literals.php index 17bfaf8ce158..33d0ed0be026 100644 --- a/src/Composer/DependencyResolver/Rule2Literals.php +++ b/src/Composer/DependencyResolver/Rule2Literals.php @@ -43,7 +43,7 @@ public function __construct(int $literal1, int $literal2, $reason, $reasonData) } /** - * @return list + * @return non-empty-list */ public function getLiterals(): array { diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index 4a406238a26f..cca9eb12383b 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -179,7 +179,7 @@ public function getPrettyString(?RepositorySet $repositorySet = null, ?Request $ foreach ($this->rules as $type => $rules) { $string .= str_pad(self::TYPES[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose) : $rule)."\n"; + $string .= ($repositorySet !== null && $request !== null && $pool !== null ? $rule->getPrettyString($repositorySet, $request, $pool, $isVerbose) : $rule)."\n"; } $string .= "\n\n"; } diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 69d1191349aa..08c874ff7afd 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -56,7 +56,7 @@ public function __construct(PolicyInterface $policy, Pool $pool) * * @phpstan-param ReasonData $reasonData */ - protected function createRequireRule(BasePackage $package, array $providers, $reason, $reasonData = null): ?Rule + protected function createRequireRule(BasePackage $package, array $providers, $reason, $reasonData): ?Rule { $literals = [-$package->id]; @@ -77,7 +77,7 @@ protected function createRequireRule(BasePackage $package, array $providers, $re * The rule is (A|B|C) with A, B and C different packages. If the given * set of packages is empty an impossible rule is generated. * - * @param BasePackage[] $packages The set of packages to choose from + * @param non-empty-array $packages The set of packages to choose from * @param Rule::RULE_* $reason A RULE_* constant describing the reason for * generating this rule * @param mixed $reasonData Additional data like the root require or fix request info @@ -109,7 +109,7 @@ protected function createInstallOneOfRule(array $packages, $reason, $reasonData) * * @phpstan-param ReasonData $reasonData */ - protected function createRule2Literals(BasePackage $issuer, BasePackage $provider, $reason, $reasonData = null): ?Rule + protected function createRule2Literals(BasePackage $issuer, BasePackage $provider, $reason, $reasonData): ?Rule { // ignore self conflict if ($issuer === $provider) { @@ -120,7 +120,7 @@ protected function createRule2Literals(BasePackage $issuer, BasePackage $provide } /** - * @param BasePackage[] $packages + * @param non-empty-array $packages * @param Rule::RULE_* $reason A RULE_* constant * @param mixed $reasonData * @@ -151,7 +151,7 @@ protected function createMultiConflictRule(array $packages, $reason, $reasonData */ private function addRule($type, ?Rule $newRule = null): void { - if (!$newRule) { + if (null === $newRule) { return; } @@ -276,7 +276,7 @@ protected function addRulesForRequest(Request $request, PlatformRequirementFilte } $packages = $this->pool->whatProvides($packageName, $constraint); - if ($packages) { + if (\count($packages) > 0) { foreach ($packages as $package) { $this->addRulesForPackage($package, $platformRequirementFilter); } @@ -307,7 +307,7 @@ protected function addRulesForRootAliases(PlatformRequirementFilterInterface $pl public function getRulesFor(Request $request, ?PlatformRequirementFilterInterface $platformRequirementFilter = null): RuleSet { - $platformRequirementFilter = $platformRequirementFilter ?: PlatformRequirementFilterFactory::ignoreNothing(); + $platformRequirementFilter = $platformRequirementFilter ?? PlatformRequirementFilterFactory::ignoreNothing(); $this->addRulesForRequest($request, $platformRequirementFilter); diff --git a/src/Composer/DependencyResolver/RuleSetIterator.php b/src/Composer/DependencyResolver/RuleSetIterator.php index 2bf67f55bf11..3b8383d47d47 100644 --- a/src/Composer/DependencyResolver/RuleSetIterator.php +++ b/src/Composer/DependencyResolver/RuleSetIterator.php @@ -14,7 +14,7 @@ /** * @author Nils Adermann - * @implements \Iterator + * @implements \Iterator */ class RuleSetIterator implements \Iterator { diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 373e8325d526..b8aa847d1ce8 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -43,7 +43,7 @@ class Solver /** @var int */ protected $propagateIndex; - /** @var mixed[] */ + /** @var array, int}> */ protected $branches = []; /** @var Problem[] */ protected $problems = []; @@ -109,7 +109,7 @@ private function makeAssertionRuleDecisions(): void $conflict = $this->decisions->decisionRule($literal); - if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { + if (RuleSet::TYPE_PACKAGE === $conflict->getType()) { $problem = new Problem(); $problem->addRule($rule); @@ -164,7 +164,7 @@ protected function checkForRootRequireProblems(Request $request, PlatformRequire $constraint = $platformRequirementFilter->filterConstraint($packageName, $constraint); } - if (!$this->pool->whatProvides($packageName, $constraint)) { + if (0 === \count($this->pool->whatProvides($packageName, $constraint))) { $problem = new Problem(); $problem->addRule(new GenericRule([], Rule::RULE_ROOT_REQUIRE, ['packageName' => $packageName, 'constraint' => $constraint])); $this->problems[] = $problem; @@ -174,7 +174,7 @@ protected function checkForRootRequireProblems(Request $request, PlatformRequire public function solve(Request $request, ?PlatformRequirementFilterInterface $platformRequirementFilter = null): LockTransaction { - $platformRequirementFilter = $platformRequirementFilter ?: PlatformRequirementFilterFactory::ignoreNothing(); + $platformRequirementFilter = $platformRequirementFilter ?? PlatformRequirementFilterFactory::ignoreNothing(); $this->setupFixedMap($request); @@ -199,7 +199,7 @@ public function solve(Request $request, ?PlatformRequirementFilterInterface $pla $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); - if ($this->problems) { + if (\count($this->problems) > 0) { throw new SolverProblemsException($this->problems, $this->learnedPool); } @@ -227,7 +227,7 @@ protected function propagate(int $level): ?Rule $this->propagateIndex++; - if ($conflict) { + if ($conflict !== null) { return $conflict; } } @@ -257,7 +257,7 @@ private function revert(int $level): void $this->propagateIndex = \count($this->decisions); } - while (!empty($this->branches) && $this->branches[\count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) { + while (\count($this->branches) > 0 && $this->branches[\count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) { array_pop($this->branches); } } @@ -274,10 +274,8 @@ private function revert(int $level): void * rule (always unit) and re-propagate. * * returns the new solver level or 0 if unsolvable - * - * @param string|int $literal */ - private function setPropagateLearn(int $level, $literal, Rule $rule): int + private function setPropagateLearn(int $level, int $literal, Rule $rule): int { $level++; @@ -291,7 +289,9 @@ private function setPropagateLearn(int $level, $literal, Rule $rule): int } if ($level === 1) { - return $this->analyzeUnsolvable($rule); + $this->analyzeUnsolvable($rule); + + return 0; } // conflict @@ -322,7 +322,7 @@ private function setPropagateLearn(int $level, $literal, Rule $rule): int } /** - * @param int[] $decisionQueue + * @param non-empty-list $decisionQueue */ private function selectAndInstall(int $level, array $decisionQueue, Rule $rule): int { @@ -332,7 +332,7 @@ private function selectAndInstall(int $level, array $decisionQueue, Rule $rule): $selectedLiteral = array_shift($literals); // if there are multiple candidates, then branch - if (\count($literals)) { + if (\count($literals) > 0) { $this->branches[] = [$literals, $level]; } @@ -349,7 +349,8 @@ protected function analyze(int $level, Rule $rule): array $num = 0; $l1num = 0; $seen = []; - $learnedLiterals = [null]; + $learnedLiteral = null; + $otherLearnedLiterals = []; $decisionId = \count($this->decisions); @@ -382,7 +383,7 @@ protected function analyze(int $level, Rule $rule): array $num++; } else { // not level1 or conflict level, add to new rule - $learnedLiterals[] = $literal; + $otherLearnedLiterals[] = $literal; if ($l > $ruleLevel) { $ruleLevel = $l; @@ -423,16 +424,14 @@ protected function analyze(int $level, Rule $rule): array if ($literal < 0) { $this->testFlagLearnedPositiveLiteral = true; } - $learnedLiterals[0] = -$literal; + $learnedLiteral = -$literal; - if (!$l1num) { + if (0 === $l1num) { break 2; } - foreach ($learnedLiterals as $i => $learnedLiteral) { - if ($i !== 0) { - unset($seen[abs($learnedLiteral)]); - } + foreach ($otherLearnedLiterals as $otherLiteral) { + unset($seen[abs($otherLiteral)]); } // only level 1 marks left $l1num++; @@ -442,24 +441,24 @@ protected function analyze(int $level, Rule $rule): array $rule = $decision[Decisions::DECISION_REASON]; if ($rule instanceof MultiConflictRule) { - // there is only ever exactly one positive decision in a multiconflict rule - foreach ($rule->getLiterals() as $literal) { - if (!isset($seen[abs($literal)]) && $this->decisions->satisfy(-$literal)) { + // there is only ever exactly one positive decision in a MultiConflictRule + foreach ($rule->getLiterals() as $ruleLiteral) { + if (!isset($seen[abs($ruleLiteral)]) && $this->decisions->satisfy(-$ruleLiteral)) { $this->learnedPool[\count($this->learnedPool) - 1][] = $rule; - $l = $this->decisions->decisionLevel($literal); + $l = $this->decisions->decisionLevel($ruleLiteral); if (1 === $l) { $l1num++; } elseif ($level === $l) { $num++; } else { // not level1 or conflict level, add to new rule - $learnedLiterals[] = $literal; + $otherLearnedLiterals[] = $ruleLiteral; if ($l > $ruleLevel) { $ruleLevel = $l; } } - $seen[abs($literal)] = true; + $seen[abs($ruleLiteral)] = true; break; } } @@ -475,15 +474,16 @@ protected function analyze(int $level, Rule $rule): array $why = \count($this->learnedPool) - 1; - if (null === $learnedLiterals[0]) { + if (null === $learnedLiteral) { throw new SolverBugException( "Did not find a learnable literal in analyzed rule $analyzedRule." ); } - $newRule = new GenericRule($learnedLiterals, Rule::RULE_LEARNED, $why); + array_unshift($otherLearnedLiterals, $learnedLiteral); + $newRule = new GenericRule($otherLearnedLiterals, Rule::RULE_LEARNED, $why); - return [$learnedLiterals[0], $ruleLevel, $newRule, $why]; + return [$learnedLiteral, $ruleLevel, $newRule, $why]; } /** @@ -516,7 +516,7 @@ private function analyzeUnsolvableRule(Problem $problem, Rule $conflictRule, arr $problem->addRule($conflictRule); } - private function analyzeUnsolvable(Rule $conflictRule): int + private function analyzeUnsolvable(Rule $conflictRule): void { $problem = new Problem(); $problem->addRule($conflictRule); @@ -539,10 +539,10 @@ private function analyzeUnsolvable(Rule $conflictRule): int } foreach ($this->decisions as $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; + $decisionLiteral = $decision[Decisions::DECISION_LITERAL]; // skip literals that are not in this rule - if (!isset($seen[abs($literal)])) { + if (!isset($seen[abs($decisionLiteral)])) { continue; } @@ -552,7 +552,6 @@ private function analyzeUnsolvable(Rule $conflictRule): int $this->analyzeUnsolvableRule($problem, $why, $ruleSeen); $literals = $why->getLiterals(); - foreach ($literals as $literal) { // skip the one true literal if ($this->decisions->satisfy($literal)) { @@ -561,8 +560,6 @@ private function analyzeUnsolvable(Rule $conflictRule): int $seen[abs($literal)] = true; } } - - return 0; } private function runSat(): void @@ -586,9 +583,7 @@ private function runSat(): void if (1 === $level) { $conflictRule = $this->propagate($level); if (null !== $conflictRule) { - if ($this->analyzeUnsolvable($conflictRule)) { - continue; - } + $this->analyzeUnsolvable($conflictRule); return; } @@ -612,7 +607,7 @@ private function runSat(): void } } - if ($noneSatisfied && \count($decisionQueue)) { + if ($noneSatisfied && \count($decisionQueue) > 0) { // if any of the options in the decision queue are fixed, only use those $prunedQueue = []; foreach ($decisionQueue as $literal) { @@ -620,12 +615,12 @@ private function runSat(): void $prunedQueue[] = $literal; } } - if (!empty($prunedQueue)) { + if (\count($prunedQueue) > 0) { $decisionQueue = $prunedQueue; } } - if ($noneSatisfied && \count($decisionQueue)) { + if ($noneSatisfied && \count($decisionQueue) > 0) { $oLevel = $level; $level = $this->selectAndInstall($level, $decisionQueue, $rule); @@ -719,7 +714,7 @@ private function runSat(): void } // minimization step - if (\count($this->branches)) { + if (\count($this->branches) > 0) { $lastLiteral = null; $lastLevel = null; $lastBranchIndex = 0; @@ -729,7 +724,7 @@ private function runSat(): void [$literals, $l] = $this->branches[$i]; foreach ($literals as $offset => $literal) { - if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) { + if ($literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) { $lastLiteral = $literal; $lastBranchIndex = $i; $lastBranchOffset = $offset; @@ -738,7 +733,8 @@ private function runSat(): void } } - if ($lastLiteral) { + if ($lastLiteral !== null) { + assert($lastLevel !== null); unset($this->branches[$lastBranchIndex][self::BRANCH_LITERALS][$lastBranchOffset]); $level = $lastLevel; diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index 5870c0cae0f3..bd76e4fa3fc0 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -38,7 +38,7 @@ public function __construct(array $problems, array $learnedPool) $this->problems = $problems; $this->learnedPool = $learnedPool; - parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', self::ERROR_DEPENDENCY_RESOLUTION_FAILED); + parent::__construct('Failed resolving dependencies with '.\count($problems).' problems, call getPrettyString to get formatted details', self::ERROR_DEPENDENCY_RESOLUTION_FAILED); } public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, bool $isVerbose, bool $isDevExtraction = false): string @@ -63,11 +63,11 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, } $hints = []; - if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) { + if (!$isDevExtraction && (str_contains($text, 'could not be found') || str_contains($text, 'no matching package found'))) { $hints[] = "Potential causes:\n - A typo in the package name\n - The package is not available in a stable-enough version according to your minimum-stability setting\n see for more details.\n - It's a private package and you forgot to add a custom repository to find it\n\nRead for further common problems."; } - if (!empty($missingExtensions)) { + if (\count($missingExtensions) > 0) { $hints[] = $this->createExtensionHint($missingExtensions); } @@ -75,17 +75,17 @@ public function getPrettyString(RepositorySet $repositorySet, Request $request, $hints[] = "Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions."; } - if (strpos($text, 'found composer-plugin-api[2.0.0] but it does not match') && strpos($text, '- ocramius/package-versions')) { + if (str_contains($text, 'found composer-plugin-api[2.0.0] but it does not match') && str_contains($text, '- ocramius/package-versions')) { $hints[] = "ocramius/package-versions only provides support for Composer 2 in 1.8+, which requires PHP 7.4.\nIf you can not upgrade PHP you can require composer/package-versions-deprecated to resolve this with PHP 7.0+."; } if (!class_exists('PHPUnit\Framework\TestCase', false)) { - if (strpos($text, 'found composer-plugin-api[2.0.0] but it does not match')) { + if (str_contains($text, 'found composer-plugin-api[2.0.0] but it does not match')) { $hints[] = "You are using Composer 2, which some of your plugins seem to be incompatible with. Make sure you update your plugins or report a plugin-issue to ask them to support Composer 2."; } } - if ($hints) { + if (\count($hints) > 0) { $text .= "\n" . implode("\n\n", $hints); } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index ef6860c11712..3443dd768f13 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -123,7 +123,7 @@ protected function calculateOperations(): array $visited = []; $processed = []; - while (!empty($stack)) { + while (\count($stack) > 0) { $package = array_pop($stack); if (isset($processed[spl_object_hash($package)])) { @@ -283,17 +283,18 @@ private function movePluginsToFront(array $operations): array continue; } - $isDownloadsModifyingPlugin = $package->getType() === 'composer-plugin' && ($extra = $package->getExtra()) && isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true; + $extra = $package->getExtra(); + $isDownloadsModifyingPlugin = $package->getType() === 'composer-plugin' && isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true; // is this a downloads modifying plugin or a dependency of one? - if ($isDownloadsModifyingPlugin || count(array_intersect($package->getNames(), $dlModifyingPluginRequires))) { + if ($isDownloadsModifyingPlugin || \count(array_intersect($package->getNames(), $dlModifyingPluginRequires)) > 0) { // get the package's requires, but filter out any platform requirements $requires = array_filter(array_keys($package->getRequires()), static function ($req): bool { return !PlatformRepository::isPlatformPackage($req); }); // is this a plugin with no meaningful dependencies? - if ($isDownloadsModifyingPlugin && !count($requires)) { + if ($isDownloadsModifyingPlugin && 0 === \count($requires)) { // plugins with no dependencies go to the very front array_unshift($dlModifyingPluginsNoDeps, $op); } else { @@ -311,14 +312,14 @@ private function movePluginsToFront(array $operations): array $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; // is this a plugin or a dependency of a plugin? - if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { + if ($isPlugin || \count(array_intersect($package->getNames(), $pluginRequires)) > 0) { // get the package's requires, but filter out any platform requirements $requires = array_filter(array_keys($package->getRequires()), static function ($req): bool { return !PlatformRepository::isPlatformPackage($req); }); // is this a plugin with no meaningful dependencies? - if ($isPlugin && !count($requires)) { + if ($isPlugin && 0 === \count($requires)) { // plugins with no dependencies go to the very front array_unshift($pluginsNoDeps, $op); } else { diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 6de51ee58ba2..ff132e28b419 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -68,7 +68,7 @@ public function install(PackageInterface $package, string $path, bool $output = } do { - $temporaryDir = $vendorDir.'/composer/'.substr(md5(uniqid('', true)), 0, 8); + $temporaryDir = $vendorDir.'/composer/'.bin2hex(random_bytes(4)); } while (is_dir($temporaryDir)); $this->addCleanupPath($package, $temporaryDir); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 2a2847f33f16..5f3b24279612 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -27,6 +27,7 @@ use Composer\Plugin\PreFileDownloadEvent; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; +use Composer\Util\Http\Response; use Composer\Util\Platform; use Composer\Util\Silencer; use Composer\Util\HttpDownloader; @@ -99,9 +100,9 @@ public function __construct(IOInterface $io, Config $config, HttpDownloader $htt $this->httpDownloader = $httpDownloader; $this->cache = $cache; $this->process = $process ?? new ProcessExecutor($io); - $this->filesystem = $filesystem ?: new Filesystem($this->process); + $this->filesystem = $filesystem ?? new Filesystem($this->process); - if ($this->cache && $this->cache->gcIsNecessary()) { + if ($this->cache !== null && $this->cache->gcIsNecessary()) { $this->io->writeError('Running cache garbage collection', true, IOInterface::VERY_VERBOSE); $this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize')); } @@ -120,12 +121,12 @@ public function getInstallationSource(): string */ public function download(PackageInterface $package, string $path, ?PackageInterface $prevPackage = null, bool $output = true): PromiseInterface { - if (!$package->getDistUrl()) { + if (null === $package->getDistUrl()) { throw new \InvalidArgumentException('The given package is missing url information'); } $cacheKeyGenerator = static function (PackageInterface $package, $key): string { - $cacheKey = sha1($key); + $cacheKey = hash('sha1', $key); return $package->getName().'/'.$cacheKey.'.'.$package->getDistType(); }; @@ -152,21 +153,15 @@ public function download(PackageInterface $package, string $path, ?PackageInterf $this->filesystem->ensureDirectoryExists($path); $this->filesystem->ensureDirectoryExists(dirname($fileName)); - $io = $this->io; - $cache = $this->cache; - $httpDownloader = $this->httpDownloader; - $eventDispatcher = $this->eventDispatcher; - $filesystem = $this->filesystem; - $accept = null; $reject = null; - $download = function () use ($io, $output, $httpDownloader, $cache, $cacheKeyGenerator, $eventDispatcher, $package, $fileName, &$urls, &$accept, &$reject) { + $download = function () use ($output, $cacheKeyGenerator, $package, $fileName, &$urls, &$accept, &$reject) { $url = reset($urls); $index = key($urls); - if ($eventDispatcher) { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed'], 'package', $package); - $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + if ($this->eventDispatcher !== null) { + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $url['processed'], 'package', $package); + $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); if ($preFileDownloadEvent->getCustomCacheKey() !== null) { $url['cacheKey'] = $cacheKeyGenerator($package, $preFileDownloadEvent->getCustomCacheKey()); } elseif ($preFileDownloadEvent->getProcessedUrl() !== $url['processed']) { @@ -181,27 +176,27 @@ public function download(PackageInterface $package, string $path, ?PackageInterf $cacheKey = $url['cacheKey']; // use from cache if it is present and has a valid checksum or we have no checksum to check against - if ($cache && (!$checksum || $checksum === $cache->sha1($cacheKey)) && $cache->copyTo($cacheKey, $fileName)) { + if ($this->cache !== null && ($checksum === null || $checksum === '' || $checksum === $this->cache->sha1($cacheKey)) && $this->cache->copyTo($cacheKey, $fileName)) { if ($output) { - $io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache", true, IOInterface::VERY_VERBOSE); + $this->io->writeError(" - Loading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") from cache", true, IOInterface::VERY_VERBOSE); } // mark the file as having been written in cache even though it is only read from cache, so that if // the cache is corrupt the archive will be deleted and the next attempt will re-download it // see https://github.com/composer/composer/issues/10028 - if (!$cache->isReadOnly()) { + if (!$this->cache->isReadOnly()) { $this->lastCacheWrites[$package->getName()] = $cacheKey; } $result = \React\Promise\resolve($fileName); } else { if ($output) { - $io->writeError(" - Downloading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - Downloading " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); } - $result = $httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions()) + $result = $this->httpDownloader->addCopy($url['processed'], $fileName, $package->getTransportOptions()) ->then($accept, $reject); } - return $result->then(static function ($result) use ($fileName, $checksum, $url, $package, $eventDispatcher): string { + return $result->then(function ($result) use ($fileName, $checksum, $url, $package): string { // in case of retry, the first call's Promise chain finally calls this twice at the end, // once with $result being the returned $fileName from $accept, and then once for every // failed request with a null result, which can be skipped. @@ -214,31 +209,35 @@ public function download(PackageInterface $package, string $path, ?PackageInterf .' directory is writable and you have internet connectivity'); } - if ($checksum && hash_file('sha1', $fileName) !== $checksum) { + if ($checksum !== null && $checksum !== '' && hash_file('sha1', $fileName) !== $checksum) { throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url['base'].')'); } - if ($eventDispatcher) { + if ($this->eventDispatcher !== null) { $postFileDownloadEvent = new PostFileDownloadEvent(PluginEvents::POST_FILE_DOWNLOAD, $fileName, $checksum, $url['processed'], 'package', $package); - $eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); + $this->eventDispatcher->dispatch($postFileDownloadEvent->getName(), $postFileDownloadEvent); } return $fileName; }); }; - $accept = function ($response) use ($cache, $package, $fileName, &$urls): string { + $accept = function (Response $response) use ($package, $fileName, &$urls): string { $url = reset($urls); $cacheKey = $url['cacheKey']; - FileDownloader::$downloadMetadata[$package->getName()] = @filesize($fileName) ?: $response->getHeader('Content-Length') ?: '?'; + $fileSize = @filesize($fileName); + if (false === $fileSize) { + $fileSize = $response->getHeader('Content-Length') ?? '?'; + } + FileDownloader::$downloadMetadata[$package->getName()] = $fileSize; if (Platform::getEnv('GITHUB_ACTIONS') !== false && Platform::getEnv('COMPOSER_TESTS_ARE_RUNNING') === false) { FileDownloader::$responseHeaders[$package->getName()] = $response->getHeaders(); } - if ($cache && !$cache->isReadOnly()) { + if ($this->cache !== null && !$this->cache->isReadOnly()) { $this->lastCacheWrites[$package->getName()] = $cacheKey; - $cache->copyFrom($cacheKey, $fileName); + $this->cache->copyFrom($cacheKey, $fileName); } $response->collect(); @@ -246,10 +245,10 @@ public function download(PackageInterface $package, string $path, ?PackageInterf return $fileName; }; - $reject = function ($e) use ($io, &$urls, $download, $fileName, $package, &$retries, $filesystem) { + $reject = function ($e) use (&$urls, $download, $fileName, $package, &$retries) { // clean up if (file_exists($fileName)) { - $filesystem->unlink($fileName); + $this->filesystem->unlink($fileName); } $this->clearLastCacheWrite($package); @@ -263,7 +262,7 @@ public function download(PackageInterface $package, string $path, ?PackageInterf if ($e instanceof TransportException) { // if we got an http response with a proper code, then requesting again will probably not help, abort - if ((0 !== $e->getCode() && !in_array($e->getCode(), [500, 502, 503, 504])) || !$retries) { + if (0 !== $e->getCode() && !in_array($e->getCode(), [500, 502, 503, 504], true)) { $retries = 0; } } @@ -274,7 +273,7 @@ public function download(PackageInterface $package, string $path, ?PackageInterf $urls = []; } - if ($retries) { + if ($retries > 0) { usleep(500000); $retries--; @@ -282,12 +281,12 @@ public function download(PackageInterface $package, string $path, ?PackageInterf } array_shift($urls); - if ($urls) { - if ($io->isDebug()) { - $io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); - $io->writeError(' Trying the next URL for '.$package->getName()); + if (\count($urls) > 0) { + if ($this->io->isDebug()) { + $this->io->writeError(' Failed downloading '.$package->getName().': ['.get_class($e).'] '.$e->getCode().': '.$e->getMessage()); + $this->io->writeError(' Trying the next URL for '.$package->getName()); } else { - $io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); + $this->io->writeError(' Failed downloading '.$package->getName().', trying the next URL ('.$e->getCode().': '.$e->getMessage().')'); } $retries = 3; @@ -328,8 +327,8 @@ public function cleanup(string $type, PackageInterface $package, string $path, ? ]; if (isset($this->additionalCleanupPaths[$package->getName()])) { - foreach ($this->additionalCleanupPaths[$package->getName()] as $path) { - $this->filesystem->remove($path); + foreach ($this->additionalCleanupPaths[$package->getName()] as $pathToClean) { + $this->filesystem->remove($pathToClean); } } @@ -351,23 +350,31 @@ public function install(PackageInterface $package, string $path, bool $output = $this->io->writeError(" - " . InstallOperation::format($package)); } - $this->filesystem->emptyDirectory($path); + $vendorDir = $this->config->get('vendor-dir'); + + // clean up the target directory, unless it contains the vendor dir, as the vendor dir contains + // the file to be installed. This is the case when installing with create-project in the current directory + // but in that case we ensure the directory is empty already in ProjectInstaller so no need to empty it here. + if (false === strpos($this->filesystem->normalizePath($vendorDir), $this->filesystem->normalizePath($path.DIRECTORY_SEPARATOR))) { + $this->filesystem->emptyDirectory($path); + } $this->filesystem->ensureDirectoryExists($path); $this->filesystem->rename($this->getFileName($package, $path), $path . '/' . $this->getDistPath($package, PATHINFO_BASENAME)); - if ($package->getBinaries()) { - // Single files can not have a mode set like files in archives - // so we make sure if the file is a binary that it is executable - foreach ($package->getBinaries() as $bin) { - if (file_exists($path . '/' . $bin) && !is_executable($path . '/' . $bin)) { - Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask()); - } + // Single files can not have a mode set like files in archives + // so we make sure if the file is a binary that it is executable + foreach ($package->getBinaries() as $bin) { + if (file_exists($path . '/' . $bin) && !is_executable($path . '/' . $bin)) { + Silencer::call('chmod', $path . '/' . $bin, 0777 & ~umask()); } } return \React\Promise\resolve(null); } + /** + * @param PATHINFO_EXTENSION|PATHINFO_BASENAME $component + */ protected function getDistPath(PackageInterface $package, int $component): string { return pathinfo((string) parse_url(strtr((string) $package->getDistUrl(), '\\', '/'), PHP_URL_PATH), $component); @@ -375,7 +382,7 @@ protected function getDistPath(PackageInterface $package, int $component): strin protected function clearLastCacheWrite(PackageInterface $package): void { - if ($this->cache && isset($this->lastCacheWrites[$package->getName()])) { + if ($this->cache !== null && isset($this->lastCacheWrites[$package->getName()])) { $this->cache->remove($this->lastCacheWrites[$package->getName()]); unset($this->lastCacheWrites[$package->getName()]); } @@ -389,7 +396,7 @@ protected function addCleanupPath(PackageInterface $package, string $path): void protected function removeCleanupPath(PackageInterface $package, string $path): void { if (isset($this->additionalCleanupPaths[$package->getName()])) { - $idx = array_search($path, $this->additionalCleanupPaths[$package->getName()]); + $idx = array_search($path, $this->additionalCleanupPaths[$package->getName()], true); if (false !== $idx) { unset($this->additionalCleanupPaths[$package->getName()][$idx]); } @@ -441,7 +448,7 @@ protected function getFileName(PackageInterface $package, string $path): string $extension = $package->getDistType(); } - return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . md5($package . spl_object_hash($package)) . '.' . $extension, '.'); + return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . hash('md5', $package . spl_object_hash($package)) . '.' . $extension, '.'); } /** @@ -469,7 +476,7 @@ protected function processUrl(PackageInterface $package, string $url): string throw new \RuntimeException('You must enable the openssl extension to download files via https'); } - if ($package->getDistReference()) { + if ($package->getDistReference() !== null) { $url = UrlUtil::updateDistReference($this->config, $url, $package->getDistReference()); } @@ -495,10 +502,22 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin $this->filesystem->removeDirectory($targetDir.'_compare'); } - $this->download($package, $targetDir.'_compare', null, false); + $promise = $this->download($package, $targetDir.'_compare', null, false); + $promise->then(null, function ($ex) use (&$e) { + $e = $ex; + }); $this->httpDownloader->wait(); - $this->install($package, $targetDir.'_compare', false); + if ($e !== null) { + throw $e; + } + $promise = $this->install($package, $targetDir.'_compare', false); + $promise->then(null, function ($ex) use (&$e) { + $e = $ex; + }); $this->process->wait(); + if ($e !== null) { + throw $e; + } $comparer = new Comparer(); $comparer->setSource($targetDir.'_compare'); @@ -511,8 +530,12 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin $this->io = $prevIO; - if ($e) { - throw $e; + if ($e !== null) { + if ($this->io->isDebug()) { + throw $e; + } + + return 'Failed to detect changes: ['.get_class($e).'] '.$e->getMessage(); } $output = trim($output); diff --git a/src/Composer/Downloader/FossilDownloader.php b/src/Composer/Downloader/FossilDownloader.php index 6634771dc0e1..60c6ed49da32 100644 --- a/src/Composer/Downloader/FossilDownloader.php +++ b/src/Composer/Downloader/FossilDownloader.php @@ -12,10 +12,12 @@ namespace Composer\Downloader; +use Composer\Util\Platform; use React\Promise\PromiseInterface; use Composer\Package\PackageInterface; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; +use RuntimeException; /** * @author BohwaZ @@ -38,22 +40,13 @@ protected function doInstall(PackageInterface $package, string $path, string $ur // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); - $url = ProcessExecutor::escape($url); - $ref = ProcessExecutor::escape($package->getSourceReference()); $repoFile = $path . '.fossil'; + $realPath = Platform::realpath($path); + $this->io->writeError("Cloning ".$package->getSourceReference()); - $command = sprintf('fossil clone -- %s %s', $url, ProcessExecutor::escape($repoFile)); - if (0 !== $this->process->execute($command, $ignoredOutput)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } - $command = sprintf('fossil open --nested -- %s', ProcessExecutor::escape($repoFile)); - if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } - $command = sprintf('fossil update -- %s', $ref); - if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } + $this->execute(['fossil', 'clone', '--', $url, $repoFile]); + $this->execute(['fossil', 'open', '--nested', '--', $repoFile], $realPath); + $this->execute(['fossil', 'update', '--', (string) $package->getSourceReference()], $realPath); return \React\Promise\resolve(null); } @@ -66,17 +59,15 @@ protected function doUpdate(PackageInterface $initial, PackageInterface $target, // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); - $ref = ProcessExecutor::escape($target->getSourceReference()); $this->io->writeError(" Updating to ".$target->getSourceReference()); if (!$this->hasMetadataRepository($path)) { throw new \RuntimeException('The .fslckout file is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } - $command = sprintf('fossil pull && fossil up %s', $ref); - if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } + $realPath = Platform::realpath($path); + $this->execute(['fossil', 'pull'], $realPath); + $this->execute(['fossil', 'up', (string) $target->getSourceReference()], $realPath); return \React\Promise\resolve(null); } @@ -90,7 +81,7 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin return null; } - $this->process->execute('fossil changes', $output, realpath($path)); + $this->process->execute(['fossil', 'changes'], $output, Platform::realpath($path)); $output = trim($output); @@ -102,11 +93,7 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin */ protected function getCommitLogs(string $fromReference, string $toReference, string $path): string { - $command = sprintf('fossil timeline -t ci -W 0 -n 0 before %s', ProcessExecutor::escape($toReference)); - - if (0 !== $this->process->execute($command, $output, realpath($path))) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); - } + $this->execute(['fossil', 'timeline', '-t', 'ci', '-W', '0', '-n', '0', 'before', $toReference], Platform::realpath($path), $output); $log = ''; $match = '/\d\d:\d\d:\d\d\s+\[' . $toReference . '\]/'; @@ -121,6 +108,17 @@ protected function getCommitLogs(string $fromReference, string $toReference, str return $log; } + /** + * @param non-empty-list $command + * @throws \RuntimeException + */ + private function execute(array $command, ?string $cwd = null, ?string &$output = null): void + { + if (0 !== $this->process->execute($command, $output, $cwd)) { + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); + } + } + /** * @inheritDoc */ diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 0840219d0fe6..e54d95473f66 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -67,13 +67,13 @@ protected function doDownload(PackageInterface $package, string $path, string $u GitUtil::cleanEnv(); - $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', $url).'/'; + $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($url)).'/'; $gitVersion = GitUtil::getVersion($this->process); // --dissociate option is only available since git 2.3.0-rc0 if ($gitVersion && version_compare($gitVersion, '2.3.0-rc0', '>=') && Cache::isUsable($cachePath)) { $this->io->writeError(" - Syncing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ") into cache"); - $this->io->writeError(sprintf(' Cloning to cache at %s', ProcessExecutor::escape($cachePath)), true, IOInterface::DEBUG); + $this->io->writeError(sprintf(' Cloning to cache at %s', $cachePath), true, IOInterface::DEBUG); $ref = $package->getSourceReference(); if ($this->gitUtil->fetchRefOrSyncMirror($url, $cachePath, $ref, $package->getPrettyVersion()) && is_dir($cachePath)) { $this->cachedPackages[$package->getId()][$ref] = true; @@ -92,26 +92,32 @@ protected function doInstall(PackageInterface $package, string $path, string $ur { GitUtil::cleanEnv(); $path = $this->normalizePath($path); - $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', $url).'/'; + $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($url)).'/'; $ref = $package->getSourceReference(); - $flag = Platform::isWindows() ? '/D ' : ''; if (!empty($this->cachedPackages[$package->getId()][$ref])) { $msg = "Cloning ".$this->getShortHash($ref).' from cache'; - $cloneFlags = '--dissociate --reference %cachePath% '; + $cloneFlags = ['--dissociate', '--reference', $cachePath]; $transportOptions = $package->getTransportOptions(); if (isset($transportOptions['git']['single_use_clone']) && $transportOptions['git']['single_use_clone']) { - $cloneFlags = ''; + $cloneFlags = []; } - $command = - 'git clone --no-checkout %cachePath% %path% ' . $cloneFlags - . '&& cd '.$flag.'%path% ' - . '&& git remote set-url origin -- %sanitizedUrl% && git remote add composer -- %sanitizedUrl%'; + $commands = [ + array_merge(['git', 'clone', '--no-checkout', $cachePath, $path], $cloneFlags), + ['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'], + ['git', 'remote', 'add', 'composer', '--', '%sanitizedUrl%'], + ]; } else { $msg = "Cloning ".$this->getShortHash($ref); - $command = 'git clone --no-checkout -- %url% %path% && cd '.$flag.'%path% && git remote add composer -- %url% && git fetch composer && git remote set-url origin -- %sanitizedUrl% && git remote set-url composer -- %sanitizedUrl%'; + $commands = [ + array_merge(['git', 'clone', '--no-checkout', '--', '%url%', $path]), + ['git', 'remote', 'add', 'composer', '--', '%url%'], + ['git', 'fetch', 'composer'], + ['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'], + ['git', 'remote', 'set-url', 'composer', '--', '%sanitizedUrl%'], + ]; if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) { throw new \RuntimeException('The required git reference for '.$package->getName().' is not in cache and network is disabled, aborting'); } @@ -119,20 +125,8 @@ protected function doInstall(PackageInterface $package, string $path, string $ur $this->io->writeError($msg); - $commandCallable = static function (string $url) use ($path, $command, $cachePath): string { - return str_replace( - ['%url%', '%path%', '%cachePath%', '%sanitizedUrl%'], - [ - ProcessExecutor::escape($url), - ProcessExecutor::escape($path), - ProcessExecutor::escape($cachePath), - ProcessExecutor::escape(Preg::replace('{://([^@]+?):(.+?)@}', '://', $url)), - ], - $command - ); - }; + $this->gitUtil->runCommands($commands, $url, $path, true); - $this->gitUtil->runCommand($commandCallable, $url, $path, true); $sourceUrl = $package->getSourceUrl(); if ($url !== $sourceUrl && $sourceUrl !== null) { $this->updateOriginUrl($path, $sourceUrl); @@ -161,15 +155,15 @@ protected function doUpdate(PackageInterface $initial, PackageInterface $target, throw new \RuntimeException('The .git directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } - $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', $url).'/'; + $cachePath = $this->config->get('cache-vcs-dir').'/'.Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($url)).'/'; $ref = $target->getSourceReference(); if (!empty($this->cachedPackages[$target->getId()][$ref])) { $msg = "Checking out ".$this->getShortHash($ref).' from cache'; - $command = '(git rev-parse --quiet --verify %ref% || (git remote set-url composer -- %cachePath% && git fetch composer && git fetch --tags composer)) && git remote set-url composer -- %sanitizedUrl%'; + $remoteUrl = $cachePath; } else { $msg = "Checking out ".$this->getShortHash($ref); - $command = '(git remote set-url composer -- %url% && git rev-parse --quiet --verify %ref% || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- %sanitizedUrl%'; + $remoteUrl = '%url%'; if (Platform::getEnv('COMPOSER_DISABLE_NETWORK')) { throw new \RuntimeException('The required git reference for '.$target->getName().' is not in cache and network is disabled, aborting'); } @@ -177,20 +171,19 @@ protected function doUpdate(PackageInterface $initial, PackageInterface $target, $this->io->writeError($msg); - $commandCallable = static function ($url) use ($ref, $command, $cachePath): string { - return str_replace( - ['%url%', '%ref%', '%cachePath%', '%sanitizedUrl%'], - [ - ProcessExecutor::escape($url), - ProcessExecutor::escape($ref.'^{commit}'), - ProcessExecutor::escape($cachePath), - ProcessExecutor::escape(Preg::replace('{://([^@]+?):(.+?)@}', '://', $url)), - ], - $command - ); - }; + if (0 !== $this->process->execute(['git', 'rev-parse', '--quiet', '--verify', $ref.'^{commit}'], $output, $path)) { + $commands = [ + ['git', 'remote', 'set-url', 'composer', '--', $remoteUrl], + ['git', 'fetch', 'composer'], + ['git', 'fetch', '--tags', 'composer'], + ]; + + $this->gitUtil->runCommands($commands, $url, $path); + } + + $command = ['git', 'remote', 'set-url', 'composer', '--', '%sanitizedUrl%']; + $this->gitUtil->runCommands([$command], $url, $path); - $this->gitUtil->runCommand($commandCallable, $url, $path); if ($newRef = $this->updateToCommit($target, $path, (string) $ref, $target->getPrettyVersion())) { if ($target->getDistReference() === $target->getSourceReference()) { $target->setDistReference($newRef); @@ -200,7 +193,7 @@ protected function doUpdate(PackageInterface $initial, PackageInterface $target, $updateOriginUrl = false; if ( - 0 === $this->process->execute('git remote -v', $output, $path) + 0 === $this->process->execute(['git', 'remote', '-v'], $output, $path) && Preg::isMatch('{^origin\s+(?P\S+)}m', $output, $originMatch) && Preg::isMatch('{^composer\s+(?P\S+)}m', $output, $composerMatch) ) { @@ -225,9 +218,9 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin return null; } - $command = 'git status --porcelain --untracked-files=no'; + $command = ['git', 'status', '--porcelain', '--untracked-files=no']; if (0 !== $this->process->execute($command, $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } $output = trim($output); @@ -243,9 +236,9 @@ public function getUnpushedChanges(PackageInterface $package, string $path): ?st return null; } - $command = 'git show-ref --head -d'; + $command = ['git', 'show-ref', '--head', '-d']; if (0 !== $this->process->execute($command, $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } $refs = trim($output); @@ -255,7 +248,7 @@ public function getUnpushedChanges(PackageInterface $package, string $path): ?st } $headRef = $match[1]; - if (!Preg::isMatchAllStrictGroups('{^'.$headRef.' refs/heads/(.+)$}mi', $refs, $matches)) { + if (!Preg::isMatchAllStrictGroups('{^'.preg_quote($headRef).' refs/heads/(.+)$}mi', $refs, $matches)) { // not on a branch, we are either on a not-modified tag or some sort of detached head, so skip this return null; } @@ -294,9 +287,9 @@ public function getUnpushedChanges(PackageInterface $package, string $path): ?st $unpushedChanges = null; } foreach ($remoteBranches as $remoteBranch) { - $command = sprintf('git diff --name-status %s...%s --', $remoteBranch, $branch); + $command = ['git', 'diff', '--name-status', $remoteBranch.'...'.$branch, '--']; if (0 !== $this->process->execute($command, $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } $output = trim($output); @@ -310,12 +303,12 @@ public function getUnpushedChanges(PackageInterface $package, string $path): ?st // first pass and we found unpushed changes, fetch from all remotes to make sure we have up to date // remotes and then try again as outdated remotes can sometimes cause false-positives if ($unpushedChanges && $i === 0) { - $this->process->execute('git fetch --all', $output, $path); + $this->process->execute(['git', 'fetch', '--all'], $output, $path); // update list of refs after fetching - $command = 'git show-ref --head -d'; + $command = ['git', 'show-ref', '--head', '-d']; if (0 !== $this->process->execute($command, $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } $refs = trim($output); } @@ -425,7 +418,7 @@ protected function reapplyChanges(string $path): void if (!empty($this->hasStashedChanges[$path])) { unset($this->hasStashedChanges[$path]); $this->io->writeError(' Re-applying stashed changes'); - if (0 !== $this->process->execute('git stash pop', $output, $path)) { + if (0 !== $this->process->execute(['git', 'stash', 'pop'], $output, $path)) { throw new \RuntimeException("Failed to apply stashed changes:\n\n".$this->process->getErrorOutput()); } } @@ -441,18 +434,29 @@ protected function reapplyChanges(string $path): void */ protected function updateToCommit(PackageInterface $package, string $path, string $reference, string $prettyVersion): ?string { - $force = !empty($this->hasDiscardedChanges[$path]) || !empty($this->hasStashedChanges[$path]) ? '-f ' : ''; + $force = !empty($this->hasDiscardedChanges[$path]) || !empty($this->hasStashedChanges[$path]) ? ['-f'] : []; // This uses the "--" sequence to separate branch from file parameters. // // Otherwise git tries the branch name as well as file name. // If the non-existent branch is actually the name of a file, the file // is checked out. - $template = 'git checkout '.$force.'%s -- && git reset --hard %1$s --'; + $branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $prettyVersion); + /** + * @var \Closure(non-empty-list): bool $execute + * @phpstan-ignore varTag.nativeType + */ + $execute = function (array $command) use (&$output, $path) { + /** @var non-empty-list $command */ + $output = ''; + + return 0 === $this->process->execute($command, $output, $path); + }; + $branches = null; - if (0 === $this->process->execute('git branch -r', $output, $path)) { + if ($execute(['git', 'branch', '-r'])) { $branches = $output; } @@ -462,8 +466,10 @@ protected function updateToCommit(PackageInterface $package, string $path, strin && null !== $branches && Preg::isMatch('{^\s+composer/'.preg_quote($reference).'$}m', $branches) ) { - $command = sprintf('git checkout '.$force.'-B %s %s -- && git reset --hard %2$s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$reference)); - if (0 === $this->process->execute($command, $output, $path)) { + $command1 = array_merge(['git', 'checkout'], $force, ['-B', $branch, 'composer/'.$reference, '--']); + $command2 = ['git', 'reset', '--hard', 'composer/'.$reference, '--']; + + if ($execute($command1) && $execute($command2)) { return null; } } @@ -475,17 +481,18 @@ protected function updateToCommit(PackageInterface $package, string $path, strin $branch = 'v' . $branch; } - $command = sprintf('git checkout %s --', ProcessExecutor::escape($branch)); - $fallbackCommand = sprintf('git checkout '.$force.'-B %s %s --', ProcessExecutor::escape($branch), ProcessExecutor::escape('composer/'.$branch)); - $resetCommand = sprintf('git reset --hard %s --', ProcessExecutor::escape($reference)); + $command = ['git', 'checkout', $branch, '--']; + $fallbackCommand = array_merge(['git', 'checkout'], $force, ['-B', $branch, 'composer/'.$branch, '--']); + $resetCommand = ['git', 'reset', '--hard', $reference, '--']; - if (0 === $this->process->execute("($command || $fallbackCommand) && $resetCommand", $output, $path)) { + if (($execute($command) || $execute($fallbackCommand)) && $execute($resetCommand)) { return null; } } - $command = sprintf($template, ProcessExecutor::escape($gitRef)); - if (0 === $this->process->execute($command, $output, $path)) { + $command1 = array_merge(['git', 'checkout'], $force, [$gitRef, '--']); + $command2 = ['git', 'reset', '--hard', $gitRef, '--']; + if ($execute($command1) && $execute($command2)) { return null; } @@ -497,12 +504,14 @@ protected function updateToCommit(PackageInterface $package, string $path, strin $exceptionExtra = "\nIt looks like the commit hash is not available in the repository, maybe ".($package->isDev() ? 'the commit was removed from the branch' : 'the tag was recreated').'? Run "composer update '.$package->getPrettyName().'" to resolve this.'; } + $command = implode(' ', $command1). ' && '.implode(' ', $command2); + throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput() . $exceptionExtra)); } protected function updateOriginUrl(string $path, string $url): void { - $this->process->execute(sprintf('git remote set-url origin -- %s', ProcessExecutor::escape($url)), $output, $path); + $this->process->execute(['git', 'remote', 'set-url', 'origin', '--', $url], $output, $path); $this->setPushUrl($path, $url); } @@ -515,7 +524,7 @@ protected function setPushUrl(string $path, string $url): void if (!in_array('ssh', $protocols, true)) { $pushUrl = 'https://' . $match[1] . '/'.$match[2].'/'.$match[3].'.git'; } - $cmd = sprintf('git remote set-url --push origin -- %s', ProcessExecutor::escape($pushUrl)); + $cmd = ['git', 'remote', 'set-url', '--push', 'origin', '--', $pushUrl]; $this->process->execute($cmd, $ignoredOutput, $path); } } @@ -526,10 +535,10 @@ protected function setPushUrl(string $path, string $url): void protected function getCommitLogs(string $fromReference, string $toReference, string $path): string { $path = $this->normalizePath($path); - $command = sprintf('git log %s..%s --pretty=format:"%%h - %%an: %%s"'.GitUtil::getNoShowSignatureFlag($this->process), ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference)); + $command = array_merge(['git', 'log', $fromReference.'..'.$toReference, '--pretty=format:%h - %an: %s'], GitUtil::getNoShowSignatureFlags($this->process)); if (0 !== $this->process->execute($command, $output, $path)) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } return $output; @@ -542,7 +551,10 @@ protected function getCommitLogs(string $fromReference, string $toReference, str protected function discardChanges(string $path): PromiseInterface { $path = $this->normalizePath($path); - if (0 !== $this->process->execute('git clean -df && git reset --hard', $output, $path)) { + if (0 !== $this->process->execute(['git', 'clean', '-df'], $output, $path)) { + throw new \RuntimeException("Could not reset changes\n\n:".$output); + } + if (0 !== $this->process->execute(['git', 'reset', '--hard'], $output, $path)) { throw new \RuntimeException("Could not reset changes\n\n:".$output); } @@ -558,7 +570,7 @@ protected function discardChanges(string $path): PromiseInterface protected function stashChanges(string $path): PromiseInterface { $path = $this->normalizePath($path); - if (0 !== $this->process->execute('git stash --include-untracked', $output, $path)) { + if (0 !== $this->process->execute(['git', 'stash', '--include-untracked'], $output, $path)) { throw new \RuntimeException("Could not stash changes\n\n:".$output); } @@ -573,7 +585,7 @@ protected function stashChanges(string $path): PromiseInterface protected function viewDiff(string $path): void { $path = $this->normalizePath($path); - if (0 !== $this->process->execute('git diff HEAD', $output, $path)) { + if (0 !== $this->process->execute(['git', 'diff', 'HEAD'], $output, $path)) { throw new \RuntimeException("Could not view diff\n\n:".$output); } diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 9037f5226d14..010219822dfd 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -31,7 +31,7 @@ protected function extract(PackageInterface $package, string $file, string $path // Try to use gunzip on *nix if (!Platform::isWindows()) { - $command = 'gzip -cd -- ' . ProcessExecutor::escape($file) . ' > ' . ProcessExecutor::escape($targetFilepath); + $command = ['sh', '-c', 'gzip -cd -- "$0" > "$1"', $file, $targetFilepath]; if (0 === $this->process->execute($command, $ignoredOutput)) { return \React\Promise\resolve(null); @@ -44,7 +44,7 @@ protected function extract(PackageInterface $package, string $file, string $path return \React\Promise\resolve(null); } - $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); + $processError = 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput(); throw new \RuntimeException($processError); } diff --git a/src/Composer/Downloader/HgDownloader.php b/src/Composer/Downloader/HgDownloader.php index b0cc9cd7da2d..4709ae35ef38 100644 --- a/src/Composer/Downloader/HgDownloader.php +++ b/src/Composer/Downloader/HgDownloader.php @@ -41,16 +41,15 @@ protected function doInstall(PackageInterface $package, string $path, string $ur { $hgUtils = new HgUtils($this->io, $this->config, $this->process); - $cloneCommand = static function (string $url) use ($path): string { - return sprintf('hg clone -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($path)); + $cloneCommand = static function (string $url) use ($path): array { + return ['hg', 'clone', '--', $url, $path]; }; $hgUtils->runCommand($cloneCommand, $url, $path); - $ref = ProcessExecutor::escape($package->getSourceReference()); - $command = sprintf('hg up -- %s', $ref); + $command = ['hg', 'up', '--', (string) $package->getSourceReference()]; if (0 !== $this->process->execute($command, $ignoredOutput, realpath($path))) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } return \React\Promise\resolve(null); @@ -70,10 +69,14 @@ protected function doUpdate(PackageInterface $initial, PackageInterface $target, throw new \RuntimeException('The .hg directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } - $command = static function ($url) use ($ref): string { - return sprintf('hg pull -- %s && hg up -- %s', ProcessExecutor::escape($url), ProcessExecutor::escape($ref)); + $command = static function ($url): array { + return ['hg', 'pull', '--', $url]; }; + $hgUtils->runCommand($command, $url, $path); + $command = static function () use ($ref): array { + return ['hg', 'up', '--', $ref]; + }; $hgUtils->runCommand($command, $url, $path); return \React\Promise\resolve(null); @@ -88,7 +91,7 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin return null; } - $this->process->execute('hg st', $output, realpath($path)); + $this->process->execute(['hg', 'st'], $output, realpath($path)); $output = trim($output); @@ -100,10 +103,10 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin */ protected function getCommitLogs(string $fromReference, string $toReference, string $path): string { - $command = sprintf('hg log -r %s:%s --style compact', ProcessExecutor::escape($fromReference), ProcessExecutor::escape($toReference)); + $command = ['hg', 'log', '-r', $fromReference.':'.$toReference, '--style', 'compact']; if (0 !== $this->process->execute($command, $output, realpath($path))) { - throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); + throw new \RuntimeException('Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput()); } return $output; diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 791521bd62bc..f71ea2568a1a 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -43,6 +43,9 @@ public function download(PackageInterface $package, string $path, ?PackageInterf { $path = Filesystem::trimTrailingSlash($path); $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot download.'); + } $realUrl = realpath($url); if (false === $realUrl || !file_exists($realUrl) || !is_dir($realUrl)) { throw new \RuntimeException(sprintf( @@ -79,7 +82,13 @@ public function install(PackageInterface $package, string $path, bool $output = { $path = Filesystem::trimTrailingSlash($path); $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot install.'); + } $realUrl = realpath($url); + if (false === $realUrl) { + throw new \RuntimeException('Failed to realpath '.$url); + } if (realpath($path) === $realUrl) { if ($output) { @@ -111,16 +120,16 @@ public function install(PackageInterface $package, string $path, bool $output = } $this->filesystem->junction($realUrl, $path); } else { - $absolutePath = $path; - if (!$this->filesystem->isAbsolutePath($absolutePath)) { - $absolutePath = Platform::getCwd() . DIRECTORY_SEPARATOR . $path; - } - $shortestPath = $this->filesystem->findShortestPath($absolutePath, $realUrl); $path = rtrim($path, "/"); if ($output) { $this->io->writeError(sprintf('Symlinking from %s', $url), false); } - if ($transportOptions['relative']) { + if ($transportOptions['relative'] === true) { + $absolutePath = $path; + if (!$this->filesystem->isAbsolutePath($absolutePath)) { + $absolutePath = Platform::getCwd() . DIRECTORY_SEPARATOR . $path; + } + $shortestPath = $this->filesystem->findShortestPath($absolutePath, $realUrl, false, true); $symfonyFilesystem->symlink($shortestPath.'/', $path); } else { $symfonyFilesystem->symlink($realUrl.'/', $path); @@ -185,13 +194,18 @@ public function remove(PackageInterface $package, string $path, bool $output = t return \React\Promise\resolve(null); } + $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot remove.'); + } + // ensure that the source path (dist url) is not the same as the install path, which // can happen when using custom installers, see https://github.com/composer/composer/pull/9116 // not using realpath here as we do not want to resolve the symlink to the original dist url // it points to $fs = new Filesystem; $absPath = $fs->isAbsolutePath($path) ? $path : Platform::getCwd() . '/' . $path; - $absDistUrl = $fs->isAbsolutePath($package->getDistUrl()) ? $package->getDistUrl() : Platform::getCwd() . '/' . $package->getDistUrl(); + $absDistUrl = $fs->isAbsolutePath($url) ? $url : Platform::getCwd() . '/' . $url; if ($fs->normalizePath($absPath) === $fs->normalizePath($absDistUrl)) { if ($output) { $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); @@ -210,11 +224,12 @@ public function getVcsReference(PackageInterface $package, string $path): ?strin { $path = Filesystem::trimTrailingSlash($path); $parser = new VersionParser; - $guesser = new VersionGuesser($this->config, $this->process, $parser); + $guesser = new VersionGuesser($this->config, $this->process, $parser, $this->io); $dumper = new ArrayDumper; $packageConfig = $dumper->dump($package); - if ($packageVersion = $guesser->guessVersion($packageConfig, $path)) { + $packageVersion = $guesser->guessVersion($packageConfig, $path); + if ($packageVersion !== null) { return $packageVersion['commit']; } @@ -226,7 +241,14 @@ public function getVcsReference(PackageInterface $package, string $path): ?strin */ protected function getInstallOperationAppendix(PackageInterface $package, string $path): string { - $realUrl = realpath($package->getDistUrl()); + $url = $package->getDistUrl(); + if (null === $url) { + throw new \RuntimeException('The package '.$package->getPrettyName().' has no dist url configured, cannot install.'); + } + $realUrl = realpath($url); + if (false === $realUrl) { + throw new \RuntimeException('Failed to realpath '.$url); + } if (realpath($path) === $realUrl) { return ': Source already present'; @@ -257,7 +279,7 @@ private function computeAllowedStrategies(array $transportOptions): array $allowedStrategies = [self::STRATEGY_SYMLINK, self::STRATEGY_MIRROR]; $mirrorPathRepos = Platform::getEnv('COMPOSER_MIRROR_PATH_REPOS'); - if ($mirrorPathRepos) { + if ((bool) $mirrorPathRepos) { $currentStrategy = self::STRATEGY_MIRROR; } diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index a4142c65599b..04e16eaffbee 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -34,13 +34,13 @@ protected function extract(PackageInterface $package, string $file, string $path // Try to use unrar on *nix if (!Platform::isWindows()) { - $command = 'unrar x -- ' . ProcessExecutor::escape($file) . ' ' . ProcessExecutor::escape($path) . ' >/dev/null && chmod -R u+w ' . ProcessExecutor::escape($path); + $command = ['sh', '-c', 'unrar x -- "$0" "$1" >/dev/null && chmod -R u+w "$1"', $file, $path]; if (0 === $this->process->execute($command, $ignoredOutput)) { return \React\Promise\resolve(null); } - $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); + $processError = 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput(); } if (!class_exists('RarArchive')) { diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index be180d63d7f6..ca88ea839467 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -59,7 +59,7 @@ protected function doInstall(PackageInterface $package, string $path, string $ur } $this->io->writeError(" Checking out ".$package->getSourceReference()); - $this->execute($package, $url, "svn co", sprintf("%s/%s", $url, $ref), null, $path); + $this->execute($package, $url, ['svn', 'co'], sprintf("%s/%s", $url, $ref), null, $path); return \React\Promise\resolve(null); } @@ -77,13 +77,13 @@ protected function doUpdate(PackageInterface $initial, PackageInterface $target, } $util = new SvnUtil($url, $this->io, $this->config, $this->process); - $flags = ""; + $flags = []; if (version_compare($util->binaryVersion(), '1.7.0', '>=')) { - $flags .= ' --ignore-ancestry'; + $flags[] = '--ignore-ancestry'; } $this->io->writeError(" Checking out " . $ref); - $this->execute($target, $url, "svn switch" . $flags, sprintf("%s/%s", $url, $ref), $path); + $this->execute($target, $url, array_merge(['svn', 'switch'], $flags), sprintf("%s/%s", $url, $ref), $path); return \React\Promise\resolve(null); } @@ -97,7 +97,7 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin return null; } - $this->process->execute('svn status --ignore-externals', $output, $path); + $this->process->execute(['svn', 'status', '--ignore-externals'], $output, $path); return Preg::isMatch('{^ *[^X ] +}m', $output) ? $output : null; } @@ -107,13 +107,13 @@ public function getLocalChanges(PackageInterface $package, string $path): ?strin * if necessary. * * @param string $baseUrl Base URL of the repository - * @param string $command SVN command to run + * @param non-empty-list $command SVN command to run * @param string $url SVN url * @param string $cwd Working directory * @param string $path Target for a checkout * @throws \RuntimeException */ - protected function execute(PackageInterface $package, string $baseUrl, string $command, string $url, ?string $cwd = null, ?string $path = null): string + protected function execute(PackageInterface $package, string $baseUrl, array $command, string $url, ?string $cwd = null, ?string $path = null): string { $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util->setCacheCredentials($this->cacheCredentials); @@ -194,10 +194,10 @@ protected function getCommitLogs(string $fromReference, string $toReference, str { if (Preg::isMatch('{@(\d+)$}', $fromReference) && Preg::isMatch('{@(\d+)$}', $toReference)) { // retrieve the svn base url from the checkout folder - $command = sprintf('svn info --non-interactive --xml -- %s', ProcessExecutor::escape($path)); + $command = ['svn', 'info', '--non-interactive', '--xml', '--', $path]; if (0 !== $this->process->execute($command, $output, $path)) { throw new \RuntimeException( - 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput() + 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput() ); } @@ -214,7 +214,7 @@ protected function getCommitLogs(string $fromReference, string $toReference, str $fromRevision = Preg::replace('{.*@(\d+)$}', '$1', $fromReference); $toRevision = Preg::replace('{.*@(\d+)$}', '$1', $toReference); - $command = sprintf('svn log -r%s:%s --incremental', ProcessExecutor::escape($fromRevision), ProcessExecutor::escape($toRevision)); + $command = ['svn', 'log', '-r', $fromRevision.':'.$toRevision, '--incremental']; $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util->setCacheCredentials($this->cacheCredentials); @@ -222,7 +222,7 @@ protected function getCommitLogs(string $fromReference, string $toReference, str return $util->executeLocal($command, $path, null, $this->io->isVerbose()); } catch (\RuntimeException $e) { throw new \RuntimeException( - 'Failed to execute ' . $command . "\n\n".$e->getMessage() + 'Failed to execute ' . implode(' ', $command) . "\n\n".$e->getMessage() ); } } @@ -235,7 +235,7 @@ protected function getCommitLogs(string $fromReference, string $toReference, str */ protected function discardChanges(string $path): PromiseInterface { - if (0 !== $this->process->execute('svn revert -R .', $output, $path)) { + if (0 !== $this->process->execute(['svn', 'revert', '-R', '.'], $output, $path)) { throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); } diff --git a/src/Composer/Downloader/TransportException.php b/src/Composer/Downloader/TransportException.php index c91897c1c144..a30842e1e4b2 100644 --- a/src/Composer/Downloader/TransportException.php +++ b/src/Composer/Downloader/TransportException.php @@ -26,6 +26,11 @@ class TransportException extends \RuntimeException /** @var array */ protected $responseInfo = []; + public function __construct(string $message = "", int $code = 400, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } + /** * @param array $headers */ diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index a1c5979d5731..626bcb5c5594 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -241,7 +241,7 @@ public function remove(PackageInterface $package, string $path): PromiseInterfac public function getVcsReference(PackageInterface $package, string $path): ?string { $parser = new VersionParser; - $guesser = new VersionGuesser($this->config, $this->process, $parser); + $guesser = new VersionGuesser($this->config, $this->process, $parser, $this->io); $dumper = new ArrayDumper; $packageConfig = $dumper->dump($package); diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 8c44cd1997ec..286d32cff5ef 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -26,13 +26,13 @@ class XzDownloader extends ArchiveDownloader { protected function extract(PackageInterface $package, string $file, string $path): PromiseInterface { - $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path); + $command = ['tar', '-xJf', $file, '-C', $path]; if (0 === $this->process->execute($command, $ignoredOutput)) { return \React\Promise\resolve(null); } - $processError = 'Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput(); + $processError = 'Failed to execute ' . implode(' ', $command) . "\n\n" . $this->process->getErrorOutput(); throw new \RuntimeException($processError); } diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 9d0f35353e67..54d23a5c6791 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -27,7 +27,7 @@ */ class ZipDownloader extends ArchiveDownloader { - /** @var array */ + /** @var array> */ private static $unzipCommands; /** @var bool */ private static $hasZipArchive; @@ -35,7 +35,7 @@ class ZipDownloader extends ArchiveDownloader private static $isWindows; /** @var ZipArchive|null */ - private $zipArchiveObject; // @phpstan-ignore-line helper property that is set via reflection for testing purposes + private $zipArchiveObject; // @phpstan-ignore property.onlyRead (helper property that is set via reflection for testing purposes) /** * @inheritDoc @@ -46,16 +46,16 @@ public function download(PackageInterface $package, string $path, ?PackageInterf self::$unzipCommands = []; $finder = new ExecutableFinder; if (Platform::isWindows() && ($cmd = $finder->find('7z', null, ['C:\Program Files\7-Zip']))) { - self::$unzipCommands[] = ['7z', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s']; + self::$unzipCommands[] = ['7z', $cmd, 'x', '-bb0', '-y', '%file%', '-o%path%']; } if ($cmd = $finder->find('unzip')) { - self::$unzipCommands[] = ['unzip', ProcessExecutor::escape($cmd).' -qq %s -d %s']; + self::$unzipCommands[] = ['unzip', $cmd, '-qq', '%file%', '-d', '%path%']; } if (!Platform::isWindows() && ($cmd = $finder->find('7z'))) { // 7z linux/macOS support is only used if unzip is not present - self::$unzipCommands[] = ['7z', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s']; + self::$unzipCommands[] = ['7z', $cmd, 'x', '-bb0', '-y', '%file%', '-o%path%']; } if (!Platform::isWindows() && ($cmd = $finder->find('7zz'))) { // 7zz linux/macOS support is only used if unzip is not present - self::$unzipCommands[] = ['7zz', ProcessExecutor::escape($cmd).' x -bb0 -y %s -o%s']; + self::$unzipCommands[] = ['7zz', $cmd, 'x', '-bb0', '-y', '%file%', '-o%path%']; } } @@ -114,24 +114,28 @@ private function extractWithSystemUnzip(PackageInterface $package, string $file, // Force Exception throwing if the other alternative extraction method is not available $isLastChance = !self::$hasZipArchive; - if (!self::$unzipCommands) { + if (0 === \count(self::$unzipCommands)) { // This was call as the favorite extract way, but is not available // We switch to the alternative return $this->extractWithZipArchive($package, $file, $path); } $commandSpec = reset(self::$unzipCommands); - $command = sprintf($commandSpec[1], ProcessExecutor::escape($file), ProcessExecutor::escape($path)); - // normalize separators to backslashes to avoid problems with 7-zip on windows - // see https://github.com/composer/composer/issues/10058 - if (Platform::isWindows()) { - $command = sprintf($commandSpec[1], ProcessExecutor::escape(strtr($file, '/', '\\')), ProcessExecutor::escape(strtr($path, '/', '\\'))); - } - $executable = $commandSpec[0]; + $command = array_slice($commandSpec, 1); + $map = [ + // normalize separators to backslashes to avoid problems with 7-zip on windows + // see https://github.com/composer/composer/issues/10058 + '%file%' => strtr($file, '/', DIRECTORY_SEPARATOR), + '%path%' => strtr($path, '/', DIRECTORY_SEPARATOR), + ]; + $command = array_map(static function ($value) use ($map) { + return strtr($value, $map); + }, $command); + if (!$warned7ZipLinux && !Platform::isWindows() && in_array($executable, ['7z', '7zz'], true)) { $warned7ZipLinux = true; - if (0 === $this->process->execute($executable, $output)) { + if (0 === $this->process->execute([$commandSpec[1]], $output)) { if (Preg::isMatchStrictGroups('{^\s*7-Zip(?: \[64\])? ([0-9.]+)}', $output, $match) && version_compare($match[1], '21.01', '<')) { $this->io->writeError(' Unzipping using '.$executable.' '.$match[1].' may result in incorrect file permissions. Install '.$executable.' 21.01+ or unzip to ensure you get correct permissions.'); } @@ -144,6 +148,10 @@ private function extractWithSystemUnzip(PackageInterface $package, string $file, throw $processError; } + if (str_contains($processError->getMessage(), 'zip bomb')) { + throw $processError; + } + if (!is_file($file)) { $io->writeError(' '.$processError->getMessage().''); $io->writeError(' This most likely is due to a custom installer plugin not handling the returned Promise from the downloader'); @@ -182,7 +190,7 @@ private function extractWithSystemUnzip(PackageInterface $package, string $file, $output = $process->getErrorOutput(); $output = str_replace(', '.$file.'.zip or '.$file.'.ZIP', '', $output); - return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$output)); + return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.implode(' ', $command)."\n\n".$output)); } }); } catch (\Throwable $e) { @@ -208,7 +216,27 @@ private function extractWithZipArchive(PackageInterface $package, string $file, } else { $retval = $zipArchive->open($file); } + if (true === $retval) { + $totalSize = 0; + $archiveSize = filesize($file); + $totalFiles = $zipArchive->count(); + if ($totalFiles > 0) { + for ($i = 0; $i < min($totalFiles, 5); $i++) { + $stat = $zipArchive->statIndex(random_int(0, $totalFiles - 1)); + if ($stat === false) { + continue; + } + $totalSize += $stat['size']; + if ($stat['size'] > $stat['comp_size'] * 200) { + throw new \RuntimeException('Invalid zip file with compression ratio >99% (possible zip bomb)'); + } + } + if ($archiveSize !== false && $totalSize > $archiveSize * 100 && $totalSize > 50*1024*1024) { + throw new \RuntimeException('Invalid zip file with compression ratio >99% (possible zip bomb)'); + } + } + $extractResult = $zipArchive->extractTo($path); if (true === $extractResult) { diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 61bb84dde996..30720e8b4e31 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -90,7 +90,7 @@ public function __construct(PartialComposer $composer, IOInterface $io, ?Process */ public function setRunScripts(bool $runScripts = true): self { - $this->runScripts = (bool) $runScripts; + $this->runScripts = $runScripts; return $this; } @@ -194,13 +194,20 @@ protected function doDispatch(Event $event) $this->pushEvent($event); + $autoloadersBefore = spl_autoload_functions(); + try { $returnMax = 0; foreach ($listeners as $callable) { $return = 0; $this->ensureBinDirIsInPath(); - $formattedEventNameWithArgs = $event->getName() . ($event->getArguments() !== [] ? ' (' . implode(', ', $event->getArguments()) . ')' : ''); + $additionalArgs = $event->getArguments(); + if (is_string($callable) && str_contains($callable, '@no_additional_args')) { + $callable = Preg::replace('{ ?@no_additional_args}', '', $callable); + $additionalArgs = []; + } + $formattedEventNameWithArgs = $event->getName() . ($additionalArgs !== [] ? ' (' . implode(', ', $additionalArgs) . ')' : ''); if (!is_string($callable)) { if (!is_callable($callable)) { $className = is_object($callable[0]) ? get_class($callable[0]) : $callable[0]; @@ -218,7 +225,12 @@ protected function doDispatch(Event $event) $scriptName = $script[0]; unset($script[0]); - $args = array_merge($script, $event->getArguments()); + $index = array_search('@additional_args', $script, true); + if ($index !== false) { + $args = array_splice($script, $index, 0, $additionalArgs); + } else { + $args = array_merge($script, $additionalArgs); + } $flags = $event->getFlags(); if (isset($flags['script-alias-input'])) { $argsString = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $script)); @@ -292,12 +304,12 @@ protected function doDispatch(Event $event) $app->add($cmd); $app->setDefaultCommand((string) $cmd->getName(), true); try { - $args = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $event->getArguments())); + $args = implode(' ', array_map(static function ($arg) { return ProcessExecutor::escape($arg); }, $additionalArgs)); // reusing the output from $this->io is mostly needed for tests, but generally speaking // it does not hurt to keep the same stream as the current Application if ($this->io instanceof ConsoleIO) { $reflProp = new \ReflectionProperty($this->io, 'output'); - if (PHP_VERSION_ID < 80100) { + if (\PHP_VERSION_ID < 80100) { $reflProp->setAccessible(true); } $output = $reflProp->getValue($this->io); @@ -311,13 +323,17 @@ protected function doDispatch(Event $event) throw $e; } } else { - $args = implode(' ', array_map(['Composer\Util\ProcessExecutor', 'escape'], $event->getArguments())); + $args = implode(' ', array_map(['Composer\Util\ProcessExecutor', 'escape'], $additionalArgs)); // @putenv does not receive arguments if (strpos($callable, '@putenv ') === 0) { $exec = $callable; } else { - $exec = $callable . ($args === '' ? '' : ' '.$args); + if (str_contains($callable, '@additional_args')) { + $exec = str_replace('@additional_args', $args, $callable); + } else { + $exec = $callable . ($args === '' ? '' : ' '.$args); + } } if ($this->io->isVerbose()) { @@ -328,7 +344,7 @@ protected function doDispatch(Event $event) } $possibleLocalBinaries = $this->composer->getPackage()->getBinaries(); - if ($possibleLocalBinaries) { + if (count($possibleLocalBinaries) > 0) { foreach ($possibleLocalBinaries as $localExec) { if (Preg::isMatch('{\b'.preg_quote($callable).'$}', $localExec)) { $caller = BinaryInstaller::determineBinaryCaller($localExec); @@ -352,7 +368,7 @@ protected function doDispatch(Event $event) $pathAndArgs = substr($exec, 5); if (Platform::isWindows()) { $pathAndArgs = Preg::replaceCallback('{^\S+}', static function ($path) { - return str_replace('/', '\\', (string) $path[0]); + return str_replace('/', '\\', $path[0]); }, $pathAndArgs); } // match somename (not in quote, and not a qualified path) and if it is not a valid path from CWD then try to find it @@ -384,8 +400,6 @@ protected function doDispatch(Event $event) if (Platform::isWindows()) { $exec = Preg::replaceCallback('{^\S+}', static function ($path) { - assert(is_string($path[0])); - return str_replace('/', '\\', $path[0]); }, $exec); } @@ -413,6 +427,26 @@ protected function doDispatch(Event $event) } } finally { $this->popEvent(); + + $knownIdentifiers = []; + foreach ($autoloadersBefore as $key => $cb) { + $knownIdentifiers[$this->getCallbackIdentifier($cb)] = ['key' => $key, 'callback' => $cb]; + } + foreach (spl_autoload_functions() as $cb) { + // once we get to the first known autoloader, we can leave any appended autoloader without problems + if (isset($knownIdentifiers[$this->getCallbackIdentifier($cb)]) && $knownIdentifiers[$this->getCallbackIdentifier($cb)]['key'] === 0) { + break; + } + + // other newly appeared prepended autoloaders should be appended instead to ensure Composer loads its classes first + if ($cb instanceof ClassLoader) { + $cb->unregister(); + $cb->register(false); + } else { + spl_autoload_unregister($cb); + spl_autoload_register($cb); + } + } } return $returnMax; @@ -640,4 +674,23 @@ private function ensureBinDirIsInPath(): void } } } + + /** + * @param callable $cb DO NOT MOVE TO TYPE HINT as private autoload callbacks are not technically callable + */ + private function getCallbackIdentifier($cb): string + { + if (is_string($cb)) { + return 'fn:'.$cb; + } + if (is_object($cb)) { + return 'obj:'.spl_object_hash($cb); + } + if (is_array($cb)) { + return 'array:'.(is_string($cb[0]) ? $cb[0] : get_class($cb[0]) .'#'.spl_object_hash($cb[0])).'::'.$cb[1]; + } + + // not great but also do not want to break everything here + return 'unsupported'; + } } diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 03a5fa2ae713..4389c487e9a1 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -216,29 +216,26 @@ public static function createConfig(?IOInterface $io = null, ?string $cwd = null } $config->setAuthConfigSource(new JsonConfigSource($file, true)); - // load COMPOSER_AUTH environment variable if set - if ($composerAuthEnv = Platform::getEnv('COMPOSER_AUTH')) { - $authData = json_decode($composerAuthEnv); - if (null === $authData) { - throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed, should be a valid JSON object'); - } else { - if ($io instanceof IOInterface) { - $io->writeError('Loading auth config from COMPOSER_AUTH', true, IOInterface::DEBUG); - } - self::validateJsonSchema($io, $authData, JsonFile::AUTH_SCHEMA, 'COMPOSER_AUTH'); - $authData = json_decode($composerAuthEnv, true); - if (null !== $authData) { - $config->merge(['config' => $authData], 'COMPOSER_AUTH'); - } - } - } + self::loadComposerAuthEnv($config, $io); return $config; } public static function getComposerFile(): string { - return trim((string) Platform::getEnv('COMPOSER')) ?: './composer.json'; + $env = Platform::getEnv('COMPOSER'); + if (is_string($env)) { + $env = trim($env); + if ('' !== $env) { + if (is_dir($env)) { + throw new \RuntimeException('The COMPOSER environment variable is set to '.$env.' which is a directory, this variable should point to a composer.json or be left unset.'); + } + + return $env; + } + } + + return './composer.json'; } public static function getLockFile(string $composerFile): string @@ -341,6 +338,9 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu } } + // make sure we load the auth env again over the local auth.json + composer.json config + self::loadComposerAuthEnv($config, $io); + $vendorDir = $config->get('vendor-dir'); // initialize composer @@ -384,7 +384,7 @@ public function createComposer(IOInterface $io, $localConfig = null, $disablePlu // load package $parser = new VersionParser; - $guesser = new VersionGuesser($config, $process, $parser); + $guesser = new VersionGuesser($config, $process, $parser, $io); $loader = $this->loadRootPackage($rm, $config, $parser, $guesser, $io); $package = $loader->load($localConfig, 'Composer\Package\RootPackage', $cwd); $composer->setPackage($package); @@ -678,10 +678,32 @@ public static function createHttpDownloader(IOInterface $io, Config $config, arr return $httpDownloader; } + private static function loadComposerAuthEnv(Config $config, ?IOInterface $io): void + { + $composerAuthEnv = Platform::getEnv('COMPOSER_AUTH'); + if (false === $composerAuthEnv || '' === $composerAuthEnv) { + return; + } + + $authData = json_decode($composerAuthEnv); + if (null === $authData) { + throw new \UnexpectedValueException('COMPOSER_AUTH environment variable is malformed, should be a valid JSON object'); + } + + if ($io instanceof IOInterface) { + $io->writeError('Loading auth config from COMPOSER_AUTH', true, IOInterface::DEBUG); + } + self::validateJsonSchema($io, $authData, JsonFile::AUTH_SCHEMA, 'COMPOSER_AUTH'); + $authData = json_decode($composerAuthEnv, true); + if (null !== $authData) { + $config->merge(['config' => $authData], 'COMPOSER_AUTH'); + } + } + private static function useXdg(): bool { foreach (array_keys($_SERVER) as $key) { - if (strpos($key, 'XDG_') === 0) { + if (strpos((string) $key, 'XDG_') === 0) { return true; } } diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 710f5a108b8f..55ac166044a6 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -15,6 +15,7 @@ use Composer\Config; use Composer\Pcre\Preg; use Composer\Util\ProcessExecutor; +use Composer\Util\Silencer; use Psr\Log\LogLevel; abstract class BaseIO implements IOInterface @@ -175,50 +176,85 @@ public function loadConfiguration(Config $config) ProcessExecutor::setTimeout($config->get('process-timeout')); } + /** + * @param string|\Stringable $message + */ public function emergency($message, array $context = []): void { $this->log(LogLevel::EMERGENCY, $message, $context); } + /** + * @param string|\Stringable $message + */ public function alert($message, array $context = []): void { $this->log(LogLevel::ALERT, $message, $context); } + /** + * @param string|\Stringable $message + */ public function critical($message, array $context = []): void { $this->log(LogLevel::CRITICAL, $message, $context); } + /** + * @param string|\Stringable $message + */ public function error($message, array $context = []): void { $this->log(LogLevel::ERROR, $message, $context); } + /** + * @param string|\Stringable $message + */ public function warning($message, array $context = []): void { $this->log(LogLevel::WARNING, $message, $context); } + /** + * @param string|\Stringable $message + */ public function notice($message, array $context = []): void { $this->log(LogLevel::NOTICE, $message, $context); } + /** + * @param string|\Stringable $message + */ public function info($message, array $context = []): void { $this->log(LogLevel::INFO, $message, $context); } + /** + * @param string|\Stringable $message + */ public function debug($message, array $context = []): void { $this->log(LogLevel::DEBUG, $message, $context); } + /** + * @param mixed|LogLevel::* $level + * @param string|\Stringable $message + */ public function log($level, $message, array $context = []): void { $message = (string) $message; + if ($context !== []) { + $json = Silencer::call('json_encode', $context, JSON_INVALID_UTF8_IGNORE|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); + if ($json !== false) { + $message .= ' ' . $json; + } + } + if (in_array($level, [LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL, LogLevel::ERROR])) { $this->writeError(''.$message.''); } elseif ($level === LogLevel::WARNING) { diff --git a/src/Composer/IO/BufferIO.php b/src/Composer/IO/BufferIO.php index 0404287a0cbb..6cf962b84b47 100644 --- a/src/Composer/IO/BufferIO.php +++ b/src/Composer/IO/BufferIO.php @@ -14,6 +14,8 @@ use Composer\Pcre\Preg; use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Input\StreamableInputInterface; @@ -25,17 +27,16 @@ */ class BufferIO extends ConsoleIO { - /** @var StringInput */ - protected $input; - /** @var StreamOutput */ - protected $output; - public function __construct(string $input = '', int $verbosity = StreamOutput::VERBOSITY_NORMAL, ?OutputFormatterInterface $formatter = null) { $input = new StringInput($input); $input->setInteractive(false); - $output = new StreamOutput(fopen('php://memory', 'rw'), $verbosity, $formatter ? $formatter->isDecorated() : false, $formatter); + $stream = fopen('php://memory', 'rw'); + if ($stream === false) { + throw new \RuntimeException('Unable to open memory output stream'); + } + $output = new StreamOutput($stream, $verbosity, $formatter !== null ? $formatter->isDecorated() : false, $formatter); parent::__construct($input, $output, new HelperSet([ new QuestionHelper(), @@ -47,13 +48,12 @@ public function __construct(string $input = '', int $verbosity = StreamOutput::V */ public function getOutput(): string { + assert($this->output instanceof StreamOutput); fseek($this->output->getStream(), 0); - $output = stream_get_contents($this->output->getStream()); + $output = (string) stream_get_contents($this->output->getStream()); $output = Preg::replaceCallback("{(?<=^|\n|\x08)(.+?)(\x08+)}", static function ($matches): string { - assert(is_string($matches[1])); - assert(is_string($matches[2])); $pre = strip_tags($matches[1]); if (strlen($pre) === strlen($matches[2])) { @@ -85,11 +85,14 @@ public function setUserInputs(array $inputs): void /** * @param string[] $inputs * - * @return false|resource stream + * @return resource stream */ private function createStream(array $inputs) { $stream = fopen('php://memory', 'r+'); + if ($stream === false) { + throw new \RuntimeException('Unable to open memory output stream'); + } foreach ($inputs as $input) { fwrite($stream, $input.PHP_EOL); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index fe50c8fdeda6..2502a7d0234a 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -80,6 +80,8 @@ class Installer // used/declared in SolverProblemsException, carried over here for completeness public const ERROR_DEPENDENCY_RESOLUTION_FAILED = 2; public const ERROR_AUDIT_FAILED = 5; + // technically exceptions are thrown with various status codes >400, but the process exit code is normalized to 100 + public const ERROR_TRANSPORT_EXCEPTION = 100; /** * @var IOInterface @@ -176,6 +178,10 @@ class Installer protected $errorOnAudit = false; /** @var Auditor::FORMAT_* */ protected $auditFormat = Auditor::FORMAT_SUMMARY; + /** @var list */ + private $ignoredTypes = ['php-ext', 'php-ext-zend']; + /** @var list|null */ + private $allowedTypes = null; /** @var bool */ protected $updateMirrors = false; @@ -492,7 +498,7 @@ protected function doUpdate(InstalledRepositoryInterface $localRepo, bool $doIns $request->setUpdateAllowList($this->updateAllowList, $this->updateAllowTransitiveDependencies); } - $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy)); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, $this->createPoolOptimizer($policy), $this->ignoredTypes, $this->allowedTypes); $this->io->writeError('Updating dependencies'); @@ -532,10 +538,7 @@ protected function doUpdate(InstalledRepositoryInterface $localRepo, bool $doIns return $exitCode; } - // exists as of composer/semver 3.3.0 - if (method_exists('Composer\Semver\CompilingMatcher', 'clear')) { // @phpstan-ignore-line - \Composer\Semver\CompilingMatcher::clear(); - } + \Composer\Semver\CompilingMatcher::clear(); // write lock $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); @@ -739,18 +742,33 @@ protected function doInstall(InstalledRepositoryInterface $localRepo, bool $alre if ($missingRequirementInfo !== []) { $this->io->writeError($missingRequirementInfo); - return self::ERROR_LOCK_FILE_INVALID; + if (!$this->config->get('allow-missing-requirements')) { + return self::ERROR_LOCK_FILE_INVALID; + } } foreach ($lockedRepository->getPackages() as $package) { $request->fixLockedPackage($package); } + $rootRequires = $this->package->getRequires(); + if ($this->devMode) { + $rootRequires = array_merge($rootRequires, $this->package->getDevRequires()); + } + foreach ($rootRequires as $link) { + if (PlatformRepository::isPlatformPackage($link->getTarget())) { + $request->requireName($link->getTarget(), $link->getConstraint()); + } + } + foreach ($this->locker->getPlatformRequirements($this->devMode) as $link) { - $request->requireName($link->getTarget(), $link->getConstraint()); + if (!isset($rootRequires[$link->getTarget()])) { + $request->requireName($link->getTarget(), $link->getConstraint()); + } } + unset($rootRequires, $link); - $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher); + $pool = $repositorySet->createPool($request, $this->io, $this->eventDispatcher, null, $this->ignoredTypes, $this->allowedTypes); // solve dependencies $solver = new Solver($policy, $pool, $this->io); @@ -894,7 +912,7 @@ private function createRepositorySet(bool $forUpdate, PlatformRepository $platfo $this->fixedRootPackage->setRequires([]); $this->fixedRootPackage->setDevRequires([]); - $stabilityFlags[$this->package->getName()] = BasePackage::$stabilities[VersionParser::parseStability($this->package->getVersion())]; + $stabilityFlags[$this->package->getName()] = BasePackage::STABILITIES[VersionParser::parseStability($this->package->getVersion())]; $repositorySet = new RepositorySet($minimumStability, $stabilityFlags, $rootAliases, $this->package->getReferences(), $rootRequires, $this->temporaryConstraints); $repositorySet->addRepository(new RootPackageRepository($this->fixedRootPackage)); @@ -1103,6 +1121,32 @@ public static function create(IOInterface $io, Composer $composer): self ); } + /** + * Packages of those types are ignored, by default php-ext and php-ext-zend are ignored + * + * @param list $types + * @return $this + */ + public function setIgnoredTypes(array $types): self + { + $this->ignoredTypes = $types; + + return $this; + } + + /** + * Only packages of those types are allowed if set to non-null + * + * @param list|null $types + * @return $this + */ + public function setAllowedTypes(?array $types): self + { + $this->allowedTypes = $types; + + return $this; + } + /** * @return $this */ @@ -1131,7 +1175,7 @@ public function setTemporaryConstraints(array $constraints): self */ public function setDryRun(bool $dryRun = true): self { - $this->dryRun = (bool) $dryRun; + $this->dryRun = $dryRun; return $this; } @@ -1163,7 +1207,7 @@ public function setDownloadOnly(bool $downloadOnly = true): self */ public function setPreferSource(bool $preferSource = true): self { - $this->preferSource = (bool) $preferSource; + $this->preferSource = $preferSource; return $this; } @@ -1175,7 +1219,7 @@ public function setPreferSource(bool $preferSource = true): self */ public function setPreferDist(bool $preferDist = true): self { - $this->preferDist = (bool) $preferDist; + $this->preferDist = $preferDist; return $this; } @@ -1187,7 +1231,7 @@ public function setPreferDist(bool $preferDist = true): self */ public function setOptimizeAutoloader(bool $optimizeAutoloader): self { - $this->optimizeAutoloader = (bool) $optimizeAutoloader; + $this->optimizeAutoloader = $optimizeAutoloader; if (!$this->optimizeAutoloader) { // Force classMapAuthoritative off when not optimizing the // autoloader @@ -1205,7 +1249,7 @@ public function setOptimizeAutoloader(bool $optimizeAutoloader): self */ public function setClassMapAuthoritative(bool $classMapAuthoritative): self { - $this->classMapAuthoritative = (bool) $classMapAuthoritative; + $this->classMapAuthoritative = $classMapAuthoritative; if ($this->classMapAuthoritative) { // Force optimizeAutoloader when classmap is authoritative $this->setOptimizeAutoloader(true); @@ -1234,7 +1278,7 @@ public function setApcuAutoloader(bool $apcuAutoloader, ?string $apcuAutoloaderP */ public function setUpdate(bool $update): self { - $this->update = (bool) $update; + $this->update = $update; return $this; } @@ -1246,7 +1290,7 @@ public function setUpdate(bool $update): self */ public function setInstall(bool $install): self { - $this->install = (bool) $install; + $this->install = $install; return $this; } @@ -1258,7 +1302,7 @@ public function setInstall(bool $install): self */ public function setDevMode(bool $devMode = true): self { - $this->devMode = (bool) $devMode; + $this->devMode = $devMode; return $this; } @@ -1272,7 +1316,7 @@ public function setDevMode(bool $devMode = true): self */ public function setDumpAutoloader(bool $dumpAutoloader = true): self { - $this->dumpAutoloader = (bool) $dumpAutoloader; + $this->dumpAutoloader = $dumpAutoloader; return $this; } @@ -1287,7 +1331,7 @@ public function setDumpAutoloader(bool $dumpAutoloader = true): self */ public function setRunScripts(bool $runScripts = true): self { - $this->runScripts = (bool) $runScripts; + $this->runScripts = $runScripts; return $this; } @@ -1311,7 +1355,7 @@ public function setConfig(Config $config): self */ public function setVerbose(bool $verbose = true): self { - $this->verbose = (bool) $verbose; + $this->verbose = $verbose; return $this; } diff --git a/src/Composer/Installer/BinaryInstaller.php b/src/Composer/Installer/BinaryInstaller.php index ba50fe17f5eb..921132552792 100644 --- a/src/Composer/Installer/BinaryInstaller.php +++ b/src/Composer/Installer/BinaryInstaller.php @@ -213,7 +213,7 @@ protected function generateUnixyProxyCode(string $bin, string $link): string $binDir = ProcessExecutor::escape(dirname($binPath)); $binFile = basename($binPath); - $binContents = file_get_contents($bin); + $binContents = (string) file_get_contents($bin, false, null, 0, 500); // For php files, we generate a PHP proxy instead of a shell one, // which allows calling the proxy with a custom php process if (Preg::isMatch('{^(#!.*\r?\n)?[\r\n\t ]*<\?php}', $binContents, $match)) { @@ -224,8 +224,13 @@ protected function generateUnixyProxyCode(string $bin, string $link): string $globalsCode = '$GLOBALS[\'_composer_bin_dir\'] = __DIR__;'."\n"; $phpunitHack1 = $phpunitHack2 = ''; // Don't expose autoload path when vendor dir was not set in custom installers - if ($this->vendorDir) { - $globalsCode .= '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $this->vendorDir . '/autoload.php', false, true).";\n"; + if ($this->vendorDir !== null) { + // ensure comparisons work accurately if the CWD is a symlink, as $link is realpath'd already + $vendorDirReal = realpath($this->vendorDir); + if ($vendorDirReal === false) { + $vendorDirReal = $this->vendorDir; + } + $globalsCode .= '$GLOBALS[\'_composer_autoload_path\'] = ' . $this->filesystem->findShortestPathCode($link, $vendorDirReal . '/autoload.php', false, true).";\n"; } // Add workaround for PHPUnit process isolation if ($this->filesystem->normalizePath($bin) === $this->filesystem->normalizePath($this->vendorDir.'/phpunit/phpunit/phpunit')) { @@ -237,7 +242,7 @@ protected function generateUnixyProxyCode(string $bin, string $link): string $data = str_replace(\'__DIR__\', var_export(dirname($this->realpath), true), $data); $data = str_replace(\'__FILE__\', var_export($this->realpath, true), $data);'; } - if (trim((string) $match[0]) !== ' */ + /** @var list */ private $installers = []; /** @var array */ private $cache = []; @@ -180,7 +180,7 @@ public function ensureBinariesPresence(PackageInterface $package): void */ public function execute(InstalledRepositoryInterface $repo, array $operations, bool $devMode = true, bool $runScripts = true, bool $downloadOnly = false): void { - /** @var array> */ + /** @var array> $cleanupPromises */ $cleanupPromises = []; $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use (&$cleanupPromises) { @@ -197,25 +197,28 @@ public function execute(InstalledRepositoryInterface $repo, array $operations, b foreach ($operations as $index => $operation) { if ($operation instanceof UpdateOperation || $operation instanceof InstallOperation) { $package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage(); - if ($package->getType() === 'composer-plugin' && ($extra = $package->getExtra()) && isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true) { - if ($batch) { - $batches[] = $batch; + if ($package->getType() === 'composer-plugin') { + $extra = $package->getExtra(); + if (isset($extra['plugin-modifies-downloads']) && $extra['plugin-modifies-downloads'] === true) { + if (count($batch) > 0) { + $batches[] = $batch; + } + $batches[] = [$index => $operation]; + $batch = []; + + continue; } - $batches[] = [$index => $operation]; - $batch = []; - - continue; } } $batch[$index] = $operation; } - if ($batch) { + if (count($batch) > 0) { $batches[] = $batch; } - foreach ($batches as $batch) { - $this->downloadAndExecuteBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $downloadOnly, $operations); + foreach ($batches as $batchToExecute) { + $this->downloadAndExecuteBatch($repo, $batchToExecute, $cleanupPromises, $devMode, $runScripts, $downloadOnly, $operations); } } catch (\Exception $e) { $this->runCleanup($cleanupPromises); @@ -248,7 +251,7 @@ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, arr $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything at this stage - if (!in_array($opType, ['update', 'install', 'uninstall'])) { + if (!in_array($opType, ['update', 'install', 'uninstall'], true)) { continue; } @@ -266,7 +269,7 @@ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, arr $cleanupPromises[$index] = static function () use ($opType, $installer, $package, $initialPackage): ?PromiseInterface { // avoid calling cleanup if the download was not even initialized for a package // as without installation source configured nothing will work - if (!$package->getInstallationSource()) { + if (null === $package->getInstallationSource()) { return \React\Promise\resolve(null); } @@ -282,7 +285,7 @@ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, arr } // execute all downloads first - if (count($promises)) { + if (count($promises) > 0) { $this->waitOnPromises($promises); } @@ -299,7 +302,7 @@ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, arr if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) { $package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage(); if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { - if ($batch) { + if (count($batch) > 0) { $batches[] = $batch; } $batches[] = [$index => $operation]; @@ -311,12 +314,12 @@ private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, arr $batch[$index] = $operation; } - if ($batch) { + if (count($batch) > 0) { $batches[] = $batch; } - foreach ($batches as $batch) { - $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $allOperations); + foreach ($batches as $batchToExecute) { + $this->executeBatch($repo, $batchToExecute, $cleanupPromises, $devMode, $runScripts, $allOperations); } } @@ -334,7 +337,7 @@ private function executeBatch(InstalledRepositoryInterface $repo, array $operati $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything - if (!in_array($opType, ['update', 'install', 'uninstall'])) { + if (!in_array($opType, ['update', 'install', 'uninstall'], true)) { // output alias ops in debug verbosity as they have no output otherwise if ($this->io->isDebug()) { $this->io->writeError(' - ' . $operation->show(false)); @@ -360,9 +363,9 @@ private function executeBatch(InstalledRepositoryInterface $repo, array $operati 'install' => PackageEvents::PRE_PACKAGE_INSTALL, 'update' => PackageEvents::PRE_PACKAGE_UPDATE, 'uninstall' => PackageEvents::PRE_PACKAGE_UNINSTALL, - ][$opType] ?? null; + ][$opType]; - if (null !== $eventName && $runScripts && $this->eventDispatcher) { + if ($runScripts && $this->eventDispatcher !== null) { $this->eventDispatcher->dispatchPackageEvent($eventName, $devMode, $repo, $allOperations, $operation); } @@ -389,9 +392,9 @@ private function executeBatch(InstalledRepositoryInterface $repo, array $operati 'install' => PackageEvents::POST_PACKAGE_INSTALL, 'update' => PackageEvents::POST_PACKAGE_UPDATE, 'uninstall' => PackageEvents::POST_PACKAGE_UNINSTALL, - ][$opType] ?? null; + ][$opType]; - if (null !== $eventName && $runScripts && $dispatcher) { + if ($runScripts && $dispatcher !== null) { $postExecCallbacks[] = static function () use ($dispatcher, $eventName, $devMode, $repo, $allOperations, $operation): void { $dispatcher->dispatchPackageEvent($eventName, $devMode, $repo, $allOperations, $operation); }; @@ -401,7 +404,7 @@ private function executeBatch(InstalledRepositoryInterface $repo, array $operati } // execute all prepare => installs/updates/removes => cleanup steps - if (count($promises)) { + if (count($promises) > 0) { $this->waitOnPromises($promises); } @@ -421,14 +424,14 @@ private function waitOnPromises(array $promises): void if ( $this->outputProgress && $this->io instanceof ConsoleIO - && !Platform::getEnv('CI') + && !((bool) Platform::getEnv('CI')) && !$this->io->isDebug() && count($promises) > 1 ) { $progress = $this->io->getProgressBar(); } $this->loop->wait($promises, $progress); - if ($progress) { + if ($progress !== null) { $progress->clear(); // ProgressBar in non-decorated output does not output a final line-break and clear() does nothing if (!$this->io->isDecorated()) { @@ -573,7 +576,7 @@ public function notifyInstalls(IOInterface $io): void try { foreach ($this->notifiablePackages as $repoUrl => $packages) { // non-batch API, deprecated - if (strpos($repoUrl, '%package%')) { + if (str_contains($repoUrl, '%package%')) { foreach ($packages as $package) { $url = str_replace('%package%', $package->getPrettyName(), $repoUrl); @@ -635,7 +638,7 @@ public function notifyInstalls(IOInterface $io): void private function markForNotification(PackageInterface $package): void { - if ($package->getNotificationUrl()) { + if ($package->getNotificationUrl() !== null) { $this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package; } } @@ -663,7 +666,7 @@ private function runCleanup(array $cleanupPromises): void }); } - if (!empty($promises)) { + if (count($promises) > 0) { $this->loop->wait($promises); } } diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 7f9814d61d3e..02ce5196c810 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -32,6 +32,7 @@ class JsonFile public const LAX_SCHEMA = 1; public const STRICT_SCHEMA = 2; public const AUTH_SCHEMA = 3; + public const LOCK_SCHEMA = 4; /** @deprecated Use \JSON_UNESCAPED_SLASHES */ public const JSON_UNESCAPED_SLASHES = 64; @@ -41,6 +42,7 @@ class JsonFile public const JSON_UNESCAPED_UNICODE = 256; public const COMPOSER_SCHEMA_PATH = __DIR__ . '/../../../res/composer-schema.json'; + public const LOCK_SCHEMA_PATH = __DIR__ . '/../../../res/composer-lock-schema.json'; public const INDENT_DEFAULT = ' '; @@ -228,8 +230,12 @@ public static function validateJsonSchema(string $source, $data, int $schema, ?s { $isComposerSchemaFile = false; if (null === $schemaFile) { - $isComposerSchemaFile = true; - $schemaFile = self::COMPOSER_SCHEMA_PATH; + if ($schema === self::LOCK_SCHEMA) { + $schemaFile = self::LOCK_SCHEMA_PATH; + } else { + $isComposerSchemaFile = true; + $schemaFile = self::COMPOSER_SCHEMA_PATH; + } } // Prepend with file:// only when not using a special schema already (e.g. in the phar) @@ -284,7 +290,7 @@ public static function encode($data, int $options = 448, string $indent = self:: return Preg::replaceCallback( '#^ {4,}#m', static function ($match) use ($indent): string { - return str_repeat($indent, (int)(strlen($match[0] ?? '') / 4)); + return str_repeat($indent, (int)(strlen($match[0]) / 4)); }, $json ); diff --git a/src/Composer/Json/JsonFormatter.php b/src/Composer/Json/JsonFormatter.php index 006365675474..fa1a3c53f2f1 100644 --- a/src/Composer/Json/JsonFormatter.php +++ b/src/Composer/Json/JsonFormatter.php @@ -67,9 +67,7 @@ public static function format(string $json, bool $unescapeUnicode, bool $unescap if ($unescapeUnicode && function_exists('mb_convert_encoding')) { // https://stackoverflow.com/questions/2934563/how-to-decode-unicode-escape-sequences-like-u00ed-to-proper-utf-8-encoded-cha - $buffer = Preg::replaceCallback('/(\\\\+)u([0-9a-f]{4})/i', static function ($match) { - assert(is_string($match[1])); - assert(is_string($match[2])); + $buffer = Preg::replaceCallback('/(\\\\+)u([0-9a-f]{4})/i', static function ($match): string { $l = strlen($match[1]); if ($l % 2) { diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index 8d6759671080..c6c5eb555649 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -214,6 +214,14 @@ public function removeProperty(string $name): bool return $this->removeSubNode('scripts', substr($name, 8)); } + if (strpos($name, 'autoload.') === 0) { + return $this->removeSubNode('autoload', substr($name, 9)); + } + + if (strpos($name, 'autoload-dev.') === 0) { + return $this->removeSubNode('autoload-dev', substr($name, 13)); + } + return $this->removeMainKey($name); } @@ -416,6 +424,9 @@ public function removeSubNode(string $mainNode, string $name): bool if ($subName !== null) { $curVal = json_decode($children, true); unset($curVal[$name][$subName]); + if ($curVal[$name] === []) { + $curVal[$name] = new \ArrayObject(); + } $this->addSubNode($mainNode, $name, $curVal[$name]); } @@ -427,7 +438,10 @@ public function removeSubNode(string $mainNode, string $name): bool if ($subName !== null) { $curVal = json_decode($matches['content'], true); unset($curVal[$name][$subName]); - $childrenClean = $this->format($curVal); + if ($curVal[$name] === []) { + $curVal[$name] = new \ArrayObject(); + } + $childrenClean = $this->format($curVal, 0, true); } return $matches['start'] . $childrenClean . $matches['end']; @@ -534,12 +548,19 @@ public function removeMainKeyIfEmpty(string $key): bool /** * @param mixed $data */ - public function format($data, int $depth = 0): string + public function format($data, int $depth = 0, bool $wasObject = false): string { + if ($data instanceof \stdClass || $data instanceof \ArrayObject) { + $data = (array) $data; + $wasObject = true; + } + if (is_array($data)) { - reset($data); + if (\count($data) === 0) { + return $wasObject ? '{' . $this->newline . str_repeat($this->indent, $depth + 1) . '}' : '[]'; + } - if (is_numeric(key($data))) { + if (array_is_list($data)) { foreach ($data as $key => $val) { $data[$key] = $this->format($val, $depth + 1); } @@ -550,7 +571,7 @@ public function format($data, int $depth = 0): string $out = '{' . $this->newline; $elems = []; foreach ($data as $key => $val) { - $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); + $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode((string) $key). ': '.$this->format($val, $depth + 1); } return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; diff --git a/src/Composer/PHPStan/ConfigReturnTypeExtension.php b/src/Composer/PHPStan/ConfigReturnTypeExtension.php index e7ff6e52e7d6..88cd635c58fc 100644 --- a/src/Composer/PHPStan/ConfigReturnTypeExtension.php +++ b/src/Composer/PHPStan/ConfigReturnTypeExtension.php @@ -61,28 +61,26 @@ public function isMethodSupported(MethodReflection $methodReflection): bool return strtolower($methodReflection->getName()) === 'get'; } - public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type { $args = $methodCall->getArgs(); - $defaultReturn = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); - if (count($args) < 1) { - return $defaultReturn; + return null; } $keyType = $scope->getType($args[0]->value); - if (method_exists($keyType, 'getConstantStrings')) { // @phpstan-ignore-line - depending on PHPStan version, this method will always exist, or not. + if (method_exists($keyType, 'getConstantStrings')) { // @phpstan-ignore function.alreadyNarrowedType (- depending on PHPStan version, this method will always exist, or not.) $strings = $keyType->getConstantStrings(); } else { // for compat with old phpstan versions, we use a deprecated phpstan method. - $strings = TypeUtils::getConstantStrings($keyType); // @phpstan-ignore-line ignore deprecation + $strings = TypeUtils::getConstantStrings($keyType); // @phpstan-ignore staticMethod.deprecated (ignore deprecation) } if ($strings !== []) { $types = []; foreach($strings as $string) { if (!isset($this->properties[$string->getValue()])) { - return $defaultReturn; + return null; } $types[] = $this->properties[$string->getValue()]; } @@ -90,7 +88,7 @@ public function getTypeFromMethodCall(MethodReflection $methodReflection, Method return TypeCombinator::union(...$types); } - return $defaultReturn; + return null; } /** diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index 75d3158cc573..932ea3658acf 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -351,6 +351,11 @@ public function getIncludePaths(): array return $this->aliasOf->getIncludePaths(); } + public function getPhpExt(): ?array + { + return $this->aliasOf->getPhpExt(); + } + public function getReleaseDate(): ?\DateTimeInterface { return $this->aliasOf->getReleaseDate(); diff --git a/src/Composer/Package/Archiver/ArchivableFilesFinder.php b/src/Composer/Package/Archiver/ArchivableFilesFinder.php index b2b6b87b8a8e..2cf7ffc72e0a 100644 --- a/src/Composer/Package/Archiver/ArchivableFilesFinder.php +++ b/src/Composer/Package/Archiver/ArchivableFilesFinder.php @@ -47,7 +47,11 @@ public function __construct(string $sources, array $excludes, bool $ignoreFilter { $fs = new Filesystem(); - $sources = $fs->normalizePath(realpath($sources)); + $sourcesRealPath = realpath($sources); + if ($sourcesRealPath === false) { + throw new \RuntimeException('Could not realpath() the source directory "'.$sources.'"'); + } + $sources = $fs->normalizePath($sourcesRealPath); if ($ignoreFilters) { $filters = []; @@ -61,14 +65,18 @@ public function __construct(string $sources, array $excludes, bool $ignoreFilter $this->finder = new Finder(); $filter = static function (\SplFileInfo $file) use ($sources, $filters, $fs): bool { - if ($file->isLink() && ($file->getRealPath() === false || strpos($file->getRealPath(), $sources) !== 0)) { + $realpath = $file->getRealPath(); + if ($realpath === false) { + return false; + } + if ($file->isLink() && strpos($realpath, $sources) !== 0) { return false; } $relativePath = Preg::replace( '#^'.preg_quote($sources, '#').'#', '', - $fs->normalizePath($file->getRealPath()) + $fs->normalizePath($realpath) ); $exclude = false; @@ -79,10 +87,6 @@ public function __construct(string $sources, array $excludes, bool $ignoreFilter return !$exclude; }; - if (method_exists($filter, 'bindTo')) { - $filter = $filter->bindTo(null); - } - $this->finder ->in($sources) ->filter($filter) diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index 6efa4793887b..77c3ebe3dcc1 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -96,10 +96,12 @@ public function getPackageFilenameParts(CompletePackageInterface $package): arra $sourceReference = $package->getSourceReference(); if (null !== $sourceReference) { - $parts['source_reference'] = substr(sha1($sourceReference), 0, 6); + $parts['source_reference'] = substr(hash('sha1', $sourceReference), 0, 6); } - $parts = array_filter($parts); + $parts = array_filter($parts, function (?string $part) { + return $part !== null; + }); foreach ($parts as $key => $part) { $parts[$key] = str_replace('/', '-', $part); } @@ -169,7 +171,7 @@ public function archive(CompletePackageInterface $package, string $format, strin $sourcePath = realpath('.'); } else { // Directory used to download the sources - $sourcePath = sys_get_temp_dir().'/composer_archive'.uniqid(); + $sourcePath = sys_get_temp_dir().'/composer_archive'.bin2hex(random_bytes(5)); $filesystem->ensureDirectoryExists($sourcePath); try { @@ -214,7 +216,7 @@ public function archive(CompletePackageInterface $package, string $format, strin } // Create the archive - $tempTarget = sys_get_temp_dir().'/composer_archive'.uniqid().'.'.$format; + $tempTarget = sys_get_temp_dir().'/composer_archive'.bin2hex(random_bytes(5)).'.'.$format; $filesystem->ensureDirectoryExists(dirname($tempTarget)); $archivePath = $usableArchiver->archive( diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index d6d37d360d91..764fdb424769 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -40,8 +40,7 @@ abstract class BasePackage implements PackageInterface public const STABILITY_ALPHA = 15; public const STABILITY_DEV = 20; - /** @var array */ - public static $stabilities = [ + public const STABILITIES = [ 'stable' => self::STABILITY_STABLE, 'RC' => self::STABILITY_RC, 'beta' => self::STABILITY_BETA, @@ -49,6 +48,14 @@ abstract class BasePackage implements PackageInterface 'dev' => self::STABILITY_DEV, ]; + /** + * @deprecated + * @readonly + * @var array, self::STABILITY_*> + * @phpstan-ignore property.readOnlyByPhpDocDefaultValue + */ + public static $stabilities = self::STABILITIES; + /** * READ-ONLY: The package id, public for fast access in dependency solver * @var int @@ -234,7 +241,7 @@ public function getFullPrettyVersion(bool $truncate = true, int $displayMode = P */ public function getStabilityPriority(): int { - return self::$stabilities[$this->getStability()]; + return self::STABILITIES[$this->getStability()]; } public function __clone() diff --git a/src/Composer/Package/Comparer/Comparer.php b/src/Composer/Package/Comparer/Comparer.php index 1fd79b8bbd6d..70a7a28f83dd 100644 --- a/src/Composer/Package/Comparer/Comparer.php +++ b/src/Composer/Package/Comparer/Comparer.php @@ -136,7 +136,7 @@ private function doTree(string $dir, array &$array) return false; } } elseif (is_file($dir.'/'.$file) && filesize($dir.'/'.$file)) { - $array[$dir][$file] = md5_file($dir.'/'.$file); + $array[$dir][$file] = hash_file(\PHP_VERSION_ID > 80100 ? 'xxh3' : 'sha1', $dir.'/'.$file); } } } diff --git a/src/Composer/Package/Dumper/ArrayDumper.php b/src/Composer/Package/Dumper/ArrayDumper.php index f713fbe333aa..9333bd9a010c 100644 --- a/src/Composer/Package/Dumper/ArrayDumper.php +++ b/src/Composer/Package/Dumper/ArrayDumper.php @@ -37,6 +37,7 @@ public function dump(PackageInterface $package): array 'devAutoload' => 'autoload-dev', 'notificationUrl' => 'notification-url', 'includePaths' => 'include-path', + 'phpExt' => 'php-ext', ]; $data = []; @@ -44,11 +45,11 @@ public function dump(PackageInterface $package): array $data['version'] = $package->getPrettyVersion(); $data['version_normalized'] = $package->getVersion(); - if ($package->getTargetDir()) { + if ($package->getTargetDir() !== null) { $data['target-dir'] = $package->getTargetDir(); } - if ($package->getSourceType()) { + if ($package->getSourceType() !== null) { $data['source']['type'] = $package->getSourceType(); $data['source']['url'] = $package->getSourceUrl(); if (null !== ($value = $package->getSourceReference())) { @@ -59,7 +60,7 @@ public function dump(PackageInterface $package): array } } - if ($package->getDistType()) { + if ($package->getDistType() !== null) { $data['dist']['type'] = $package->getDistType(); $data['dist']['url'] = $package->getDistUrl(); if (null !== ($value = $package->getDistReference())) { @@ -74,15 +75,18 @@ public function dump(PackageInterface $package): array } foreach (BasePackage::$supportedLinkTypes as $type => $opts) { - if ($links = $package->{'get'.ucfirst($opts['method'])}()) { - foreach ($links as $link) { - $data[$type][$link->getTarget()] = $link->getPrettyConstraint(); - } - ksort($data[$type]); + $links = $package->{'get'.ucfirst($opts['method'])}(); + if (\count($links) === 0) { + continue; } + foreach ($links as $link) { + $data[$type][$link->getTarget()] = $link->getPrettyConstraint(); + } + ksort($data[$type]); } - if ($packages = $package->getSuggests()) { + $packages = $package->getSuggests(); + if (\count($packages) > 0) { ksort($packages); $data['suggest'] = $packages; } @@ -130,7 +134,7 @@ public function dump(PackageInterface $package): array if ($package instanceof RootPackageInterface) { $minimumStability = $package->getMinimumStability(); - if ($minimumStability) { + if ($minimumStability !== '') { $data['minimum-stability'] = $minimumStability; } } diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index 44b778f299f2..887f2913bb6c 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -228,6 +228,10 @@ private function configureObject(PackageInterface $package, array $config): Base $package->setIncludePaths($config['include-path']); } + if (isset($config['php-ext'])) { + $package->setPhpExt($config['php-ext']); + } + if (!empty($config['time'])) { $time = Preg::isMatch('/^\d++$/D', $config['time']) ? '@'.$config['time'] : $config['time']; @@ -313,8 +317,8 @@ private function configureObject(PackageInterface $package, array $config): Base } /** - * @param array>>> $linkCache - * @param mixed[] $config + * @param array>>> $linkCache + * @param mixed[] $config */ private function configureCachedLinks(array &$linkCache, PackageInterface $package, array $config): void { diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 9796bb3c9ad0..50807e50628b 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -60,7 +60,7 @@ public function __construct(RepositoryManager $manager, Config $config, ?Version $this->manager = $manager; $this->config = $config; - $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor($io), $this->versionParser); + $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor($io), $this->versionParser, $io); $this->io = $io; } @@ -88,7 +88,7 @@ public function load(array $config, string $class = 'Composer\Package\RootPackag // override with env var if available if (Platform::getEnv('COMPOSER_ROOT_VERSION')) { - $config['version'] = Platform::getEnv('COMPOSER_ROOT_VERSION'); + $config['version'] = $this->versionGuesser->getRootVersionFromEnv(); } else { $versionData = $this->versionGuesser->guessVersion($config, $cwd ?? Platform::getCwd(true)); if ($versionData) { @@ -99,7 +99,7 @@ public function load(array $config, string $class = 'Composer\Package\RootPackag } if (!isset($config['version'])) { - if ($this->io !== null && $config['name'] !== '__root__') { + if ($this->io !== null && $config['name'] !== '__root__' && 'project' !== ($config['type'] ?? '')) { $this->io->warning( sprintf( "Composer could not detect the root package (%s) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version", @@ -227,6 +227,7 @@ private function extractAliases(array $requires, array $aliases): array * * @param array $requires * @param array $stabilityFlags + * @param key-of $minimumStability * * @return array * @@ -235,8 +236,7 @@ private function extractAliases(array $requires, array $aliases): array */ public static function extractStabilityFlags(array $requires, string $minimumStability, array $stabilityFlags): array { - $stabilities = BasePackage::$stabilities; - /** @var int $minimumStability */ + $stabilities = BasePackage::STABILITIES; $minimumStability = $stabilities[$minimumStability]; foreach ($requires as $reqName => $reqVersion) { $constraints = []; diff --git a/src/Composer/Package/Loader/ValidatingArrayLoader.php b/src/Composer/Package/Loader/ValidatingArrayLoader.php index a94448a6159b..e3600d6e1bef 100644 --- a/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -132,33 +132,54 @@ public function load(array $config, string $class = 'Composer\Package\CompletePa } } - // check for license validity on newly updated branches - if (isset($this->config['license']) && (null === $releaseDate || $releaseDate->getTimestamp() >= strtotime('-8days'))) { + if (isset($this->config['license'])) { + // validate main data types if (is_array($this->config['license']) || is_string($this->config['license'])) { $licenses = (array) $this->config['license']; - $licenseValidator = new SpdxLicenses(); - foreach ($licenses as $license) { - // replace proprietary by MIT for validation purposes since it's not a valid SPDX identifier, but is accepted by composer - if ('proprietary' === $license) { - continue; + foreach ($licenses as $index => $license) { + if (!is_string($license)) { + $this->warnings[] = sprintf( + 'License %s should be a string.', + json_encode($license) + ); + unset($licenses[$index]); } - $licenseToValidate = str_replace('proprietary', 'MIT', $license); - if (!$licenseValidator->validate($licenseToValidate)) { - if ($licenseValidator->validate(trim($licenseToValidate))) { - $this->warnings[] = sprintf( - 'License %s must not contain extra spaces, make sure to trim it.', - json_encode($license) - ); - } else { - $this->warnings[] = sprintf( - 'License %s is not a valid SPDX license identifier, see https://spdx.org/licenses/ if you use an open license.' . PHP_EOL . - 'If the software is closed-source, you may use "proprietary" as license.', - json_encode($license) - ); + } + + // check for license validity on newly updated branches/tags + if (null === $releaseDate || $releaseDate->getTimestamp() >= strtotime('-8days')) { + $licenseValidator = new SpdxLicenses(); + foreach ($licenses as $license) { + // replace proprietary by MIT for validation purposes since it's not a valid SPDX identifier, but is accepted by composer + if ('proprietary' === $license) { + continue; + } + $licenseToValidate = str_replace('proprietary', 'MIT', $license); + if (!$licenseValidator->validate($licenseToValidate)) { + if ($licenseValidator->validate(trim($licenseToValidate))) { + $this->warnings[] = sprintf( + 'License %s must not contain extra spaces, make sure to trim it.', + json_encode($license) + ); + } else { + $this->warnings[] = sprintf( + 'License %s is not a valid SPDX license identifier, see https://spdx.org/licenses/ if you use an open license.' . PHP_EOL . + 'If the software is closed-source, you may use "proprietary" as license.', + json_encode($license) + ); + } } } } + + $this->config['license'] = array_values($licenses); + } else { + $this->warnings[] = sprintf( + 'License must be a string or array of strings, got %s.', + json_encode($this->config['license']) + ); + unset($this->config['license']); } } @@ -247,6 +268,12 @@ public function load(array $config, string $class = 'Composer\Package\CompletePa } } + $this->validateArray('php-ext'); + if (isset($this->config['php-ext']) && !in_array($this->config['type'] ?? '', ['php-ext', 'php-ext-zend'], true)) { + $this->errors[] = 'php-ext can only be set by packages of type "php-ext" or "php-ext-zend" which must be C extensions'; + unset($this->config['php-ext']); + } + $unboundConstraint = new Constraint('=', '10000000-dev'); foreach (array_keys(BasePackage::$supportedLinkTypes) as $linkType) { @@ -317,8 +344,8 @@ public function load(array $config, string $class = 'Composer\Package\CompletePa } if ($this->validateString('minimum-stability') && isset($this->config['minimum-stability'])) { - if (!isset(BasePackage::$stabilities[strtolower($this->config['minimum-stability'])]) && $this->config['minimum-stability'] !== 'RC') { - $this->errors[] = 'minimum-stability : invalid value ('.$this->config['minimum-stability'].'), must be one of '.implode(', ', array_keys(BasePackage::$stabilities)); + if (!isset(BasePackage::STABILITIES[strtolower($this->config['minimum-stability'])]) && $this->config['minimum-stability'] !== 'RC') { + $this->errors[] = 'minimum-stability : invalid value ('.$this->config['minimum-stability'].'), must be one of '.implode(', ', array_keys(BasePackage::STABILITIES)); unset($this->config['minimum-stability']); } } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index f832e797cf43..38cd8ef3a92d 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -66,13 +66,21 @@ public function __construct(IOInterface $io, JsonFile $lockFile, InstallationMan { $this->lockFile = $lockFile; $this->installationManager = $installationManager; - $this->hash = md5($composerFileContents); + $this->hash = hash('md5', $composerFileContents); $this->contentHash = self::getContentHash($composerFileContents); $this->loader = new ArrayLoader(null, true); $this->dumper = new ArrayDumper(); $this->process = $process ?? new ProcessExecutor($io); } + /** + * @internal + */ + public function getJsonFile(): JsonFile + { + return $this->lockFile; + } + /** * Returns the md5 hash of the sorted content of the composer file. * @@ -107,7 +115,7 @@ public static function getContentHash(string $composerFileContents): string ksort($relevantContent); - return md5(JsonFile::encode($relevantContent, 0)); + return hash('md5', JsonFile::encode($relevantContent, 0)); } /** @@ -247,6 +255,9 @@ public function getPlatformRequirements(bool $withDevReqs = false): array return $requirements; } + /** + * @return key-of + */ public function getMinimumStability(): string { $lockData = $this->getLockData(); @@ -361,7 +372,7 @@ public function setLockData(array $packages, ?array $devPackages, array $platfor 'Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies', 'This file is @gener'.'ated automatically', ], 'content-hash' => $this->contentHash, - 'packages' => null, + 'packages' => $this->lockPackages($packages), 'packages-dev' => null, 'aliases' => $aliases, 'minimum-stability' => $minimumStability, @@ -370,7 +381,6 @@ public function setLockData(array $packages, ?array $devPackages, array $platfor 'prefer-lowest' => $preferLowest, ]; - $lock['packages'] = $this->lockPackages($packages); if (null !== $devPackages) { $lock['packages-dev'] = $this->lockPackages($devPackages); } @@ -382,6 +392,8 @@ public function setLockData(array $packages, ?array $devPackages, array $platfor } $lock['plugin-api-version'] = PluginInterface::PLUGIN_API_VERSION; + $lock = $this->fixupJsonDataType($lock); + try { $isLocked = $this->isLocked(); } catch (ParsingException $e) { @@ -403,6 +415,60 @@ public function setLockData(array $packages, ?array $devPackages, array $platfor return false; } + /** + * Updates the lock file's hash in-place from a given composer.json's JsonFile + * + * This does not reload or require any packages, and retains the filemtime of the lock file. + * + * Use this only to update the lock file hash after updating a composer.json in ways that are guaranteed NOT to impact the dependency resolution. + * + * This is a risky method, use carefully. + * + * @param (callable(array): array)|null $dataProcessor Receives the lock data and can process it before it gets written to disk + */ + public function updateHash(JsonFile $composerJson, ?callable $dataProcessor = null): void + { + $contents = file_get_contents($composerJson->getPath()); + if (false === $contents) { + throw new \RuntimeException('Unable to read '.$composerJson->getPath().' contents to update the lock file hash.'); + } + + $lockMtime = filemtime($this->lockFile->getPath()); + $lockData = $this->lockFile->read(); + $lockData['content-hash'] = Locker::getContentHash($contents); + if ($dataProcessor !== null) { + $lockData = $dataProcessor($lockData); + } + + $this->lockFile->write($this->fixupJsonDataType($lockData)); + $this->lockDataCache = null; + $this->virtualFileWritten = false; + if (is_int($lockMtime)) { + @touch($this->lockFile->getPath(), $lockMtime); + } + } + + /** + * Ensures correct data types and ordering for the JSON lock format + * + * @param array $lockData + * @return array + */ + private function fixupJsonDataType(array $lockData): array + { + foreach (['stability-flags', 'platform', 'platform-dev'] as $key) { + if (isset($lockData[$key]) && is_array($lockData[$key]) && \count($lockData[$key]) === 0) { + $lockData[$key] = new \stdClass(); + } + } + + if (is_array($lockData['stability-flags'])) { + ksort($lockData['stability-flags']); + } + + return $lockData; + } + /** * @param PackageInterface[] $packages * @@ -488,13 +554,14 @@ private function getPackageTime(PackageInterface $package): ?string case 'git': GitUtil::cleanEnv(); - if (0 === $this->process->execute('git log -n1 --pretty=%ct '.ProcessExecutor::escape($sourceRef).GitUtil::getNoShowSignatureFlag($this->process), $output, $path) && Preg::isMatch('{^\s*\d+\s*$}', $output)) { + $command = array_merge(['git', 'log', '-n1', '--pretty=%ct', (string) $sourceRef], GitUtil::getNoShowSignatureFlags($this->process)); + if (0 === $this->process->execute($command, $output, $path) && Preg::isMatch('{^\s*\d+\s*$}', $output)) { $datetime = new \DateTime('@'.trim($output), new \DateTimeZone('UTC')); } break; case 'hg': - if (0 === $this->process->execute('hg log --template "{date|hgdate}" -r '.ProcessExecutor::escape($sourceRef), $output, $path) && Preg::isMatch('{^\s*(\d+)\s*}', $output, $match)) { + if (0 === $this->process->execute(['hg', 'log', '--template', '{date|hgdate}', '-r', (string) $sourceRef], $output, $path) && Preg::isMatch('{^\s*(\d+)\s*}', $output, $match)) { $datetime = new \DateTime('@'.$match[1], new \DateTimeZone('UTC')); } break; diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 295bbd249cd5..faa2a07a06ee 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -23,6 +23,7 @@ * * @phpstan-import-type AutoloadRules from PackageInterface * @phpstan-import-type DevAutoloadRules from PackageInterface + * @phpstan-import-type PhpExtConfig from PackageInterface */ class Package extends BasePackage { @@ -98,6 +99,11 @@ class Package extends BasePackage protected $isDefaultBranch = false; /** @var mixed[] */ protected $transportOptions = []; + /** + * @var array|null + * @phpstan-var PhpExtConfig|null + */ + protected $phpExt = null; /** * Creates a new in memory package. @@ -590,6 +596,26 @@ public function getIncludePaths(): array return $this->includePaths; } + /** + * Sets the settings for php extension packages + * + * @param array|null $phpExt + * + * @phpstan-param PhpExtConfig|null $phpExt + */ + public function setPhpExt(?array $phpExt): void + { + $this->phpExt = $phpExt; + } + + /** + * @inheritDoc + */ + public function getPhpExt(): ?array + { + return $this->phpExt; + } + /** * Sets the notification URL */ diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 4d874ef120a2..dec040334962 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -23,6 +23,7 @@ * * @phpstan-type AutoloadRules array{psr-0?: array, psr-4?: array, classmap?: list, files?: list, exclude-from-classmap?: list} * @phpstan-type DevAutoloadRules array{psr-0?: array, psr-4?: array, classmap?: list, files?: list} + * @phpstan-type PhpExtConfig array{extension-name?: string, priority?: int, support-zts?: bool, support-nts?: bool, build-path?: string|null, configure-options?: list} */ interface PackageInterface { @@ -181,6 +182,8 @@ public function getDistReference(): ?string; /** * Returns the sha1 checksum for the distribution archive of this version * + * Can be an empty string which should be treated as null + * * @return ?string */ public function getDistSha1Checksum(): ?string; @@ -321,6 +324,15 @@ public function getDevAutoload(): array; */ public function getIncludePaths(): array; + /** + * Returns the settings for php extension packages + * + * @return array|null + * + * @phpstan-return PhpExtConfig|null + */ + public function getPhpExt(): ?array; + /** * Stores a reference to the repository that owns the package */ diff --git a/src/Composer/Package/RootPackage.php b/src/Composer/Package/RootPackage.php index e1305426f3db..a0f8e659fb3f 100644 --- a/src/Composer/Package/RootPackage.php +++ b/src/Composer/Package/RootPackage.php @@ -21,7 +21,7 @@ class RootPackage extends CompletePackage implements RootPackageInterface { public const DEFAULT_PRETTY_VERSION = '1.0.0+no-version-set'; - /** @var string */ + /** @var key-of */ protected $minimumStability = 'stable'; /** @var bool */ protected $preferStable = false; diff --git a/src/Composer/Package/RootPackageInterface.php b/src/Composer/Package/RootPackageInterface.php index 4adad6c8b5ab..8a08060f8925 100644 --- a/src/Composer/Package/RootPackageInterface.php +++ b/src/Composer/Package/RootPackageInterface.php @@ -33,6 +33,8 @@ public function getAliases(): array; /** * Returns the minimum stability of the package + * + * @return key-of */ public function getMinimumStability(): string; @@ -120,12 +122,14 @@ public function setDevAutoload(array $devAutoload): void; /** * Set the stabilityFlags * - * @param array $stabilityFlags + * @phpstan-param array $stabilityFlags */ public function setStabilityFlags(array $stabilityFlags): void; /** * Set the minimumStability + * + * @phpstan-param key-of $minimumStability */ public function setMinimumStability(string $minimumStability): void; diff --git a/src/Composer/Package/Version/StabilityFilter.php b/src/Composer/Package/Version/StabilityFilter.php index 172901d44931..7e0182a6e16b 100644 --- a/src/Composer/Package/Version/StabilityFilter.php +++ b/src/Composer/Package/Version/StabilityFilter.php @@ -23,11 +23,11 @@ class StabilityFilter * Checks if any of the provided package names in the given stability match the configured acceptable stability and flags * * @param int[] $acceptableStabilities array of stability => BasePackage::STABILITY_* value - * @phpstan-param array $acceptableStabilities + * @phpstan-param array, BasePackage::STABILITY_*> $acceptableStabilities * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array $stabilityFlags * @param string[] $names The package name(s) to check for stability flags - * @param string $stability one of 'stable', 'RC', 'beta', 'alpha' or 'dev' + * @param key-of $stability one of 'stable', 'RC', 'beta', 'alpha' or 'dev' * @return bool true if any package name is acceptable */ public static function isPackageAcceptable(array $acceptableStabilities, array $stabilityFlags, array $names, string $stability): bool @@ -35,7 +35,7 @@ public static function isPackageAcceptable(array $acceptableStabilities, array $ foreach ($names as $name) { // allow if package matches the package-specific stability flag if (isset($stabilityFlags[$name])) { - if (BasePackage::$stabilities[$stability] <= $stabilityFlags[$name]) { + if (BasePackage::STABILITIES[$stability] <= $stabilityFlags[$name]) { return true; } } elseif (isset($acceptableStabilities[$stability])) { diff --git a/src/Composer/Package/Version/VersionBumper.php b/src/Composer/Package/Version/VersionBumper.php index e6dbdebe6b7f..b100d0e00edf 100644 --- a/src/Composer/Package/Version/VersionBumper.php +++ b/src/Composer/Package/Version/VersionBumper.php @@ -39,6 +39,8 @@ class VersionBumper * * ^1.2 || ^2.3 + 2.4.0 -> ^1.2 || ^2.4 * * ^3@dev + 3.2.99999-dev -> ^3.2@dev * * ~2 + 2.0-beta.1 -> ~2 + * * ~2.0.0 + 2.0.3 -> ~2.0.3 + * * ~2.0 + 2.0.3 -> ^2.0.3 * * dev-master + dev-master -> dev-master * * * + 1.2.3 -> >=1.2.3 */ @@ -84,7 +86,7 @@ public function bumpRequirement(ConstraintInterface $constraint, PackageInterfac (?<=,|\ |\||^) # leading separator (?P \^v?'.$major.'(?:\.\d+)* # e.g. ^2.anything - | ~v?'.$major.'(?:\.\d+){0,2} # e.g. ~2 or ~2.2 or ~2.2.2 but no more + | ~v?'.$major.'(?:\.\d+){1,3} # e.g. ~2.2 or ~2.2.2 or ~2.2.2.2 | v?'.$major.'(?:\.[*x])+ # e.g. 2.* or 2.*.* or 2.x.x.x etc | >=v?\d(?:\.\d+)* # e.g. >=2 or >=1.2 etc | \* # full wildcard @@ -99,8 +101,11 @@ public function bumpRequirement(ConstraintInterface $constraint, PackageInterfac if (substr_count($match[0], '.') === 2 && substr_count($versionWithoutSuffix, '.') === 1) { $suffix = '.0'; } - if (str_starts_with($match[0], '~') && substr_count($match[0], '.') === 2) { - $replacement = '~'.$versionWithoutSuffix.$suffix; + if (str_starts_with($match[0], '~') && substr_count($match[0], '.') !== 1) { + // take as many version bits from the current version as we have in the constraint to bump it without making it more specific + $versionBits = explode('.', $versionWithoutSuffix); + $versionBits = array_pad($versionBits, substr_count($match[0], '.') + 1, '0'); + $replacement = '~'.implode('.', array_slice($versionBits, 0, substr_count($match[0], '.') + 1)); } elseif ($match[0] === '*' || str_starts_with($match[0], '>=')) { $replacement = '>='.$versionWithoutSuffix.$suffix; } else { diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 2b2b19706506..7e0f8278e443 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -13,6 +13,7 @@ namespace Composer\Package\Version; use Composer\Config; +use Composer\IO\IOInterface; use Composer\Pcre\Preg; use Composer\Repository\Vcs\HgDriver; use Composer\IO\NullIO; @@ -50,11 +51,17 @@ class VersionGuesser */ private $versionParser; - public function __construct(Config $config, ProcessExecutor $process, SemverVersionParser $versionParser) + /** + * @var IOInterface|null + */ + private $io; + + public function __construct(Config $config, ProcessExecutor $process, SemverVersionParser $versionParser, ?IOInterface $io = null) { $this->config = $config; $this->process = $process; $this->versionParser = $versionParser; + $this->io = $io; } /** @@ -173,11 +180,12 @@ private function guessGitVersion(array $packageConfig, string $path): array $featurePrettyVersion = $prettyVersion; // try to find the best (nearest) version branch to assume this feature's version - $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'git rev-list %candidate%..%branch%', $path); + $result = $this->guessFeatureVersion($packageConfig, $version, $branches, ['git', 'rev-list', '%candidate%..%branch%'], $path); $version = $result['version']; $prettyVersion = $result['pretty_version']; } } + GitUtil::checkForRepoOwnershipError($this->process->getErrorOutput(), $path, $this->io); if (!$version || $isDetached) { $result = $this->versionFromGitTags($path); @@ -190,7 +198,7 @@ private function guessGitVersion(array $packageConfig, string $path): array } if (null === $commit) { - $command = 'git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($this->process); + $command = array_merge(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], GitUtil::getNoShowSignatureFlags($this->process)); if (0 === $this->process->execute($command, $output, $path)) { $commit = trim($output) ?: null; } @@ -209,7 +217,7 @@ private function guessGitVersion(array $packageConfig, string $path): array private function versionFromGitTags(string $path): ?array { // try to fetch current version from git tags - if (0 === $this->process->execute('git describe --exact-match --tags', $output, $path)) { + if (0 === $this->process->execute(['git', 'describe', '--exact-match', '--tags'], $output, $path)) { try { $version = $this->versionParser->normalize(trim($output)); @@ -229,7 +237,7 @@ private function versionFromGitTags(string $path): ?array private function guessHgVersion(array $packageConfig, string $path): ?array { // try to fetch current version from hg branch - if (0 === $this->process->execute('hg branch', $output, $path)) { + if (0 === $this->process->execute(['hg', 'branch'], $output, $path)) { $branch = trim($output); $version = $this->versionParser->normalizeBranch($branch); $isFeatureBranch = 0 === strpos($version, 'dev-'); @@ -248,7 +256,7 @@ private function guessHgVersion(array $packageConfig, string $path): ?array $branches = array_map('strval', array_keys($driver->getBranches())); // try to find the best (nearest) version branch to assume this feature's version - $result = $this->guessFeatureVersion($packageConfig, $version, $branches, 'hg log -r "not ancestors(\'%candidate%\') and ancestors(\'%branch%\')" --template "{node}\\n"', $path); + $result = $this->guessFeatureVersion($packageConfig, $version, $branches, ['hg', 'log', '-r', 'not ancestors(\'%candidate%\') and ancestors(\'%branch%\')', '--template', '"{node}\\n"'], $path); $result['commit'] = ''; $result['feature_version'] = $version; $result['feature_pretty_version'] = $version; @@ -261,13 +269,12 @@ private function guessHgVersion(array $packageConfig, string $path): ?array /** * @param array $packageConfig - * @param string[] $branches - * - * @phpstan-param non-empty-string $scmCmdline + * @param list $branches + * @param list $scmCmdline * * @return array{version: string|null, pretty_version: string|null} */ - private function guessFeatureVersion(array $packageConfig, ?string $version, array $branches, string $scmCmdline, string $path): array + private function guessFeatureVersion(array $packageConfig, ?string $version, array $branches, array $scmCmdline, string $path): array { $prettyVersion = $version; @@ -285,7 +292,7 @@ private function guessFeatureVersion(array $packageConfig, ?string $version, arr } // sort local branches first then remote ones - // and sort numeric branches below named ones, to make sure if the branch has the same distance from main and 1.10 and 1.9 for example, main is picked + // and sort numeric branches below named ones, to make sure if the branch has the same distance from main and 1.10 and 1.9 for example, 1.9 is picked // and sort using natural sort so that 1.10 will appear before 1.9 usort($branches, static function ($a, $b): int { $aRemote = 0 === strpos($a, 'remotes/'); @@ -301,7 +308,8 @@ private function guessFeatureVersion(array $packageConfig, ?string $version, arr $promises = []; $this->process->setMaxJobs(30); try { - foreach ($branches as $candidate) { + $lastIndex = -1; + foreach ($branches as $index => $candidate) { $candidateVersion = Preg::replace('{^remotes/\S+/}', '', $candidate); // do not compare against itself or other feature branches @@ -309,14 +317,20 @@ private function guessFeatureVersion(array $packageConfig, ?string $version, arr continue; } - $cmdLine = str_replace(['%candidate%', '%branch%'], [$candidate, $branch], $scmCmdline); - $promises[] = $this->process->executeAsync($cmdLine, $path)->then(function (Process $process) use (&$length, &$version, &$prettyVersion, $candidateVersion, &$promises): void { + $cmdLine = array_map(static function (string $component) use ($candidate, $branch) { + return str_replace(['%candidate%', '%branch%'], [$candidate, $branch], $component); + }, $scmCmdline); + $promises[] = $this->process->executeAsync($cmdLine, $path)->then(function (Process $process) use (&$lastIndex, $index, &$length, &$version, &$prettyVersion, $candidateVersion, &$promises): void { if (!$process->isSuccessful()) { return; } $output = $process->getOutput(); - if (strlen($output) < $length) { + // overwrite existing if we have a shorter diff, or we have an equal diff and an index that comes later in the array (i.e. older version) + // as newer versions typically have more commits, if the feature branch is based on a newer branch it should have a longer diff to the old version + // but if it doesn't and they have equal diffs, then it probably is based on the old version + if (strlen($output) < $length || (strlen($output) === $length && $lastIndex < $index)) { + $lastIndex = $index; $length = strlen($output); $version = $this->versionParser->normalizeBranch($candidateVersion); $prettyVersion = 'dev-' . $candidateVersion; @@ -361,14 +375,14 @@ private function guessFossilVersion(string $path): array $prettyVersion = null; // try to fetch current version from fossil - if (0 === $this->process->execute('fossil branch list', $output, $path)) { + if (0 === $this->process->execute(['fossil', 'branch', 'list'], $output, $path)) { $branch = trim($output); $version = $this->versionParser->normalizeBranch($branch); $prettyVersion = 'dev-' . $branch; } // try to fetch current version from fossil tags - if (0 === $this->process->execute('fossil tag list', $output, $path)) { + if (0 === $this->process->execute(['fossil', 'tag', 'list'], $output, $path)) { try { $version = $this->versionParser->normalize(trim($output)); $prettyVersion = trim($output); @@ -389,7 +403,7 @@ private function guessSvnVersion(array $packageConfig, string $path): ?array SvnUtil::cleanEnv(); // try to fetch current version from svn - if (0 === $this->process->execute('svn info --xml', $output, $path)) { + if (0 === $this->process->execute(['svn', 'info', '--xml'], $output, $path)) { $trunkPath = isset($packageConfig['trunk-path']) ? preg_quote($packageConfig['trunk-path'], '#') : 'trunk'; $branchesPath = isset($packageConfig['branches-path']) ? preg_quote($packageConfig['branches-path'], '#') : 'branches'; $tagsPath = isset($packageConfig['tags-path']) ? preg_quote($packageConfig['tags-path'], '#') : 'tags'; @@ -419,4 +433,17 @@ private function guessSvnVersion(array $packageConfig, string $path): ?array return null; } + + public function getRootVersionFromEnv(): string + { + $version = Platform::getEnv('COMPOSER_ROOT_VERSION'); + if (!is_string($version) || $version === '') { + throw new \RuntimeException('COMPOSER_ROOT_VERSION not set or empty'); + } + if (Preg::isMatch('{^(\d+(?:\.\d+)*)-dev$}i', $version, $match)) { + $version = $match[1].'.x-dev'; + } + + return $version; + } } diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index 9ab322084ad1..7c0c61a3116d 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -71,7 +71,7 @@ public function __construct(RepositorySet $repositorySet, ?PlatformRepository $p */ public function findBestCandidate(string $packageName, ?string $targetPackageVersion = null, string $preferredStability = 'stable', $platformRequirementFilter = null, int $repoSetFlags = 0, ?IOInterface $io = null, $showWarnings = true) { - if (!isset(BasePackage::$stabilities[$preferredStability])) { + if (!isset(BasePackage::STABILITIES[$preferredStability])) { // If you get this, maybe you are still relying on the Composer 1.x signature where the 3rd arg was the php version throw new \UnexpectedValueException('Expected a valid stability name as 3rd argument, got '.$preferredStability); } @@ -86,7 +86,7 @@ public function findBestCandidate(string $packageName, ?string $targetPackageVer $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint, $repoSetFlags); - $minPriority = BasePackage::$stabilities[$preferredStability]; + $minPriority = BasePackage::STABILITIES[$preferredStability]; usort($candidates, static function (PackageInterface $a, PackageInterface $b) use ($minPriority) { $aPriority = $a->getStabilityPriority(); $bPriority = $b->getStabilityPriority(); @@ -121,6 +121,7 @@ public function findBestCandidate(string $packageName, ?string $targetPackageVer foreach ($candidates as $pkg) { $reqs = $pkg->getRequires(); + $skip = false; foreach ($reqs as $name => $link) { if (!PlatformRepository::isPlatformPackage($name) || $platformRequirementFilter->isIgnored($name)) { continue; @@ -151,8 +152,8 @@ public function findBestCandidate(string $packageName, ?string $targetPackageVer $isLatestVersion = !isset($alreadySeenNames[$pkg->getName()]); $alreadySeenNames[$pkg->getName()] = true; if ($io !== null && ($showWarnings === true || (is_callable($showWarnings) && $showWarnings($pkg)))) { - $isFirstWarning = !isset($alreadyWarnedNames[$pkg->getName()]); - $alreadyWarnedNames[$pkg->getName()] = true; + $isFirstWarning = !isset($alreadyWarnedNames[$pkg->getName().'/'.$link->getTarget()]); + $alreadyWarnedNames[$pkg->getName().'/'.$link->getTarget()] = true; $latest = $isLatestVersion ? "'s latest version" : ''; $io->writeError( 'Cannot use '.$pkg->getPrettyName().$latest.' '.$pkg->getPrettyVersion().' as it '.$link->getDescription().' '.$link->getTarget().' '.$link->getPrettyConstraint().' which '.$reason.'.', @@ -162,7 +163,11 @@ public function findBestCandidate(string $packageName, ?string $targetPackageVer } // skip candidate - continue 2; + $skip = true; + } + + if ($skip) { + continue; } $package = $pkg; diff --git a/src/Composer/Platform/HhvmDetector.php b/src/Composer/Platform/HhvmDetector.php index 46886c353418..284b0ba8ad25 100644 --- a/src/Composer/Platform/HhvmDetector.php +++ b/src/Composer/Platform/HhvmDetector.php @@ -49,11 +49,7 @@ public function getVersion(): ?string $hhvmPath = $this->executableFinder->find('hhvm'); if ($hhvmPath !== null) { $this->processExecutor = $this->processExecutor ?? new ProcessExecutor(); - $exitCode = $this->processExecutor->execute( - ProcessExecutor::escape($hhvmPath). - ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', - self::$hhvmVersion - ); + $exitCode = $this->processExecutor->execute([$hhvmPath, '--php', '-d', 'hhvm.jit=0', '-r', 'echo HHVM_VERSION;'], self::$hhvmVersion); if ($exitCode !== 0) { self::$hhvmVersion = false; } diff --git a/src/Composer/Platform/Runtime.php b/src/Composer/Platform/Runtime.php index b05af085cb3a..940c02d0fc13 100644 --- a/src/Composer/Platform/Runtime.php +++ b/src/Composer/Platform/Runtime.php @@ -56,9 +56,12 @@ public function hasClass(string $class): bool } /** - * @param class-string $class + * @template T of object * @param mixed[] $arguments * + * @phpstan-param class-string $class + * @phpstan-return T + * * @throws \ReflectionException */ public function construct(string $class, array $arguments = []): object diff --git a/src/Composer/Platform/Version.php b/src/Composer/Platform/Version.php index 8a3f23e4ade6..56abb1bb1cc9 100644 --- a/src/Composer/Platform/Version.php +++ b/src/Composer/Platform/Version.php @@ -20,7 +20,9 @@ class Version { /** - * @param bool $isFips Set by the method + * @param bool $isFips Set by the method + * + * @param-out bool $isFips */ public static function parseOpenssl(string $opensslVersion, ?bool &$isFips): ?string { diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index f594478a6fe0..7cd08060e593 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -20,11 +20,13 @@ use Composer\Package\CompletePackage; use Composer\Package\Locker; use Composer\Package\Package; +use Composer\Package\RootPackageInterface; use Composer\Package\Version\VersionParser; use Composer\PartialComposer; use Composer\Pcre\Preg; use Composer\Repository\RepositoryInterface; use Composer\Repository\InstalledRepository; +use Composer\Repository\RepositoryUtils; use Composer\Repository\RootPackageRepository; use Composer\Package\PackageInterface; use Composer\Package\Link; @@ -98,7 +100,7 @@ public function loadInstalledPlugins(): void { if (!$this->arePluginsDisabled('local')) { $repo = $this->composer->getRepositoryManager()->getLocalRepository(); - $this->loadRepository($repo, false); + $this->loadRepository($repo, false, $this->composer->getPackage()); } if ($this->globalComposer !== null && !$this->arePluginsDisabled('global')) { @@ -445,9 +447,11 @@ public function uninstallPlugin(PluginInterface $plugin): void * * @param RepositoryInterface $repo Repository to scan for plugins to install * + * @phpstan-param ($isGlobalRepo is true ? null : RootPackageInterface) $rootPackage + * * @throws \RuntimeException */ - private function loadRepository(RepositoryInterface $repo, bool $isGlobalRepo): void + private function loadRepository(RepositoryInterface $repo, bool $isGlobalRepo, ?RootPackageInterface $rootPackage = null): void { $packages = $repo->getPackages(); @@ -462,10 +466,28 @@ private function loadRepository(RepositoryInterface $repo, bool $isGlobalRepo): } $sortedPackages = PackageSorter::sortPackages($packages, $weights); + if (!$isGlobalRepo) { + $requiredPackages = RepositoryUtils::filterRequiredPackages($packages, $rootPackage, true); + } + foreach ($sortedPackages as $package) { if (!($package instanceof CompletePackage)) { continue; } + + if (!in_array($package->getType(), ['composer-plugin', 'composer-installer'], true)) { + continue; + } + + if ( + !$isGlobalRepo + && !in_array($package, $requiredPackages, true) + && !$this->isPluginAllowed($package->getName(), false, true, false) + ) { + $this->io->writeError('The "'.$package->getName().'" plugin was not loaded as it is not listed in allow-plugins and is not required by the root package anymore.'); + continue; + } + if ('composer-plugin' === $package->getType()) { $this->registerPackage($package, false, $isGlobalRepo); // Backward compatibility @@ -668,7 +690,7 @@ public function disablePlugins(): void /** * @internal */ - public function isPluginAllowed(string $package, bool $isGlobalPlugin, bool $optional = false): bool + public function isPluginAllowed(string $package, bool $isGlobalPlugin, bool $optional = false, bool $prompt = true): bool { if ($isGlobalPlugin) { $rules = &$this->allowGlobalPluginRules; @@ -703,7 +725,7 @@ public function isPluginAllowed(string $package, bool $isGlobalPlugin, bool $opt return false; } - if ($this->io->isInteractive()) { + if ($this->io->isInteractive() && $prompt) { $composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer; $this->io->writeError(''.$package.($isGlobalPlugin || $this->runningInGlobalDir ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins'); @@ -728,7 +750,15 @@ public function isPluginAllowed(string $package, bool $isGlobalPlugin, bool $opt // persist answer in composer.json if it wasn't simply discarded if ($answer === 'y' || $answer === 'n') { - $composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins.'.$package, $allow); + $allowPlugins = $composer->getConfig()->get('allow-plugins'); + if (is_array($allowPlugins)) { + $allowPlugins[$package] = $allow; + if ($composer->getConfig()->get('sort-packages')) { + ksort($allowPlugins); + } + $composer->getConfig()->getConfigSource()->addConfigSetting('allow-plugins', $allowPlugins); + $composer->getConfig()->merge(['config' => ['allow-plugins' => $allowPlugins]]); + } } return $allow; diff --git a/src/Composer/Plugin/PostFileDownloadEvent.php b/src/Composer/Plugin/PostFileDownloadEvent.php index e789916460dc..a8d9a025f7d3 100644 --- a/src/Composer/Plugin/PostFileDownloadEvent.php +++ b/src/Composer/Plugin/PostFileDownloadEvent.php @@ -59,7 +59,7 @@ class PostFileDownloadEvent extends Event */ public function __construct(string $name, ?string $fileName, ?string $checksum, string $url, string $type, $context = null) { - /** @phpstan-ignore-next-line */ + /** @phpstan-ignore instanceof.alwaysFalse, booleanAnd.alwaysFalse */ if ($context === null && $type instanceof PackageInterface) { $context = $type; $type = 'package'; diff --git a/src/Composer/Question/StrictConfirmationQuestion.php b/src/Composer/Question/StrictConfirmationQuestion.php index c27753b32f27..9cbc74ec5f6c 100644 --- a/src/Composer/Question/StrictConfirmationQuestion.php +++ b/src/Composer/Question/StrictConfirmationQuestion.php @@ -40,7 +40,7 @@ class StrictConfirmationQuestion extends Question */ public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y(?:es)?$/i', string $falseAnswerRegex = '/^no?$/i') { - parent::__construct($question, (bool) $default); + parent::__construct($question, $default); $this->trueAnswerRegex = $trueAnswerRegex; $this->falseAnswerRegex = $falseAnswerRegex; diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index f73f4e9e1c5a..78176fad7588 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -129,7 +129,7 @@ private function getComposerInformation(\SplFileInfo $file): ?BasePackage $package['dist'] = [ 'type' => $fileType, 'url' => strtr($file->getPathname(), '\\', '/'), - 'shasum' => sha1_file($file->getRealPath()), + 'shasum' => hash_file('sha1', $file->getRealPath()), ]; try { diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index ec244b129963..fe93fb7d9dc2 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -432,8 +432,7 @@ private function getVendorNames(): array $uniques = []; foreach ($names as $name) { - // @phpstan-ignore-next-line - $uniques[substr($name, 0, strpos($name, '/'))] = true; + $uniques[explode('/', $name, 2)[0]] = true; } $vendors = array_keys($uniques); @@ -828,7 +827,7 @@ private function hasProviders(): bool /** * @param string $name package name * @param array|null $acceptableStabilities - * @phpstan-param array|null $acceptableStabilities + * @phpstan-param array, BasePackage::STABILITY_*>|null $acceptableStabilities * @param array|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array|null $stabilityFlags * @param array> $alreadyLoaded @@ -998,7 +997,7 @@ public function addPackage(PackageInterface $package) * @param array $packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only * packages matching it will be loaded * @param array|null $acceptableStabilities - * @phpstan-param array|null $acceptableStabilities + * @phpstan-param array, BasePackage::STABILITY_*>|null $acceptableStabilities * @param array|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array|null $stabilityFlags * @param array> $alreadyLoaded @@ -1130,7 +1129,7 @@ private function startCachedAsyncDownload(string $fileName, ?string $packageName * @param string $name package name (must be lowercased already) * @param array $versionData * @param array|null $acceptableStabilities - * @phpstan-param array|null $acceptableStabilities + * @phpstan-param array, BasePackage::STABILITY_*>|null $acceptableStabilities * @param array|null $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array|null $stabilityFlags */ diff --git a/src/Composer/Repository/FilesystemRepository.php b/src/Composer/Repository/FilesystemRepository.php index ec37573f2f97..abb9f125e704 100644 --- a/src/Composer/Repository/FilesystemRepository.php +++ b/src/Composer/Repository/FilesystemRepository.php @@ -329,6 +329,14 @@ private function generateInstalledVersions(InstallationManager $installationMana ksort($versions['versions']); ksort($versions); + foreach ($versions['versions'] as $name => $version) { + foreach (['aliases', 'replaced', 'provided'] as $key) { + if (isset($versions['versions'][$name][$key])) { + sort($versions['versions'][$name][$key], SORT_NATURAL); + } + } + } + return $versions; } diff --git a/src/Composer/Repository/InstalledRepository.php b/src/Composer/Repository/InstalledRepository.php index e3b52079e351..3520fdec67fb 100644 --- a/src/Composer/Repository/InstalledRepository.php +++ b/src/Composer/Repository/InstalledRepository.php @@ -84,7 +84,7 @@ public function findPackagesWithReplacersAndProviders(string $name, $constraint * @param string[] $packagesFound Used internally when recurring * * @return array[] An associative array of arrays as described above. - * @phpstan-return array + * @phpstan-return array|false}> */ public function getDependents($needle, ?ConstraintInterface $constraint = null, bool $invert = false, bool $recurse = true, ?array $packagesFound = null): array { @@ -137,6 +137,7 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null, } } } + unset($needle); } // Require-dev is only relevant for the root package @@ -163,7 +164,7 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null, } // When inverting, we need to check for conflicts of the needles against installed packages - if ($invert && in_array($package->getName(), $needles)) { + if ($invert && in_array($package->getName(), $needles, true)) { foreach ($package->getConflicts() as $link) { foreach ($this->findPackages($link->getTarget()) as $pkg) { $version = new Constraint('=', $pkg->getVersion()); @@ -176,7 +177,7 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null, // List conflicts against X as they may explain why the current version was selected, or explain why it is rejected if the conflict matched when inverting foreach ($package->getConflicts() as $link) { - if (in_array($link->getTarget(), $needles)) { + if (in_array($link->getTarget(), $needles, true)) { foreach ($this->findPackages($link->getTarget()) as $pkg) { $version = new Constraint('=', $pkg->getVersion()); if ($link->getConstraint()->matches($version) === $invert) { @@ -187,7 +188,7 @@ public function getDependents($needle, ?ConstraintInterface $constraint = null, } // When inverting, we need to check for conflicts of the needles' requirements against installed packages - if ($invert && $constraint && in_array($package->getName(), $needles) && $constraint->matches(new Constraint('=', $package->getVersion()))) { + if ($invert && $constraint && in_array($package->getName(), $needles, true) && $constraint->matches(new Constraint('=', $package->getVersion()))) { foreach ($package->getRequires() as $link) { if (PlatformRepository::isPlatformPackage($link->getTarget())) { if ($this->findPackage($link->getTarget(), $link->getConstraint())) { diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 06676e73e60d..0b8d992360fb 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -116,7 +116,7 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config, $this->loader = new ArrayLoader(null, true); $this->url = Platform::expandPath($repoConfig['url']); $this->process = $process ?? new ProcessExecutor($io); - $this->versionGuesser = new VersionGuesser($config, $this->process, new VersionParser()); + $this->versionGuesser = new VersionGuesser($config, $this->process, new VersionParser(), $io); $this->repoConfig = $repoConfig; $this->options = $repoConfig['options'] ?? []; if (!isset($this->options['relative'])) { @@ -181,7 +181,7 @@ protected function initialize(): void if ('none' === $reference) { $package['dist']['reference'] = null; } elseif ('config' === $reference || 'auto' === $reference) { - $package['dist']['reference'] = sha1($json . serialize($this->options)); + $package['dist']['reference'] = hash('sha1', $json . serialize($this->options)); } // copy symlink/relative options to transport options @@ -194,16 +194,16 @@ protected function initialize(): void // carry over the root package version if this path repo is in the same git repository as root package if (!isset($package['version']) && ($rootVersion = Platform::getEnv('COMPOSER_ROOT_VERSION'))) { if ( - 0 === $this->process->execute('git rev-parse HEAD', $ref1, $path) - && 0 === $this->process->execute('git rev-parse HEAD', $ref2) + 0 === $this->process->execute(['git', 'rev-parse', 'HEAD'], $ref1, $path) + && 0 === $this->process->execute(['git', 'rev-parse', 'HEAD'], $ref2) && $ref1 === $ref2 ) { - $package['version'] = $rootVersion; + $package['version'] = $this->versionGuesser->getRootVersionFromEnv(); } } $output = ''; - if ('auto' === $reference && is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute('git log -n1 --pretty=%H'.GitUtil::getNoShowSignatureFlag($this->process), $output, $path)) { + if ('auto' === $reference && is_dir($path . DIRECTORY_SEPARATOR . '.git') && 0 === $this->process->execute(array_merge(['git', 'log', '-n1', '--pretty=%H'], GitUtil::getNoShowSignatureFlags($this->process)), $output, $path)) { $package['dist']['reference'] = trim($output); } diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 2f8f206e8ff6..f5a2eb5ace07 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -110,6 +110,8 @@ protected function initialize(): void { parent::initialize(); + $libraries = []; + $this->versionParser = new VersionParser(); // Add each of the override versions as options. @@ -207,12 +209,12 @@ protected function initialize(): void // librabbitmq version => 0.9.0 if (Preg::isMatch('/^librabbitmq version => (?.+)$/im', $info, $librabbitmqMatches)) { - $this->addLibrary($name.'-librabbitmq', $librabbitmqMatches['version'], 'AMQP librabbitmq version'); + $this->addLibrary($libraries, $name.'-librabbitmq', $librabbitmqMatches['version'], 'AMQP librabbitmq version'); } // AMQP protocol version => 0-9-1 if (Preg::isMatchStrictGroups('/^AMQP protocol version => (?.+)$/im', $info, $protocolMatches)) { - $this->addLibrary($name.'-protocol', str_replace('-', '.', $protocolMatches['version']), 'AMQP protocol version'); + $this->addLibrary($libraries, $name.'-protocol', str_replace('-', '.', $protocolMatches['version']), 'AMQP protocol version'); } break; @@ -221,13 +223,13 @@ protected function initialize(): void // BZip2 Version => 1.0.6, 6-Sept-2010 if (Preg::isMatch('/^BZip2 Version => (?.*),/im', $info, $matches)) { - $this->addLibrary($name, $matches['version']); + $this->addLibrary($libraries, $name, $matches['version']); } break; case 'curl': $curlVersion = $this->runtime->invoke('curl_version'); - $this->addLibrary($name, $curlVersion['version']); + $this->addLibrary($libraries, $name, $curlVersion['version']); $info = $this->runtime->getExtensionInfo($name); @@ -236,25 +238,25 @@ protected function initialize(): void $library = strtolower($sslMatches['library']); if ($library === 'openssl') { $parsedVersion = Version::parseOpenssl($sslMatches['version'], $isFips); - $this->addLibrary($name.'-openssl'.($isFips ? '-fips' : ''), $parsedVersion, 'curl OpenSSL version ('.$parsedVersion.')', [], $isFips ? ['curl-openssl'] : []); + $this->addLibrary($libraries, $name.'-openssl'.($isFips ? '-fips' : ''), $parsedVersion, 'curl OpenSSL version ('.$parsedVersion.')', [], $isFips ? ['curl-openssl'] : []); } else { if ($library === '(securetransport) openssl') { $shortlib = 'securetransport'; } else { $shortlib = $library; } - $this->addLibrary($name.'-'.$shortlib, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', ['curl-openssl']); + $this->addLibrary($libraries, $name.'-'.$shortlib, $sslMatches['version'], 'curl '.$library.' version ('.$sslMatches['version'].')', ['curl-openssl']); } } // libSSH Version => libssh2/1.4.3 if (Preg::isMatchStrictGroups('{^libSSH Version => (?[^/]+)/(?.+?)(?:/.*)?$}im', $info, $sshMatches)) { - $this->addLibrary($name.'-'.strtolower($sshMatches['library']), $sshMatches['version'], 'curl '.$sshMatches['library'].' version'); + $this->addLibrary($libraries, $name.'-'.strtolower($sshMatches['library']), $sshMatches['version'], 'curl '.$sshMatches['library'].' version'); } // ZLib Version => 1.2.8 if (Preg::isMatchStrictGroups('{^ZLib Version => (?.+)$}im', $info, $zlibMatches)) { - $this->addLibrary($name.'-zlib', $zlibMatches['version'], 'curl zlib version'); + $this->addLibrary($libraries, $name.'-zlib', $zlibMatches['version'], 'curl zlib version'); } break; @@ -263,7 +265,7 @@ protected function initialize(): void // timelib version => 2018.03 if (Preg::isMatchStrictGroups('/^timelib version => (?.+)$/im', $info, $timelibMatches)) { - $this->addLibrary($name.'-timelib', $timelibMatches['version'], 'date timelib version'); + $this->addLibrary($libraries, $name.'-timelib', $timelibMatches['version'], 'date timelib version'); } // Timezone Database => internal @@ -272,9 +274,9 @@ protected function initialize(): void if (Preg::isMatchStrictGroups('/^"Olson" Timezone Database Version => (?.+?)(?:\.system)?$/im', $info, $zoneinfoMatches)) { // If the timezonedb is provided by ext/timezonedb, register that version as a replacement if ($external && in_array('timezonedb', $loadedExtensions, true)) { - $this->addLibrary('timezonedb-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date (replaced by timezonedb)', [$name.'-zoneinfo']); + $this->addLibrary($libraries, 'timezonedb-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date (replaced by timezonedb)', [$name.'-zoneinfo']); } else { - $this->addLibrary($name.'-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date'); + $this->addLibrary($libraries, $name.'-zoneinfo', $zoneinfoMatches['version'], 'zoneinfo ("Olson") database for date'); } } } @@ -285,39 +287,39 @@ protected function initialize(): void // libmagic => 537 if (Preg::isMatch('/^libmagic => (?.+)$/im', $info, $magicMatches)) { - $this->addLibrary($name.'-libmagic', $magicMatches['version'], 'fileinfo libmagic version'); + $this->addLibrary($libraries, $name.'-libmagic', $magicMatches['version'], 'fileinfo libmagic version'); } break; case 'gd': - $this->addLibrary($name, $this->runtime->getConstant('GD_VERSION')); + $this->addLibrary($libraries, $name, $this->runtime->getConstant('GD_VERSION')); $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatchStrictGroups('/^libJPEG Version => (?.+?)(?: compatible)?$/im', $info, $libjpegMatches)) { - $this->addLibrary($name.'-libjpeg', Version::parseLibjpeg($libjpegMatches['version']), 'libjpeg version for gd'); + $this->addLibrary($libraries, $name.'-libjpeg', Version::parseLibjpeg($libjpegMatches['version']), 'libjpeg version for gd'); } if (Preg::isMatchStrictGroups('/^libPNG Version => (?.+)$/im', $info, $libpngMatches)) { - $this->addLibrary($name.'-libpng', $libpngMatches['version'], 'libpng version for gd'); + $this->addLibrary($libraries, $name.'-libpng', $libpngMatches['version'], 'libpng version for gd'); } if (Preg::isMatchStrictGroups('/^FreeType Version => (?.+)$/im', $info, $freetypeMatches)) { - $this->addLibrary($name.'-freetype', $freetypeMatches['version'], 'freetype version for gd'); + $this->addLibrary($libraries, $name.'-freetype', $freetypeMatches['version'], 'freetype version for gd'); } if (Preg::isMatchStrictGroups('/^libXpm Version => (?\d+)$/im', $info, $libxpmMatches)) { - $this->addLibrary($name.'-libxpm', Version::convertLibxpmVersionId((int) $libxpmMatches['versionId']), 'libxpm version for gd'); + $this->addLibrary($libraries, $name.'-libxpm', Version::convertLibxpmVersionId((int) $libxpmMatches['versionId']), 'libxpm version for gd'); } break; case 'gmp': - $this->addLibrary($name, $this->runtime->getConstant('GMP_VERSION')); + $this->addLibrary($libraries, $name, $this->runtime->getConstant('GMP_VERSION')); break; case 'iconv': - $this->addLibrary($name, $this->runtime->getConstant('ICONV_VERSION')); + $this->addLibrary($libraries, $name, $this->runtime->getConstant('ICONV_VERSION')); break; case 'intl': @@ -326,47 +328,49 @@ protected function initialize(): void $description = 'The ICU unicode and globalization support library'; // Truthy check is for testing only so we can make the condition fail if ($this->runtime->hasConstant('INTL_ICU_VERSION')) { - $this->addLibrary('icu', $this->runtime->getConstant('INTL_ICU_VERSION'), $description); + $this->addLibrary($libraries, 'icu', $this->runtime->getConstant('INTL_ICU_VERSION'), $description); } elseif (Preg::isMatch('/^ICU version => (?.+)$/im', $info, $matches)) { - $this->addLibrary('icu', $matches['version'], $description); + $this->addLibrary($libraries, 'icu', $matches['version'], $description); } // ICU TZData version => 2019c if (Preg::isMatchStrictGroups('/^ICU TZData version => (?.*)$/im', $info, $zoneinfoMatches) && null !== ($version = Version::parseZoneinfoVersion($zoneinfoMatches['version']))) { - $this->addLibrary('icu-zoneinfo', $version, 'zoneinfo ("Olson") database for icu'); + $this->addLibrary($libraries, 'icu-zoneinfo', $version, 'zoneinfo ("Olson") database for icu'); } // Add a separate version for the CLDR library version if ($this->runtime->hasClass('ResourceBundle')) { $resourceBundle = $this->runtime->invoke(['ResourceBundle', 'create'], ['root', 'ICUDATA', false]); if ($resourceBundle !== null) { - $this->addLibrary('icu-cldr', $resourceBundle->get('Version'), 'ICU CLDR project version'); + $this->addLibrary($libraries, 'icu-cldr', $resourceBundle->get('Version'), 'ICU CLDR project version'); } } if ($this->runtime->hasClass('IntlChar')) { - $this->addLibrary('icu-unicode', implode('.', array_slice($this->runtime->invoke(['IntlChar', 'getUnicodeVersion']), 0, 3)), 'ICU unicode version'); + $this->addLibrary($libraries, 'icu-unicode', implode('.', array_slice($this->runtime->invoke(['IntlChar', 'getUnicodeVersion']), 0, 3)), 'ICU unicode version'); } break; case 'imagick': + // @phpstan-ignore staticMethod.dynamicCall (called like this for mockability) $imageMagickVersion = $this->runtime->construct('Imagick')->getVersion(); // 6.x: ImageMagick 6.2.9 08/24/06 Q16 http://www.imagemagick.org // 7.x: ImageMagick 7.0.8-34 Q16 x86_64 2019-03-23 https://imagemagick.org - Preg::match('/^ImageMagick (?[\d.]+)(?:-(?\d+))?/', $imageMagickVersion['versionString'], $matches); - $version = $matches['version']; - if (isset($matches['patch'])) { - $version .= '.'.$matches['patch']; - } + if (Preg::isMatch('/^ImageMagick (?[\d.]+)(?:-(?\d+))?/', $imageMagickVersion['versionString'], $matches)) { + $version = $matches['version']; + if (isset($matches['patch'])) { + $version .= '.'.$matches['patch']; + } - $this->addLibrary($name.'-imagemagick', $version, null, ['imagick']); + $this->addLibrary($libraries, $name.'-imagemagick', $version, null, ['imagick']); + } break; case 'ldap': $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatchStrictGroups('/^Vendor Version => (?\d+)$/im', $info, $matches) && Preg::isMatchStrictGroups('/^Vendor Name => (?.+)$/im', $info, $vendorMatches)) { - $this->addLibrary($name.'-'.strtolower($vendorMatches['vendor']), Version::convertOpenldapVersionId((int) $matches['versionId']), $vendorMatches['vendor'].' version of ldap'); + $this->addLibrary($libraries, $name.'-'.strtolower($vendorMatches['vendor']), Version::convertOpenldapVersionId((int) $matches['versionId']), $vendorMatches['vendor'].' version of ldap'); } break; @@ -375,7 +379,7 @@ protected function initialize(): void $libxmlProvides = array_map(static function ($extension): string { return $extension . '-libxml'; }, array_intersect($loadedExtensions, ['dom', 'simplexml', 'xml', 'xmlreader', 'xmlwriter'])); - $this->addLibrary($name, $this->runtime->getConstant('LIBXML_DOTTED_VERSION'), 'libxml library version', [], $libxmlProvides); + $this->addLibrary($libraries, $name, $this->runtime->getConstant('LIBXML_DOTTED_VERSION'), 'libxml library version', [], $libxmlProvides); break; @@ -384,16 +388,16 @@ protected function initialize(): void // libmbfl version => 1.3.2 if (Preg::isMatch('/^libmbfl version => (?.+)$/im', $info, $libmbflMatches)) { - $this->addLibrary($name.'-libmbfl', $libmbflMatches['version'], 'mbstring libmbfl version'); + $this->addLibrary($libraries, $name.'-libmbfl', $libmbflMatches['version'], 'mbstring libmbfl version'); } if ($this->runtime->hasConstant('MB_ONIGURUMA_VERSION')) { - $this->addLibrary($name.'-oniguruma', $this->runtime->getConstant('MB_ONIGURUMA_VERSION'), 'mbstring oniguruma version'); + $this->addLibrary($libraries, $name.'-oniguruma', $this->runtime->getConstant('MB_ONIGURUMA_VERSION'), 'mbstring oniguruma version'); // Multibyte regex (oniguruma) version => 5.9.5 // oniguruma version => 6.9.0 } elseif (Preg::isMatch('/^(?:oniguruma|Multibyte regex \(oniguruma\)) version => (?.+)$/im', $info, $onigurumaMatches)) { - $this->addLibrary($name.'-oniguruma', $onigurumaMatches['version'], 'mbstring oniguruma version'); + $this->addLibrary($libraries, $name.'-oniguruma', $onigurumaMatches['version'], 'mbstring oniguruma version'); } break; @@ -403,7 +407,7 @@ protected function initialize(): void // libmemcached version => 1.0.18 if (Preg::isMatch('/^libmemcached version => (?.+)$/im', $info, $matches)) { - $this->addLibrary($name.'-libmemcached', $matches['version'], 'libmemcached version'); + $this->addLibrary($libraries, $name.'-libmemcached', $matches['version'], 'libmemcached version'); } break; @@ -411,18 +415,18 @@ protected function initialize(): void // OpenSSL 1.1.1g 21 Apr 2020 if (Preg::isMatchStrictGroups('{^(?:OpenSSL|LibreSSL)?\s*(?\S+)}i', $this->runtime->getConstant('OPENSSL_VERSION_TEXT'), $matches)) { $parsedVersion = Version::parseOpenssl($matches['version'], $isFips); - $this->addLibrary($name.($isFips ? '-fips' : ''), $parsedVersion, $this->runtime->getConstant('OPENSSL_VERSION_TEXT'), [], $isFips ? [$name] : []); + $this->addLibrary($libraries, $name.($isFips ? '-fips' : ''), $parsedVersion, $this->runtime->getConstant('OPENSSL_VERSION_TEXT'), [], $isFips ? [$name] : []); } break; case 'pcre': - $this->addLibrary($name, Preg::replace('{^(\S+).*}', '$1', $this->runtime->getConstant('PCRE_VERSION'))); + $this->addLibrary($libraries, $name, Preg::replace('{^(\S+).*}', '$1', $this->runtime->getConstant('PCRE_VERSION'))); $info = $this->runtime->getExtensionInfo($name); // PCRE Unicode Version => 12.1.0 if (Preg::isMatchStrictGroups('/^PCRE Unicode Version => (?.+)$/im', $info, $pcreUnicodeMatches)) { - $this->addLibrary($name.'-unicode', $pcreUnicodeMatches['version'], 'PCRE Unicode version support'); + $this->addLibrary($libraries, $name.'-unicode', $pcreUnicodeMatches['version'], 'PCRE Unicode version support'); } break; @@ -432,7 +436,7 @@ protected function initialize(): void $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatchStrictGroups('/^(?:Client API version|Version) => mysqlnd (?.+?) /mi', $info, $matches)) { - $this->addLibrary($name.'-mysqlnd', $matches['version'], 'mysqlnd library version for '.$name); + $this->addLibrary($libraries, $name.'-mysqlnd', $matches['version'], 'mysqlnd library version for '.$name); } break; @@ -440,17 +444,17 @@ protected function initialize(): void $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatchStrictGroups('/^libmongoc bundled version => (?.+)$/im', $info, $libmongocMatches)) { - $this->addLibrary($name.'-libmongoc', $libmongocMatches['version'], 'libmongoc version of mongodb'); + $this->addLibrary($libraries, $name.'-libmongoc', $libmongocMatches['version'], 'libmongoc version of mongodb'); } if (Preg::isMatchStrictGroups('/^libbson bundled version => (?.+)$/im', $info, $libbsonMatches)) { - $this->addLibrary($name.'-libbson', $libbsonMatches['version'], 'libbson version of mongodb'); + $this->addLibrary($libraries, $name.'-libbson', $libbsonMatches['version'], 'libbson version of mongodb'); } break; case 'pgsql': if ($this->runtime->hasConstant('PGSQL_LIBPQ_VERSION')) { - $this->addLibrary('pgsql-libpq', $this->runtime->getConstant('PGSQL_LIBPQ_VERSION'), 'libpq for pgsql'); + $this->addLibrary($libraries, 'pgsql-libpq', $this->runtime->getConstant('PGSQL_LIBPQ_VERSION'), 'libpq for pgsql'); break; } // intentional fall-through to next case... @@ -459,7 +463,7 @@ protected function initialize(): void $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^PostgreSQL\(libpq\) Version => (?.*)$/im', $info, $matches)) { - $this->addLibrary($name.'-libpq', $matches['version'], 'libpq for '.$name); + $this->addLibrary($libraries, $name.'-libpq', $matches['version'], 'libpq for '.$name); } break; @@ -469,7 +473,7 @@ protected function initialize(): void // Used Library => Compiled => Linked // libpq => 14.3 (Ubuntu 14.3-1.pgdg22.04+1) => 15.0.2 if (Preg::isMatch('/^libpq => (?.+) => (?.+)$/im', $info, $matches)) { - $this->addLibrary($name.'-libpq', $matches['linked'], 'libpq for '.$name); + $this->addLibrary($libraries, $name.'-libpq', $matches['linked'], 'libpq for '.$name); } break; @@ -485,14 +489,15 @@ protected function initialize(): void * pre-release ID in practice is always 0xff even for RCs etc, so we ignore it */ $libRdKafkaVersionInt = $this->runtime->getConstant('RD_KAFKA_VERSION'); - $this->addLibrary($name.'-librdkafka', sprintf('%d.%d.%d', ($libRdKafkaVersionInt & 0xFF000000) >> 24, ($libRdKafkaVersionInt & 0x00FF0000) >> 16, ($libRdKafkaVersionInt & 0x0000FF00) >> 8), 'librdkafka for '.$name); + $this->addLibrary($libraries, $name.'-librdkafka', sprintf('%d.%d.%d', ($libRdKafkaVersionInt & 0xFF000000) >> 24, ($libRdKafkaVersionInt & 0x00FF0000) >> 16, ($libRdKafkaVersionInt & 0x0000FF00) >> 8), 'librdkafka for '.$name); } break; case 'libsodium': case 'sodium': if ($this->runtime->hasConstant('SODIUM_LIBRARY_VERSION')) { - $this->addLibrary('libsodium', $this->runtime->getConstant('SODIUM_LIBRARY_VERSION')); + $this->addLibrary($libraries, 'libsodium', $this->runtime->getConstant('SODIUM_LIBRARY_VERSION')); + $this->addLibrary($libraries, 'libsodium', $this->runtime->getConstant('SODIUM_LIBRARY_VERSION')); } break; @@ -501,7 +506,7 @@ protected function initialize(): void $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^SQLite Library => (?.+)$/im', $info, $matches)) { - $this->addLibrary($name.'-sqlite', $matches['version']); + $this->addLibrary($libraries, $name.'-sqlite', $matches['version']); } break; @@ -509,16 +514,16 @@ protected function initialize(): void $info = $this->runtime->getExtensionInfo($name); if (Preg::isMatch('/^libssh2 version => (?.+)$/im', $info, $matches)) { - $this->addLibrary($name.'-libssh2', $matches['version']); + $this->addLibrary($libraries, $name.'-libssh2', $matches['version']); } break; case 'xsl': - $this->addLibrary('libxslt', $this->runtime->getConstant('LIBXSLT_DOTTED_VERSION'), null, ['xsl']); + $this->addLibrary($libraries, 'libxslt', $this->runtime->getConstant('LIBXSLT_DOTTED_VERSION'), null, ['xsl']); $info = $this->runtime->getExtensionInfo('xsl'); if (Preg::isMatch('/^libxslt compiled against libxml Version => (?.+)$/im', $info, $matches)) { - $this->addLibrary('libxslt-libxml', $matches['version'], 'libxml version libxslt is compiled against'); + $this->addLibrary($libraries, 'libxslt-libxml', $matches['version'], 'libxml version libxslt is compiled against'); } break; @@ -526,23 +531,23 @@ protected function initialize(): void $info = $this->runtime->getExtensionInfo('yaml'); if (Preg::isMatch('/^LibYAML Version => (?.+)$/im', $info, $matches)) { - $this->addLibrary($name.'-libyaml', $matches['version'], 'libyaml version of yaml'); + $this->addLibrary($libraries, $name.'-libyaml', $matches['version'], 'libyaml version of yaml'); } break; case 'zip': if ($this->runtime->hasConstant('LIBZIP_VERSION', 'ZipArchive')) { - $this->addLibrary($name.'-libzip', $this->runtime->getConstant('LIBZIP_VERSION', 'ZipArchive'), null, ['zip']); + $this->addLibrary($libraries, $name.'-libzip', $this->runtime->getConstant('LIBZIP_VERSION', 'ZipArchive'), null, ['zip']); } break; case 'zlib': if ($this->runtime->hasConstant('ZLIB_VERSION')) { - $this->addLibrary($name, $this->runtime->getConstant('ZLIB_VERSION')); + $this->addLibrary($libraries, $name, $this->runtime->getConstant('ZLIB_VERSION')); // Linked Version => 1.2.8 } elseif (Preg::isMatch('/^Linked Version => (?.+)$/im', $this->runtime->getExtensionInfo($name), $matches)) { - $this->addLibrary($name, $matches['version']); + $this->addLibrary($libraries, $name, $matches['version']); } break; @@ -661,6 +666,7 @@ private function addExtension(string $name, string $prettyVersion): void $packageName = $this->buildPackageName($name); $ext = new CompletePackage($packageName, $version, $prettyVersion); $ext->setDescription('The '.$name.' PHP extension'.$extraDescription); + $ext->setType('php-ext'); if ($name === 'uuid') { $ext->setReplaces([ @@ -677,10 +683,11 @@ private function buildPackageName(string $name): string } /** - * @param string[] $replaces - * @param string[] $provides + * @param array $libraries + * @param array $replaces + * @param array $provides */ - private function addLibrary(string $name, ?string $prettyVersion, ?string $description = null, array $replaces = [], array $provides = []): void + private function addLibrary(array &$libraries, string $name, ?string $prettyVersion, ?string $description = null, array $replaces = [], array $provides = []): void { if (null === $prettyVersion) { return; @@ -691,6 +698,13 @@ private function addLibrary(string $name, ?string $prettyVersion, ?string $descr return; } + // avoid adding the same lib twice even if two conflicting extensions provide the same lib + // see https://github.com/composer/composer/issues/12082 + if (isset($libraries['lib-'.$name])) { + return; + } + $libraries['lib-'.$name] = true; + if ($description === null) { $description = 'The '.$name.' library'; } diff --git a/src/Composer/Repository/RepositoryFactory.php b/src/Composer/Repository/RepositoryFactory.php index 62e9183246ff..52da0d604e76 100644 --- a/src/Composer/Repository/RepositoryFactory.php +++ b/src/Composer/Repository/RepositoryFactory.php @@ -178,7 +178,7 @@ private static function createRepos(RepositoryManager $rm, array $repoConfigs): /** * @param int|string $index * @param array{url?: string} $repo - * @param array $existingRepos + * @param array $existingRepos */ public static function generateRepositoryName($index, array $repo, array $existingRepos): string { diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index c26248801a26..f90c96d50797 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -72,12 +72,13 @@ public function getPackages(); * - The namesFound returned are names which should be considered as canonically found in this repository, that should not be looked up in any further lower priority repositories * * @param ConstraintInterface[] $packageNameMap package names pointing to constraints - * @param array $acceptableStabilities array of stability => BasePackage::STABILITY_* value + * @param array $acceptableStabilities array of stability => BasePackage::STABILITY_* value * @param array $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @param array> $alreadyLoaded an array of package name => package version => package * * @return array * + * @phpstan-param array, BasePackage::STABILITY_*> $acceptableStabilities * @phpstan-param array $packageNameMap * @phpstan-return array{namesFound: array, packages: array} */ @@ -103,7 +104,7 @@ public function search(string $query, int $mode = 0, ?string $type = null); * @param string $packageName package name which must be provided * * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...') - * @phpstan-return array + * @phpstan-return array */ public function getProviders(string $packageName); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 48cf424a4f8a..dcde3632746e 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -65,7 +65,7 @@ class RepositorySet /** * @var int[] array of stability => BasePackage::STABILITY_* value - * @phpstan-var array + * @phpstan-var array, BasePackage::STABILITY_*> */ private $acceptableStabilities; @@ -96,6 +96,7 @@ class RepositorySet * passing minimumStability is all you need to worry about. The rest is for advanced pool creation including * aliases, pinned references and other special cases. * + * @param key-of $minimumStability * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @phpstan-param array $stabilityFlags * @param array[] $rootAliases @@ -112,8 +113,8 @@ public function __construct(string $minimumStability = 'stable', array $stabilit $this->rootReferences = $rootReferences; $this->acceptableStabilities = []; - foreach (BasePackage::$stabilities as $stability => $value) { - if ($value <= BasePackage::$stabilities[$minimumStability]) { + foreach (BasePackage::STABILITIES as $stability => $value) { + if ($value <= BasePackage::STABILITIES[$minimumStability]) { $this->acceptableStabilities[$stability] = $value; } } @@ -195,7 +196,7 @@ public function findPackages(string $name, ?ConstraintInterface $constraint = nu } } else { foreach ($this->repositories as $repository) { - $result = $repository->loadPackages([$name => $constraint], $ignoreStability ? BasePackage::$stabilities : $this->acceptableStabilities, $ignoreStability ? [] : $this->stabilityFlags); + $result = $repository->loadPackages([$name => $constraint], $ignoreStability ? BasePackage::STABILITIES : $this->acceptableStabilities, $ignoreStability ? [] : $this->stabilityFlags); $packages[] = $result['packages']; foreach ($result['namesFound'] as $nameFound) { @@ -283,7 +284,7 @@ private function getSecurityAdvisoriesForConstraints(array $packageConstraintMap /** * @return array[] an array with the provider name as key and value of array('name' => '...', 'description' => '...', 'type' => '...') - * @phpstan-return array + * @phpstan-return array */ public function getProviders(string $packageName): array { @@ -300,8 +301,8 @@ public function getProviders(string $packageName): array /** * Check for each given package name whether it would be accepted by this RepositorySet in the given $stability * - * @param string[] $names - * @param string $stability one of 'stable', 'RC', 'beta', 'alpha' or 'dev' + * @param string[] $names + * @param key-of $stability one of 'stable', 'RC', 'beta', 'alpha' or 'dev' */ public function isPackageAcceptable(array $names, string $stability): bool { @@ -310,10 +311,15 @@ public function isPackageAcceptable(array $names, string $stability): bool /** * Create a pool for dependency resolution from the packages in this repository set. + * + * @param list $ignoredTypes Packages of those types are ignored + * @param list|null $allowedTypes Only packages of those types are allowed if set to non-null */ - public function createPool(Request $request, IOInterface $io, ?EventDispatcher $eventDispatcher = null, ?PoolOptimizer $poolOptimizer = null): Pool + public function createPool(Request $request, IOInterface $io, ?EventDispatcher $eventDispatcher = null, ?PoolOptimizer $poolOptimizer = null, array $ignoredTypes = [], ?array $allowedTypes = null): Pool { $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $io, $eventDispatcher, $poolOptimizer, $this->temporaryConstraints); + $poolBuilder->setIgnoredTypes($ignoredTypes); + $poolBuilder->setAllowedTypes($allowedTypes); foreach ($this->repositories as $repo) { if (($repo instanceof InstalledRepositoryInterface || $repo instanceof InstalledRepository) && !$this->allowInstalledRepositories) { diff --git a/src/Composer/Repository/RepositoryUtils.php b/src/Composer/Repository/RepositoryUtils.php index 62e1c5b0f59f..e6960c63d0c1 100644 --- a/src/Composer/Repository/RepositoryUtils.php +++ b/src/Composer/Repository/RepositoryUtils.php @@ -24,23 +24,28 @@ class RepositoryUtils /** * Find all of $packages which are required by $requirer, either directly or transitively * - * Require-dev is ignored + * Require-dev is ignored by default, you can enable the require-dev of the initial $requirer + * packages by passing $includeRequireDev=true, but require-dev of transitive dependencies + * are always ignored. * * @template T of PackageInterface * @param array $packages * @param list $bucket Do not pass this in, only used to avoid recursion with circular deps * @return list */ - public static function filterRequiredPackages(array $packages, PackageInterface $requirer, array $bucket = []): array + public static function filterRequiredPackages(array $packages, PackageInterface $requirer, bool $includeRequireDev = false, array $bucket = []): array { $requires = $requirer->getRequires(); + if ($includeRequireDev) { + $requires = array_merge($requires, $requirer->getDevRequires()); + } foreach ($packages as $candidate) { foreach ($candidate->getNames() as $name) { if (isset($requires[$name])) { if (!in_array($candidate, $bucket, true)) { $bucket[] = $candidate; - $bucket = self::filterRequiredPackages($packages, $candidate, $bucket); + $bucket = self::filterRequiredPackages($packages, $candidate, false, $bucket); } break; } diff --git a/src/Composer/Repository/Vcs/FossilDriver.php b/src/Composer/Repository/Vcs/FossilDriver.php index e55b0d3acd74..8a305216c58c 100644 --- a/src/Composer/Repository/Vcs/FossilDriver.php +++ b/src/Composer/Repository/Vcs/FossilDriver.php @@ -71,7 +71,7 @@ public function initialize(): void */ protected function checkFossil(): void { - if (0 !== $this->process->execute('fossil version', $ignoredOutput)) { + if (0 !== $this->process->execute(['fossil', 'version'], $ignoredOutput)) { throw new \RuntimeException("fossil was not found, check that it is installed and in your PATH env.\n\n" . $this->process->getErrorOutput()); } } @@ -81,6 +81,8 @@ protected function checkFossil(): void */ protected function updateLocalRepo(): void { + assert($this->repoFile !== null); + $fs = new Filesystem(); $fs->ensureDirectoryExists($this->checkoutDir); @@ -89,8 +91,8 @@ protected function updateLocalRepo(): void } // update the repo if it is a valid fossil repository - if (is_file($this->repoFile) && is_dir($this->checkoutDir) && 0 === $this->process->execute('fossil info', $output, $this->checkoutDir)) { - if (0 !== $this->process->execute('fossil pull', $output, $this->checkoutDir)) { + if (is_file($this->repoFile) && is_dir($this->checkoutDir) && 0 === $this->process->execute(['fossil', 'info'], $output, $this->checkoutDir)) { + if (0 !== $this->process->execute(['fossil', 'pull'], $output, $this->checkoutDir)) { $this->io->writeError('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')'); } } else { @@ -100,13 +102,13 @@ protected function updateLocalRepo(): void $fs->ensureDirectoryExists($this->checkoutDir); - if (0 !== $this->process->execute(sprintf('fossil clone -- %s %s', ProcessExecutor::escape($this->url), ProcessExecutor::escape($this->repoFile)), $output)) { + if (0 !== $this->process->execute(['fossil', 'clone', '--', $this->url, $this->repoFile], $output)) { $output = $this->process->getErrorOutput(); throw new \RuntimeException('Failed to clone '.$this->url.' to repository ' . $this->repoFile . "\n\n" .$output); } - if (0 !== $this->process->execute(sprintf('fossil open --nested -- %s', ProcessExecutor::escape($this->repoFile)), $output, $this->checkoutDir)) { + if (0 !== $this->process->execute(['fossil', 'open', '--nested', '--', $this->repoFile], $output, $this->checkoutDir)) { $output = $this->process->getErrorOutput(); throw new \RuntimeException('Failed to open repository '.$this->repoFile.' in ' . $this->checkoutDir . "\n\n" .$output); @@ -155,10 +157,9 @@ public function getDist(string $identifier): ?array */ public function getFileContent(string $file, string $identifier): ?string { - $command = sprintf('fossil cat -r %s -- %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); - $this->process->execute($command, $content, $this->checkoutDir); + $this->process->execute(['fossil', 'cat', '-r', $identifier, '--', $file], $content, $this->checkoutDir); - if (!trim($content)) { + if ('' === trim($content)) { return null; } @@ -170,7 +171,7 @@ public function getFileContent(string $file, string $identifier): ?string */ public function getChangeDate(string $identifier): ?\DateTimeImmutable { - $this->process->execute('fossil finfo -b -n 1 composer.json', $output, $this->checkoutDir); + $this->process->execute(['fossil', 'finfo', '-b', '-n', '1', 'composer.json'], $output, $this->checkoutDir); [, $date] = explode(' ', trim($output), 3); return new \DateTimeImmutable($date, new \DateTimeZone('UTC')); @@ -184,7 +185,7 @@ public function getTags(): array if (null === $this->tags) { $tags = []; - $this->process->execute('fossil tag list', $output, $this->checkoutDir); + $this->process->execute(['fossil', 'tag', 'list'], $output, $this->checkoutDir); foreach ($this->process->splitLines($output) as $tag) { $tags[$tag] = $tag; } @@ -203,7 +204,7 @@ public function getBranches(): array if (null === $this->branches) { $branches = []; - $this->process->execute('fossil branch list', $output, $this->checkoutDir); + $this->process->execute(['fossil', 'branch', 'list'], $output, $this->checkoutDir); foreach ($this->process->splitLines($output) as $branch) { $branch = trim(Preg::replace('/^\*/', '', trim($branch))); $branches[$branch] = $branch; @@ -237,7 +238,7 @@ public static function supports(IOInterface $io, Config $config, string $url, bo $process = new ProcessExecutor($io); // check whether there is a fossil repo in that path - if ($process->execute('fossil info', $output, $url) === 0) { + if ($process->execute(['fossil', 'info'], $output, $url) === 0) { return true; } } diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index 75ede6910d04..7be1faba058d 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -52,7 +52,7 @@ public function initialize(): void throw new \RuntimeException('GitDriver requires a usable cache directory, and it looks like you set it to be disabled'); } - $this->repoDir = $this->config->get('cache-vcs-dir') . '/' . Preg::replace('{[^a-z0-9.]}i', '-', $this->url) . '/'; + $this->repoDir = $this->config->get('cache-vcs-dir') . '/' . Preg::replace('{[^a-z0-9.]}i', '-', Url::sanitize($this->url)) . '/'; GitUtil::cleanEnv(); @@ -102,7 +102,7 @@ public function getRootIdentifier(): string } // select currently checked out branch as default branch - $this->process->execute('git branch --no-color', $output, $this->repoDir); + $this->process->execute(['git', 'branch', '--no-color'], $output, $this->repoDir); $branches = $this->process->splitLines($output); if (!in_array('* master', $branches)) { foreach ($branches as $branch) { @@ -150,10 +150,9 @@ public function getFileContent(string $file, string $identifier): ?string throw new \RuntimeException('Invalid git identifier detected. Identifier must not start with a -, given: ' . $identifier); } - $resource = sprintf('%s:%s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); - $this->process->execute(sprintf('git show %s', $resource), $content, $this->repoDir); + $this->process->execute(['git', 'show', $identifier.':'.$file], $content, $this->repoDir); - if (!trim($content)) { + if (trim($content) === '') { return null; } @@ -165,10 +164,7 @@ public function getFileContent(string $file, string $identifier): ?string */ public function getChangeDate(string $identifier): ?\DateTimeImmutable { - $this->process->execute(sprintf( - 'git -c log.showSignature=false log -1 --format=%%at %s', - ProcessExecutor::escape($identifier) - ), $output, $this->repoDir); + $this->process->execute(['git', '-c', 'log.showSignature=false', 'log', '-1', '--format=%at', $identifier], $output, $this->repoDir); return new \DateTimeImmutable('@'.trim($output), new \DateTimeZone('UTC')); } @@ -181,10 +177,10 @@ public function getTags(): array if (null === $this->tags) { $this->tags = []; - $this->process->execute('git show-ref --tags --dereference', $output, $this->repoDir); - foreach ($output = $this->process->splitLines($output) as $tag) { - if ($tag && Preg::isMatch('{^([a-f0-9]{40}) refs/tags/(\S+?)(\^\{\})?$}', $tag, $match)) { - $this->tags[$match[2]] = (string) $match[1]; + $this->process->execute(['git', 'show-ref', '--tags', '--dereference'], $output, $this->repoDir); + foreach ($this->process->splitLines($output) as $tag) { + if ($tag !== '' && Preg::isMatch('{^([a-f0-9]{40}) refs/tags/(\S+?)(\^\{\})?$}', $tag, $match)) { + $this->tags[$match[2]] = $match[1]; } } } @@ -200,9 +196,9 @@ public function getBranches(): array if (null === $this->branches) { $branches = []; - $this->process->execute('git branch --no-color --no-abbrev -v', $output, $this->repoDir); + $this->process->execute(['git', 'branch', '--no-color', '--no-abbrev', '-v'], $output, $this->repoDir); foreach ($this->process->splitLines($output) as $branch) { - if ($branch && !Preg::isMatch('{^ *[^/]+/HEAD }', $branch)) { + if ($branch !== '' && !Preg::isMatch('{^ *[^/]+/HEAD }', $branch)) { if (Preg::isMatchStrictGroups('{^(?:\* )? *(\S+) *([a-f0-9]+)(?: .*)?$}', $branch, $match) && $match[1][0] !== '-') { $branches[$match[1]] = $match[2]; } @@ -233,9 +229,10 @@ public static function supports(IOInterface $io, Config $config, string $url, bo $process = new ProcessExecutor($io); // check whether there is a git repo in that path - if ($process->execute('git tag', $output, $url) === 0) { + if ($process->execute(['git', 'tag'], $output, $url) === 0) { return true; } + GitUtil::checkForRepoOwnershipError($process->getErrorOutput(), $url); } if (!$deep) { @@ -246,9 +243,7 @@ public static function supports(IOInterface $io, Config $config, string $url, bo GitUtil::cleanEnv(); try { - $gitUtil->runCommand(static function ($url): string { - return 'git ls-remote --heads -- ' . ProcessExecutor::escape($url); - }, $url, sys_get_temp_dir()); + $gitUtil->runCommands([['git', 'ls-remote', '--heads', '--', '%url%']], $url, sys_get_temp_dir()); } catch (\RuntimeException $e) { return false; } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 44766a185543..97a334f94e5e 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -63,8 +63,6 @@ public function initialize(): void throw new \InvalidArgumentException(sprintf('The GitHub repository URL %s is invalid.', $this->url)); } - assert(is_string($match[3])); - assert(is_string($match[4])); $this->owner = $match[3]; $this->repository = $match[4]; $this->originUrl = strtolower($match[1] ?? (string) $match[2]); @@ -237,7 +235,7 @@ private function getFundingInfo() $key = $match[1]; continue; } - if (Preg::isMatchStrictGroups('{^\[(.*)\](?:\s*#.*)?$}', $match[2], $match2)) { + if (Preg::isMatchStrictGroups('{^\[(.*?)\](?:\s*#.*)?$}', $match[2], $match2)) { foreach (array_map('trim', Preg::split('{[\'"]?\s*,\s*[\'"]?}', $match2[1])) as $item) { $result[] = ['type' => $match[1], 'url' => trim($item, '"\' ')]; } @@ -286,6 +284,9 @@ private function getFundingInfo() case 'community_bridge': $result[$key]['url'] = 'https://funding.communitybridge.org/projects/' . basename($item['url']); break; + case 'buy_me_a_coffee': + $result[$key]['url'] = 'https://www.buymeacoffee.com/' . basename($item['url']); + break; } } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 3721419b1788..09fb4258c3ca 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -97,8 +97,6 @@ public function initialize(): void throw new \InvalidArgumentException(sprintf('The GitLab repository URL %s is invalid. It must be the HTTP URL of a GitLab project.', $this->url)); } - assert(is_string($match['parts'])); - assert(is_string($match['repo'])); $guessedDomain = $match['domain'] ?? (string) $match['domain2']; $configuredDomains = $this->config->get('gitlab-domains'); $urlParts = explode('/', $match['parts']); @@ -109,13 +107,13 @@ public function initialize(): void ; $origin = self::determineOrigin($configuredDomains, $guessedDomain, $urlParts, $match['port']); if (false === $origin) { - throw new \LogicException('It should not be possible to create a gitlab driver with an unparseable origin URL ('.$this->url.')'); + throw new \LogicException('It should not be possible to create a gitlab driver with an unparsable origin URL ('.$this->url.')'); } $this->originUrl = $origin; if (is_string($protocol = $this->config->get('gitlab-protocol'))) { // https treated as a synonym for http. - if (!in_array($protocol, ['git', 'http', 'https'])) { + if (!in_array($protocol, ['git', 'http', 'https'], true)) { throw new \RuntimeException('gitlab-protocol must be one of git, http.'); } $this->protocol = $protocol === 'git' ? 'ssh' : 'http'; @@ -566,8 +564,6 @@ public static function supports(IOInterface $io, Config $config, string $url, bo return false; } - assert(is_string($match['parts'])); - assert(is_string($match['repo'])); $scheme = $match['scheme']; $guessedDomain = $match['domain'] ?? (string) $match['domain2']; $urlParts = explode('/', $match['parts']); diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php index e70b9a6ac517..625a2a1ebded 100644 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -19,6 +19,7 @@ use Composer\Util\ProcessExecutor; use Composer\Util\Filesystem; use Composer\IO\IOInterface; +use Composer\Util\Url; /** * @author Per Bernhardt @@ -47,7 +48,7 @@ public function initialize(): void } $cacheDir = $this->config->get('cache-vcs-dir'); - $this->repoDir = $cacheDir . '/' . Preg::replace('{[^a-z0-9]}i', '-', $this->url) . '/'; + $this->repoDir = $cacheDir . '/' . Preg::replace('{[^a-z0-9]}i', '-', Url::sanitize($this->url)) . '/'; $fs = new Filesystem(); $fs->ensureDirectoryExists($cacheDir); @@ -62,8 +63,8 @@ public function initialize(): void $hgUtils = new HgUtils($this->io, $this->config, $this->process); // update the repo if it is a valid hg repository - if (is_dir($this->repoDir) && 0 === $this->process->execute('hg summary', $output, $this->repoDir)) { - if (0 !== $this->process->execute('hg pull', $output, $this->repoDir)) { + if (is_dir($this->repoDir) && 0 === $this->process->execute(['hg', 'summary'], $output, $this->repoDir)) { + if (0 !== $this->process->execute(['hg', 'pull'], $output, $this->repoDir)) { $this->io->writeError('Failed to update '.$this->url.', package information from this repository may be outdated ('.$this->process->getErrorOutput().')'); } } else { @@ -71,8 +72,8 @@ public function initialize(): void $fs->removeDirectory($this->repoDir); $repoDir = $this->repoDir; - $command = static function ($url) use ($repoDir): string { - return sprintf('hg clone --noupdate -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($repoDir)); + $command = static function ($url) use ($repoDir): array { + return ['hg', 'clone', '--noupdate', '--', $url, $repoDir]; }; $hgUtils->runCommand($command, $this->url, null); @@ -89,7 +90,7 @@ public function initialize(): void public function getRootIdentifier(): string { if (null === $this->rootIdentifier) { - $this->process->execute('hg tip --template "{node}"', $output, $this->repoDir); + $this->process->execute(['hg', 'tip', '--template', '{node}'], $output, $this->repoDir); $output = $this->process->splitLines($output); $this->rootIdentifier = $output[0]; } @@ -130,7 +131,7 @@ public function getFileContent(string $file, string $identifier): ?string throw new \RuntimeException('Invalid hg identifier detected. Identifier must not start with a -, given: ' . $identifier); } - $resource = sprintf('hg cat -r %s -- %s', ProcessExecutor::escape($identifier), ProcessExecutor::escape($file)); + $resource = ['hg', 'cat', '-r', $identifier, '--', $file]; $this->process->execute($resource, $content, $this->repoDir); if (!trim($content)) { @@ -146,10 +147,7 @@ public function getFileContent(string $file, string $identifier): ?string public function getChangeDate(string $identifier): ?\DateTimeImmutable { $this->process->execute( - sprintf( - 'hg log --template "{date|rfc3339date}" -r %s', - ProcessExecutor::escape($identifier) - ), + ['hg', 'log', '--template', '{date|rfc3339date}', '-r', $identifier], $output, $this->repoDir ); @@ -165,7 +163,7 @@ public function getTags(): array if (null === $this->tags) { $tags = []; - $this->process->execute('hg tags', $output, $this->repoDir); + $this->process->execute(['hg', 'tags'], $output, $this->repoDir); foreach ($this->process->splitLines($output) as $tag) { if ($tag && Preg::isMatchStrictGroups('(^([^\s]+)\s+\d+:(.*)$)', $tag, $match)) { $tags[$match[1]] = $match[2]; @@ -188,14 +186,14 @@ public function getBranches(): array $branches = []; $bookmarks = []; - $this->process->execute('hg branches', $output, $this->repoDir); + $this->process->execute(['hg', 'branches'], $output, $this->repoDir); foreach ($this->process->splitLines($output) as $branch) { if ($branch && Preg::isMatchStrictGroups('(^([^\s]+)\s+\d+:([a-f0-9]+))', $branch, $match) && $match[1][0] !== '-') { $branches[$match[1]] = $match[2]; } } - $this->process->execute('hg bookmarks', $output, $this->repoDir); + $this->process->execute(['hg', 'bookmarks'], $output, $this->repoDir); foreach ($this->process->splitLines($output) as $branch) { if ($branch && Preg::isMatchStrictGroups('(^(?:[\s*]*)([^\s]+)\s+\d+:(.*)$)', $branch, $match) && $match[1][0] !== '-') { $bookmarks[$match[1]] = $match[2]; @@ -227,7 +225,7 @@ public static function supports(IOInterface $io, Config $config, string $url, bo $process = new ProcessExecutor($io); // check whether there is a hg repo in that path - if ($process->execute('hg summary', $output, $url) === 0) { + if ($process->execute(['hg', 'summary'], $output, $url) === 0) { return true; } } @@ -237,7 +235,7 @@ public static function supports(IOInterface $io, Config $config, string $url, bo } $process = new ProcessExecutor($io); - $exit = $process->execute(sprintf('hg identify -- %s', ProcessExecutor::escape($url)), $ignored); + $exit = $process->execute(['hg', 'identify', '--', $url], $ignored); return $exit === 0; } diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php index ea8158e34d2a..9a303ee14997 100644 --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -177,8 +177,7 @@ public function getFileContent(string $file, string $identifier): ?string { $identifier = '/' . trim($identifier, '/') . '/'; - Preg::match('{^(.+?)(@\d+)?/$}', $identifier, $match); - if (!empty($match[2])) { + if (Preg::isMatch('{^(.+?)(@\d+)?/$}', $identifier, $match) && $match[2] !== null) { $path = $match[1]; $rev = $match[2]; } else { @@ -188,8 +187,8 @@ public function getFileContent(string $file, string $identifier): ?string try { $resource = $path.$file; - $output = $this->execute('svn cat', $this->baseUrl . $resource . $rev); - if (!trim($output)) { + $output = $this->execute(['svn', 'cat'], $this->baseUrl . $resource . $rev); + if ('' === trim($output)) { return null; } } catch (\RuntimeException $e) { @@ -206,8 +205,7 @@ public function getChangeDate(string $identifier): ?\DateTimeImmutable { $identifier = '/' . trim($identifier, '/') . '/'; - Preg::match('{^(.+?)(@\d+)?/$}', $identifier, $match); - if (null !== $match[2] && null !== $match[1]) { + if (Preg::isMatch('{^(.+?)(@\d+)?/$}', $identifier, $match) && null !== $match[2]) { $path = $match[1]; $rev = $match[2]; } else { @@ -215,9 +213,9 @@ public function getChangeDate(string $identifier): ?\DateTimeImmutable $rev = ''; } - $output = $this->execute('svn info', $this->baseUrl . $path . $rev); + $output = $this->execute(['svn', 'info'], $this->baseUrl . $path . $rev); foreach ($this->process->splitLines($output) as $line) { - if ($line && Preg::isMatchStrictGroups('{^Last Changed Date: ([^(]+)}', $line, $match)) { + if ($line !== '' && Preg::isMatchStrictGroups('{^Last Changed Date: ([^(]+)}', $line, $match)) { return new \DateTimeImmutable($match[1], new \DateTimeZone('UTC')); } } @@ -234,21 +232,19 @@ public function getTags(): array $tags = []; if ($this->tagsPath !== false) { - $output = $this->execute('svn ls --verbose', $this->baseUrl . '/' . $this->tagsPath); - if ($output) { + $output = $this->execute(['svn', 'ls', '--verbose'], $this->baseUrl . '/' . $this->tagsPath); + if ($output !== '') { $lastRev = 0; foreach ($this->process->splitLines($output) as $line) { $line = trim($line); - if ($line && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { - if (isset($match[1], $match[2])) { - if ($match[2] === './') { - $lastRev = (int) $match[1]; - } else { - $tags[rtrim($match[2], '/')] = $this->buildIdentifier( - '/' . $this->tagsPath . '/' . $match[2], - max($lastRev, (int) $match[1]) - ); - } + if ($line !== '' && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { + if ($match[2] === './') { + $lastRev = (int) $match[1]; + } else { + $tags[rtrim($match[2], '/')] = $this->buildIdentifier( + '/' . $this->tagsPath . '/' . $match[2], + max($lastRev, (int) $match[1]) + ); } } } @@ -275,12 +271,12 @@ public function getBranches(): array $trunkParent = $this->baseUrl . '/' . $this->trunkPath; } - $output = $this->execute('svn ls --verbose', $trunkParent); - if ($output) { + $output = $this->execute(['svn', 'ls', '--verbose'], $trunkParent); + if ($output !== '') { foreach ($this->process->splitLines($output) as $line) { $line = trim($line); - if ($line && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { - if (isset($match[1], $match[2]) && $match[2] === './') { + if ($line !== '' && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { + if ($match[2] === './') { $branches['trunk'] = $this->buildIdentifier( '/' . $this->trunkPath, (int) $match[1] @@ -294,21 +290,19 @@ public function getBranches(): array unset($output); if ($this->branchesPath !== false) { - $output = $this->execute('svn ls --verbose', $this->baseUrl . '/' . $this->branchesPath); - if ($output) { + $output = $this->execute(['svn', 'ls', '--verbose'], $this->baseUrl . '/' . $this->branchesPath); + if ($output !== '') { $lastRev = 0; foreach ($this->process->splitLines(trim($output)) as $line) { $line = trim($line); - if ($line && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { - if (isset($match[1], $match[2])) { - if ($match[2] === './') { - $lastRev = (int) $match[1]; - } else { - $branches[rtrim($match[2], '/')] = $this->buildIdentifier( - '/' . $this->branchesPath . '/' . $match[2], - max($lastRev, (int) $match[1]) - ); - } + if ($line !== '' && Preg::isMatch('{^\s*(\S+).*?(\S+)\s*$}', $line, $match)) { + if ($match[2] === './') { + $lastRev = (int) $match[1]; + } else { + $branches[rtrim($match[2], '/')] = $this->buildIdentifier( + '/' . $this->branchesPath . '/' . $match[2], + max($lastRev, (int) $match[1]) + ); } } } @@ -337,10 +331,7 @@ public static function supports(IOInterface $io, Config $config, string $url, bo } $process = new ProcessExecutor($io); - $exit = $process->execute( - "svn info --non-interactive -- ".ProcessExecutor::escape($url), - $ignoredOutput - ); + $exit = $process->execute(['svn', 'info', '--non-interactive', '--', $url], $ignoredOutput); if ($exit === 0) { // This is definitely a Subversion repository. @@ -381,11 +372,11 @@ protected static function normalizeUrl(string $url): string * Execute an SVN command and try to fix up the process with credentials * if necessary. * - * @param string $command The svn command to run. + * @param non-empty-list $command The svn command to run. * @param string $url The SVN URL. * @throws \RuntimeException */ - protected function execute(string $command, string $url): string + protected function execute(array $command, string $url): string { if (null === $this->util) { $this->util = new SvnUtil($this->baseUrl, $this->io, $this->config, $this->process); diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index d10ad87a0a81..6ca321e58e7e 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -66,7 +66,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt private $driver; /** @var ?VersionCacheInterface */ private $versionCache; - /** @var string[] */ + /** @var list */ private $emptyReferences = []; /** @var array<'tags'|'branches', array> */ private $versionTransportExceptions = []; @@ -165,7 +165,7 @@ public function hadInvalidBranches(): bool } /** - * @return string[] + * @return list */ public function getEmptyReferences(): array { @@ -217,11 +217,6 @@ protected function initialize() foreach ($driver->getTags() as $tag => $identifier) { $tag = (string) $tag; $msg = 'Reading composer.json of ' . ($this->packageName ?: $this->url) . ' (' . $tag . ')'; - if ($isVeryVerbose) { - $this->io->writeError($msg); - } elseif ($isVerbose) { - $this->io->overwriteError($msg, false); - } // strip the release- prefix from tags if present $tag = str_replace('release-', '', $tag); @@ -245,6 +240,12 @@ protected function initialize() continue; } + if ($isVeryVerbose) { + $this->io->writeError($msg); + } elseif ($isVerbose) { + $this->io->overwriteError($msg, false); + } + try { $data = $driver->getComposerInformation($identifier); if (null === $data) { @@ -341,7 +342,8 @@ protected function initialize() // make sure branch packages have a dev flag if (strpos($parsedBranch, 'dev-') === 0 || VersionParser::DEFAULT_BRANCH_ALIAS === $parsedBranch) { - $version = 'dev-' . $branch; + $version = 'dev-' . str_replace('#', '+', $branch); + $parsedBranch = str_replace('#', '+', $parsedBranch); } else { $prefix = strpos($branch, 'v') === 0 ? 'v' : ''; $version = $prefix . Preg::replace('{(\.9{7})+}', '.x', $parsedBranch); diff --git a/src/Composer/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index 045fb22a7754..8cc7d455c76f 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -89,7 +89,7 @@ public function getLatest(?string $channel = null): array $versions = $this->getVersionsData(); foreach ($versions[$channel ?: $this->getChannel()] as $version) { - if ($version['min-php'] <= PHP_VERSION_ID) { + if ($version['min-php'] <= \PHP_VERSION_ID) { return $version; } } diff --git a/src/Composer/Util/Bitbucket.php b/src/Composer/Util/Bitbucket.php index 2be4a815cb05..15743a7e3225 100644 --- a/src/Composer/Util/Bitbucket.php +++ b/src/Composer/Util/Bitbucket.php @@ -77,7 +77,7 @@ public function authorizeOAuth(string $originUrl): bool } // if available use token from git config - if (0 === $this->process->execute('git config bitbucket.accesstoken', $output)) { + if (0 === $this->process->execute(['git', 'config', 'bitbucket.accesstoken'], $output)) { $this->io->setAuthentication($originUrl, 'x-token-auth', trim($output)); return true; diff --git a/src/Composer/Util/ComposerMirror.php b/src/Composer/Util/ComposerMirror.php index 106e76c41e99..6be5396932a8 100644 --- a/src/Composer/Util/ComposerMirror.php +++ b/src/Composer/Util/ComposerMirror.php @@ -28,9 +28,9 @@ class ComposerMirror public static function processUrl(string $mirrorUrl, string $packageName, string $version, ?string $reference, ?string $type, ?string $prettyVersion = null): string { if ($reference) { - $reference = Preg::isMatch('{^([a-f0-9]*|%reference%)$}', $reference) ? $reference : md5($reference); + $reference = Preg::isMatch('{^([a-f0-9]*|%reference%)$}', $reference) ? $reference : hash('md5', $reference); } - $version = strpos($version, '/') === false ? $version : md5($version); + $version = strpos($version, '/') === false ? $version : hash('md5', $version); $from = ['%package%', '%version%', '%reference%', '%type%']; $to = [$packageName, $version, $reference, $type]; diff --git a/src/Composer/Util/ErrorHandler.php b/src/Composer/Util/ErrorHandler.php index 4ed0cfefba1c..d06b57718987 100644 --- a/src/Composer/Util/ErrorHandler.php +++ b/src/Composer/Util/ErrorHandler.php @@ -40,7 +40,7 @@ public static function handle(int $level, string $message, string $file, int $li $isDeprecationNotice = $level === E_DEPRECATED || $level === E_USER_DEPRECATED; // error code is not included in error_reporting - if (!$isDeprecationNotice && !(error_reporting() & $level)) { + if (!$isDeprecationNotice && 0 === (error_reporting() & $level)) { return true; } @@ -53,7 +53,7 @@ public static function handle(int $level, string $message, string $file, int $li throw new \ErrorException($message, 0, $level, $file, $line); } - if (self::$io) { + if (self::$io !== null) { self::$io->writeError('Deprecation Notice: '.$message.' in '.$file.':'.$line.''); if (self::$io->isVerbose()) { self::$io->writeError('Stack trace:'); @@ -63,7 +63,9 @@ public static function handle(int $level, string $message, string $file, int $li } return null; - }, array_slice(debug_backtrace(), 2)))); + }, array_slice(debug_backtrace(), 2)), function (?string $line) { + return $line !== null; + })); } } @@ -76,7 +78,7 @@ public static function handle(int $level, string $message, string $file, int $li public static function register(?IOInterface $io = null): void { set_error_handler([__CLASS__, 'handle']); - error_reporting(E_ALL | E_STRICT); + error_reporting(E_ALL); self::$io = $io; } } diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index 7ddcfa69f077..57e4fd6f3532 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -13,6 +13,7 @@ namespace Composer\Util; use Composer\Pcre\Preg; +use ErrorException; use React\Promise\PromiseInterface; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -108,9 +109,9 @@ public function removeDirectory(string $directory) } if (Platform::isWindows()) { - $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); + $cmd = ['rmdir', '/S', '/Q', Platform::realpath($directory)]; } else { - $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); + $cmd = ['rm', '-rf', $directory]; } $result = $this->getProcess()->execute($cmd, $output) === 0; @@ -143,9 +144,9 @@ public function removeDirectoryAsync(string $directory) } if (Platform::isWindows()) { - $cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory))); + $cmd = ['rmdir', '/S', '/Q', Platform::realpath($directory)]; } else { - $cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory)); + $cmd = ['rm', '-rf', $directory]; } $promise = $this->getProcess()->executeAsync($cmd); @@ -257,7 +258,21 @@ public function ensureDirectoryExists(string $directory) } if (!@mkdir($directory, 0777, true)) { - throw new \RuntimeException($directory.' does not exist and could not be created: '.(error_get_last()['message'] ?? '')); + $e = new \RuntimeException($directory.' does not exist and could not be created: '.(error_get_last()['message'] ?? '')); + + // in pathological cases with paths like path/to/broken-symlink/../foo is_dir will fail to detect path/to/foo + // but normalizing the ../ away first makes it work so we attempt this just in case, and if it still fails we + // report the initial error we had with the original path, and ignore the normalized path exception + // see https://github.com/composer/composer/issues/11864 + $normalized = $this->normalizePath($directory); + if ($normalized !== $directory) { + try { + $this->ensureDirectoryExists($normalized); + return; + } catch (\Throwable $ignoredEx) {} + } + + throw $e; } } } @@ -349,8 +364,33 @@ public function copyThenRemove(string $source, string $target) */ public function copy(string $source, string $target) { + // refs https://github.com/composer/composer/issues/11864 + $target = $this->normalizePath($target); + if (!is_dir($source)) { - return copy($source, $target); + try { + return copy($source, $target); + } catch (ErrorException $e) { + // if copy fails we attempt to copy it manually as this can help bypass issues with VirtualBox shared folders + // see https://github.com/composer/composer/issues/12057 + if (str_contains($e->getMessage(), 'Bad address')) { + $sourceHandle = fopen($source, 'r'); + $targetHandle = fopen($target, 'w'); + if (false === $sourceHandle || false === $targetHandle) { + throw $e; + } + while (!feof($sourceHandle)) { + if (false === fwrite($targetHandle, (string) fread($sourceHandle, 1024 * 1024))) { + throw $e; + } + } + fclose($sourceHandle); + fclose($targetHandle); + + return true; + } + throw $e; + } } $it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS); @@ -387,8 +427,7 @@ public function rename(string $source, string $target) if (Platform::isWindows()) { // Try to copy & delete - this is a workaround for random "Access denied" errors. - $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); - $result = $this->getProcess()->execute($command, $output); + $result = $this->getProcess()->execute(['xcopy', $source, $target, '/E', '/I', '/Q', '/Y'], $output); // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); @@ -401,8 +440,7 @@ public function rename(string $source, string $target) } else { // We do not use PHP's "rename" function here since it does not support // the case where $source, and $target are located on different partitions. - $command = sprintf('mv %s %s', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); - $result = $this->getProcess()->execute($command, $output); + $result = $this->getProcess()->execute(['mv', $source, $target], $output); // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); @@ -419,10 +457,11 @@ public function rename(string $source, string $target) * Returns the shortest path from $from to $to * * @param bool $directories if true, the source/target are considered to be directories + * @param bool $preferRelative if true, relative paths will be preferred even if longer * @throws \InvalidArgumentException * @return string */ - public function findShortestPath(string $from, string $to, bool $directories = false) + public function findShortestPath(string $from, string $to, bool $directories = false, bool $preferRelative = false) { if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); @@ -454,7 +493,7 @@ public function findShortestPath(string $from, string $to, bool $directories = f $commonPathCode = str_repeat('../', $sourcePathDepth); // allow top level /foo & /bar dirs to be addressed relatively as this is common in Docker setups - if ('/' === $commonPath && $sourcePathDepth > 1) { + if (!$preferRelative && '/' === $commonPath && $sourcePathDepth > 1) { return $to; } @@ -470,10 +509,11 @@ public function findShortestPath(string $from, string $to, bool $directories = f * Returns PHP code that, when executed in $from, will return the path to $to * * @param bool $directories if true, the source/target are considered to be directories + * @param bool $preferRelative if true, relative paths will be preferred even if longer * @throws \InvalidArgumentException * @return string */ - public function findShortestPathCode(string $from, string $to, bool $directories = false, bool $staticCode = false) + public function findShortestPathCode(string $from, string $to, bool $directories = false, bool $staticCode = false, bool $preferRelative = false) { if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { throw new \InvalidArgumentException(sprintf('$from (%s) and $to (%s) must be absolute paths.', $from, $to)); @@ -503,7 +543,7 @@ public function findShortestPathCode(string $from, string $to, bool $directories $sourcePathDepth = substr_count((string) substr($from, \strlen($commonPath)), '/') + (int) $directories; // allow top level /foo & /bar dirs to be addressed relatively as this is common in Docker setups - if ('/' === $commonPath && $sourcePathDepth > 1) { + if (!$preferRelative && '/' === $commonPath && $sourcePathDepth > 1) { return var_export($to, true); } @@ -591,7 +631,6 @@ public function normalizePath(string $path) // ensure c: is normalized to C: $prefix = Preg::replaceCallback('{(^|://)[a-z]:$}i', static function (array $m) { - assert(is_string($m[0])); return strtoupper($m[0]); }, $prefix); @@ -621,7 +660,13 @@ public static function trimTrailingSlash(string $path) */ public static function isLocalPath(string $path) { - return Preg::isMatch('{^(file://(?!//)|/(?!/)|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); + // on windows, \\foo indicates network paths so we exclude those from local paths, however it is unsafe + // on linux as file:////foo (which would be a network path \\foo on windows) will resolve to /foo which could be a local path + if (Platform::isWindows()) { + return Preg::isMatch('{^(file://(?!//)|/(?!/)|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); + } + + return Preg::isMatch('{^(file://|/|/?[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i', $path); } /** @@ -794,11 +839,7 @@ public function junction(string $target, string $junction) @rmdir($junction); } - $cmd = sprintf( - 'mklink /J %s %s', - ProcessExecutor::escape(str_replace('/', DIRECTORY_SEPARATOR, $junction)), - ProcessExecutor::escape(realpath($target)) - ); + $cmd = ['mklink', '/J', str_replace('/', DIRECTORY_SEPARATOR, $junction), Platform::realpath($target)]; if ($this->getProcess()->execute($cmd, $output) !== 0) { throw new IOException(sprintf('Failed to create junction to "%s" at "%s".', $target, $junction), 0, null, $target); } diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index f8e503d82855..e340c1b46d17 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -32,6 +32,8 @@ class Git protected $process; /** @var Filesystem */ protected $filesystem; + /** @var HttpDownloader */ + protected $httpDownloader; public function __construct(IOInterface $io, Config $config, ProcessExecutor $process, Filesystem $fs) { @@ -42,26 +44,109 @@ public function __construct(IOInterface $io, Config $config, ProcessExecutor $pr } /** + * @param IOInterface|null $io If present, a warning is output there instead of throwing, so pass this in only for cases where this is a soft failure + */ + public static function checkForRepoOwnershipError(string $output, string $path, ?IOInterface $io = null): void + { + if (str_contains($output, 'fatal: detected dubious ownership')) { + $msg = 'The repository at "' . $path . '" does not have the correct ownership and git refuses to use it:' . PHP_EOL . PHP_EOL . $output; + if ($io === null) { + throw new \RuntimeException($msg); + } + $io->writeError(''.$msg.''); + } + } + + public function setHttpDownloader(HttpDownloader $httpDownloader): void + { + $this->httpDownloader = $httpDownloader; + } + + /** + * Runs a set of commands using the $url or a variation of it (with auth, ssh, ..) + * + * Commands should use %url% placeholders for the URL instead of inlining it to allow this function to do its job + * %sanitizedUrl% is also automatically replaced by the url without user/pass + * + * As soon as a single command fails it will halt, so assume the commands are run as && in bash + * + * @param non-empty-array> $commands + * @param mixed $commandOutput the output will be written into this var if passed by ref + * if a callable is passed it will be used as output handler + */ + public function runCommands(array $commands, string $url, ?string $cwd, bool $initialClone = false, &$commandOutput = null): void + { + $callables = []; + foreach ($commands as $cmd) { + $callables[] = static function (string $url) use ($cmd): array { + $map = [ + '%url%' => $url, + '%sanitizedUrl%' => Preg::replace('{://([^@]+?):(.+?)@}', '://', $url), + ]; + + return array_map(static function ($value) use ($map): string { + return $map[$value] ?? $value; + }, $cmd); + }; + } + + // @phpstan-ignore method.deprecated + $this->runCommand($callables, $url, $cwd, $initialClone, $commandOutput); + } + + /** + * @param callable|array $commandCallable * @param mixed $commandOutput the output will be written into this var if passed by ref * if a callable is passed it will be used as output handler + * @deprecated Use runCommands with placeholders instead of callbacks for simplicity */ - public function runCommand(callable $commandCallable, string $url, ?string $cwd, bool $initialClone = false, &$commandOutput = null): void + public function runCommand($commandCallable, string $url, ?string $cwd, bool $initialClone = false, &$commandOutput = null): void { + $commandCallables = is_callable($commandCallable) ? [$commandCallable] : $commandCallable; + $lastCommand = ''; + // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); if ($initialClone) { $origCwd = $cwd; - $cwd = null; } + $runCommands = function ($url) use ($commandCallables, $cwd, &$commandOutput, &$lastCommand, $initialClone) { + $collectOutputs = !is_callable($commandOutput); + $outputs = []; + + $status = 0; + $counter = 0; + foreach ($commandCallables as $callable) { + $lastCommand = $callable($url); + if ($collectOutputs) { + $outputs[] = ''; + $output = &$outputs[count($outputs) - 1]; + } else { + $output = &$commandOutput; + } + $status = $this->process->execute($lastCommand, $output, $initialClone && $counter === 0 ? null : $cwd); + if ($status !== 0) { + break; + } + $counter++; + } + + if ($collectOutputs) { + $commandOutput = implode('', $outputs); + } + + return $status; + }; + if (Preg::isMatch('{^ssh://[^@]+@[^:]+:[^0-9]+}', $url)) { throw new \InvalidArgumentException('The source URL ' . $url . ' is invalid, ssh URLs should have a port number after ":".' . "\n" . 'Use ssh://git@example.com:22/path or just git@example.com:path if you do not want to provide a password or custom port.'); } if (!$initialClone) { // capture username/password from URL if there is one and we have no auth configured yet - $this->process->execute('git remote -v', $output, $cwd); + $this->process->execute(['git', 'remote', '-v'], $output, $cwd); if (Preg::isMatchStrictGroups('{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im', $output, $match) && !$this->io->hasAuthentication($match[3])) { $this->io->setAuthentication($match[3], rawurldecode($match[1]), rawurldecode($match[2])); } @@ -69,6 +154,7 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, $protocols = $this->config->get('github-protocols'); // public github, autoswitch protocols + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups if (Preg::isMatchStrictGroups('{^(?:https?|git)://' . self::getGitHubDomainsRegex($this->config) . '/(.*)}', $url, $match)) { $messages = []; foreach ($protocols as $protocol) { @@ -78,7 +164,7 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, $protoUrl = $protocol . "://" . $match[1] . "/" . $match[2]; } - if (0 === $this->process->execute($commandCallable($protoUrl), $commandOutput, $cwd)) { + if (0 === $runCommands($protoUrl)) { return; } $messages[] = '- ' . $protoUrl . "\n" . Preg::replace('#^#m', ' ', $this->process->getErrorOutput()); @@ -97,14 +183,14 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, // if we have a private github url and the ssh protocol is disabled then we skip it and directly fallback to https $bypassSshForGitHub = Preg::isMatch('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url) && !in_array('ssh', $protocols, true); - $command = $commandCallable($url); - $auth = null; $credentials = []; - if ($bypassSshForGitHub || 0 !== $this->process->execute($command, $commandOutput, $cwd)) { + if ($bypassSshForGitHub || 0 !== $runCommands($url)) { $errorMsg = $this->process->getErrorOutput(); // private github repository without ssh key access, try https with auth + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups if (Preg::isMatchStrictGroups('{^git@' . self::getGitHubDomainsRegex($this->config) . ':(.+?)\.git$}i', $url, $match) + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups || Preg::isMatchStrictGroups('{^https?://' . self::getGitHubDomainsRegex($this->config) . '/(.*?)(?:\.git)?$}i', $url, $match) ) { if (!$this->io->hasAuthentication($match[1])) { @@ -119,60 +205,78 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, if ($this->io->hasAuthentication($match[1])) { $auth = $this->io->getAuthentication($match[1]); $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git'; - $command = $commandCallable($authUrl); - if (0 === $this->process->execute($command, $commandOutput, $cwd)) { + if (0 === $runCommands($authUrl)) { return; } $credentials = [rawurlencode($auth['username']), rawurlencode($auth['password'])]; $errorMsg = $this->process->getErrorOutput(); } - } elseif (Preg::isMatchStrictGroups('{^https://(bitbucket\.org)/(.*?)(?:\.git)?$}i', $url, $match)) { //bitbucket oauth - $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process); - - if (!$this->io->hasAuthentication($match[1])) { + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups + } elseif ( + Preg::isMatchStrictGroups('{^(https?)://(bitbucket\.org)/(.*?)(?:\.git)?$}i', $url, $match) + || Preg::isMatchStrictGroups('{^(git)@(bitbucket\.org):(.+?\.git)$}i', $url, $match) + ) { //bitbucket either through oauth or app password, with fallback to ssh. + $bitbucketUtil = new Bitbucket($this->io, $this->config, $this->process, $this->httpDownloader); + + $domain = $match[2]; + $repo_with_git_part = $match[3]; + if (!str_ends_with($repo_with_git_part, '.git')) { + $repo_with_git_part .= '.git'; + } + if (!$this->io->hasAuthentication($domain)) { $message = 'Enter your Bitbucket credentials to access private repos'; - if (!$bitbucketUtil->authorizeOAuth($match[1]) && $this->io->isInteractive()) { + if (!$bitbucketUtil->authorizeOAuth($domain) && $this->io->isInteractive()) { $bitbucketUtil->authorizeOAuthInteractively($match[1], $message); $accessToken = $bitbucketUtil->getToken(); - $this->io->setAuthentication($match[1], 'x-token-auth', $accessToken); + $this->io->setAuthentication($domain, 'x-token-auth', $accessToken); + } + } + + // First we try to authenticate with whatever we have stored. + // This will be successful if there is for example an app + // password in there. + if ($this->io->hasAuthentication($domain)) { + $auth = $this->io->getAuthentication($domain); + $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part; + + if (0 === $runCommands($authUrl)) { + // Well if that succeeded on our first try, let's just + // take the win. + return; } - } else { //We're authenticating with a locally stored consumer. - $auth = $this->io->getAuthentication($match[1]); //We already have an access_token from a previous request. if ($auth['username'] !== 'x-token-auth') { - $accessToken = $bitbucketUtil->requestToken($match[1], $auth['username'], $auth['password']); + $accessToken = $bitbucketUtil->requestToken($domain, $auth['username'], $auth['password']); if (!empty($accessToken)) { - $this->io->setAuthentication($match[1], 'x-token-auth', $accessToken); + $this->io->setAuthentication($domain, 'x-token-auth', $accessToken); } } } - if ($this->io->hasAuthentication($match[1])) { - $auth = $this->io->getAuthentication($match[1]); - $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[1] . '/' . $match[2] . '.git'; - - $command = $commandCallable($authUrl); - if (0 === $this->process->execute($command, $commandOutput, $cwd)) { + if ($this->io->hasAuthentication($domain)) { + $auth = $this->io->getAuthentication($domain); + $authUrl = 'https://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $domain . '/' . $repo_with_git_part; + if (0 === $runCommands($authUrl)) { return; } $credentials = [rawurlencode($auth['username']), rawurlencode($auth['password'])]; - $errorMsg = $this->process->getErrorOutput(); - } else { // Falling back to ssh - $sshUrl = 'git@bitbucket.org:' . $match[2] . '.git'; - $this->io->writeError(' No bitbucket authentication configured. Falling back to ssh.'); - $command = $commandCallable($sshUrl); - if (0 === $this->process->execute($command, $commandOutput, $cwd)) { - return; - } - - $errorMsg = $this->process->getErrorOutput(); } + //Falling back to ssh + $sshUrl = 'git@bitbucket.org:' . $repo_with_git_part; + $this->io->writeError(' No bitbucket authentication configured. Falling back to ssh.'); + if (0 === $runCommands($sshUrl)) { + return; + } + + $errorMsg = $this->process->getErrorOutput(); } elseif ( + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups Preg::isMatchStrictGroups('{^(git)@' . self::getGitLabDomainsRegex($this->config) . ':(.+?\.git)$}i', $url, $match) + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups || Preg::isMatchStrictGroups('{^(https?)://' . self::getGitLabDomainsRegex($this->config) . '/(.*)}i', $url, $match) ) { if ($match[1] === 'git') { @@ -191,21 +295,20 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, if ($this->io->hasAuthentication($match[2])) { $auth = $this->io->getAuthentication($match[2]); if ($auth['password'] === 'private-token' || $auth['password'] === 'oauth2' || $auth['password'] === 'gitlab-ci-token') { - $authUrl = $match[1] . '://' . rawurlencode($auth['password']) . ':' . rawurlencode($auth['username']) . '@' . $match[2] . '/' . $match[3]; // swap username and password + $authUrl = $match[1] . '://' . rawurlencode($auth['password']) . ':' . rawurlencode((string) $auth['username']) . '@' . $match[2] . '/' . $match[3]; // swap username and password } else { - $authUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[2] . '/' . $match[3]; + $authUrl = $match[1] . '://' . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . '/' . $match[3]; } - $command = $commandCallable($authUrl); - if (0 === $this->process->execute($command, $commandOutput, $cwd)) { + if (0 === $runCommands($authUrl)) { return; } - $credentials = [rawurlencode($auth['username']), rawurlencode($auth['password'])]; + $credentials = [rawurlencode((string) $auth['username']), rawurlencode((string) $auth['password'])]; $errorMsg = $this->process->getErrorOutput(); } - } elseif ($this->isAuthenticationFailure($url, $match)) { // private non-github/gitlab/bitbucket repo that failed to authenticate - if (strpos($match[2], '@')) { + } elseif (null !== ($match = $this->getAuthenticationFailure($url))) { // private non-github/gitlab/bitbucket repo that failed to authenticate + if (str_contains($match[2], '@')) { [$authParts, $match[2]] = explode('@', $match[2], 2); } @@ -214,8 +317,8 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, $auth = $this->io->getAuthentication($match[2]); } elseif ($this->io->isInteractive()) { $defaultUsername = null; - if (isset($authParts) && $authParts) { - if (false !== strpos($authParts, ':')) { + if (isset($authParts) && $authParts !== '') { + if (str_contains($authParts, ':')) { [$defaultUsername, ] = explode(':', $authParts, 2); } else { $defaultUsername = $authParts; @@ -232,10 +335,9 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, } if (null !== $auth) { - $authUrl = $match[1] . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[2] . $match[3]; + $authUrl = $match[1] . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $match[2] . $match[3]; - $command = $commandCallable($authUrl); - if (0 === $this->process->execute($command, $commandOutput, $cwd)) { + if (0 === $runCommands($authUrl)) { $this->io->setAuthentication($match[2], $auth['username'], $auth['password']); $authHelper = new AuthHelper($this->io, $this->config); $authHelper->storeAuth($match[2], $storeAuth); @@ -243,7 +345,7 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, return; } - $credentials = [rawurlencode($auth['username']), rawurlencode($auth['password'])]; + $credentials = [rawurlencode((string) $auth['username']), rawurlencode((string) $auth['password'])]; $errorMsg = $this->process->getErrorOutput(); } } @@ -252,31 +354,34 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd, $this->filesystem->removeDirectory($origCwd); } + $lastCommand = implode(' ', $lastCommand); if (count($credentials) > 0) { - $command = $this->maskCredentials($command, $credentials); + $lastCommand = $this->maskCredentials($lastCommand, $credentials); $errorMsg = $this->maskCredentials($errorMsg, $credentials); } - $this->throwException('Failed to execute ' . $command . "\n\n" . $errorMsg, $url); + $this->throwException('Failed to execute ' . $lastCommand . "\n\n" . $errorMsg, $url); } } public function syncMirror(string $url, string $dir): bool { - if (Platform::getEnv('COMPOSER_DISABLE_NETWORK') && Platform::getEnv('COMPOSER_DISABLE_NETWORK') !== 'prime') { + if ((bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK') && Platform::getEnv('COMPOSER_DISABLE_NETWORK') !== 'prime') { $this->io->writeError('Aborting git mirror sync of '.$url.' as network is disabled'); return false; } // update the repo if it is a valid git repository - if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { + if (is_dir($dir) && 0 === $this->process->execute(['git', 'rev-parse', '--git-dir'], $output, $dir) && trim($output) === '.') { try { - $commandCallable = static function ($url): string { - $sanitizedUrl = Preg::replace('{://([^@]+?):(.+?)@}', '://', $url); - - return sprintf('git remote set-url origin -- %s && git remote update --prune origin && git remote set-url origin -- %s && git gc --auto', ProcessExecutor::escape($url), ProcessExecutor::escape($sanitizedUrl)); - }; - $this->runCommand($commandCallable, $url, $dir); + $commands = [ + ['git', 'remote', 'set-url', 'origin', '--', '%url%'], + ['git', 'remote', 'update', '--prune', 'origin'], + ['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'], + ['git', 'gc', '--auto'], + ]; + + $this->runCommands($commands, $url, $dir); } catch (\Exception $e) { $this->io->writeError('Sync mirror failed: ' . $e->getMessage() . '', true, IOInterface::DEBUG); @@ -285,30 +390,27 @@ public function syncMirror(string $url, string $dir): bool return true; } + self::checkForRepoOwnershipError($this->process->getErrorOutput(), $dir); // clean up directory and do a fresh clone into it $this->filesystem->removeDirectory($dir); - $commandCallable = static function ($url) use ($dir): string { - return sprintf('git clone --mirror -- %s %s', ProcessExecutor::escape($url), ProcessExecutor::escape($dir)); - }; - - $this->runCommand($commandCallable, $url, $dir, true); + $this->runCommands([['git', 'clone', '--mirror', '--', '%url%', $dir]], $url, $dir, true); return true; } - public function fetchRefOrSyncMirror(string $url, string $dir, string $ref, string $prettyVersion = null): bool + public function fetchRefOrSyncMirror(string $url, string $dir, string $ref, ?string $prettyVersion = null): bool { if ($this->checkRefIsInMirror($dir, $ref)) { if (Preg::isMatch('{^[a-f0-9]{40}$}', $ref) && $prettyVersion !== null) { $branch = Preg::replace('{(?:^dev-|(?:\.x)?-dev$)}i', '', $prettyVersion); $branches = null; $tags = null; - if (0 === $this->process->execute('git branch', $output, $dir)) { + if (0 === $this->process->execute(['git', 'branch'], $output, $dir)) { $branches = $output; } - if (0 === $this->process->execute('git tag', $output, $dir)) { + if (0 === $this->process->execute(['git', 'tag'], $output, $dir)) { $tags = $output; } @@ -343,26 +445,39 @@ public static function getNoShowSignatureFlag(ProcessExecutor $process): string return ''; } + /** + * @return list + */ + public static function getNoShowSignatureFlags(ProcessExecutor $process): array + { + $flags = static::getNoShowSignatureFlag($process); + if ('' === $flags) { + return []; + } + + return explode(' ', substr($flags, 1)); + } + private function checkRefIsInMirror(string $dir, string $ref): bool { - if (is_dir($dir) && 0 === $this->process->execute('git rev-parse --git-dir', $output, $dir) && trim($output) === '.') { - $escapedRef = ProcessExecutor::escape($ref.'^{commit}'); - $exitCode = $this->process->execute(sprintf('git rev-parse --quiet --verify %s', $escapedRef), $ignoredOutput, $dir); + if (is_dir($dir) && 0 === $this->process->execute(['git', 'rev-parse', '--git-dir'], $output, $dir) && trim($output) === '.') { + $exitCode = $this->process->execute(['git', 'rev-parse', '--quiet', '--verify', $ref.'^{commit}'], $ignoredOutput, $dir); if ($exitCode === 0) { return true; } } + self::checkForRepoOwnershipError($this->process->getErrorOutput(), $dir); return false; } /** - * @param string[] $match + * @return array|null */ - private function isAuthenticationFailure(string $url, array &$match): bool + private function getAuthenticationFailure(string $url): ?array { - if (!Preg::isMatch('{^(https?://)([^/]+)(.*)$}i', $url, $match)) { - return false; + if (!Preg::isMatchStrictGroups('{^(https?://)([^/]+)(.*)$}i', $url, $match)) { + return null; } $authFailures = [ @@ -376,11 +491,11 @@ private function isAuthenticationFailure(string $url, array &$match): bool $errorOutput = $this->process->getErrorOutput(); foreach ($authFailures as $authFailure) { if (strpos($errorOutput, $authFailure) !== false) { - return true; + return $match; } } - return false; + return null; } public function getMirrorDefaultBranch(string $url, string $dir, bool $isLocalPathRepository): ?string @@ -391,20 +506,20 @@ public function getMirrorDefaultBranch(string $url, string $dir, bool $isLocalPa try { if ($isLocalPathRepository) { - $this->process->execute('git remote show origin', $output, $dir); + $this->process->execute(['git', 'remote', 'show', 'origin'], $output, $dir); } else { - $commandCallable = static function ($url): string { - $sanitizedUrl = Preg::replace('{://([^@]+?):(.+?)@}', '://', $url); - - return sprintf('git remote set-url origin -- %s && git remote show origin && git remote set-url origin -- %s', ProcessExecutor::escape($url), ProcessExecutor::escape($sanitizedUrl)); - }; + $commands = [ + ['git', 'remote', 'set-url', 'origin', '--', '%url%'], + ['git', 'remote', 'show', 'origin'], + ['git', 'remote', 'set-url', 'origin', '--', '%sanitizedUrl%'], + ]; - $this->runCommand($commandCallable, $url, $dir, false, $output); + $this->runCommands($commands, $url, $dir, false, $output); } $lines = $this->process->splitLines($output); foreach ($lines as $line) { - if (Preg::match('{^\s*HEAD branch:\s(.+)\s*$}m', $line, $matches) > 0) { + if (Preg::isMatch('{^\s*HEAD branch:\s(.+)\s*$}m', $line, $matches)) { return $matches[1]; } } @@ -465,7 +580,7 @@ private function throwException($message, string $url): void // git might delete a directory when it fails and php will not know clearstatcache(); - if (0 !== $this->process->execute('git --version', $ignoredOutput)) { + if (0 !== $this->process->execute(['git', '--version'], $ignoredOutput)) { throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } @@ -481,7 +596,7 @@ public static function getVersion(ProcessExecutor $process): ?string { if (false === self::$version) { self::$version = null; - if (0 === $process->execute('git --version', $output) && Preg::isMatch('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) { + if (0 === $process->execute(['git', '--version'], $output) && Preg::isMatch('/^git version (\d+(?:\.\d+)+)/m', $output, $matches)) { self::$version = $matches[1]; } } diff --git a/src/Composer/Util/GitHub.php b/src/Composer/Util/GitHub.php index 3574e5183215..64ee4f559f59 100644 --- a/src/Composer/Util/GitHub.php +++ b/src/Composer/Util/GitHub.php @@ -61,7 +61,7 @@ public function authorizeOAuth(string $originUrl): bool } // if available use token from git config - if (0 === $this->process->execute('git config github.accesstoken', $output)) { + if (0 === $this->process->execute(['git', 'config', 'github.accesstoken'], $output)) { $this->io->setAuthentication($originUrl, trim($output), 'x-oauth-basic'); return true; @@ -86,7 +86,7 @@ public function authorizeOAuthInteractively(string $originUrl, ?string $message } $note = 'Composer'; - if ($this->config->get('github-expose-hostname') === true && 0 === $this->process->execute('hostname', $output)) { + if ($this->config->get('github-expose-hostname') === true && 0 === $this->process->execute(['hostname'], $output)) { $note .= ' on ' . trim($output); } $note .= ' ' . date('Y-m-d Hi'); diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php index e5985c2db7fe..2f108bc5a97e 100644 --- a/src/Composer/Util/GitLab.php +++ b/src/Composer/Util/GitLab.php @@ -65,14 +65,14 @@ public function authorizeOAuth(string $originUrl): bool } // if available use token from git config - if (0 === $this->process->execute('git config gitlab.accesstoken', $output)) { + if (0 === $this->process->execute(['git', 'config', 'gitlab.accesstoken'], $output)) { $this->io->setAuthentication($originUrl, trim($output), 'oauth2'); return true; } // if available use deploy token from git config - if (0 === $this->process->execute('git config gitlab.deploytoken.user', $tokenUser) && 0 === $this->process->execute('git config gitlab.deploytoken.token', $tokenPassword)) { + if (0 === $this->process->execute(['git', 'config', 'gitlab.deploytoken.user'], $tokenUser) && 0 === $this->process->execute(['git', 'config', 'gitlab.deploytoken.token'], $tokenPassword)) { $this->io->setAuthentication($originUrl, trim($tokenUser), trim($tokenPassword)); return true; @@ -126,8 +126,8 @@ public function authorizeOAuthInteractively(string $scheme, string $originUrl, ? } $localAuthConfig = $this->config->getLocalAuthConfigSource(); - $personalAccessTokenLink = $scheme.'://'.$originUrl.'/-/profile/personal_access_tokens'; - $revokeLink = $scheme.'://'.$originUrl.'/-/profile/applications'; + $personalAccessTokenLink = $scheme.'://'.$originUrl.'/-/user_settings/personal_access_tokens'; + $revokeLink = $scheme.'://'.$originUrl.'/-/user_settings/applications'; $this->io->writeError(sprintf('A token will be created and stored in "%s", your password will never be stored', ($localAuthConfig !== null ? $localAuthConfig->getName() . ' OR ' : '') . $this->config->getAuthConfigSource()->getName())); $this->io->writeError('To revoke access to this token you can visit:'); $this->io->writeError($revokeLink); @@ -312,7 +312,7 @@ private function refreshToken(string $scheme, string $originUrl): array $token = $this->httpDownloader->get($scheme.'://'.$originUrl.'/oauth/token', $options)->decodeJson(); $this->io->writeError('GitLab token successfully refreshed', true, IOInterface::VERY_VERBOSE); - $this->io->writeError('To revoke access to this token you can visit '.$scheme.'://'.$originUrl.'/-/profile/applications', true, IOInterface::VERY_VERBOSE); + $this->io->writeError('To revoke access to this token you can visit '.$scheme.'://'.$originUrl.'/-/user_settings/applications', true, IOInterface::VERY_VERBOSE); return $token; } diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 0e9f6e58ffa0..34b4796fa49b 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -58,10 +58,20 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd) } // Try with the authentication information available - if (Preg::isMatch('{^(https?)://((.+)(?:\:(.+))?@)?([^/]+)(/.*)?}mi', $url, $match) && $this->io->hasAuthentication((string) $match[5])) { - $auth = $this->io->getAuthentication((string) $match[5]); - $authenticatedUrl = $match[1] . '://' . rawurlencode($auth['username']) . ':' . rawurlencode($auth['password']) . '@' . $match[5] . $match[6]; - + if ( + Preg::isMatch('{^(?Pssh|https?)://(?:(?P[^:@]+)(?::(?P[^:@]+))?@)?(?P[^/]+)(?P/.*)?}mi', $url, $matches) + && $this->io->hasAuthentication($matches['host']) + ) { + if ($matches['proto'] === 'ssh') { + $user = ''; + if ($matches['user'] !== null) { + $user = rawurlencode($matches['user']) . '@'; + } + $authenticatedUrl = $matches['proto'] . '://' . $user . $matches['host'] . $matches['path']; + } else { + $auth = $this->io->getAuthentication($matches['host']); + $authenticatedUrl = $matches['proto'] . '://' . rawurlencode((string) $auth['username']) . ':' . rawurlencode((string) $auth['password']) . '@' . $matches['host'] . $matches['path']; + } $command = $commandCallable($authenticatedUrl); if (0 === $this->process->execute($command, $ignoredOutput, $cwd)) { @@ -70,10 +80,10 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd) $error = $this->process->getErrorOutput(); } else { - $error = 'The given URL (' . $url . ') does not match the required format (http(s)://(username:password@)example.com/path-to-repository)'; + $error = 'The given URL (' .$url. ') does not match the required format (ssh|http(s)://(username:password@)example.com/path-to-repository)'; } - $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url); + $this->throwException("Failed to clone $url, \n\n" . $error, $url); } /** @@ -84,7 +94,9 @@ public function runCommand(callable $commandCallable, string $url, ?string $cwd) private function throwException($message, string $url): void { if (null === self::getVersion($this->process)) { - throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize( + 'Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput() + )); } throw new \RuntimeException(Url::sanitize($message)); @@ -99,7 +111,7 @@ public static function getVersion(ProcessExecutor $process): ?string { if (false === self::$version) { self::$version = null; - if (0 === $process->execute('hg --version', $output) && Preg::isMatch('/^.+? (\d+(?:\.\d+)+)(?:\+.*?)?\)?\r?\n/', $output, $matches)) { + if (0 === $process->execute(['hg', '--version'], $output) && Preg::isMatch('/^.+? (\d+(?:\.\d+)+)(?:\+.*?)?\)?\r?\n/', $output, $matches)) { self::$version = $matches[1]; } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index f5bbe24aae10..1cdae6df9b38 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -23,19 +23,28 @@ use Composer\Util\Url; use Composer\Util\HttpDownloader; use React\Promise\Promise; +use Symfony\Component\HttpFoundation\IpUtils; /** * @internal * @author Jordi Boggiano * @author Nicolas Grekas * @phpstan-type Attributes array{retryAuthFailure: bool, redirects: int<0, max>, retries: int<0, max>, storeAuth: 'prompt'|bool, ipResolve: 4|6|null} - * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable} + * @phpstan-type Job array{url: non-empty-string, origin: string, attributes: Attributes, options: mixed[], progress: mixed[], curlHandle: \CurlHandle, filename: string|null, headerHandle: resource, bodyHandle: resource, resolve: callable, reject: callable, primaryIp: string} */ class CurlDownloader { - /** @var ?resource */ + /** + * Known libcurl's broken versions when proxy is in use with HTTP/2 + * multiplexing. + * + * @var list + */ + private const BAD_MULTIPLEXING_CURL_VERSIONS = ['7.87.0', '7.88.0', '7.88.1']; + + /** @var \CurlMultiHandle */ private $multiHandle; - /** @var ?resource */ + /** @var \CurlShareHandle */ private $shareHandle; /** @var Job[] */ private $jobs = []; @@ -51,10 +60,6 @@ class CurlDownloader private $maxRedirects = 20; /** @var int */ private $maxRetries = 3; - /** @var ProxyManager */ - private $proxyManager; - /** @var bool */ - private $supportsSecureProxy; /** @var array */ protected $multiErrors = [ CURLM_BAD_HANDLE => ['CURLM_BAD_HANDLE', 'The passed-in handle is not a valid CURLM handle.'], @@ -102,7 +107,18 @@ public function __construct(IOInterface $io, Config $config, array $options = [] $this->multiHandle = $mh = curl_multi_init(); if (function_exists('curl_multi_setopt')) { - curl_multi_setopt($mh, CURLMOPT_PIPELINING, PHP_VERSION_ID >= 70400 ? /* CURLPIPE_MULTIPLEX */ 2 : /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); + if (ProxyManager::getInstance()->hasProxy() && ($version = curl_version()) !== false && in_array($version['version'], self::BAD_MULTIPLEXING_CURL_VERSIONS, true)) { + /** + * Disable HTTP/2 multiplexing for some broken versions of libcurl. + * + * In certain versions of libcurl when proxy is in use with HTTP/2 + * multiplexing, connections will continue stacking up. This was + * fixed in libcurl 8.0.0 in curl/curl@821f6e2a89de8aec1c7da3c0f381b92b2b801efc + */ + curl_multi_setopt($mh, CURLMOPT_PIPELINING, /* CURLPIPE_NOTHING */ 0); + } else { + curl_multi_setopt($mh, CURLMOPT_PIPELINING, \PHP_VERSION_ID >= 70400 ? /* CURLPIPE_MULTIPLEX */ 2 : /*CURLPIPE_HTTP1 | CURLPIPE_MULTIPLEX*/ 3); + } if (defined('CURLMOPT_MAX_HOST_CONNECTIONS') && !defined('HHVM_VERSION')) { curl_multi_setopt($mh, CURLMOPT_MAX_HOST_CONNECTIONS, 8); } @@ -116,11 +132,6 @@ public function __construct(IOInterface $io, Config $config, array $options = [] } $this->authHelper = new AuthHelper($io, $config); - $this->proxyManager = ProxyManager::getInstance(); - - $version = curl_version(); - $features = $version['features']; - $this->supportsSecureProxy = defined('CURL_VERSION_HTTPS_PROXY') && ($features & CURL_VERSION_HTTPS_PROXY); } /** @@ -184,12 +195,13 @@ private function initDownload(callable $resolve, callable $reject, string $origi } $errorMessage = ''; - // @phpstan-ignore-next-line - set_error_handler(static function ($code, $msg) use (&$errorMessage): void { + set_error_handler(static function (int $code, string $msg) use (&$errorMessage): bool { if ($errorMessage) { $errorMessage .= "\n"; } $errorMessage .= Preg::replace('{^fopen\(.*?\): }', '', $msg); + + return true; }); $bodyHandle = fopen($bodyTarget, 'w+b'); restore_error_handler(); @@ -225,10 +237,16 @@ private function initDownload(callable $resolve, callable $reject, string $origi $version = curl_version(); $features = $version['features']; - if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features)) { + if (0 === strpos($url, 'https://') && \defined('CURL_VERSION_HTTP2') && \defined('CURL_HTTP_VERSION_2_0') && (CURL_VERSION_HTTP2 & $features) !== 0) { curl_setopt($curlHandle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); } + // curl 8.7.0 - 8.7.1 has a bug whereas automatic accept-encoding header results in an error when reading the response + // https://github.com/composer/composer/issues/11913 + if (isset($version['version']) && in_array($version['version'], ['8.7.0', '8.7.1'], true) && \defined('CURL_VERSION_LIBZ') && (CURL_VERSION_LIBZ & $features) !== 0) { + curl_setopt($curlHandle, CURLOPT_ENCODING, "gzip"); + } + $options['http']['header'] = $this->authHelper->addAuthenticationHeader($options['http']['header'], $origin, $url); $options = StreamContextFactory::initOptions($url, $options, true); @@ -244,26 +262,8 @@ private function initDownload(callable $resolve, callable $reject, string $origi } } - // Always set CURLOPT_PROXY to enable/disable proxy handling - // Any proxy authorization is included in the proxy url - $proxy = $this->proxyManager->getProxyForRequest($url); - if ($proxy->getUrl() !== '') { - curl_setopt($curlHandle, CURLOPT_PROXY, $proxy->getUrl()); - } - - // Curl needs certificate locations for secure proxies. - // CURLOPT_PROXY_SSL_VERIFY_PEER/HOST are enabled by default - if ($proxy->isSecure()) { - if (!$this->supportsSecureProxy) { - throw new TransportException('Connecting to a secure proxy using curl is not supported on PHP versions below 7.3.0.'); - } - if (!empty($options['ssl']['cafile'])) { - curl_setopt($curlHandle, CURLOPT_PROXY_CAINFO, $options['ssl']['cafile']); - } - if (!empty($options['ssl']['capath'])) { - curl_setopt($curlHandle, CURLOPT_PROXY_CAPATH, $options['ssl']['capath']); - } - } + $proxy = ProxyManager::getInstance()->getProxyForRequest($url); + curl_setopt_array($curlHandle, $proxy->getCurlOptions($options['ssl'] ?? [])); $progress = array_diff_key(curl_getinfo($curlHandle), self::$timeInfo); @@ -279,9 +279,10 @@ private function initDownload(callable $resolve, callable $reject, string $origi 'bodyHandle' => $bodyHandle, 'resolve' => $resolve, 'reject' => $reject, + 'primaryIp' => '', ]; - $usingProxy = $proxy->getFormattedUrl(' using proxy (%s)'); + $usingProxy = $proxy->getStatus(' using proxy (%s)'); $ifModified = false !== stripos(implode(',', $options['http']['header']), 'if-modified-since:') ? ' if modified' : ''; if ($attributes['redirects'] === 0 && $attributes['retries'] === 0) { $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); @@ -381,7 +382,7 @@ public function tick(): void continue; } - if ($errno === 28 /* CURLE_OPERATION_TIMEDOUT */ && PHP_VERSION_ID >= 70300 && $progress['namelookup_time'] === 0.0 && !$timeoutWarning) { + if ($errno === 28 /* CURLE_OPERATION_TIMEDOUT */ && \PHP_VERSION_ID >= 70300 && $progress['namelookup_time'] === 0.0 && !$timeoutWarning) { $timeoutWarning = true; $this->io->writeError('A connection timeout was encountered. If you intend to run Composer without connecting to the internet, run the command again prefixed with COMPOSER_DISABLE_NETWORK=1 to make Composer run in offline mode.'); } @@ -505,6 +506,18 @@ public function tick(): void } } + if (isset($progress['primary_ip']) && $progress['primary_ip'] !== $this->jobs[$i]['primaryIp']) { + if ( + isset($this->jobs[$i]['options']['prevent_ip_access_callable']) && + is_callable($this->jobs[$i]['options']['prevent_ip_access_callable']) && + $this->jobs[$i]['options']['prevent_ip_access_callable']($progress['primary_ip']) + ) { + $this->rejectJob($this->jobs[$i], new TransportException(sprintf('IP "%s" is blocked for "%s".', $progress['primary_ip'], $progress['url']))); + } + + $this->jobs[$i]['primaryIp'] = (string) $progress['primary_ip']; + } + // TODO progress } } diff --git a/src/Composer/Util/Http/ProxyHelper.php b/src/Composer/Util/Http/ProxyHelper.php deleted file mode 100644 index 58056e42970c..000000000000 --- a/src/Composer/Util/Http/ProxyHelper.php +++ /dev/null @@ -1,181 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Util\Http; - -/** - * Proxy discovery and helper class - * - * @internal - * @author John Stevenson - */ -class ProxyHelper -{ - /** - * Returns proxy environment values - * - * @return array{string|null, string|null, string|null} httpProxy, httpsProxy, noProxy values - * - * @throws \RuntimeException on malformed url - */ - public static function getProxyData(): array - { - $httpProxy = null; - $httpsProxy = null; - - // Handle http_proxy/HTTP_PROXY on CLI only for security reasons - if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { - if ($env = self::getProxyEnv(['http_proxy', 'HTTP_PROXY'], $name)) { - $httpProxy = self::checkProxy($env, $name); - } - } - - // Prefer CGI_HTTP_PROXY if available - if ($env = self::getProxyEnv(['CGI_HTTP_PROXY'], $name)) { - $httpProxy = self::checkProxy($env, $name); - } - - // Handle https_proxy/HTTPS_PROXY - if ($env = self::getProxyEnv(['https_proxy', 'HTTPS_PROXY'], $name)) { - $httpsProxy = self::checkProxy($env, $name); - } else { - $httpsProxy = $httpProxy; - } - - // Handle no_proxy - $noProxy = self::getProxyEnv(['no_proxy', 'NO_PROXY'], $name); - - return [$httpProxy, $httpsProxy, $noProxy]; - } - - /** - * Returns http context options for the proxy url - * - * @return array{http: array{proxy: string, header?: string}} - */ - public static function getContextOptions(string $proxyUrl): array - { - $proxy = parse_url($proxyUrl); - - // Remove any authorization - $proxyUrl = self::formatParsedUrl($proxy, false); - $proxyUrl = str_replace(['http://', 'https://'], ['tcp://', 'ssl://'], $proxyUrl); - - $options['http']['proxy'] = $proxyUrl; - - // Handle any authorization - if (isset($proxy['user'])) { - $auth = rawurldecode($proxy['user']); - - if (isset($proxy['pass'])) { - $auth .= ':' . rawurldecode($proxy['pass']); - } - $auth = base64_encode($auth); - // Set header as a string - $options['http']['header'] = "Proxy-Authorization: Basic {$auth}"; - } - - return $options; - } - - /** - * Sets/unsets request_fulluri value in http context options array - * - * @param mixed[] $options Set by method - */ - public static function setRequestFullUri(string $requestUrl, array &$options): void - { - if ('http' === parse_url($requestUrl, PHP_URL_SCHEME)) { - $options['http']['request_fulluri'] = true; - } else { - unset($options['http']['request_fulluri']); - } - } - - /** - * Searches $_SERVER for case-sensitive values - * - * @param string[] $names Names to search for - * @param string|null $name Name of any found value - * - * @return string|null The found value - */ - private static function getProxyEnv(array $names, ?string &$name): ?string - { - foreach ($names as $name) { - if (!empty($_SERVER[$name])) { - return $_SERVER[$name]; - } - } - - return null; - } - - /** - * Checks and formats a proxy url from the environment - * - * @throws \RuntimeException on malformed url - * @return string The formatted proxy url - */ - private static function checkProxy(string $proxyUrl, string $envName): string - { - $error = sprintf('malformed %s url', $envName); - $proxy = parse_url($proxyUrl); - - // We need parse_url to have identified a host - if (!isset($proxy['host'])) { - throw new \RuntimeException($error); - } - - $proxyUrl = self::formatParsedUrl($proxy, true); - - // We need a port because streams and curl use different defaults - if (!parse_url($proxyUrl, PHP_URL_PORT)) { - throw new \RuntimeException($error); - } - - return $proxyUrl; - } - - /** - * Formats a url from its component parts - * - * @param array{scheme?: string, host: string, port?: int, user?: string, pass?: string} $proxy - * - * @return string The formatted value - */ - private static function formatParsedUrl(array $proxy, bool $includeAuth): string - { - $proxyUrl = isset($proxy['scheme']) ? strtolower($proxy['scheme']) . '://' : ''; - - if ($includeAuth && isset($proxy['user'])) { - $proxyUrl .= $proxy['user']; - - if (isset($proxy['pass'])) { - $proxyUrl .= ':' . $proxy['pass']; - } - $proxyUrl .= '@'; - } - - $proxyUrl .= $proxy['host']; - - if (isset($proxy['port'])) { - $proxyUrl .= ':' . $proxy['port']; - } elseif (strpos($proxyUrl, 'http://') === 0) { - $proxyUrl .= ':80'; - } elseif (strpos($proxyUrl, 'https://') === 0) { - $proxyUrl .= ':443'; - } - - return $proxyUrl; - } -} diff --git a/src/Composer/Util/Http/ProxyItem.php b/src/Composer/Util/Http/ProxyItem.php new file mode 100644 index 000000000000..2839be923e79 --- /dev/null +++ b/src/Composer/Util/Http/ProxyItem.php @@ -0,0 +1,119 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Util\Http; + +/** + * @internal + * @author John Stevenson + */ +class ProxyItem +{ + /** @var non-empty-string */ + private $url; + /** @var non-empty-string */ + private $safeUrl; + /** @var ?non-empty-string */ + private $curlAuth; + /** @var string */ + private $optionsProxy; + /** @var ?non-empty-string */ + private $optionsAuth; + + /** + * @param string $proxyUrl The value from the environment + * @param string $envName The name of the environment variable + * @throws \RuntimeException If the proxy url is invalid + */ + public function __construct(string $proxyUrl, string $envName) + { + $syntaxError = sprintf('unsupported `%s` syntax', $envName); + + if (strpbrk($proxyUrl, "\r\n\t") !== false) { + throw new \RuntimeException($syntaxError); + } + if (false === ($proxy = parse_url($proxyUrl))) { + throw new \RuntimeException($syntaxError); + } + if (!isset($proxy['host'])) { + throw new \RuntimeException('unable to find proxy host in ' . $envName); + } + + $scheme = isset($proxy['scheme']) ? strtolower($proxy['scheme']) . '://' : 'http://'; + $safe = ''; + + if (isset($proxy['user'])) { + $safe = '***'; + $user = $proxy['user']; + $auth = rawurldecode($proxy['user']); + + if (isset($proxy['pass'])) { + $safe .= ':***'; + $user .= ':' . $proxy['pass']; + $auth .= ':' . rawurldecode($proxy['pass']); + } + + $safe .= '@'; + + if (strlen($user) > 0) { + $this->curlAuth = $user; + $this->optionsAuth = 'Proxy-Authorization: Basic ' . base64_encode($auth); + } + } + + $host = $proxy['host']; + $port = null; + + if (isset($proxy['port'])) { + $port = $proxy['port']; + } elseif ($scheme === 'http://') { + $port = 80; + } elseif ($scheme === 'https://') { + $port = 443; + } + + // We need a port because curl uses 1080 for http. Port 0 is reserved, + // but is considered valid depending on the PHP or Curl version. + if ($port === null) { + throw new \RuntimeException('unable to find proxy port in ' . $envName); + } + if ($port === 0) { + throw new \RuntimeException('port 0 is reserved in ' . $envName); + } + + $this->url = sprintf('%s%s:%d', $scheme, $host, $port); + $this->safeUrl = sprintf('%s%s%s:%d', $scheme, $safe, $host, $port); + + $scheme = str_replace(['http://', 'https://'], ['tcp://', 'ssl://'], $scheme); + $this->optionsProxy = sprintf('%s%s:%d', $scheme, $host, $port); + } + + /** + * Returns a RequestProxy instance for the scheme of the request url + * + * @param string $scheme The scheme of the request url + */ + public function toRequestProxy(string $scheme): RequestProxy + { + $options = ['http' => ['proxy' => $this->optionsProxy]]; + + if ($this->optionsAuth !== null) { + $options['http']['header'] = $this->optionsAuth; + } + + if ($scheme === 'http') { + $options['http']['request_fulluri'] = true; + } + + return new RequestProxy($this->url, $this->curlAuth, $options, $this->safeUrl); + } +} diff --git a/src/Composer/Util/Http/ProxyManager.php b/src/Composer/Util/Http/ProxyManager.php index 5af53ad0c483..3747cedaa722 100644 --- a/src/Composer/Util/Http/ProxyManager.php +++ b/src/Composer/Util/Http/ProxyManager.php @@ -14,7 +14,6 @@ use Composer\Downloader\TransportException; use Composer\Util\NoProxyPattern; -use Composer\Util\Url; /** * @internal @@ -24,40 +23,28 @@ class ProxyManager { /** @var ?string */ private $error = null; - /** @var array{http: ?string, https: ?string} */ - private $fullProxy; - /** @var array{http: ?string, https: ?string} */ - private $safeProxy; - /** @var array{http: array{options: mixed[]|null}, https: array{options: mixed[]|null}} */ - private $streams; - /** @var bool */ - private $hasProxy; - /** @var ?string */ - private $info = null; + /** @var ?ProxyItem */ + private $httpProxy = null; + /** @var ?ProxyItem */ + private $httpsProxy = null; /** @var ?NoProxyPattern */ private $noProxyHandler = null; - /** @var ?ProxyManager */ + /** @var ?self */ private static $instance = null; private function __construct() { - $this->fullProxy = $this->safeProxy = [ - 'http' => null, - 'https' => null, - ]; - - $this->streams['http'] = $this->streams['https'] = [ - 'options' => null, - ]; - - $this->hasProxy = false; - $this->initProxyData(); + try { + $this->getProxyData(); + } catch (\RuntimeException $e) { + $this->error = $e->getMessage(); + } } public static function getInstance(): ProxyManager { - if (!self::$instance) { + if (self::$instance === null) { self::$instance = new self(); } @@ -72,6 +59,11 @@ public static function reset(): void self::$instance = null; } + public function hasProxy(): bool + { + return $this->httpProxy !== null || $this->httpsProxy !== null; + } + /** * Returns a RequestProxy instance for the request url * @@ -79,95 +71,92 @@ public static function reset(): void */ public function getProxyForRequest(string $requestUrl): RequestProxy { - if ($this->error) { + if ($this->error !== null) { throw new TransportException('Unable to use a proxy: '.$this->error); } - $scheme = parse_url($requestUrl, PHP_URL_SCHEME) ?: 'http'; - $proxyUrl = ''; - $options = []; - $formattedProxyUrl = ''; - - if ($this->hasProxy && in_array($scheme, ['http', 'https'], true) && $this->fullProxy[$scheme]) { - if ($this->noProxy($requestUrl)) { - $formattedProxyUrl = 'excluded by no_proxy'; - } else { - $proxyUrl = $this->fullProxy[$scheme]; - $options = $this->streams[$scheme]['options']; - ProxyHelper::setRequestFullUri($requestUrl, $options); - $formattedProxyUrl = $this->safeProxy[$scheme]; - } + $scheme = (string) parse_url($requestUrl, PHP_URL_SCHEME); + $proxy = $this->getProxyForScheme($scheme); + + if ($proxy === null) { + return RequestProxy::none(); } - return new RequestProxy($proxyUrl, $options, $formattedProxyUrl); - } + if ($this->noProxy($requestUrl)) { + return RequestProxy::noProxy(); + } - /** - * Returns true if a proxy is being used - * - * @return bool If false any error will be in $message - */ - public function isProxying(): bool - { - return $this->hasProxy; + return $proxy->toRequestProxy($scheme); } /** - * Returns proxy configuration info which can be shown to the user - * - * @return string|null Safe proxy URL or an error message if setting up proxy failed or null if no proxy was configured + * Returns a ProxyItem if one is set for the scheme, otherwise null */ - public function getFormattedProxy(): ?string + private function getProxyForScheme(string $scheme): ?ProxyItem { - return $this->hasProxy ? $this->info : $this->error; + if ($scheme === 'http') { + return $this->httpProxy; + } + + if ($scheme === 'https') { + return $this->httpsProxy; + } + + return null; } /** - * Initializes proxy values from the environment + * Finds proxy values from the environment and sets class properties */ - private function initProxyData(): void + private function getProxyData(): void { - try { - [$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData(); - } catch (\RuntimeException $e) { - $this->error = $e->getMessage(); - - return; + // Handle http_proxy/HTTP_PROXY on CLI only for security reasons + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + [$env, $name] = $this->getProxyEnv('http_proxy'); + if ($env !== null) { + $this->httpProxy = new ProxyItem($env, $name); + } } - $info = []; - - if ($httpProxy) { - $info[] = $this->setData($httpProxy, 'http'); + // Handle cgi_http_proxy/CGI_HTTP_PROXY if needed + if ($this->httpProxy === null) { + [$env, $name] = $this->getProxyEnv('cgi_http_proxy'); + if ($env !== null) { + $this->httpProxy = new ProxyItem($env, $name); + } } - if ($httpsProxy) { - $info[] = $this->setData($httpsProxy, 'https'); + + // Handle https_proxy/HTTPS_PROXY + [$env, $name] = $this->getProxyEnv('https_proxy'); + if ($env !== null) { + $this->httpsProxy = new ProxyItem($env, $name); } - if ($this->hasProxy) { - $this->info = implode(', ', $info); - if ($noProxy) { - $this->noProxyHandler = new NoProxyPattern($noProxy); - } + + // Handle no_proxy/NO_PROXY + [$env, $name] = $this->getProxyEnv('no_proxy'); + if ($env !== null) { + $this->noProxyHandler = new NoProxyPattern($env); } } /** - * Sets initial data + * Searches $_SERVER for case-sensitive values * - * @param non-empty-string $url Proxy url - * @param 'http'|'https' $scheme Environment variable scheme - * - * @return non-empty-string + * @return array{0: string|null, 1: string} value, name */ - private function setData($url, $scheme): string + private function getProxyEnv(string $envName): array { - $safeProxy = Url::sanitize($url); - $this->fullProxy[$scheme] = $url; - $this->safeProxy[$scheme] = $safeProxy; - $this->streams[$scheme]['options'] = ProxyHelper::getContextOptions($url); - $this->hasProxy = true; + $names = [strtolower($envName), strtoupper($envName)]; - return sprintf('%s=%s', $scheme, $safeProxy); + foreach ($names as $name) { + if (is_string($_SERVER[$name] ?? null)) { + if ($_SERVER[$name] !== '') { + return [$_SERVER[$name], $name]; + } + } + } + + return [null, '']; } /** @@ -175,6 +164,10 @@ private function setData($url, $scheme): string */ private function noProxy(string $requestUrl): bool { - return $this->noProxyHandler && $this->noProxyHandler->test($requestUrl); + if ($this->noProxyHandler === null) { + return false; + } + + return $this->noProxyHandler->test($requestUrl); } } diff --git a/src/Composer/Util/Http/RequestProxy.php b/src/Composer/Util/Http/RequestProxy.php index c80c8799ea4f..d9df68861f03 100644 --- a/src/Composer/Util/Http/RequestProxy.php +++ b/src/Composer/Util/Http/RequestProxy.php @@ -12,78 +12,157 @@ namespace Composer\Util\Http; -use Composer\Util\Url; +use Composer\Downloader\TransportException; /** * @internal * @author John Stevenson + * + * @phpstan-type contextOptions array{http: array{proxy: string, header?: string, request_fulluri?: bool}} */ class RequestProxy { - /** @var mixed[] */ + /** @var ?contextOptions */ private $contextOptions; - /** @var bool */ - private $isSecure; - /** @var string */ - private $formattedUrl; - /** @var string */ + /** @var ?non-empty-string */ + private $status; + /** @var ?non-empty-string */ private $url; + /** @var ?non-empty-string */ + private $auth; /** - * @param mixed[] $contextOptions + * @param ?non-empty-string $url The proxy url, without authorization + * @param ?non-empty-string $auth Authorization for curl + * @param ?contextOptions $contextOptions + * @param ?non-empty-string $status */ - public function __construct(string $url, array $contextOptions, string $formattedUrl) + public function __construct(?string $url, ?string $auth, ?array $contextOptions, ?string $status) { $this->url = $url; + $this->auth = $auth; $this->contextOptions = $contextOptions; - $this->formattedUrl = $formattedUrl; - $this->isSecure = 0 === strpos($url, 'https://'); + $this->status = $status; + } + + public static function none(): RequestProxy + { + return new self(null, null, null, null); + } + + public static function noProxy(): RequestProxy + { + return new self(null, null, null, 'excluded by no_proxy'); } /** - * Returns an array of context options + * Returns the context options to use for this request, otherwise null * - * @return mixed[] + * @return ?contextOptions */ - public function getContextOptions(): array + public function getContextOptions(): ?array { return $this->contextOptions; } /** - * Returns the safe proxy url from the last request + * Returns an array of curl proxy options + * + * @param array $sslOptions + * @return array + */ + public function getCurlOptions(array $sslOptions): array + { + if ($this->isSecure() && !$this->supportsSecureProxy()) { + throw new TransportException('Cannot use an HTTPS proxy. PHP >= 7.3 and cUrl >= 7.52.0 are required.'); + } + + // Always set a proxy url, even an empty value, because it tells curl + // to ignore proxy environment variables + $options = [CURLOPT_PROXY => (string) $this->url]; + + // If using a proxy, tell curl to ignore no_proxy environment variables + if ($this->url !== null) { + $options[CURLOPT_NOPROXY] = ''; + } + + // Set any authorization + if ($this->auth !== null) { + $options[CURLOPT_PROXYAUTH] = CURLAUTH_BASIC; + $options[CURLOPT_PROXYUSERPWD] = $this->auth; + } + + if ($this->isSecure()) { + if (isset($sslOptions['cafile'])) { + $options[CURLOPT_PROXY_CAINFO] = $sslOptions['cafile']; + } + if (isset($sslOptions['capath'])) { + $options[CURLOPT_PROXY_CAPATH] = $sslOptions['capath']; + } + } + + return $options; + } + + /** + * Returns proxy info associated with this request * - * @param string|null $format Output format specifier - * @return string Safe proxy, no proxy or empty + * An empty return value means that the user has not set a proxy. + * A non-empty value will either be the sanitized proxy url if a proxy is + * required, or a message indicating that a no_proxy value has disabled the + * proxy. + * + * @param ?string $format Output format specifier */ - public function getFormattedUrl(?string $format = ''): string + public function getStatus(?string $format = null): string { - $result = ''; - if ($this->formattedUrl) { - $format = $format ?: '%s'; - $result = sprintf($format, $this->formattedUrl); + if ($this->status === null) { + return ''; + } + + $format = $format ?? '%s'; + if (strpos($format, '%s') !== false) { + return sprintf($format, $this->status); } - return $result; + throw new \InvalidArgumentException('String format specifier is missing'); } /** - * Returns the proxy url + * Returns true if the request url has been excluded by a no_proxy value * - * @return string Proxy url or empty + * A false value can also mean that the user has not set a proxy. */ - public function getUrl(): string + public function isExcludedByNoProxy(): bool { - return $this->url; + return $this->status !== null && $this->url === null; } /** - * Returns true if this is a secure-proxy + * Returns true if this is a secure (HTTPS) proxy * - * @return bool False if not secure or there is no proxy + * A false value means that this is either an HTTP proxy, or that a proxy + * is not required for this request, or that the user has not set a proxy. */ public function isSecure(): bool { - return $this->isSecure; + return 0 === strpos((string) $this->url, 'https://'); + } + + /** + * Returns true if an HTTPS proxy can be used. + * + * This depends on PHP7.3+ for CURL_VERSION_HTTPS_PROXY + * and curl including the feature (from version 7.52.0) + */ + public function supportsSecureProxy(): bool + { + if (false === ($version = curl_version()) || !defined('CURL_VERSION_HTTPS_PROXY')) { + return false; + } + + $features = $version['features']; + + return (bool) ($features & CURL_VERSION_HTTPS_PROXY); } } diff --git a/src/Composer/Util/Http/Response.php b/src/Composer/Util/Http/Response.php index 5e5d18a2e9a2..e355f256ab6f 100644 --- a/src/Composer/Util/Http/Response.php +++ b/src/Composer/Util/Http/Response.php @@ -36,7 +36,7 @@ class Response */ public function __construct(array $request, ?int $code, array $headers, ?string $body) { - if (!isset($request['url'])) { // @phpstan-ignore-line + if (!isset($request['url'])) { throw new \LogicException('url key missing from request array'); } $this->request = $request; @@ -101,8 +101,7 @@ public function decodeJson() */ public function collect(): void { - /** @phpstan-ignore-next-line */ - $this->request = $this->code = $this->headers = $this->body = null; + unset($this->request, $this->code, $this->headers, $this->body); } /** diff --git a/src/Composer/Util/Perforce.php b/src/Composer/Util/Perforce.php index d24209a42183..bfed834b16cd 100644 --- a/src/Composer/Util/Perforce.php +++ b/src/Composer/Util/Perforce.php @@ -14,6 +14,7 @@ use Composer\IO\IOInterface; use Composer\Pcre\Preg; +use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; /** @@ -81,7 +82,7 @@ public static function create($repoConfig, string $port, string $path, ProcessEx public static function checkServerExists(string $url, ProcessExecutor $processExecutor): bool { - return 0 === $processExecutor->execute('p4 -p ' . ProcessExecutor::escape($url) . ' info -s', $ignoredOutput); + return 0 === $processExecutor->execute(['p4', '-p', $url, 'info', '-s'], $ignoredOutput); } /** @@ -248,7 +249,7 @@ public function queryP4User(): void } $this->p4User = $this->io->ask('Enter P4 User:'); if ($this->windowsFlag) { - $command = 'p4 set P4USER=' . $this->p4User; + $command = $this->getP4Executable().' set P4USER=' . $this->p4User; } else { $command = 'export P4USER=' . $this->p4User; } @@ -261,7 +262,7 @@ public function queryP4User(): void protected function getP4variable(string $name): ?string { if ($this->windowsFlag) { - $command = 'p4 set'; + $command = $this->getP4Executable().' set'; $this->executeCommand($command); $result = trim($this->commandResult); $resArray = explode(PHP_EOL, $result); @@ -309,7 +310,7 @@ public function queryP4Password(): ?string */ public function generateP4Command(string $command, bool $useClient = true): string { - $p4Command = 'p4 '; + $p4Command = $this->getP4Executable().' '; $p4Command .= '-u ' . $this->getUser() . ' '; if ($useClient) { $p4Command .= '-c ' . $this->getClient() . ' '; @@ -342,7 +343,7 @@ public function isLoggedIn(): bool public function connectClient(): void { $p4CreateClientCommand = $this->generateP4Command( - 'client -i < ' . str_replace(" ", "\\ ", $this->getP4ClientSpec()) + 'client -i < ' . ProcessExecutor::escape($this->getP4ClientSpec()) ); $this->executeCommand($p4CreateClientCommand); } @@ -620,4 +621,17 @@ public function setFilesystem(Filesystem $fs): void { $this->filesystem = $fs; } + + private function getP4Executable(): string + { + static $p4Executable; + + if ($p4Executable) { + return $p4Executable; + } + + $finder = new ExecutableFinder(); + + return $p4Executable = $finder->find('p4') ?? 'p4'; + } } diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index 33ff620560e3..dcbfaa1c5104 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -25,6 +25,8 @@ class Platform private static $isVirtualBoxGuest = null; /** @var ?bool */ private static $isWindowsSubsystemForLinux = null; + /** @var ?bool */ + private static $isDocker = null; /** * getcwd() equivalent which always returns a string @@ -52,6 +54,19 @@ public static function getCwd(bool $allowEmpty = false): string return $cwd; } + /** + * Infallible realpath version that falls back on the given $path if realpath is not working + */ + public static function realpath(string $path): string + { + $realPath = realpath($path); + if ($realPath === false) { + return $path; + } + + return $realPath; + } + /** * getenv() equivalent but reads from the runtime global variables first * @@ -76,7 +91,6 @@ public static function getEnv(string $name) */ public static function putEnv(string $name, string $value): void { - $value = (string) $value; putenv($name . '=' . $value); $_SERVER[$name] = $_ENV[$name] = $value; } @@ -100,12 +114,12 @@ public static function expandPath(string $path): string } return Preg::replaceCallback('#^(\$|(?P%))(?P\w++)(?(percent)%)(?P.*)#', static function ($matches): string { - assert(is_string($matches['var'])); - assert('' !== $matches['var']); - // Treat HOME as an alias for USERPROFILE on Windows for legacy reasons if (Platform::isWindows() && $matches['var'] === 'HOME') { - return (Platform::getEnv('HOME') ?: Platform::getEnv('USERPROFILE')) . $matches['path']; + if ((bool) Platform::getEnv('HOME')) { + return Platform::getEnv('HOME') . $matches['path']; + } + return Platform::getEnv('USERPROFILE') . $matches['path']; } return Platform::getEnv($matches['var']) . $matches['path']; @@ -129,7 +143,9 @@ public static function getUserDirectory(): string if (\function_exists('posix_getuid') && \function_exists('posix_getpwuid')) { $info = posix_getpwuid(posix_getuid()); - return $info['dir']; + if (is_array($info)) { + return $info['dir']; + } } throw new \RuntimeException('Could not determine user directory'); @@ -149,10 +165,10 @@ public static function isWindowsSubsystemForLinux(): bool } if ( - !ini_get('open_basedir') + !(bool) ini_get('open_basedir') && is_readable('/proc/version') && false !== stripos((string)Silencer::call('file_get_contents', '/proc/version'), 'microsoft') - && !file_exists('/.dockerenv') // docker running inside WSL should not be seen as WSL + && !self::isDocker() // Docker and Podman running inside WSL should not be seen as WSL ) { return self::$isWindowsSubsystemForLinux = true; } @@ -169,6 +185,46 @@ public static function isWindows(): bool return \defined('PHP_WINDOWS_VERSION_BUILD'); } + public static function isDocker(): bool + { + if (null !== self::$isDocker) { + return self::$isDocker; + } + + // cannot check so assume no + if ((bool) ini_get('open_basedir')) { + return self::$isDocker = false; + } + + // .dockerenv and .containerenv are present in some cases but not reliably + if (file_exists('/.dockerenv') || file_exists('/run/.containerenv') || file_exists('/var/run/.containerenv')) { + return self::$isDocker = true; + } + + // see https://www.baeldung.com/linux/is-process-running-inside-container + $cgroups = [ + '/proc/self/mountinfo', // cgroup v2 + '/proc/1/cgroup', // cgroup v1 + ]; + foreach ($cgroups as $cgroup) { + if (!is_readable($cgroup)) { + continue; + } + // suppress errors as some environments have these files as readable but system restrictions prevent the read from succeeding + // see https://github.com/composer/composer/issues/12095 + try { + $data = @file_get_contents($cgroup); + } catch (\Throwable $e) { + break; + } + if (is_string($data) && str_contains($data, '/var/lib/docker/')) { + return self::$isDocker = true; + } + } + + return self::$isDocker = false; + } + /** * @return int return a guaranteed binary length of the string, regardless of silly mbstring configs */ @@ -176,7 +232,7 @@ public static function strlen(string $str): int { static $useMbString = null; if (null === $useMbString) { - $useMbString = \function_exists('mb_strlen') && ini_get('mbstring.func_overload'); + $useMbString = \function_exists('mb_strlen') && (bool) ini_get('mbstring.func_overload'); } if ($useMbString) { @@ -200,7 +256,7 @@ public static function isTty($fd = null): bool // detect msysgit/mingw and assume this is a tty because detection // does not work correctly, see https://github.com/composer/composer/issues/9690 - if (in_array(strtoupper(self::getEnv('MSYSTEM') ?: ''), ['MINGW32', 'MINGW64'], true)) { + if (in_array(strtoupper((string) self::getEnv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { return true; } @@ -216,8 +272,11 @@ public static function isTty($fd = null): bool } $stat = @fstat($fd); + if ($stat === false) { + return false; + } // Check if formatted mode is S_IFCHR - return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; + return 0020000 === ($stat['mode'] & 0170000); } /** @@ -250,7 +309,7 @@ private static function isVirtualBoxGuest(): bool if (function_exists('posix_getpwuid') && function_exists('posix_geteuid')) { $processUser = posix_getpwuid(posix_geteuid()); - if ($processUser && $processUser['name'] === 'vagrant') { + if (is_array($processUser) && $processUser['name'] === 'vagrant') { return self::$isVirtualBoxGuest = true; } } @@ -262,7 +321,7 @@ private static function isVirtualBoxGuest(): bool if (defined('PHP_OS_FAMILY') && PHP_OS_FAMILY === 'Linux') { $process = new ProcessExecutor(); try { - if (0 === $process->execute('lsmod | grep vboxguest', $ignoredOutput)) { + if (0 === $process->execute(['lsmod'], $output) && str_contains($output, 'vboxguest')) { return self::$isVirtualBoxGuest = true; } } catch (\Exception $e) { diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 25e4c903b57c..dc773beb91ee 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -20,6 +20,7 @@ use Symfony\Component\Process\Exception\RuntimeException; use React\Promise\Promise; use React\Promise\PromiseInterface; +use Symfony\Component\Process\ExecutableFinder; /** * @author Robert Schönthal @@ -33,6 +34,21 @@ class ProcessExecutor private const STATUS_FAILED = 4; private const STATUS_ABORTED = 5; + private const BUILTIN_CMD_COMMANDS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; + + private const GIT_CMDS_NEED_GIT_DIR = [ + ['show'], + ['log'], + ['branch'], + ['remote', 'set-url'] + ]; + /** @var int */ protected static $timeout = 300; @@ -56,6 +72,9 @@ class ProcessExecutor /** @var bool */ private $allowAsync = false; + /** @var array */ + private static $executables = []; + public function __construct(?IOInterface $io = null) { $this->io = $io; @@ -64,7 +83,7 @@ public function __construct(?IOInterface $io = null) /** * runs a process on the commandline * - * @param string|list $command the command to execute + * @param string|non-empty-list $command the command to execute * @param mixed $output the output will be written into this var if passed by ref * if a callable is passed it will be used as output handler * @param null|string $cwd the working directory @@ -82,7 +101,7 @@ public function execute($command, &$output = null, ?string $cwd = null): int /** * runs a process on the commandline in TTY mode * - * @param string|list $command the command to execute + * @param string|non-empty-list $command the command to execute * @param null|string $cwd the working directory * @return int statuscode */ @@ -96,23 +115,30 @@ public function executeTty($command, ?string $cwd = null): int } /** - * @param string|list $command + * @param string|non-empty-list $command + * @param array|null $env * @param mixed $output */ - private function doExecute($command, ?string $cwd, bool $tty, &$output = null): int + private function runProcess($command, ?string $cwd, ?array $env, bool $tty, &$output = null): ?int { - $this->outputCommandRun($command, $cwd, false); - - $this->captureOutput = func_num_args() > 3; - $this->errorOutput = ''; + // On Windows, we don't rely on the OS to find the executable if possible to avoid lookups + // in the current directory which could be untrusted. Instead we use the ExecutableFinder. if (is_string($command)) { - $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout()); + if (Platform::isWindows() && Preg::isMatch('{^([^:/\\\\]++) }', $command, $match)) { + $command = substr_replace($command, self::escape(self::getExecutable($match[1])), 0, strlen($match[1])); + } + + $process = Process::fromShellCommandline($command, $cwd, $env, null, static::getTimeout()); } else { - $process = new Process($command, $cwd, null, null, static::getTimeout()); + if (Platform::isWindows() && \strlen($command[0]) === strcspn($command[0], ':/\\')) { + $command[0] = self::getExecutable($command[0]); + } + + $process = new Process($command, $cwd, $env, null, static::getTimeout()); } - if (!Platform::isWindows() && $tty) { + if (! Platform::isWindows() && $tty) { try { $process->setTty(true); } catch (RuntimeException $e) { @@ -124,11 +150,18 @@ private function doExecute($command, ?string $cwd, bool $tty, &$output = null): $this->outputHandler($type, $buffer); }; - $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal) { - if ($this->io !== null) { - $this->io->writeError('Received '.$signal.', aborting when child process is done', true, IOInterface::DEBUG); + $signalHandler = SignalHandler::create( + [SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], + function (string $signal) { + if ($this->io !== null) { + $this->io->writeError( + 'Received '.$signal.', aborting when child process is done', + true, + IOInterface::DEBUG + ); + } } - }); + ); try { $process->run($callback); @@ -150,6 +183,35 @@ private function doExecute($command, ?string $cwd, bool $tty, &$output = null): return $process->getExitCode(); } + /** + * @param string|non-empty-list $command + * @param mixed $output + */ + private function doExecute($command, ?string $cwd, bool $tty, &$output = null): int + { + $this->outputCommandRun($command, $cwd, false); + + $this->captureOutput = func_num_args() > 3; + $this->errorOutput = ''; + + $env = null; + + $requiresGitDirEnv = $this->requiresGitDirEnv($command); + if ($cwd !== null && $requiresGitDirEnv) { + $isBareRepository = !is_dir(sprintf('%s/.git', rtrim($cwd, '/'))); + if ($isBareRepository) { + $configValue = ''; + $this->runProcess(['git', 'config', 'safe.bareRepository'], $cwd, ['GIT_DIR' => $cwd], $tty, $configValue); + $configValue = trim($configValue); + if ($configValue === 'explicit') { + $env = ['GIT_DIR' => $cwd]; + } + } + } + + return $this->runProcess($command, $cwd, $env, $tty, $output); + } + /** * starts a process on the commandline in async mode * @@ -416,8 +478,6 @@ private function outputCommandRun($command, ?string $cwd, bool $async): void $commandString = is_string($command) ? $command : implode(' ', array_map(self::class.'::escape', $command)); $safeCommand = Preg::replaceCallback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', static function ($m): string { - assert(is_string($m['user'])); - // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx) we obfuscate that if (Preg::isMatch('{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+)$}', $m['user'])) { return '://***:***@'; @@ -456,7 +516,23 @@ private static function escapeArgument($argument): string } // New lines break cmd.exe command parsing - $argument = strtr($argument, "\n", ' '); + // and special chars like the fullwidth quote can be used to break out + // of parameter encoding via "Best Fit" encoding conversion + $argument = strtr($argument, [ + "\n" => ' ', + "\u{ff02}" => '"', + "\u{02ba}" => '"', + "\u{301d}" => '"', + "\u{301e}" => '"', + "\u{030e}" => '"', + "\u{ff1a}" => ':', + "\u{0589}" => ':', + "\u{2236}" => ':', + "\u{ff0f}" => '/', + "\u{2044}" => '/', + "\u{2215}" => '/', + "\u{00b4}" => '/', + ]); // In addition to whitespace, commas need quoting to preserve paths $quote = strpbrk($argument, " \t,") !== false; @@ -478,4 +554,42 @@ private static function escapeArgument($argument): string return $argument; } + + /** + * @param string[]|string $command + */ + public function requiresGitDirEnv($command): bool + { + $cmd = !is_array($command) ? explode(' ', $command) : $command; + if ($cmd[0] !== 'git') { + return false; + } + + foreach (self::GIT_CMDS_NEED_GIT_DIR as $gitCmd) { + if (array_intersect($cmd, $gitCmd) === $gitCmd) { + return true; + } + } + + return false; + } + + /** + * Resolves executable paths on Windows + */ + private static function getExecutable(string $name): string + { + if (\in_array(strtolower($name), self::BUILTIN_CMD_COMMANDS, true)) { + return $name; + } + + if (!isset(self::$executables[$name])) { + $path = (new ExecutableFinder())->find($name, $name); + if ($path !== null) { + self::$executables[$name] = $path; + } + } + + return self::$executables[$name] ?? $name; + } } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index 8abf015a7eb1..cafdee21384a 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -64,8 +64,6 @@ class RemoteFilesystem private $redirects; /** @var int */ private $maxRedirects = 20; - /** @var ProxyManager */ - private $proxyManager; /** * Constructor. @@ -91,7 +89,6 @@ public function __construct(IOInterface $io, Config $config, array $options = [] $this->options = array_replace_recursive($this->options, $options); $this->config = $config; $this->authHelper = $authHelper ?? new AuthHelper($io, $config); - $this->proxyManager = ProxyManager::getInstance(); } /** @@ -249,6 +246,10 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti $origFileUrl = $fileUrl; + if (isset($options['prevent_ip_access_callable'])) { + throw new \RuntimeException("RemoteFilesystem doesn't support the 'prevent_ip_access_callable' config."); + } + if (isset($options['gitlab-token'])) { $fileUrl .= (false === strpos($fileUrl, '?') ? '?' : '&') . 'access_token='.$options['gitlab-token']; unset($options['gitlab-token']); @@ -272,8 +273,8 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti $ctx = StreamContextFactory::getContext($fileUrl, $options, ['notification' => [$this, 'callbackGet']]); - $proxy = $this->proxyManager->getProxyForRequest($fileUrl); - $usingProxy = $proxy->getFormattedUrl(' using proxy (%s)'); + $proxy = ProxyManager::getInstance()->getProxyForRequest($fileUrl); + $usingProxy = $proxy->getStatus(' using proxy (%s)'); $this->io->writeError((strpos($origFileUrl, 'http') === 0 ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); unset($origFileUrl, $proxy, $usingProxy); @@ -509,6 +510,8 @@ protected function get(string $originUrl, string $fileUrl, array $additionalOpti * @param int $maxFileSize The maximum allowed file size * * @return string|false The response contents or false on failure + * + * @param-out list $responseHeaders */ protected function getRemoteContents(string $originUrl, string $fileUrl, $context, ?array &$responseHeaders = null, ?int $maxFileSize = null) { @@ -530,7 +533,12 @@ protected function getRemoteContents(string $originUrl, string $fileUrl, $contex } // https://www.php.net/manual/en/reserved.variables.httpresponseheader.php - $responseHeaders = $http_response_header ?? []; + if (\PHP_VERSION_ID >= 80400) { + $responseHeaders = http_get_last_response_headers(); + http_clear_last_response_headers(); + } else { + $responseHeaders = $http_response_header ?? []; + } if (null !== $e) { throw $e; diff --git a/src/Composer/Util/Silencer.php b/src/Composer/Util/Silencer.php index f2b9f73fc9ad..6dd0efb34987 100644 --- a/src/Composer/Util/Silencer.php +++ b/src/Composer/Util/Silencer.php @@ -33,7 +33,7 @@ class Silencer public static function suppress(?int $mask = null): int { if (!isset($mask)) { - $mask = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT; + $mask = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED; } $old = error_reporting(); self::$stack[] = $old; diff --git a/src/Composer/Util/StreamContextFactory.php b/src/Composer/Util/StreamContextFactory.php index 57fbe0f0e7af..be4c976a9055 100644 --- a/src/Composer/Util/StreamContextFactory.php +++ b/src/Composer/Util/StreamContextFactory.php @@ -76,7 +76,8 @@ public static function initOptions(string $url, array $options, bool $forCurl = // Add stream proxy options if there is a proxy if (!$forCurl) { $proxy = ProxyManager::getInstance()->getProxyForRequest($url); - if ($proxyOptions = $proxy->getContextOptions()) { + $proxyOptions = $proxy->getContextOptions(); + if ($proxyOptions !== null) { $isHttpsRequest = 0 === strpos($url, 'https://'); if ($proxy->isSecure()) { diff --git a/src/Composer/Util/Svn.php b/src/Composer/Util/Svn.php index ea7d5dbeb7a1..506a14ec752b 100644 --- a/src/Composer/Util/Svn.php +++ b/src/Composer/Util/Svn.php @@ -90,7 +90,7 @@ public static function cleanEnv(): void * Execute an SVN remote command and try to fix up the process with credentials * if necessary. * - * @param string $command SVN command to run + * @param non-empty-list $command SVN command to run * @param string $url SVN url * @param ?string $cwd Working directory * @param ?string $path Target for a checkout @@ -98,7 +98,7 @@ public static function cleanEnv(): void * * @throws \RuntimeException */ - public function execute(string $command, string $url, ?string $cwd = null, ?string $path = null, bool $verbose = false): string + public function execute(array $command, string $url, ?string $cwd = null, ?string $path = null, bool $verbose = false): string { // Ensure we are allowed to use this URL by config $this->config->prohibitUrlByConfig($url, $this->io); @@ -110,20 +110,23 @@ public function execute(string $command, string $url, ?string $cwd = null, ?stri * Execute an SVN local command and try to fix up the process with credentials * if necessary. * - * @param string $command SVN command to run + * @param non-empty-list $command SVN command to run * @param string $path Path argument passed thru to the command * @param string $cwd Working directory * @param bool $verbose Output all output to the user * * @throws \RuntimeException */ - public function executeLocal(string $command, string $path, ?string $cwd = null, bool $verbose = false): string + public function executeLocal(array $command, string $path, ?string $cwd = null, bool $verbose = false): string { // A local command has no remote url return $this->executeWithAuthRetry($command, $cwd, '', $path, $verbose); } - private function executeWithAuthRetry(string $svnCommand, ?string $cwd, string $url, ?string $path, bool $verbose): ?string + /** + * @param non-empty-list $svnCommand + */ + private function executeWithAuthRetry(array $svnCommand, ?string $cwd, string $url, ?string $path, bool $verbose): ?string { // Regenerate the command at each try, to use the newly user-provided credentials $command = $this->getCommand($svnCommand, $url, $path); @@ -209,22 +212,23 @@ protected function doAuthDance(): Svn /** * A method to create the svn commands run. * - * @param string $cmd Usually 'svn ls' or something like that. + * @param non-empty-list $cmd Usually 'svn ls' or something like that. * @param string $url Repo URL. * @param string $path Target for a checkout + * + * @return non-empty-list */ - protected function getCommand(string $cmd, string $url, ?string $path = null): string + protected function getCommand(array $cmd, string $url, ?string $path = null): array { - $cmd = sprintf( - '%s %s%s -- %s', + $cmd = array_merge( $cmd, - '--non-interactive ', - $this->getCredentialString(), - ProcessExecutor::escape($url) + ['--non-interactive'], + $this->getCredentialArgs(), + ['--', $url] ); - if ($path) { - $cmd .= ' ' . ProcessExecutor::escape($path); + if ($path !== null) { + $cmd[] = $path; } return $cmd; @@ -234,18 +238,18 @@ protected function getCommand(string $cmd, string $url, ?string $path = null): s * Return the credential string for the svn command. * * Adds --no-auth-cache when credentials are present. + * + * @return list */ - protected function getCredentialString(): string + protected function getCredentialArgs(): array { if (!$this->hasAuth()) { - return ''; + return []; } - return sprintf( - ' %s--username %s --password %s ', - $this->getAuthCache(), - ProcessExecutor::escape($this->getUsername()), - ProcessExecutor::escape($this->getPassword()) + return array_merge( + $this->getAuthCacheArgs(), + ['--username', $this->getUsername(), '--password', $this->getPassword()] ); } @@ -295,10 +299,12 @@ protected function hasAuth(): bool /** * Return the no-auth-cache switch. + * + * @return list */ - protected function getAuthCache(): string + protected function getAuthCacheArgs(): array { - return $this->cacheCredentials ? '' : '--no-auth-cache '; + return $this->cacheCredentials ? [] : ['--no-auth-cache']; } /** @@ -349,7 +355,7 @@ private function createAuthFromUrl(): bool public function binaryVersion(): ?string { if (!self::$version) { - if (0 === $this->process->execute('svn --version', $output)) { + if (0 === $this->process->execute(['svn', '--version'], $output)) { if (Preg::isMatch('{(\d+(?:\.\d+)+)}', $output, $match)) { self::$version = $match[1]; } diff --git a/src/Composer/Util/TlsHelper.php b/src/Composer/Util/TlsHelper.php index aca227270bfe..da0801a1a74b 100644 --- a/src/Composer/Util/TlsHelper.php +++ b/src/Composer/Util/TlsHelper.php @@ -76,13 +76,18 @@ public static function getCertificateNames($certificate): ?array if (isset($info['extensions']['subjectAltName'])) { $subjectAltNames = Preg::split('{\s*,\s*}', $info['extensions']['subjectAltName']); - $subjectAltNames = array_filter(array_map(static function ($name): ?string { - if (0 === strpos($name, 'DNS:')) { - return strtolower(ltrim(substr($name, 4))); + $subjectAltNames = array_filter( + array_map(static function ($name): ?string { + if (0 === strpos($name, 'DNS:')) { + return strtolower(ltrim(substr($name, 4))); + } + + return null; + }, $subjectAltNames), + function (?string $san) { + return $san !== null; } - - return null; - }, $subjectAltNames)); + ); $subjectAltNames = array_values($subjectAltNames); } @@ -145,7 +150,7 @@ public static function getCertificateFingerprint(string $certificate): string $pemtrim = substr($pubkeypem, strpos($pubkeypem, $start) + strlen($start), (strlen($pubkeypem) - strpos($pubkeypem, $end)) * (-1)); $der = base64_decode($pemtrim); - return sha1($der); + return hash('sha1', $der); } /** diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index 5d703a20def2..32f61347b451 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -75,7 +75,7 @@ public static function getOrigin(Config $config, string $url): string $origin .= ':'.$port; } - if (strpos($origin, '.github.com') === (strlen($origin) - 11)) { + if (str_ends_with($origin, '.github.com') && $origin !== 'codeload.github.com') { return 'github.com'; } @@ -110,7 +110,6 @@ public static function sanitize(string $url): string $url = Preg::replace('{([&?]access_token=)[^&]+}', '$1***', $url); $url = Preg::replaceCallback('{^(?P[a-z0-9]+://)?(?P[^:/\s@]+):(?P[^@\s/]+)@}i', static function ($m): string { - assert(is_string($m['user'])); // if the username looks like a long (12char+) hex string, or a modern github token (e.g. ghp_xxx) we obfuscate that if (Preg::isMatch('{^([a-f0-9]{12,}|gh[a-z]_[a-zA-Z0-9_]+|github_pat_[a-zA-Z0-9_]+)$}', $m['user'])) { return $m['prefix'].'***:***@'; diff --git a/tests/Composer/Test/Advisory/AuditorTest.php b/tests/Composer/Test/Advisory/AuditorTest.php index 748f6a5f8055..a5472f0d804f 100644 --- a/tests/Composer/Test/Advisory/AuditorTest.php +++ b/tests/Composer/Test/Advisory/AuditorTest.php @@ -15,7 +15,6 @@ use Composer\Advisory\PartialSecurityAdvisory; use Composer\Advisory\SecurityAdvisory; use Composer\IO\BufferIO; -use Composer\IO\NullIO; use Composer\Package\CompletePackage; use Composer\Package\Package; use Composer\Package\Version\VersionParser; @@ -23,7 +22,6 @@ use Composer\Repository\RepositorySet; use Composer\Test\TestCase; use Composer\Advisory\Auditor; -use Composer\Util\Platform; use InvalidArgumentException; class AuditorTest extends TestCase @@ -39,7 +37,7 @@ public static function auditProvider() ], 'warningOnly' => true, ], - 'expected' => 0, + 'expected' => Auditor::STATUS_OK, 'output' => 'No security vulnerability advisories found.', ]; @@ -52,7 +50,7 @@ public static function auditProvider() ], 'warningOnly' => true, ], - 'expected' => 1, + 'expected' => Auditor::STATUS_VULNERABLE, 'output' => 'Found 2 security vulnerability advisories affecting 1 package: Package: vendor1/package1 Severity: high @@ -85,7 +83,7 @@ public static function auditProvider() 'warningOnly' => false, 'abandoned' => Auditor::ABANDONED_IGNORE, ], - 'expected' => 0, + 'expected' => Auditor::STATUS_OK, 'output' => 'No security vulnerability advisories found.', ]; @@ -98,7 +96,7 @@ public static function auditProvider() 'warningOnly' => true, 'abandoned' => Auditor::ABANDONED_REPORT, ], - 'expected' => 0, + 'expected' => Auditor::STATUS_OK, 'output' => 'No security vulnerability advisories found. Found 2 abandoned packages: vendor/abandoned is abandoned. Use foo/bar instead. @@ -115,7 +113,7 @@ public static function auditProvider() 'abandoned' => Auditor::ABANDONED_FAIL, 'format' => Auditor::FORMAT_TABLE, ], - 'expected' => 2, + 'expected' => Auditor::STATUS_ABANDONED, 'output' => 'No security vulnerability advisories found. Found 2 abandoned packages: +-------------------+----------------------------------------------------------------------------------+ @@ -126,6 +124,46 @@ public static function auditProvider() +-------------------+----------------------------------------------------------------------------------+', ]; + yield 'vulnerable and abandoned packages fails' => [ + 'data' => [ + 'packages' => [ + new Package('vendor1/package1', '8.2.1', '8.2.1'), + $abandonedWithReplacement, + $abandonedNoReplacement, + ], + 'warningOnly' => false, + 'abandoned' => Auditor::ABANDONED_FAIL, + 'format' => Auditor::FORMAT_TABLE, + ], + 'expected' => Auditor::STATUS_VULNERABLE | Auditor::STATUS_ABANDONED, + 'output' => 'Found 2 security vulnerability advisories affecting 1 package: ++-------------------+----------------------------------------------------------------------------------+ +| Package | vendor1/package1 | +| Severity | high | +| CVE | CVE3 | +| Title | advisory4 | +| URL | https://advisory.example.com/advisory4 | +| Affected versions | >=8,<8.2.2|>=1,<2.5.6 | +| Reported at | 2022-05-25T13:21:00+00:00 | ++-------------------+----------------------------------------------------------------------------------+ ++-------------------+----------------------------------------------------------------------------------+ +| Package | vendor1/package1 | +| Severity | medium | +| CVE | | +| Title | advisory5 | +| URL | https://advisory.example.com/advisory5 | +| Affected versions | >=8,<8.2.2|>=1,<2.5.6 | +| Reported at | 2022-05-25T13:21:00+00:00 | ++-------------------+----------------------------------------------------------------------------------+ +Found 2 abandoned packages: ++-------------------+----------------------------------------------------------------------------------+ +| Abandoned Package | Suggested Replacement | ++-------------------+----------------------------------------------------------------------------------+ +| vendor/abandoned | foo/bar | +| vendor/abandoned2 | none | ++-------------------+----------------------------------------------------------------------------------+', + ]; + yield 'abandoned packages fails with json format' => [ 'data' => [ 'packages' => [ @@ -136,7 +174,7 @@ public static function auditProvider() 'abandoned' => Auditor::ABANDONED_FAIL, 'format' => Auditor::FORMAT_JSON, ], - 'expected' => 2, + 'expected' => Auditor::STATUS_ABANDONED, 'output' => '{ "advisories": [], "abandoned": { @@ -158,11 +196,12 @@ public function testAudit(array $data, int $expected, string $output): void } $auditor = new Auditor(); $result = $auditor->audit($io = new BufferIO(), $this->getRepoSet(), $data['packages'], $data['format'] ?? Auditor::FORMAT_PLAIN, $data['warningOnly'], [], $data['abandoned'] ?? Auditor::ABANDONED_IGNORE); - $this->assertSame($expected, $result); - $this->assertSame($output, trim(str_replace("\r", '', $io->getOutput()))); + self::assertSame($expected, $result); + self::assertSame($output, trim(str_replace("\r", '', $io->getOutput()))); } - public function ignoredIdsProvider(): \Generator { + public function ignoredIdsProvider(): \Generator + { yield 'ignore by CVE' => [ [ new Package('vendor1/package1', '3.0.0.0', '3.0.0'), @@ -178,7 +217,7 @@ public function ignoredIdsProvider(): \Generator { ['text' => 'URL: https://advisory.example.com/advisory1'], ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], - ] + ], ]; yield 'ignore by CVE with reasoning' => [ [ @@ -196,7 +235,7 @@ public function ignoredIdsProvider(): \Generator { ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], ['text' => 'Ignore reason: A good reason'], - ] + ], ]; yield 'ignore by advisory id' => [ [ @@ -213,7 +252,7 @@ public function ignoredIdsProvider(): \Generator { ['text' => 'URL: https://advisory.example.com/advisory2'], ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], - ] + ], ]; yield 'ignore by remote id' => [ [ @@ -230,7 +269,7 @@ public function ignoredIdsProvider(): \Generator { ['text' => 'URL: https://advisory.example.com/advisory17'], ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'], - ] + ], ]; yield '1 vulnerability, 0 ignored' => [ [ @@ -247,7 +286,7 @@ public function ignoredIdsProvider(): \Generator { ['text' => 'URL: https://advisory.example.com/advisory1'], ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], ['text' => 'Reported at: 2022-05-25T13:21:00+00:00'], - ] + ], ]; yield '1 vulnerability, 3 ignored affecting 2 packages' => [ [ @@ -295,7 +334,7 @@ public function ignoredIdsProvider(): \Generator { ['text' => 'URL: https://advisory.example.com/advisory7'], ['text' => 'Affected versions: >=3,<3.4.3|>=1,<2.5.6'], ['text' => 'Reported at: 2015-05-25T13:21:00+00:00'], - ] + ], ]; } @@ -311,7 +350,56 @@ public function testAuditWithIgnore($packages, $ignoredIds, $exitCode, $expected $auditor = new Auditor(); $result = $auditor->audit($io = $this->getIOMock(), $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false, $ignoredIds); $io->expects($expectedOutput, true); - $this->assertSame($exitCode, $result); + self::assertSame($exitCode, $result); + } + + public function ignoreSeverityProvider(): \Generator + { + yield 'ignore medium' => [ + [ + new Package('vendor1/package1', '2.0.0.0', '2.0.0'), + ], + ['medium'], + 1, + [ + ['text' => 'Found 2 ignored security vulnerability advisories affecting 1 package:'], + ], + ]; + yield 'ignore high' => [ + [ + new Package('vendor1/package1', '2.0.0.0', '2.0.0'), + ], + ['high'], + 1, + [ + ['text' => 'Found 1 ignored security vulnerability advisory affecting 1 package:'], + ], + ]; + yield 'ignore high and medium' => [ + [ + new Package('vendor1/package1', '2.0.0.0', '2.0.0'), + ], + ['high', 'medium'], + 0, + [ + ['text' => 'Found 3 ignored security vulnerability advisories affecting 1 package:'], + ], + ]; + } + + /** + * @dataProvider ignoreSeverityProvider + * @phpstan-param array<\Composer\Package\Package> $packages + * @phpstan-param array $ignoredSeverities + * @phpstan-param 0|positive-int $exitCode + * @phpstan-param list $expectedOutput + */ + public function testAuditWithIgnoreSeverity($packages, $ignoredSeverities, $exitCode, $expectedOutput): void + { + $auditor = new Auditor(); + $result = $auditor->audit($io = $this->getIOMock(), $this->getRepoSet(), $packages, Auditor::FORMAT_PLAIN, false, [], Auditor::ABANDONED_IGNORE, $ignoredSeverities); + $io->expects($expectedOutput, true); + self::assertSame($exitCode, $result); } private function getRepoSet(): RepositorySet diff --git a/tests/Composer/Test/AllFunctionalTest.php b/tests/Composer/Test/AllFunctionalTest.php index 7d68344313c0..1ef22001d43c 100644 --- a/tests/Composer/Test/AllFunctionalTest.php +++ b/tests/Composer/Test/AllFunctionalTest.php @@ -78,7 +78,7 @@ public function testBuildPhar(): void $ri = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::SELF_FIRST); foreach ($ri as $file) { - $targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathName(); + $targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathname(); if ($file->isDir()) { $fs->ensureDirectoryExists($targetPath); } else { @@ -87,13 +87,14 @@ public function testBuildPhar(): void } $proc = new Process([PHP_BINARY, '-dphar.readonly=0', './bin/compile'], $target); + $proc->setTimeout(300); $exitcode = $proc->run(); - if ($exitcode !== 0 || trim($proc->getOutput())) { + if ($exitcode !== 0 || trim($proc->getOutput()) !== '') { $this->fail($proc->getOutput()); } - $this->assertFileExists(self::$pharPath); + self::assertFileExists(self::$pharPath); copy(self::$pharPath, __DIR__.'/../../composer-test.phar'); } @@ -136,7 +137,9 @@ public function testIntegration(string $testFile): void $line++; } if ($expected[$i] === '%') { - Preg::isMatchStrictGroups('{%(.+?)%}', substr($expected, $i), $match); + if (!Preg::isMatchStrictGroups('{%(.+?)%}', substr($expected, $i), $match)) { + throw new \LogicException('Failed to match %...% in '.substr($expected, $i)); + } $regex = $match[1]; if (Preg::isMatch('{'.$regex.'}', substr($output, $j), $match)) { @@ -146,7 +149,7 @@ public function testIntegration(string $testFile): void } else { $this->fail( 'Failed to match pattern '.$regex.' at line '.$line.' / abs offset '.$i.': ' - .substr($output, $j, min(strpos($output, "\n", $j) - $j, 100)).PHP_EOL.PHP_EOL. + .substr($output, $j, min(((int) strpos($output, "\n", $j)) - $j, 100)).PHP_EOL.PHP_EOL. 'Output:'.PHP_EOL.$output ); } @@ -154,8 +157,8 @@ public function testIntegration(string $testFile): void if ($expected[$i] !== $output[$j]) { $this->fail( 'Output does not match expectation at line '.$line.' / abs offset '.$i.': '.PHP_EOL - .'-'.substr($expected, $i, min(strpos($expected, "\n", $i) - $i, 100)).PHP_EOL - .'+'.substr($output, $j, min(strpos($output, "\n", $j) - $j, 100)).PHP_EOL.PHP_EOL + .'-'.substr($expected, $i, min(((int) strpos($expected, "\n", $i)) - $i, 100)).PHP_EOL + .'+'.substr($output, $j, min(((int) strpos($output, "\n", $j)) - $j, 100)).PHP_EOL.PHP_EOL .'Output:'.PHP_EOL.$output ); } @@ -164,16 +167,16 @@ public function testIntegration(string $testFile): void } } if (isset($testData['EXPECT-REGEX'])) { - $this->assertMatchesRegularExpression($testData['EXPECT-REGEX'], $this->cleanOutput($output)); + self::assertMatchesRegularExpression($testData['EXPECT-REGEX'], $this->cleanOutput($output)); } if (isset($testData['EXPECT-REGEXES'])) { $cleanOutput = $this->cleanOutput($output); foreach (explode("\n", $testData['EXPECT-REGEXES']) as $regex) { - $this->assertMatchesRegularExpression($regex, $cleanOutput, 'Output: '.$output); + self::assertMatchesRegularExpression($regex, $cleanOutput, 'Output: '.$output); } } if (isset($testData['EXPECT-EXIT-CODE'])) { - $this->assertSame($testData['EXPECT-EXIT-CODE'], $exitCode); + self::assertSame($testData['EXPECT-EXIT-CODE'], $exitCode); } } @@ -195,7 +198,7 @@ public static function getTestFiles(): array */ private function parseTestFile(string $file): array { - $tokens = Preg::split('#(?:^|\n*)--([A-Z-]+)--\n#', file_get_contents($file), -1, PREG_SPLIT_DELIM_CAPTURE); + $tokens = Preg::split('#(?:^|\n*)--([A-Z-]+)--\n#', (string) file_get_contents($file), -1, PREG_SPLIT_DELIM_CAPTURE); $data = []; $section = null; @@ -248,7 +251,7 @@ private function parseTestFile(string $file): array throw new \RuntimeException('The test file must have a section named "EXPECT", "EXPECT-REGEX", or "EXPECT-REGEXES".'); } - return $data; // @phpstan-ignore-line + return $data; // @phpstan-ignore return.type } private function cleanOutput(string $output): string diff --git a/tests/Composer/Test/ApplicationTest.php b/tests/Composer/Test/ApplicationTest.php index 0cd0d1b3886e..866892a3b7ef 100644 --- a/tests/Composer/Test/ApplicationTest.php +++ b/tests/Composer/Test/ApplicationTest.php @@ -49,7 +49,7 @@ public function testDevWarning(): void $application->doRun(new ArrayInput(['command' => 'about']), $output); $expectedOutput = sprintf('Warning: This development build of Composer is over 60 days old. It is recommended to update it by running "%s self-update" to get the latest version.', $_SERVER['PHP_SELF']).PHP_EOL; - $this->assertStringContainsString($expectedOutput, $output->fetch()); + self::assertStringContainsString($expectedOutput, $output->fetch()); } /** @@ -72,6 +72,18 @@ public function testDevWarningSuppressedForSelfUpdate(): void $output = new BufferedOutput(); $application->doRun(new ArrayInput(['command' => 'self-update']), $output); - $this->assertSame('', $output->fetch()); + self::assertSame('', $output->fetch()); + } + + /** + * @runInSeparateProcess + * @see https://github.com/composer/composer/issues/12107 + */ + public function testProcessIsolationWorksMultipleTimes(): void + { + $application = new Application; + $application->add(new \Composer\Command\AboutCommand); + self::assertSame(0, $application->doRun(new ArrayInput(['command' => 'about']), new BufferedOutput())); + self::assertSame(0, $application->doRun(new ArrayInput(['command' => 'about']), new BufferedOutput())); } } diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 08d51ac749dd..67930eabb04f 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -210,13 +210,13 @@ public function testRootPackageAutoloading(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); // Assert that autoload_namespaces.php was correctly generated. - $this->assertAutoloadFiles('main', $this->vendorDir.'/composer'); + self::assertAutoloadFiles('main', $this->vendorDir.'/composer'); // Assert that autoload_psr4.php was correctly generated. - $this->assertAutoloadFiles('psr4', $this->vendorDir.'/composer', 'psr4'); + self::assertAutoloadFiles('psr4', $this->vendorDir.'/composer', 'psr4'); // Assert that autoload_classmap.php was correctly generated. - $this->assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap'); } public function testRootPackageDevAutoloading(): void @@ -250,11 +250,11 @@ public function testRootPackageDevAutoloading(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); // check standard autoload - $this->assertAutoloadFiles('main5', $this->vendorDir.'/composer'); - $this->assertAutoloadFiles('classmap7', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('main5', $this->vendorDir.'/composer'); + self::assertAutoloadFiles('classmap7', $this->vendorDir.'/composer', 'classmap'); // make sure dev autoload is correctly dumped - $this->assertAutoloadFiles('files2', $this->vendorDir.'/composer', 'files'); + self::assertAutoloadFiles('files2', $this->vendorDir.'/composer', 'files'); } public function testRootPackageDevAutoloadingDisabledByDefault(): void @@ -283,11 +283,11 @@ public function testRootPackageDevAutoloadingDisabledByDefault(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); // check standard autoload - $this->assertAutoloadFiles('main4', $this->vendorDir.'/composer'); - $this->assertAutoloadFiles('classmap7', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('main4', $this->vendorDir.'/composer'); + self::assertAutoloadFiles('classmap7', $this->vendorDir.'/composer', 'classmap'); // make sure dev autoload is disabled when dev mode is set to false - $this->assertFalse(is_file($this->vendorDir.'/composer/autoload_files.php')); + self::assertFalse(is_file($this->vendorDir.'/composer/autoload_files.php')); } public function testVendorDirSameAsWorkingDir(): void @@ -316,9 +316,9 @@ public function testVendorDirSameAsWorkingDir(): void file_put_contents($this->vendorDir.'/composersrc/foo.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_2'); - $this->assertAutoloadFiles('main3', $this->vendorDir.'/composer'); - $this->assertAutoloadFiles('psr4_3', $this->vendorDir.'/composer', 'psr4'); - $this->assertAutoloadFiles('classmap3', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('main3', $this->vendorDir.'/composer'); + self::assertAutoloadFiles('psr4_3', $this->vendorDir.'/composer', 'psr4'); + self::assertAutoloadFiles('classmap3', $this->vendorDir.'/composer', 'classmap'); } public function testRootPackageAutoloadingAlternativeVendorDir(): void @@ -345,9 +345,9 @@ public function testRootPackageAutoloadingAlternativeVendorDir(): void $this->fs->ensureDirectoryExists($this->workingDir.'/composersrc'); file_put_contents($this->workingDir.'/composersrc/foo.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_3'); - $this->assertAutoloadFiles('main2', $this->vendorDir.'/composer'); - $this->assertAutoloadFiles('psr4_2', $this->vendorDir.'/composer', 'psr4'); - $this->assertAutoloadFiles('classmap2', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('main2', $this->vendorDir.'/composer'); + self::assertAutoloadFiles('psr4_2', $this->vendorDir.'/composer', 'psr4'); + self::assertAutoloadFiles('classmap2', $this->vendorDir.'/composer', 'classmap'); } public function testRootPackageAutoloadingWithTargetDir(): void @@ -374,11 +374,11 @@ public function testRootPackageAutoloadingWithTargetDir(): void file_put_contents($this->workingDir.'/bar.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'TargetDir'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_target_dir.php', $this->vendorDir.'/autoload.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_target_dir.php', $this->vendorDir.'/composer/autoload_real.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_target_dir.php', $this->vendorDir.'/composer/autoload_static.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_target_dir.php', $this->vendorDir.'/composer/autoload_files.php'); - $this->assertAutoloadFiles('classmap6', $this->vendorDir.'/composer', 'classmap'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_target_dir.php', $this->vendorDir.'/autoload.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_target_dir.php', $this->vendorDir.'/composer/autoload_real.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_target_dir.php', $this->vendorDir.'/composer/autoload_static.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_target_dir.php', $this->vendorDir.'/composer/autoload_files.php'); + self::assertAutoloadFiles('classmap6', $this->vendorDir.'/composer', 'classmap'); } public function testDuplicateFilesWarning(): void @@ -431,8 +431,8 @@ public function testVendorsAutoloading(): void $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b/src'); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_5'); - $this->assertAutoloadFiles('vendors', $this->vendorDir.'/composer'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); + self::assertAutoloadFiles('vendors', $this->vendorDir.'/composer'); + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); } public function testVendorsAutoloadingWithMetapackages(): void @@ -465,8 +465,8 @@ public function testVendorsAutoloadingWithMetapackages(): void $this->fs->ensureDirectoryExists($this->vendorDir.'/a/a/lib'); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_5'); - $this->assertAutoloadFiles('vendors_meta', $this->vendorDir.'/composer'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); + self::assertAutoloadFiles('vendors_meta', $this->vendorDir.'/composer'); + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); } public function testNonDevAutoloadExclusionWithRecursion(): void @@ -498,8 +498,8 @@ public function testNonDevAutoloadExclusionWithRecursion(): void $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b/src'); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_5'); - $this->assertAutoloadFiles('vendors', $this->vendorDir.'/composer'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); + self::assertAutoloadFiles('vendors', $this->vendorDir.'/composer'); + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); } public function testNonDevAutoloadShouldIncludeReplacedPackages(): void @@ -527,7 +527,7 @@ public function testNonDevAutoloadShouldIncludeReplacedPackages(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_5'); - $this->assertEquals( + self::assertEquals( [ 'B\\C\\C' => $this->vendorDir.'/b/b/src/C/C.php', 'Composer\\InstalledVersions' => $this->vendorDir . '/composer/InstalledVersions.php', @@ -565,8 +565,8 @@ public function testNonDevAutoloadExclusionWithRecursionReplace(): void $this->fs->ensureDirectoryExists($this->vendorDir.'/b/b/src'); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_5'); - $this->assertAutoloadFiles('vendors', $this->vendorDir.'/composer'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); + self::assertAutoloadFiles('vendors', $this->vendorDir.'/composer'); + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated, even if empty."); } public function testNonDevAutoloadReplacesNestedRequirements(): void @@ -618,7 +618,7 @@ public function testNonDevAutoloadReplacesNestedRequirements(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_5'); - $this->assertAutoloadFiles('classmap9', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('classmap9', $this->vendorDir.'/composer', 'classmap'); } public function testPharAutoload(): void @@ -657,9 +657,9 @@ public function testPharAutoload(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, 'Phar'); - $this->assertAutoloadFiles('phar', $this->vendorDir . '/composer'); - $this->assertAutoloadFiles('phar_psr4', $this->vendorDir . '/composer', 'psr4'); - $this->assertAutoloadFiles('phar_static', $this->vendorDir . '/composer', 'static'); + self::assertAutoloadFiles('phar', $this->vendorDir . '/composer'); + self::assertAutoloadFiles('phar_psr4', $this->vendorDir . '/composer', 'psr4'); + self::assertAutoloadFiles('phar_static', $this->vendorDir . '/composer', 'static'); } public function testPSRToClassMapIgnoresNonExistingDir(): void @@ -676,8 +676,8 @@ public function testPSRToClassMapIgnoresNonExistingDir(): void ->will($this->returnValue([])); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_8'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); - $this->assertEquals( + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + self::assertEquals( [ 'Composer\\InstalledVersions' => $this->vendorDir.'/composer/InstalledVersions.php', ], @@ -706,7 +706,7 @@ public function testPSRToClassMapIgnoresNonPSRClasses(): void file_put_contents($this->workingDir.'/psr4/badfile.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); $expectedClassmap = <<assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); } public function testVendorsClassMapAutoloading(): void @@ -753,8 +753,8 @@ public function testVendorsClassMapAutoloading(): void file_put_contents($this->vendorDir.'/b/b/lib/c.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_6'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); - $this->assertEquals( + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + self::assertEquals( [ 'ClassMapBar' => $this->vendorDir.'/b/b/src/b.php', 'ClassMapBaz' => $this->vendorDir.'/b/b/lib/c.php', @@ -763,7 +763,7 @@ public function testVendorsClassMapAutoloading(): void ], include $this->vendorDir.'/composer/autoload_classmap.php' ); - $this->assertAutoloadFiles('classmap4', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('classmap4', $this->vendorDir.'/composer', 'classmap'); } public function testVendorsClassMapAutoloadingWithTargetDir(): void @@ -794,8 +794,8 @@ public function testVendorsClassMapAutoloadingWithTargetDir(): void file_put_contents($this->vendorDir.'/b/b/src/c.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_6'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); - $this->assertEquals( + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + self::assertEquals( [ 'ClassMapBar' => $this->vendorDir.'/a/a/target/lib/b.php', 'ClassMapBaz' => $this->vendorDir.'/b/b/src/c.php', @@ -836,8 +836,8 @@ public function testClassMapAutoloadingEmptyDirAndExactFile(): void file_put_contents($this->vendorDir.'/c/c/foo/test.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_7'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); - $this->assertEquals( + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + self::assertEquals( [ 'ClassMapBar' => $this->vendorDir.'/b/b/test.php', 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/test.php', @@ -846,9 +846,9 @@ public function testClassMapAutoloadingEmptyDirAndExactFile(): void ], include $this->vendorDir.'/composer/autoload_classmap.php' ); - $this->assertAutoloadFiles('classmap5', $this->vendorDir.'/composer', 'classmap'); - $this->assertStringNotContainsString('$loader->setClassMapAuthoritative(true);', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); - $this->assertStringNotContainsString('$loader->setApcuPrefix(', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertAutoloadFiles('classmap5', $this->vendorDir.'/composer', 'classmap'); + self::assertStringNotContainsString('$loader->setClassMapAuthoritative(true);', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertStringNotContainsString('$loader->setApcuPrefix(', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); } public function testClassMapAutoloadingAuthoritativeAndApcu(): void @@ -884,8 +884,8 @@ public function testClassMapAutoloadingAuthoritativeAndApcu(): void $this->generator->setApcu(true); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_7'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); - $this->assertEquals( + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + self::assertEquals( [ 'ClassMapBar' => $this->vendorDir.'/b/b/ClassMapBar.php', 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/ClassMapBaz.php', @@ -894,10 +894,10 @@ public function testClassMapAutoloadingAuthoritativeAndApcu(): void ], include $this->vendorDir.'/composer/autoload_classmap.php' ); - $this->assertAutoloadFiles('classmap8', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('classmap8', $this->vendorDir.'/composer', 'classmap'); - $this->assertStringContainsString('$loader->setClassMapAuthoritative(true);', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); - $this->assertStringContainsString('$loader->setApcuPrefix(', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertStringContainsString('$loader->setClassMapAuthoritative(true);', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertStringContainsString('$loader->setApcuPrefix(', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); } public function testClassMapAutoloadingAuthoritativeAndApcuPrefix(): void @@ -933,8 +933,8 @@ public function testClassMapAutoloadingAuthoritativeAndApcuPrefix(): void $this->generator->setApcu(true, 'custom\'Prefix'); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_7'); - $this->assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); - $this->assertEquals( + self::assertFileExists($this->vendorDir.'/composer/autoload_classmap.php', "ClassMap file needs to be generated."); + self::assertEquals( [ 'ClassMapBar' => $this->vendorDir.'/b/b/ClassMapBar.php', 'ClassMapBaz' => $this->vendorDir.'/c/c/foo/ClassMapBaz.php', @@ -943,10 +943,10 @@ public function testClassMapAutoloadingAuthoritativeAndApcuPrefix(): void ], include $this->vendorDir.'/composer/autoload_classmap.php' ); - $this->assertAutoloadFiles('classmap8', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('classmap8', $this->vendorDir.'/composer', 'classmap'); - $this->assertStringContainsString('$loader->setClassMapAuthoritative(true);', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); - $this->assertStringContainsString('$loader->setApcuPrefix(\'custom\\\'Prefix\');', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertStringContainsString('$loader->setClassMapAuthoritative(true);', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertStringContainsString('$loader->setApcuPrefix(\'custom\\\'Prefix\');', (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); } public function testFilesAutoloadGeneration(): void @@ -982,18 +982,18 @@ public function testFilesAutoloadGeneration(): void file_put_contents($this->workingDir.'/root.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'FilesAutoload'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions.php', $this->vendorDir.'/composer/autoload_real.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions.php', $this->vendorDir.'/composer/autoload_static.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions.php', $this->vendorDir.'/composer/autoload_files.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions.php', $this->vendorDir.'/composer/autoload_real.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions.php', $this->vendorDir.'/composer/autoload_static.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions.php', $this->vendorDir.'/composer/autoload_files.php'); $loader = require $this->vendorDir . '/autoload.php'; $loader->unregister(); - $this->assertTrue(function_exists('testFilesAutoloadGeneration1')); - $this->assertTrue(function_exists('testFilesAutoloadGeneration2')); - $this->assertTrue(function_exists('testFilesAutoloadGeneration3')); - $this->assertTrue(function_exists('testFilesAutoloadGeneration4')); - $this->assertTrue(function_exists('testFilesAutoloadGenerationRoot')); + self::assertTrue(function_exists('testFilesAutoloadGeneration1')); + self::assertTrue(function_exists('testFilesAutoloadGeneration2')); + self::assertTrue(function_exists('testFilesAutoloadGeneration3')); + self::assertTrue(function_exists('testFilesAutoloadGeneration4')); + self::assertTrue(function_exists('testFilesAutoloadGenerationRoot')); } public function testFilesAutoloadGenerationRemoveExtraEntitiesFromAutoloadFiles(): void @@ -1047,24 +1047,24 @@ public function testFilesAutoloadGenerationRemoveExtraEntitiesFromAutoloadFiles( file_put_contents($this->workingDir.'/root.php', 'generator->dump($this->config, $this->repository, $autoloadPackage, $this->im, 'composer', false, 'FilesAutoload'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions_with_include_paths.php', $this->vendorDir.'/composer/autoload_real.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions_with_include_paths.php', $this->vendorDir.'/composer/autoload_static.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions.php', $this->vendorDir.'/composer/autoload_files.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/include_paths_functions.php', $this->vendorDir.'/composer/include_paths.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions_with_include_paths.php', $this->vendorDir.'/composer/autoload_real.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions_with_include_paths.php', $this->vendorDir.'/composer/autoload_static.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions.php', $this->vendorDir.'/composer/autoload_files.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/include_paths_functions.php', $this->vendorDir.'/composer/include_paths.php'); $this->generator->dump($this->config, $this->repository, $autoloadPackage, $this->im, 'composer', false, 'FilesAutoload'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions_with_include_paths.php', $this->vendorDir.'/composer/autoload_real.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions_with_removed_extra.php', $this->vendorDir.'/composer/autoload_files.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/include_paths_functions_with_removed_extra.php', $this->vendorDir.'/composer/include_paths.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions_with_include_paths.php', $this->vendorDir.'/composer/autoload_real.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_files_functions_with_removed_extra.php', $this->vendorDir.'/composer/autoload_files.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/include_paths_functions_with_removed_extra.php', $this->vendorDir.'/composer/include_paths.php'); $this->generator->dump($this->config, $this->repository, $notAutoloadPackage, $this->im, 'composer', false, 'FilesAutoload'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php', $this->vendorDir.'/composer/autoload_real.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php', $this->vendorDir.'/composer/autoload_static.php'); - $this->assertFileDoesNotExist($this->vendorDir.'/composer/autoload_files.php'); - $this->assertFileDoesNotExist($this->vendorDir.'/composer/include_paths.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_functions.php', $this->vendorDir.'/autoload.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_functions_with_removed_include_paths_and_autolad_files.php', $this->vendorDir.'/composer/autoload_real.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_functions_with_removed_include_paths_and_autolad_files.php', $this->vendorDir.'/composer/autoload_static.php'); + self::assertFileDoesNotExist($this->vendorDir.'/composer/autoload_files.php'); + self::assertFileDoesNotExist($this->vendorDir.'/composer/include_paths.php'); } public function testFilesAutoloadOrderByDependencies(): void @@ -1124,19 +1124,19 @@ public function testFilesAutoloadOrderByDependencies(): void file_put_contents($this->workingDir . '/root2.php', 'generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'FilesAutoloadOrder'); - $this->assertFileContentEquals(__DIR__ . '/Fixtures/autoload_functions_by_dependency.php', $this->vendorDir . '/autoload.php'); - $this->assertFileContentEquals(__DIR__ . '/Fixtures/autoload_real_files_by_dependency.php', $this->vendorDir . '/composer/autoload_real.php'); - $this->assertFileContentEquals(__DIR__ . '/Fixtures/autoload_static_files_by_dependency.php', $this->vendorDir . '/composer/autoload_static.php'); + self::assertFileContentEquals(__DIR__ . '/Fixtures/autoload_functions_by_dependency.php', $this->vendorDir . '/autoload.php'); + self::assertFileContentEquals(__DIR__ . '/Fixtures/autoload_real_files_by_dependency.php', $this->vendorDir . '/composer/autoload_real.php'); + self::assertFileContentEquals(__DIR__ . '/Fixtures/autoload_static_files_by_dependency.php', $this->vendorDir . '/composer/autoload_static.php'); $loader = require $this->vendorDir . '/autoload.php'; $loader->unregister(); - $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency1')); - $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency2')); - $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency3')); - $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency4')); - $this->assertTrue(function_exists('testFilesAutoloadOrderByDependency5')); - $this->assertTrue(function_exists('testFilesAutoloadOrderByDependencyRoot')); + self::assertTrue(function_exists('testFilesAutoloadOrderByDependency1')); + self::assertTrue(function_exists('testFilesAutoloadOrderByDependency2')); + self::assertTrue(function_exists('testFilesAutoloadOrderByDependency3')); + self::assertTrue(function_exists('testFilesAutoloadOrderByDependency4')); + self::assertTrue(function_exists('testFilesAutoloadOrderByDependency5')); + self::assertTrue(function_exists('testFilesAutoloadOrderByDependencyRoot')); } /** @@ -1235,9 +1235,9 @@ public function testOverrideVendorsAutoloading(): void EOF; $this->generator->dump($this->config, $this->repository, $rootPackage, $this->im, 'composer', true, '_9'); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); } public function testIncludePathFileGeneration(): void @@ -1266,8 +1266,8 @@ public function testIncludePathFileGeneration(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_10'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/include_paths.php', $this->vendorDir.'/composer/include_paths.php'); - $this->assertEquals( + self::assertFileContentEquals(__DIR__.'/Fixtures/include_paths.php', $this->vendorDir.'/composer/include_paths.php'); + self::assertEquals( [ $this->vendorDir."/a/a/lib", $this->vendorDir."/b/b/library", @@ -1300,7 +1300,7 @@ public function testIncludePathsArePrependedInAutoloadFile(): void $loader = require $this->vendorDir."/autoload.php"; $loader->unregister(); - $this->assertEquals( + self::assertEquals( $this->vendorDir."/a/a/lib".PATH_SEPARATOR.$oldIncludePath, get_include_path() ); @@ -1329,7 +1329,7 @@ public function testIncludePathsInRootPackage(): void $loader = require $this->vendorDir."/autoload.php"; $loader->unregister(); - $this->assertEquals( + self::assertEquals( $this->workingDir."/lib".PATH_SEPARATOR.$this->workingDir."/src".PATH_SEPARATOR.$this->vendorDir."/a/a/lib".PATH_SEPARATOR.$oldIncludePath, get_include_path() ); @@ -1353,7 +1353,7 @@ public function testIncludePathFileWithoutPathsIsSkipped(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, '_12'); - $this->assertFileDoesNotExist($this->vendorDir."/composer/include_paths.php"); + self::assertFileDoesNotExist($this->vendorDir."/composer/include_paths.php"); } public function testPreAndPostEventsAreDispatchedDuringAutoloadDump(): void @@ -1367,7 +1367,7 @@ public function testPreAndPostEventsAreDispatchedDuringAutoloadDump(): void [ScriptEvents::POST_AUTOLOAD_DUMP, false] ]; - $this->assertSame(array_shift($series), [$type, $dev]); + self::assertSame(array_shift($series), [$type, $dev]); return 0; }); @@ -1400,8 +1400,8 @@ public function testUseGlobalIncludePath(): void $this->fs->ensureDirectoryExists($this->vendorDir.'/a'); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'IncludePath'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_include_path.php', $this->vendorDir.'/composer/autoload_real.php'); - $this->assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_include_path.php', $this->vendorDir.'/composer/autoload_static.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_real_include_path.php', $this->vendorDir.'/composer/autoload_real.php'); + self::assertFileContentEquals(__DIR__.'/Fixtures/autoload_static_include_path.php', $this->vendorDir.'/composer/autoload_static.php'); } public function testVendorDirExcludedFromWorkingDir(): void @@ -1511,11 +1511,11 @@ public function testVendorDirExcludedFromWorkingDir(): void EOF; - $this->assertStringEqualsFile($vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); - $this->assertStringEqualsFile($vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); - $this->assertStringEqualsFile($vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); - $this->assertStringContainsString("\$vendorDir . '/b/b/bootstrap.php',\n", (string) file_get_contents($vendorDir.'/composer/autoload_files.php')); - $this->assertStringContainsString("\$baseDir . '/test.php',\n", (string) file_get_contents($vendorDir.'/composer/autoload_files.php')); + self::assertStringEqualsFile($vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); + self::assertStringEqualsFile($vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); + self::assertStringEqualsFile($vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); + self::assertStringContainsString("\$vendorDir . '/b/b/bootstrap.php',\n", (string) file_get_contents($vendorDir.'/composer/autoload_files.php')); + self::assertStringContainsString("\$baseDir . '/test.php',\n", (string) file_get_contents($vendorDir.'/composer/autoload_files.php')); } public function testUpLevelRelativePaths(): void @@ -1596,10 +1596,10 @@ public function testUpLevelRelativePaths(): void EOF; - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); - $this->assertStringContainsString("\$baseDir . '/../test.php',\n", (string) file_get_contents($this->vendorDir.'/composer/autoload_files.php')); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); + self::assertStringContainsString("\$baseDir . '/../test.php',\n", (string) file_get_contents($this->vendorDir.'/composer/autoload_files.php')); } public function testAutoloadRulesInPackageThatDoesNotExistOnDisk(): void @@ -1632,7 +1632,7 @@ public function testAutoloadRulesInPackageThatDoesNotExistOnDisk(): void ); EOF; - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); $dep->setAutoload([ 'psr-4' => ['Acme\Foo\\' => './src-psr4'], @@ -1652,7 +1652,7 @@ public function testAutoloadRulesInPackageThatDoesNotExistOnDisk(): void ); EOF; - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); $dep->setAutoload([ 'classmap' => ['classmap'], @@ -1660,14 +1660,14 @@ public function testAutoloadRulesInPackageThatDoesNotExistOnDisk(): void try { $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_19'); } catch (\RuntimeException $e) { - $this->assertSame('Could not scan for classes inside "'.$this->vendorDir.'/dep/a/classmap" which does not appear to be a file nor a folder', $e->getMessage()); + self::assertSame('Could not scan for classes inside "'.$this->vendorDir.'/dep/a/classmap" which does not appear to be a file nor a folder', $e->getMessage()); } $dep->setAutoload([ 'files' => ['./test.php'], ]); $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_19'); - $this->assertStringContainsString("\$vendorDir . '/dep/a/test.php',\n", (string) file_get_contents($this->vendorDir.'/composer/autoload_files.php')); + self::assertStringContainsString("\$vendorDir . '/dep/a/test.php',\n", (string) file_get_contents($this->vendorDir.'/composer/autoload_files.php')); $package->setAutoload([ 'exclude-from-classmap' => ['../excludedroot', 'root/excl'], @@ -1677,7 +1677,7 @@ public function testAutoloadRulesInPackageThatDoesNotExistOnDisk(): void ]); $map = $this->generator->buildPackageMap($this->im, $package, [$dep]); $parsed = $this->generator->parseAutoloads($map, $package); - $this->assertSame([ + self::assertSame([ preg_quote(strtr((string) realpath(dirname($this->workingDir)), '\\', '/')).'/excludedroot($|/)', preg_quote(strtr((string) realpath($this->workingDir), '\\', '/')).'/root/excl($|/)', ], $parsed['exclude-from-classmap']); @@ -1746,9 +1746,9 @@ public function testEmptyPaths(): void EOF; - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_classmap.php', $expectedClassmap); } public function testVendorSubstringPath(): void @@ -1794,8 +1794,8 @@ public function testVendorSubstringPath(): void EOF; $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', false, 'VendorSubstring'); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); - $this->assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_namespaces.php', $expectedNamespace); + self::assertStringEqualsFile($this->vendorDir.'/composer/autoload_psr4.php', $expectedPsr4); } public function testExcludeFromClassmap(): void @@ -1864,7 +1864,7 @@ public function testExcludeFromClassmap(): void $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); // Assert that autoload_classmap.php was correctly generated. - $this->assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap'); + self::assertAutoloadFiles('classmap', $this->vendorDir.'/composer', 'classmap'); } /** @@ -1896,11 +1896,11 @@ public function testGeneratesPlatformCheck(array $requires, ?string $expectedFix $this->generator->dump($this->config, $this->repository, $package, $this->im, 'composer', true, '_1'); if (null === $expectedFixture) { - $this->assertFileDoesNotExist($this->vendorDir . '/composer/platform_check.php'); - $this->assertStringNotContainsString("require __DIR__ . '/platform_check.php';", (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertFileDoesNotExist($this->vendorDir . '/composer/platform_check.php'); + self::assertStringNotContainsString("require __DIR__ . '/platform_check.php';", (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); } else { - $this->assertFileContentEquals(__DIR__ . '/Fixtures/platform/' . $expectedFixture . '.php', $this->vendorDir . '/composer/platform_check.php'); - $this->assertStringContainsString("require __DIR__ . '/platform_check.php';", (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); + self::assertFileContentEquals(__DIR__ . '/Fixtures/platform/' . $expectedFixture . '.php', $this->vendorDir . '/composer/platform_check.php'); + self::assertStringContainsString("require __DIR__ . '/platform_check.php';", (string) file_get_contents($this->vendorDir.'/composer/autoload_real.php')); } } @@ -2025,7 +2025,7 @@ private function assertAutoloadFiles(string $name, string $dir, string $type = ' { $a = __DIR__.'/Fixtures/autoload_'.$name.'.php'; $b = $dir.'/autoload_'.$type.'.php'; - $this->assertFileContentEquals($a, $b); + self::assertFileContentEquals($a, $b); } public static function assertFileContentEquals(string $expected, string $actual, ?string $message = null): void diff --git a/tests/Composer/Test/Autoload/ClassLoaderTest.php b/tests/Composer/Test/Autoload/ClassLoaderTest.php index 96ba28c0c789..5d385c4fe1e8 100644 --- a/tests/Composer/Test/Autoload/ClassLoaderTest.php +++ b/tests/Composer/Test/Autoload/ClassLoaderTest.php @@ -34,7 +34,7 @@ public function testLoadClass(string $class): void $loader->add('Pearlike_', __DIR__ . '/Fixtures'); $loader->addPsr4('ShinyVendor\\ShinyPackage\\', __DIR__ . '/Fixtures'); $loader->loadClass($class); - $this->assertTrue(class_exists($class, false), "->loadClass() loads '$class'"); + self::assertTrue(class_exists($class, false), "->loadClass() loads '$class'"); } /** @@ -57,7 +57,7 @@ public static function getLoadClassTests(): array public function testGetPrefixesWithNoPSR0Configuration(): void { $loader = new ClassLoader(); - $this->assertEmpty($loader->getPrefixes()); + self::assertEmpty($loader->getPrefixes()); } public function testSerializability(): void diff --git a/tests/Composer/Test/CacheTest.php b/tests/Composer/Test/CacheTest.php index 81a07b6d1368..e5eb0e2524d5 100644 --- a/tests/Composer/Test/CacheTest.php +++ b/tests/Composer/Test/CacheTest.php @@ -77,9 +77,9 @@ public function testRemoveOutdatedFiles(): void $this->cache->gc(600, 1024 * 1024 * 1024); for ($i = 1; $i < 4; $i++) { - $this->assertFileDoesNotExist("{$this->root}/cached.file{$i}.zip"); + self::assertFileDoesNotExist("{$this->root}/cached.file{$i}.zip"); } - $this->assertFileExists("{$this->root}/cached.file0.zip"); + self::assertFileExists("{$this->root}/cached.file0.zip"); } public function testRemoveFilesWhenCacheIsTooLarge(): void @@ -106,15 +106,15 @@ public function testRemoveFilesWhenCacheIsTooLarge(): void $this->cache->gc(600, 1500); for ($i = 0; $i < 3; $i++) { - $this->assertFileDoesNotExist("{$this->root}/cached.file{$i}.zip"); + self::assertFileDoesNotExist("{$this->root}/cached.file{$i}.zip"); } - $this->assertFileExists("{$this->root}/cached.file3.zip"); + self::assertFileExists("{$this->root}/cached.file3.zip"); } public function testClearCache(): void { $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $cache = new Cache($io, $this->root, 'a-z0-9.', $this->filesystem); - $this->assertTrue($cache->clear()); + self::assertTrue($cache->clear()); } } diff --git a/tests/Composer/Test/Command/AboutCommandTest.php b/tests/Composer/Test/Command/AboutCommandTest.php index 7701fbfd3dcf..ad560435dcd6 100644 --- a/tests/Composer/Test/Command/AboutCommandTest.php +++ b/tests/Composer/Test/Command/AboutCommandTest.php @@ -12,10 +12,10 @@ public function testAbout(): void { $composerVersion = Composer::getVersion(); $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'about'])); - $this->assertStringContainsString("Composer - Dependency Manager for PHP - version $composerVersion", $appTester->getDisplay()); + self::assertSame(0, $appTester->run(['command' => 'about'])); + self::assertStringContainsString("Composer - Dependency Manager for PHP - version $composerVersion", $appTester->getDisplay()); - $this->assertStringContainsString("Composer is a dependency manager tracking local dependencies of your projects and libraries.", $appTester->getDisplay()); - $this->assertStringContainsString("See https://getcomposer.org/ for more information.", $appTester->getDisplay()); + self::assertStringContainsString("Composer is a dependency manager tracking local dependencies of your projects and libraries.", $appTester->getDisplay()); + self::assertStringContainsString("See https://getcomposer.org/ for more information.", $appTester->getDisplay()); } } diff --git a/tests/Composer/Test/Command/ArchiveCommandTest.php b/tests/Composer/Test/Command/ArchiveCommandTest.php index 1ed93f5fdd81..be9e8df57f35 100644 --- a/tests/Composer/Test/Command/ArchiveCommandTest.php +++ b/tests/Composer/Test/Command/ArchiveCommandTest.php @@ -103,7 +103,7 @@ public function testUsesConfigFromFactoryWhenComposerIsNotDefined(): void null )->willReturn(0); - $this->assertEquals(0, $command->run($input, $output)); + self::assertEquals(0, $command->run($input, $output)); } public function testUsesConfigFromComposerObjectWithPackageName(): void diff --git a/tests/Composer/Test/Command/BaseDependencyCommandTest.php b/tests/Composer/Test/Command/BaseDependencyCommandTest.php index 6442b7f70285..0fad665aef7f 100644 --- a/tests/Composer/Test/Command/BaseDependencyCommandTest.php +++ b/tests/Composer/Test/Command/BaseDependencyCommandTest.php @@ -15,6 +15,7 @@ use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MatchAllConstraint; +use Composer\Semver\Constraint\MultiConstraint; use Symfony\Component\Console\Command\Command; use UnexpectedValueException; use InvalidArgumentException; @@ -45,7 +46,7 @@ public function testExceptionWhenNoRequiredParameters( $this->expectExceptionMessage($expectedExceptionMessage); $appTester = $this->getApplicationTester(); - $this->assertEquals(Command::FAILURE, $appTester->run(['command' => $command] + $parameters)); + self::assertEquals(Command::FAILURE, $appTester->run(['command' => $command] + $parameters)); } /** @@ -97,7 +98,7 @@ public function testExceptionWhenRunningLockedWithoutLockFile(string $command, a $this->expectExceptionMessage('A valid composer.lock file is required to run this command with --locked'); $appTester = $this->getApplicationTester(); - $this->assertEquals( + self::assertEquals( Command::FAILURE, $appTester->run(['command' => $command] + $parameters + ['--locked' => true] ) @@ -125,7 +126,7 @@ public function testExceptionWhenItCouldNotFoundThePackage(string $command, arra $this->expectExceptionMessage(sprintf('Could not find package "%s" in your project', $packageToBeInspected)); $appTester = $this->getApplicationTester(); - $this->assertEquals( + self::assertEquals( Command::FAILURE, $appTester->run(['command' => $command] + $parameters) ); @@ -164,7 +165,7 @@ public function testExceptionWhenPackageWasNotFoundInProject(string $command, ar $appTester = $this->getApplicationTester(); - $this->assertEquals(Command::FAILURE, $appTester->run(['command' => $command] + $parameters)); + self::assertEquals(Command::FAILURE, $appTester->run(['command' => $command] + $parameters)); } /** @@ -199,7 +200,7 @@ public function testWarningWhenDependenciesAreNotInstalled(string $command, arra $appTester = $this->getApplicationTester(); $appTester->run(['command' => $command] + $parameters); - $this->assertSame($expectedWarningMessage, trim($appTester->getDisplay(true))); + self::assertSame($expectedWarningMessage, trim($appTester->getDisplay(true))); } /** @@ -296,7 +297,7 @@ public function testWhyCommandOutputs(array $parameters, string $expectedOutput, self::assertSame($expectedStatusCode, $appTester->getStatusCode()); - $this->assertEquals(trim($expectedOutput), $this->trimLines($appTester->getDisplay(true))); + self::assertEquals(trim($expectedOutput), $this->trimLines($appTester->getDisplay(true))); } /** @@ -374,19 +375,25 @@ public function testWhyNotCommandOutputs(array $parameters, string $expectedOutp 'package' => [ ['name' => 'vendor1/package1', 'version' => '1.3.0'], ['name' => 'vendor2/package1', 'version' => '2.0.0'], - ['name' => 'vendor2/package2', 'version' => '1.0.0', 'require' => ['vendor2/package3' => '1.4.*']], + ['name' => 'vendor2/package2', 'version' => '1.0.0', 'require' => ['vendor2/package3' => '1.4.*', 'php' => '^8.2']], ['name' => 'vendor2/package3', 'version' => '1.4.0'], ['name' => 'vendor2/package3', 'version' => '1.5.0'] ], ], ], 'require' => [ - 'vendor1/package1' => '1.*' + 'vendor1/package1' => '1.*', + 'php' => '^8', ], 'require-dev' => [ 'vendor2/package1' => '2.*', 'vendor2/package2' => '^1' - ] + ], + 'config' => [ + 'platform' => [ + 'php' => '8.3.2', + ], + ], ]); $someRequiredPackage = self::getPackage('vendor1/package1', '1.3.0'); @@ -399,7 +406,14 @@ public function testWhyNotCommandOutputs(array $parameters, string $expectedOutp new MatchAllConstraint(), Link::TYPE_REQUIRE, '1.4.*' - ) + ), + 'php' => new Link( + 'vendor2/package2', + 'php', + new MultiConstraint([self::getVersionConstraint('>=', '8.2.0.0'), self::getVersionConstraint('<', '9.0.0.0-dev')]), + Link::TYPE_REQUIRE, + '^8.2' + ), ]); $secondDevNestedRequiredPackage = self::getPackage('vendor2/package3', '1.4.0'); @@ -420,7 +434,7 @@ public function testWhyNotCommandOutputs(array $parameters, string $expectedOutp ]); self::assertSame($expectedStatusCode, $appTester->getStatusCode()); - $this->assertSame(trim($expectedOutput), $this->trimLines($appTester->getDisplay(true))); + self::assertSame(trim($expectedOutput), $this->trimLines($appTester->getDisplay(true))); } /** @@ -466,6 +480,26 @@ public function caseWhyNotProvider(): Generator vendor2/package2 1.0.0 requires vendor2/package3 (1.4.*) Not finding what you were looking for? Try calling `composer update "vendor2/package3:1.5.0" --dry-run` to get another view on the problem. OUTPUT +, + 1 + ]; + + yield 'all compatible with the inspected platform package (range matching installed)' => [ + ['package' => 'php', 'version' => '^8'], + << [ + ['package' => 'php', 'version' => '9.1.0'], + <<getApplicationTester(); - $this->assertSame($exitCode, $appTester->run(array_merge(['command' => 'bump'], $command))); + self::assertSame($exitCode, $appTester->run(array_merge(['command' => 'bump'], $command))); $json = new JsonFile('./composer.json'); - $this->assertSame($expected, $json->read()); + self::assertSame($expected, $json->read()); } public function testBumpFailsOnNonExistingComposerFile(): void @@ -54,9 +54,9 @@ public function testBumpFailsOnNonExistingComposerFile(): void unlink($composerJsonPath); $appTester = $this->getApplicationTester(); - $this->assertSame(1, $appTester->run(['command' => 'bump'], ['capture_stderr_separately' => true])); + self::assertSame(1, $appTester->run(['command' => 'bump'], ['capture_stderr_separately' => true])); - $this->assertStringContainsString("./composer.json is not readable.", $appTester->getErrorOutput()); + self::assertStringContainsString("./composer.json is not readable.", $appTester->getErrorOutput()); } public function testBumpFailsOnWriteErrorToComposerFile(): void @@ -66,9 +66,9 @@ public function testBumpFailsOnWriteErrorToComposerFile(): void chmod($composerJsonPath, 0444); $appTester = $this->getApplicationTester(); - $this->assertSame(1, $appTester->run(['command' => 'bump'], ['capture_stderr_separately' => true])); + self::assertSame(1, $appTester->run(['command' => 'bump'], ['capture_stderr_separately' => true])); - $this->assertStringContainsString("./composer.json is not writable.", $appTester->getErrorOutput()); + self::assertStringContainsString("./composer.json is not writable.", $appTester->getErrorOutput()); } public static function provideTests(): \Generator diff --git a/tests/Composer/Test/Command/CheckPlatformReqsCommandTest.php b/tests/Composer/Test/Command/CheckPlatformReqsCommandTest.php index 9f9d56df6713..27d852697ca0 100644 --- a/tests/Composer/Test/Command/CheckPlatformReqsCommandTest.php +++ b/tests/Composer/Test/Command/CheckPlatformReqsCommandTest.php @@ -47,7 +47,7 @@ public function testPlatformReqsAreSatisfied( $appTester->run(array_merge(['command' => 'check-platform-reqs'], $command)); $appTester->assertCommandIsSuccessful(); - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } public function testExceptionThrownIfNoLockfileFound(): void @@ -89,7 +89,7 @@ public static function caseProvider(): \Generator ]; } - public function testFailedPlatformRequirement(): void + public function testFailedPlatformRequirement(): void { $this->initTempComposer([ 'require' => [ @@ -137,6 +137,6 @@ public function testFailedPlatformRequirement(): void } ]'; - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } } diff --git a/tests/Composer/Test/Command/ClearCacheCommandTest.php b/tests/Composer/Test/Command/ClearCacheCommandTest.php index 7a528ffaebe7..b76414423273 100644 --- a/tests/Composer/Test/Command/ClearCacheCommandTest.php +++ b/tests/Composer/Test/Command/ClearCacheCommandTest.php @@ -32,7 +32,7 @@ public function testClearCacheCommandSuccess(): void $output = $appTester->getDisplay(true); - $this->assertStringContainsString('All caches cleared.', $output); + self::assertStringContainsString('All caches cleared.', $output); } public function testClearCacheCommandWithOptionGarbageCollection(): void @@ -44,7 +44,7 @@ public function testClearCacheCommandWithOptionGarbageCollection(): void $output = $appTester->getDisplay(true); - $this->assertStringContainsString('All caches garbage-collected.', $output); + self::assertStringContainsString('All caches garbage-collected.', $output); } public function testClearCacheCommandWithOptionNoCache(): void @@ -56,6 +56,6 @@ public function testClearCacheCommandWithOptionNoCache(): void $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Cache is not enabled', $output); + self::assertStringContainsString('Cache is not enabled', $output); } } diff --git a/tests/Composer/Test/Command/ConfigCommandTest.php b/tests/Composer/Test/Command/ConfigCommandTest.php index 68653c622903..5542eb828c82 100644 --- a/tests/Composer/Test/Command/ConfigCommandTest.php +++ b/tests/Composer/Test/Command/ConfigCommandTest.php @@ -32,7 +32,7 @@ public function testConfigUpdates(array $before, array $command, array $expected $appTester->assertCommandIsSuccessful($appTester->getDisplay()); - $this->assertSame($expected, json_decode((string) file_get_contents('composer.json'), true)); + self::assertSame($expected, json_decode((string) file_get_contents('composer.json'), true)); } public static function provideConfigUpdates(): \Generator @@ -82,6 +82,41 @@ public static function provideConfigUpdates(): \Generator ['setting-key' => 'preferred-install.foo/*', '--unset' => true], ['config' => ['preferred-install' => []]], ]; + yield 'unset platform' => [ + ['config' => ['platform' => ['php' => '7.2.5'], 'platform-check' => false]], + ['setting-key' => 'platform.php', '--unset' => true], + ['config' => ['platform' => [], 'platform-check' => false]], + ]; + yield 'set extra with merge' => [ + [], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value']]]], + ]; + yield 'combine extra with merge' => [ + ['extra' => ['patches' => ['foo/bar' => [5 => 'oldvalue']]]], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value', 5 => 'oldvalue']]]], + ]; + yield 'combine extra with list' => [ + ['extra' => ['patches' => ['foo/bar' => ['oldvalue']]]], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value', 0 => 'oldvalue']]]], + ]; + yield 'overwrite extra with merge' => [ + ['extra' => ['patches' => ['foo/bar' => [123 => 'oldvalue']]]], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value']]]], + ]; + yield 'unset autoload' => [ + ['autoload' => ['psr-4' => ['test'], 'classmap' => ['test']]], + ['setting-key' => 'autoload.psr-4', '--unset' => true], + ['autoload' => ['classmap' => ['test']]], + ]; + yield 'unset autoload-dev' => [ + ['autoload-dev' => ['psr-4' => ['test'], 'classmap' => ['test']]], + ['setting-key' => 'autoload-dev.psr-4', '--unset' => true], + ['autoload-dev' => ['classmap' => ['test']]], + ]; } /** @@ -98,8 +133,8 @@ public function testConfigReads(array $composerJson, array $command, string $exp $appTester->assertCommandIsSuccessful(); - $this->assertSame($expected, trim($appTester->getDisplay(true))); - $this->assertSame($composerJson, json_decode((string) file_get_contents('composer.json'), true), 'The composer.json should not be modified by config reads'); + self::assertSame($expected, trim($appTester->getDisplay(true))); + self::assertSame($composerJson, json_decode((string) file_get_contents('composer.json'), true), 'The composer.json should not be modified by config reads'); } public static function provideConfigReads(): \Generator diff --git a/tests/Composer/Test/Command/DiagnoseCommandTest.php b/tests/Composer/Test/Command/DiagnoseCommandTest.php index 5ca6702655ad..ef241c9c9dd0 100644 --- a/tests/Composer/Test/Command/DiagnoseCommandTest.php +++ b/tests/Composer/Test/Command/DiagnoseCommandTest.php @@ -13,6 +13,7 @@ namespace Composer\Test\Command; use Composer\Test\TestCase; +use Composer\Util\Platform; class DiagnoseCommandTest extends TestCase { @@ -23,13 +24,17 @@ public function testCmdFail(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'diagnose']); - $this->assertSame(1, $appTester->getStatusCode()); + if (Platform::getEnv('COMPOSER_LOWEST_DEPS_TEST') === '1') { + self::assertGreaterThanOrEqual(1, $appTester->getStatusCode()); + } else { + self::assertSame(1, $appTester->getStatusCode()); + } $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Checking composer.json: WARNING + self::assertStringContainsString('Checking composer.json: WARNING No license specified, it is recommended to do so. For closed-source software you may use "proprietary" as license.', $output); - $this->assertStringContainsString('Checking http connectivity to packagist: OK + self::assertStringContainsString('Checking http connectivity to packagist: OK Checking https connectivity to packagist: OK Checking github.com rate limit: ', $output); } @@ -41,12 +46,14 @@ public function testCmdSuccess(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'diagnose']); - $appTester->assertCommandIsSuccessful(); + if (Platform::getEnv('COMPOSER_LOWEST_DEPS_TEST') !== '1') { + $appTester->assertCommandIsSuccessful(); + } $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Checking composer.json: OK', $output); + self::assertStringContainsString('Checking composer.json: OK', $output); - $this->assertStringContainsString('Checking http connectivity to packagist: OK + self::assertStringContainsString('Checking http connectivity to packagist: OK Checking https connectivity to packagist: OK Checking github.com rate limit: ', $output); } diff --git a/tests/Composer/Test/Command/DumpAutoloadCommandTest.php b/tests/Composer/Test/Command/DumpAutoloadCommandTest.php index 88d1287723a5..c8c46a0649b7 100644 --- a/tests/Composer/Test/Command/DumpAutoloadCommandTest.php +++ b/tests/Composer/Test/Command/DumpAutoloadCommandTest.php @@ -10,41 +10,41 @@ class DumpAutoloadCommandTest extends TestCase public function testDumpAutoload(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload'])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload'])); $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Generating autoload files', $output); - $this->assertStringContainsString('Generated autoload files', $output); + self::assertStringContainsString('Generating autoload files', $output); + self::assertStringContainsString('Generated autoload files', $output); } public function testDumpDevAutoload(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--dev' => true])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload', '--dev' => true])); $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Generating autoload files', $output); - $this->assertStringContainsString('Generated autoload files', $output); + self::assertStringContainsString('Generating autoload files', $output); + self::assertStringContainsString('Generated autoload files', $output); } public function testDumpNoDevAutoload(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--dev' => true])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload', '--dev' => true])); $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Generating autoload files', $output); - $this->assertStringContainsString('Generated autoload files', $output); + self::assertStringContainsString('Generating autoload files', $output); + self::assertStringContainsString('Generated autoload files', $output); } public function testUsingOptimizeAndStrictPsr(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--optimize' => true, '--strict-psr' => true])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload', '--optimize' => true, '--strict-psr' => true])); $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Generating optimized autoload files', $output); - $this->assertMatchesRegularExpression('/Generated optimized autoload files containing \d+ classes/', $output); + self::assertStringContainsString('Generating optimized autoload files', $output); + self::assertMatchesRegularExpression('/Generated optimized autoload files containing \d+ classes/', $output); } public function testFailsUsingStrictPsrIfClassMapViolationsAreFound(): void @@ -59,30 +59,30 @@ public function testFailsUsingStrictPsrIfClassMapViolationsAreFound(): void mkdir($dir . '/src/'); file_put_contents($dir . '/src/Foo.php', 'getApplicationTester(); - $this->assertSame(1, $appTester->run(['command' => 'dump-autoload', '--optimize' => true, '--strict-psr' => true])); + self::assertSame(1, $appTester->run(['command' => 'dump-autoload', '--optimize' => true, '--strict-psr' => true])); $output = $appTester->getDisplay(true); - $this->assertMatchesRegularExpression('/Class Application\\\Src\\\Foo located in .*? does not comply with psr-4 autoloading standard. Skipping./', $output); + self::assertMatchesRegularExpression('#Class Application\\\\Src\\\\Foo located in .*? does not comply with psr-4 autoloading standard \(rule: Application\\\\ => \./src\)\. Skipping\.#', $output); } public function testUsingClassmapAuthoritative(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--classmap-authoritative' => true])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload', '--classmap-authoritative' => true])); $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Generating optimized autoload files (authoritative)', $output); - $this->assertMatchesRegularExpression('/Generated optimized autoload files \(authoritative\) containing \d+ classes/', $output); + self::assertStringContainsString('Generating optimized autoload files (authoritative)', $output); + self::assertMatchesRegularExpression('/Generated optimized autoload files \(authoritative\) containing \d+ classes/', $output); } public function testUsingClassmapAuthoritativeAndStrictPsr(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload', '--classmap-authoritative' => true, '--strict-psr' => true])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload', '--classmap-authoritative' => true, '--strict-psr' => true])); $output = $appTester->getDisplay(true); - $this->assertStringContainsString('Generating optimized autoload files', $output); - $this->assertMatchesRegularExpression('/Generated optimized autoload files \(authoritative\) containing \d+ classes/', $output); + self::assertStringContainsString('Generating optimized autoload files', $output); + self::assertMatchesRegularExpression('/Generated optimized autoload files \(authoritative\) containing \d+ classes/', $output); } public function testStrictPsrDoesNotWorkWithoutOptimizedAutoloader(): void @@ -112,7 +112,7 @@ public function testWithCustomAutoloaderSuffix(): void ]); $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload'])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload'])); self::assertStringContainsString('ComposerAutoloaderInitFoobar', (string) file_get_contents($dir . '/vendor/autoload.php')); } @@ -147,7 +147,7 @@ public function testWithExistingComposerLockAndAutoloaderSuffix(): void ); $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload'])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload'])); self::assertStringContainsString('ComposerAutoloaderInitFoobar', (string) file_get_contents($dir . '/vendor/autoload.php')); } @@ -180,7 +180,7 @@ public function testWithExistingComposerLockWithoutAutoloaderSuffix(): void ); $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'dump-autoload'])); + self::assertSame(0, $appTester->run(['command' => 'dump-autoload'])); self::assertStringContainsString('ComposerAutoloaderInit2d4a6be9a93712c5d6a119b26734a047', (string) file_get_contents($dir . '/vendor/autoload.php')); } diff --git a/tests/Composer/Test/Command/ExecCommandTest.php b/tests/Composer/Test/Command/ExecCommandTest.php index ee195c4b5e3b..102122b64d0f 100644 --- a/tests/Composer/Test/Command/ExecCommandTest.php +++ b/tests/Composer/Test/Command/ExecCommandTest.php @@ -49,7 +49,7 @@ public function testList(): void $output = $appTester->getDisplay(true); - $this->assertSame( + self::assertSame( 'Available binaries: - b - c diff --git a/tests/Composer/Test/Command/FundCommandTest.php b/tests/Composer/Test/Command/FundCommandTest.php index b1563b8ad9f0..45e31fb14ea7 100644 --- a/tests/Composer/Test/Command/FundCommandTest.php +++ b/tests/Composer/Test/Command/FundCommandTest.php @@ -56,7 +56,7 @@ public function testFundCommand( $appTester->run(array_merge(['command' => 'fund'], $command)); $appTester->assertCommandIsSuccessful(); - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } public static function useCaseProvider(): Generator diff --git a/tests/Composer/Test/Command/GlobalCommandTest.php b/tests/Composer/Test/Command/GlobalCommandTest.php new file mode 100644 index 000000000000..1c3b32a36146 --- /dev/null +++ b/tests/Composer/Test/Command/GlobalCommandTest.php @@ -0,0 +1,80 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Command; + +use Composer\Test\TestCase; +use Composer\Util\Platform; + +class GlobalCommandTest extends TestCase +{ + public function tearDown(): void + { + parent::tearDown(); + Platform::clearEnv('COMPOSER_HOME'); + Platform::clearEnv('COMPOSER'); + } + + public function testGlobal(): void + { + $script = '@php -r \'echo getenv("COMPOSER") . PHP_EOL;\''; + $fakeComposer = 'TMP_COMPOSER.JSON'; + $composerHome = $this->initTempComposer( + [ + "scripts" => [ + "test-script" => $script, + ], + ] + ); + + Platform::putEnv('COMPOSER_HOME', $composerHome); + Platform::putEnv('COMPOSER', $fakeComposer); + + $dir = self::getUniqueTmpDirectory(); + chdir($dir); + + $appTester = $this->getApplicationTester(); + $appTester->run([ + 'command' => 'global', + 'command-name' => 'test-script', + '--no-interaction' => true, + ]); + + $display = $appTester->getDisplay(true); + + self::assertStringContainsString( + 'Changed current directory to ' . $composerHome, + $display + ); + self::assertStringContainsString($script, $display); + self::assertStringNotContainsString($fakeComposer, $display, '$COMPOSER is not unset by global command'); + } + + public function testCannotCreateHome(): void + { + $dir = self::getUniqueTmpDirectory(); + $filename = $dir . '/file'; + file_put_contents($filename, ''); + + Platform::putEnv('COMPOSER_HOME', $filename); + + self::expectException(\RuntimeException::class); + $this->expectExceptionMessage($filename . ' exists and is not a directory.'); + + $appTester = $this->getApplicationTester(); + $appTester->run([ + 'command' => 'global', + 'command-name' => 'test-script', + '--no-interaction' => true, + ]); + } +} diff --git a/tests/Composer/Test/Command/HomeCommandTest.php b/tests/Composer/Test/Command/HomeCommandTest.php index fd37e2f89fe3..d0b722ec20fd 100644 --- a/tests/Composer/Test/Command/HomeCommandTest.php +++ b/tests/Composer/Test/Command/HomeCommandTest.php @@ -54,7 +54,7 @@ public function testHomeCommandWithShowFlag( $appTester = $this->getApplicationTester(); $appTester->run(array_merge(['command' => 'home', '--show' => true], $command)); - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } public static function useCaseProvider(): Generator diff --git a/tests/Composer/Test/Command/InitCommandTest.php b/tests/Composer/Test/Command/InitCommandTest.php index 6030868ef4e4..3e50fbdb41eb 100644 --- a/tests/Composer/Test/Command/InitCommandTest.php +++ b/tests/Composer/Test/Command/InitCommandTest.php @@ -23,24 +23,24 @@ public function testParseValidAuthorString(): void { $command = new InitCommand; $author = $this->callParseAuthorString($command, 'John Smith '); - $this->assertEquals('John Smith', $author['name']); - $this->assertEquals('john@example.com', $author['email']); + self::assertEquals('John Smith', $author['name']); + self::assertEquals('john@example.com', $author['email']); } public function testParseValidAuthorStringWithoutEmail(): void { $command = new InitCommand; $author = $this->callParseAuthorString($command, 'John Smith'); - $this->assertEquals('John Smith', $author['name']); - $this->assertNull($author['email']); + self::assertEquals('John Smith', $author['name']); + self::assertNull($author['email']); } public function testParseValidUtf8AuthorString(): void { $command = new InitCommand; $author = $this->callParseAuthorString($command, 'Matti Meikäläinen '); - $this->assertEquals('Matti Meikäläinen', $author['name']); - $this->assertEquals('matti@example.com', $author['email']); + self::assertEquals('Matti Meikäläinen', $author['name']); + self::assertEquals('matti@example.com', $author['email']); } public function testParseValidUtf8AuthorStringWithNonSpacingMarks(): void @@ -49,16 +49,16 @@ public function testParseValidUtf8AuthorStringWithNonSpacingMarks(): void $utf8_expected = "Matti Meika\xCC\x88la\xCC\x88inen"; $command = new InitCommand; $author = $this->callParseAuthorString($command, $utf8_expected." "); - $this->assertEquals($utf8_expected, $author['name']); - $this->assertEquals('matti@example.com', $author['email']); + self::assertEquals($utf8_expected, $author['name']); + self::assertEquals('matti@example.com', $author['email']); } public function testParseNumericAuthorString(): void { $command = new InitCommand; $author = $this->callParseAuthorString($command, 'h4x0r '); - $this->assertEquals('h4x0r', $author['name']); - $this->assertEquals('h4x@example.com', $author['email']); + self::assertEquals('h4x0r', $author['name']); + self::assertEquals('h4x@example.com', $author['email']); } /** @@ -72,8 +72,8 @@ public function testParseValidAlias1AuthorString(): void $command, 'Johnathon "Johnny" Smith ' ); - $this->assertEquals('Johnathon "Johnny" Smith', $author['name']); - $this->assertEquals('john@example.com', $author['email']); + self::assertEquals('Johnathon "Johnny" Smith', $author['name']); + self::assertEquals('john@example.com', $author['email']); } /** @@ -87,8 +87,8 @@ public function testParseValidAlias2AuthorString(): void $command, 'Johnathon (Johnny) Smith ' ); - $this->assertEquals('Johnathon (Johnny) Smith', $author['name']); - $this->assertEquals('john@example.com', $author['email']); + self::assertEquals('Johnathon (Johnny) Smith', $author['name']); + self::assertEquals('john@example.com', $author['email']); } public function testParseEmptyAuthorString(): void @@ -109,21 +109,21 @@ public function testNamespaceFromValidPackageName(): void { $command = new InitCommand; $namespace = $command->namespaceFromPackageName('new_projects.acme-extra/package-name'); - $this->assertEquals('NewProjectsAcmeExtra\PackageName', $namespace); + self::assertEquals('NewProjectsAcmeExtra\PackageName', $namespace); } public function testNamespaceFromInvalidPackageName(): void { $command = new InitCommand; $namespace = $command->namespaceFromPackageName('invalid-package-name'); - $this->assertNull($namespace); + self::assertNull($namespace); } public function testNamespaceFromMissingPackageName(): void { $command = new InitCommand; $namespace = $command->namespaceFromPackageName(''); - $this->assertNull($namespace); + self::assertNull($namespace); } /** @@ -168,7 +168,7 @@ public function testRunNameArgument(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'init', '--no-interaction' => true, '--name' => 'test/pkg']); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -176,7 +176,7 @@ public function testRunNameArgument(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunInvalidAuthorArgumentInvalidEmail(): void @@ -210,7 +210,7 @@ public function testRunAuthorArgument(): void '--author' => 'Mr. Test ', ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -224,7 +224,7 @@ public function testRunAuthorArgument(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunAuthorArgumentMissingEmail(): void @@ -241,7 +241,7 @@ public function testRunAuthorArgumentMissingEmail(): void '--author' => 'Mr. Test', ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -254,7 +254,7 @@ public function testRunAuthorArgumentMissingEmail(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunSingleRepositoryArgument(): void @@ -273,7 +273,7 @@ public function testRunSingleRepositoryArgument(): void ], ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -287,7 +287,7 @@ public function testRunSingleRepositoryArgument(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunMultipleRepositoryArguments(): void @@ -308,7 +308,7 @@ public function testRunMultipleRepositoryArguments(): void ], ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -335,7 +335,7 @@ public function testRunMultipleRepositoryArguments(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunStability(): void @@ -352,7 +352,7 @@ public function testRunStability(): void '--stability' => 'dev', ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -361,7 +361,7 @@ public function testRunStability(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunInvalidStability(): void @@ -378,9 +378,9 @@ public function testRunInvalidStability(): void '--stability' => 'bogus', ], ['capture_stderr_separately' => true]); - $this->assertSame(1, $appTester->getStatusCode()); + self::assertSame(1, $appTester->getStatusCode()); - $this->assertMatchesRegularExpression("/minimum-stability\s+:\s+Does not have a value in the enumeration/", $appTester->getErrorOutput()); + self::assertMatchesRegularExpression("/minimum-stability\s+:\s+Does not have a value in the enumeration/", $appTester->getErrorOutput()); } public function testRunRequireOne(): void @@ -399,7 +399,7 @@ public function testRunRequireOne(): void ], ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -409,7 +409,7 @@ public function testRunRequireOne(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunRequireMultiple(): void @@ -429,7 +429,7 @@ public function testRunRequireMultiple(): void ], ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -440,7 +440,7 @@ public function testRunRequireMultiple(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunInvalidRequire(): void @@ -479,7 +479,7 @@ public function testRunRequireDevOne(): void ], ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -490,7 +490,7 @@ public function testRunRequireDevOne(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunRequireDevMultiple(): void @@ -510,7 +510,7 @@ public function testRunRequireDevMultiple(): void ], ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -522,7 +522,7 @@ public function testRunRequireDevMultiple(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunInvalidRequireDev(): void @@ -559,7 +559,7 @@ public function testRunAutoload(): void '--autoload' => 'testMapping/' ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -572,7 +572,7 @@ public function testRunAutoload(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunHomepage(): void @@ -589,7 +589,7 @@ public function testRunHomepage(): void '--homepage' => 'https://example.org/' ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -598,7 +598,7 @@ public function testRunHomepage(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunInvalidHomepage(): void @@ -615,8 +615,8 @@ public function testRunInvalidHomepage(): void '--homepage' => 'not-a-url', ], ['capture_stderr_separately' => true]); - $this->assertSame(1, $appTester->getStatusCode()); - $this->assertMatchesRegularExpression("/homepage\s*:\s*Invalid URL format/", $appTester->getErrorOutput()); + self::assertSame(1, $appTester->getStatusCode()); + self::assertMatchesRegularExpression("/homepage\s*:\s*Invalid URL format/", $appTester->getErrorOutput()); } public function testRunDescription(): void @@ -633,7 +633,7 @@ public function testRunDescription(): void '--description' => 'My first example package' ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -642,7 +642,7 @@ public function testRunDescription(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunType(): void @@ -659,7 +659,7 @@ public function testRunType(): void '--type' => 'library' ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -668,7 +668,7 @@ public function testRunType(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); } public function testRunLicense(): void @@ -685,7 +685,7 @@ public function testRunLicense(): void '--license' => 'MIT' ]); - $this->assertSame(0, $appTester->getStatusCode()); + self::assertSame(0, $appTester->getStatusCode()); $expected = [ 'name' => 'test/pkg', @@ -694,6 +694,45 @@ public function testRunLicense(): void ]; $file = new JsonFile($dir . '/composer.json'); - $this->assertEquals($expected, $file->read()); + self::assertEquals($expected, $file->read()); + } + + public function testInteractiveRun(): void + { + $dir = $this->initTempComposer(); + unlink($dir . '/composer.json'); + unlink($dir . '/auth.json'); + + $appTester = $this->getApplicationTester(); + + $appTester->setInputs([ + 'vendor/pkg', // Pkg name + 'my description', // Description + 'Mr. Test ', // Author + 'stable', // Minimum stability + 'library', // Type + 'AGPL-3.0-only', // License + 'no', // Define dependencies + 'no', // Define dev dependencies + 'n', // Add PSR-4 autoload mapping + '', // Confirm generation + ]); + + $appTester->run(['command' => 'init']); + + self::assertSame(0, $appTester->getStatusCode()); + + $expected = [ + 'name' => 'vendor/pkg', + 'description' => 'my description', + 'type' => 'library', + 'license' => 'AGPL-3.0-only', + 'authors' => [['name' => 'Mr. Test', 'email' => 'test@example.org']], + 'minimum-stability' => 'stable', + 'require' => [], + ]; + + $file = new JsonFile($dir . '/composer.json'); + self::assertEquals($expected, $file->read()); } } diff --git a/tests/Composer/Test/Command/InstallCommandTest.php b/tests/Composer/Test/Command/InstallCommandTest.php index 8787de8893a9..ebaf15cbe4c6 100644 --- a/tests/Composer/Test/Command/InstallCommandTest.php +++ b/tests/Composer/Test/Command/InstallCommandTest.php @@ -42,7 +42,7 @@ public function testInstallCommandErrors( $appTester = $this->getApplicationTester(); $appTester->run(array_merge(['command' => 'install'], $command)); - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } public function errorCaseProvider(): Generator @@ -118,7 +118,7 @@ public function testInstallFromEmptyVendor(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'install', '--no-progress' => true]); - $this->assertSame('Installing dependencies from lock file (including require-dev) + self::assertSame('Installing dependencies from lock file (including require-dev) Verifying lock file contents can be installed on current platform. Package operations: 2 installs, 0 updates, 0 removals - Installing root/another (1.0.0) @@ -148,7 +148,7 @@ public function testInstallFromEmptyVendorNoDev(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'install', '--no-progress' => true, '--no-dev' => true]); - $this->assertSame('Installing dependencies from lock file + self::assertSame('Installing dependencies from lock file Verifying lock file contents can be installed on current platform. Package operations: 1 install, 0 updates, 0 removals - Installing root/req (1.0.0) @@ -175,7 +175,7 @@ public function testInstallNewPackagesWithExistingPartialVendor(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'install', '--no-progress' => true]); - $this->assertSame('Installing dependencies from lock file (including require-dev) + self::assertSame('Installing dependencies from lock file (including require-dev) Verifying lock file contents can be installed on current platform. Package operations: 1 install, 0 updates, 0 removals - Installing root/another (1.0.0) diff --git a/tests/Composer/Test/Command/LicensesCommandTest.php b/tests/Composer/Test/Command/LicensesCommandTest.php index b48e625f3b88..b7c86bd04263 100644 --- a/tests/Composer/Test/Command/LicensesCommandTest.php +++ b/tests/Composer/Test/Command/LicensesCommandTest.php @@ -54,7 +54,7 @@ protected function setUp(): void public function testBasicRun(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'license'])); + self::assertSame(0, $appTester->run(['command' => 'license'])); $expected = [ ["Name:", "test/pkg"], @@ -78,14 +78,17 @@ public function testBasicRun(): void continue; } - $this->assertMatchesRegularExpression("/" . implode("\s+", $expected[$i]) . "/", $line); + if (!isset($expected[$i])) { + $this->fail('Got more output lines than expected'); + } + self::assertMatchesRegularExpression("/" . implode("\s+", $expected[$i]) . "/", $line); } } public function testNoDev(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'license', '--no-dev' => true])); + self::assertSame(0, $appTester->run(['command' => 'license', '--no-dev' => true])); $expected = [ ["Name:", "test/pkg"], @@ -108,14 +111,17 @@ public function testNoDev(): void continue; } - $this->assertMatchesRegularExpression("/" . implode("\s+", $expected[$i]) . "/", $line); + if (!isset($expected[$i])) { + $this->fail('Got more output lines than expected'); + } + self::assertMatchesRegularExpression("/" . implode("\s+", $expected[$i]) . "/", $line); } } public function testFormatJson(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'license', '--format' => 'json'], ['capture_stderr_separately' => true])); + self::assertSame(0, $appTester->run(['command' => 'license', '--format' => 'json'], ['capture_stderr_separately' => true])); $expected = [ "name" => "test/pkg", @@ -147,13 +153,13 @@ public function testFormatJson(): void ] ]; - $this->assertSame($expected, json_decode($appTester->getDisplay(), true)); + self::assertSame($expected, json_decode($appTester->getDisplay(), true)); } public function testFormatSummary(): void { $appTester = $this->getApplicationTester(); - $this->assertSame(0, $appTester->run(['command' => 'license', '--format' => 'summary'])); + self::assertSame(0, $appTester->run(['command' => 'license', '--format' => 'summary'])); $expected = [ ['-', '-'], @@ -170,7 +176,7 @@ public function testFormatSummary(): void foreach ($expected as $i => $expect) { [$key, $value] = $expect; - $this->assertMatchesRegularExpression("/$key\s+$value/", $lines[$i]); + self::assertMatchesRegularExpression("/$key\s+$value/", $lines[$i]); } } diff --git a/tests/Composer/Test/Command/ReinstallCommandTest.php b/tests/Composer/Test/Command/ReinstallCommandTest.php index c25c00202a8a..cc21ce54590e 100644 --- a/tests/Composer/Test/Command/ReinstallCommandTest.php +++ b/tests/Composer/Test/Command/ReinstallCommandTest.php @@ -19,49 +19,70 @@ class ReinstallCommandTest extends TestCase { /** * @dataProvider caseProvider - * @param array $packages + * @param array $options * @param string $expected */ - public function testReinstallCommand(array $packages, string $expected): void + public function testReinstallCommand(array $options, string $expected): void { $this->initTempComposer([ 'require' => [ 'root/req' => '1.*', - 'root/anotherreq' => '2.*' + ], + 'require-dev' => [ + 'root/anotherreq' => '2.*', + 'root/anotherreq2' => '2.*', + 'root/lala' => '2.*', ] ]); $rootReqPackage = self::getPackage('root/req'); $anotherReqPackage = self::getPackage('root/anotherreq'); + $anotherReqPackage2 = self::getPackage('root/anotherreq2'); + $anotherReqPackage3 = self::getPackage('root/lala'); $rootReqPackage->setType('metapackage'); $anotherReqPackage->setType('metapackage'); + $anotherReqPackage2->setType('metapackage'); + $anotherReqPackage3->setType('metapackage'); - $this->createComposerLock([$rootReqPackage], [$anotherReqPackage]); - $this->createInstalledJson([$rootReqPackage], [$anotherReqPackage]); + $this->createComposerLock([$rootReqPackage], [$anotherReqPackage, $anotherReqPackage2, $anotherReqPackage3]); + $this->createInstalledJson([$rootReqPackage], [$anotherReqPackage, $anotherReqPackage2, $anotherReqPackage3]); $appTester = $this->getApplicationTester(); - $appTester->run([ + $appTester->run(array_merge([ 'command' => 'reinstall', '--no-progress' => true, '--no-plugins' => true, - 'packages' => $packages - ]); + ], $options)); - $this->assertSame($expected, trim($appTester->getDisplay(true))); + self::assertSame($expected, trim($appTester->getDisplay(true))); } - public function caseProvider(): Generator + public function caseProvider(): Generator { - yield 'reinstall a package' => [ - ['root/req', 'root/anotherreq'], + yield 'reinstall a package by name' => [ + ['packages' => ['root/req', 'root/anotherreq*']], +'- Removing root/req (1.0.0) + - Removing root/anotherreq2 (1.0.0) + - Removing root/anotherreq (1.0.0) + - Installing root/anotherreq (1.0.0) + - Installing root/anotherreq2 (1.0.0) + - Installing root/req (1.0.0)' + ]; + + yield 'reinstall packages by type' => [ + ['--type' => ['metapackage']], '- Removing root/req (1.0.0) + - Removing root/lala (1.0.0) + - Removing root/anotherreq2 (1.0.0) - Removing root/anotherreq (1.0.0) - Installing root/anotherreq (1.0.0) + - Installing root/anotherreq2 (1.0.0) + - Installing root/lala (1.0.0) - Installing root/req (1.0.0)' ]; yield 'reinstall a package that is not installed' => [ - ['root/unknownreq'], + ['packages' => ['root/unknownreq']], 'Pattern "root/unknownreq" does not match any currently installed packages. Found no packages to reinstall, aborting.' ]; diff --git a/tests/Composer/Test/Command/RemoveCommandTest.php b/tests/Composer/Test/Command/RemoveCommandTest.php index f34b0c7618d2..f8719932346a 100644 --- a/tests/Composer/Test/Command/RemoveCommandTest.php +++ b/tests/Composer/Test/Command/RemoveCommandTest.php @@ -28,7 +28,7 @@ public function testExceptionRunningWithNoRemovePackages(): void $this->expectExceptionMessage('Not enough arguments (missing: "packages").'); $appTester = $this->getApplicationTester(); - $this->assertEquals(Command::FAILURE, $appTester->run(['command' => 'remove'])); + self::assertEquals(Command::FAILURE, $appTester->run(['command' => 'remove'])); } public function testExceptionWhenRunningUnusedWithoutLockFile(): void @@ -39,7 +39,7 @@ public function testExceptionWhenRunningUnusedWithoutLockFile(): void $this->expectExceptionMessage('A valid composer.lock file is required to run this command with --unused'); $appTester = $this->getApplicationTester(); - $this->assertEquals(Command::FAILURE, $appTester->run(['command' => 'remove', '--unused' => true])); + self::assertEquals(Command::FAILURE, $appTester->run(['command' => 'remove', '--unused' => true])); } public function testWarningWhenRemovingNonExistentPackage(): void @@ -64,7 +64,7 @@ public function testWarningWhenRemovingPackageFromWrongType(): void self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--dev' => true, '--no-update' => true, '--no-interaction' => true])); self::assertSame('root/req could not be found in require-dev but it is present in require ./composer.json has been updated', trim($appTester->getDisplay(true))); - $this->assertEquals(['require' => ['root/req' => '1.*']], (new JsonFile('./composer.json'))->read()); + self::assertEquals(['require' => ['root/req' => '1.*']], (new JsonFile('./composer.json'))->read()); } public function testWarningWhenRemovingPackageWithDeprecatedDependenciesFlag(): void @@ -79,7 +79,7 @@ public function testWarningWhenRemovingPackageWithDeprecatedDependenciesFlag(): self::assertEquals(Command::SUCCESS, $appTester->run(['command' => 'remove', 'packages' => ['root/req'], '--update-with-dependencies' => true, '--no-update' => true, '--no-interaction' => true])); self::assertSame('You are using the deprecated option "update-with-dependencies". This is now default behaviour. The --no-update-with-dependencies option can be used to remove a package without its dependencies. ./composer.json has been updated', trim($appTester->getDisplay(true))); - $this->assertEmpty((new JsonFile('./composer.json'))->read()); + self::assertEmpty((new JsonFile('./composer.json'))->read()); } public function testMessageOutputWhenNoUnusedPackagesToRemove(): void diff --git a/tests/Composer/Test/Command/RequireCommandTest.php b/tests/Composer/Test/Command/RequireCommandTest.php index c819a35296ed..d7cc957757b4 100644 --- a/tests/Composer/Test/Command/RequireCommandTest.php +++ b/tests/Composer/Test/Command/RequireCommandTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Command; +use Composer\Json\JsonFile; use Composer\Test\TestCase; use InvalidArgumentException; @@ -96,9 +97,9 @@ public function testRequire(array $composerJson, array $command, string $expecte if (str_contains($expected, '%d')) { $pattern = '{^'.str_replace('%d', '[0-9.]+', preg_quote(trim($expected))).'$}'; - $this->assertMatchesRegularExpression($pattern, trim($appTester->getDisplay(true))); + self::assertMatchesRegularExpression($pattern, trim($appTester->getDisplay(true))); } else { - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } } @@ -263,4 +264,91 @@ public static function provideRequire(): \Generator OUTPUT ]; } + + /** + * @dataProvider provideInconsistentRequireKeys + * @param bool $isDev + * @param bool $isInteractive + * @param string $expectedWarning + */ + public function testInconsistentRequireKeys(bool $isDev, bool $isInteractive, string $expectedWarning): void + { + $currentKey = $isDev ? "require" : "require-dev"; + $otherKey = $isDev ? "require-dev" : "require"; + + $dir = $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'required/pkg', 'version' => '1.0.0'], + ], + ], + ], + $currentKey => [ + "required/pkg" => "^1.0", + ], + ]); + + $package = self::getPackage('required/pkg'); + if ($isDev) { + $this->createComposerLock([], [$package]); + $this->createInstalledJson([], [$package]); + } else { + $this->createComposerLock([$package], []); + $this->createInstalledJson([$package], []); + } + + $appTester = $this->getApplicationTester(); + $command = [ + 'command' => 'require', + '--no-audit' => true, + '--dev' => $isDev, + '--no-install' => true, + 'packages' => ['required/pkg'] + ]; + + if ($isInteractive) + $appTester->setInputs(['yes']); + else + $command['--no-interaction'] = true; + + $appTester->run($command); + + self::assertStringContainsString( + $expectedWarning, + $appTester->getDisplay(true) + ); + + $composer_content = (new JsonFile($dir . '/composer.json'))->read(); + self::assertArrayHasKey($otherKey, $composer_content); + self::assertArrayNotHasKey($currentKey, $composer_content); + } + + public function provideInconsistentRequireKeys(): \Generator + { + yield [ + true, + false, + 'required/pkg is currently present in the require key and you ran the command with the --dev flag, which will move it to the require-dev key.' + ]; + + yield [ + false, + false, + 'required/pkg is currently present in the require-dev key and you ran the command without the --dev flag, which will move it to the require key.' + ]; + + yield [ + true, + true, + 'required/pkg is currently present in the require key and you ran the command with the --dev flag, which will move it to the require-dev key.' + ]; + + yield [ + false, + true, + 'required/pkg is currently present in the require-dev key and you ran the command without the --dev flag, which will move it to the require key.' + ]; + } } diff --git a/tests/Composer/Test/Command/RunScriptCommandTest.php b/tests/Composer/Test/Command/RunScriptCommandTest.php index dc724ae8d296..f69791048355 100644 --- a/tests/Composer/Test/Command/RunScriptCommandTest.php +++ b/tests/Composer/Test/Command/RunScriptCommandTest.php @@ -105,8 +105,8 @@ public function testCanListScripts(): void $output = $appTester->getDisplay(); - $this->assertStringContainsString('Runs the test script as defined in composer.json', $output, 'The default description for the test script should be printed'); - $this->assertStringContainsString('Run the codestyle fixer', $output, 'The custom description for the fix-cs script should be printed'); + self::assertStringContainsString('Runs the test script as defined in composer.json', $output, 'The default description for the test script should be printed'); + self::assertStringContainsString('Run the codestyle fixer', $output, 'The custom description for the fix-cs script should be printed'); } public function testCanDefineAliases(): void @@ -132,7 +132,7 @@ public function testCanDefineAliases(): void $actualAliases = $array['usage']; array_shift($actualAliases); - $this->assertSame($expectedAliases, $actualAliases, 'The custom aliases for the test command should be printed'); + self::assertSame($expectedAliases, $actualAliases, 'The custom aliases for the test command should be printed'); } public function testExecutionOfCustomSymfonyCommand(): void diff --git a/tests/Composer/Test/Command/SelfUpdateCommandTest.php b/tests/Composer/Test/Command/SelfUpdateCommandTest.php index d0b855563fb5..d919dc9d6e96 100644 --- a/tests/Composer/Test/Command/SelfUpdateCommandTest.php +++ b/tests/Composer/Test/Command/SelfUpdateCommandTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Command; +use Composer\Composer; use Composer\Test\TestCase; /** @@ -44,11 +45,15 @@ public function tearDown(): void public function testSuccessfulUpdate(): void { + if (Composer::VERSION !== '@package_version'.'@') { + $this->markTestSkipped('On releases this test can fail to upgrade as we are already on latest version'); + } + $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'self-update']); $appTester->assertCommandIsSuccessful(); - $this->assertStringContainsString('Upgrading to version', $appTester->getDisplay()); + self::assertStringContainsString('Upgrading to version', $appTester->getDisplay()); } public function testUpdateToSpecificVersion(): void @@ -57,7 +62,7 @@ public function testUpdateToSpecificVersion(): void $appTester->run(['command' => 'self-update', 'version' => '2.4.0']); $appTester->assertCommandIsSuccessful(); - $this->assertStringContainsString('Upgrading to version 2.4.0', $appTester->getDisplay()); + self::assertStringContainsString('Upgrading to version 2.4.0', $appTester->getDisplay()); } public function testUpdateWithInvalidOptionThrowsException(): void @@ -74,12 +79,16 @@ public function testUpdateWithInvalidOptionThrowsException(): void */ public function testUpdateToDifferentChannel(string $option, string $expectedOutput): void { + if (Composer::VERSION !== '@package_version'.'@' && in_array($option, ['--stable', '--preview'], true)) { + $this->markTestSkipped('On releases this test can fail to upgrade as we are already on latest version'); + } + $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'self-update', $option => true]); $appTester->assertCommandIsSuccessful(); - $this->assertStringContainsString('Upgrading to version', $appTester->getDisplay()); - $this->assertStringContainsString($expectedOutput, $appTester->getDisplay()); + self::assertStringContainsString('Upgrading to version', $appTester->getDisplay()); + self::assertStringContainsString($expectedOutput, $appTester->getDisplay()); } /** diff --git a/tests/Composer/Test/Command/ShowCommandTest.php b/tests/Composer/Test/Command/ShowCommandTest.php index 331ae0017bc3..f807d188e8b6 100644 --- a/tests/Composer/Test/Command/ShowCommandTest.php +++ b/tests/Composer/Test/Command/ShowCommandTest.php @@ -36,13 +36,13 @@ public function testShow(array $command, string $expected, array $requires = []) 'packages' => [ 'type' => 'package', 'package' => [ - ['name' => 'vendor/package', 'description' => 'generic description', 'version' => '1.0.0'], + ['name' => 'vendor/package', 'description' => 'generic description', 'version' => 'v1.0.0'], - ['name' => 'outdated/major', 'description' => 'outdated/major v1.0.0 description', 'version' => '1.0.0'], - ['name' => 'outdated/major', 'description' => 'outdated/major v1.0.1 description', 'version' => '1.0.1'], - ['name' => 'outdated/major', 'description' => 'outdated/major v1.1.0 description', 'version' => '1.1.0'], - ['name' => 'outdated/major', 'description' => 'outdated/major v1.1.1 description', 'version' => '1.1.1'], - ['name' => 'outdated/major', 'description' => 'outdated/major v2.0.0 description', 'version' => '2.0.0'], + ['name' => 'outdated/major', 'description' => 'outdated/major v1.0.0 description', 'version' => 'v1.0.0'], + ['name' => 'outdated/major', 'description' => 'outdated/major v1.0.1 description', 'version' => 'v1.0.1'], + ['name' => 'outdated/major', 'description' => 'outdated/major v1.1.0 description', 'version' => 'v1.1.0'], + ['name' => 'outdated/major', 'description' => 'outdated/major v1.1.1 description', 'version' => 'v1.1.1'], + ['name' => 'outdated/major', 'description' => 'outdated/major v2.0.0 description', 'version' => 'v2.0.0'], ['name' => 'outdated/minor', 'description' => 'outdated/minor v1.0.0 description', 'version' => '1.0.0'], ['name' => 'outdated/minor', 'description' => 'outdated/minor v1.0.1 description', 'version' => '1.0.1'], @@ -57,9 +57,9 @@ public function testShow(array $command, string $expected, array $requires = []) 'require' => $requires === [] ? new \stdClass : $requires, ]); - $pkg = self::getPackage('vendor/package', '1.0.0'); + $pkg = self::getPackage('vendor/package', 'v1.0.0'); $pkg->setDescription('description of installed package'); - $major = self::getPackage('outdated/major', '1.0.0'); + $major = self::getPackage('outdated/major', 'v1.0.0'); $major->setReleaseDate(new DateTimeImmutable()); $minor = self::getPackage('outdated/minor', '1.0.0'); $minor->setReleaseDate(new DateTimeImmutable('-2 years')); @@ -299,23 +299,57 @@ public function testOutdatedFiltersAccordingToPlatformReqsWithoutWarningForHighe vendor/package 1.1.0 ! 1.2.0", trim($appTester->getDisplay(true))); } - public function testShowDirectWithNameOnlyShowsDirectDependents(): void + public function testShowDirectWithNameDoesNotShowTransientDependencies(): void { self::expectException(InvalidArgumentException::class); self::expectExceptionMessage('Package "vendor/package" is installed but not a direct dependent of the root package.'); $this->initTempComposer([ 'repositories' => [], + 'require' => [ + 'direct/dependent' => '*', + ], ]); $this->createInstalledJson([ + $direct = self::getPackage('direct/dependent', '1.0.0'), self::getPackage('vendor/package', '1.0.0'), ]); + self::configureLinks($direct, ['require' => ['vendor/package' => '*']]); + $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'show', '--direct' => true, 'package' => 'vendor/package']); } + public function testShowDirectWithNameOnlyShowsDirectDependents(): void + { + $this->initTempComposer([ + 'repositories' => [], + 'require' => [ + 'direct/dependent' => '*', + ], + 'require-dev' => [ + 'direct/dependent2' => '*', + ], + ]); + + $this->createInstalledJson([ + self::getPackage('direct/dependent', '1.0.0'), + self::getPackage('direct/dependent2', '1.0.0'), + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'show', '--direct' => true, 'package' => 'direct/dependent']); + $appTester->assertCommandIsSuccessful(); + self::assertStringContainsString('name : direct/dependent' . "\n", $appTester->getDisplay(true)); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'show', '--direct' => true, 'package' => 'direct/dependent2']); + $appTester->assertCommandIsSuccessful(); + self::assertStringContainsString('name : direct/dependent2' . "\n", $appTester->getDisplay(true)); + } + public function testShowPlatformOnlyShowsPlatformPackages(): void { $this->initTempComposer([ diff --git a/tests/Composer/Test/Command/StatusCommandTest.php b/tests/Composer/Test/Command/StatusCommandTest.php index fce072b029ad..17d65c3c810e 100644 --- a/tests/Composer/Test/Command/StatusCommandTest.php +++ b/tests/Composer/Test/Command/StatusCommandTest.php @@ -30,7 +30,7 @@ public function testNoLocalChanges(): void $appTester = $this->getApplicationTester(); $appTester->run(['command' => 'status']); - $this->assertSame('No local changes', trim($appTester->getDisplay(true))); + self::assertSame('No local changes', trim($appTester->getDisplay(true))); } /** @@ -73,8 +73,8 @@ public function testLocallyModifiedPackages( $expected = 'You have changes in the following dependencies:'; $actual = trim($appTester->getDisplay(true)); - $this->assertStringContainsString($expected, $actual); - $this->assertStringContainsString($packageData['name'], $actual); + self::assertStringContainsString($expected, $actual); + self::assertStringContainsString($packageData['name'], $actual); } public static function locallyModifiedPackagesUseCaseProvider(): Generator diff --git a/tests/Composer/Test/Command/UpdateCommandTest.php b/tests/Composer/Test/Command/UpdateCommandTest.php index fce735087872..0a5938805944 100644 --- a/tests/Composer/Test/Command/UpdateCommandTest.php +++ b/tests/Composer/Test/Command/UpdateCommandTest.php @@ -12,6 +12,8 @@ namespace Composer\Test\Command; +use Composer\Package\Link; +use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Test\TestCase; use InvalidArgumentException; @@ -22,14 +24,18 @@ class UpdateCommandTest extends TestCase * @param array $composerJson * @param array $command */ - public function testUpdate(array $composerJson, array $command, string $expected): void + public function testUpdate(array $composerJson, array $command, string $expected, bool $createLock = false): void { $this->initTempComposer($composerJson); + if ($createLock) { + $this->createComposerLock(); + } + $appTester = $this->getApplicationTester(); $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true], $command)); - $this->assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); + self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); } public static function provideUpdates(): \Generator @@ -122,17 +128,265 @@ public static function provideUpdates(): \Generator << [ + $rootDepAndTransitiveDep, + ['--bump-after-update' => true], + <<Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users. +If your package is not a library, you can explicitly specify the "type" by using "composer config type project". +Alternatively you can use --dev-only to only bump dependencies within "require-dev". +No requirements to update in ./composer.json. +OUTPUT + , true + ]; + + yield 'update & bump dev only' => [ + $rootDepAndTransitiveDep, + ['--bump-after-update' => 'dev'], + << [ + $rootDepAndTransitiveDep, + ['--with' => ['dep/pkg:^2'], '--bump-after-update' => true], + << satisfiable by root/req[1.0.0]. + - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2). OUTPUT ]; } + public function testUpdateWithPatchOnly(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0'], + ['name' => 'root/req', 'version' => '1.0.1'], + ['name' => 'root/req', 'version' => '1.1.0'], + ['name' => 'root/req2', 'version' => '1.0.0'], + ['name' => 'root/req2', 'version' => '1.0.1'], + ['name' => 'root/req2', 'version' => '1.1.0'], + ['name' => 'root/req3', 'version' => '1.0.0'], + ['name' => 'root/req3', 'version' => '1.0.1'], + ['name' => 'root/req3', 'version' => '1.1.0'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + 'root/req2' => '1.*', + 'root/req3' => '1.*', + ], + ]); + + $package = self::getPackage('root/req', '1.0.0'); + $package2 = self::getPackage('root/req2', '1.0.0'); + $package3 = self::getPackage('root/req3', '1.0.0'); + $this->createComposerLock([$package, $package2, $package3]); + + $appTester = $this->getApplicationTester(); + // root/req fails because of incompatible --with requirement + $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.1']])); + + $expected = <<= 1.1.0.0-dev < 2.0.0.0-dev] [>= 1.0.0.0-dev < 1.1.0.0-dev]]). +OUTPUT; + + self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); + + $appTester = $this->getApplicationTester(); + // root/req upgrades to 1.0.1 as that is compatible with the --with requirement now + // root/req2 upgrades to 1.0.1 only due to --patch-only + // root/req3 does not update as it is not in the allowlist + $appTester->run(array_merge(['command' => 'update', '--dry-run' => true, '--no-audit' => true, '--no-install' => true, '--patch-only' => true, '--with' => ['root/req:^1.0.1'], 'packages' => ['root/req', 'root/req2']])); + + $expected = << 1.0.1) + - Upgrading root/req2 (1.0.0 => 1.0.1) +OUTPUT; + + self::assertStringMatchesFormat(trim($expected), trim($appTester->getDisplay(true))); + } + + public function testInteractiveModeThrowsIfNoPackageToUpdate(): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + ], + ]); + $this->createComposerLock([self::getPackage('root/req', '1.0.0')]); + self::expectExceptionMessage('Could not find any package with new versions available'); + + $appTester = $this->getApplicationTester(); + $appTester->setInputs(['']); + $appTester->run(['command' => 'update', '--interactive' => true]); + } + public function testInteractiveModeThrowsIfNoPackageEntered(): void { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('You must enter minimum one package.'); + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0'], + ['name' => 'root/req', 'version' => '1.0.1'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + ], + ]); + $this->createComposerLock([self::getPackage('root/req', '1.0.0')]); + self::expectExceptionMessage('No package named "" is installed.'); $appTester = $this->getApplicationTester(); $appTester->setInputs(['']); $appTester->run(['command' => 'update', '--interactive' => true]); } + + /** + * @dataProvider provideInteractiveUpdates + * @param array $packageNames + */ + public function testInteractiveTmp(array $packageNames, string $expected): void + { + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'root/req', 'version' => '1.0.0', 'require' => ['dep/pkg' => '^1']], + ['name' => 'dep/pkg', 'version' => '1.0.0'], + ['name' => 'dep/pkg', 'version' => '1.0.1'], + ['name' => 'dep/pkg', 'version' => '1.0.2'], + ['name' => 'another-dep/pkg', 'version' => '1.0.2'], + ], + ], + ], + 'require' => [ + 'root/req' => '1.*', + ], + ]); + + $rootPackage = self::getPackage('root/req'); + $packages = [$rootPackage]; + + foreach ($packageNames as $pkg => $ver) { + $currentPkg = self::getPackage($pkg, $ver); + array_push($packages, $currentPkg); + } + + $rootPackage->setRequires([ + 'dep/pkg' => new Link( + 'root/req', + 'dep/pkg', + new MatchAllConstraint(), + Link::TYPE_REQUIRE, + '^1' + ), + 'another-dep/pkg' => new Link( + 'root/req', + 'another-dep/pkg', + new MatchAllConstraint(), + Link::TYPE_REQUIRE, + '^1' + ), + ]); + + $this->createComposerLock($packages); + $this->createInstalledJson($packages); + + $appTester = $this->getApplicationTester(); + $appTester->setInputs(array_merge(array_keys($packageNames), ['', 'yes'])); + $appTester->run([ + 'command' => 'update', '--interactive' => true, + '--no-audit' => true, + '--dry-run' => true, + ]); + + self::assertStringEndsWith( + trim($expected), + trim($appTester->getDisplay(true)) + ); + } + + public function provideInteractiveUpdates(): \Generator + { + yield [ + ['dep/pkg' => '1.0.1'], + << 1.0.2) +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 1 update, 0 removals + - Upgrading dep/pkg (1.0.1 => 1.0.2) + - Installing another-dep/pkg (1.0.2) +OUTPUT + ]; + + yield [ + ['dep/pkg' => '1.0.1', 'another-dep/pkg' => '1.0.2'], + << 1.0.2) +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 1 update, 0 removals + - Upgrading dep/pkg (1.0.1 => 1.0.2) +OUTPUT + ]; + } } diff --git a/tests/Composer/Test/Command/ValidateCommandTest.php b/tests/Composer/Test/Command/ValidateCommandTest.php index 363cd0391284..bae0b261c3e5 100644 --- a/tests/Composer/Test/Command/ValidateCommandTest.php +++ b/tests/Composer/Test/Command/ValidateCommandTest.php @@ -30,7 +30,7 @@ public function testValidate(array $composerJson, array $command, string $expect $appTester = $this->getApplicationTester(); $appTester->run(array_merge(['command' => 'validate'], $command)); - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } public function testValidateOnFileIssues(): void @@ -42,7 +42,7 @@ public function testValidateOnFileIssues(): void $appTester->run(['command' => 'validate']); $expected = './composer.json not found.'; - $this->assertSame($expected, trim($appTester->getDisplay(true))); + self::assertSame($expected, trim($appTester->getDisplay(true))); } public function testWithComposerLock(): void @@ -63,7 +63,7 @@ public function testWithComposerLock(): void and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require-r OUTPUT; - $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + self::assertSame(trim($expected), trim($appTester->getDisplay(true))); } public function testUnaccessibleFile(): void @@ -79,8 +79,8 @@ public function testUnaccessibleFile(): void $appTester->run(['command' => 'validate']); $expected = './composer.json is not readable.'; - $this->assertSame($expected, trim($appTester->getDisplay(true))); - $this->assertSame(3, $appTester->getStatusCode()); + self::assertSame($expected, trim($appTester->getDisplay(true))); + self::assertSame(3, $appTester->getStatusCode()); chmod($directory.'/composer.json', 0700); } diff --git a/tests/Composer/Test/CompletionFunctionalTest.php b/tests/Composer/Test/CompletionFunctionalTest.php index cb452f1300aa..1a691af7d518 100644 --- a/tests/Composer/Test/CompletionFunctionalTest.php +++ b/tests/Composer/Test/CompletionFunctionalTest.php @@ -136,13 +136,13 @@ public function testComplete(string $input, ?array $expectedSuggestions): void $suggestions = $tester->complete($input); if (null === $expectedSuggestions) { - $this->assertEmpty($suggestions); + self::assertEmpty($suggestions); return; } $diff = array_diff($expectedSuggestions, $suggestions); - $this->assertEmpty($diff, sprintf('Suggestions must contain "%s". Got "%s".', implode('", "', $diff), implode('", "', $suggestions))); + self::assertEmpty($diff, sprintf('Suggestions must contain "%s". Got "%s".', implode('", "', $diff), implode('", "', $suggestions))); } private function getApplication(): Application diff --git a/tests/Composer/Test/ComposerTest.php b/tests/Composer/Test/ComposerTest.php index dea0f6a6b48b..c4bde7df80f0 100644 --- a/tests/Composer/Test/ComposerTest.php +++ b/tests/Composer/Test/ComposerTest.php @@ -22,7 +22,7 @@ public function testSetGetPackage(): void $package = $this->getMockBuilder('Composer\Package\RootPackageInterface')->getMock(); $composer->setPackage($package); - $this->assertSame($package, $composer->getPackage()); + self::assertSame($package, $composer->getPackage()); } public function testSetGetLocker(): void @@ -31,7 +31,7 @@ public function testSetGetLocker(): void $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $composer->setLocker($locker); - $this->assertSame($locker, $composer->getLocker()); + self::assertSame($locker, $composer->getLocker()); } public function testSetGetRepositoryManager(): void @@ -40,7 +40,7 @@ public function testSetGetRepositoryManager(): void $manager = $this->getMockBuilder('Composer\Repository\RepositoryManager')->disableOriginalConstructor()->getMock(); $composer->setRepositoryManager($manager); - $this->assertSame($manager, $composer->getRepositoryManager()); + self::assertSame($manager, $composer->getRepositoryManager()); } public function testSetGetDownloadManager(): void @@ -50,7 +50,7 @@ public function testSetGetDownloadManager(): void $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')->setConstructorArgs([$io])->getMock(); $composer->setDownloadManager($manager); - $this->assertSame($manager, $composer->getDownloadManager()); + self::assertSame($manager, $composer->getDownloadManager()); } public function testSetGetInstallationManager(): void @@ -59,6 +59,6 @@ public function testSetGetInstallationManager(): void $manager = $this->getMockBuilder('Composer\Installer\InstallationManager')->disableOriginalConstructor()->getMock(); $composer->setInstallationManager($manager); - $this->assertSame($manager, $composer->getInstallationManager()); + self::assertSame($manager, $composer->getInstallationManager()); } } diff --git a/tests/Composer/Test/Config/JsonConfigSourceTest.php b/tests/Composer/Test/Config/JsonConfigSourceTest.php index 536764fc01ee..9ec1c6868f28 100644 --- a/tests/Composer/Test/Config/JsonConfigSourceTest.php +++ b/tests/Composer/Test/Config/JsonConfigSourceTest.php @@ -50,7 +50,7 @@ public function testAddRepository(): void $jsonConfigSource = new JsonConfigSource(new JsonFile($config)); $jsonConfigSource->addRepository('example_tld', ['type' => 'git', 'url' => 'example.tld']); - $this->assertFileEquals(self::fixturePath('config/config-with-exampletld-repository.json'), $config); + self::assertFileEquals(self::fixturePath('config/config-with-exampletld-repository.json'), $config); } public function testAddRepositoryWithOptions(): void @@ -68,7 +68,7 @@ public function testAddRepositoryWithOptions(): void ], ]); - $this->assertFileEquals(self::fixturePath('config/config-with-exampletld-repository-and-options.json'), $config); + self::assertFileEquals(self::fixturePath('config/config-with-exampletld-repository-and-options.json'), $config); } public function testRemoveRepository(): void @@ -78,7 +78,7 @@ public function testRemoveRepository(): void $jsonConfigSource = new JsonConfigSource(new JsonFile($config)); $jsonConfigSource->removeRepository('example_tld'); - $this->assertFileEquals(self::fixturePath('composer-repositories.json'), $config); + self::assertFileEquals(self::fixturePath('composer-repositories.json'), $config); } public function testAddPackagistRepositoryWithFalseValue(): void @@ -88,7 +88,7 @@ public function testAddPackagistRepositoryWithFalseValue(): void $jsonConfigSource = new JsonConfigSource(new JsonFile($config)); $jsonConfigSource->addRepository('packagist', false); - $this->assertFileEquals(self::fixturePath('config/config-with-packagist-false.json'), $config); + self::assertFileEquals(self::fixturePath('config/config-with-packagist-false.json'), $config); } public function testRemovePackagist(): void @@ -98,7 +98,7 @@ public function testRemovePackagist(): void $jsonConfigSource = new JsonConfigSource(new JsonFile($config)); $jsonConfigSource->removeRepository('packagist'); - $this->assertFileEquals(self::fixturePath('composer-repositories.json'), $config); + self::assertFileEquals(self::fixturePath('composer-repositories.json'), $config); } /** @@ -120,7 +120,7 @@ public function testAddLink(string $sourceFile, string $type, string $name, stri $jsonConfigSource->addLink($type, $name, $value); - $this->assertFileEquals($compareAgainst, $composerJson); + self::assertFileEquals($compareAgainst, $composerJson); } /** @@ -141,7 +141,7 @@ public function testRemoveLink(string $sourceFile, string $type, string $name, s $jsonConfigSource->removeLink($type, $name); - $this->assertFileEquals($compareAgainst, $composerJson); + self::assertFileEquals($compareAgainst, $composerJson); } /** diff --git a/tests/Composer/Test/ConfigTest.php b/tests/Composer/Test/ConfigTest.php index 8a169b745a3d..1f35fbd6e078 100644 --- a/tests/Composer/Test/ConfigTest.php +++ b/tests/Composer/Test/ConfigTest.php @@ -33,7 +33,7 @@ public function testAddPackagistRepository(array $expected, array $localConfig, } $config->merge(['repositories' => $localConfig]); - $this->assertEquals($expected, $config->getRepositories()); + self::assertEquals($expected, $config->getRepositories()); } public static function dataAddPackagistRepository(): array @@ -149,7 +149,7 @@ public function testPreferredInstallAsString(): void $config->merge(['config' => ['preferred-install' => 'source']]); $config->merge(['config' => ['preferred-install' => 'dist']]); - $this->assertEquals('dist', $config->get('preferred-install')); + self::assertEquals('dist', $config->get('preferred-install')); } public function testMergePreferredInstall(): void @@ -161,7 +161,7 @@ public function testMergePreferredInstall(): void // This assertion needs to make sure full wildcard preferences are placed last // Handled by composer because we convert string preferences for BC, all other // care for ordering and collision prevention is up to the user - $this->assertEquals(['foo/*' => 'source', '*' => 'dist'], $config->get('preferred-install')); + self::assertEquals(['foo/*' => 'source', '*' => 'dist'], $config->get('preferred-install')); } public function testMergeGithubOauth(): void @@ -170,7 +170,7 @@ public function testMergeGithubOauth(): void $config->merge(['config' => ['github-oauth' => ['foo' => 'bar']]]); $config->merge(['config' => ['github-oauth' => ['bar' => 'baz']]]); - $this->assertEquals(['foo' => 'bar', 'bar' => 'baz'], $config->get('github-oauth')); + self::assertEquals(['foo' => 'bar', 'bar' => 'baz'], $config->get('github-oauth')); } public function testVarReplacement(): void @@ -180,9 +180,9 @@ public function testVarReplacement(): void $config->merge(['config' => ['bin-dir' => '$HOME', 'cache-dir' => '~/foo/']]); $home = rtrim(getenv('HOME') ?: getenv('USERPROFILE'), '\\/'); - $this->assertEquals('b', $config->get('c')); - $this->assertEquals($home, $config->get('bin-dir')); - $this->assertEquals($home.'/foo', $config->get('cache-dir')); + self::assertEquals('b', $config->get('c')); + self::assertEquals($home, $config->get('bin-dir')); + self::assertEquals($home.'/foo', $config->get('cache-dir')); } public function testRealpathReplacement(): void @@ -195,9 +195,9 @@ public function testRealpathReplacement(): void ]]); $home = rtrim(getenv('HOME') ?: getenv('USERPROFILE'), '\\/'); - $this->assertEquals('/foo/bar/vendor', $config->get('vendor-dir')); - $this->assertEquals($home.'/foo', $config->get('bin-dir')); - $this->assertEquals('/baz', $config->get('cache-dir')); + self::assertEquals('/foo/bar/vendor', $config->get('vendor-dir')); + self::assertEquals($home.'/foo', $config->get('bin-dir')); + self::assertEquals('/baz', $config->get('cache-dir')); } public function testStreamWrapperDirs(): void @@ -207,7 +207,7 @@ public function testStreamWrapperDirs(): void 'cache-dir' => 's3://baz/', ]]); - $this->assertEquals('s3://baz', $config->get('cache-dir')); + self::assertEquals('s3://baz', $config->get('cache-dir')); } public function testFetchingRelativePaths(): void @@ -218,10 +218,10 @@ public function testFetchingRelativePaths(): void 'vendor-dir' => 'vendor', ]]); - $this->assertEquals('/foo/bar/vendor', $config->get('vendor-dir')); - $this->assertEquals('/foo/bar/vendor/foo', $config->get('bin-dir')); - $this->assertEquals('vendor', $config->get('vendor-dir', Config::RELATIVE_PATHS)); - $this->assertEquals('vendor/foo', $config->get('bin-dir', Config::RELATIVE_PATHS)); + self::assertEquals('/foo/bar/vendor', $config->get('vendor-dir')); + self::assertEquals('/foo/bar/vendor/foo', $config->get('bin-dir')); + self::assertEquals('vendor', $config->get('vendor-dir', Config::RELATIVE_PATHS)); + self::assertEquals('vendor/foo', $config->get('bin-dir', Config::RELATIVE_PATHS)); } public function testOverrideGithubProtocols(): void @@ -230,17 +230,17 @@ public function testOverrideGithubProtocols(): void $config->merge(['config' => ['github-protocols' => ['https', 'ssh']]]); $config->merge(['config' => ['github-protocols' => ['https']]]); - $this->assertEquals(['https'], $config->get('github-protocols')); + self::assertEquals(['https'], $config->get('github-protocols')); } public function testGitDisabledByDefaultInGithubProtocols(): void { $config = new Config(false); $config->merge(['config' => ['github-protocols' => ['https', 'git']]]); - $this->assertEquals(['https'], $config->get('github-protocols')); + self::assertEquals(['https'], $config->get('github-protocols')); $config->merge(['config' => ['secure-http' => false]]); - $this->assertEquals(['https', 'git'], $config->get('github-protocols')); + self::assertEquals(['https', 'git'], $config->get('github-protocols')); } /** @@ -294,6 +294,7 @@ public static function prohibitedUrlProvider(): array 'http://packagist.org', 'http://10.1.0.1/satis', 'http://127.0.0.1/satis', + 'http://💛@example.org', 'svn://localhost/trunk', 'svn://will.not.resolve/trunk', 'svn://192.168.0.1/trunk', @@ -330,11 +331,11 @@ public function testDisableTlsCanBeOverridden(): void $config->merge( ['config' => ['disable-tls' => 'false']] ); - $this->assertFalse($config->get('disable-tls')); + self::assertFalse($config->get('disable-tls')); $config->merge( ['config' => ['disable-tls' => 'true']] ); - $this->assertTrue($config->get('disable-tls')); + self::assertTrue($config->get('disable-tls')); } public function testProcessTimeout(): void @@ -344,7 +345,7 @@ public function testProcessTimeout(): void $result = $config->get('process-timeout'); Platform::clearEnv('COMPOSER_PROCESS_TIMEOUT'); - $this->assertEquals(0, $result); + self::assertEquals(0, $result); } public function testHtaccessProtect(): void @@ -354,7 +355,7 @@ public function testHtaccessProtect(): void $result = $config->get('htaccess-protect'); Platform::clearEnv('COMPOSER_HTACCESS_PROTECT'); - $this->assertEquals(0, $result); + self::assertEquals(0, $result); } public function testGetSourceOfValue(): void @@ -363,14 +364,14 @@ public function testGetSourceOfValue(): void $config = new Config; - $this->assertSame(Config::SOURCE_DEFAULT, $config->getSourceOfValue('process-timeout')); + self::assertSame(Config::SOURCE_DEFAULT, $config->getSourceOfValue('process-timeout')); $config->merge( ['config' => ['process-timeout' => 1]], 'phpunit-test' ); - $this->assertSame('phpunit-test', $config->getSourceOfValue('process-timeout')); + self::assertSame('phpunit-test', $config->getSourceOfValue('process-timeout')); } public function testGetSourceOfValueEnvVariables(): void @@ -380,7 +381,7 @@ public function testGetSourceOfValueEnvVariables(): void $result = $config->getSourceOfValue('htaccess-protect'); Platform::clearEnv('COMPOSER_HTACCESS_PROTECT'); - $this->assertEquals('COMPOSER_HTACCESS_PROTECT', $result); + self::assertEquals('COMPOSER_HTACCESS_PROTECT', $result); } public function testAudit(): void @@ -420,8 +421,8 @@ public function testGetDefaultsToAnEmptyArray(): void ]; foreach ($keys as $key) { $value = $config->get($key); - $this->assertIsArray($value); // @phpstan-ignore-line - PHPStan knows that its an array for all given keys - $this->assertCount(0, $value); + self::assertIsArray($value); + self::assertCount(0, $value); } } @@ -429,29 +430,29 @@ public function testMergesPluginConfig(): void { $config = new Config(false); $config->merge(['config' => ['allow-plugins' => ['some/plugin' => true]]]); - $this->assertEquals(['some/plugin' => true], $config->get('allow-plugins')); + self::assertEquals(['some/plugin' => true], $config->get('allow-plugins')); $config->merge(['config' => ['allow-plugins' => ['another/plugin' => true]]]); - $this->assertEquals(['some/plugin' => true, 'another/plugin' => true], $config->get('allow-plugins')); + self::assertEquals(['some/plugin' => true, 'another/plugin' => true], $config->get('allow-plugins')); } public function testOverridesGlobalBooleanPluginsConfig(): void { $config = new Config(false); $config->merge(['config' => ['allow-plugins' => true]]); - $this->assertEquals(true, $config->get('allow-plugins')); + self::assertEquals(true, $config->get('allow-plugins')); $config->merge(['config' => ['allow-plugins' => ['another/plugin' => true]]]); - $this->assertEquals(['another/plugin' => true], $config->get('allow-plugins')); + self::assertEquals(['another/plugin' => true], $config->get('allow-plugins')); } public function testAllowsAllPluginsFromLocalBoolean(): void { $config = new Config(false); $config->merge(['config' => ['allow-plugins' => ['some/plugin' => true]]]); - $this->assertEquals(['some/plugin' => true], $config->get('allow-plugins')); + self::assertEquals(['some/plugin' => true], $config->get('allow-plugins')); $config->merge(['config' => ['allow-plugins' => true]]); - $this->assertEquals(true, $config->get('allow-plugins')); + self::assertEquals(true, $config->get('allow-plugins')); } } diff --git a/tests/Composer/Test/Console/HtmlOutputFormatterTest.php b/tests/Composer/Test/Console/HtmlOutputFormatterTest.php index 6d02a7396f1a..0d8032410aeb 100644 --- a/tests/Composer/Test/Console/HtmlOutputFormatterTest.php +++ b/tests/Composer/Test/Console/HtmlOutputFormatterTest.php @@ -24,9 +24,9 @@ public function testFormatting(): void 'warning' => new OutputFormatterStyle('black', 'yellow'), ]); - $this->assertEquals( - 'text green yellow black w/ yello bg', - $formatter->format('text green yellow black w/ yello bg') + self::assertEquals( + 'text green yellow black w/ yellow bg', + $formatter->format('text green yellow black w/ yellow bg') ); } } diff --git a/tests/Composer/Test/DefaultConfigTest.php b/tests/Composer/Test/DefaultConfigTest.php index a49a98ef80aa..b98da30da5a7 100644 --- a/tests/Composer/Test/DefaultConfigTest.php +++ b/tests/Composer/Test/DefaultConfigTest.php @@ -22,6 +22,6 @@ class DefaultConfigTest extends TestCase public function testDefaultValuesAreAsExpected(): void { $config = new Config; - $this->assertFalse($config->get('disable-tls')); + self::assertFalse($config->get('disable-tls')); } } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 2f888d768c1e..cfa86acf91c1 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -53,7 +53,7 @@ public function testSelectSingle(): void $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectNewest(): void @@ -69,7 +69,7 @@ public function testSelectNewest(): void $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectNewestPicksLatest(): void @@ -85,7 +85,7 @@ public function testSelectNewestPicksLatest(): void $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectNewestPicksLatestStableWithPreferStable(): void @@ -102,7 +102,7 @@ public function testSelectNewestPicksLatestStableWithPreferStable(): void $policy = new DefaultPolicy(true); $selected = $policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectNewestWithDevPicksNonDev(): void @@ -118,7 +118,7 @@ public function testSelectNewestWithDevPicksNonDev(): void $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectNewestWithPreferredVersionPicksPreferredVersionIfAvailable(): void @@ -137,7 +137,7 @@ public function testSelectNewestWithPreferredVersionPicksPreferredVersionIfAvail $policy = new DefaultPolicy(false, false, ['a' => '1.1.0.0']); $selected = $policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectNewestWithPreferredVersionPicksNewestOtherwise(): void @@ -154,7 +154,7 @@ public function testSelectNewestWithPreferredVersionPicksNewestOtherwise(): void $policy = new DefaultPolicy(false, false, ['a' => '1.1.0.0']); $selected = $policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectNewestWithPreferredVersionPicksLowestIfPreferLowest(): void @@ -171,7 +171,7 @@ public function testSelectNewestWithPreferredVersionPicksLowestIfPreferLowest(): $policy = new DefaultPolicy(false, true, ['a' => '1.1.0.0']); $selected = $policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testRepositoryOrderingAffectsPriority(): void @@ -193,7 +193,7 @@ public function testRepositoryOrderingAffectsPriority(): void $expected = [$package2->getId()]; $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); $this->repositorySet = new RepositorySet('dev'); $this->repositorySet->addRepository($repo2); @@ -204,7 +204,7 @@ public function testRepositoryOrderingAffectsPriority(): void $expected = [$package4->getId()]; $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectLocalReposFirst(): void @@ -226,6 +226,7 @@ public function testSelectLocalReposFirst(): void $pool = $this->repositorySet->createPoolForPackage('A', $this->repoLocked); $packages = $pool->whatProvides('a', new Constraint('=', '2.1.9999999.9999999-dev')); + self::assertNotEmpty($packages); $literals = []; foreach ($packages as $package) { $literals[] = $package->getId(); @@ -235,7 +236,7 @@ public function testSelectLocalReposFirst(): void $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectAllProviders(): void @@ -255,7 +256,7 @@ public function testSelectAllProviders(): void $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testPreferNonReplacingFromSameRepo(): void @@ -274,7 +275,7 @@ public function testPreferNonReplacingFromSameRepo(): void $selected = $this->policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testPreferReplacingPackageFromSameVendor(): void @@ -294,7 +295,7 @@ public function testPreferReplacingPackageFromSameVendor(): void $expected = $literals; $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); - $this->assertEquals($expected, $selected); + self::assertEquals($expected, $selected); // test with reversed order in repo $repo = new ArrayRepository; @@ -310,7 +311,7 @@ public function testPreferReplacingPackageFromSameVendor(): void $expected = $literals; $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } public function testSelectLowest(): void @@ -328,6 +329,6 @@ public function testSelectLowest(): void $selected = $policy->selectPreferredPackages($pool, $literals); - $this->assertSame($expected, $selected); + self::assertSame($expected, $selected); } } diff --git a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php index 1b66f43195ce..fc226259912f 100644 --- a/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php @@ -50,7 +50,10 @@ public function testPoolBuilder(string $file, string $message, array $expect, ar $stabilityFlags = !empty($root['stability-flags']) ? $root['stability-flags'] : []; $rootReferences = !empty($root['references']) ? $root['references'] : []; $stabilityFlags = array_map(static function ($stability): int { - return BasePackage::$stabilities[$stability]; + if (!isset(BasePackage::STABILITIES[$stability])) { + throw new \LogicException('Invalid stability given: '.$stability); + } + return BasePackage::STABILITIES[$stability]; }, $stabilityFlags); $parser = new VersionParser(); @@ -139,13 +142,13 @@ public function testPoolBuilder(string $file, string $message, array $expect, ar sort($expect); sort($result); - $this->assertSame($expect, $result, 'Unoptimized pool does not match expected package set'); + self::assertSame($expect, $result, 'Unoptimized pool does not match expected package set'); $optimizer = new PoolOptimizer(new DefaultPolicy()); $result = $this->getPackageResultSet($optimizer->optimize($request, $pool), $packageIds); sort($expectOptimized); sort($result); - $this->assertSame($expectOptimized, $result, 'Optimized pool does not match expected package set'); + self::assertSame($expectOptimized, $result, 'Optimized pool does not match expected package set'); chdir($oldCwd); } diff --git a/tests/Composer/Test/DependencyResolver/PoolOptimizerTest.php b/tests/Composer/Test/DependencyResolver/PoolOptimizerTest.php index abba7f433149..d89745130d18 100644 --- a/tests/Composer/Test/DependencyResolver/PoolOptimizerTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolOptimizerTest.php @@ -63,7 +63,7 @@ public function testPoolOptimizer(array $requestData, array $packagesBefore, arr $pool = $poolOptimizer->optimize($request, $pool); - $this->assertSame( + self::assertSame( $this->reducePackagesInfoForComparison($expectedPackages), $this->reducePackagesInfoForComparison($pool->getPackages()), $message diff --git a/tests/Composer/Test/DependencyResolver/PoolTest.php b/tests/Composer/Test/DependencyResolver/PoolTest.php index 7e96474aebe7..839b6c89ca6e 100644 --- a/tests/Composer/Test/DependencyResolver/PoolTest.php +++ b/tests/Composer/Test/DependencyResolver/PoolTest.php @@ -23,8 +23,8 @@ public function testPool(): void $pool = $this->createPool([$package]); - $this->assertEquals([$package], $pool->whatProvides('foo')); - $this->assertEquals([$package], $pool->whatProvides('foo')); + self::assertEquals([$package], $pool->whatProvides('foo')); + self::assertEquals([$package], $pool->whatProvides('foo')); } public function testWhatProvidesPackageWithConstraint(): void @@ -37,8 +37,8 @@ public function testWhatProvidesPackageWithConstraint(): void $secondPackage, ]); - $this->assertEquals([$firstPackage, $secondPackage], $pool->whatProvides('foo')); - $this->assertEquals([$secondPackage], $pool->whatProvides('foo', self::getVersionConstraint('==', '2'))); + self::assertEquals([$firstPackage, $secondPackage], $pool->whatProvides('foo')); + self::assertEquals([$secondPackage], $pool->whatProvides('foo', self::getVersionConstraint('==', '2'))); } public function testPackageById(): void @@ -47,14 +47,14 @@ public function testPackageById(): void $pool = $this->createPool([$package]); - $this->assertSame($package, $pool->packageById(1)); + self::assertSame($package, $pool->packageById(1)); } public function testWhatProvidesWhenPackageCannotBeFound(): void { $pool = $this->createPool(); - $this->assertEquals([], $pool->whatProvides('foo')); + self::assertEquals([], $pool->whatProvides('foo')); } /** diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index dec915744f81..5fe4f9286d3d 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -33,7 +33,7 @@ public function testRequestInstall(): void $request = new Request(); $request->requireName('foo'); - $this->assertEquals( + self::assertEquals( [ 'foo' => new MatchAllConstraint(), ], @@ -55,7 +55,7 @@ public function testRequestInstallSamePackageFromDifferentRepositories(): void $request = new Request(); $request->requireName('foo', $constraint = self::getVersionConstraint('=', '1')); - $this->assertEquals( + self::assertEquals( [ 'foo' => $constraint, ], diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index d158335d3eea..1df257dc2e52 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -58,7 +58,7 @@ public function testForeach(): void $this->rules[RuleSet::TYPE_LEARNED][0], ]; - $this->assertEquals($expected, $result); + self::assertEquals($expected, $result); } public function testKeys(): void @@ -76,6 +76,6 @@ public function testKeys(): void RuleSet::TYPE_LEARNED, ]; - $this->assertEquals($expected, $result); + self::assertEquals($expected, $result); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index ee84c877059d..acebb74b3ded 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -41,7 +41,7 @@ public function testAdd(): void $ruleSet->add($rules[RuleSet::TYPE_LEARNED][0], RuleSet::TYPE_LEARNED); $ruleSet->add($rules[RuleSet::TYPE_REQUEST][1], RuleSet::TYPE_REQUEST); - $this->assertEquals($rules, $ruleSet->getRules()); + self::assertEquals($rules, $ruleSet->getRules()); } public function testAddIgnoresDuplicates(): void @@ -60,7 +60,7 @@ public function testAddIgnoresDuplicates(): void $ruleSet->add($rules[RuleSet::TYPE_REQUEST][1], RuleSet::TYPE_REQUEST); $ruleSet->add($rules[RuleSet::TYPE_REQUEST][2], RuleSet::TYPE_REQUEST); - $this->assertCount(1, $ruleSet->getIteratorFor([RuleSet::TYPE_REQUEST])); + self::assertCount(1, $ruleSet->getIteratorFor([RuleSet::TYPE_REQUEST])); } public function testAddWhenTypeIsNotRecognized(): void @@ -68,7 +68,7 @@ public function testAddWhenTypeIsNotRecognized(): void $ruleSet = new RuleSet; self::expectException('OutOfBoundsException'); - // @phpstan-ignore-next-line + // @phpstan-ignore argument.type $ruleSet->add(new GenericRule([], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]), 7); } @@ -79,7 +79,7 @@ public function testCount(): void $ruleSet->add(new GenericRule([1], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]), RuleSet::TYPE_REQUEST); $ruleSet->add(new GenericRule([2], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]), RuleSet::TYPE_REQUEST); - $this->assertEquals(2, $ruleSet->count()); + self::assertEquals(2, $ruleSet->count()); } public function testRuleById(): void @@ -89,7 +89,7 @@ public function testRuleById(): void $rule = new GenericRule([], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); $ruleSet->add($rule, RuleSet::TYPE_REQUEST); - $this->assertSame($rule, $ruleSet->ruleById[0]); + self::assertSame($rule, $ruleSet->ruleById[0]); } public function testGetIterator(): void @@ -103,9 +103,9 @@ public function testGetIterator(): void $iterator = $ruleSet->getIterator(); - $this->assertSame($rule1, $iterator->current()); + self::assertSame($rule1, $iterator->current()); $iterator->next(); - $this->assertSame($rule2, $iterator->current()); + self::assertSame($rule2, $iterator->current()); } public function testGetIteratorFor(): void @@ -119,7 +119,7 @@ public function testGetIteratorFor(): void $iterator = $ruleSet->getIteratorFor(RuleSet::TYPE_LEARNED); - $this->assertSame($rule2, $iterator->current()); + self::assertSame($rule2, $iterator->current()); } public function testGetIteratorWithout(): void @@ -133,7 +133,7 @@ public function testGetIteratorWithout(): void $iterator = $ruleSet->getIteratorWithout(RuleSet::TYPE_REQUEST); - $this->assertSame($rule2, $iterator->current()); + self::assertSame($rule2, $iterator->current()); } public function testPrettyString(): void @@ -151,6 +151,6 @@ public function testPrettyString(): void $ruleSet->add($rule, RuleSet::TYPE_REQUEST); - $this->assertStringContainsString('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock, $pool)); + self::assertStringContainsString('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock, $pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 4c31c5f77aa9..3742289b2922 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -26,8 +26,8 @@ public function testGetHash(): void { $rule = new GenericRule([123], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); - $hash = unpack('ihash', md5('123', true)); - $this->assertEquals($hash['hash'], $rule->getHash()); + $hash = unpack('ihash', (string) hash(\PHP_VERSION_ID > 80100 ? 'xxh3' : 'sha1', '123', true)); + self::assertEquals($hash['hash'], $rule->getHash()); } public function testEqualsForRulesWithDifferentHashes(): void @@ -35,7 +35,7 @@ public function testEqualsForRulesWithDifferentHashes(): void $rule = new GenericRule([1, 2], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); $rule2 = new GenericRule([1, 3], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); - $this->assertFalse($rule->equals($rule2)); + self::assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithDifferLiteralsQuantity(): void @@ -43,7 +43,7 @@ public function testEqualsForRulesWithDifferLiteralsQuantity(): void $rule = new GenericRule([1, 12], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); $rule2 = new GenericRule([1], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); - $this->assertFalse($rule->equals($rule2)); + self::assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithSameLiterals(): void @@ -51,7 +51,7 @@ public function testEqualsForRulesWithSameLiterals(): void $rule = new GenericRule([1, 12], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); $rule2 = new GenericRule([1, 12], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); - $this->assertTrue($rule->equals($rule2)); + self::assertTrue($rule->equals($rule2)); } public function testSetAndGetType(): void @@ -59,7 +59,7 @@ public function testSetAndGetType(): void $rule = new GenericRule([], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); $rule->setType(RuleSet::TYPE_REQUEST); - $this->assertEquals(RuleSet::TYPE_REQUEST, $rule->getType()); + self::assertEquals(RuleSet::TYPE_REQUEST, $rule->getType()); } public function testEnable(): void @@ -68,8 +68,8 @@ public function testEnable(): void $rule->disable(); $rule->enable(); - $this->assertTrue($rule->isEnabled()); - $this->assertFalse($rule->isDisabled()); + self::assertTrue($rule->isEnabled()); + self::assertFalse($rule->isDisabled()); } public function testDisable(): void @@ -78,8 +78,8 @@ public function testDisable(): void $rule->enable(); $rule->disable(); - $this->assertTrue($rule->isDisabled()); - $this->assertFalse($rule->isEnabled()); + self::assertTrue($rule->isDisabled()); + self::assertFalse($rule->isEnabled()); } public function testIsAssertions(): void @@ -87,8 +87,8 @@ public function testIsAssertions(): void $rule = new GenericRule([1, 12], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); $rule2 = new GenericRule([1], Rule::RULE_ROOT_REQUIRE, ['packageName' => '', 'constraint' => new MatchAllConstraint]); - $this->assertFalse($rule->isAssertion()); - $this->assertTrue($rule2->isAssertion()); + self::assertFalse($rule->isAssertion()); + self::assertTrue($rule2->isAssertion()); } public function testPrettyString(): void @@ -106,6 +106,6 @@ public function testPrettyString(): void $rule = new GenericRule([$p1->getId(), -$p2->getId()], Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo', $emptyConstraint)); - $this->assertEquals('baz 1.1 relates to foo * -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool, false)); + self::assertEquals('baz 1.1 relates to foo * -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool, false)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 23d0c94e241a..2e7d3eb10b49 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -94,9 +94,9 @@ public function testInstallNonExistingPackageFails(): void $this->fail('Unsolvable conflict did not result in exception.'); } catch (SolverProblemsException $e) { $problems = $e->getProblems(); - $this->assertCount(1, $problems); - $this->assertEquals(2, $e->getCode()); - $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool, false)); + self::assertCount(1, $problems); + self::assertEquals(2, $e->getCode()); + self::assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -772,14 +772,14 @@ public function testConflictResultEmpty(): void $this->fail('Unsolvable conflict did not result in exception.'); } catch (SolverProblemsException $e) { $problems = $e->getProblems(); - $this->assertCount(1, $problems); + self::assertCount(1, $problems); $msg = "\n"; $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a * -> satisfiable by A[1.0].\n"; - $msg .= " - A 1.0 conflicts with B 1.0.\n"; $msg .= " - Root composer.json requires b * -> satisfiable by B[1.0].\n"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); + $msg .= " - A 1.0 conflicts with B 1.0.\n"; + self::assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -802,14 +802,14 @@ public function testUnsatisfiableRequires(): void $this->fail('Unsolvable conflict did not result in exception.'); } catch (SolverProblemsException $e) { $problems = $e->getProblems(); - $this->assertCount(1, $problems); + self::assertCount(1, $problems); // TODO assert problem properties $msg = "\n"; $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a * -> satisfiable by A[1.0].\n"; $msg .= " - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match the constraint.\n"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); + self::assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -847,17 +847,17 @@ public function testRequireMismatchException(): void $this->fail('Unsolvable conflict did not result in exception.'); } catch (SolverProblemsException $e) { $problems = $e->getProblems(); - $this->assertCount(1, $problems); + self::assertCount(1, $problems); $msg = "\n"; $msg .= " Problem 1\n"; + $msg .= " - Root composer.json requires a * -> satisfiable by A[1.0].\n"; + $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; + $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; - $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; $msg .= " - You can only install one version of a package, so only one of these can be installed: B[0.9, 1.0].\n"; - $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; - $msg .= " - Root composer.json requires a * -> satisfiable by A[1.0].\n"; - $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); + self::assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool, false)); } } @@ -1023,7 +1023,7 @@ public function testLearnPositiveLiteral(): void $this->createSolver(); // check correct setup for assertion later - $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral); + self::assertFalse($this->solver->testFlagLearnedPositiveLiteral); $this->checkSolverResult([ ['job' => 'install', 'package' => $packageF1], @@ -1037,7 +1037,7 @@ public function testLearnPositiveLiteral(): void // verify that the code path leading to a negative literal resulting in a positive learned literal is actually // executed - $this->assertTrue($this->solver->testFlagLearnedPositiveLiteral); + self::assertTrue($this->solver->testFlagLearnedPositiveLiteral); } protected function reposComplete(): void @@ -1094,7 +1094,7 @@ protected function checkSolverResult(array $expected): void $resultReadable[] = array_map('strval', $op); } - $this->assertEquals($expectedReadable, $resultReadable); - $this->assertEquals($expected, $result); + self::assertEquals($expectedReadable, $resultReadable); + self::assertEquals($expected, $result); } } diff --git a/tests/Composer/Test/DependencyResolver/TransactionTest.php b/tests/Composer/Test/DependencyResolver/TransactionTest.php index 554ae3b3fcd6..6b5e230f5c0d 100644 --- a/tests/Composer/Test/DependencyResolver/TransactionTest.php +++ b/tests/Composer/Test/DependencyResolver/TransactionTest.php @@ -122,6 +122,6 @@ protected function checkTransactionOperations(Transaction $transaction, array $e } } - $this->assertEquals($expected, $result); + self::assertEquals($expected, $result); } } diff --git a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php index 36c6404d88c2..8a56de0e9dbd 100644 --- a/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php @@ -37,8 +37,8 @@ public function testGetFileName(): void ->will($this->returnValue('/vendor')); $first = $method->invoke($downloader, $packageMock, '/path'); - $this->assertMatchesRegularExpression('#/vendor/composer/tmp-[a-z0-9]+\.js#', $first); - $this->assertSame($first, $method->invoke($downloader, $packageMock, '/path')); + self::assertMatchesRegularExpression('#/vendor/composer/tmp-[a-z0-9]+\.js#', $first); + self::assertSame($first, $method->invoke($downloader, $packageMock, '/path')); } public function testProcessUrl(): void @@ -54,7 +54,7 @@ public function testProcessUrl(): void $expected = 'https://github.com/composer/composer/zipball/master'; $url = $method->invoke($downloader, $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(), $expected); - $this->assertEquals($expected, $url); + self::assertEquals($expected, $url); } public function testProcessUrl2(): void @@ -70,7 +70,7 @@ public function testProcessUrl2(): void $expected = 'https://github.com/composer/composer/archive/master.tar.gz'; $url = $method->invoke($downloader, $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(), $expected); - $this->assertEquals($expected, $url); + self::assertEquals($expected, $url); } public function testProcessUrl3(): void @@ -86,7 +86,7 @@ public function testProcessUrl3(): void $expected = 'https://api.github.com/repos/composer/composer/zipball/master'; $url = $method->invoke($downloader, $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(), $expected); - $this->assertEquals($expected, $url); + self::assertEquals($expected, $url); } /** @@ -111,7 +111,7 @@ public function testProcessUrlRewriteDist(string $url): void ->will($this->returnValue('ref')); $url = $method->invoke($downloader, $package, $url); - $this->assertEquals($expected, $url); + self::assertEquals($expected, $url); } public static function provideUrls(): array @@ -148,7 +148,7 @@ public function testProcessUrlRewriteBitbucketDist(string $url, string $extensio ->will($this->returnValue('ref')); $url = $method->invoke($downloader, $package, $url); - $this->assertEquals($expected, $url); + self::assertEquals($expected, $url); } public static function provideBitbucketUrls(): array diff --git a/tests/Composer/Test/Downloader/DownloadManagerTest.php b/tests/Composer/Test/Downloader/DownloadManagerTest.php index 05d169cad0dc..885a17424d13 100644 --- a/tests/Composer/Test/Downloader/DownloadManagerTest.php +++ b/tests/Composer/Test/Downloader/DownloadManagerTest.php @@ -36,7 +36,7 @@ public function testSetGetDownloader(): void $manager = new DownloadManager($this->io, false, $this->filesystem); $manager->setDownloader('test', $downloader); - $this->assertSame($downloader, $manager->getDownloader('test')); + self::assertSame($downloader, $manager->getDownloader('test')); self::expectException('InvalidArgumentException'); $manager->getDownloader('unregistered'); @@ -86,7 +86,7 @@ public function testGetDownloaderForCorrectlyInstalledDistPackage(): void ->with('pear') ->will($this->returnValue($downloader)); - $this->assertSame($downloader, $manager->getDownloaderForPackage($package)); + self::assertSame($downloader, $manager->getDownloaderForPackage($package)); } public function testGetDownloaderForIncorrectlyInstalledDistPackage(): void @@ -152,7 +152,7 @@ public function testGetDownloaderForCorrectlyInstalledSourcePackage(): void ->with('git') ->will($this->returnValue($downloader)); - $this->assertSame($downloader, $manager->getDownloaderForPackage($package)); + self::assertSame($downloader, $manager->getDownloaderForPackage($package)); } public function testGetDownloaderForIncorrectlyInstalledSourcePackage(): void @@ -199,7 +199,7 @@ public function testGetDownloaderForMetapackage(): void $manager = new DownloadManager($this->io, false, $this->filesystem); - $this->assertNull($manager->getDownloaderForPackage($package)); + self::assertNull($manager->getDownloaderForPackage($package)); } public function testFullPackageDownload(): void @@ -264,7 +264,7 @@ public function testFullPackageDownloadFailover(): void 'source', ]; - $this->assertSame(array_shift($series), $type); + self::assertSame(array_shift($series), $type); }); $downloaderFail = $this->createDownloaderMock(); @@ -674,7 +674,7 @@ public function testGetAvailableSourcesUpdateSticksToSameSource(?string $prevPkg $manager = new DownloadManager($this->io, false, $this->filesystem); $method = new \ReflectionMethod($manager, 'getAvailableSources'); $method->setAccessible(true); - $this->assertEquals($expected, $method->invoke($manager, $target, $initial ?? null)); + self::assertEquals($expected, $method->invoke($manager, $target, $initial ?? null)); } public static function updatesProvider(): array diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 0978392f0408..a03dd9fc4105 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -81,8 +81,8 @@ public function testDownloadToExistingFile(): void } elseif (is_file($path)) { unlink($path); } - $this->assertInstanceOf('RuntimeException', $e); - $this->assertStringContainsString('exists and is not a directory', $e->getMessage()); + self::assertInstanceOf('RuntimeException', $e); + self::assertStringContainsString('exists and is not a directory', $e->getMessage()); } } @@ -96,7 +96,7 @@ public function testGetFileName(): void $method = new \ReflectionMethod($downloader, 'getFileName'); $method->setAccessible(true); - $this->assertMatchesRegularExpression('#/vendor/composer/tmp-[a-z0-9]+\.js#', $method->invoke($downloader, $package, '/path')); + self::assertMatchesRegularExpression('#/vendor/composer/tmp-[a-z0-9]+\.js#', $method->invoke($downloader, $package, '/path')); } public function testDownloadButFileIsUnsaved(): void @@ -134,8 +134,8 @@ public function testDownloadButFileIsUnsaved(): void unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); - $this->assertStringContainsString('could not be saved to', $e->getMessage()); + self::assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); + self::assertStringContainsString('could not be saved to', $e->getMessage()); } } @@ -158,7 +158,7 @@ public function testDownloadWithCustomProcessedUrl(): void $composer->setConfig($config); $expectedUrl = 'foobar'; - $expectedCacheKey = 'dummy/pkg/'.sha1($expectedUrl).'.'; + $expectedCacheKey = 'dummy/pkg/'.hash('sha1', $expectedUrl).'.'; $dispatcher = new EventDispatcher( $composer, @@ -176,7 +176,7 @@ public function testDownloadWithCustomProcessedUrl(): void ->expects($this->any()) ->method('copyTo') ->will($this->returnCallback(function ($cacheKey) use ($expectedCacheKey): bool { - $this->assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyTo method:'); + self::assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyTo method:'); return false; })); @@ -184,7 +184,7 @@ public function testDownloadWithCustomProcessedUrl(): void ->expects($this->any()) ->method('copyFrom') ->will($this->returnCallback(function ($cacheKey) use ($expectedCacheKey): bool { - $this->assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyFrom method:'); + self::assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyFrom method:'); return false; })); @@ -194,7 +194,7 @@ public function testDownloadWithCustomProcessedUrl(): void ->expects($this->any()) ->method('addCopy') ->will($this->returnCallback(function ($url) use ($expectedUrl) { - $this->assertEquals($expectedUrl, $url, 'Failed assertion on $url argument of HttpDownloader::addCopy method:'); + self::assertEquals($expectedUrl, $url, 'Failed assertion on $url argument of HttpDownloader::addCopy method:'); return \React\Promise\resolve( new Response(['url' => 'http://example.org/'], 200, [], 'file~') @@ -217,8 +217,8 @@ public function testDownloadWithCustomProcessedUrl(): void unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); - $this->assertStringContainsString('could not be saved to', $e->getMessage()); + self::assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); + self::assertStringContainsString('could not be saved to', $e->getMessage()); } } @@ -242,7 +242,7 @@ public function testDownloadWithCustomCacheKey(): void $expectedUrl = 'url'; $customCacheKey = 'xyzzy'; - $expectedCacheKey = 'dummy/pkg/'.sha1($customCacheKey).'.'; + $expectedCacheKey = 'dummy/pkg/'.hash('sha1', $customCacheKey).'.'; $dispatcher = new EventDispatcher( $composer, @@ -260,7 +260,7 @@ public function testDownloadWithCustomCacheKey(): void ->expects($this->any()) ->method('copyTo') ->will($this->returnCallback(function ($cacheKey) use ($expectedCacheKey): bool { - $this->assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyTo method:'); + self::assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyTo method:'); return false; })); @@ -268,7 +268,7 @@ public function testDownloadWithCustomCacheKey(): void ->expects($this->any()) ->method('copyFrom') ->will($this->returnCallback(function ($cacheKey) use ($expectedCacheKey): bool { - $this->assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyFrom method:'); + self::assertEquals($expectedCacheKey, $cacheKey, 'Failed assertion on $cacheKey argument of Cache::copyFrom method:'); return false; })); @@ -278,7 +278,7 @@ public function testDownloadWithCustomCacheKey(): void ->expects($this->any()) ->method('addCopy') ->will($this->returnCallback(function ($url) use ($expectedUrl) { - $this->assertEquals($expectedUrl, $url, 'Failed assertion on $url argument of HttpDownloader::addCopy method:'); + self::assertEquals($expectedUrl, $url, 'Failed assertion on $url argument of HttpDownloader::addCopy method:'); return \React\Promise\resolve( new Response(['url' => 'http://example.org/'], 200, [], 'file~') @@ -301,8 +301,8 @@ public function testDownloadWithCustomCacheKey(): void unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); - $this->assertStringContainsString('could not be saved to', $e->getMessage()); + self::assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); + self::assertStringContainsString('could not be saved to', $e->getMessage()); } } @@ -364,8 +364,8 @@ public function testDownloadFileWithInvalidChecksum(): void unlink($path); } - $this->assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); - $this->assertStringContainsString('checksum verification', $e->getMessage()); + self::assertInstanceOf('UnexpectedValueException', $e, $e->getMessage()); + self::assertStringContainsString('checksum verification', $e->getMessage()); } } @@ -388,6 +388,9 @@ public function testDowngradeShowsAppropriateMessage(): void $filesystem->expects($this->once()) ->method('removeDirectoryAsync') ->will($this->returnValue(\React\Promise\resolve(true))); + $filesystem->expects($this->any()) + ->method('normalizePath') + ->will(self::returnArgument(0)); $downloader = $this->getDownloader($ioMock, $config, null, null, null, $filesystem); diff --git a/tests/Composer/Test/Downloader/FossilDownloaderTest.php b/tests/Composer/Test/Downloader/FossilDownloaderTest.php index c04450ab16f5..0f8103e5e570 100644 --- a/tests/Composer/Test/Downloader/FossilDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FossilDownloaderTest.php @@ -76,9 +76,9 @@ public function testInstall(): void $process = $this->getProcessExecutorMock(); $process->expects([ - self::getCmd('fossil clone -- \'http://fossil.kd2.org/kd2fw/\' \''.$this->workingDir.'.fossil\''), - self::getCmd('fossil open --nested -- \''.$this->workingDir.'.fossil\''), - self::getCmd('fossil update -- \'trunk\''), + ['fossil', 'clone', '--', 'http://fossil.kd2.org/kd2fw/', $this->workingDir.'.fossil'], + ['fossil', 'open', '--nested', '--', $this->workingDir.'.fossil'], + ['fossil', 'update', '--', 'trunk'], ], true); $downloader = $this->getDownloaderMock(null, null, $process); @@ -123,8 +123,9 @@ public function testUpdate(): void $process = $this->getProcessExecutorMock(); $process->expects([ - self::getCmd("fossil changes"), - self::getCmd("fossil pull && fossil up 'trunk'"), + ['fossil', 'changes'], + ['fossil', 'pull'], + ['fossil', 'up', 'trunk'], ], true); $downloader = $this->getDownloaderMock(null, null, $process); @@ -143,7 +144,7 @@ public function testRemove(): void $process = $this->getProcessExecutorMock(); $process->expects([ - self::getCmd('fossil changes'), + ['fossil', 'changes'], ], true); $filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); @@ -162,6 +163,6 @@ public function testGetInstallationSource(): void { $downloader = $this->getDownloaderMock(null); - $this->assertEquals('source', $downloader->getInstallationSource()); + self::assertEquals('source', $downloader->getInstallationSource()); } } diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 5411767e56b3..a46054b87e0e 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -66,7 +66,7 @@ protected function setupConfig($config = null): Config $config = new Config(); } if (!$config->has('home')) { - $tmpDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.md5(uniqid('', true)); + $tmpDir = realpath(sys_get_temp_dir()).DIRECTORY_SEPARATOR.'cmptest-'.bin2hex(random_bytes(5)); $config->merge(['config' => ['home' => $tmpDir]]); } @@ -122,10 +122,16 @@ public function testDownload(): void ->will($this->returnValue('dev-master')); $process = $this->getProcessExecutorMock(); + $expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath'; $process->expects([ - $this->winCompat("git clone --no-checkout -- 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer -- 'https://example.com/composer/composer' && git fetch composer && git remote set-url origin -- 'https://example.com/composer/composer' && git remote set-url composer -- 'https://example.com/composer/composer'"), - $this->winCompat("git branch -r"), - $this->winCompat("(git checkout 'master' -- || git checkout -B 'master' 'composer/master' --) && git reset --hard '1234567890123456789012345678901234567890' --"), + ['git', 'clone', '--no-checkout', '--', 'https://example.com/composer/composer', $expectedPath], + ['git', 'remote', 'add', 'composer', '--', 'https://example.com/composer/composer'], + ['git', 'fetch', 'composer'], + ['git', 'remote', 'set-url', 'origin', '--', 'https://example.com/composer/composer'], + ['git', 'remote', 'set-url', 'composer', '--', 'https://example.com/composer/composer'], + ['git', 'branch', '-r'], + ['git', 'checkout', 'master', '--'], + ['git', 'reset', '--hard', '1234567890123456789012345678901234567890', '--'], ], true); $downloader = $this->getDownloaderMock(null, null, $process); @@ -160,16 +166,24 @@ public function testDownloadWithCache(): void $filesystem = new \Composer\Util\Filesystem; $filesystem->removeDirectory($cachePath); + $expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath'; $process = $this->getProcessExecutorMock(); $process->expects([ - ['cmd' => $this->winCompat(sprintf("git clone --mirror -- 'https://example.com/composer/composer' '%s'", $cachePath)), 'callback' => static function () use ($cachePath): void { - @mkdir($cachePath, 0777, true); - }], - ['cmd' => 'git rev-parse --git-dir', 'stdout' => '.'], - $this->winCompat('git rev-parse --quiet --verify \'1234567890123456789012345678901234567890^{commit}\''), - $this->winCompat(sprintf("git clone --no-checkout '%1\$s' 'composerPath' --dissociate --reference '%1\$s' && cd 'composerPath' && git remote set-url origin -- 'https://example.com/composer/composer' && git remote add composer -- 'https://example.com/composer/composer'", $cachePath)), - 'git branch -r', - $this->winCompat("(git checkout 'master' -- || git checkout -B 'master' 'composer/master' --) && git reset --hard '1234567890123456789012345678901234567890' --"), + [ + 'cmd' => ['git', 'clone', '--mirror', '--', 'https://example.com/composer/composer', $cachePath], + 'callback' => static function () use ($cachePath): void { + @mkdir($cachePath, 0777, true); + } + ], + ['cmd' => ['git', 'rev-parse', '--git-dir'], 'stdout' => '.'], + ['git', 'rev-parse', '--quiet', '--verify', '1234567890123456789012345678901234567890^{commit}'], + ['git', 'clone', '--no-checkout', $cachePath, $expectedPath, '--dissociate', '--reference', $cachePath], + ['git', 'remote', 'set-url', 'origin', '--', 'https://example.com/composer/composer'], + ['git', 'remote', 'add', 'composer', '--', 'https://example.com/composer/composer'], + ['git', 'branch', '-r'], + ['cmd' => ['git', 'checkout', 'master', '--'], 'return' => 1], + ['git', 'checkout', '-B', 'master', 'composer/master', '--'], + ['git', 'reset', '--hard', '1234567890123456789012345678901234567890', '--'], ], true); $downloader = $this->getDownloaderMock(null, $config, $process); @@ -197,17 +211,21 @@ public function testDownloadUsesVariousProtocolsAndSetsPushUrlForGithub(): void ->will($this->returnValue('1.0.0')); $process = $this->getProcessExecutorMock(); + $expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath'; $process->expects([ - [ - 'cmd' => $this->winCompat("git clone --no-checkout -- 'https://github.com/mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer -- 'https://github.com/mirrors/composer' && git fetch composer && git remote set-url origin -- 'https://github.com/mirrors/composer' && git remote set-url composer -- 'https://github.com/mirrors/composer'"), - 'return' => 1, - 'stderr' => 'Error1', - ], - $this->winCompat("git clone --no-checkout -- 'git@github.com:mirrors/composer' 'composerPath' && cd 'composerPath' && git remote add composer -- 'git@github.com:mirrors/composer' && git fetch composer && git remote set-url origin -- 'git@github.com:mirrors/composer' && git remote set-url composer -- 'git@github.com:mirrors/composer'"), - $this->winCompat("git remote set-url origin -- 'https://github.com/composer/composer'"), - $this->winCompat("git remote set-url --push origin -- 'git@github.com:composer/composer.git'"), - 'git branch -r', - $this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), + ['cmd' => ['git', 'clone', '--no-checkout', '--', 'https://github.com/mirrors/composer', $expectedPath], 'return' => 1, 'stderr' => 'Error1'], + + ['git', 'clone', '--no-checkout', '--', 'git@github.com:mirrors/composer', $expectedPath], + ['git', 'remote', 'add', 'composer', '--', 'git@github.com:mirrors/composer'], + ['git', 'fetch', 'composer'], + ['git', 'remote', 'set-url', 'origin', '--', 'git@github.com:mirrors/composer'], + ['git', 'remote', 'set-url', 'composer', '--', 'git@github.com:mirrors/composer'], + + ['git', 'remote', 'set-url', 'origin', '--', 'https://github.com/composer/composer'], + ['git', 'remote', 'set-url', '--push', 'origin', '--', 'git@github.com:composer/composer.git'], + ['git', 'branch', '-r'], + ['git', 'checkout', 'ref', '--'], + ['git', 'reset', '--hard', 'ref', '--'], ], true); $downloader = $this->getDownloaderMock(null, new Config(), $process); @@ -250,11 +268,18 @@ public function testDownloadAndSetPushUrlUseCustomVariousProtocolsForGithub(arra ->will($this->returnValue('1.0.0')); $process = $this->getProcessExecutorMock(); + $expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath'; $process->expects([ - $this->winCompat("git clone --no-checkout -- '{$url}' 'composerPath' && cd 'composerPath' && git remote add composer -- '{$url}' && git fetch composer && git remote set-url origin -- '{$url}' && git remote set-url composer -- '{$url}'"), - $this->winCompat("git remote set-url --push origin -- '{$pushUrl}'"), - 'git branch -r', - $this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), + ['git', 'clone', '--no-checkout', '--', $url, $expectedPath], + ['git', 'remote', 'add', 'composer', '--', $url], + ['git', 'fetch', 'composer'], + ['git', 'remote', 'set-url', 'origin', '--', $url], + ['git', 'remote', 'set-url', 'composer', '--', $url], + + ['git', 'remote', 'set-url', '--push', 'origin', '--', $pushUrl], + ['git', 'branch', '-r'], + ['git', 'checkout', 'ref', '--'], + ['git', 'reset', '--hard', 'ref', '--'], ], true); $config = new Config(); @@ -284,28 +309,21 @@ public function testDownloadThrowsRuntimeExceptionIfGitCommandFails(): void ->will($this->returnValue('1.0.0')); $process = $this->getProcessExecutorMock(); + $expectedPath = Platform::isWindows() ? Platform::getCwd().'/composerPath' : 'composerPath'; $process->expects([ [ - 'cmd' => $this->winCompat("git clone --no-checkout -- 'https://example.com/composer/composer' 'composerPath' && cd 'composerPath' && git remote add composer -- 'https://example.com/composer/composer' && git fetch composer && git remote set-url origin -- 'https://example.com/composer/composer' && git remote set-url composer -- 'https://example.com/composer/composer'"), + 'cmd' => ['git', 'clone', '--no-checkout', '--', 'https://example.com/composer/composer', $expectedPath], 'return' => 1, ], ]); - // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe - try { - $downloader = $this->getDownloaderMock(null, null, $process); - $downloader->download($packageMock, 'composerPath'); - $downloader->prepare('install', $packageMock, 'composerPath'); - $downloader->install($packageMock, 'composerPath'); - $downloader->cleanup('install', $packageMock, 'composerPath'); - - $this->fail('This test should throw'); - } catch (\RuntimeException $e) { - if ('RuntimeException' !== get_class($e)) { - throw $e; - } - $this->assertEquals('RuntimeException', get_class($e)); - } + self::expectException('RuntimeException'); + self::expectExceptionMessage('Failed to execute git clone --no-checkout -- https://example.com/composer/composer '.$expectedPath); + $downloader = $this->getDownloaderMock(null, null, $process); + $downloader->download($packageMock, 'composerPath'); + $downloader->prepare('install', $packageMock, 'composerPath'); + $downloader->install($packageMock, 'composerPath'); + $downloader->cleanup('install', $packageMock, 'composerPath'); } public function testUpdateforPackageWithoutSourceReference(): void @@ -327,8 +345,6 @@ public function testUpdateforPackageWithoutSourceReference(): void public function testUpdate(): void { - $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer -- 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- 'https://github.com/composer/composer'"); - $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) ->method('getSourceReference') @@ -345,13 +361,23 @@ public function testUpdate(): void $process = $this->getProcessExecutorMock(); $process->expects([ - $this->winCompat('git show-ref --head -d'), - $this->winCompat('git status --porcelain --untracked-files=no'), - $this->winCompat('git remote -v'), - $expectedGitUpdateCommand, - $this->winCompat('git branch -r'), - $this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), - $this->winCompat('git remote -v'), + ['git', 'show-ref', '--head', '-d'], + ['git', 'status', '--porcelain', '--untracked-files=no'], + ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1], + + // fallback commands for the above failing + ['git', 'remote', '-v'], + ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'], + ['git', 'fetch', 'composer'], + ['git', 'fetch', '--tags', 'composer'], + + ['git', 'remote', '-v'], + ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'], + + ['git', 'branch', '-r'], + ['git', 'checkout', 'ref', '--'], + ['git', 'reset', '--hard', 'ref', '--'], + ['git', 'remote', '-v'], ], true); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); @@ -364,8 +390,6 @@ public function testUpdate(): void public function testUpdateWithNewRepoUrl(): void { - $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer -- 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- 'https://github.com/composer/composer'"); - $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) ->method('getSourceReference') @@ -385,22 +409,26 @@ public function testUpdateWithNewRepoUrl(): void $process = $this->getProcessExecutorMock(); $process->expects([ - $this->winCompat("git show-ref --head -d"), - $this->winCompat("git status --porcelain --untracked-files=no"), - $this->winCompat("git remote -v"), - $this->winCompat($expectedGitUpdateCommand), - 'git branch -r', - $this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), + ['git', 'show-ref', '--head', '-d'], + ['git', 'status', '--porcelain', '--untracked-files=no'], + ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 0], + + ['git', 'remote', '-v'], + ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'], + + ['git', 'branch', '-r'], + ['git', 'checkout', 'ref', '--'], + ['git', 'reset', '--hard', 'ref', '--'], [ - 'cmd' => $this->winCompat("git remote -v"), + 'cmd' => ['git', 'remote', '-v'], 'stdout' => 'origin https://github.com/old/url (fetch) origin https://github.com/old/url (push) composer https://github.com/old/url (fetch) composer https://github.com/old/url (push) ', ], - $this->winCompat("git remote set-url origin -- 'https://github.com/composer/composer'"), - $this->winCompat("git remote set-url --push origin -- 'git@github.com:composer/composer.git'"), + ['git', 'remote', 'set-url', 'origin', '--', 'https://github.com/composer/composer'], + ['git', 'remote', 'set-url', '--push', 'origin', '--', 'git@github.com:composer/composer.git'], ], true); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); @@ -416,9 +444,6 @@ public function testUpdateWithNewRepoUrl(): void */ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails(): void { - $expectedGitUpdateCommand = $this->winCompat("(git remote set-url composer -- 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- 'https://github.com/composer/composer'"); - $expectedGitUpdateCommand2 = $this->winCompat("(git remote set-url composer -- 'git@github.com:composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- 'git@github.com:composer/composer'"); - $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) ->method('getSourceReference') @@ -432,44 +457,38 @@ public function testUpdateThrowsRuntimeExceptionIfGitCommandFails(): void $process = $this->getProcessExecutorMock(); $process->expects([ - $this->winCompat('git show-ref --head -d'), - $this->winCompat('git status --porcelain --untracked-files=no'), - $this->winCompat('git remote -v'), - [ - 'cmd' => $expectedGitUpdateCommand, - 'return' => 1, - ], - [ - 'cmd' => $expectedGitUpdateCommand2, - 'return' => 1, - ], - $this->winCompat('git --version'), + ['git', 'show-ref', '--head', '-d'], + ['git', 'status', '--porcelain', '--untracked-files=no'], + + // commit not yet in so we try to fetch + ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1], + + // fail first fetch + ['git', 'remote', '-v'], + ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'], + ['cmd' => ['git', 'fetch', 'composer'], 'return' => 1], + + // fail second fetch + ['git', 'remote', 'set-url', 'composer', '--', 'git@github.com:composer/composer'], + ['cmd' => ['git', 'fetch', 'composer'], 'return' => 1], + + ['git', '--version'], ], true); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); - // not using PHPUnit's expected exception because Prophecy exceptions extend from RuntimeException too so it is not safe - try { - $downloader = $this->getDownloaderMock(null, new Config(), $process); - $downloader->download($packageMock, $this->workingDir, $packageMock); - $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); - $downloader->update($packageMock, $packageMock, $this->workingDir); - $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); - - $this->fail('This test should throw'); - } catch (\RuntimeException $e) { - if ('RuntimeException' !== get_class($e)) { - throw $e; - } - $this->assertEquals('RuntimeException', get_class($e)); - } + self::expectException('RuntimeException'); + self::expectExceptionMessage('Failed to clone https://github.com/composer/composer via https, ssh protocols, aborting.'); + self::expectExceptionMessageMatches('{git@github\.com:composer/composer}'); + $downloader = $this->getDownloaderMock(null, new Config(), $process); + $downloader->download($packageMock, $this->workingDir, $packageMock); + $downloader->prepare('update', $packageMock, $this->workingDir, $packageMock); + $downloader->update($packageMock, $packageMock, $this->workingDir); + $downloader->cleanup('update', $packageMock, $this->workingDir, $packageMock); } public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstButIsAbleToRecover(): void { - $expectedFirstGitUpdateCommand = $this->winCompat("(git remote set-url composer -- '".(Platform::isWindows() ? 'C:\\' : '/')."' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- '".(Platform::isWindows() ? 'C:\\' : '/')."'"); - $expectedSecondGitUpdateCommand = $this->winCompat("(git remote set-url composer -- 'https://github.com/composer/composer' && git rev-parse --quiet --verify 'ref^{commit}' || (git fetch composer && git fetch --tags composer)) && git remote set-url composer -- 'https://github.com/composer/composer'"); - $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $packageMock->expects($this->any()) ->method('getSourceReference') @@ -486,22 +505,33 @@ public function testUpdateDoesntThrowsRuntimeExceptionIfGitCommandFailsAtFirstBu $process = $this->getProcessExecutorMock(); $process->expects([ - $this->winCompat('git show-ref --head -d'), - $this->winCompat('git status --porcelain --untracked-files=no'), - $this->winCompat('git remote -v'), - [ - 'cmd' => $expectedFirstGitUpdateCommand, - 'return' => 1, - ], - $this->winCompat('git --version'), - $this->winCompat('git remote -v'), - [ - 'cmd' => $expectedSecondGitUpdateCommand, - 'return' => 0, - ], - $this->winCompat('git branch -r'), - $this->winCompat("git checkout 'ref' -- && git reset --hard 'ref' --"), - $this->winCompat('git remote -v'), + ['git', 'show-ref', '--head', '-d'], + ['git', 'status', '--porcelain', '--untracked-files=no'], + + // commit not yet in so we try to fetch + ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1], + + // fail first source URL + ['git', 'remote', '-v'], + ['git', 'remote', 'set-url', 'composer', '--', Platform::isWindows() ? 'C:\\' : '/'], + ['cmd' => ['git', 'fetch', 'composer'], 'return' => 1], + ['git', '--version'], + + // commit not yet in so we try to fetch + ['cmd' => ['git', 'rev-parse', '--quiet', '--verify', 'ref^{commit}'], 'return' => 1], + + // pass second source URL + ['git', 'remote', '-v'], + ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'], + ['cmd' => ['git', 'fetch', 'composer'], 'return' => 0], + ['git', 'fetch', '--tags', 'composer'], + ['git', 'remote', '-v'], + ['git', 'remote', 'set-url', 'composer', '--', 'https://github.com/composer/composer'], + + ['git', 'branch', '-r'], + ['git', 'checkout', 'ref', '--'], + ['git', 'reset', '--hard', 'ref', '--'], + ['git', 'remote', '-v'], ], true); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); @@ -604,13 +634,11 @@ public function testNotUsingDowngradingWithReferences(): void public function testRemove(): void { - $expectedGitResetCommand = $this->winCompat("git status --porcelain --untracked-files=no"); - $packageMock = $this->getMockBuilder('Composer\Package\PackageInterface')->getMock(); $process = $this->getProcessExecutorMock(); $process->expects([ - 'git show-ref --head -d', - $expectedGitResetCommand, + ['git', 'show-ref', '--head', '-d'], + ['git', 'status', '--porcelain', '--untracked-files=no'], ], true); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); @@ -631,18 +659,6 @@ public function testGetInstallationSource(): void { $downloader = $this->getDownloaderMock(); - $this->assertEquals('source', $downloader->getInstallationSource()); - } - - private function winCompat(string $cmd): string - { - if (Platform::isWindows()) { - $cmd = str_replace('cd ', 'cd /D ', $cmd); - $cmd = str_replace('composerPath', Platform::getCwd().'/composerPath', $cmd); - - return self::getCmd($cmd); - } - - return $cmd; + self::assertEquals('source', $downloader->getInstallationSource()); } } diff --git a/tests/Composer/Test/Downloader/HgDownloaderTest.php b/tests/Composer/Test/Downloader/HgDownloaderTest.php index bdb8bde3ea3c..3788864630d5 100644 --- a/tests/Composer/Test/Downloader/HgDownloaderTest.php +++ b/tests/Composer/Test/Downloader/HgDownloaderTest.php @@ -76,8 +76,8 @@ public function testDownload(): void $process = $this->getProcessExecutorMock(); $process->expects([ - self::getCmd('hg clone -- \'https://mercurial.dev/l3l0/composer\' \''.$this->workingDir.'\''), - self::getCmd('hg up -- \'ref\''), + ['hg', 'clone', '--', 'https://mercurial.dev/l3l0/composer', $this->workingDir], + ['hg', 'up', '--', 'ref'], ], true); $downloader = $this->getDownloaderMock(null, null, $process); @@ -117,8 +117,9 @@ public function testUpdate(): void $process = $this->getProcessExecutorMock(); $process->expects([ - self::getCmd('hg st'), - self::getCmd("hg pull -- 'https://github.com/l3l0/composer' && hg up -- 'ref'"), + ['hg', 'st'], + ['hg', 'pull', '--', 'https://github.com/l3l0/composer'], + ['hg', 'up', '--', 'ref'], ], true); $downloader = $this->getDownloaderMock(null, null, $process); @@ -135,7 +136,7 @@ public function testRemove(): void $process = $this->getProcessExecutorMock(); $process->expects([ - self::getCmd('hg st'), + ['hg', 'st'], ], true); $filesystem = $this->getMockBuilder('Composer\Util\Filesystem')->getMock(); @@ -154,6 +155,6 @@ public function testGetInstallationSource(): void { $downloader = $this->getDownloaderMock(null); - $this->assertEquals('source', $downloader->getInstallationSource()); + self::assertEquals('source', $downloader->getInstallationSource()); } } diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index 83f54c8847f2..1cabefb568f4 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -66,7 +66,7 @@ public function testErrorMessages(): void $this->fail('Download of invalid tarball should throw an exception'); } catch (\RuntimeException $e) { - $this->assertMatchesRegularExpression('/(File format not recognized|Unrecognized archive format)/i', $e->getMessage()); + self::assertMatchesRegularExpression('/(File format not recognized|Unrecognized archive format)/i', $e->getMessage()); } } } diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 80e9378d99fd..c5c168831504 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -102,7 +102,7 @@ public function testErrorMessages(): void $this->fail('Download of invalid zip files should throw an exception'); } catch (\Exception $e) { - $this->assertStringContainsString('is not a zip archive', $e->getMessage()); + self::assertStringContainsString('is not a zip archive', $e->getMessage()); } } @@ -167,6 +167,9 @@ public function testZipArchiveOnlyGood(): void $zipArchive->expects($this->once()) ->method('extractTo') ->will($this->returnValue(true)); + $zipArchive->expects($this->once()) + ->method('count') + ->will($this->returnValue(0)); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); $promise = $downloader->extract($this->package, $this->filename, 'vendor/dir'); @@ -261,6 +264,9 @@ public function testNonWindowsFallbackGood(): void $zipArchive->expects($this->once()) ->method('extractTo') ->will($this->returnValue(true)); + $zipArchive->expects($this->once()) + ->method('count') + ->will($this->returnValue(0)); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index f0a7ecc5f8f3..1439c9f87515 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -200,7 +200,7 @@ public function testDispatcherRemoveListener(): void .'> ev1: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest->someMethod'.PHP_EOL; - $this->assertEquals($expected, $io->getOutput()); + self::assertEquals($expected, $io->getOutput()); $dispatcher->removeListener($this); $dispatcher->dispatch('ev1'); @@ -208,7 +208,7 @@ public function testDispatcherRemoveListener(): void $expected .= '> ev1: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL .'> ev2: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL; - $this->assertEquals($expected, $io->getOutput()); + self::assertEquals($expected, $io->getOutput()); } public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack(): void @@ -245,7 +245,7 @@ public function testDispatcherCanExecuteCliAndPhpInSameEventScriptStack(): void $expected = '> post-install-cmd: echo -n foo'.PHP_EOL. '> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::someMethod'.PHP_EOL. '> post-install-cmd: echo -n bar'.PHP_EOL; - $this->assertEquals($expected, $io->getOutput()); + self::assertEquals($expected, $io->getOutput()); } public function testDispatcherCanPutEnv(): void @@ -274,7 +274,7 @@ public function testDispatcherCanPutEnv(): void $expected = '> post-install-cmd: @putenv ABC=123'.PHP_EOL. '> post-install-cmd: Composer\Test\EventDispatcher\EventDispatcherTest::getTestEnv'.PHP_EOL; - $this->assertEquals($expected, $io->getOutput()); + self::assertEquals($expected, $io->getOutput()); } public function testDispatcherAppendsDirBinOnPathForEveryListener(): void @@ -311,6 +311,51 @@ public function testDispatcherAppendsDirBinOnPathForEveryListener(): void } } + public function testDispatcherSupportForAdditionalArgs(): void + { + $process = $this->getProcessExecutorMock(); + $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') + ->setConstructorArgs([ + $this->createComposerInstance(), + $io = new BufferIO('', OutputInterface::VERBOSITY_VERBOSE), + $process, + ]) + ->onlyMethods([ + 'getListeners', + ]) + ->getMock(); + + $reflMethod = new \ReflectionMethod($dispatcher, 'getPhpExecCommand'); + if (PHP_VERSION_ID < 80100) { + $reflMethod->setAccessible(true); + } + $phpCmd = $reflMethod->invoke($dispatcher); + + $args = ProcessExecutor::escape('ARG').' '.ProcessExecutor::escape('ARG2').' '.ProcessExecutor::escape('--arg'); + $process->expects([ + 'echo -n foo', + $phpCmd.' foo.php '.$args.' then the rest', + 'echo -n bar '.$args, + ], true); + + $listeners = [ + 'echo -n foo @no_additional_args', + '@php foo.php @additional_args then the rest', + 'echo -n bar', + ]; + + $dispatcher->expects($this->atLeastOnce()) + ->method('getListeners') + ->will($this->returnValue($listeners)); + + $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false, ['ARG', 'ARG2', '--arg']); + + $expected = '> post-install-cmd: echo -n foo'.PHP_EOL. + '> post-install-cmd: @php foo.php '.$args.' then the rest'.PHP_EOL. + '> post-install-cmd: echo -n bar '.$args.PHP_EOL; + self::assertEquals($expected, $io->getOutput()); + } + public static function createsVendorBinFolderChecksEnvDoesNotContainsBin(): void { mkdir(__DIR__ . '/vendor/bin', 0700, true); @@ -386,7 +431,7 @@ public function testDispatcherCanExecuteComposerScriptGroups(): void '> group: @subgroup'.PHP_EOL. '> subgroup: echo -n baz'.PHP_EOL. '> group: echo -n bar'.PHP_EOL; - $this->assertEquals($expected, $io->getOutput()); + self::assertEquals($expected, $io->getOutput()); } public function testRecursionInScriptsNames(): void @@ -425,7 +470,7 @@ public function testRecursionInScriptsNames(): void $expected = "> helloWorld: @hello World".PHP_EOL. "> hello: echo Hello " .self::getCmd("'World'").PHP_EOL; - $this->assertEquals($expected, $io->getOutput()); + self::assertEquals($expected, $io->getOutput()); } public function testDispatcherDetectInfiniteRecursion(): void diff --git a/tests/Composer/Test/FactoryTest.php b/tests/Composer/Test/FactoryTest.php index 636e58c317d4..a4b5e6df107d 100644 --- a/tests/Composer/Test/FactoryTest.php +++ b/tests/Composer/Test/FactoryTest.php @@ -13,9 +13,16 @@ namespace Composer\Test; use Composer\Factory; +use Composer\Util\Platform; class FactoryTest extends TestCase { + public function tearDown(): void + { + parent::tearDown(); + Platform::clearEnv('COMPOSER'); + } + /** * @group TLS */ @@ -37,4 +44,23 @@ public function testDefaultValuesAreAsExpected(): void Factory::createHttpDownloader($ioMock, $config); } + + public function testGetComposerJsonPath(): void + { + self::assertSame('./composer.json', Factory::getComposerFile()); + } + + public function testGetComposerJsonPathFailsIfDir(): void + { + Platform::putEnv('COMPOSER', __DIR__); + self::expectException('RuntimeException'); + self::expectExceptionMessage('The COMPOSER environment variable is set to '.__DIR__.' which is a directory, this variable should point to a composer.json or be left unset.'); + Factory::getComposerFile(); + } + + public function testGetComposerJsonPathFromEnv(): void + { + Platform::putEnv('COMPOSER', ' foo.json '); + self::assertSame('foo.json', Factory::getComposerFile()); + } } diff --git a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php index 8d148ec7cab6..67d212720f51 100644 --- a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php +++ b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreAllPlatformRequirementFilterTest.php @@ -24,8 +24,8 @@ public function testIsIgnored(string $req, bool $expectIgnored): void { $platformRequirementFilter = new IgnoreAllPlatformRequirementFilter(); - $this->assertSame($expectIgnored, $platformRequirementFilter->isIgnored($req)); - $this->assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req)); + self::assertSame($expectIgnored, $platformRequirementFilter->isIgnored($req)); + self::assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req)); } /** diff --git a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php index 5f3cc1f514dd..30ff79bbc5b6 100644 --- a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php +++ b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilterTest.php @@ -26,7 +26,7 @@ public function testIsIgnored(array $reqList, string $req, bool $expectIgnored): { $platformRequirementFilter = new IgnoreListPlatformRequirementFilter($reqList); - $this->assertSame($expectIgnored, $platformRequirementFilter->isIgnored($req)); + self::assertSame($expectIgnored, $platformRequirementFilter->isIgnored($req)); } /** @@ -58,7 +58,7 @@ public function testIsUpperBoundIgnored(array $reqList, string $req, bool $expec { $platformRequirementFilter = new IgnoreListPlatformRequirementFilter($reqList); - $this->assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req)); + self::assertSame($expectIgnored, $platformRequirementFilter->isUpperBoundIgnored($req)); } /** diff --git a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php index 70c3fabd204d..512e747719b5 100644 --- a/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php +++ b/tests/Composer/Test/Filter/PlatformRequirementFilter/IgnoreNothingPlatformRequirementFilterTest.php @@ -24,8 +24,8 @@ public function testIsIgnored(string $req): void { $platformRequirementFilter = new IgnoreNothingPlatformRequirementFilter(); - $this->assertFalse($platformRequirementFilter->isIgnored($req)); // @phpstan-ignore-line - $this->assertFalse($platformRequirementFilter->isUpperBoundIgnored($req)); // @phpstan-ignore-line + self::assertFalse($platformRequirementFilter->isIgnored($req)); + self::assertFalse($platformRequirementFilter->isUpperBoundIgnored($req)); } /** diff --git a/tests/Composer/Test/Filter/PlatformRequirementFilter/PlatformRequirementFilterFactoryTest.php b/tests/Composer/Test/Filter/PlatformRequirementFilter/PlatformRequirementFilterFactoryTest.php index 7fa9feba3cd5..b8c1c795dd36 100644 --- a/tests/Composer/Test/Filter/PlatformRequirementFilter/PlatformRequirementFilterFactoryTest.php +++ b/tests/Composer/Test/Filter/PlatformRequirementFilter/PlatformRequirementFilterFactoryTest.php @@ -25,7 +25,7 @@ final class PlatformRequirementFilterFactoryTest extends TestCase */ public function testFromBoolOrList($boolOrList, $expectedInstance): void { - $this->assertInstanceOf($expectedInstance, PlatformRequirementFilterFactory::fromBoolOrList($boolOrList)); + self::assertInstanceOf($expectedInstance, PlatformRequirementFilterFactory::fromBoolOrList($boolOrList)); } /** @@ -52,13 +52,13 @@ public function testIgnoreAll(): void { $platformRequirementFilter = PlatformRequirementFilterFactory::ignoreAll(); - $this->assertInstanceOf('Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter', $platformRequirementFilter); + self::assertInstanceOf('Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter', $platformRequirementFilter); } public function testIgnoreNothing(): void { $platformRequirementFilter = PlatformRequirementFilterFactory::ignoreNothing(); - $this->assertInstanceOf('Composer\Filter\PlatformRequirementFilter\IgnoreNothingPlatformRequirementFilter', $platformRequirementFilter); + self::assertInstanceOf('Composer\Filter\PlatformRequirementFilter\IgnoreNothingPlatformRequirementFilter', $platformRequirementFilter); } } diff --git a/tests/Composer/Test/Fixtures/functional/installed-versions2/composer.lock b/tests/Composer/Test/Fixtures/functional/installed-versions2/composer.lock index 825f8a49642a..f78da55917ca 100644 --- a/tests/Composer/Test/Fixtures/functional/installed-versions2/composer.lock +++ b/tests/Composer/Test/Fixtures/functional/installed-versions2/composer.lock @@ -231,10 +231,10 @@ "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "plugin-api-version": "2.0.0" } diff --git a/tests/Composer/Test/Fixtures/functional/plugin-autoloading-only-loads-dependencies/composer.lock b/tests/Composer/Test/Fixtures/functional/plugin-autoloading-only-loads-dependencies/composer.lock index 0beb2f5a7efc..6499f4b924e5 100644 --- a/tests/Composer/Test/Fixtures/functional/plugin-autoloading-only-loads-dependencies/composer.lock +++ b/tests/Composer/Test/Fixtures/functional/plugin-autoloading-only-loads-dependencies/composer.lock @@ -29,10 +29,10 @@ "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [], + "platform": {}, + "platform-dev": {}, "plugin-api-version": "2.2.0" } diff --git a/tests/Composer/Test/Fixtures/installer-slow/github-issues-7665.test b/tests/Composer/Test/Fixtures/installer-slow/github-issues-7665.test index 33e29deecfb2..a971ad45bdf9 100644 --- a/tests/Composer/Test/Fixtures/installer-slow/github-issues-7665.test +++ b/tests/Composer/Test/Fixtures/installer-slow/github-issues-7665.test @@ -2799,237 +2799,237 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.1 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.2 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.3 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.4 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.5 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.6 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.7 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.8 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.0 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.1 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.2 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.3 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.14.0 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.14.1 (conflict analysis result) - - Conclusion: don't install friendsofphp/php-cs-fixer v2.14.2 (conflict analysis result) + - Root composer.json requires behat/behat ~2.5 -> satisfiable by behat/behat[v2.5.5]. - Root composer.json requires friendsofphp/php-cs-fixer ^2.12 -> satisfiable by friendsofphp/php-cs-fixer[v2.12.0, ..., v2.14.2]. - - Conclusion: don't install symfony/finder v2.7.3 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.2 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.4 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.3 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.4 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.10 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.0.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.11 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.0 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.12 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.1 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.13 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.2 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.14 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.3 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.15 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.4 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.16 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.17 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.18 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.19 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.20 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.21 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.1.10 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.22 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.0 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.23 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.1 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.24 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.2 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.25 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.3 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.26 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.4 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.27 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.28 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.29 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.30 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.31 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.32 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.10 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.33 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.11 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.34 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.12 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.35 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.13 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.36 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.2.14 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.37 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.0 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.38 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.1 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.39 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.2 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.40 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.3 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.41 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.4 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.42 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.43 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.44 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.45 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.46 (conflict analysis result) + - behat/behat v2.5.5 requires symfony/finder ~2.0 -> satisfiable by symfony/finder[v2.6.0, ..., v2.8.49]. + - friendsofphp/php-cs-fixer v2.12.0 requires symfony/finder ^3.0 || ^4.0 -> satisfiable by symfony/finder[v3.0.0, ..., v3.4.24, v4.0.0, ..., v4.2.5]. + - Conclusion: don't install symfony/finder v4.1.2 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.1.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.1.0 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.9 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.2 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.15 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.14 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.13 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.12 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.11 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.10 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v4.0.0 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.9 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.24 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.23 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.22 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.21 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.20 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.2 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.19 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.18 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.17 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.16 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.15 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.14 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.13 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.12 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.11 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.10 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.4.0 (conflict analysis result) - Conclusion: don't install symfony/finder v3.3.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.47 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.10 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.48 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.11 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.49 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.12 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.7.50 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.13 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.0 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.14 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.1 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.15 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.2 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.16 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.3 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.3.17 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.2 (conflict analysis result) - Conclusion: don't install symfony/finder v3.3.18 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.0 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.1 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.2 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.17 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.16 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.15 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.14 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.13 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.12 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.11 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.10 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.3.0 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.9 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.2 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.14 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.13 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.12 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.11 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.10 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.2.0 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.9 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.2 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.10 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.1.0 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.9 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.2 (conflict analysis result) + - Conclusion: don't install symfony/finder v3.0.1 (conflict analysis result) - Conclusion: don't install symfony/finder v2.8.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.4 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.10 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.11 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.12 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.13 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.14 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.15 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.10 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.16 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.11 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.17 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.12 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.18 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.13 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.19 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.14 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.20 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.15 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.21 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.16 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.22 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.17 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.23 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.18 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.24 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.19 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.25 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.20 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.26 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.21 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.27 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.22 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.28 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.23 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.29 (conflict analysis result) - - Conclusion: don't install symfony/finder v3.4.24 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.30 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.0 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.31 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.1 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.32 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.2 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.33 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.3 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.34 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.4 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.35 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.5 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.36 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.6 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.37 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.7 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.38 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.8 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.39 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.9 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.40 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.10 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.41 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.11 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.42 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.12 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.43 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.13 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.44 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.14 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.45 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.0.15 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.46 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.1.0 (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.47 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.1.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.49 (conflict analysis result) - Conclusion: don't install symfony/finder v2.8.48 (conflict analysis result) - - Conclusion: don't install symfony/finder v4.1.2 (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.2.5] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.2.4] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.2.3] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.2.2] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.2.1] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.2.0] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.1.11] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.47 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.46 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.45 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.44 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.43 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.42 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.41 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.40 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.39 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.38 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.37 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.36 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.35 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.34 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.33 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.32 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.31 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.30 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.29 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.28 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.27 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.26 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.25 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.24 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.23 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.22 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.21 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.20 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.2 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.19 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.18 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.17 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.16 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.15 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.14 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.13 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.12 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.11 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.10 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.1 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.8.0 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.9 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.8 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.7 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.6 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.50 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.5 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.49 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.48 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.47 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.46 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.45 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.44 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.43 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.42 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.41 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.40 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.4 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.39 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.38 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.37 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.36 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.35 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.34 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.33 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.32 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.31 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.30 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.3 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.29 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.28 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.27 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.26 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.25 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.24 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.23 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.22 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.21 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.20 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.19 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.18 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.17 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.16 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.15 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.14 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.13 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.12 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.11 (conflict analysis result) + - Conclusion: don't install symfony/finder v2.7.10 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.14.2 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.14.1 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.14.0 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.3 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.2 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.1 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.13.0 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.8 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.7 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.6 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.5 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.4 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.3 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.2 (conflict analysis result) + - Conclusion: don't install friendsofphp/php-cs-fixer v2.12.1 (conflict analysis result) - Conclusion: don't install symfony/finder[v4.1.10] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.1.9] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.1.8] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.1.7] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.1.6] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.1.5] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder[v4.1.4] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.1.11] | install symfony/finder[v2.8.49] (conflict analysis result) - Conclusion: don't install symfony/finder[v4.1.3] | install symfony/finder[v2.8.49] (conflict analysis result) - - Conclusion: don't install symfony/finder v2.8.49 (conflict analysis result) - - Root composer.json requires behat/behat ~2.5 -> satisfiable by behat/behat[v2.5.5]. - - behat/behat v2.5.5 requires symfony/finder ~2.0 -> satisfiable by symfony/finder[v2.6.0, ..., v2.8.49]. + - Conclusion: don't install symfony/finder[v4.1.4] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.1.5] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.1.6] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.1.7] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.1.8] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.1.9] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.2.0] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.2.1] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.2.2] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.2.3] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.2.4] | install symfony/finder[v2.8.49] (conflict analysis result) + - Conclusion: don't install symfony/finder[v4.2.5] | install symfony/finder[v2.8.49] (conflict analysis result) - You can only install one version of a package, so only one of these can be installed: symfony/finder[v2.6.0, ..., v2.8.49, v3.0.0, ..., v3.4.24, v4.0.0, ..., v4.2.5]. - - friendsofphp/php-cs-fixer v2.12.0 requires symfony/finder ^3.0 || ^4.0 -> satisfiable by symfony/finder[v3.0.0, ..., v3.4.24, v4.0.0, ..., v4.2.5]. - - Conclusion: don't install symfony/finder v3.0.1 (conflict analysis result) --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/alias-in-lock.test b/tests/Composer/Test/Fixtures/installer/alias-in-lock.test index 49cd1519faf2..deb9cb2f17c1 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-in-lock.test +++ b/tests/Composer/Test/Fixtures/installer/alias-in-lock.test @@ -57,11 +57,11 @@ update "alias_normalized": "3.0.3.0" }], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing a/aliased2 (3.0.2) diff --git a/tests/Composer/Test/Fixtures/installer/alias-in-lock2.test b/tests/Composer/Test/Fixtures/installer/alias-in-lock2.test index c781ae32f450..92b1323d6eff 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-in-lock2.test +++ b/tests/Composer/Test/Fixtures/installer/alias-in-lock2.test @@ -35,11 +35,11 @@ Newly defined root aliases end up in lock file only if the package is updated "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update a/aliased @@ -63,11 +63,11 @@ update a/aliased "alias_normalized": "3.0.3.0" }], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing a/aliased (3.0.2) diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test index 1729826ec082..2b1a46e78554 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems.test @@ -26,11 +26,11 @@ Test the error output of solver problems with dev-master aliases. "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- @@ -45,12 +45,11 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 + - Root composer.json requires a/a *@dev -> satisfiable by a/a[dev-master]. - Root composer.json requires b/b *@dev -> satisfiable by b/b[dev-master]. - a/a dev-master requires d/d 1.0.0 -> satisfiable by d/d[1.0.0]. - - You can only install one version of a package, so only one of these can be installed: d/d[1.0.0, 2.0.0]. - b/b dev-master requires d/d 2.0.0 -> satisfiable by d/d[2.0.0]. - Conclusion: install b/b dev-master (conflict analysis result) - - Root composer.json requires a/a *@dev -> satisfiable by a/a[dev-master]. + - You can only install one version of a package, so only one of these can be installed: d/d[1.0.0, 2.0.0]. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test index 3e37004d5f61..c5e3e3b5ebda 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test +++ b/tests/Composer/Test/Fixtures/installer/alias-solver-problems2.test @@ -24,11 +24,11 @@ Test the error output of solver problems with dev-master aliases. "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- @@ -43,9 +43,8 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file. - locked/pkg is locked to version dev-master and an update of this package was not requested. + - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file. Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test index f7e51e5f9a6d..f6b35ebe6849 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-with-reference.test +++ b/tests/Composer/Test/Fixtures/installer/alias-with-reference.test @@ -55,8 +55,8 @@ update }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing a/aliased (dev-master abcd) diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test index b409a5cfc08e..3c8b613d55ca 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -80,8 +80,8 @@ Aliases take precedence over default package even if default is selected }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test index 1500d156b8ed..805428165e8a 100644 --- a/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test +++ b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test @@ -78,8 +78,8 @@ update }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing a/aliased (dev-next) diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index c9a9dba6e6a3..baccf01ff3e5 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -26,9 +26,9 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - c/c 1.0.0 requires x/x 1.0 -> could not be found in any version, there may be a typo in the package name. - - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0]. - Root composer.json requires b/b 1.* -> satisfiable by b/b[1.0.0]. + - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0]. + - c/c 1.0.0 requires x/x 1.0 -> could not be found in any version, there may be a typo in the package name. Potential causes: - A typo in the package name diff --git a/tests/Composer/Test/Fixtures/installer/circular-dependency-errors.test b/tests/Composer/Test/Fixtures/installer/circular-dependency-errors.test index 1bd4518f7a71..0a9f42544ce2 100644 --- a/tests/Composer/Test/Fixtures/installer/circular-dependency-errors.test +++ b/tests/Composer/Test/Fixtures/installer/circular-dependency-errors.test @@ -49,7 +49,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires requires/root 1.0.0 -> satisfiable by requires/root[1.0.0]. - - requires/root 1.0.0 requires root/pkg ^1.0 -> satisfiable by root/pkg[1.0.0] from package repo (defining 3 packages) but root/pkg is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance. + - requires/root 1.0.0 requires root/pkg ^1.0 -> satisfiable by root/pkg[1.0.0] from package repo (defining 3 packages) but root/pkg dev-master is the root package and cannot be modified. See https://getcomposer.org/dep-on-root for details and assistance. Problem 2 - Root composer.json requires requires/root2 1.0.0 -> satisfiable by requires/root2[1.0.0]. - requires/root2 1.0.0 requires root/pkg ^2.0 -> found root/pkg[dev-master] but it does not match the constraint. See https://getcomposer.org/dep-on-root for details and assistance. diff --git a/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-by-dep-package-problem.test b/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-by-dep-package-problem.test index 81035601a06d..f316ccc68c82 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-by-dep-package-problem.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-by-dep-package-problem.test @@ -41,8 +41,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires conflicting/pkg * -> satisfiable by conflicting/pkg[1.0.0]. - - conflicting/pkg 1.0.0 conflicts with replaced/pkg2 >=2.1 (provider/pkg 1.0.0 replaces replaced/pkg2 2.5). - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + - conflicting/pkg 1.0.0 conflicts with replaced/pkg2 >=2.1 (provider/pkg 1.0.0 replaces replaced/pkg2 2.5). --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-package-problem.test b/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-package-problem.test index b6cca7270d11..2fe44ae4d8b3 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-package-problem.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-against-replaced-package-problem.test @@ -38,8 +38,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - __root__ is present at version 1.2.3 and cannot be modified by Composer - - conflicting/pkg 1.0.0 conflicts with replaced/pkg 2.* (__root__ 1.2.3 replaces replaced/pkg 2.*). - Root composer.json requires conflicting/pkg * -> satisfiable by conflicting/pkg[1.0.0]. + - conflicting/pkg 1.0.0 conflicts with replaced/pkg 2.* (__root__ 1.2.3 replaces replaced/pkg 2.*). --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test index a59b2ef98a24..71bf55709dab 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test @@ -31,8 +31,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0]. - - conflicter/pkg 1.0.0 conflicts with victim/pkg 1.0.0. - Root composer.json requires victim/pkg 1.0.0 -> satisfiable by victim/pkg[1.0.0]. + - conflicter/pkg 1.0.0 conflicts with victim/pkg 1.0.0. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/conflict-on-root-with-alias-prevents-update-if-not-required.test b/tests/Composer/Test/Fixtures/installer/conflict-on-root-with-alias-prevents-update-if-not-required.test index 3a457b22c3dd..d562163c64e4 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-on-root-with-alias-prevents-update-if-not-required.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-on-root-with-alias-prevents-update-if-not-required.test @@ -32,7 +32,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - __root__ is present at version 1.0.0+no-version-set and cannot be modified by Composer - - some/dep 1.3.x-dev is an alias of some/dep dev-main and must be installed with it. - - __root__ 1.0.0+no-version-set conflicts with some/dep 1.3.x-dev. - Root composer.json requires some/dep dev-main -> satisfiable by some/dep[dev-main]. + - __root__ 1.0.0+no-version-set conflicts with some/dep 1.3.x-dev. + - some/dep 1.3.x-dev is an alias of some/dep dev-main and must be installed with it. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-in-lock-does-prevents-install.test b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-in-lock-does-prevents-install.test index c19ca396ca1c..ec77808d23f3 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-in-lock-does-prevents-install.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-in-lock-does-prevents-install.test @@ -34,8 +34,8 @@ Test that conflict with a branch alias in the lock file leads to an error on ins }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- install @@ -48,7 +48,7 @@ Your lock file does not contain a compatible set of packages. Please run compose Problem 1 - conflictor/foo is locked to version 1.0.0 and an update of this package was not requested. - - conflictor/foo 1.0.0 conflicts with some/dep 1.3.x-dev. - some/dep is locked to version 1.3.x-dev and an update of this package was not requested. + - conflictor/foo 1.0.0 conflicts with some/dep 1.3.x-dev. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update-if-not-required.test b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update-if-not-required.test index 78cfd0362a1e..8475f41fb0f4 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update-if-not-required.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update-if-not-required.test @@ -30,9 +30,9 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 + - Root composer.json requires conflictor/foo 1.0.0 -> satisfiable by conflictor/foo[1.0.0]. - Root composer.json requires some/dep dev-main -> satisfiable by some/dep[dev-main]. - conflictor/foo 1.0.0 conflicts with some/dep 1.3.x-dev. - some/dep 1.3.x-dev is an alias of some/dep dev-main and must be installed with it. - - Root composer.json requires conflictor/foo 1.0.0 -> satisfiable by conflictor/foo[1.0.0]. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update.test b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update.test index 74b61304e96a..efe40a9fd80e 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update.test @@ -36,8 +36,8 @@ update }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing some/dep (1.2.x-dev) diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test b/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test index 1f5120880d40..b24c04334df2 100644 --- a/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-all-dependencies-option-dont-recommend-to-use-it.test @@ -24,11 +24,11 @@ Verify that a conflict with all dependencies option enabled don't recommend to u "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- @@ -43,8 +43,7 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file. - locked/pkg is locked to version dev-master and an update of this package was not requested. + - locked/pkg dev-master requires locked/dependency 1.0.0 -> found locked/dependency[1.0.0] in the lock file but not in remote repositories, make sure you avoid updating this package to keep the one from the lock file. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test b/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test index 02155ef52b00..fc26296ddf64 100644 --- a/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/deduplicate-solver-problems.test @@ -41,9 +41,8 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - package/a[2.2.0, ..., 2.6.0] require missing/dep ^1.1 -> found missing/dep[2.0.0] but it does not match the constraint. - - package/a[2.0.0, ..., 2.1.0] require missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint. - Root composer.json requires package/a * -> satisfiable by package/a[2.0.0, ..., 2.6.0]. + - package/a[2.0.0, ..., 2.1.0] require missing/dep ^1.0 -> found missing/dep[2.0.0] but it does not match the constraint. + - package/a[2.2.0, ..., 2.6.0] require missing/dep ^1.1 -> found missing/dep[2.0.0] but it does not match the constraint. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index dcc94dff237b..513e800678f0 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #4319 ( github.com/composer/composer/issues/4319 ). +See GitHub issue #4319 ( github.com/composer/composer/issues/4319 ). Present a clear error message when config.platform.php version results in a conflict rule. diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 62bda3d7dc7b..fa8e4529fa64 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #4795 ( github.com/composer/composer/issues/4795 ). +See GitHub issue #4795 ( github.com/composer/composer/issues/4795 ). Composer\Installer::allowListUpdateDependencies should not output a warning for dependencies that need to be updated that are also a root package, when that root package is also explicitly allowed. @@ -45,8 +45,8 @@ that are also a root package, when that root package is also explicitly allowed. "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update a/a b/b --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index b619785f0fb0..9044481a3da7 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #4795 ( github.com/composer/composer/issues/4795 ). +See GitHub issue #4795 ( github.com/composer/composer/issues/4795 ). Composer\Installer::allowListUpdateDependencies intentionally ignores root requirements even if said package is also a dependency of one the requirements that is allowed for update. @@ -39,11 +39,11 @@ dependency of one the requirements that is allowed for update. "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update b/b --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-7051.test b/tests/Composer/Test/Fixtures/installer/github-issues-7051.test index 190689db90e0..ada6386759f4 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-7051.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-7051.test @@ -115,19 +115,19 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Conclusion: don't install friendsofphp/php-cs-fixer v2.10.5 (conflict analysis result) - Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.4, v2.10.5]. - - illuminate/queue v5.2.0 requires illuminate/console 5.2.* -> satisfiable by illuminate/console[v5.2.25, v5.2.26]. + - Root composer.json requires illuminate/queue * -> satisfiable by illuminate/queue[v5.2.0]. + - friendsofphp/php-cs-fixer v2.10.4 requires symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.2.13, ..., v3.4.29]. + - illuminate/console v5.2.25 requires symfony/console 3.1.* -> satisfiable by symfony/console[v3.1.9, v3.1.10]. - illuminate/console v5.2.26 requires symfony/console 2.8.* -> satisfiable by symfony/console[v2.8.7, v2.8.8]. + - illuminate/queue v5.2.0 requires illuminate/console 5.2.* -> satisfiable by illuminate/console[v5.2.25, v5.2.26]. - Conclusion: don't install symfony/console v2.8.7 (conflict analysis result) - - Conclusion: don't install symfony/console v3.4.28 (conflict analysis result) - Conclusion: don't install symfony/console v2.8.8 (conflict analysis result) + - Conclusion: don't install symfony/console v3.1.10 (conflict analysis result) + - Conclusion: don't install symfony/console v3.4.28 (conflict analysis result) - Conclusion: don't install symfony/console v3.4.29 (conflict analysis result) - - Root composer.json requires illuminate/queue * -> satisfiable by illuminate/queue[v5.2.0]. - - friendsofphp/php-cs-fixer v2.10.4 requires symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.2.13, ..., v3.4.29]. + - Conclusion: don't install friendsofphp/php-cs-fixer v2.10.5 (conflict analysis result) - You can only install one version of a package, so only one of these can be installed: symfony/console[v2.8.7, v2.8.8, v3.1.9, ..., v3.4.29]. - - illuminate/console v5.2.25 requires symfony/console 3.1.* -> satisfiable by symfony/console[v3.1.9, v3.1.10]. - - Conclusion: don't install symfony/console v3.1.10 (conflict analysis result) --EXPECT-OUTPUT-OPTIMIZED-- Loading composer repositories with package information @@ -135,16 +135,15 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 + - Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.4, v2.10.5]. - Root composer.json requires illuminate/queue * -> satisfiable by illuminate/queue[v5.2.0]. - - illuminate/queue v5.2.0 requires illuminate/console 5.2.* -> satisfiable by illuminate/console[v5.2.25, v5.2.26]. + - friendsofphp/php-cs-fixer[v2.10.4, ..., v2.10.5] require symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.2.13, ..., v3.4.29]. - illuminate/console v5.2.25 requires symfony/console 3.1.* -> satisfiable by symfony/console[v3.1.9, v3.1.10]. - illuminate/console v5.2.26 requires symfony/console 2.8.* -> satisfiable by symfony/console[v2.8.7, v2.8.8]. + - illuminate/queue v5.2.0 requires illuminate/console 5.2.* -> satisfiable by illuminate/console[v5.2.25, v5.2.26]. - You can only install one version of a package, so only one of these can be installed: symfony/console[v2.8.7, v2.8.8, v3.1.9, ..., v3.4.29]. - - friendsofphp/php-cs-fixer[v2.10.4, ..., v2.10.5] require symfony/console ^3.2 || ^4.0 -> satisfiable by symfony/console[v3.2.13, ..., v3.4.29]. - - Root composer.json requires friendsofphp/php-cs-fixer * -> satisfiable by friendsofphp/php-cs-fixer[v2.10.4, v2.10.5]. --EXPECT-- --EXPECT-EXIT-CODE-- 2 - diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-8902.test b/tests/Composer/Test/Fixtures/installer/github-issues-8902.test index 53fc1275bcf3..56b4299d0765 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-8902.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-8902.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #8902 ( https://github.com/composer/composer/issues/8902 ). +See GitHub issue #8902 ( https://github.com/composer/composer/issues/8902 ). Avoid installing packages twice if they are required in different versions and one is matched by a dev package. @@ -34,11 +34,11 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires beyondcode/laravel-dump-server ^1.3 -> satisfiable by beyondcode/laravel-dump-server[1.4.0]. + - Root composer.json requires laravel/framework ^6.8 -> satisfiable by laravel/framework[6.8.14]. + - beyondcode/laravel-dump-server 1.4.0 requires symfony/var-dumper ^5.0 -> satisfiable by symfony/var-dumper[5.2.x-dev (alias of dev-master)]. + - laravel/framework 6.8.14 requires symfony/var-dumper ^4.3.4 -> satisfiable by symfony/var-dumper[4.4.0]. - You can only install one version of a package, so only one of these can be installed: symfony/var-dumper[dev-master, 4.4.0]. - symfony/var-dumper 5.2.x-dev is an alias of symfony/var-dumper dev-master and thus requires it to be installed too. - - laravel/framework 6.8.14 requires symfony/var-dumper ^4.3.4 -> satisfiable by symfony/var-dumper[4.4.0]. - - beyondcode/laravel-dump-server 1.4.0 requires symfony/var-dumper ^5.0 -> satisfiable by symfony/var-dumper[5.2.x-dev (alias of dev-master)]. - - Root composer.json requires laravel/framework ^6.8 -> satisfiable by laravel/framework[6.8.14]. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-8903.test b/tests/Composer/Test/Fixtures/installer/github-issues-8903.test index f2c8c9f202bb..90ef85ef4507 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-8903.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-8903.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #8903 ( https://github.com/composer/composer/issues/8903 ). +See GitHub issue #8903 ( https://github.com/composer/composer/issues/8903 ). Recursive output of learnt rules can lead to infinite loop. diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-9012.test b/tests/Composer/Test/Fixtures/installer/github-issues-9012.test index f118584d1733..69d1f51e9ac8 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-9012.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-9012.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #9012 ( https://github.com/composer/composer/issues/9012 and https://gist.github.com/Seldaek/4e2dbc2cea4b4fd7a8207bb310ec8e34). +See GitHub issue #9012 ( https://github.com/composer/composer/issues/9012 and https://gist.github.com/Seldaek/4e2dbc2cea4b4fd7a8207bb310ec8e34). Recursive analysis of the same learnt rules can lead to infinite recursion in solver. diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-9290.test b/tests/Composer/Test/Fixtures/installer/github-issues-9290.test index 8c1122b14b87..af9ea4596eff 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-9290.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-9290.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #9290 ( https://github.com/composer/composer/issues/9290 ). +See GitHub issue #9290 ( https://github.com/composer/composer/issues/9290 ). MultiConflictRule with a level 1 decision needs to exit correctly. --COMPOSER-- diff --git a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test index 3c171a4b03a3..5bdf07c14c07 100644 --- a/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test +++ b/tests/Composer/Test/Fixtures/installer/install-dev-using-dist.test @@ -50,8 +50,8 @@ install }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing a/a (dev-master) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock-with-ignore.test b/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock-with-ignore.test new file mode 100644 index 000000000000..54ace3491279 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock-with-ignore.test @@ -0,0 +1,47 @@ +--TEST-- +Requirements from the composer file are not installed if the lock file is present and missing requirements warning +is issued when allow-missing-requirements if enabled +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "required/pkg", "version": "1.0.0" }, + { "name": "newly-required/pkg", "version": "1.0.0" } + ] + } + ], + "require": { + "required/pkg": "1.0.0", + "newly-required/pkg": "1.0.0" + }, + "config": { + "allow-missing-requirements": true + } +} +--LOCK-- +{ + "packages": [ + { "name": "required/pkg", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false +} +--RUN-- +install +--EXPECT-OUTPUT-- +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +- Required package "newly-required/pkg" is not present in the lock file. +This usually happens when composer files are incorrectly merged or the composer.json file is manually edited. +Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md +and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require-r +Package operations: 1 install, 0 updates, 0 removals +Generating autoload files +--EXPECT-- +Installing required/pkg (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock.test index 0bbef6ace4c9..e068f5ae9acc 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-incomplete-lock.test @@ -24,7 +24,7 @@ Requirements from the composer file are not installed if the lock file is presen "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index 4160fac55b64..65650cd41702 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -28,7 +28,7 @@ Install from a lock file that deleted a package "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false } --INSTALLED-- diff --git a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test index 5f6459799be7..c318aca76a4d 100644 --- a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test @@ -32,7 +32,7 @@ Installing an old alias that doesn't exist anymore from a lock is possible "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } diff --git a/tests/Composer/Test/Fixtures/installer/install-package-and-its-provider-skips-original.test b/tests/Composer/Test/Fixtures/installer/install-package-and-its-provider-skips-original.test index 70a74be8ddd2..4f7309e7a40c 100644 --- a/tests/Composer/Test/Fixtures/installer/install-package-and-its-provider-skips-original.test +++ b/tests/Composer/Test/Fixtures/installer/install-package-and-its-provider-skips-original.test @@ -6,17 +6,17 @@ Install package and it's replacer skips the original { "type": "package", "package": [ - { "name": "ext-foo", "version": "1.0.0" }, - { "name": "ext-foo/fork", "version": "0.5.0", "replace": { "ext-foo": "1.0.*" } } + { "name": "example/foo", "version": "1.0.0" }, + { "name": "example/foo-fork", "version": "0.5.0", "replace": { "example/foo": "1.0.*" } } ] } ], "require": { - "ext-foo": "1.0.0", - "ext-foo/fork": "0.5.*" + "example/foo": "1.0.0", + "example/foo-fork": "0.5.*" } } --RUN-- install --EXPECT-- -Installing ext-foo/fork (0.5.0) +Installing example/foo-fork (0.5.0) diff --git a/tests/Composer/Test/Fixtures/installer/load-replaced-package-if-replacer-dropped.test b/tests/Composer/Test/Fixtures/installer/load-replaced-package-if-replacer-dropped.test index 851e03a97e00..3c249f678985 100644 --- a/tests/Composer/Test/Fixtures/installer/load-replaced-package-if-replacer-dropped.test +++ b/tests/Composer/Test/Fixtures/installer/load-replaced-package-if-replacer-dropped.test @@ -29,11 +29,11 @@ Ensure that a package gets loaded which was previously skipped due to replacemen "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update root/dep --with-all-dependencies @@ -42,4 +42,3 @@ Installing replacer/pkg (1.1.0) Installing root/dep (1.2.0) Installing replaced/pkg (1.0.0) Installing root/no-update (1.0.0) - diff --git a/tests/Composer/Test/Fixtures/installer/outdated-lock-file-fails-install.test b/tests/Composer/Test/Fixtures/installer/outdated-lock-file-fails-install.test new file mode 100644 index 000000000000..e4891a3e8779 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/outdated-lock-file-fails-install.test @@ -0,0 +1,39 @@ +--TEST-- +Test that install checks missing requirements from both composer.json if the lock file is outdated. +--COMPOSER-- +{ + "require": { + "some/dep": "dev-main", + "some/dep2": "dev-main" + } +} +--LOCK-- +{ + "content-hash": "old", + "packages": [ + {"name": "some/dep", "version": "dev-foo"} + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {} +} +--RUN-- +install +--EXPECT-EXIT-CODE-- +4 +--EXPECT-OUTPUT-- +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update `. +- Required package "some/dep" is in the lock file as "dev-foo" but that does not satisfy your constraint "dev-main". +- Required package "some/dep2" is not present in the lock file. +This usually happens when composer files are incorrectly merged or the composer.json file is manually edited. +Read more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md +and prefer using the "require" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require-r + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/outdated-lock-file-with-new-platform-reqs-fails.test b/tests/Composer/Test/Fixtures/installer/outdated-lock-file-with-new-platform-reqs-fails.test new file mode 100644 index 000000000000..c859cb4f1f50 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/outdated-lock-file-with-new-platform-reqs-fails.test @@ -0,0 +1,46 @@ +--TEST-- +Test that install checks platform requirements from both composer.json AND composer.lock even if the lock file is outdated. +Platform requires appearing in both lock and composer.json will be checked using the composer.json as source of truth (see ext-foo). +--COMPOSER-- +{ + "require": { + "php-64bit": "^25", + "ext-foo": "^10" + } +} +--LOCK-- +{ + "content-hash": "old", + "packages": [ + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {"php": "^20", "ext-foo": "^5"}, + "platform-dev": {} +} +--RUN-- +install +--EXPECT-EXIT-CODE-- +2 +--EXPECT-OUTPUT-- +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. It is recommended that you run `composer update` or `composer update `. +Your lock file does not contain a compatible set of packages. Please run composer update. + + Problem 1 + - Root composer.json requires php-64bit ^25 but your php-64bit version (%s) does not satisfy that requirement. + Problem 2 + - Root composer.json requires PHP extension ext-foo ^10 but it is missing from your system. Install or enable PHP's foo extension. + Problem 3 + - Root composer.json requires php ^20 but your php version (%s) does not satisfy that requirement. + +To enable extensions, verify that they are enabled in your .ini files: +__inilist__ +You can also run `php --ini` in a terminal to see which files are used by PHP in CLI mode. +Alternatively, you can run Composer with `--ignore-platform-req=ext-foo` to temporarily ignore these required extensions. +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-always-updates-symlinked-path-repos.test b/tests/Composer/Test/Fixtures/installer/partial-update-always-updates-symlinked-path-repos.test index e30d3a4052d9..ff0b1c6d353a 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-always-updates-symlinked-path-repos.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-always-updates-symlinked-path-repos.test @@ -44,8 +44,8 @@ Partial updates always update path repos which are symlinked, but not those whic }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --INSTALLED-- diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test index 2ad9ea3f6607..1a14aab73e65 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-allow-listed-unstable.test @@ -37,8 +37,8 @@ Partial update from lock file should apply lock file and if an unstable package }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test index 614c8751565b..7b76991a14dd 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test @@ -59,11 +59,11 @@ Partial update forces updates dev reference from lock file for non allowed packa "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update b/b @@ -86,11 +86,11 @@ update b/b "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Upgrading a/a (dev-master oldmaster-a => dev-master newmaster-a) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock-with-root-alias.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock-with-root-alias.test index b5d195a46148..7aafbfc3f2f4 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock-with-root-alias.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock-with-root-alias.test @@ -36,11 +36,11 @@ Partial update from lock file with root aliases should work } ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --INSTALLED-- [ @@ -67,11 +67,11 @@ update --lock } ], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Marking c/aliased (2.0.0) as installed, alias of c/aliased (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index d176b02d300e..febca0ba80f8 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -43,8 +43,8 @@ Partial update from lock file should update everything to the state of the lock, }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --INSTALLED-- [ @@ -67,11 +67,11 @@ update b/unstable "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Upgrading a/old (0.9.0 => 1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test index 9d971b3847e5..4164590977f8 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test @@ -62,11 +62,11 @@ Partial update installs from lock even if package don't exist in public repo any "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update b/b @@ -90,11 +90,11 @@ update b/b "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required-with-provide.test b/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required-with-provide.test index 4cae3c87ce73..0f80a42525d4 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required-with-provide.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required-with-provide.test @@ -36,8 +36,8 @@ Ensure that a partial update of a dependency does not conflict if the only way t "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update dep/pkg2 --with-dependencies @@ -52,11 +52,11 @@ update dep/pkg2 --with-dependencies "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing dep/pkg1 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required.test b/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required.test index b2a6c67cf2f4..ee20c9d7720c 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-keeps-older-dep-if-still-required.test @@ -36,8 +36,8 @@ Ensure that a partial update of a dependency does not conflict if the only way t "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update dep/pkg2 --with-dependencies @@ -52,11 +52,11 @@ update dep/pkg2 --with-dependencies "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing dep/pkg1 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-loads-root-aliases-for-path-repos.test b/tests/Composer/Test/Fixtures/installer/partial-update-loads-root-aliases-for-path-repos.test index 71d961e09c8e..a3f9921f0e77 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-loads-root-aliases-for-path-repos.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-loads-root-aliases-for-path-repos.test @@ -34,8 +34,8 @@ Partial updates always load root aliases even for path repos which are symlinked }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --INSTALLED-- diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-provide.test b/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-provide.test index c332ca045a7a..e7935fb29ad0 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-provide.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-provide.test @@ -36,8 +36,8 @@ Ensure a partial update of a dependency does NOT update dependencies which provi "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update root/req1 --with-dependencies @@ -52,11 +52,11 @@ update root/req1 --with-dependencies "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing dep/pkg1 (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-replace.test b/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-replace.test index 39994422d81f..24d717d7dc38 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-replace.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-with-dependencies-replace.test @@ -35,8 +35,8 @@ Ensure a partial update of a dependency updates dependencies which replace one o "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update root/req1 --with-dependencies @@ -50,11 +50,11 @@ update root/req1 --with-dependencies "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing dep/pkg1 (1.2.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-with-deps-warns-root.test b/tests/Composer/Test/Fixtures/installer/partial-update-with-deps-warns-root.test index d8d49a246dbc..86b3faf13da3 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-with-deps-warns-root.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-with-deps-warns-root.test @@ -74,8 +74,8 @@ Ensure that a partial update of a dependency does not conflict if the only way t "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update update/pkg1 --with-dependencies @@ -101,11 +101,11 @@ update update/pkg1 --with-dependencies "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-OUTPUT-- Loading composer repositories with package information diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-with-symlinked-path-repos.test b/tests/Composer/Test/Fixtures/installer/partial-update-with-symlinked-path-repos.test index 2ea842613956..a6e0aa52782c 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-with-symlinked-path-repos.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-with-symlinked-path-repos.test @@ -64,8 +64,8 @@ Partially updating one root requirement with transitive deps fully updates trans }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- diff --git a/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test b/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test index be8313f55820..34d07c456754 100644 --- a/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test @@ -27,11 +27,11 @@ Test the error output of solver problems with ext/platform packages which have p "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- diff --git a/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test b/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test index 377c6a742d41..4832906d537f 100644 --- a/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test +++ b/tests/Composer/Test/Fixtures/installer/problems-reduce-versions.test @@ -91,11 +91,11 @@ Test the error output minifies version lists "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- @@ -115,4 +115,3 @@ Your requirements could not be resolved to an installable set of packages. Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test index a73dd3dc9f56..572440468479 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test @@ -42,8 +42,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - __root__ is present at version 1.2.3 and cannot be modified by Composer - - provider/pkg[1.0.0] cannot be installed as that would require removing __root__[1.2.3]. They both replace root-replaced/transitive-replaced and thus cannot coexist. - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + - provider/pkg[1.0.0] cannot be installed as that would require removing __root__[1.2.3]. They both replace root-replaced/transitive-replaced and thus cannot coexist. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test index 29f0ebe86d83..eb54bb93df05 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -37,12 +37,12 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Conclusion: don't install regular/pkg 1.0.1 (conflict analysis result) - - Conclusion: don't install regular/pkg 1.0.2 (conflict analysis result) - - Conclusion: don't install regular/pkg 1.0.3 (conflict analysis result) - - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Conclusion: don't install regular/pkg 1.0.3 (conflict analysis result) + - Conclusion: don't install regular/pkg 1.0.2 (conflict analysis result) + - Conclusion: don't install regular/pkg 1.0.1 (conflict analysis result) + - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. --EXPECT-OUTPUT-OPTIMIZED-- Loading composer repositories with package information @@ -50,9 +50,8 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3], replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. replacer/pkg replaces regular/pkg and thus cannot coexist with it. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test index b5d3fefe7477..a4c2b66789e1 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test +++ b/tests/Composer/Test/Fixtures/installer/provider-dev-require-can-satisfy-require.test @@ -42,11 +42,11 @@ update --no-dev ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Installing b/b (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/remove-deletes-unused-deps.test b/tests/Composer/Test/Fixtures/installer/remove-deletes-unused-deps.test index 8a81d02e9648..0e5467e593b1 100644 --- a/tests/Composer/Test/Fixtures/installer/remove-deletes-unused-deps.test +++ b/tests/Composer/Test/Fixtures/installer/remove-deletes-unused-deps.test @@ -34,7 +34,7 @@ Removing a package deletes unused dependencies and does not update other depende "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } diff --git a/tests/Composer/Test/Fixtures/installer/remove-does-nothing-if-removal-requires-update-of-dep.test b/tests/Composer/Test/Fixtures/installer/remove-does-nothing-if-removal-requires-update-of-dep.test index 5df46f1188b6..5a703492c4ae 100644 --- a/tests/Composer/Test/Fixtures/installer/remove-does-nothing-if-removal-requires-update-of-dep.test +++ b/tests/Composer/Test/Fixtures/installer/remove-does-nothing-if-removal-requires-update-of-dep.test @@ -27,7 +27,7 @@ Removing a package has no effect if another package would require an update in o "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } diff --git a/tests/Composer/Test/Fixtures/installer/replaced-packages-should-not-be-installed-when-installing-from-lock.test b/tests/Composer/Test/Fixtures/installer/replaced-packages-should-not-be-installed-when-installing-from-lock.test index 947d96b28129..ea06b8e9a167 100644 --- a/tests/Composer/Test/Fixtures/installer/replaced-packages-should-not-be-installed-when-installing-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/replaced-packages-should-not-be-installed-when-installing-from-lock.test @@ -29,8 +29,8 @@ Requiring a replaced package in a version, that is not provided by the replacing "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/root-alias-change-with-circular-dep.test b/tests/Composer/Test/Fixtures/installer/root-alias-change-with-circular-dep.test index bcbbcc81f29a..009f1c55da7d 100644 --- a/tests/Composer/Test/Fixtures/installer/root-alias-change-with-circular-dep.test +++ b/tests/Composer/Test/Fixtures/installer/root-alias-change-with-circular-dep.test @@ -41,11 +41,11 @@ This also checks that an implicit stabilityFlag is added for the root package, i "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/root-alias-gets-loaded-for-locked-pkgs.test b/tests/Composer/Test/Fixtures/installer/root-alias-gets-loaded-for-locked-pkgs.test index 172288f75756..03945bd476eb 100644 --- a/tests/Composer/Test/Fixtures/installer/root-alias-gets-loaded-for-locked-pkgs.test +++ b/tests/Composer/Test/Fixtures/installer/root-alias-gets-loaded-for-locked-pkgs.test @@ -24,11 +24,11 @@ Newly defined root alias does not get loaded if package is loaded from lock file "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index 32127b612835..a3a75e79f974 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -45,7 +45,7 @@ The locked version will not get overwritten by an install but fails on invalid p "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } diff --git a/tests/Composer/Test/Fixtures/installer/solver-problem-with-hash-in-branch.test b/tests/Composer/Test/Fixtures/installer/solver-problem-with-hash-in-branch.test new file mode 100644 index 000000000000..e45a57720d96 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/solver-problem-with-hash-in-branch.test @@ -0,0 +1,40 @@ +--TEST-- +Test the problem output suggests fixes for branch names where the # was replaced by + +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "package/found", "version": "dev-foo+bar" }, + { "name": "package/found2", "version": "dev-foo+abcd09832478" }, + { "name": "package/works", "version": "dev-foo+abcd09832478" }, + { "name": "package/works2", "version": "dev-+123" } + ] + } + ], + "require": { + "package/found": "dev-foo#bar", + "package/found2": "dev-foo#abcd09832478", + "package/works": "dev-foo+abcd09832478", + "package/works2": "dev-+123" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires package/found dev-foo#bar, found package/found[dev-foo+bar]. The # character in branch names is replaced by a + character. Make sure to require it as "dev-foo+bar". + Problem 2 + - Root composer.json requires package/found2 dev-foo#abcd09832478, found package/found2[dev-foo+abcd09832478]. The # character in branch names is replaced by a + character. Make sure to require it as "dev-foo+abcd09832478". + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 610911bb98ed..65a28cc028da 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -86,11 +86,11 @@ Test the error output of solver problems. "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- @@ -141,9 +141,9 @@ Your requirements could not be resolved to an installable set of packages. - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). Problem 15 - - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). - - package/found5 2.0.0 requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - Root composer.json requires package/found5 2.* -> satisfiable by package/found5[2.0.0]. + - package/found5 2.0.0 requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). Potential causes: - A typo in the package name diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index ed202350486f..b0e22b17754f 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -22,11 +22,11 @@ Suggestions are not displayed for when not updating the lock file "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- install diff --git a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-matches-default-branch.test b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-matches-default-branch.test index d4a3b4d85aa8..ae1bfdb5cd87 100644 --- a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-matches-default-branch.test +++ b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-matches-default-branch.test @@ -32,8 +32,8 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - conflicter/pkg 1.0.0 conflicts with victim/pkg dev-master. - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0]. - Root composer.json requires victim/pkg * -> satisfiable by victim/pkg[dev-master]. + - conflicter/pkg 1.0.0 conflicts with victim/pkg dev-master. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index 5aceda8ea942..96001fa15ccd 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -52,11 +52,11 @@ update "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Upgrading a/a (dev-master 1234 => dev-master master) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock2.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock2.test index b06ce3119225..bd38a6668a96 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock2.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock2.test @@ -37,7 +37,7 @@ Updating an aliased package where the old alias matches the new package should n "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } @@ -64,11 +64,11 @@ update "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-INSTALLED-- [ diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test index 317f2396ddea..a4c40e3afcf9 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test @@ -40,11 +40,11 @@ Update with a package allow list only updates those packages if they are not pre "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update allowed/pkg dependency/pkg diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-minimal-changes.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-minimal-changes.test index 889555ed0acb..fc3aa0ecf396 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-minimal-changes.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-minimal-changes.test @@ -49,11 +49,11 @@ Updating transitive dependencies only updates what is really required when a min "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update allowed/pkg --with-all-dependencies --minimal-changes diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test index 5c796181a685..9dabdd134e1b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test @@ -51,11 +51,11 @@ Update with a package allow list pattern and all-dependencies flag updates packa "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update allowed/pkg-* --with-all-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test index 5cabcbb4a910..221769085dc4 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test @@ -63,11 +63,11 @@ Update with a package allow list only updates those packages and their dependenc "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update allowed/pkg-* foobar --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test index fde1c9eab9f1..a4314d6cd2ff 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test @@ -54,11 +54,11 @@ Update with a package allow list only updates those corresponding to the pattern "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update vendor/Test* exact/test-package notexact/Test all/* no/reg.?xp diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test index ef7ca56c0c1e..68ae0a53b86c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-reads-lock.test @@ -31,7 +31,7 @@ Limited update takes rules from lock if available, and not from the installed re "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test index 7f2299566ea0..f59c149962a2 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test @@ -35,11 +35,11 @@ Update with a package allow list removes unused packages "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update --with-dependencies allowed/pkg diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test index 163471fa6d66..13e99ad58e60 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -31,11 +31,11 @@ A partial update for a new package yet to be installed should remove another dep "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update new/pkg diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test index 4d07bc245c31..9450c29ae57c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-warns-non-existing-patterns.test @@ -33,11 +33,11 @@ Verify that partial updates warn about using patterns in the argument which have "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update b/b foo/bar baz/* --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test index 5e5f047779f2..182207786be9 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-alias.test @@ -49,11 +49,11 @@ Verify that a partial update with deps correctly keeps track of all aliases. } ], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update new/pkg --with-all-dependencies @@ -87,8 +87,8 @@ update new/pkg --with-all-dependencies }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Marking current/dep2 (1.0.x-dev) as uninstalled, alias of current/dep2 (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test index ff1257498fed..0b4f53f57f8b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-new-requirement.test @@ -36,11 +36,11 @@ When partially updating a package to a newer version and the new version has a n "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update root/pkg2 --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test index a850748c8d2c..b110a178c399 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace-mutual.test @@ -36,11 +36,11 @@ Require a new package in the composer.json and updating with its name as an argu "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update new/pkg --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test index 8bb676e760ae..0f544953796b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new-replace.test @@ -31,11 +31,11 @@ Require a new package in the composer.json and updating with its name as an argu "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update new/pkg --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test index 24eb95538e68..bd0146596db6 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies-require-new.test @@ -35,11 +35,11 @@ Require a new package in the composer.json and updating with its name as an argu "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update new/pkg --with-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list.test b/tests/Composer/Test/Fixtures/installer/update-allow-list.test index 59337544234b..6f7f29dc2505 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list.test @@ -45,11 +45,11 @@ Update with a package allow list only updates those packages listed as command a "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update allowed/pkg diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 15600aefeaa7..c1fe0363a689 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -159,8 +159,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-LOCK-- { @@ -226,8 +226,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update a/a b/b d/d g/g diff --git a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test index 0a918b183c95..77e95a66a216 100644 --- a/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-dev-packages-updates-repo-url.test @@ -59,11 +59,11 @@ Updating dev packages where no reference change happened triggers a repo url cha "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update @@ -86,11 +86,11 @@ update "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Upgrading a/a (dev-master oldmaster => dev-master newmaster) diff --git a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test index edb821f8c077..78a2d60d30af 100644 --- a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test @@ -16,8 +16,9 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "package": [ { "name": "a/a", "version": "dev-master", - "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, - "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "require": { "b/b": "^2.0.1" }, + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git", "mirrors": [{"url": "https://example.org/src/%package%/%version%/r%reference%.%type%", "preferred": true}] }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip", "mirrors": [{"url": "https://example.org/dists/%package%/%version%/r%reference%.%type%", "preferred": true}] }, "time": "2021-03-27T14:32:16+00:00" }, { @@ -67,6 +68,7 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an [ { "name": "a/a", "version": "dev-master", + "require": { "b/b": "^2" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "time": "2021-03-14T16:24:37+00:00" @@ -102,6 +104,7 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "packages": [ { "name": "a/a", "version": "dev-master", + "require": { "b/b": "^2" }, "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "time": "2021-03-14T16:24:37+00:00", @@ -144,16 +147,17 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-LOCK-- { "packages": [ { "name": "a/a", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/newa", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/newa/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "require": { "b/b": "^2" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/newa", "type": "git", "mirrors": [{"url": "https://example.org/src/%package%/%version%/r%reference%.%type%", "preferred": true}] }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/newa/zipball/1111111111111111111111111111111111111111", "type": "zip", "mirrors": [{"url": "https://example.org/dists/%package%/%version%/r%reference%.%type%", "preferred": true}] }, "time": "2021-03-14T16:24:37+00:00", "type": "library" }, @@ -200,8 +204,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update mirrors diff --git a/tests/Composer/Test/Fixtures/installer/update-mirrors-fails-with-new-req.test b/tests/Composer/Test/Fixtures/installer/update-mirrors-fails-with-new-req.test index 0b47f4b5aa12..d057b32e08de 100644 --- a/tests/Composer/Test/Fixtures/installer/update-mirrors-fails-with-new-req.test +++ b/tests/Composer/Test/Fixtures/installer/update-mirrors-fails-with-new-req.test @@ -38,11 +38,11 @@ Update mirrors with a new root require which is not yet in lock should simply ig ], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update mirrors @@ -56,10 +56,10 @@ update mirrors ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index 9844616f412b..16a40e13be6b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -54,8 +54,8 @@ Updates with --no-dev but we still end up with a complete lock file including de }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update --no-dev diff --git a/tests/Composer/Test/Fixtures/installer/update-no-install.test b/tests/Composer/Test/Fixtures/installer/update-no-install.test index e1b6f4954938..1fe7c0af09c8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-install.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-install.test @@ -47,11 +47,11 @@ update --no-install ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-INSTALLED-- diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-at-all-in-remote.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-at-all-in-remote.test index c98d34dc6efe..5853fd782622 100644 --- a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-at-all-in-remote.test +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-at-all-in-remote.test @@ -28,11 +28,11 @@ Update package which is in lock file but not in remote repo at all should show t "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update main/dep --with-all-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote-due-to-min-stability.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote-due-to-min-stability.test index c5582a0e3a55..3816ff647e37 100644 --- a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote-due-to-min-stability.test +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote-due-to-min-stability.test @@ -30,11 +30,11 @@ Update package which is in lock file but not remote repo due to min-stability sh "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update main/dep --with-all-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote.test index 6ff3f6f5b7d8..9bafe3a7074a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote.test +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lock-but-not-in-remote.test @@ -29,11 +29,11 @@ Update package which is in lock file but not in remote repo in the correct versi "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update main/dep --with-all-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lower-repo-prio-but-not-main-due-to-min-stability.test b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lower-repo-prio-but-not-main-due-to-min-stability.test index 263282e78a57..be20fd2000ce 100644 --- a/tests/Composer/Test/Fixtures/installer/update-package-present-in-lower-repo-prio-but-not-main-due-to-min-stability.test +++ b/tests/Composer/Test/Fixtures/installer/update-package-present-in-lower-repo-prio-but-not-main-due-to-min-stability.test @@ -33,11 +33,11 @@ Update package which is in lower prio repo but not main repo due to min-stabilit "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update main/dep --with-all-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index c1e74635630f..3558a785e750 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -35,11 +35,11 @@ Converting from one VCS type to another (including an URL change) should update "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update mirrors @@ -55,11 +55,11 @@ update mirrors "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Upgrading a/a (1.0.0 old-hg-ref => 1.0.0 new-git-ref) diff --git a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test index a47afb7fff54..f769413570fa 100644 --- a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test +++ b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test @@ -24,11 +24,11 @@ A composer update should remove unused locked dependencies from the lock file an "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --INSTALLED-- [ @@ -46,11 +46,11 @@ update "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-OUTPUT-- Loading composer repositories with package information diff --git a/tests/Composer/Test/Fixtures/installer/update-syncs-outdated.test b/tests/Composer/Test/Fixtures/installer/update-syncs-outdated.test index acef95924122..17120249270a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-syncs-outdated.test +++ b/tests/Composer/Test/Fixtures/installer/update-syncs-outdated.test @@ -34,11 +34,11 @@ Update updates the outdated state of packages "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test index e9892c3b6329..5f0d0aed1046 100644 --- a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-blank.test @@ -11,10 +11,10 @@ update "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test index 1a2227cec603..29e035ba0ead 100644 --- a/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test +++ b/tests/Composer/Test/Fixtures/installer/update-to-empty-from-locked.test @@ -25,11 +25,11 @@ Update to a state without dependency works well from locked with dependency "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update @@ -39,11 +39,11 @@ update "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Removing a/a (dev-master 1234) diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index e9fc88ef9777..2a0cc5222967 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -1,6 +1,6 @@ --TEST-- -See Github issue #6661 ( github.com/composer/composer/issues/6661 ). +See GitHub issue #6661 ( github.com/composer/composer/issues/6661 ). When `--with-all-dependencies` is used, Composer should update the dependencies of all allowed packages, even if the dependency is a root requirement. @@ -37,11 +37,11 @@ When `--with-all-dependencies` is used, Composer should update the dependencies "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --RUN-- update b/b --with-all-dependencies diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index 545f92fe9a0a..952c9534a9d9 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -20,7 +20,7 @@ Installing locked dev packages should remove old dependencies "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false } diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index c84ab3525f4a..a915cd63fb13 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -61,8 +61,8 @@ update "stability-flags": {"a/a":20}, "prefer-stable": false, "prefer-lowest": false, - "platform": [], - "platform-dev": [] + "platform": {}, + "platform-dev": {} } --EXPECT-- Upgrading a/a (dev-master oldref => dev-master newref) diff --git a/tests/Composer/Test/IO/BufferIOTest.php b/tests/Composer/Test/IO/BufferIOTest.php index 0cf9796772f5..9ee7d7f0ec62 100644 --- a/tests/Composer/Test/IO/BufferIOTest.php +++ b/tests/Composer/Test/IO/BufferIOTest.php @@ -37,8 +37,8 @@ public function testSetUserInputs(): void '', ]); - $this->assertTrue($bufferIO->askConfirmation('Please say yes!', false)); - $this->assertFalse($bufferIO->askConfirmation('Now please say no!', true)); - $this->assertSame('default', $bufferIO->ask('Empty string last', 'default')); + self::assertTrue($bufferIO->askConfirmation('Please say yes!', false)); + self::assertFalse($bufferIO->askConfirmation('Now please say no!', true)); + self::assertSame('default', $bufferIO->ask('Empty string last', 'default')); } } diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index 2d152eb616ed..26f04e39f50e 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -34,8 +34,8 @@ public function testIsInteractive(): void $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock); - $this->assertTrue($consoleIO->isInteractive()); - $this->assertFalse($consoleIO->isInteractive()); + self::assertTrue($consoleIO->isInteractive()); + self::assertFalse($consoleIO->isInteractive()); } public function testWrite(): void @@ -127,7 +127,7 @@ public function testOverwrite(): void } if (count($series) > 0) { - $this->assertSame(array_shift($series), [$args[0], $args[1]]); + self::assertSame(array_shift($series), [$args[0], $args[1]]); } }); @@ -252,10 +252,10 @@ public function testSelect(): void $consoleIO = new ConsoleIO($inputMock, $outputMock, $setMock); $result = $consoleIO->select('Select item', ["item1", "item2"], 'item1', false, "Error message", true); - $this->assertEquals(['1'], $result); + self::assertEquals(['1'], $result); } - public function testSetAndgetAuthentication(): void + public function testSetAndGetAuthentication(): void { $inputMock = $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(); $outputMock = $this->getMockBuilder('Symfony\Component\Console\Output\OutputInterface')->getMock(); @@ -264,7 +264,7 @@ public function testSetAndgetAuthentication(): void $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock); $consoleIO->setAuthentication('repoName', 'l3l0', 'passwd'); - $this->assertEquals( + self::assertEquals( ['username' => 'l3l0', 'password' => 'passwd'], $consoleIO->getAuthentication('repoName') ); @@ -278,7 +278,7 @@ public function testGetAuthenticationWhenDidNotSet(): void $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock); - $this->assertEquals( + self::assertEquals( ['username' => null, 'password' => null], $consoleIO->getAuthentication('repoName') ); @@ -293,7 +293,7 @@ public function testHasAuthentication(): void $consoleIO = new ConsoleIO($inputMock, $outputMock, $helperMock); $consoleIO->setAuthentication('repoName', 'l3l0', 'passwd'); - $this->assertTrue($consoleIO->hasAuthentication('repoName')); - $this->assertFalse($consoleIO->hasAuthentication('repoName2')); + self::assertTrue($consoleIO->hasAuthentication('repoName')); + self::assertFalse($consoleIO->hasAuthentication('repoName2')); } } diff --git a/tests/Composer/Test/IO/NullIOTest.php b/tests/Composer/Test/IO/NullIOTest.php index 9aa6bc6a91b2..c33a372bf1a5 100644 --- a/tests/Composer/Test/IO/NullIOTest.php +++ b/tests/Composer/Test/IO/NullIOTest.php @@ -21,51 +21,51 @@ public function testIsInteractive(): void { $io = new NullIO(); - $this->assertFalse($io->isInteractive()); + self::assertFalse($io->isInteractive()); } - public function testhasAuthentication(): void + public function testHasAuthentication(): void { $io = new NullIO(); - $this->assertFalse($io->hasAuthentication('foo')); + self::assertFalse($io->hasAuthentication('foo')); } public function testAskAndHideAnswer(): void { $io = new NullIO(); - $this->assertNull($io->askAndHideAnswer('foo')); + self::assertNull($io->askAndHideAnswer('foo')); } - public function testgetAuthentications(): void + public function testGetAuthentications(): void { $io = new NullIO(); - $this->assertIsArray($io->getAuthentications()); // @phpstan-ignore-line - $this->assertEmpty($io->getAuthentications()); - $this->assertEquals(['username' => null, 'password' => null], $io->getAuthentication('foo')); + self::assertIsArray($io->getAuthentications()); + self::assertEmpty($io->getAuthentications()); + self::assertEquals(['username' => null, 'password' => null], $io->getAuthentication('foo')); } public function testAsk(): void { $io = new NullIO(); - $this->assertEquals('foo', $io->ask('bar', 'foo')); + self::assertEquals('foo', $io->ask('bar', 'foo')); } public function testAskConfirmation(): void { $io = new NullIO(); - $this->assertEquals(false, $io->askConfirmation('bar', false)); + self::assertFalse($io->askConfirmation('bar', false)); } public function testAskAndValidate(): void { $io = new NullIO(); - $this->assertEquals('foo', $io->askAndValidate('question', static function ($x): bool { + self::assertEquals('foo', $io->askAndValidate('question', static function ($x): bool { return true; }, null, 'foo')); } @@ -74,6 +74,6 @@ public function testSelect(): void { $io = new NullIO(); - $this->assertEquals('1', $io->select('question', ['item1', 'item2'], '1', 2, 'foo', true)); + self::assertEquals('1', $io->select('question', ['item1', 'item2'], '1', 2, 'foo', true)); } } diff --git a/tests/Composer/Test/InstalledVersionsTest.php b/tests/Composer/Test/InstalledVersionsTest.php index 3fa9b1bc1fc5..fdcf28fbba70 100644 --- a/tests/Composer/Test/InstalledVersionsTest.php +++ b/tests/Composer/Test/InstalledVersionsTest.php @@ -41,7 +41,7 @@ public static function tearDownAfterClass(): void $prop = new \ReflectionProperty('Composer\Autoload\ClassLoader', 'registeredLoaders'); $prop->setAccessible(true); $prop->setValue(null, self::$previousRegisteredLoaders); - InstalledVersions::reload(null); // @phpstan-ignore-line + InstalledVersions::reload(null); // @phpstan-ignore argument.type } public function setUp(): void @@ -65,7 +65,7 @@ public function testGetInstalledPackages(): void 'foo/replaced', 'meta/package', ]; - $this->assertSame($names, InstalledVersions::getInstalledPackages()); + self::assertSame($names, InstalledVersions::getInstalledPackages()); } /** @@ -73,7 +73,7 @@ public function testGetInstalledPackages(): void */ public function testIsInstalled(bool $expected, string $name, bool $includeDevRequirements = true): void { - $this->assertSame($expected, InstalledVersions::isInstalled($name, $includeDevRequirements)); + self::assertSame($expected, InstalledVersions::isInstalled($name, $includeDevRequirements)); } public static function isInstalledProvider(): array @@ -95,7 +95,7 @@ public static function isInstalledProvider(): array */ public function testSatisfies(bool $expected, string $name, string $constraint): void { - $this->assertSame($expected, InstalledVersions::satisfies(new VersionParser, $name, $constraint)); + self::assertSame($expected, InstalledVersions::satisfies(new VersionParser, $name, $constraint)); } public static function satisfiesProvider(): array @@ -133,7 +133,7 @@ public static function satisfiesProvider(): array */ public function testGetVersionRanges(string $expected, string $name): void { - $this->assertSame($expected, InstalledVersions::getVersionRanges($name)); + self::assertSame($expected, InstalledVersions::getVersionRanges($name)); } public static function getVersionRangesProvider(): array @@ -155,7 +155,7 @@ public static function getVersionRangesProvider(): array */ public function testGetVersion(?string $expected, string $name): void { - $this->assertSame($expected, InstalledVersions::getVersion($name)); + self::assertSame($expected, InstalledVersions::getVersion($name)); } public static function getVersionProvider(): array @@ -177,7 +177,7 @@ public static function getVersionProvider(): array */ public function testGetPrettyVersion(?string $expected, string $name): void { - $this->assertSame($expected, InstalledVersions::getPrettyVersion($name)); + self::assertSame($expected, InstalledVersions::getPrettyVersion($name)); } public static function getPrettyVersionProvider(): array @@ -202,7 +202,7 @@ public function testGetVersionOutOfBounds(): void public function testGetRootPackage(): void { - $this->assertSame([ + self::assertSame([ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', @@ -222,7 +222,7 @@ public function testGetRootPackage(): void public function testGetRawData(): void { $dir = $this->root; - $this->assertSame(require __DIR__.'/Repository/Fixtures/installed_relative.php', InstalledVersions::getRawData()); + self::assertSame(require __DIR__.'/Repository/Fixtures/installed_relative.php', InstalledVersions::getRawData()); } /** @@ -230,7 +230,7 @@ public function testGetRawData(): void */ public function testGetReference(?string $expected, string $name): void { - $this->assertSame($expected, InstalledVersions::getReference($name)); + self::assertSame($expected, InstalledVersions::getReference($name)); } public static function getReferenceProvider(): array @@ -257,13 +257,13 @@ public function testGetInstalledPackagesByType(): void 'c/c', ]; - $this->assertSame($names, \Composer\InstalledVersions::getInstalledPackagesByType('library')); + self::assertSame($names, \Composer\InstalledVersions::getInstalledPackagesByType('library')); } public function testGetInstallPath(): void { - $this->assertSame(realpath($this->root), realpath(\Composer\InstalledVersions::getInstallPath('__root__'))); - $this->assertSame('/foo/bar/vendor/c/c', \Composer\InstalledVersions::getInstallPath('c/c')); - $this->assertNull(\Composer\InstalledVersions::getInstallPath('foo/impl')); + self::assertSame(realpath($this->root), realpath(\Composer\InstalledVersions::getInstallPath('__root__'))); + self::assertSame('/foo/bar/vendor/c/c', \Composer\InstalledVersions::getInstallPath('c/c')); + self::assertNull(\Composer\InstalledVersions::getInstallPath('foo/impl')); } } diff --git a/tests/Composer/Test/Installer/BinaryInstallerTest.php b/tests/Composer/Test/Installer/BinaryInstallerTest.php index 8b6d472435e1..e613ca7e1f63 100644 --- a/tests/Composer/Test/Installer/BinaryInstallerTest.php +++ b/tests/Composer/Test/Installer/BinaryInstallerTest.php @@ -82,8 +82,8 @@ public function testInstallAndExecBinaryWithFullCompat(string $contents): void $proc = new ProcessExecutor(); $proc->execute($this->binDir.'/binary arg', $output); - $this->assertEquals('', $proc->getErrorOutput()); - $this->assertEquals('success arg', $output); + self::assertEquals('', $proc->getErrorOutput()); + self::assertEquals('success arg', $output); } public static function executableBinaryProvider(): array @@ -121,7 +121,7 @@ public static function executableBinaryProvider(): array protected function createPackageMock() { return $this->getMockBuilder('Composer\Package\Package') - ->setConstructorArgs([md5((string) mt_rand()), '1.0.0.0', '1.0.0']) + ->setConstructorArgs([bin2hex(random_bytes(5)), '1.0.0.0', '1.0.0']) ->getMock(); } } diff --git a/tests/Composer/Test/Installer/InstallationManagerTest.php b/tests/Composer/Test/Installer/InstallationManagerTest.php index d1c7746d0319..f253176bb28d 100644 --- a/tests/Composer/Test/Installer/InstallationManagerTest.php +++ b/tests/Composer/Test/Installer/InstallationManagerTest.php @@ -57,7 +57,7 @@ public function testAddGetInstaller(): void $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); - $this->assertSame($installer, $manager->getInstaller('vendor')); + self::assertSame($installer, $manager->getInstaller('vendor')); self::expectException('InvalidArgumentException'); $manager->getInstaller('unregistered'); @@ -86,11 +86,11 @@ public function testAddRemoveInstaller(): void $manager = new InstallationManager($this->loop, $this->io); $manager->addInstaller($installer); - $this->assertSame($installer, $manager->getInstaller('vendor')); + self::assertSame($installer, $manager->getInstaller('vendor')); $manager->addInstaller($installer2); - $this->assertSame($installer2, $manager->getInstaller('vendor')); + self::assertSame($installer2, $manager->getInstaller('vendor')); $manager->removeInstaller($installer2); - $this->assertSame($installer, $manager->getInstaller('vendor')); + self::assertSame($installer, $manager->getInstaller('vendor')); } public function testExecute(): void diff --git a/tests/Composer/Test/Installer/InstallerEventTest.php b/tests/Composer/Test/Installer/InstallerEventTest.php index 834f7ac7b0fd..de87ba7b8bdf 100644 --- a/tests/Composer/Test/Installer/InstallerEventTest.php +++ b/tests/Composer/Test/Installer/InstallerEventTest.php @@ -24,11 +24,9 @@ public function testGetter(): void $transaction = $this->getMockBuilder('Composer\DependencyResolver\LockTransaction')->disableOriginalConstructor()->getMock(); $event = new InstallerEvent('EVENT_NAME', $composer, $io, true, true, $transaction); - $this->assertSame('EVENT_NAME', $event->getName()); - $this->assertInstanceOf('Composer\Composer', $event->getComposer()); - $this->assertInstanceOf('Composer\IO\IOInterface', $event->getIO()); - $this->assertTrue($event->isDevMode()); - $this->assertTrue($event->isExecutingOperations()); - $this->assertInstanceOf('Composer\DependencyResolver\Transaction', $event->getTransaction()); + self::assertSame('EVENT_NAME', $event->getName()); + self::assertTrue($event->isDevMode()); + self::assertTrue($event->isExecutingOperations()); + self::assertInstanceOf('Composer\DependencyResolver\Transaction', $event->getTransaction()); } } diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index 3161687de278..aa63eb2241ac 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -108,7 +108,7 @@ public function testInstallerCreationShouldNotCreateVendorDirectory(): void $this->fs->removeDirectory($this->vendorDir); new LibraryInstaller($this->io, $this->composer); - $this->assertFileDoesNotExist($this->vendorDir); + self::assertFileDoesNotExist($this->vendorDir); } public function testInstallerCreationShouldNotCreateBinDirectory(): void @@ -116,7 +116,7 @@ public function testInstallerCreationShouldNotCreateBinDirectory(): void $this->fs->removeDirectory($this->binDir); new LibraryInstaller($this->io, $this->composer); - $this->assertFileDoesNotExist($this->binDir); + self::assertFileDoesNotExist($this->binDir); } public function testIsInstalled(): void @@ -125,18 +125,18 @@ public function testIsInstalled(): void $package = self::getPackage('test/pkg', '1.0.0'); $repository = new InstalledArrayRepository(); - $this->assertFalse($library->isInstalled($repository, $package)); + self::assertFalse($library->isInstalled($repository, $package)); // package being in repo is not enough to be installed $repository->addPackage($package); - $this->assertFalse($library->isInstalled($repository, $package)); + self::assertFalse($library->isInstalled($repository, $package)); // package being in repo and vendor/pkg/foo dir present means it is seen as installed self::ensureDirectoryExistsAndClear($this->vendorDir.'/'.$package->getPrettyName()); - $this->assertTrue($library->isInstalled($repository, $package)); + self::assertTrue($library->isInstalled($repository, $package)); $repository->removePackage($package); - $this->assertFalse($library->isInstalled($repository, $package)); + self::assertFalse($library->isInstalled($repository, $package)); } /** @@ -160,8 +160,8 @@ public function testInstall(): void ->with($package); $library->install($this->repository, $package); - $this->assertFileExists($this->vendorDir, 'Vendor dir should be created'); - $this->assertFileExists($this->binDir, 'Bin dir should be created'); + self::assertFileExists($this->vendorDir, 'Vendor dir should be created'); + self::assertFileExists($this->binDir, 'Bin dir should be created'); } /** @@ -206,8 +206,8 @@ public function testUpdate(): void $library = new LibraryInstaller($this->io, $this->composer, 'library', $filesystem); $library->update($this->repository, $initial, $target); - $this->assertFileExists($this->vendorDir, 'Vendor dir should be created'); - $this->assertFileExists($this->binDir, 'Bin dir should be created'); + self::assertFileExists($this->vendorDir, 'Vendor dir should be created'); + self::assertFileExists($this->binDir, 'Bin dir should be created'); self::expectException('InvalidArgumentException'); @@ -248,7 +248,7 @@ public function testGetInstallPathWithoutTargetDir(): void $library = new LibraryInstaller($this->io, $this->composer); $package = self::getPackage('Vendor/Pkg', '1.0.0'); - $this->assertEquals($this->vendorDir.'/'.$package->getPrettyName(), $library->getInstallPath($package)); + self::assertEquals($this->vendorDir.'/'.$package->getPrettyName(), $library->getInstallPath($package)); } public function testGetInstallPathWithTargetDir(): void @@ -257,7 +257,7 @@ public function testGetInstallPathWithTargetDir(): void $package = self::getPackage('Foo/Bar', '1.0.0'); $package->setTargetDir('Some/Namespace'); - $this->assertEquals($this->vendorDir.'/'.$package->getPrettyName().'/Some/Namespace', $library->getInstallPath($package)); + self::assertEquals($this->vendorDir.'/'.$package->getPrettyName().'/Some/Namespace', $library->getInstallPath($package)); } /** diff --git a/tests/Composer/Test/Installer/MetapackageInstallerTest.php b/tests/Composer/Test/Installer/MetapackageInstallerTest.php index 83bc2704feab..6f083dd4b1c7 100644 --- a/tests/Composer/Test/Installer/MetapackageInstallerTest.php +++ b/tests/Composer/Test/Installer/MetapackageInstallerTest.php @@ -113,7 +113,7 @@ public function testUninstall(): void private function createPackageMock() { return $this->getMockBuilder('Composer\Package\Package') - ->setConstructorArgs([md5((string) mt_rand()), '1.0.0.0', '1.0.0']) + ->setConstructorArgs([bin2hex(random_bytes(5)), '1.0.0.0', '1.0.0']) ->getMock(); } } diff --git a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php index a9969c4f4cc8..d2d5df994718 100644 --- a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php +++ b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php @@ -56,7 +56,7 @@ public function testConstructor(): void */ public function testGetPackagesEmptyByDefault(): void { - $this->assertEmpty($this->suggestedPackagesReporter->getPackages()); + self::assertEmpty($this->suggestedPackagesReporter->getPackages()); } /** @@ -71,7 +71,7 @@ public function testGetPackages(): void $suggestedPackage['target'], $suggestedPackage['reason'] ); - $this->assertSame( + self::assertSame( [$suggestedPackage], $this->suggestedPackagesReporter->getPackages() ); @@ -99,7 +99,7 @@ public function testAddPackageAppends(): void $suggestedPackageB['target'], $suggestedPackageB['reason'] ); - $this->assertSame( + self::assertSame( [$suggestedPackageA, $suggestedPackageB], $this->suggestedPackagesReporter->getPackages() ); @@ -122,7 +122,7 @@ public function testAddSuggestionsFromPackage(): void ->will($this->returnValue('package-pretty-name')); $this->suggestedPackagesReporter->addSuggestionsFromPackage($package); - $this->assertSame([ + self::assertSame([ [ 'source' => 'package-pretty-name', 'target' => 'target-a', @@ -272,7 +272,7 @@ private function getSuggestedPackageArray(): array private function createPackageMock() { return $this->getMockBuilder('Composer\Package\Package') - ->setConstructorArgs([md5((string) mt_rand()), '1.0.0.0', '1.0.0']) + ->setConstructorArgs([bin2hex(random_bytes(5)), '1.0.0.0', '1.0.0']) ->getMock(); } } diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 858d88e7c03c..64a11cd4988f 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -138,20 +138,20 @@ public function testInstaller(RootPackageInterface $rootPackage, array $reposito $result = $installer->run(); $output = str_replace("\r", '', $io->getOutput()); - $this->assertEquals(0, $result, $output); + self::assertEquals(0, $result, $output); $expectedInstalled = $options['install'] ?? []; $expectedUpdated = $options['update'] ?? []; $expectedUninstalled = $options['uninstall'] ?? []; $installed = $installationManager->getInstalledPackages(); - $this->assertEquals($this->makePackagesComparable($expectedInstalled), $this->makePackagesComparable($installed)); + self::assertEquals($this->makePackagesComparable($expectedInstalled), $this->makePackagesComparable($installed)); $updated = $installationManager->getUpdatedPackages(); - $this->assertSame($expectedUpdated, $updated); + self::assertSame($expectedUpdated, $updated); $uninstalled = $installationManager->getUninstalledPackages(); - $this->assertSame($expectedUninstalled, $uninstalled); + self::assertSame($expectedUninstalled, $uninstalled); } /** @@ -279,7 +279,7 @@ private function doTestIntegration(string $file, string $message, ?string $condi { if ($condition) { eval('$res = '.$condition.';'); - if (!$res) { // @phpstan-ignore-line + if (!$res) { // @phpstan-ignore variable.undefined $this->markTestSkipped($condition); } } @@ -441,10 +441,15 @@ private function doTestIntegration(string $file, string $message, ?string $condi } $output = str_replace("\r", '', $io->getOutput()); - $this->assertEquals($expectResult, $result, $output . stream_get_contents($appOutput)); + self::assertEquals($expectResult, $result, $output . stream_get_contents($appOutput)); if ($expectLock && isset($actualLock)) { unset($actualLock['hash'], $actualLock['content-hash'], $actualLock['_readme'], $actualLock['plugin-api-version']); - $this->assertEquals($expectLock, $actualLock); + foreach (['stability-flags', 'platform', 'platform-dev'] as $key) { + if ($expectLock[$key] === []) { + $expectLock[$key] = new \stdClass; + } + } + self::assertEquals($expectLock, $actualLock); } if ($expectInstalled !== null) { @@ -461,18 +466,18 @@ private function doTestIntegration(string $file, string $message, ?string $condi return strcmp($a['name'], $b['name']); }); - $this->assertSame($expectInstalled, $actualInstalled); + self::assertSame($expectInstalled, $actualInstalled); } /** @var InstallationManagerMock $installationManager */ $installationManager = $composer->getInstallationManager(); - $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); + self::assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); if ($expectOutput) { $output = Preg::replace('{^ - .*?\.ini$}m', '__inilist__', $output); $output = Preg::replace('{(__inilist__\r?\n)+}', "__inilist__\n", $output); - $this->assertStringMatchesFormat(rtrim($expectOutput), rtrim($output)); + self::assertStringMatchesFormat(rtrim($expectOutput), rtrim($output)); } } @@ -533,7 +538,7 @@ public static function loadIntegrationTests(string $path): array if (!empty($testData['LOCK'])) { $lock = JsonFile::parseJson($testData['LOCK']); if (!isset($lock['hash'])) { - $lock['hash'] = md5(JsonFile::encode($composer, 0)); + $lock['hash'] = hash('md5', JsonFile::encode($composer, 0)); } } if (!empty($testData['INSTALLED'])) { diff --git a/tests/Composer/Test/Json/ComposerSchemaTest.php b/tests/Composer/Test/Json/ComposerSchemaTest.php index 9a673509102c..4a2acee3db04 100644 --- a/tests/Composer/Test/Json/ComposerSchemaTest.php +++ b/tests/Composer/Test/Json/ComposerSchemaTest.php @@ -33,21 +33,21 @@ public function testNamePattern(): void ]; $json = '{"name": "vendor/-pack__age", "description": "description"}'; - $this->assertEquals($expectedError, $this->check($json)); + self::assertEquals($expectedError, $this->check($json)); $json = '{"name": "Vendor/Package", "description": "description"}'; - $this->assertEquals($expectedError, $this->check($json)); + self::assertEquals($expectedError, $this->check($json)); } public function testOptionalAbandonedProperty(): void { $json = '{"name": "vendor/package", "description": "description", "abandoned": true}'; - $this->assertTrue($this->check($json)); + self::assertTrue($this->check($json)); } public function testRequireTypes(): void { $json = '{"name": "vendor/package", "description": "description", "require": {"a": ["b"]} }'; - $this->assertEquals([ + self::assertEquals([ ['property' => 'require.a', 'message' => 'Array value found, but a string is required', 'constraint' => 'type'], ], $this->check($json)); } @@ -64,31 +64,31 @@ public function testMinimumStabilityValues(): void ]; $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "" }'; - $this->assertEquals($expectedError, $this->check($json), 'empty string'); + self::assertEquals($expectedError, $this->check($json), 'empty string'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "dummy" }'; - $this->assertEquals($expectedError, $this->check($json), 'dummy'); + self::assertEquals($expectedError, $this->check($json), 'dummy'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "devz" }'; - $this->assertEquals($expectedError, $this->check($json), 'devz'); + self::assertEquals($expectedError, $this->check($json), 'devz'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "dev" }'; - $this->assertTrue($this->check($json), 'dev'); + self::assertTrue($this->check($json), 'dev'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "alpha" }'; - $this->assertTrue($this->check($json), 'alpha'); + self::assertTrue($this->check($json), 'alpha'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "beta" }'; - $this->assertTrue($this->check($json), 'beta'); + self::assertTrue($this->check($json), 'beta'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "rc" }'; - $this->assertTrue($this->check($json), 'rc lowercase'); + self::assertTrue($this->check($json), 'rc lowercase'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "RC" }'; - $this->assertTrue($this->check($json), 'rc uppercase'); + self::assertTrue($this->check($json), 'rc uppercase'); $json = '{ "name": "vendor/package", "description": "generic description", "minimum-stability": "stable" }'; - $this->assertTrue($this->check($json), 'stable'); + self::assertTrue($this->check($json), 'stable'); } /** diff --git a/tests/Composer/Test/Json/JsonFileTest.php b/tests/Composer/Test/Json/JsonFileTest.php index 74b124ec3891..ed7623613252 100644 --- a/tests/Composer/Test/Json/JsonFileTest.php +++ b/tests/Composer/Test/Json/JsonFileTest.php @@ -110,15 +110,15 @@ public function testSchemaValidationError(): void $json->validateSchema(); $this->fail('Expected exception to be thrown (strict)'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); - $this->assertContains($expectedError, $e->getErrors()); + self::assertEquals($expectedMessage, $e->getMessage()); + self::assertContains($expectedError, $e->getErrors()); } try { $json->validateSchema(JsonFile::LAX_SCHEMA); $this->fail('Expected exception to be thrown (lax)'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); - $this->assertContains($expectedError, $e->getErrors()); + self::assertEquals($expectedMessage, $e->getMessage()); + self::assertContains($expectedError, $e->getErrors()); } unlink($file); } @@ -132,8 +132,8 @@ public function testSchemaValidationLaxAdditionalProperties(): void $json->validateSchema(); $this->fail('Expected exception to be thrown (strict)'); } catch (JsonValidationException $e) { - $this->assertEquals(sprintf('"%s" does not match the expected JSON schema', $file), $e->getMessage()); - $this->assertEquals(['The property foo is not defined and the definition does not allow additional properties'], $e->getErrors()); + self::assertEquals(sprintf('"%s" does not match the expected JSON schema', $file), $e->getMessage()); + self::assertEquals(['The property foo is not defined and the definition does not allow additional properties'], $e->getErrors()); } $json->validateSchema(JsonFile::LAX_SCHEMA); unlink($file); @@ -151,10 +151,10 @@ public function testSchemaValidationLaxRequired(): void $json->validateSchema(); $this->fail('Expected exception to be thrown (strict)'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); + self::assertEquals($expectedMessage, $e->getMessage()); $errors = $e->getErrors(); - $this->assertContains('name : The property name is required', $errors); - $this->assertContains('description : The property description is required', $errors); + self::assertContains('name : The property name is required', $errors); + self::assertContains('description : The property description is required', $errors); } $json->validateSchema(JsonFile::LAX_SCHEMA); @@ -163,8 +163,8 @@ public function testSchemaValidationLaxRequired(): void $json->validateSchema(); $this->fail('Expected exception to be thrown (strict)'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); - $this->assertEquals(['description : The property description is required'], $e->getErrors()); + self::assertEquals($expectedMessage, $e->getMessage()); + self::assertEquals(['description : The property description is required'], $e->getErrors()); } $json->validateSchema(JsonFile::LAX_SCHEMA); @@ -173,8 +173,8 @@ public function testSchemaValidationLaxRequired(): void $json->validateSchema(); $this->fail('Expected exception to be thrown (strict)'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); - $this->assertEquals(['name : The property name is required'], $e->getErrors()); + self::assertEquals($expectedMessage, $e->getMessage()); + self::assertEquals(['name : The property name is required'], $e->getErrors()); } $json->validateSchema(JsonFile::LAX_SCHEMA); @@ -183,10 +183,10 @@ public function testSchemaValidationLaxRequired(): void $json->validateSchema(); $this->fail('Expected exception to be thrown (strict)'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); + self::assertEquals($expectedMessage, $e->getMessage()); $errors = $e->getErrors(); - $this->assertContains('name : The property name is required', $errors); - $this->assertContains('description : The property description is required', $errors); + self::assertContains('name : The property name is required', $errors); + self::assertContains('description : The property description is required', $errors); } $json->validateSchema(JsonFile::LAX_SCHEMA); @@ -195,10 +195,10 @@ public function testSchemaValidationLaxRequired(): void $json->validateSchema(); $this->fail('Expected exception to be thrown (strict)'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); + self::assertEquals($expectedMessage, $e->getMessage()); $errors = $e->getErrors(); - $this->assertContains('name : The property name is required', $errors); - $this->assertContains('description : The property description is required', $errors); + self::assertContains('name : The property name is required', $errors); + self::assertContains('description : The property description is required', $errors); } $json->validateSchema(JsonFile::LAX_SCHEMA); @@ -252,8 +252,8 @@ public function testAuthSchemaValidationWithCustomDataSource(): void JsonFile::validateJsonSchema('COMPOSER_AUTH', $json, JsonFile::AUTH_SCHEMA); $this->fail('Expected exception to be thrown'); } catch (JsonValidationException $e) { - $this->assertEquals($expectedMessage, $e->getMessage()); - $this->assertSame([$expectedError], $e->getErrors()); + self::assertEquals($expectedMessage, $e->getMessage()); + self::assertSame([$expectedError], $e->getErrors()); } } @@ -282,7 +282,7 @@ public function testSimpleJsonString(): void $json = '{ "name": "composer/composer" }'; - $this->assertJsonFormat($json, $data); + self::assertJsonFormat($json, $data); } public function testTrailingBackslash(): void @@ -291,7 +291,7 @@ public function testTrailingBackslash(): void $json = '{ "Metadata\\\\": "src/" }'; - $this->assertJsonFormat($json, $data); + self::assertJsonFormat($json, $data); } public function testFormatEmptyArray(): void @@ -301,7 +301,7 @@ public function testFormatEmptyArray(): void "test": [], "test2": {} }'; - $this->assertJsonFormat($json, $data); + self::assertJsonFormat($json, $data); } public function testEscape(): void @@ -311,7 +311,7 @@ public function testEscape(): void "Metadata\\\\\\"": "src/" }'; - $this->assertJsonFormat($json, $data); + self::assertJsonFormat($json, $data); } public function testUnicode(): void @@ -321,35 +321,35 @@ public function testUnicode(): void "Žluťoučký \" kůň": "úpěl ďábelské ódy za €" }'; - $this->assertJsonFormat($json, $data); + self::assertJsonFormat($json, $data); } public function testOnlyUnicode(): void { $data = "\\/ƌ"; - $this->assertJsonFormat('"\\\\\\/ƌ"', $data, JSON_UNESCAPED_UNICODE); + self::assertJsonFormat('"\\\\\\/ƌ"', $data, JSON_UNESCAPED_UNICODE); } public function testEscapedSlashes(): void { $data = "\\/foo"; - $this->assertJsonFormat('"\\\\\\/foo"', $data, 0); + self::assertJsonFormat('"\\\\\\/foo"', $data, 0); } public function testEscapedBackslashes(): void { $data = "a\\b"; - $this->assertJsonFormat('"a\\\\b"', $data, 0); + self::assertJsonFormat('"a\\\\b"', $data, 0); } public function testEscapedUnicode(): void { $data = "ƌ"; - $this->assertJsonFormat('"\\u018c"', $data, 0); + self::assertJsonFormat('"\\u018c"', $data, 0); } public function testDoubleEscapedUnicode(): void @@ -361,7 +361,7 @@ public function testDoubleEscapedUnicode(): void $decodedData = json_decode($doubleEncodedData, true); $doubleData = json_decode($decodedData['t'], true); - $this->assertEquals($data, $doubleData); + self::assertEquals($data, $doubleData); } public function testPreserveIndentationAfterRead(): void @@ -393,7 +393,7 @@ private function expectParseException(string $text, string $json): void $result = JsonFile::parseJson($json); $this->fail(sprintf("Parsing should have failed but didn't.\nExpected:\n\"%s\"\nFor:\n\"%s\"\nGot:\n\"%s\"", $text, $json, var_export($result, true))); } catch (ParsingException $e) { - $this->assertStringContainsString($text, $e->getMessage()); + self::assertStringContainsString($text, $e->getMessage()); } } @@ -406,9 +406,9 @@ private function assertJsonFormat(string $json, $data, ?int $options = null): vo $json = str_replace("\r", '', $json); if (null === $options) { - $this->assertEquals($json, $file->encode($data)); + self::assertEquals($json, $file->encode($data)); } else { - $this->assertEquals($json, $file->encode($data, $options)); + self::assertEquals($json, $file->encode($data, $options)); } } } diff --git a/tests/Composer/Test/Json/JsonFormatterTest.php b/tests/Composer/Test/Json/JsonFormatterTest.php index 88f76dfece5d..44ca8d2cf78d 100644 --- a/tests/Composer/Test/Json/JsonFormatterTest.php +++ b/tests/Composer/Test/Json/JsonFormatterTest.php @@ -29,8 +29,8 @@ public function testUnicodeWithPrependedSlash(): void $backslash = chr(92); $data = '"' . $backslash . $backslash . $backslash . 'u0119"'; $expected = '"' . $backslash . $backslash . 'ę"'; - /** @phpstan-ignore-next-line */ - $this->assertEquals($expected, JsonFormatter::format($data, true, true)); + /** @phpstan-ignore staticMethod.dynamicCall, staticMethod.deprecatedClass */ + self::assertEquals($expected, JsonFormatter::format($data, true, true)); } /** @@ -44,7 +44,7 @@ public function testUtf16SurrogatePair(): void } $escaped = '"\ud83d\ude00"'; - /** @phpstan-ignore-next-line */ - $this->assertEquals($escaped, JsonFormatter::format($escaped, true, true)); + /** @phpstan-ignore staticMethod.dynamicCall, staticMethod.deprecatedClass */ + self::assertEquals($escaped, JsonFormatter::format($escaped, true, true)); } } diff --git a/tests/Composer/Test/Json/JsonManipulatorTest.php b/tests/Composer/Test/Json/JsonManipulatorTest.php index 8ee8b58629ea..9cce7845ccdc 100644 --- a/tests/Composer/Test/Json/JsonManipulatorTest.php +++ b/tests/Composer/Test/Json/JsonManipulatorTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Json; +use Composer\Json\JsonFile; use Composer\Json\JsonManipulator; use Composer\Test\TestCase; @@ -23,8 +24,8 @@ class JsonManipulatorTest extends TestCase public function testAddLink(string $json, string $type, string $package, string $constraint, string $expected): void { $manipulator = new JsonManipulator($json); - $this->assertTrue($manipulator->addLink($type, $package, $constraint)); - $this->assertEquals($expected, $manipulator->getContents()); + self::assertTrue($manipulator->addLink($type, $package, $constraint)); + self::assertEquals($expected, $manipulator->getContents()); } public static function linkProvider(): array @@ -1293,8 +1294,8 @@ public static function linkProvider(): array public function testAddLinkAndSortPackages(string $json, string $type, string $package, string $constraint, bool $sortPackages, string $expected): void { $manipulator = new JsonManipulator($json); - $this->assertTrue($manipulator->addLink($type, $package, $constraint, $sortPackages)); - $this->assertEquals($expected, $manipulator->getContents()); + self::assertTrue($manipulator->addLink($type, $package, $constraint, $sortPackages)); + self::assertEquals($expected, $manipulator->getContents()); } public static function providerAddLinkAndSortPackages(): array @@ -1374,9 +1375,9 @@ public function testRemoveSubNode(string $json, string $name, bool $expected, ?s { $manipulator = new JsonManipulator($json); - $this->assertEquals($expected, $manipulator->removeSubNode('repositories', $name)); + self::assertEquals($expected, $manipulator->removeSubNode('repositories', $name)); if (null !== $expectedContent) { - $this->assertEquals($expectedContent, $manipulator->getContents()); + self::assertEquals($expectedContent, $manipulator->getContents()); } } @@ -1696,9 +1697,9 @@ public function testRemoveSubNodeFromRequire(): void } }'); - $this->assertTrue($manipulator->removeSubNode('require', 'package/c')); - $this->assertTrue($manipulator->removeSubNode('require-dev', 'package/d')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeSubNode('require', 'package/c')); + self::assertTrue($manipulator->removeSubNode('require-dev', 'package/d')); + self::assertEquals('{ "repositories": [ { "package": { @@ -1721,6 +1722,38 @@ public function testRemoveSubNodeFromRequire(): void ', $manipulator->getContents()); } + public function testRemoveSubNodePreservesObjectTypeWhenEmpty(): void + { + $manipulator = new JsonManipulator('{ + "test": {"0": "foo"} +}'); + + self::assertTrue($manipulator->removeSubNode('test', '0')); + self::assertEquals('{ + "test": { + } +} +', $manipulator->getContents()); + } + + public function testRemoveSubNodePreservesObjectTypeWhenEmpty2(): void + { + $manipulator = new JsonManipulator('{ + "config": { + "preferred-install": {"foo/*": "source"} + } +}'); + + self::assertTrue($manipulator->removeConfigSetting('preferred-install.foo/*')); + self::assertEquals('{ + "config": { + "preferred-install": { + } + } +} +', $manipulator->getContents()); + } + public function testAddSubNodeInRequire(): void { $manipulator = new JsonManipulator('{ @@ -1745,9 +1778,9 @@ public function testAddSubNodeInRequire(): void } }'); - $this->assertTrue($manipulator->addSubNode('require', 'package/c', '*')); - $this->assertTrue($manipulator->addSubNode('require-dev', 'package/e', '*')); - $this->assertEquals('{ + self::assertTrue($manipulator->addSubNode('require', 'package/c', '*')); + self::assertTrue($manipulator->addSubNode('require-dev', 'package/e', '*')); + self::assertEquals('{ "repositories": [ { "package": { @@ -1793,8 +1826,8 @@ public function testAddExtraWithPackage(): void } }'); - $this->assertTrue($manipulator->addProperty('extra.foo-bar', true)); - $this->assertEquals('{ + self::assertTrue($manipulator->addProperty('extra.foo-bar', true)); + self::assertEquals('{ "repositories": [ { "type": "package", @@ -1835,8 +1868,8 @@ public function testAddConfigWithPackage(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('preferred-install.my-organization/stable-package', 'dist')); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('preferred-install.my-organization/stable-package', 'dist')); + self::assertEquals('{ "repositories": [ { "type": "package", @@ -1879,8 +1912,8 @@ public function testAddSuggestWithPackage(): void } }'); - $this->assertTrue($manipulator->addProperty('suggest.new-package', 'new-description')); - $this->assertEquals('{ + self::assertTrue($manipulator->addProperty('suggest.new-package', 'new-description')); + self::assertEquals('{ "repositories": [ { "type": "package", @@ -1907,8 +1940,8 @@ public function testAddRepositoryCanInitializeEmptyRepositories(): void } }'); - $this->assertTrue($manipulator->addRepository('bar', ['type' => 'composer'])); - $this->assertEquals('{ + self::assertTrue($manipulator->addRepository('bar', ['type' => 'composer'])); + self::assertEquals('{ "repositories": { "bar": { "type": "composer" @@ -1924,8 +1957,8 @@ public function testAddRepositoryCanInitializeFromScratch(): void \t\"a\": \"b\" }"); - $this->assertTrue($manipulator->addRepository('bar2', ['type' => 'composer'])); - $this->assertEquals("{ + self::assertTrue($manipulator->addRepository('bar2', ['type' => 'composer'])); + self::assertEquals("{ \t\"a\": \"b\", \t\"repositories\": { \t\t\"bar2\": { @@ -1947,8 +1980,8 @@ public function testAddRepositoryCanAppend(): void } }'); - $this->assertTrue($manipulator->addRepository('bar', ['type' => 'composer'], true)); - $this->assertEquals('{ + self::assertTrue($manipulator->addRepository('bar', ['type' => 'composer'], true)); + self::assertEquals('{ "repositories": { "foo": { "type": "vcs", @@ -1973,8 +2006,8 @@ public function testAddRepositoryCanPrepend(): void } }'); - $this->assertTrue($manipulator->addRepository('bar', ['type' => 'composer'], false)); - $this->assertEquals('{ + self::assertTrue($manipulator->addRepository('bar', ['type' => 'composer'], false)); + self::assertEquals('{ "repositories": { "bar": { "type": "composer" @@ -1999,8 +2032,8 @@ public function testAddRepositoryCanOverrideDeepRepos(): void } }'); - $this->assertTrue($manipulator->addRepository('baz', ['type' => 'composer'])); - $this->assertEquals('{ + self::assertTrue($manipulator->addRepository('baz', ['type' => 'composer'])); + self::assertEquals('{ "repositories": { "baz": { "type": "composer" @@ -2017,9 +2050,9 @@ public function testAddConfigSettingEscapes(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('test', 'a\b')); - $this->assertTrue($manipulator->addConfigSetting('test2', "a\nb\fa")); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('test', 'a\b')); + self::assertTrue($manipulator->addConfigSetting('test2', "a\nb\fa")); + self::assertEquals('{ "config": { "test": "a\\\\b", "test2": "a\nb\fa" @@ -2033,8 +2066,8 @@ public function testAddConfigSettingWorksFromScratch(): void $manipulator = new JsonManipulator('{ }'); - $this->assertTrue($manipulator->addConfigSetting('foo.bar', 'baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('foo.bar', 'baz')); + self::assertEquals('{ "config": { "foo": { "bar": "baz" @@ -2052,8 +2085,8 @@ public function testAddConfigSettingCanAdd(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('bar', 'baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('bar', 'baz')); + self::assertEquals('{ "config": { "foo": "bar", "bar": "baz" @@ -2071,8 +2104,8 @@ public function testAddConfigSettingCanOverwrite(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('foo', 'zomg')); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('foo', 'zomg')); + self::assertEquals('{ "config": { "foo": "zomg", "bar": "baz" @@ -2089,8 +2122,8 @@ public function testAddConfigSettingCanOverwriteNumbers(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('foo', 50)); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('foo', 50)); + self::assertEquals('{ "config": { "foo": 50 } @@ -2109,8 +2142,8 @@ public function testAddConfigSettingCanOverwriteArrays(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('github-protocols', ['https', 'http'])); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('github-protocols', ['https', 'http'])); + self::assertEquals('{ "config": { "github-oauth": { "github.com": "foo" @@ -2120,8 +2153,8 @@ public function testAddConfigSettingCanOverwriteArrays(): void } ', $manipulator->getContents()); - $this->assertTrue($manipulator->addConfigSetting('github-oauth', ['github.com' => 'bar', 'alt.example.org' => 'baz'])); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('github-oauth', ['github.com' => 'bar', 'alt.example.org' => 'baz'])); + self::assertEquals('{ "config": { "github-oauth": { "github.com": "bar", @@ -2140,8 +2173,8 @@ public function testAddConfigSettingCanAddSubKeyInEmptyConfig(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + self::assertEquals('{ "config": { "github-oauth": { "bar": "baz" @@ -2161,10 +2194,10 @@ public function testAddConfigSettingCanAddSubKeyInEmptyVal(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); - $this->assertTrue($manipulator->addConfigSetting('github-oauth2.a.bar', 'baz2')); - $this->assertTrue($manipulator->addConfigSetting('github-oauth3.b', 'c')); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + self::assertTrue($manipulator->addConfigSetting('github-oauth2.a.bar', 'baz2')); + self::assertTrue($manipulator->addConfigSetting('github-oauth3.b', 'c')); + self::assertEquals('{ "config": { "github-oauth": { "bar": "baz" @@ -2190,8 +2223,8 @@ public function testAddConfigSettingCanAddSubKeyInHash(): void } }'); - $this->assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addConfigSetting('github-oauth.bar', 'baz')); + self::assertEquals('{ "config": { "github-oauth": { "github.com": "foo", @@ -2210,8 +2243,8 @@ public function testAddRootSettingDoesNotBreakDots(): void } }'); - $this->assertTrue($manipulator->addSubNode('github-oauth', 'bar', 'baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addSubNode('github-oauth', 'bar', 'baz')); + self::assertEquals('{ "github-oauth": { "github.com": "foo", "bar": "baz" @@ -2231,8 +2264,8 @@ public function testRemoveConfigSettingCanRemoveSubKeyInHash(): void } }'); - $this->assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); + self::assertEquals('{ "config": { "github-oauth": { "github.com": "foo" @@ -2254,8 +2287,8 @@ public function testRemoveConfigSettingCanRemoveSubKeyInHashWithSiblings(): void } }'); - $this->assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeConfigSetting('github-oauth.bar')); + self::assertEquals('{ "config": { "foo": "bar", "github-oauth": { @@ -2272,8 +2305,8 @@ public function testAddMainKey(): void "foo": "bar" }'); - $this->assertTrue($manipulator->addMainKey('bar', 'baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('bar', 'baz')); + self::assertEquals('{ "foo": "bar", "bar": "baz" } @@ -2286,8 +2319,8 @@ public function testAddMainKeyWithContentHavingDollarSignFollowedByDigit(): void "foo": "bar" }'); - $this->assertTrue($manipulator->addMainKey('bar', '$1baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('bar', '$1baz')); + self::assertEquals('{ "foo": "bar", "bar": "$1baz" } @@ -2298,8 +2331,8 @@ public function testAddMainKeyWithContentHavingDollarSignFollowedByDigit2(): voi { $manipulator = new JsonManipulator('{}'); - $this->assertTrue($manipulator->addMainKey('foo', '$1bar')); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('foo', '$1bar')); + self::assertEquals('{ "foo": "$1bar" } ', $manipulator->getContents()); @@ -2311,8 +2344,8 @@ public function testUpdateMainKey(): void "foo": "bar" }'); - $this->assertTrue($manipulator->addMainKey('foo', 'baz')); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('foo', 'baz')); + self::assertEquals('{ "foo": "baz" } ', $manipulator->getContents()); @@ -2329,9 +2362,9 @@ public function testUpdateMainKey2(): void "baz": "bar" }'); - $this->assertTrue($manipulator->addMainKey('foo', 'baz')); - $this->assertTrue($manipulator->addMainKey('baz', 'quux')); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('foo', 'baz')); + self::assertTrue($manipulator->addMainKey('baz', 'quux')); + self::assertEquals('{ "a": { "foo": "bar", "baz": "qux" @@ -2353,8 +2386,8 @@ public function testUpdateMainKey3(): void } }'); - $this->assertTrue($manipulator->addMainKey('require-dev', ['foo' => 'qux'])); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('require-dev', ['foo' => 'qux'])); + self::assertEquals('{ "require": { "php": "5.*" }, @@ -2371,8 +2404,8 @@ public function testUpdateMainKeyWithContentHavingDollarSignFollowedByDigit(): v "foo": "bar" }'); - $this->assertTrue($manipulator->addMainKey('foo', '$1bar')); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('foo', '$1bar')); + self::assertEquals('{ "foo": "$1bar" } ', $manipulator->getContents()); @@ -2404,8 +2437,8 @@ public function testRemoveMainKey(): void } }'); - $this->assertTrue($manipulator->removeMainKey('repositories')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeMainKey('repositories')); + self::assertEquals('{ "require": { "package/a": "*", "package/b": "*", @@ -2418,8 +2451,8 @@ public function testRemoveMainKey(): void } ', $manipulator->getContents()); - $this->assertTrue($manipulator->removeMainKey('foo')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeMainKey('foo')); + self::assertEquals('{ "require": { "package/a": "*", "package/b": "*", @@ -2431,9 +2464,9 @@ public function testRemoveMainKey(): void } ', $manipulator->getContents()); - $this->assertTrue($manipulator->removeMainKey('require')); - $this->assertTrue($manipulator->removeMainKey('require-dev')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeMainKey('require')); + self::assertTrue($manipulator->removeMainKey('require-dev')); + self::assertEquals('{ } ', $manipulator->getContents()); } @@ -2453,8 +2486,8 @@ public function testRemoveMainKeyIfEmpty(): void } }'); - $this->assertTrue($manipulator->removeMainKeyIfEmpty('repositories')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeMainKeyIfEmpty('repositories')); + self::assertEquals('{ "require": { "package/a": "*", "package/b": "*", @@ -2466,10 +2499,10 @@ public function testRemoveMainKeyIfEmpty(): void } ', $manipulator->getContents()); - $this->assertTrue($manipulator->removeMainKeyIfEmpty('foo')); - $this->assertTrue($manipulator->removeMainKeyIfEmpty('require')); - $this->assertTrue($manipulator->removeMainKeyIfEmpty('require-dev')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeMainKeyIfEmpty('foo')); + self::assertTrue($manipulator->removeMainKeyIfEmpty('require')); + self::assertTrue($manipulator->removeMainKeyIfEmpty('require-dev')); + self::assertEquals('{ "require": { "package/a": "*", "package/b": "*", @@ -2489,11 +2522,11 @@ public function testRemoveMainKeyRemovesKeyWhereValueIsNull(): void $manipulator->removeMainKey('bar'); - $expected = json_encode([ + $expected = JsonFile::encode([ 'foo' => 9000, ]); - $this->assertJsonStringEqualsJsonString($expected, $manipulator->getContents()); + self::assertJsonStringEqualsJsonString($expected, $manipulator->getContents()); } public function testIndentDetection(): void @@ -2505,8 +2538,8 @@ public function testIndentDetection(): void } }'); - $this->assertTrue($manipulator->addMainKey('require-dev', ['foo' => 'qux'])); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('require-dev', ['foo' => 'qux'])); + self::assertEquals('{ "require": { "php": "5.*" @@ -2526,9 +2559,9 @@ public function testRemoveMainKeyAtEndOfFile(): void } } '); - $this->assertTrue($manipulator->addMainKey('homepage', 'http...')); - $this->assertTrue($manipulator->addMainKey('license', 'mit')); - $this->assertEquals('{ + self::assertTrue($manipulator->addMainKey('homepage', 'http...')); + self::assertTrue($manipulator->addMainKey('license', 'mit')); + self::assertEquals('{ "require": { "package/a": "*" }, @@ -2537,9 +2570,9 @@ public function testRemoveMainKeyAtEndOfFile(): void } ', $manipulator->getContents()); - $this->assertTrue($manipulator->removeMainKey('homepage')); - $this->assertTrue($manipulator->removeMainKey('license')); - $this->assertEquals('{ + self::assertTrue($manipulator->removeMainKey('homepage')); + self::assertTrue($manipulator->removeMainKey('license')); + self::assertEquals('{ "require": { "package/a": "*" } @@ -2556,8 +2589,8 @@ public function testEscapedUnicodeDoesNotCauseBacktrackLimitErrorGithubIssue8131 } }'); - $this->assertTrue($manipulator->addLink('require', 'foo/baz', '^1.0')); - $this->assertEquals('{ + self::assertTrue($manipulator->addLink('require', 'foo/baz', '^1.0')); + self::assertEquals('{ "description": "Some U\u00F1icode", "require": { "foo/bar": "^1.0", @@ -3219,8 +3252,8 @@ public function testLargeFileDoesNotCauseBacktrackLimitErrorGithubIssue9595(): v "prefer-stable": true }'); - $this->assertTrue($manipulator->addSubNode('config', 'platform-check', false)); - $this->assertEquals('{ + self::assertTrue($manipulator->addSubNode('config', 'platform-check', false)); + self::assertEquals('{ "name": "leoloso/pop", "require": { "php": "^7.4|^8.0", diff --git a/tests/Composer/Test/Json/JsonValidationExceptionTest.php b/tests/Composer/Test/Json/JsonValidationExceptionTest.php index 69a446341fff..672f99364b49 100644 --- a/tests/Composer/Test/Json/JsonValidationExceptionTest.php +++ b/tests/Composer/Test/Json/JsonValidationExceptionTest.php @@ -25,14 +25,14 @@ class JsonValidationExceptionTest extends TestCase public function testGetErrors(string $message, array $errors, string $expectedMessage, array $expectedErrors): void { $object = new JsonValidationException($message, $errors); - $this->assertSame($expectedMessage, $object->getMessage()); - $this->assertSame($expectedErrors, $object->getErrors()); + self::assertSame($expectedMessage, $object->getMessage()); + self::assertSame($expectedErrors, $object->getErrors()); } public function testGetErrorsWhenNoErrorsProvided(): void { $object = new JsonValidationException('test message'); - $this->assertEquals([], $object->getErrors()); + self::assertEquals([], $object->getErrors()); } public static function errorProvider(): array diff --git a/tests/Composer/Test/Mock/HttpDownloaderMock.php b/tests/Composer/Test/Mock/HttpDownloaderMock.php index 0005a5d87770..a0325118cac7 100644 --- a/tests/Composer/Test/Mock/HttpDownloaderMock.php +++ b/tests/Composer/Test/Mock/HttpDownloaderMock.php @@ -90,7 +90,7 @@ public function assertComplete(): void } // dummy assertion to ensure the test is not marked as having no assertions - Assert::assertTrue(true); // @phpstan-ignore-line + Assert::assertTrue(true); // @phpstan-ignore staticMethod.alreadyNarrowedType } public function get($fileUrl, $options = []): Response diff --git a/tests/Composer/Test/Mock/IOMock.php b/tests/Composer/Test/Mock/IOMock.php index 13a87d1b5ba7..42ff1b258cc8 100644 --- a/tests/Composer/Test/Mock/IOMock.php +++ b/tests/Composer/Test/Mock/IOMock.php @@ -142,7 +142,7 @@ public function assertComplete(): void } // dummy assertion to ensure the test is not marked as having no assertions - Assert::assertTrue(true); // @phpstan-ignore-line + Assert::assertTrue(true); // @phpstan-ignore staticMethod.alreadyNarrowedType } /** diff --git a/tests/Composer/Test/Mock/ProcessExecutorMock.php b/tests/Composer/Test/Mock/ProcessExecutorMock.php index 9f633a6269d7..00f094990ca8 100644 --- a/tests/Composer/Test/Mock/ProcessExecutorMock.php +++ b/tests/Composer/Test/Mock/ProcessExecutorMock.php @@ -57,16 +57,16 @@ public function __construct(MockBuilder $processMockBuilder) } /** - * @param array, return?: int, stdout?: string, stderr?: string, callback?: callable}> $expectations + * @param array|array{cmd: string|non-empty-list, return?: int, stdout?: string, stderr?: string, callback?: callable}> $expectations * @param bool $strict set to true if you want to provide *all* expected commands, and not just a subset you are interested in testing * @param array{return: int, stdout?: string, stderr?: string} $defaultHandler default command handler for undefined commands if not in strict mode */ public function expects(array $expectations, bool $strict = false, array $defaultHandler = ['return' => 0, 'stdout' => '', 'stderr' => '']): void { - /** @var array{cmd: string|list, return: int, stdout: string, stderr: string, callback: callable|null} $default */ + /** @var array{cmd: string|non-empty-list, return: int, stdout: string, stderr: string, callback: callable|null} $default */ $default = ['cmd' => '', 'return' => 0, 'stdout' => '', 'stderr' => '', 'callback' => null]; $this->expectations = array_map(static function ($expect) use ($default): array { - if (is_string($expect)) { + if (is_string($expect) || array_is_list($expect)) { $command = $expect; $expect = $default; $expect['cmd'] = $command; @@ -100,7 +100,7 @@ public function assertComplete(): void } // dummy assertion to ensure the test is not marked as having no assertions - Assert::assertTrue(true); // @phpstan-ignore-line + Assert::assertTrue(true); // @phpstan-ignore staticMethod.alreadyNarrowedType } public function execute($command, &$output = null, ?string $cwd = null): int diff --git a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php index ad5cb98af9d5..377a820c292a 100644 --- a/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php @@ -116,7 +116,7 @@ public function testManualExcludes(): void $this->finder = new ArchivableFilesFinder($this->sources, $excludes); - $this->assertArchivableFiles([ + self::assertArchivableFiles([ '/!important!.txt', '/!important_too!.txt', '/#weirdfile', @@ -191,7 +191,7 @@ public function testGitExcludes(): void $this->finder = new ArchivableFilesFinder($this->sources, []); - $this->assertArchivableFiles($this->getArchivedFiles( + self::assertArchivableFiles($this->getArchivedFiles( 'git init && '. 'git config user.email "you@example.com" && '. 'git config user.name "Your Name" && '. @@ -212,7 +212,7 @@ public function testSkipExcludes(): void $this->finder = new ArchivableFilesFinder($this->sources, $excludes, true); - $this->assertArchivableFiles([ + self::assertArchivableFiles([ '/!important!.txt', '/!important_too!.txt', '/#weirdfile', @@ -306,6 +306,6 @@ protected function assertArchivableFiles(array $expectedFiles): void { $actualFiles = $this->getArchivableFiles(); - $this->assertEquals($expectedFiles, $actualFiles); + self::assertEquals($expectedFiles, $actualFiles); } } diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index ca07aede41a8..e75e49a70bd1 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -69,10 +69,10 @@ public function testArchiveTar(): void $this->manager->archive($package, 'tar', $this->targetDir); $target = $this->getTargetName($package, 'tar'); - $this->assertFileExists($target); + self::assertFileExists($target); $tmppath = sys_get_temp_dir().'/composer_archiver/'.$this->manager->getPackageFilename($package); - $this->assertFileDoesNotExist($tmppath); + self::assertFileDoesNotExist($tmppath); unlink($target); } @@ -91,10 +91,10 @@ public function testArchiveCustomFileName(): void $target = $this->targetDir . '/' . $fileName . '.tar'; - $this->assertFileExists($target); + self::assertFileExists($target); $tmppath = sys_get_temp_dir().'/composer_archiver/'.$this->manager->getPackageFilename($package); - $this->assertFileDoesNotExist($tmppath); + self::assertFileDoesNotExist($tmppath); unlink($target); } diff --git a/tests/Composer/Test/Package/Archiver/GitExcludeFilterTest.php b/tests/Composer/Test/Package/Archiver/GitExcludeFilterTest.php index 0f28bd35de4e..56134e23af07 100644 --- a/tests/Composer/Test/Package/Archiver/GitExcludeFilterTest.php +++ b/tests/Composer/Test/Package/Archiver/GitExcludeFilterTest.php @@ -26,7 +26,7 @@ public function testPatternEscape(string $ignore, array $expected): void { $filter = new GitExcludeFilter('/'); - $this->assertEquals($expected, $filter->parseGitAttributesLine($ignore)); + self::assertEquals($expected, $filter->parseGitAttributesLine($ignore)); } public static function providePatterns(): array diff --git a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php index 1d6e68e2ec60..cfe203004082 100644 --- a/tests/Composer/Test/Package/Archiver/PharArchiverTest.php +++ b/tests/Composer/Test/Package/Archiver/PharArchiverTest.php @@ -27,7 +27,7 @@ public function testTarArchive(): void // Test archive $archiver = new PharArchiver(); $archiver->archive($package->getSourceUrl(), $target, 'tar', ['foo/bar', 'baz', '!/foo/bar/baz']); - $this->assertFileExists($target); + self::assertFileExists($target); $this->filesystem->removeDirectory(dirname($target)); } @@ -42,7 +42,7 @@ public function testZipArchive(): void // Test archive $archiver = new PharArchiver(); $archiver->archive($package->getSourceUrl(), $target, 'zip'); - $this->assertFileExists($target); + self::assertFileExists($target); $this->filesystem->removeDirectory(dirname($target)); } diff --git a/tests/Composer/Test/Package/BasePackageTest.php b/tests/Composer/Test/Package/BasePackageTest.php index 324bda08f782..ae40844170d1 100644 --- a/tests/Composer/Test/Package/BasePackageTest.php +++ b/tests/Composer/Test/Package/BasePackageTest.php @@ -54,7 +54,7 @@ public function testFormatVersionForDevPackage(string $sourceReference, bool $tr $package->expects($this->once())->method('getPrettyVersion')->will($this->returnValue('PrettyVersion')); $package->expects($this->any())->method('getSourceReference')->will($this->returnValue($sourceReference)); - $this->assertSame($expected, $package->getFullPrettyVersion($truncate)); + self::assertSame($expected, $package->getFullPrettyVersion($truncate)); } public static function provideFormattedVersions(): array @@ -93,7 +93,7 @@ public function testPackageNamesToRegexp(array $packageNames, $wrap, string $exp { $regexp = BasePackage::packageNamesToRegexp($packageNames, $wrap); - $this->assertSame($expectedRegexp, $regexp); + self::assertSame($expectedRegexp, $regexp); } /** diff --git a/tests/Composer/Test/Package/CompletePackageTest.php b/tests/Composer/Test/Package/CompletePackageTest.php index 17dce9fa13d3..6f7fe5f2b28f 100644 --- a/tests/Composer/Test/Package/CompletePackageTest.php +++ b/tests/Composer/Test/Package/CompletePackageTest.php @@ -42,7 +42,7 @@ public function testPackageHasExpectedNamingSemantics(string $name, string $vers $versionParser = new VersionParser(); $normVersion = $versionParser->normalize($version); $package = new Package($name, $normVersion, $version); - $this->assertEquals(strtolower($name), $package->getName()); + self::assertEquals(strtolower($name), $package->getName()); } /** @@ -53,8 +53,8 @@ public function testPackageHasExpectedVersioningSemantics(string $name, string $ $versionParser = new VersionParser(); $normVersion = $versionParser->normalize($version); $package = new Package($name, $normVersion, $version); - $this->assertEquals($version, $package->getPrettyVersion()); - $this->assertEquals($normVersion, $package->getVersion()); + self::assertEquals($version, $package->getPrettyVersion()); + self::assertEquals($normVersion, $package->getVersion()); } /** @@ -65,34 +65,34 @@ public function testPackageHasExpectedMarshallingSemantics(string $name, string $versionParser = new VersionParser(); $normVersion = $versionParser->normalize($version); $package = new Package($name, $normVersion, $version); - $this->assertEquals(strtolower($name).'-'.$normVersion, (string) $package); + self::assertEquals(strtolower($name).'-'.$normVersion, (string) $package); } public function testGetTargetDir(): void { $package = new Package('a', '1.0.0.0', '1.0'); - $this->assertNull($package->getTargetDir()); + self::assertNull($package->getTargetDir()); $package->setTargetDir('./../foo/'); - $this->assertEquals('foo/', $package->getTargetDir()); + self::assertEquals('foo/', $package->getTargetDir()); $package->setTargetDir('foo/../../../bar/'); - $this->assertEquals('foo/bar/', $package->getTargetDir()); + self::assertEquals('foo/bar/', $package->getTargetDir()); $package->setTargetDir('../..'); - $this->assertEquals('', $package->getTargetDir()); + self::assertEquals('', $package->getTargetDir()); $package->setTargetDir('..'); - $this->assertEquals('', $package->getTargetDir()); + self::assertEquals('', $package->getTargetDir()); $package->setTargetDir('/..'); - $this->assertEquals('', $package->getTargetDir()); + self::assertEquals('', $package->getTargetDir()); $package->setTargetDir('/foo/..'); - $this->assertEquals('foo/', $package->getTargetDir()); + self::assertEquals('foo/', $package->getTargetDir()); $package->setTargetDir('/foo/..//bar'); - $this->assertEquals('foo/bar', $package->getTargetDir()); + self::assertEquals('foo/bar', $package->getTargetDir()); } } diff --git a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php index a7738a469ba2..253b3253bd6c 100644 --- a/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php +++ b/tests/Composer/Test/Package/Dumper/ArrayDumperTest.php @@ -32,7 +32,7 @@ public function setUp(): void public function testRequiredInformation(): void { $config = $this->dumper->dump(self::getPackage()); - $this->assertEquals( + self::assertEquals( [ 'name' => 'dummy/pkg', 'version' => '1.0.0', @@ -49,7 +49,7 @@ public function testRootPackage(): void $package->setMinimumStability('dev'); $config = $this->dumper->dump($package); - $this->assertSame('dev', $config['minimum-stability']); + self::assertSame('dev', $config['minimum-stability']); } public function testDumpAbandoned(): void @@ -58,7 +58,7 @@ public function testDumpAbandoned(): void $package->setAbandoned(true); $config = $this->dumper->dump($package); - $this->assertTrue($config['abandoned']); + self::assertTrue($config['abandoned']); } public function testDumpAbandonedReplacement(): void @@ -67,7 +67,7 @@ public function testDumpAbandonedReplacement(): void $package->setAbandoned('foo/bar'); $config = $this->dumper->dump($package); - $this->assertSame('foo/bar', $config['abandoned']); + self::assertSame('foo/bar', $config['abandoned']); } /** @@ -81,12 +81,12 @@ public function testKeys(string $key, $value, ?string $method = null, $expectedV { $package = self::getRootPackage(); - // @phpstan-ignore-next-line - $package->{'set'.ucfirst($method ?: $key)}($value); + // @phpstan-ignore method.dynamicName + $package->{'set'.ucfirst($method ?? $key)}($value); $config = $this->dumper->dump($package); - $this->assertSame($expectedValue ?: $value, $config[$key]); + self::assertSame($expectedValue ?: $value, $config[$key]); } public static function provideKeys(): array @@ -234,6 +234,11 @@ public static function provideKeys(): array ['ssl' => ['local_cert' => '/opt/certs/test.pem']], 'transportOptions', ], + [ + 'php-ext', + ['extension-name' => 'test'], + 'phpExt', + ], ]; } } diff --git a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php index 31f4fdf49b44..09602fca08a5 100644 --- a/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ArrayLoaderTest.php @@ -42,7 +42,7 @@ public function testSelfVersion(): void $package = $this->loader->load($config); $replaces = $package->getReplaces(); - $this->assertEquals('== 1.2.3.4', (string) $replaces['foo']->getConstraint()); + self::assertEquals('== 1.2.3.4', (string) $replaces['foo']->getConstraint()); } public function testTypeDefault(): void @@ -53,7 +53,7 @@ public function testTypeDefault(): void ]; $package = $this->loader->load($config); - $this->assertEquals('library', $package->getType()); + self::assertEquals('library', $package->getType()); $config = [ 'name' => 'A', @@ -62,7 +62,7 @@ public function testTypeDefault(): void ]; $package = $this->loader->load($config); - $this->assertEquals('foo', $package->getType()); + self::assertEquals('foo', $package->getType()); } public function testNormalizedVersionOptimization(): void @@ -73,7 +73,7 @@ public function testNormalizedVersionOptimization(): void ]; $package = $this->loader->load($config); - $this->assertEquals('1.2.3.0', $package->getVersion()); + self::assertEquals('1.2.3.0', $package->getVersion()); $config = [ 'name' => 'A', @@ -82,7 +82,7 @@ public function testNormalizedVersionOptimization(): void ]; $package = $this->loader->load($config); - $this->assertEquals('1.2.3.4', $package->getVersion()); + self::assertEquals('1.2.3.4', $package->getVersion()); } public static function parseDumpProvider(): array @@ -161,7 +161,7 @@ public function testParseDumpDefaultLoadConfig(array $config): void $package = $this->loader->load($config); $dumper = new ArrayDumper; $expectedConfig = $this->fixConfigWhenLoadConfigIsFalse($config); - $this->assertEquals($expectedConfig, $dumper->dump($package)); + self::assertEquals($expectedConfig, $dumper->dump($package)); } /** @@ -175,7 +175,7 @@ public function testParseDumpTrueLoadConfig(array $config): void $package = $loader->load($config); $dumper = new ArrayDumper; $expectedConfig = $config; - $this->assertEquals($expectedConfig, $dumper->dump($package)); + self::assertEquals($expectedConfig, $dumper->dump($package)); } /** @@ -189,7 +189,7 @@ public function testParseDumpFalseLoadConfig(array $config): void $package = $loader->load($config); $dumper = new ArrayDumper; $expectedConfig = $this->fixConfigWhenLoadConfigIsFalse($config); - $this->assertEquals($expectedConfig, $dumper->dump($package)); + self::assertEquals($expectedConfig, $dumper->dump($package)); } public function testPackageWithBranchAlias(): void @@ -202,8 +202,8 @@ public function testPackageWithBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\AliasPackage', $package); - $this->assertEquals('1.0.x-dev', $package->getPrettyVersion()); + self::assertInstanceOf('Composer\Package\AliasPackage', $package); + self::assertEquals('1.0.x-dev', $package->getPrettyVersion()); $config = [ 'name' => 'A', @@ -213,8 +213,8 @@ public function testPackageWithBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\AliasPackage', $package); - $this->assertEquals('1.0.x-dev', $package->getPrettyVersion()); + self::assertInstanceOf('Composer\Package\AliasPackage', $package); + self::assertEquals('1.0.x-dev', $package->getPrettyVersion()); $config = [ 'name' => 'B', @@ -224,8 +224,8 @@ public function testPackageWithBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\AliasPackage', $package); - $this->assertEquals('4.0.x-dev', $package->getPrettyVersion()); + self::assertInstanceOf('Composer\Package\AliasPackage', $package); + self::assertEquals('4.0.x-dev', $package->getPrettyVersion()); $config = [ 'name' => 'B', @@ -235,8 +235,8 @@ public function testPackageWithBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\AliasPackage', $package); - $this->assertEquals('4.0.x-dev', $package->getPrettyVersion()); + self::assertInstanceOf('Composer\Package\AliasPackage', $package); + self::assertEquals('4.0.x-dev', $package->getPrettyVersion()); $config = [ 'name' => 'C', @@ -246,8 +246,8 @@ public function testPackageWithBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\CompletePackage', $package); - $this->assertEquals('4.x-dev', $package->getPrettyVersion()); + self::assertInstanceOf('Composer\Package\CompletePackage', $package); + self::assertEquals('4.x-dev', $package->getPrettyVersion()); } public function testPackageAliasingWithoutBranchAlias(): void @@ -261,8 +261,8 @@ public function testPackageAliasingWithoutBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\AliasPackage', $package); - $this->assertEquals(VersionParser::DEFAULT_BRANCH_ALIAS, $package->getPrettyVersion()); + self::assertInstanceOf('Composer\Package\AliasPackage', $package); + self::assertEquals(VersionParser::DEFAULT_BRANCH_ALIAS, $package->getPrettyVersion()); // non-default branch gets no alias even if non-numeric $config = [ @@ -273,8 +273,8 @@ public function testPackageAliasingWithoutBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\CompletePackage', $package); - $this->assertEquals('dev-main', $package->getPrettyVersion()); + self::assertInstanceOf('Composer\Package\CompletePackage', $package); + self::assertEquals('dev-main', $package->getPrettyVersion()); // default branch gets no alias if already numeric $config = [ @@ -285,8 +285,8 @@ public function testPackageAliasingWithoutBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\CompletePackage', $package); - $this->assertEquals('2.9999999.9999999.9999999-dev', $package->getVersion()); + self::assertInstanceOf('Composer\Package\CompletePackage', $package); + self::assertEquals('2.9999999.9999999.9999999-dev', $package->getVersion()); // default branch gets no alias if already numeric, with v prefix $config = [ @@ -297,8 +297,8 @@ public function testPackageAliasingWithoutBranchAlias(): void $package = $this->loader->load($config); - $this->assertInstanceOf('Composer\Package\CompletePackage', $package); - $this->assertEquals('2.9999999.9999999.9999999-dev', $package->getVersion()); + self::assertInstanceOf('Composer\Package\CompletePackage', $package); + self::assertEquals('2.9999999.9999999.9999999-dev', $package->getVersion()); } public function testAbandoned(): void @@ -310,8 +310,8 @@ public function testAbandoned(): void ]; $package = $this->loader->load($config); - $this->assertTrue($package->isAbandoned()); - $this->assertEquals('foo/bar', $package->getReplacementPackage()); + self::assertTrue($package->isAbandoned()); + self::assertEquals('foo/bar', $package->getReplacementPackage()); } public function testNotAbandoned(): void @@ -322,7 +322,7 @@ public function testNotAbandoned(): void ]; $package = $this->loader->load($config); - $this->assertFalse($package->isAbandoned()); + self::assertFalse($package->isAbandoned()); } public static function providePluginApiVersions(): array @@ -353,23 +353,23 @@ public function testPluginApiVersionAreKeptAsDeclared(string $apiVersion): void { $links = $this->loader->parseLinks('Plugin', '9.9.9', Link::TYPE_REQUIRE, ['composer-plugin-api' => $apiVersion]); - $this->assertArrayHasKey('composer-plugin-api', $links); - $this->assertSame($apiVersion, $links['composer-plugin-api']->getConstraint()->getPrettyString()); + self::assertArrayHasKey('composer-plugin-api', $links); + self::assertSame($apiVersion, $links['composer-plugin-api']->getConstraint()->getPrettyString()); } public function testPluginApiVersionDoesSupportSelfVersion(): void { $links = $this->loader->parseLinks('Plugin', '6.6.6', Link::TYPE_REQUIRE, ['composer-plugin-api' => 'self.version']); - $this->assertArrayHasKey('composer-plugin-api', $links); - $this->assertSame('6.6.6', $links['composer-plugin-api']->getConstraint()->getPrettyString()); + self::assertArrayHasKey('composer-plugin-api', $links); + self::assertSame('6.6.6', $links['composer-plugin-api']->getConstraint()->getPrettyString()); } public function testParseLinksIntegerTarget(): void { $links = $this->loader->parseLinks('Plugin', '9.9.9', Link::TYPE_REQUIRE, ['1' => 'dev-main']); - $this->assertArrayHasKey('1', $links); + self::assertArrayHasKey('1', $links); } public function testNoneStringVersion(): void @@ -380,7 +380,7 @@ public function testNoneStringVersion(): void ]; $package = $this->loader->load($config); - $this->assertSame('1', $package->getPrettyVersion()); + self::assertSame('1', $package->getPrettyVersion()); } public function testNoneStringSourceDistReference(): void @@ -401,8 +401,8 @@ public function testNoneStringSourceDistReference(): void ]; $package = $this->loader->load($config); - $this->assertSame('2019', $package->getSourceReference()); - $this->assertSame('2019', $package->getDistReference()); + self::assertSame('2019', $package->getSourceReference()); + self::assertSame('2019', $package->getDistReference()); } public function testBranchAliasIntegerIndex(): void @@ -421,7 +421,7 @@ public function testBranchAliasIntegerIndex(): void ], ]; - $this->assertNull($this->loader->getBranchAlias($config)); + self::assertNull($this->loader->getBranchAlias($config)); } public function testPackageLinksRequire(): void @@ -435,8 +435,8 @@ public function testPackageLinksRequire(): void ); $package = $this->loader->load($config); - $this->assertArrayHasKey('foo/bar', $package->getRequires()); - $this->assertSame('1.0', $package->getRequires()['foo/bar']->getConstraint()->getPrettyString()); + self::assertArrayHasKey('foo/bar', $package->getRequires()); + self::assertSame('1.0', $package->getRequires()['foo/bar']->getConstraint()->getPrettyString()); } public function testPackageLinksRequireInvalid(): void @@ -452,7 +452,7 @@ public function testPackageLinksRequireInvalid(): void ); $package = $this->loader->load($config); - $this->assertCount(0, $package->getRequires()); + self::assertCount(0, $package->getRequires()); } public function testPackageLinksReplace(): void @@ -466,8 +466,8 @@ public function testPackageLinksReplace(): void ); $package = $this->loader->load($config); - $this->assertArrayHasKey('coyote/package', $package->getReplaces()); - $this->assertSame('dev-1', $package->getReplaces()['coyote/package']->getConstraint()->getPrettyString()); + self::assertArrayHasKey('coyote/package', $package->getReplaces()); + self::assertSame('dev-1', $package->getReplaces()['coyote/package']->getConstraint()->getPrettyString()); } public function testPackageLinksReplaceInvalid(): void @@ -479,7 +479,7 @@ public function testPackageLinksReplaceInvalid(): void ); $package = $this->loader->load($config); - $this->assertCount(0, $package->getReplaces()); + self::assertCount(0, $package->getReplaces()); } public function testSupportStringValue(): void @@ -491,6 +491,6 @@ public function testSupportStringValue(): void ); $package = $this->loader->load($config); - $this->assertSame([], $package->getSupport()); + self::assertSame([], $package->getSupport()); } } diff --git a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php index 28684e56ea8e..4a12eab82a72 100644 --- a/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/RootPackageLoaderTest.php @@ -62,8 +62,8 @@ public function testStabilityFlagsParsing(): void 'minimum-stability' => 'alpha', ]); - $this->assertEquals('alpha', $package->getMinimumStability()); - $this->assertEquals([ + self::assertEquals('alpha', $package->getMinimumStability()); + self::assertEquals([ 'bar/baz' => BasePackage::STABILITY_DEV, 'qux/quux' => BasePackage::STABILITY_RC, 'zux/complex' => BasePackage::STABILITY_DEV, @@ -88,8 +88,8 @@ public function testNoVersionIsVisibleInPrettyVersion(): void $package = $loader->load([]); - $this->assertEquals("1.0.0.0", $package->getVersion()); - $this->assertEquals(RootPackage::DEFAULT_PRETTY_VERSION, $package->getPrettyVersion()); + self::assertEquals("1.0.0.0", $package->getVersion()); + self::assertEquals(RootPackage::DEFAULT_PRETTY_VERSION, $package->getPrettyVersion()); } public function testPrettyVersionForRootPackageInVersionBranch(): void @@ -110,7 +110,7 @@ public function testPrettyVersionForRootPackageInVersionBranch(): void $loader = new RootPackageLoader($manager, $config, null, $versionGuesser); $package = $loader->load([]); - $this->assertEquals('3.0-dev', $package->getPrettyVersion()); + self::assertEquals('3.0-dev', $package->getPrettyVersion()); } public function testFeatureBranchPrettyVersion(): void @@ -130,7 +130,7 @@ public function testFeatureBranchPrettyVersion(): void 'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'stdout' => "* latest-production 38137d2f6c70e775e137b2d8a7a7d3eaebf7c7e5 Commit message\n master 4f6ed96b0bc363d2aa4404c3412de1c011f67c66 Commit message\n", ], - 'git rev-list master..latest-production', + ['cmd' => ['git', 'rev-list', 'master..latest-production']], ], true); $config = new Config; @@ -138,7 +138,7 @@ public function testFeatureBranchPrettyVersion(): void $loader = new RootPackageLoader($manager, $config, null, new VersionGuesser($config, $process, new VersionParser())); $package = $loader->load(['require' => ['foo/bar' => 'self.version']]); - $this->assertEquals("dev-master", $package->getPrettyVersion()); + self::assertEquals("dev-master", $package->getPrettyVersion()); } public function testNonFeatureBranchPrettyVersion(): void @@ -165,6 +165,6 @@ public function testNonFeatureBranchPrettyVersion(): void $loader = new RootPackageLoader($manager, $config, null, new VersionGuesser($config, $process, new VersionParser())); $package = $loader->load(['require' => ['foo/bar' => 'self.version'], "non-feature-branches" => ["latest-.*"]]); - $this->assertEquals("dev-latest-production", $package->getPrettyVersion()); + self::assertEquals("dev-latest-production", $package->getPrettyVersion()); } } diff --git a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php index 40f05894e037..7f39a8789e2a 100644 --- a/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php +++ b/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php @@ -52,7 +52,7 @@ public static function successProvider(): array 'keywords' => ['a', 'b_c', 'D E', 'éîüø', '微信'], 'homepage' => 'https://foo.com', 'time' => '2010-10-10T10:10:10+00:00', - 'license' => 'MIT', + 'license' => ['MIT', 'WTFPL'], 'authors' => [ [ 'name' => 'Alice', @@ -165,12 +165,6 @@ public static function successProvider(): array 'transport-options' => ['ssl' => ['local_cert' => '/opt/certs/test.pem']], ], ], - [ // test licenses as array - [ - 'name' => 'foo/bar', - 'license' => ['MIT', 'WTFPL'], - ], - ], [ // test bin as string [ 'name' => 'foo/bar', @@ -224,7 +218,7 @@ public function testLoadFailureThrowsException(array $config, array $expectedErr $errors = $e->getErrors(); sort($expectedErrors); sort($errors); - $this->assertEquals($expectedErrors, $errors); + self::assertEquals($expectedErrors, $errors); } } @@ -243,7 +237,7 @@ public function testLoadWarnings(array $config, array $expectedWarnings): void $warnings = $loader->getWarnings(); sort($expectedWarnings); sort($warnings); - $this->assertEquals($expectedWarnings, $warnings); + self::assertEquals($expectedWarnings, $warnings); } /** @@ -251,11 +245,12 @@ public function testLoadWarnings(array $config, array $expectedWarnings): void * * @param array $config * @param string[] $expectedWarnings + * @param array|null $expectedArray */ - public function testLoadSkipsWarningDataWhenIgnoringErrors(array $config, array $expectedWarnings, bool $mustCheck = true): void + public function testLoadSkipsWarningDataWhenIgnoringErrors(array $config, array $expectedWarnings, bool $mustCheck = true, ?array $expectedArray = null): void { if (!$mustCheck) { - $this->assertTrue(true); + self::assertTrue(true); // @phpstan-ignore staticMethod.alreadyNarrowedType return; } @@ -263,7 +258,7 @@ public function testLoadSkipsWarningDataWhenIgnoringErrors(array $config, array $internalLoader ->expects($this->once()) ->method('load') - ->with(['name' => 'a/b']); + ->with($expectedArray ?? ['name' => 'a/b']); $loader = new ValidatingArrayLoader($internalLoader, true, null, ValidatingArrayLoader::CHECK_ALL); $config['name'] = 'a/b'; @@ -552,6 +547,35 @@ public static function warningProvider(): array ], false, ], + [ + [ + 'name' => 'a/b', + 'license' => 'XXXXX', + ], + [ + 'License "XXXXX" is not a valid SPDX license identifier, see https://spdx.org/licenses/ if you use an open license.'.PHP_EOL. + 'If the software is closed-source, you may use "proprietary" as license.', + ], + true, + [ + 'name' => 'a/b', + 'license' => ['XXXXX'], + ] + ], + [ + [ + 'name' => 'a/b', + 'license' => [['author'=>'bar'], 'MIT'], + ], + [ + 'License {"author":"bar"} should be a string.', + ], + true, + [ + 'name' => 'a/b', + 'license' => ['MIT'], + ] + ], ]; } } diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index 7d3233b45ac2..8bc4c598b2d5 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -39,7 +39,7 @@ public function testIsLocked(): void ->method('read') ->will($this->returnValue(['packages' => []])); - $this->assertTrue($locker->isLocked()); + self::assertTrue($locker->isLocked()); } public function testGetNotLockedPackages(): void @@ -81,8 +81,8 @@ public function testGetLockedPackages(): void ])); $repo = $locker->getLockedRepository(); - $this->assertNotNull($repo->findPackage('pkg1', '1.0.0-beta')); - $this->assertNotNull($repo->findPackage('pkg2', '0.1.10')); + self::assertNotNull($repo->findPackage('pkg1', '1.0.0-beta')); + self::assertNotNull($repo->findPackage('pkg2', '0.1.10')); } public function testSetLockData(): void @@ -96,7 +96,7 @@ public function testSetLockData(): void $package1 = self::getPackage('pkg1', '1.0.0-beta'); $package2 = self::getPackage('pkg2', '0.1.10'); - $contentHash = md5(trim($jsonContent)); + $contentHash = hash('md5', trim($jsonContent)); $json ->expects($this->once()) @@ -113,9 +113,9 @@ public function testSetLockData(): void 'packages-dev' => [], 'aliases' => [], 'minimum-stability' => 'dev', - 'stability-flags' => [], - 'platform' => [], - 'platform-dev' => [], + 'stability-flags' => new \stdClass, + 'platform' => new \stdClass, + 'platform-dev' => new \stdClass, 'platform-overrides' => ['foo/bar' => '1.0'], 'prefer-stable' => false, 'prefer-lowest' => false, @@ -154,9 +154,9 @@ public function testIsFresh(): void $json ->expects($this->once()) ->method('read') - ->will($this->returnValue(['hash' => md5($jsonContent)])); + ->will($this->returnValue(['hash' => hash('md5', $jsonContent)])); - $this->assertTrue($locker->isFresh()); + self::assertTrue($locker->isFresh()); } public function testIsFreshFalse(): void @@ -171,7 +171,7 @@ public function testIsFreshFalse(): void ->method('read') ->will($this->returnValue(['hash' => $this->getJsonContent(['name' => 'test2'])])); - $this->assertFalse($locker->isFresh()); + self::assertFalse($locker->isFresh()); } public function testIsFreshWithContentHash(): void @@ -185,9 +185,9 @@ public function testIsFreshWithContentHash(): void $json ->expects($this->once()) ->method('read') - ->will($this->returnValue(['hash' => md5($jsonContent . ' '), 'content-hash' => md5($jsonContent)])); + ->will($this->returnValue(['hash' => hash('md5', $jsonContent . ' '), 'content-hash' => hash('md5', $jsonContent)])); - $this->assertTrue($locker->isFresh()); + self::assertTrue($locker->isFresh()); } public function testIsFreshWithContentHashAndNoHash(): void @@ -201,9 +201,9 @@ public function testIsFreshWithContentHashAndNoHash(): void $json ->expects($this->once()) ->method('read') - ->will($this->returnValue(['content-hash' => md5($jsonContent)])); + ->will($this->returnValue(['content-hash' => hash('md5', $jsonContent)])); - $this->assertTrue($locker->isFresh()); + self::assertTrue($locker->isFresh()); } public function testIsFreshFalseWithContentHash(): void @@ -213,14 +213,14 @@ public function testIsFreshFalseWithContentHash(): void $locker = new Locker(new NullIO, $json, $inst, $this->getJsonContent()); - $differentHash = md5($this->getJsonContent(['name' => 'test2'])); + $differentHash = hash('md5', $this->getJsonContent(['name' => 'test2'])); $json ->expects($this->once()) ->method('read') ->will($this->returnValue(['hash' => $differentHash, 'content-hash' => $differentHash])); - $this->assertFalse($locker->isFresh()); + self::assertFalse($locker->isFresh()); } /** diff --git a/tests/Composer/Test/Package/RootAliasPackageTest.php b/tests/Composer/Test/Package/RootAliasPackageTest.php index 8f71a3bd0998..b475719fd231 100644 --- a/tests/Composer/Test/Package/RootAliasPackageTest.php +++ b/tests/Composer/Test/Package/RootAliasPackageTest.php @@ -31,9 +31,9 @@ public function testUpdateRequires(): void ->with($this->equalTo($links)); $alias = new RootAliasPackage($root, '1.0', '1.0.0.0'); - $this->assertEmpty($alias->getRequires()); + self::assertEmpty($alias->getRequires()); $alias->setRequires($links); - $this->assertNotEmpty($alias->getRequires()); + self::assertNotEmpty($alias->getRequires()); } public function testUpdateDevRequires(): void @@ -46,9 +46,9 @@ public function testUpdateDevRequires(): void ->with($this->equalTo($links)); $alias = new RootAliasPackage($root, '1.0', '1.0.0.0'); - $this->assertEmpty($alias->getDevRequires()); + self::assertEmpty($alias->getDevRequires()); $alias->setDevRequires($links); - $this->assertNotEmpty($alias->getDevRequires()); + self::assertNotEmpty($alias->getDevRequires()); } public function testUpdateConflicts(): void @@ -61,9 +61,9 @@ public function testUpdateConflicts(): void ->with($this->equalTo($links)); $alias = new RootAliasPackage($root, '1.0', '1.0.0.0'); - $this->assertEmpty($alias->getConflicts()); + self::assertEmpty($alias->getConflicts()); $alias->setConflicts($links); - $this->assertNotEmpty($alias->getConflicts()); + self::assertNotEmpty($alias->getConflicts()); } public function testUpdateProvides(): void @@ -76,9 +76,9 @@ public function testUpdateProvides(): void ->with($this->equalTo($links)); $alias = new RootAliasPackage($root, '1.0', '1.0.0.0'); - $this->assertEmpty($alias->getProvides()); + self::assertEmpty($alias->getProvides()); $alias->setProvides($links); - $this->assertNotEmpty($alias->getProvides()); + self::assertNotEmpty($alias->getProvides()); } public function testUpdateReplaces(): void @@ -91,9 +91,9 @@ public function testUpdateReplaces(): void ->with($this->equalTo($links)); $alias = new RootAliasPackage($root, '1.0', '1.0.0.0'); - $this->assertEmpty($alias->getReplaces()); + self::assertEmpty($alias->getReplaces()); $alias->setReplaces($links); - $this->assertNotEmpty($alias->getReplaces()); + self::assertNotEmpty($alias->getReplaces()); } /** diff --git a/tests/Composer/Test/Package/Version/VersionBumperTest.php b/tests/Composer/Test/Package/Version/VersionBumperTest.php index a146c2884a37..15b554ae342e 100644 --- a/tests/Composer/Test/Package/Version/VersionBumperTest.php +++ b/tests/Composer/Test/Package/Version/VersionBumperTest.php @@ -37,7 +37,7 @@ public function testBumpRequirement(string $requirement, string $prettyVersion, $newConstraint = $versionBumper->bumpRequirement($versionParser->parseConstraints($requirement), $package); // assert that the recommended version is what we expect - $this->assertSame($expectedRequirement, $newConstraint); + self::assertSame($expectedRequirement, $newConstraint); } public static function provideBumpRequirementTests(): Generator @@ -64,9 +64,14 @@ public static function provideBumpRequirementTests(): Generator yield 'upgrade major wildcard as x to caret/2' => ['2.x.x', '2.4.0', '^2.4.0']; yield 'leave minor wildcard alone' => ['2.4.*', '2.4.3', '2.4.*']; yield 'leave patch wildcard alone' => ['2.4.3.*', '2.4.3.2', '2.4.3.*']; + yield 'leave single tilde alone' => ['~2', '2.4.3', '~2']; yield 'upgrade tilde to caret when compatible' => ['~2.2', '2.4.3', '^2.4.3']; - yield 'update patch-only-tilde alone' => ['~2.2.3', '2.2.6', '~2.2.6']; - yield 'leave extra-only-tilde alone' => ['~2.2.3.1', '2.2.4.5', '~2.2.3.1']; + yield 'upgrade patch-only-tilde, longer version' => ['~2.2.3', '2.2.6.2', '~2.2.6']; + yield 'upgrade patch-only-tilde' => ['~2.2.3', '2.2.6', '~2.2.6']; + yield 'upgrade patch-only-tilde, also .0s' => ['~2.0.0', '2.0.0', '~2.0.0']; + yield 'upgrade 4 bits tilde' => ['~2.2.3.1', '2.2.4', '~2.2.4.0']; + yield 'upgrade 4 bits tilde/2' => ['~2.2.3.1', '2.2.4.0', '~2.2.4.0']; + yield 'upgrade 4 bits tilde/3' => ['~2.2.3.1', '2.2.4.5', '~2.2.4.5']; yield 'upgrade bigger-or-eq to latest' => ['>=3.0', '3.4.5', '>=3.4.5']; yield 'upgrade bigger-or-eq to latest with v' => ['>=v3.0', '3.4.5', '>=3.4.5']; yield 'leave bigger-than untouched' => ['>2.2.3', '2.2.6', '>2.2.3']; diff --git a/tests/Composer/Test/Package/Version/VersionGuesserTest.php b/tests/Composer/Test/Package/Version/VersionGuesserTest.php index 2e59afe6ae31..9dc29a299a52 100644 --- a/tests/Composer/Test/Package/Version/VersionGuesserTest.php +++ b/tests/Composer/Test/Package/Version/VersionGuesserTest.php @@ -17,6 +17,7 @@ use Composer\Semver\VersionParser; use Composer\Test\TestCase; use Composer\Util\Git as GitUtil; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; class VersionGuesserTest extends TestCase @@ -35,11 +36,11 @@ public function testHgGuessVersionReturnsData(): void $process = $this->getProcessExecutorMock(); $process->expects([ ['cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'return' => 128], - ['cmd' => 'git describe --exact-match --tags', 'return' => 128], - ['cmd' => 'git log --pretty="%H" -n1 HEAD'.GitUtil::getNoShowSignatureFlag($process), 'return' => 128], - ['cmd' => 'hg branch', 'return' => 0, 'stdout' => $branch], - ['cmd' => 'hg branches', 'return' => 0], - ['cmd' => 'hg bookmarks', 'return' => 0], + ['cmd' => ['git', 'describe', '--exact-match', '--tags'], 'return' => 128], + ['cmd' => array_merge(['git', 'log', '--pretty=%H', '-n1', 'HEAD'], GitUtil::getNoShowSignatureFlags($process)), 'return' => 128], + ['cmd' => ['hg', 'branch'], 'return' => 0, 'stdout' => $branch], + ['cmd' => ['hg', 'branches'], 'return' => 0], + ['cmd' => ['hg', 'bookmarks'], 'return' => 0], ], true); GitUtil::getVersion(new ProcessExecutor); @@ -50,9 +51,9 @@ public function testHgGuessVersionReturnsData(): void $versionArray = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionArray); - $this->assertEquals("dev-".$branch, $versionArray['version']); - $this->assertEquals("dev-".$branch, $versionArray['pretty_version']); - $this->assertEmpty($versionArray['commit']); + self::assertEquals("dev-".$branch, $versionArray['version']); + self::assertEquals("dev-".$branch, $versionArray['pretty_version']); + self::assertEmpty($versionArray['commit']); } public function testGuessVersionReturnsData(): void @@ -74,11 +75,11 @@ public function testGuessVersionReturnsData(): void $versionArray = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionArray); - $this->assertEquals("dev-master", $versionArray['version']); - $this->assertEquals("dev-master", $versionArray['pretty_version']); - $this->assertArrayNotHasKey('feature_version', $versionArray); - $this->assertArrayNotHasKey('feature_pretty_version', $versionArray); - $this->assertEquals($commitHash, $versionArray['commit']); + self::assertEquals("dev-master", $versionArray['version']); + self::assertEquals("dev-master", $versionArray['pretty_version']); + self::assertArrayNotHasKey('feature_version', $versionArray); + self::assertArrayNotHasKey('feature_pretty_version', $versionArray); + self::assertEquals($commitHash, $versionArray['commit']); } public function testGuessVersionDoesNotSeeCustomDefaultBranchAsNonFeatureBranch(): void @@ -101,8 +102,8 @@ public function testGuessVersionDoesNotSeeCustomDefaultBranchAsNonFeatureBranch( $versionArray = $guesser->guessVersion(['version' => 'self.version'], 'dummy/path'); self::assertIsArray($versionArray); - $this->assertEquals("dev-current", $versionArray['version']); - $this->assertEquals($anotherCommitHash, $versionArray['commit']); + self::assertEquals("dev-current", $versionArray['version']); + self::assertEquals($anotherCommitHash, $versionArray['commit']); } public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationForArbitraryNaming(): void @@ -117,7 +118,7 @@ public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationF 'stdout' => " arbitrary $commitHash Commit message\n* feature $anotherCommitHash Another message\n", ], [ - 'cmd' => 'git rev-list arbitrary..feature', + 'cmd' => ['git', 'rev-list', 'arbitrary..feature'], 'stdout' => "$anotherCommitHash\n", ], ], true); @@ -128,11 +129,11 @@ public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationF $versionArray = $guesser->guessVersion(['version' => 'self.version', 'non-feature-branches' => ['arbitrary']], 'dummy/path'); self::assertIsArray($versionArray); - $this->assertEquals("dev-arbitrary", $versionArray['version']); - $this->assertEquals($anotherCommitHash, $versionArray['commit']); + self::assertEquals("dev-arbitrary", $versionArray['version']); + self::assertEquals($anotherCommitHash, $versionArray['commit']); self::assertArrayHasKey('feature_version', $versionArray); - $this->assertEquals("dev-feature", $versionArray['feature_version']); - $this->assertEquals("dev-feature", $versionArray['feature_pretty_version']); + self::assertEquals("dev-feature", $versionArray['feature_version']); + self::assertEquals("dev-feature", $versionArray['feature_pretty_version']); } public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationForArbitraryNamingRegex(): void @@ -147,7 +148,7 @@ public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationF 'stdout' => " latest-testing $commitHash Commit message\n* feature $anotherCommitHash Another message\n", ], [ - 'cmd' => 'git rev-list latest-testing..feature', + 'cmd' => ['git', 'rev-list', 'latest-testing..feature'], 'stdout' => "$anotherCommitHash\n", ], ], true); @@ -158,11 +159,11 @@ public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationF $versionArray = $guesser->guessVersion(['version' => 'self.version', 'non-feature-branches' => ['latest-.*']], 'dummy/path'); self::assertIsArray($versionArray); - $this->assertEquals("dev-latest-testing", $versionArray['version']); - $this->assertEquals($anotherCommitHash, $versionArray['commit']); + self::assertEquals("dev-latest-testing", $versionArray['version']); + self::assertEquals($anotherCommitHash, $versionArray['commit']); self::assertArrayHasKey('feature_version', $versionArray); - $this->assertEquals("dev-feature", $versionArray['feature_version']); - $this->assertEquals("dev-feature", $versionArray['feature_pretty_version']); + self::assertEquals("dev-feature", $versionArray['feature_version']); + self::assertEquals("dev-feature", $versionArray['feature_pretty_version']); } public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationForArbitraryNamingWhenOnNonFeatureBranch(): void @@ -184,10 +185,10 @@ public function testGuessVersionReadsAndRespectsNonFeatureBranchesConfigurationF $versionArray = $guesser->guessVersion(['version' => 'self.version', 'non-feature-branches' => ['latest-.*']], 'dummy/path'); self::assertIsArray($versionArray); - $this->assertEquals("dev-latest-testing", $versionArray['version']); - $this->assertEquals($commitHash, $versionArray['commit']); - $this->assertArrayNotHasKey('feature_version', $versionArray); - $this->assertArrayNotHasKey('feature_pretty_version', $versionArray); + self::assertEquals("dev-latest-testing", $versionArray['version']); + self::assertEquals($commitHash, $versionArray['commit']); + self::assertArrayNotHasKey('feature_version', $versionArray); + self::assertArrayNotHasKey('feature_pretty_version', $versionArray); } public function testDetachedHeadBecomesDevHash(): void @@ -200,7 +201,7 @@ public function testDetachedHeadBecomesDevHash(): void 'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'stdout' => "* (no branch) $commitHash Commit message\n", ], - 'git describe --exact-match --tags', + ['git', 'describe', '--exact-match', '--tags'], ], true); $config = new Config; @@ -209,7 +210,7 @@ public function testDetachedHeadBecomesDevHash(): void $versionData = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals("dev-$commitHash", $versionData['version']); + self::assertEquals("dev-$commitHash", $versionData['version']); } public function testDetachedFetchHeadBecomesDevHashGit2(): void @@ -222,7 +223,7 @@ public function testDetachedFetchHeadBecomesDevHashGit2(): void 'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'stdout' => "* (HEAD detached at FETCH_HEAD) $commitHash Commit message\n", ], - 'git describe --exact-match --tags', + ['git', 'describe', '--exact-match', '--tags'], ], true); $config = new Config; @@ -231,7 +232,7 @@ public function testDetachedFetchHeadBecomesDevHashGit2(): void $versionData = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals("dev-$commitHash", $versionData['version']); + self::assertEquals("dev-$commitHash", $versionData['version']); } public function testDetachedCommitHeadBecomesDevHashGit2(): void @@ -244,7 +245,7 @@ public function testDetachedCommitHeadBecomesDevHashGit2(): void 'cmd' => ['git', 'branch', '-a', '--no-color', '--no-abbrev', '-v'], 'stdout' => "* (HEAD detached at 03a15d220) $commitHash Commit message\n", ], - 'git describe --exact-match --tags', + ['git', 'describe', '--exact-match', '--tags'], ], true); $config = new Config; @@ -253,7 +254,7 @@ public function testDetachedCommitHeadBecomesDevHashGit2(): void $versionData = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals("dev-$commitHash", $versionData['version']); + self::assertEquals("dev-$commitHash", $versionData['version']); } public function testTagBecomesVersion(): void @@ -265,7 +266,7 @@ public function testTagBecomesVersion(): void 'stdout' => "* (HEAD detached at v2.0.5-alpha2) 433b98d4218c181bae01865901aac045585e8a1a Commit message\n", ], [ - 'cmd' => 'git describe --exact-match --tags', + 'cmd' => ['git', 'describe', '--exact-match', '--tags'], 'stdout' => "v2.0.5-alpha2", ], ], true); @@ -276,7 +277,7 @@ public function testTagBecomesVersion(): void $versionData = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals("2.0.5.0-alpha2", $versionData['version']); + self::assertEquals("2.0.5.0-alpha2", $versionData['version']); } public function testTagBecomesPrettyVersion(): void @@ -288,7 +289,7 @@ public function testTagBecomesPrettyVersion(): void 'stdout' => "* (HEAD detached at 1.0.0) c006f0c12bbbf197b5c071ffb1c0e9812bb14a4d Commit message\n", ], [ - 'cmd' => 'git describe --exact-match --tags', + 'cmd' => ['git', 'describe', '--exact-match', '--tags'], 'stdout' => '1.0.0', ], ], true); @@ -299,8 +300,8 @@ public function testTagBecomesPrettyVersion(): void $versionData = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals('1.0.0.0', $versionData['version']); - $this->assertEquals('1.0.0', $versionData['pretty_version']); + self::assertEquals('1.0.0.0', $versionData['version']); + self::assertEquals('1.0.0', $versionData['pretty_version']); } public function testInvalidTagBecomesVersion(): void @@ -319,7 +320,7 @@ public function testInvalidTagBecomesVersion(): void $versionData = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals("dev-foo", $versionData['version']); + self::assertEquals("dev-foo", $versionData['version']); } public function testNumericBranchesShowNicely(): void @@ -338,8 +339,8 @@ public function testNumericBranchesShowNicely(): void $versionData = $guesser->guessVersion([], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals("1.5.x-dev", $versionData['pretty_version']); - $this->assertEquals("1.5.9999999.9999999-dev", $versionData['version']); + self::assertEquals("1.5.x-dev", $versionData['pretty_version']); + self::assertEquals("1.5.9999999.9999999-dev", $versionData['version']); } public function testRemoteBranchesAreSelected(): void @@ -352,7 +353,7 @@ public function testRemoteBranchesAreSelected(): void "remotes/origin/1.5 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n", ], [ - 'cmd' => 'git rev-list remotes/origin/1.5..feature-branch', + 'cmd' => ['git', 'rev-list', 'remotes/origin/1.5..feature-branch'], 'stdout' => "\n", ], ], true); @@ -362,7 +363,32 @@ public function testRemoteBranchesAreSelected(): void $guesser = new VersionGuesser($config, $process, new VersionParser()); $versionData = $guesser->guessVersion(['version' => 'self.version'], 'dummy/path'); self::assertIsArray($versionData); - $this->assertEquals("1.5.x-dev", $versionData['pretty_version']); - $this->assertEquals("1.5.9999999.9999999-dev", $versionData['version']); + self::assertEquals("1.5.x-dev", $versionData['pretty_version']); + self::assertEquals("1.5.9999999.9999999-dev", $versionData['version']); + } + + /** + * @dataProvider rootEnvVersionsProvider + */ + public function testGetRootVersionFromEnv(string $env, string $expectedVersion): void + { + Platform::putEnv('COMPOSER_ROOT_VERSION', $env); + $guesser = new VersionGuesser(new Config, $this->getProcessExecutorMock(), new VersionParser()); + self::assertSame($expectedVersion, $guesser->getRootVersionFromEnv()); + Platform::clearEnv('COMPOSER_ROOT_VERSION'); + } + + /** + * @return array + */ + public function rootEnvVersionsProvider(): array + { + return [ + ['1.0-dev', '1.0.x-dev'], + ['1.0.x-dev', '1.0.x-dev'], + ['1-dev', '1.x-dev'], + ['1.x-dev', '1.x-dev'], + ['1.0.0', '1.0.0'], + ]; } } diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php index c0db2142af79..845b52dfeeb4 100644 --- a/tests/Composer/Test/Package/Version/VersionParserTest.php +++ b/tests/Composer/Test/Package/Version/VersionParserTest.php @@ -27,7 +27,7 @@ public function testParseNameVersionPairs(array $pairs, array $result): void { $versionParser = new VersionParser(); - $this->assertSame($result, $versionParser->parseNameVersionPairs($pairs)); + self::assertSame($result, $versionParser->parseNameVersionPairs($pairs)); } public static function provideParseNameVersionPairsData(): array @@ -46,7 +46,7 @@ public static function provideParseNameVersionPairsData(): array */ public function testIsUpgrade(string $from, string $to, bool $expected): void { - $this->assertSame($expected, VersionParser::isUpgrade($from, $to)); + self::assertSame($expected, VersionParser::isUpgrade($from, $to)); } public static function provideIsUpgradeTests(): array diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php index aec5cf86caa7..e4d508cd8f1b 100644 --- a/tests/Composer/Test/Package/Version/VersionSelectorTest.php +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -48,7 +48,7 @@ public function testLatestVersionIsReturned(): void $best = $versionSelector->findBestCandidate($packageName); // 1.2.2 should be returned because it's the latest of the returned versions - $this->assertSame($package2, $best, 'Latest version should be 1.2.2'); + self::assertSame($package2, $best, 'Latest version should be 1.2.2'); } public function testLatestVersionIsReturnedThatMatchesPhpRequirements(): void @@ -77,12 +77,12 @@ public function testLatestVersionIsReturnedThatMatchesPhpRequirements(): void $io = new BufferIO(); $best = $versionSelector->findBestCandidate($packageName, null, 'stable', null, 0, $io); - $this->assertSame((string) $package1, (string) $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); + self::assertSame((string) $package1, (string) $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); self::assertSame("Cannot use foo/bar's latest version 2.1.0 as it requires php >=5.6 which is not satisfied by your platform.".PHP_EOL, $io->getOutput()); $io = new BufferIO('', StreamOutput::VERBOSITY_VERBOSE); $best = $versionSelector->findBestCandidate($packageName, null, 'stable', null, 0, $io); - $this->assertSame((string) $package1, (string) $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); + self::assertSame((string) $package1, (string) $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); self::assertSame( "Cannot use foo/bar's latest version 2.1.0 as it requires php >=5.6 which is not satisfied by your platform.".PHP_EOL ."Cannot use foo/bar 2.0.0 as it requires php >=5.6 which is not satisfied by your platform.".PHP_EOL, @@ -90,7 +90,7 @@ public function testLatestVersionIsReturnedThatMatchesPhpRequirements(): void ); $best = $versionSelector->findBestCandidate($packageName, null, 'stable', PlatformRequirementFilterFactory::ignoreAll()); - $this->assertSame((string) $package3, (string) $best, 'Latest version should be returned when ignoring platform reqs (2.1.0)'); + self::assertSame((string) $package3, (string) $best, 'Latest version should be returned when ignoring platform reqs (2.1.0)'); } public function testLatestVersionIsReturnedThatMatchesExtRequirements(): void @@ -114,9 +114,9 @@ public function testLatestVersionIsReturnedThatMatchesExtRequirements(): void ->will($this->returnValue($packages)); $best = $versionSelector->findBestCandidate($packageName); - $this->assertSame($package1, $best, 'Latest version supporting ext-zip 5.3.0 should be returned (1.0.0)'); + self::assertSame($package1, $best, 'Latest version supporting ext-zip 5.3.0 should be returned (1.0.0)'); $best = $versionSelector->findBestCandidate($packageName, null, 'stable', PlatformRequirementFilterFactory::ignoreAll()); - $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); + self::assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); } public function testLatestVersionIsReturnedThatMatchesPlatformExt(): void @@ -139,9 +139,9 @@ public function testLatestVersionIsReturnedThatMatchesPlatformExt(): void ->will($this->returnValue($packages)); $best = $versionSelector->findBestCandidate($packageName); - $this->assertSame($package1, $best, 'Latest version not requiring ext-barfoo should be returned (1.0.0)'); + self::assertSame($package1, $best, 'Latest version not requiring ext-barfoo should be returned (1.0.0)'); $best = $versionSelector->findBestCandidate($packageName, null, 'stable', PlatformRequirementFilterFactory::ignoreAll()); - $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); + self::assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); } public function testLatestVersionIsReturnedThatMatchesComposerRequirements(): void @@ -165,9 +165,9 @@ public function testLatestVersionIsReturnedThatMatchesComposerRequirements(): vo ->will($this->returnValue($packages)); $best = $versionSelector->findBestCandidate($packageName); - $this->assertSame($package1, $best, 'Latest version supporting composer 1 should be returned (1.0.0)'); + self::assertSame($package1, $best, 'Latest version supporting composer 1 should be returned (1.0.0)'); $best = $versionSelector->findBestCandidate($packageName, null, 'stable', PlatformRequirementFilterFactory::ignoreAll()); - $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (1.1.0)'); + self::assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (1.1.0)'); } public function testMostStableVersionIsReturned(): void @@ -187,7 +187,7 @@ public function testMostStableVersionIsReturned(): void $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName); - $this->assertSame($package1, $best, 'Latest most stable version should be returned (1.0.0)'); + self::assertSame($package1, $best, 'Latest most stable version should be returned (1.0.0)'); } public function testMostStableVersionIsReturnedRegardlessOfOrder(): void @@ -209,10 +209,10 @@ public function testMostStableVersionIsReturnedRegardlessOfOrder(): void $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName); - $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); + self::assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); $best = $versionSelector->findBestCandidate($packageName); - $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); + self::assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); } public function testHighestVersionIsReturned(): void @@ -232,7 +232,7 @@ public function testHighestVersionIsReturned(): void $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, 'dev'); - $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); + self::assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } public function testHighestVersionMatchingStabilityIsReturned(): void @@ -253,7 +253,7 @@ public function testHighestVersionMatchingStabilityIsReturned(): void $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, 'beta'); - $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); + self::assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } public function testMostStableUnstableVersionIsReturned(): void @@ -273,7 +273,7 @@ public function testMostStableUnstableVersionIsReturned(): void $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, 'stable'); - $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); + self::assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } public function testDefaultBranchAliasIsNeverReturned(): void @@ -294,7 +294,7 @@ public function testDefaultBranchAliasIsNeverReturned(): void $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate($packageName, null, 'dev'); - $this->assertSame($package2, $best, 'Latest version should be returned (dev-main)'); + self::assertSame($package2, $best, 'Latest version should be returned (dev-main)'); } public function testFalseReturnedOnNoPackages(): void @@ -306,7 +306,7 @@ public function testFalseReturnedOnNoPackages(): void $versionSelector = new VersionSelector($repositorySet); $best = $versionSelector->findBestCandidate('foobaz'); - $this->assertFalse($best, 'No versions are available returns false'); + self::assertFalse($best, 'No versions are available returns false'); } /** @@ -327,7 +327,7 @@ public function testFindRecommendedRequireVersion(string $prettyVersion, string $recommended = $versionSelector->findRecommendedRequireVersion($package); // assert that the recommended version is what we expect - $this->assertSame($expectedVersion, $recommended); + self::assertSame($expectedVersion, $recommended); } public static function provideRecommendedRequireVersionPackages(): array diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index a5eb07d3ecf6..ba2402a19c11 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -160,8 +160,8 @@ public function testInstallNewPlugin(): void $installer->install($this->repository, $this->packages[0]); $plugins = $this->pm->getPlugins(); - $this->assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore-line - $this->assertEquals( + self::assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore property.notFound + self::assertEquals( 'activate v1'.PHP_EOL, $this->io->getOutput() ); @@ -182,11 +182,11 @@ public function testInstallPluginWithRootPackageHavingFilesAutoload(): void $installer->install($this->repository, $this->packages[0]); $plugins = $this->pm->getPlugins(); - $this->assertEquals( + self::assertEquals( 'activate v1'.PHP_EOL, $this->io->getOutput() ); - $this->assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore-line + self::assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore property.notFound } public function testInstallMultiplePlugins(): void @@ -201,11 +201,11 @@ public function testInstallMultiplePlugins(): void $installer->install($this->repository, $this->packages[3]); $plugins = $this->pm->getPlugins(); - $this->assertEquals('plugin1', $plugins[0]->name); // @phpstan-ignore-line - $this->assertEquals('installer-v4', $plugins[0]->version); // @phpstan-ignore-line - $this->assertEquals('plugin2', $plugins[1]->name); // @phpstan-ignore-line - $this->assertEquals('installer-v4', $plugins[1]->version); // @phpstan-ignore-line - $this->assertEquals('activate v4-plugin1'.PHP_EOL.'activate v4-plugin2'.PHP_EOL, $this->io->getOutput()); + self::assertEquals('plugin1', $plugins[0]->name); // @phpstan-ignore property.notFound + self::assertEquals('installer-v4', $plugins[0]->version); // @phpstan-ignore property.notFound + self::assertEquals('plugin2', $plugins[1]->name); // @phpstan-ignore property.notFound + self::assertEquals('installer-v4', $plugins[1]->version); // @phpstan-ignore property.notFound + self::assertEquals('activate v4-plugin1'.PHP_EOL.'activate v4-plugin2'.PHP_EOL, $this->io->getOutput()); } public function testUpgradeWithNewClassName(): void @@ -224,9 +224,9 @@ public function testUpgradeWithNewClassName(): void $installer->update($this->repository, $this->packages[0], $this->packages[1]); $plugins = $this->pm->getPlugins(); - $this->assertCount(1, $plugins); - $this->assertEquals('installer-v2', $plugins[1]->version); // @phpstan-ignore-line - $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'activate v2'.PHP_EOL, $this->io->getOutput()); + self::assertCount(1, $plugins); + self::assertEquals('installer-v2', $plugins[1]->version); // @phpstan-ignore property.notFound + self::assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'activate v2'.PHP_EOL, $this->io->getOutput()); } public function testUninstall(): void @@ -245,8 +245,8 @@ public function testUninstall(): void $installer->uninstall($this->repository, $this->packages[0]); $plugins = $this->pm->getPlugins(); - $this->assertCount(0, $plugins); - $this->assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'uninstall v1'.PHP_EOL, $this->io->getOutput()); + self::assertCount(0, $plugins); + self::assertEquals('activate v1'.PHP_EOL.'deactivate v1'.PHP_EOL.'uninstall v1'.PHP_EOL, $this->io->getOutput()); } public function testUpgradeWithSameClassName(): void @@ -265,8 +265,8 @@ public function testUpgradeWithSameClassName(): void $installer->update($this->repository, $this->packages[1], $this->packages[2]); $plugins = $this->pm->getPlugins(); - $this->assertEquals('installer-v3', $plugins[1]->version); // @phpstan-ignore-line - $this->assertEquals('activate v2'.PHP_EOL.'deactivate v2'.PHP_EOL.'activate v3'.PHP_EOL, $this->io->getOutput()); + self::assertEquals('installer-v3', $plugins[1]->version); // @phpstan-ignore property.notFound + self::assertEquals('activate v2'.PHP_EOL.'deactivate v2'.PHP_EOL.'activate v3'.PHP_EOL, $this->io->getOutput()); } public function testRegisterPluginOnlyOneTime(): void @@ -282,9 +282,9 @@ public function testRegisterPluginOnlyOneTime(): void $installer->install($this->repository, clone $this->packages[0]); $plugins = $this->pm->getPlugins(); - $this->assertCount(1, $plugins); - $this->assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore-line - $this->assertEquals('activate v1'.PHP_EOL, $this->io->getOutput()); + self::assertCount(1, $plugins); + self::assertEquals('installer-v1', $plugins[0]->version); // @phpstan-ignore property.notFound + self::assertEquals('activate v1'.PHP_EOL, $this->io->getOutput()); } /** @@ -325,16 +325,16 @@ public function testStarPluginVersionWorksWithAnyAPIVersion(): void $starVersionPlugin = [$this->packages[4]]; $this->setPluginApiVersionWithPlugins('1.0.0', $starVersionPlugin); - $this->assertCount(1, $this->pm->getPlugins()); + self::assertCount(1, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('1.9.9', $starVersionPlugin); - $this->assertCount(1, $this->pm->getPlugins()); + self::assertCount(1, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('2.0.0-dev', $starVersionPlugin); - $this->assertCount(1, $this->pm->getPlugins()); + self::assertCount(1, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('100.0.0-stable', $starVersionPlugin); - $this->assertCount(1, $this->pm->getPlugins()); + self::assertCount(1, $this->pm->getPlugins()); } public function testPluginConstraintWorksOnlyWithCertainAPIVersion(): void @@ -342,16 +342,16 @@ public function testPluginConstraintWorksOnlyWithCertainAPIVersion(): void $pluginWithApiConstraint = [$this->packages[5]]; $this->setPluginApiVersionWithPlugins('1.0.0', $pluginWithApiConstraint); - $this->assertCount(0, $this->pm->getPlugins()); + self::assertCount(0, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('1.1.9', $pluginWithApiConstraint); - $this->assertCount(0, $this->pm->getPlugins()); + self::assertCount(0, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('1.2.0', $pluginWithApiConstraint); - $this->assertCount(1, $this->pm->getPlugins()); + self::assertCount(1, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('1.9.9', $pluginWithApiConstraint); - $this->assertCount(1, $this->pm->getPlugins()); + self::assertCount(1, $this->pm->getPlugins()); } public function testPluginRangeConstraintsWorkOnlyWithCertainAPIVersion(): void @@ -359,13 +359,13 @@ public function testPluginRangeConstraintsWorkOnlyWithCertainAPIVersion(): void $pluginWithApiConstraint = [$this->packages[6]]; $this->setPluginApiVersionWithPlugins('1.0.0', $pluginWithApiConstraint); - $this->assertCount(0, $this->pm->getPlugins()); + self::assertCount(0, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('3.0.0', $pluginWithApiConstraint); - $this->assertCount(1, $this->pm->getPlugins()); + self::assertCount(1, $this->pm->getPlugins()); $this->setPluginApiVersionWithPlugins('5.5.0', $pluginWithApiConstraint); - $this->assertCount(0, $this->pm->getPlugins()); + self::assertCount(0, $this->pm->getPlugins()); } public function testCommandProviderCapability(): void @@ -379,19 +379,19 @@ public function testCommandProviderCapability(): void /** @var \Composer\Plugin\Capability\CommandProvider[] $caps */ $caps = $this->pm->getPluginCapabilities('Composer\Plugin\Capability\CommandProvider', ['composer' => $this->composer, 'io' => $this->io]); - $this->assertCount(1, $caps); - $this->assertInstanceOf('Composer\Plugin\Capability\CommandProvider', $caps[0]); + self::assertCount(1, $caps); + self::assertInstanceOf('Composer\Plugin\Capability\CommandProvider', $caps[0]); $commands = $caps[0]->getCommands(); - $this->assertCount(1, $commands); - $this->assertInstanceOf('Composer\Command\BaseCommand', $commands[0]); + self::assertCount(1, $commands); + self::assertInstanceOf('Composer\Command\BaseCommand', $commands[0]); } public function testIncapablePluginIsCorrectlyDetected(): void { $plugin = $this->getMockBuilder('Composer\Plugin\PluginInterface') ->getMock(); - $this->assertNull($this->pm->getPluginCapability($plugin, 'Fake\Ability')); + self::assertNull($this->pm->getPluginCapability($plugin, 'Fake\Ability')); } public function testCapabilityImplementsComposerPluginApiClassAndIsConstructedWithArgs(): void @@ -411,9 +411,9 @@ public function testCapabilityImplementsComposerPluginApiClassAndIsConstructedWi /** @var \Composer\Test\Plugin\Mock\Capability $capability */ $capability = $this->pm->getPluginCapability($plugin, $capabilityApi, ['a' => 1, 'b' => 2]); - $this->assertInstanceOf($capabilityApi, $capability); - $this->assertInstanceOf($capabilityImplementation, $capability); - $this->assertSame(['a' => 1, 'b' => 2, 'plugin' => $plugin], $capability->args); + self::assertInstanceOf($capabilityApi, $capability); + self::assertInstanceOf($capabilityImplementation, $capability); + self::assertSame(['a' => 1, 'b' => 2, 'plugin' => $plugin], $capability->args); } /** @return mixed[] */ @@ -467,7 +467,7 @@ public function testQueryingNonProvidedCapabilityReturnsNullSafely(): void return []; })); - $this->assertNull($this->pm->getPluginCapability($plugin, $capabilityApi)); + self::assertNull($this->pm->getPluginCapability($plugin, $capabilityApi)); } /** @return mixed[] */ diff --git a/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php index 371f4959792e..bfad93083bc0 100644 --- a/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php +++ b/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php @@ -63,7 +63,7 @@ public function testAskConfirmation(string $question, bool $expected, bool $defa [$input, $dialog] = $this->createInput($question."\n"); $question = new StrictConfirmationQuestion('Do you like French fries?', $default); - $this->assertEquals($expected, $dialog->ask($input, $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel')); + self::assertEquals($expected, $dialog->ask($input, $this->createOutputInterface(), $question), 'confirmation question should '.($expected ? 'pass' : 'cancel')); } /** @@ -88,10 +88,10 @@ public function testAskConfirmationWithCustomTrueAndFalseAnswer(): void $question = new StrictConfirmationQuestion('Do you like French fries?', false, '/^ja$/i', '/^nein$/i'); [$input, $dialog] = $this->createInput("ja\n"); - $this->assertTrue($dialog->ask($input, $this->createOutputInterface(), $question)); + self::assertTrue($dialog->ask($input, $this->createOutputInterface(), $question)); [$input, $dialog] = $this->createInput("nein\n"); - $this->assertFalse($dialog->ask($input, $this->createOutputInterface(), $question)); + self::assertFalse($dialog->ask($input, $this->createOutputInterface(), $question)); } /** @@ -100,7 +100,7 @@ public function testAskConfirmationWithCustomTrueAndFalseAnswer(): void protected function getInputStream(string $input) { $stream = fopen('php://memory', 'r+', false); - $this->assertNotFalse($stream); + self::assertNotFalse($stream); fwrite($stream, $input); rewind($stream); diff --git a/tests/Composer/Test/Repository/ArrayRepositoryTest.php b/tests/Composer/Test/Repository/ArrayRepositoryTest.php index 95d56a867109..a06fb648ce5e 100644 --- a/tests/Composer/Test/Repository/ArrayRepositoryTest.php +++ b/tests/Composer/Test/Repository/ArrayRepositoryTest.php @@ -23,7 +23,7 @@ public function testAddPackage(): void $repo = new ArrayRepository; $repo->addPackage(self::getPackage('foo', '1')); - $this->assertCount(1, $repo); + self::assertCount(1, $repo); } public function testRemovePackage(): void @@ -34,12 +34,12 @@ public function testRemovePackage(): void $repo->addPackage(self::getPackage('foo', '1')); $repo->addPackage($package); - $this->assertCount(2, $repo); + self::assertCount(2, $repo); $repo->removePackage(self::getPackage('foo', '1')); - $this->assertCount(1, $repo); - $this->assertEquals([$package], $repo->getPackages()); + self::assertCount(1, $repo); + self::assertEquals([$package], $repo->getPackages()); } public function testHasPackage(): void @@ -48,8 +48,8 @@ public function testHasPackage(): void $repo->addPackage(self::getPackage('foo', '1')); $repo->addPackage(self::getPackage('bar', '2')); - $this->assertTrue($repo->hasPackage(self::getPackage('foo', '1'))); - $this->assertFalse($repo->hasPackage(self::getPackage('bar', '1'))); + self::assertTrue($repo->hasPackage(self::getPackage('foo', '1'))); + self::assertFalse($repo->hasPackage(self::getPackage('bar', '1'))); } public function testFindPackages(): void @@ -60,12 +60,12 @@ public function testFindPackages(): void $repo->addPackage(self::getPackage('bar', '3')); $foo = $repo->findPackages('foo'); - $this->assertCount(1, $foo); - $this->assertEquals('foo', $foo[0]->getName()); + self::assertCount(1, $foo); + self::assertEquals('foo', $foo[0]->getName()); $bar = $repo->findPackages('bar'); - $this->assertCount(2, $bar); - $this->assertEquals('bar', $bar[0]->getName()); + self::assertCount(2, $bar); + self::assertEquals('bar', $bar[0]->getName()); } public function testAutomaticallyAddAliasedPackageButNotRemove(): void @@ -77,13 +77,13 @@ public function testAutomaticallyAddAliasedPackageButNotRemove(): void $repo->addPackage($alias); - $this->assertCount(2, $repo); - $this->assertTrue($repo->hasPackage(self::getPackage('foo', '1'))); - $this->assertTrue($repo->hasPackage(self::getPackage('foo', '2'))); + self::assertCount(2, $repo); + self::assertTrue($repo->hasPackage(self::getPackage('foo', '1'))); + self::assertTrue($repo->hasPackage(self::getPackage('foo', '2'))); $repo->removePackage($alias); - $this->assertCount(1, $repo); + self::assertCount(1, $repo); } public function testSearch(): void @@ -93,17 +93,17 @@ public function testSearch(): void $repo->addPackage(self::getPackage('foo', '1')); $repo->addPackage(self::getPackage('bar', '1')); - $this->assertSame( + self::assertSame( [['name' => 'foo', 'description' => null]], $repo->search('foo', RepositoryInterface::SEARCH_FULLTEXT) ); - $this->assertSame( + self::assertSame( [['name' => 'bar', 'description' => null]], $repo->search('bar') ); - $this->assertEmpty( + self::assertEmpty( $repo->search('foobar') ); } @@ -119,14 +119,14 @@ public function testSearchWithPackageType(): void $package->setType('composer-plugin'); $repo->addPackage($package); - $this->assertSame( + self::assertSame( [['name' => 'foo', 'description' => null]], $repo->search('foo', RepositoryInterface::SEARCH_FULLTEXT, 'library') ); - $this->assertEmpty($repo->search('bar', RepositoryInterface::SEARCH_FULLTEXT, 'package')); + self::assertEmpty($repo->search('bar', RepositoryInterface::SEARCH_FULLTEXT, 'package')); - $this->assertSame( + self::assertSame( [['name' => 'foobar', 'description' => null]], $repo->search('foo', 0, 'composer-plugin') ); @@ -143,7 +143,7 @@ public function testSearchWithAbandonedPackages(): void $package2->setAbandoned('bar'); $repo->addPackage($package2); - $this->assertSame( + self::assertSame( [ ['name' => 'foo1', 'description' => null, 'abandoned' => true], ['name' => 'foo2', 'description' => null, 'abandoned' => 'bar'], diff --git a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php index 52b2b568c2b9..1db3263a11f7 100644 --- a/tests/Composer/Test/Repository/ArtifactRepositoryTest.php +++ b/tests/Composer/Test/Repository/ArtifactRepositoryTest.php @@ -51,14 +51,14 @@ public function testExtractsConfigsFromZipArchives(): void sort($expectedPackages); sort($foundPackages); - $this->assertSame($expectedPackages, $foundPackages); + self::assertSame($expectedPackages, $foundPackages); $tarPackage = array_filter($repo->getPackages(), static function (BasePackage $package): bool { return $package->getPrettyName() === 'test/jsonInRootTarFile'; }); - $this->assertCount(1, $tarPackage); + self::assertCount(1, $tarPackage); $tarPackage = array_pop($tarPackage); - $this->assertSame('tar', $tarPackage->getDistType()); + self::assertSame('tar', $tarPackage->getDistType()); } public function testAbsoluteRepoUrlCreatesAbsoluteUrlPackages(): void @@ -68,7 +68,7 @@ public function testAbsoluteRepoUrlCreatesAbsoluteUrlPackages(): void $repo = new ArtifactRepository($coordinates, new NullIO()); foreach ($repo->getPackages() as $package) { - $this->assertSame(strpos($package->getDistUrl(), strtr($absolutePath, '\\', '/')), 0); + self::assertSame(strpos($package->getDistUrl(), strtr($absolutePath, '\\', '/')), 0); } } @@ -79,7 +79,7 @@ public function testRelativeRepoUrlCreatesRelativeUrlPackages(): void $repo = new ArtifactRepository($coordinates, new NullIO()); foreach ($repo->getPackages() as $package) { - $this->assertSame(strpos($package->getDistUrl(), $relativePath), 0); + self::assertSame(strpos($package->getDistUrl(), $relativePath), 0); } } } diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 6eba1081dc32..5355c07a66fe 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -55,7 +55,7 @@ public function testLoadData(array $expected, array $repoPackages): void $packages = $repository->getPackages(); // Final sanity check, ensure the correct number of packages were added. - $this->assertCount(count($expected), $packages); + self::assertCount(count($expected), $packages); foreach ($expected as $index => $pkg) { self::assertSame($pkg['name'].' '.$pkg['version'], $packages[$index]->getName().' '.$packages[$index]->getPrettyVersion()); @@ -165,9 +165,9 @@ public function testWhatProvides(): void $reflMethod->setAccessible(true); $packages = $reflMethod->invoke($repo, 'a'); - $this->assertCount(5, $packages); - $this->assertEquals(['1', '1-alias', '2', '2-alias', '3'], array_keys($packages)); - $this->assertSame($packages['2'], $packages['2-alias']->getAliasOf()); + self::assertCount(5, $packages); + self::assertEquals(['1', '1-alias', '2', '2-alias', '3'], array_keys($packages)); + self::assertSame($packages['2'], $packages['2-alias']->getAliasOf()); } public function testSearchWithType(): void @@ -202,12 +202,12 @@ public function testSearchWithType(): void $config->merge(['config' => ['cache-read-only' => true]]); $repository = new ComposerRepository($repoConfig, new NullIO, $config, $httpDownloader, $eventDispatcher); - $this->assertSame( + self::assertSame( [['name' => 'foo', 'description' => null]], $repository->search('foo', RepositoryInterface::SEARCH_FULLTEXT, 'composer-plugin') ); - $this->assertEmpty( + self::assertEmpty( $repository->search('foo', RepositoryInterface::SEARCH_FULLTEXT, 'library') ); } @@ -234,7 +234,7 @@ public function testSearchWithSpecialChars(): void $config->merge(['config' => ['cache-read-only' => true]]); $repository = new ComposerRepository($repoConfig, new NullIO, $config, $httpDownloader, $eventDispatcher); - $this->assertEmpty( + self::assertEmpty( $repository->search('foo bar', RepositoryInterface::SEARCH_FULLTEXT) ); } @@ -277,7 +277,7 @@ public function testSearchWithAbandonedPackages(): void $config->merge(['config' => ['cache-read-only' => true]]); $repository = new ComposerRepository($repoConfig, new NullIO, $config, $httpDownloader, $eventDispatcher); - $this->assertSame( + self::assertSame( [ ['name' => 'foo1', 'description' => null, 'abandoned' => true], ['name' => 'foo2', 'description' => null, 'abandoned' => 'bar'], @@ -312,7 +312,7 @@ public function testCanonicalizeUrl(string $expected, string $url, string $repos $property->setAccessible(true); $property->setValue($repository, $repositoryUrl); - $this->assertSame($expected, $method->invoke($repository, $url)); + self::assertSame($expected, $method->invoke($repository, $url)); } public static function provideCanonicalizeUrlTestCases(): array @@ -379,7 +379,7 @@ public function testGetProviderNamesWillReturnPartialPackageNames(): void $httpDownloader ); - $this->assertEquals(['foo/bar'], $repository->getPackageNames()); + self::assertEquals(['foo/bar'], $repository->getPackageNames()); } public function testGetSecurityAdvisoriesAssertRepositoryHttpOptionsAreUsed(): void @@ -425,7 +425,7 @@ public function testGetSecurityAdvisoriesAssertRepositoryHttpOptionsAreUsed(): v $httpDownloader ); - $this->assertSame([ + self::assertSame([ 'namesFound' => [], 'advisories' => [], ], $repository->getSecurityAdvisories(['foo/bar' => new Constraint('=', '1.0.0.0')])); diff --git a/tests/Composer/Test/Repository/CompositeRepositoryTest.php b/tests/Composer/Test/Repository/CompositeRepositoryTest.php index 098396c9b392..ea2e7ffda14d 100644 --- a/tests/Composer/Test/Repository/CompositeRepositoryTest.php +++ b/tests/Composer/Test/Repository/CompositeRepositoryTest.php @@ -28,11 +28,11 @@ public function testHasPackage(): void $repo = new CompositeRepository([$arrayRepoOne, $arrayRepoTwo]); - $this->assertTrue($repo->hasPackage(self::getPackage('foo', '1')), "Should have package 'foo/1'"); - $this->assertTrue($repo->hasPackage(self::getPackage('bar', '1')), "Should have package 'bar/1'"); + self::assertTrue($repo->hasPackage(self::getPackage('foo', '1')), "Should have package 'foo/1'"); + self::assertTrue($repo->hasPackage(self::getPackage('bar', '1')), "Should have package 'bar/1'"); - $this->assertFalse($repo->hasPackage(self::getPackage('foo', '2')), "Should not have package 'foo/2'"); - $this->assertFalse($repo->hasPackage(self::getPackage('bar', '2')), "Should not have package 'bar/2'"); + self::assertFalse($repo->hasPackage(self::getPackage('foo', '2')), "Should not have package 'foo/2'"); + self::assertFalse($repo->hasPackage(self::getPackage('bar', '2')), "Should not have package 'bar/2'"); } public function testFindPackage(): void @@ -45,11 +45,11 @@ public function testFindPackage(): void $repo = new CompositeRepository([$arrayRepoOne, $arrayRepoTwo]); - $this->assertEquals('foo', $repo->findPackage('foo', '1')->getName(), "Should find package 'foo/1' and get name of 'foo'"); - $this->assertEquals('1', $repo->findPackage('foo', '1')->getPrettyVersion(), "Should find package 'foo/1' and get pretty version of '1'"); - $this->assertEquals('bar', $repo->findPackage('bar', '1')->getName(), "Should find package 'bar/1' and get name of 'bar'"); - $this->assertEquals('1', $repo->findPackage('bar', '1')->getPrettyVersion(), "Should find package 'bar/1' and get pretty version of '1'"); - $this->assertNull($repo->findPackage('foo', '2'), "Should not find package 'foo/2'"); + self::assertEquals('foo', $repo->findPackage('foo', '1')->getName(), "Should find package 'foo/1' and get name of 'foo'"); + self::assertEquals('1', $repo->findPackage('foo', '1')->getPrettyVersion(), "Should find package 'foo/1' and get pretty version of '1'"); + self::assertEquals('bar', $repo->findPackage('bar', '1')->getName(), "Should find package 'bar/1' and get name of 'bar'"); + self::assertEquals('1', $repo->findPackage('bar', '1')->getPrettyVersion(), "Should find package 'bar/1' and get pretty version of '1'"); + self::assertNull($repo->findPackage('foo', '2'), "Should not find package 'foo/2'"); } public function testFindPackages(): void @@ -67,16 +67,16 @@ public function testFindPackages(): void $repo = new CompositeRepository([$arrayRepoOne, $arrayRepoTwo]); $bats = $repo->findPackages('bat'); - $this->assertCount(1, $bats, "Should find one instance of 'bats' (defined in just one repository)"); - $this->assertEquals('bat', $bats[0]->getName(), "Should find packages named 'bat'"); + self::assertCount(1, $bats, "Should find one instance of 'bats' (defined in just one repository)"); + self::assertEquals('bat', $bats[0]->getName(), "Should find packages named 'bat'"); $bars = $repo->findPackages('bar'); - $this->assertCount(2, $bars, "Should find two instances of 'bar' (both defined in the same repository)"); - $this->assertEquals('bar', $bars[0]->getName(), "Should find packages named 'bar'"); + self::assertCount(2, $bars, "Should find two instances of 'bar' (both defined in the same repository)"); + self::assertEquals('bar', $bars[0]->getName(), "Should find packages named 'bar'"); $foos = $repo->findPackages('foo'); - $this->assertCount(3, $foos, "Should find three instances of 'foo' (two defined in one repository, the third in the other)"); - $this->assertEquals('foo', $foos[0]->getName(), "Should find packages named 'foo'"); + self::assertCount(3, $foos, "Should find three instances of 'foo' (two defined in one repository, the third in the other)"); + self::assertEquals('foo', $foos[0]->getName(), "Should find packages named 'foo'"); } public function testGetPackages(): void @@ -90,11 +90,11 @@ public function testGetPackages(): void $repo = new CompositeRepository([$arrayRepoOne, $arrayRepoTwo]); $packages = $repo->getPackages(); - $this->assertCount(2, $packages, "Should get two packages"); - $this->assertEquals("foo", $packages[0]->getName(), "First package should have name of 'foo'"); - $this->assertEquals("1", $packages[0]->getPrettyVersion(), "First package should have pretty version of '1'"); - $this->assertEquals("bar", $packages[1]->getName(), "Second package should have name of 'bar'"); - $this->assertEquals("1", $packages[1]->getPrettyVersion(), "Second package should have pretty version of '1'"); + self::assertCount(2, $packages, "Should get two packages"); + self::assertEquals("foo", $packages[0]->getName(), "First package should have name of 'foo'"); + self::assertEquals("1", $packages[0]->getPrettyVersion(), "First package should have pretty version of '1'"); + self::assertEquals("bar", $packages[1]->getName(), "Second package should have name of 'bar'"); + self::assertEquals("1", $packages[1]->getPrettyVersion(), "Second package should have pretty version of '1'"); } public function testAddRepository(): void @@ -108,9 +108,9 @@ public function testAddRepository(): void $arrayRepoTwo->addPackage(self::getPackage('bar', '3')); $repo = new CompositeRepository([$arrayRepoOne]); - $this->assertCount(1, $repo, "Composite repository should have just one package before addRepository() is called"); + self::assertCount(1, $repo, "Composite repository should have just one package before addRepository() is called"); $repo->addRepository($arrayRepoTwo); - $this->assertCount(4, $repo, "Composite repository should have four packages after addRepository() is called"); + self::assertCount(4, $repo, "Composite repository should have four packages after addRepository() is called"); } public function testCount(): void @@ -123,7 +123,7 @@ public function testCount(): void $repo = new CompositeRepository([$arrayRepoOne, $arrayRepoTwo]); - $this->assertCount(2, $repo, "Should return '2' for count(\$repo)"); + self::assertCount(2, $repo, "Should return '2' for count(\$repo)"); } /** @@ -134,7 +134,7 @@ public function testCount(): void public function testNoRepositories(string $method, array $args): void { $repo = new CompositeRepository([]); - $this->assertEquals([], call_user_func_array([$repo, $method], $args)); + self::assertEquals([], call_user_func_array([$repo, $method], $args)); } public static function provideMethodCalls(): array diff --git a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php index e932ef4a85bc..c4f62a6554f2 100644 --- a/tests/Composer/Test/Repository/FilesystemRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilesystemRepositoryTest.php @@ -39,10 +39,10 @@ public function testRepositoryRead(): void $packages = $repository->getPackages(); - $this->assertCount(1, $packages); - $this->assertSame('package1', $packages[0]->getName()); - $this->assertSame('1.0.0.0-beta', $packages[0]->getVersion()); - $this->assertSame('vendor', $packages[0]->getType()); + self::assertCount(1, $packages); + self::assertSame('package1', $packages[0]->getName()); + self::assertSame('1.0.0.0-beta', $packages[0]->getVersion()); + self::assertSame('vendor', $packages[0]->getType()); } public function testCorruptedRepositoryFile(): void @@ -75,7 +75,7 @@ public function testUnexistentRepositoryFile(): void ->method('exists') ->will($this->returnValue(false)); - $this->assertEquals([], $repository->getPackages()); + self::assertEquals([], $repository->getPackages()); } public function testRepositoryWrite(): void @@ -195,7 +195,7 @@ public function testRepositoryWritesInstalledPhp(): void })); $repository->write(true, $im); - $this->assertSame(file_get_contents(__DIR__.'/Fixtures/installed.php'), file_get_contents($dir.'/installed.php')); + self::assertSame(file_get_contents(__DIR__.'/Fixtures/installed.php'), file_get_contents($dir.'/installed.php')); } public function testSafelyLoadInstalledVersions(): void diff --git a/tests/Composer/Test/Repository/FilterRepositoryTest.php b/tests/Composer/Test/Repository/FilterRepositoryTest.php index 726cc549c475..2acb58f0d2a5 100644 --- a/tests/Composer/Test/Repository/FilterRepositoryTest.php +++ b/tests/Composer/Test/Repository/FilterRepositoryTest.php @@ -45,7 +45,7 @@ public function testRepoMatching(array $expected, $config): void $repo = new FilterRepository($this->arrayRepo, $config); $packages = $repo->getPackages(); - $this->assertSame($expected, array_map(static function ($p): string { + self::assertSame($expected, array_map(static function ($p): string { return $p->getName(); }, $packages)); } @@ -86,16 +86,16 @@ public function testSecurityAdvisoriesDisabledInChild(): void public function testCanonicalDefaultTrue(): void { $repo = new FilterRepository($this->arrayRepo, []); - $result = $repo->loadPackages(['foo/aaa' => new MatchAllConstraint], BasePackage::$stabilities, []); - $this->assertCount(1, $result['packages']); - $this->assertCount(1, $result['namesFound']); + $result = $repo->loadPackages(['foo/aaa' => new MatchAllConstraint], BasePackage::STABILITIES, []); + self::assertCount(1, $result['packages']); + self::assertCount(1, $result['namesFound']); } public function testNonCanonical(): void { $repo = new FilterRepository($this->arrayRepo, ['canonical' => false]); - $result = $repo->loadPackages(['foo/aaa' => new MatchAllConstraint], BasePackage::$stabilities, []); - $this->assertCount(1, $result['packages']); - $this->assertCount(0, $result['namesFound']); + $result = $repo->loadPackages(['foo/aaa' => new MatchAllConstraint], BasePackage::STABILITIES, []); + self::assertCount(1, $result['packages']); + self::assertCount(0, $result['namesFound']); } } diff --git a/tests/Composer/Test/Repository/Fixtures/installed.php b/tests/Composer/Test/Repository/Fixtures/installed.php index dbdda5e3c67b..3bfbc15ab494 100644 --- a/tests/Composer/Test/Repository/Fixtures/installed.php +++ b/tests/Composer/Test/Repository/Fixtures/installed.php @@ -65,10 +65,10 @@ 'foo/impl' => array( 'dev_requirement' => false, 'provided' => array( - 0 => '^1.1', - 1 => '1.2', - 2 => '1.4', - 3 => '2.0', + 0 => '1.2', + 1 => '1.4', + 2 => '2.0', + 3 => '^1.1', ), ), 'foo/impl2' => array( diff --git a/tests/Composer/Test/Repository/Fixtures/installed_relative.php b/tests/Composer/Test/Repository/Fixtures/installed_relative.php index 443e460c124e..93dc5d89049b 100644 --- a/tests/Composer/Test/Repository/Fixtures/installed_relative.php +++ b/tests/Composer/Test/Repository/Fixtures/installed_relative.php @@ -5,7 +5,7 @@ 'version' => 'dev-master', 'reference' => 'sourceref-by-default', 'type' => 'library', - // @phpstan-ignore-next-line + // @phpstan-ignore variable.undefined 'install_path' => $dir . '/./', 'aliases' => array( '1.10.x-dev', @@ -18,7 +18,7 @@ 'version' => 'dev-master', 'reference' => 'sourceref-by-default', 'type' => 'library', - // @phpstan-ignore-next-line + // @phpstan-ignore variable.undefined 'install_path' => $dir . '/./', 'aliases' => array( '1.10.x-dev', @@ -30,7 +30,7 @@ 'version' => '1.1.0.0', 'reference' => 'distref-as-no-source', 'type' => 'library', - // @phpstan-ignore-next-line + // @phpstan-ignore variable.undefined 'install_path' => $dir . '/vendor/a/provider', 'aliases' => array(), 'dev_requirement' => false, @@ -40,7 +40,7 @@ 'version' => '1.2.0.0', 'reference' => 'distref-as-installed-from-dist', 'type' => 'library', - // @phpstan-ignore-next-line + // @phpstan-ignore variable.undefined 'install_path' => $dir . '/vendor/a/provider2', 'aliases' => array( '1.4', @@ -52,7 +52,7 @@ 'version' => '2.2.0.0', 'reference' => null, 'type' => 'library', - // @phpstan-ignore-next-line + // @phpstan-ignore variable.undefined 'install_path' => $dir . '/vendor/b/replacer', 'aliases' => array(), 'dev_requirement' => false, diff --git a/tests/Composer/Test/Repository/InstalledRepositoryTest.php b/tests/Composer/Test/Repository/InstalledRepositoryTest.php index 127c58b8f0a2..47726ecadefa 100644 --- a/tests/Composer/Test/Repository/InstalledRepositoryTest.php +++ b/tests/Composer/Test/Repository/InstalledRepositoryTest.php @@ -36,9 +36,9 @@ public function testFindPackagesWithReplacersAndProviders(): void $repo = new InstalledRepository([$arrayRepoOne, $arrayRepoTwo]); - $this->assertEquals([$foo2], $repo->findPackagesWithReplacersAndProviders('foo', '2')); - $this->assertEquals([$bar], $repo->findPackagesWithReplacersAndProviders('bar', '1')); - $this->assertEquals([$foo, $bar2], $repo->findPackagesWithReplacersAndProviders('provided')); + self::assertEquals([$foo2], $repo->findPackagesWithReplacersAndProviders('foo', '2')); + self::assertEquals([$bar], $repo->findPackagesWithReplacersAndProviders('bar', '1')); + self::assertEquals([$foo, $bar2], $repo->findPackagesWithReplacersAndProviders('provided')); } public function testAddRepository(): void diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index 5edd4c423fd5..d5deb664b8ca 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -36,8 +36,8 @@ public function testLoadPackageFromFileSystemWithVersion(): void $repository = $this->createPathRepo(['url' => $repositoryUrl]); $repository->getPackages(); - $this->assertSame(1, $repository->count()); - $this->assertTrue($repository->hasPackage(self::getPackage('test/path-versioned', '0.0.2'))); + self::assertSame(1, $repository->count()); + self::assertTrue($repository->hasPackage(self::getPackage('test/path-versioned', '0.0.2'))); } public function testLoadPackageFromFileSystemWithoutVersion(): void @@ -46,13 +46,13 @@ public function testLoadPackageFromFileSystemWithoutVersion(): void $repository = $this->createPathRepo(['url' => $repositoryUrl]); $packages = $repository->getPackages(); - $this->assertGreaterThanOrEqual(1, $repository->count()); + self::assertGreaterThanOrEqual(1, $repository->count()); $package = $packages[0]; - $this->assertSame('test/path-unversioned', $package->getName()); + self::assertSame('test/path-unversioned', $package->getName()); $packageVersion = $package->getVersion(); - $this->assertNotEmpty($packageVersion); + self::assertNotEmpty($packageVersion); } public function testLoadPackageFromFileSystemWithWildcard(): void @@ -62,7 +62,7 @@ public function testLoadPackageFromFileSystemWithWildcard(): void $packages = $repository->getPackages(); $names = []; - $this->assertGreaterThanOrEqual(2, $repository->count()); + self::assertGreaterThanOrEqual(2, $repository->count()); $package = $packages[0]; $names[] = $package->getName(); @@ -71,7 +71,7 @@ public function testLoadPackageFromFileSystemWithWildcard(): void $names[] = $package->getName(); sort($names); - $this->assertEquals(['test/path-unversioned', 'test/path-versioned'], $names); + self::assertEquals(['test/path-unversioned', 'test/path-versioned'], $names); } public function testLoadPackageWithExplicitVersions(): void @@ -88,7 +88,7 @@ public function testLoadPackageWithExplicitVersions(): void $versions = []; - $this->assertEquals(2, $repository->count()); + self::assertEquals(2, $repository->count()); $package = $packages[0]; $versions[$package->getName()] = $package->getVersion(); @@ -97,7 +97,7 @@ public function testLoadPackageWithExplicitVersions(): void $versions[$package->getName()] = $package->getVersion(); ksort($versions); - $this->assertSame(['test/path-unversioned' => '4.3.2.1', 'test/path-versioned' => '3.2.1.0'], $versions); + self::assertSame(['test/path-unversioned' => '4.3.2.1', 'test/path-versioned' => '3.2.1.0'], $versions); } /** @@ -115,14 +115,14 @@ public function testUrlRemainsRelative(): void $repository = $this->createPathRepo(['url' => $relativeUrl]); $packages = $repository->getPackages(); - $this->assertSame(1, $repository->count()); + self::assertSame(1, $repository->count()); $package = $packages[0]; - $this->assertSame('test/path-versioned', $package->getName()); + self::assertSame('test/path-versioned', $package->getName()); // Convert platform specific separators back to generic URL slashes $relativeUrl = str_replace(DIRECTORY_SEPARATOR, '/', $relativeUrl); - $this->assertSame($relativeUrl, $package->getDistUrl()); + self::assertSame($relativeUrl, $package->getDistUrl()); } public function testReferenceNone(): void @@ -134,10 +134,10 @@ public function testReferenceNone(): void $repository = $this->createPathRepo(['url' => $repositoryUrl, 'options' => $options]); $packages = $repository->getPackages(); - $this->assertGreaterThanOrEqual(2, $repository->count()); + self::assertGreaterThanOrEqual(2, $repository->count()); foreach ($packages as $package) { - $this->assertEquals($package->getDistReference(), null); + self::assertEquals($package->getDistReference(), null); } } @@ -151,12 +151,12 @@ public function testReferenceConfig(): void $repository = $this->createPathRepo(['url' => $repositoryUrl, 'options' => $options]); $packages = $repository->getPackages(); - $this->assertGreaterThanOrEqual(2, $repository->count()); + self::assertGreaterThanOrEqual(2, $repository->count()); foreach ($packages as $package) { - $this->assertEquals( + self::assertEquals( $package->getDistReference(), - sha1(file_get_contents($package->getDistUrl() . '/composer.json') . serialize($options)) + hash('sha1', file_get_contents($package->getDistUrl() . '/composer.json') . serialize($options)) ); } } diff --git a/tests/Composer/Test/Repository/PlatformRepositoryTest.php b/tests/Composer/Test/Repository/PlatformRepositoryTest.php index 74f6c3179587..df52653d3cf8 100644 --- a/tests/Composer/Test/Repository/PlatformRepositoryTest.php +++ b/tests/Composer/Test/Repository/PlatformRepositoryTest.php @@ -1237,7 +1237,7 @@ static function ($package): bool { $expectedLibraries = array_keys(array_filter($expectations, static function ($expectation): bool { return $expectation !== false; })); - self::assertCount(count(array_filter($expectedLibraries)), $libraries, sprintf('Expected: %s, got %s', var_export($expectedLibraries, true), var_export($libraries, true))); + self::assertCount(count($expectedLibraries), $libraries, sprintf('Expected: %s, got %s', var_export($expectedLibraries, true), var_export($libraries, true))); foreach ($extensions as $extension) { $expectations['ext-'.$extension] = $extensionVersion; @@ -1256,8 +1256,8 @@ static function ($package): bool { } else { self::assertNotNull($package, sprintf('Expected to find package "%s"', $packageName)); self::assertSame($expectedVersion, $package->getPrettyVersion(), sprintf('Expected version %s for %s', $expectedVersion, $packageName)); - $this->assertPackageLinks('replaces', $expectedReplaces, $package, $package->getReplaces()); - $this->assertPackageLinks('provides', $expectedProvides, $package, $package->getProvides()); + self::assertPackageLinks('replaces', $expectedReplaces, $package, $package->getReplaces()); + self::assertPackageLinks('provides', $expectedProvides, $package, $package->getProvides()); } } } diff --git a/tests/Composer/Test/Repository/RepositoryFactoryTest.php b/tests/Composer/Test/Repository/RepositoryFactoryTest.php index 8b0a944c0d27..b318e030bb3e 100644 --- a/tests/Composer/Test/Repository/RepositoryFactoryTest.php +++ b/tests/Composer/Test/Repository/RepositoryFactoryTest.php @@ -30,7 +30,7 @@ public function testManagerWithAllRepositoryTypes(): void $ref->setAccessible(true); $repositoryClasses = $ref->getValue($manager); - $this->assertEquals([ + self::assertEquals([ 'composer', 'vcs', 'package', @@ -60,7 +60,7 @@ public function testManagerWithAllRepositoryTypes(): void */ public function testGenerateRepositoryName($index, array $config, array $existingRepos, string $expected): void { - $this->assertSame($expected, RepositoryFactory::generateRepositoryName($index, $config, $existingRepos)); + self::assertSame($expected, RepositoryFactory::generateRepositoryName($index, $config, $existingRepos)); } public static function generateRepositoryNameProvider(): array diff --git a/tests/Composer/Test/Repository/RepositoryManagerTest.php b/tests/Composer/Test/Repository/RepositoryManagerTest.php index d1fa09c8b249..859826e6a841 100644 --- a/tests/Composer/Test/Repository/RepositoryManagerTest.php +++ b/tests/Composer/Test/Repository/RepositoryManagerTest.php @@ -50,21 +50,17 @@ public function testPrepend(): void $rm->addRepository($repository1); $rm->prependRepository($repository2); - $this->assertEquals([$repository2, $repository1], $rm->getRepositories()); + self::assertEquals([$repository2, $repository1], $rm->getRepositories()); } /** * @dataProvider provideRepoCreationTestCases * + * @doesNotPerformAssertions * @param array $options - * @param class-string<\Throwable>|null $exception */ - public function testRepoCreation(string $type, array $options, ?string $exception = null): void + public function testRepoCreation(string $type, array $options): void { - if ($exception !== null) { - self::expectException($exception); - } - $rm = new RepositoryManager( $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $config = new Config, @@ -86,7 +82,7 @@ public function testRepoCreation(string $type, array $options, ?string $exceptio $rm->setRepositoryClass('artifact', 'Composer\Repository\ArtifactRepository'); $rm->createRepository('composer', ['url' => 'http://example.org']); - $this->assertInstanceOf('Composer\Repository\RepositoryInterface', $rm->createRepository($type, $options)); + $rm->createRepository($type, $options); } public static function provideRepoCreationTestCases(): array @@ -97,9 +93,7 @@ public static function provideRepoCreationTestCases(): array ['git', ['url' => 'http://github.com/foo/bar']], ['git', ['url' => 'git@example.org:foo/bar.git']], ['svn', ['url' => 'svn://example.org/foo/bar']], - ['pear', ['url' => 'http://pear.example.org/foo'], 'InvalidArgumentException'], ['package', ['package' => []]], - ['invalid', [], 'InvalidArgumentException'], ]; if (class_exists('ZipArchive')) { @@ -109,6 +103,36 @@ public static function provideRepoCreationTestCases(): array return $cases; } + /** + * @dataProvider provideInvalidRepoCreationTestCases + * + * @param array $options + */ + public function testInvalidRepoCreationThrows(string $type, array $options): void + { + self::expectException('InvalidArgumentException'); + + $rm = new RepositoryManager( + $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), + $config = new Config, + $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), + $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock() + ); + + $tmpdir = $this->tmpdir; + $config->merge(['config' => ['cache-repo-dir' => $tmpdir]]); + + $rm->createRepository($type, $options); + } + + public static function provideInvalidRepoCreationTestCases(): array + { + return [ + ['pear', ['url' => 'http://pear.example.org/foo']], + ['invalid', []], + ]; + } + public function testFilterRepoWrapping(): void { $rm = new RepositoryManager( @@ -122,7 +146,7 @@ public function testFilterRepoWrapping(): void /** @var \Composer\Repository\FilterRepository $repo */ $repo = $rm->createRepository('path', ['type' => 'path', 'url' => __DIR__, 'only' => ['foo/bar']]); - $this->assertInstanceOf('Composer\Repository\FilterRepository', $repo); - $this->assertInstanceOf('Composer\Repository\PathRepository', $repo->getRepository()); + self::assertInstanceOf('Composer\Repository\FilterRepository', $repo); + self::assertInstanceOf('Composer\Repository\PathRepository', $repo->getRepository()); } } diff --git a/tests/Composer/Test/Repository/RepositoryUtilsTest.php b/tests/Composer/Test/Repository/RepositoryUtilsTest.php index ddc328998037..e85d2f2f543f 100644 --- a/tests/Composer/Test/Repository/RepositoryUtilsTest.php +++ b/tests/Composer/Test/Repository/RepositoryUtilsTest.php @@ -24,13 +24,13 @@ class RepositoryUtilsTest extends TestCase * @param PackageInterface[] $pkgs * @param string[] $expected */ - public function testFilterRequiredPackages(array $pkgs, PackageInterface $requirer, array $expected): void + public function testFilterRequiredPackages(array $pkgs, PackageInterface $requirer, array $expected, bool $includeRequireDev = false): void { $expected = array_map(static function (string $name) use ($pkgs): PackageInterface { return $pkgs[$name]; }, $expected); - self::assertSame($expected, RepositoryUtils::filterRequiredPackages($pkgs, $requirer)); + self::assertSame($expected, RepositoryUtils::filterRequiredPackages($pkgs, $requirer, $includeRequireDev)); } /** @@ -72,6 +72,10 @@ public static function provideFilterRequireTests(): Generator self::configureLinks($requirer, ['require-dev' => ['required/a' => '*']]); yield 'require-dev has no effect' => [$pkgs, $requirer, []]; + $requirer = self::getPackage('requirer/pkg'); + self::configureLinks($requirer, ['require-dev' => ['required/a' => '*']]); + yield 'require-dev works if called with it enabled' => [$pkgs, $requirer, ['a'], true]; + $requirer = self::getPackage('requirer/pkg'); self::configureLinks($requirer, ['require' => ['required/a' => '*']]); yield 'simple require' => [$pkgs, $requirer, ['a']]; diff --git a/tests/Composer/Test/Repository/Vcs/FossilDriverTest.php b/tests/Composer/Test/Repository/Vcs/FossilDriverTest.php index 06ec3d177062..854c55e33ba3 100644 --- a/tests/Composer/Test/Repository/Vcs/FossilDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/FossilDriverTest.php @@ -62,6 +62,6 @@ public function testSupport(string $url, bool $assertion): void { $config = new Config(); $result = FossilDriver::supports($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $config, $url); - $this->assertEquals($assertion, $result); + self::assertEquals($assertion, $result); } } diff --git a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php index 5050b0fb49da..d1fa2bb232b9 100644 --- a/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitBitbucketDriverTest.php @@ -111,12 +111,12 @@ public function testDriver(): GitBitbucketDriver ['url' => $urls[4], 'body' => '{"date": "2016-05-17T13:19:52+00:00"}'], ], true); - $this->assertEquals( + self::assertEquals( 'main', $driver->getRootIdentifier() ); - $this->assertEquals( + self::assertEquals( [ '1.0.1' => '9b78a3932143497c519e49b8241083838c8ff8a1', '1.0.0' => 'd3393d514318a9267d2f8ebbf463a9aaa389f8eb', @@ -124,14 +124,14 @@ public function testDriver(): GitBitbucketDriver $driver->getTags() ); - $this->assertEquals( + self::assertEquals( [ 'main' => '937992d19d72b5116c3e8c4a04f960e5fa270b22', ], $driver->getBranches() ); - $this->assertEquals( + self::assertEquals( [ 'name' => 'user/repo', 'description' => 'test repo', @@ -167,9 +167,9 @@ public function testGetParams(\Composer\Repository\Vcs\VcsDriverInterface $drive { $url = 'https://bitbucket.org/user/repo.git'; - $this->assertEquals($url, $driver->getUrl()); + self::assertEquals($url, $driver->getUrl()); - $this->assertEquals( + self::assertEquals( [ 'type' => 'zip', 'url' => 'https://bitbucket.org/user/repo/get/reference.zip', @@ -179,7 +179,7 @@ public function testGetParams(\Composer\Repository\Vcs\VcsDriverInterface $drive $driver->getDist('reference') ); - $this->assertEquals( + self::assertEquals( ['type' => 'git', 'url' => $url, 'reference' => 'reference'], $driver->getSource('reference') ); @@ -217,22 +217,22 @@ public function testInvalidSupportData(): void $driver->getRootIdentifier(); $data = $driver->getComposerInformation('main'); - $this->assertIsArray($data); - $this->assertSame('https://bitbucket.org/user/repo/src/937992d19d72b5116c3e8c4a04f960e5fa270b22/?at=main', $data['support']['source']); + self::assertIsArray($data); + self::assertSame('https://bitbucket.org/user/repo/src/937992d19d72b5116c3e8c4a04f960e5fa270b22/?at=main', $data['support']['source']); } public function testSupports(): void { - $this->assertTrue( + self::assertTrue( GitBitbucketDriver::supports($this->io, $this->config, 'https://bitbucket.org/user/repo.git') ); // should not be changed, see https://github.com/composer/composer/issues/9400 - $this->assertFalse( + self::assertFalse( GitBitbucketDriver::supports($this->io, $this->config, 'git@bitbucket.org:user/repo.git') ); - $this->assertFalse( + self::assertFalse( GitBitbucketDriver::supports($this->io, $this->config, 'https://github.com/user/repo.git') ); } diff --git a/tests/Composer/Test/Repository/Vcs/GitDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitDriverTest.php index b54e29ad113a..decf6cf87953 100644 --- a/tests/Composer/Test/Repository/Vcs/GitDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitDriverTest.php @@ -72,11 +72,11 @@ public function testGetRootIdentifierFromRemoteLocalRepository(): void $process ->expects([[ - 'cmd' => 'git branch --no-color', + 'cmd' => ['git', 'branch', '--no-color'], 'stdout' => $stdout, ]], true); - $this->assertSame('main', $driver->getRootIdentifier()); + self::assertSame('main', $driver->getRootIdentifier()); } public function testGetRootIdentifierFromRemote(): void @@ -102,14 +102,20 @@ public function testGetRootIdentifierFromRemote(): void $process ->expects([[ - 'cmd' => 'git remote -v', + 'cmd' => ['git', 'remote', '-v'], 'stdout' => '', ], [ - 'cmd' => Platform::isWindows() ? "git remote set-url origin -- https://example.org/acme.git && git remote show origin && git remote set-url origin -- https://example.org/acme.git" : "git remote set-url origin -- 'https://example.org/acme.git' && git remote show origin && git remote set-url origin -- 'https://example.org/acme.git'", + 'cmd' => ['git', 'remote', 'set-url', 'origin', '--', 'https://example.org/acme.git'], + 'stdout' => '', + ], [ + 'cmd' => ['git', 'remote', 'show', 'origin'], 'stdout' => $stdout, + ], [ + 'cmd' => ['git', 'remote', 'set-url', 'origin', '--', 'https://example.org/acme.git'], + 'stdout' => '', ]]); - $this->assertSame('main', $driver->getRootIdentifier()); + self::assertSame('main', $driver->getRootIdentifier()); } public function testGetRootIdentifierFromLocalWithNetworkDisabled(): void @@ -130,11 +136,11 @@ public function testGetRootIdentifierFromLocalWithNetworkDisabled(): void $process ->expects([[ - 'cmd' => 'git branch --no-color', + 'cmd' => ['git', 'branch', '--no-color'], 'stdout' => $stdout, ]]); - $this->assertSame('main', $driver->getRootIdentifier()); + self::assertSame('main', $driver->getRootIdentifier()); } public function testGetBranchesFilterInvalidBranchNames(): void @@ -155,12 +161,12 @@ public function testGetBranchesFilterInvalidBranchNames(): void $process ->expects([[ - 'cmd' => 'git branch --no-color --no-abbrev -v', + 'cmd' => ['git', 'branch', '--no-color', '--no-abbrev', '-v'], 'stdout' => $stdout, ]]); $branches = $driver->getBranches(); - $this->assertSame([ + self::assertSame([ 'main' => '089681446ba44d6d9004350192486f2ceb4eaa06', '2.2' => '12681446ba44d6d9004350192486f2ceb4eaa06', ], $branches); @@ -174,7 +180,7 @@ public function testFileGetContentInvalidIdentifier(): void $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); $driver = new GitDriver(['url' => 'https://example.org/acme.git'], $io, $this->config, $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), $process); - $this->assertNull($driver->getFileContent('file.txt', 'h')); + self::assertNull($driver->getFileContent('file.txt', 'h')); $driver->getFileContent('file.txt', '-h'); } diff --git a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php index aa454e8946d5..9902af644a33 100644 --- a/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitHubDriverTest.php @@ -91,18 +91,18 @@ public function testPrivateRepository(): void $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', [$identifier => $sha]); - $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); + self::assertEquals('test_master', $gitHubDriver->getRootIdentifier()); $dist = $gitHubDriver->getDist($sha); self::assertIsArray($dist); - $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); - $this->assertEquals('SOMESHA', $dist['reference']); + self::assertEquals('zip', $dist['type']); + self::assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + self::assertEquals('SOMESHA', $dist['reference']); $source = $gitHubDriver->getSource($sha); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoSshUrl, $source['url']); - $this->assertEquals('SOMESHA', $source['reference']); + self::assertEquals('git', $source['type']); + self::assertEquals($repoSshUrl, $source['url']); + self::assertEquals('SOMESHA', $source['reference']); } public function testPublicRepository(): void @@ -134,18 +134,18 @@ public function testPublicRepository(): void $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', [$identifier => $sha]); - $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); + self::assertEquals('test_master', $gitHubDriver->getRootIdentifier()); $dist = $gitHubDriver->getDist($sha); self::assertIsArray($dist); - $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); - $this->assertEquals($sha, $dist['reference']); + self::assertEquals('zip', $dist['type']); + self::assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + self::assertEquals($sha, $dist['reference']); $source = $gitHubDriver->getSource($sha); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoUrl, $source['url']); - $this->assertEquals($sha, $source['reference']); + self::assertEquals('git', $source['type']); + self::assertEquals($repoUrl, $source['url']); + self::assertEquals($sha, $source['reference']); } public function testPublicRepository2(): void @@ -180,23 +180,23 @@ public function testPublicRepository2(): void $gitHubDriver->initialize(); $this->setAttribute($gitHubDriver, 'tags', [$identifier => $sha]); - $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); + self::assertEquals('test_master', $gitHubDriver->getRootIdentifier()); $dist = $gitHubDriver->getDist($sha); self::assertIsArray($dist); - $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); - $this->assertEquals($sha, $dist['reference']); + self::assertEquals('zip', $dist['type']); + self::assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + self::assertEquals($sha, $dist['reference']); $source = $gitHubDriver->getSource($sha); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoUrl, $source['url']); - $this->assertEquals($sha, $source['reference']); + self::assertEquals('git', $source['type']); + self::assertEquals($repoUrl, $source['url']); + self::assertEquals($sha, $source['reference']); $data = $gitHubDriver->getComposerInformation($identifier); - $this->assertIsArray($data); - $this->assertArrayNotHasKey('abandoned', $data); + self::assertIsArray($data); + self::assertArrayNotHasKey('abandoned', $data); } public function testInvalidSupportData(): void @@ -233,8 +233,8 @@ public function testInvalidSupportData(): void $data = $gitHubDriver->getComposerInformation($identifier); - $this->assertIsArray($data); - $this->assertSame('https://github.com/composer/packagist/tree/feature/3.2-foo', $data['support']['source']); + self::assertIsArray($data); + self::assertSame('https://github.com/composer/packagist/tree/feature/3.2-foo', $data['support']['source']); } public function testPublicRepositoryArchived(): void @@ -271,8 +271,8 @@ public function testPublicRepositoryArchived(): void $data = $gitHubDriver->getComposerInformation($sha); - $this->assertIsArray($data); - $this->assertTrue($data['abandoned']); + self::assertIsArray($data); + self::assertTrue($data['abandoned']); } public function testPrivateRepositoryNoInteraction(): void @@ -303,18 +303,18 @@ public function testPrivateRepositoryNoInteraction(): void $process = $this->getProcessExecutorMock(); $process->expects([ - ['cmd' => 'git config github.accesstoken', 'return' => 1], - 'git clone --mirror -- '.ProcessExecutor::escape($repoSshUrl).' '.ProcessExecutor::escape($this->config->get('cache-vcs-dir').'/git-github.com-composer-packagist.git/'), + ['cmd' => ['git', 'config', 'github.accesstoken'], 'return' => 1], + ['git', 'clone', '--mirror', '--', $repoSshUrl, $this->config->get('cache-vcs-dir').'/git-github.com-composer-packagist.git/'], [ - 'cmd' => 'git show-ref --tags --dereference', + 'cmd' => ['git', 'show-ref', '--tags', '--dereference'], 'stdout' => $sha.' refs/tags/'.$identifier, ], [ - 'cmd' => 'git branch --no-color --no-abbrev -v', + 'cmd' => ['git', 'branch', '--no-color', '--no-abbrev', '-v'], 'stdout' => ' test_master edf93f1fccaebd8764383dc12016d0a1a9672d89 Fix test & behavior', ], [ - 'cmd' => 'git branch --no-color', + 'cmd' => ['git', 'branch', '--no-color'], 'stdout' => '* test_master', ], ], true); @@ -326,23 +326,23 @@ public function testPrivateRepositoryNoInteraction(): void $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $process); $gitHubDriver->initialize(); - $this->assertEquals('test_master', $gitHubDriver->getRootIdentifier()); + self::assertEquals('test_master', $gitHubDriver->getRootIdentifier()); $dist = $gitHubDriver->getDist($sha); self::assertIsArray($dist); - $this->assertEquals('zip', $dist['type']); - $this->assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); - $this->assertEquals($sha, $dist['reference']); + self::assertEquals('zip', $dist['type']); + self::assertEquals('https://api.github.com/repos/composer/packagist/zipball/SOMESHA', $dist['url']); + self::assertEquals($sha, $dist['reference']); $source = $gitHubDriver->getSource($identifier); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoSshUrl, $source['url']); - $this->assertEquals($identifier, $source['reference']); + self::assertEquals('git', $source['type']); + self::assertEquals($repoSshUrl, $source['url']); + self::assertEquals($identifier, $source['reference']); $source = $gitHubDriver->getSource($sha); - $this->assertEquals('git', $source['type']); - $this->assertEquals($repoSshUrl, $source['url']); - $this->assertEquals($sha, $source['reference']); + self::assertEquals('git', $source['type']); + self::assertEquals($repoSshUrl, $source['url']); + self::assertEquals($sha, $source['reference']); } /** @@ -384,7 +384,7 @@ public function testSupports(bool $expected, string $repoUrl): void { $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $this->assertSame($expected, GitHubDriver::supports($io, $this->config, $repoUrl)); + self::assertSame($expected, GitHubDriver::supports($io, $this->config, $repoUrl)); } /** @@ -426,7 +426,7 @@ public function testGetEmptyFileContent(): void $gitHubDriver = new GitHubDriver($repoConfig, $io, $this->config, $httpDownloader, $this->getProcessExecutorMock()); $gitHubDriver->initialize(); - $this->assertSame('', $gitHubDriver->getFileContent('composer.json', 'main')); + self::assertSame('', $gitHubDriver->getFileContent('composer.json', 'main')); } /** diff --git a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php index 9d163f567ff2..acc1df9e35de 100644 --- a/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitLabDriverTest.php @@ -118,10 +118,10 @@ public function testInitialize(string $url, string $apiUrl): GitLabDriver $driver = new GitLabDriver(['url' => $url], $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); - $this->assertEquals('mymaster', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); - $this->assertEquals('git@gitlab.com:mygroup/myproject.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); - $this->assertEquals('https://gitlab.com/mygroup/myproject', $driver->getUrl()); + self::assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); + self::assertEquals('mymaster', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); + self::assertEquals('git@gitlab.com:mygroup/myproject.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); + self::assertEquals('https://gitlab.com/mygroup/myproject', $driver->getUrl()); return $driver; } @@ -158,10 +158,10 @@ public function testInitializePublicProject(string $url, string $apiUrl): GitLab $driver = new GitLabDriver(['url' => $url], $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); - $this->assertEquals('mymaster', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); - $this->assertEquals('https://gitlab.com/mygroup/myproject.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); - $this->assertEquals('https://gitlab.com/mygroup/myproject', $driver->getUrl()); + self::assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); + self::assertEquals('mymaster', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); + self::assertEquals('https://gitlab.com/mygroup/myproject.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); + self::assertEquals('https://gitlab.com/mygroup/myproject', $driver->getUrl()); return $driver; } @@ -197,10 +197,10 @@ public function testInitializePublicProjectAsAnonymous(string $url, string $apiU $driver = new GitLabDriver(['url' => $url], $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); - $this->assertEquals('mymaster', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); - $this->assertEquals('https://gitlab.com/mygroup/myproject.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); - $this->assertEquals('https://gitlab.com/mygroup/myproject', $driver->getUrl()); + self::assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); + self::assertEquals('mymaster', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); + self::assertEquals('https://gitlab.com/mygroup/myproject.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); + self::assertEquals('https://gitlab.com/mygroup/myproject', $driver->getUrl()); return $driver; } @@ -238,10 +238,10 @@ public function testInitializeWithPortNumber(): void $driver = new GitLabDriver(['url' => $url], $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); - $this->assertEquals('1.0.x', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); - $this->assertEquals($url.'.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); - $this->assertEquals($url, $driver->getUrl()); + self::assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); + self::assertEquals('1.0.x', $driver->getRootIdentifier(), 'Root identifier is the default branch in GitLab'); + self::assertEquals($url.'.git', $driver->getRepositoryUrl(), 'The repository URL is the SSH one by default'); + self::assertEquals($url, $driver->getUrl()); } public function testInvalidSupportData(): void @@ -256,8 +256,8 @@ public function testInvalidSupportData(): void $data = $driver->getComposerInformation('main'); - $this->assertIsArray($data); - $this->assertSame('https://gitlab.com/mygroup/myproject/-/tree/main', $data['support']['source']); + self::assertIsArray($data); + self::assertSame('https://gitlab.com/mygroup/myproject/-/tree/main', $data['support']['source']); } public function testGetDist(): void @@ -272,7 +272,7 @@ public function testGetDist(): void 'shasum' => '', ]; - $this->assertEquals($expected, $driver->getDist($reference)); + self::assertEquals($expected, $driver->getDist($reference)); } public function testGetSource(): void @@ -286,7 +286,7 @@ public function testGetSource(): void 'reference' => $reference, ]; - $this->assertEquals($expected, $driver->getSource($reference)); + self::assertEquals($expected, $driver->getSource($reference)); } public function testGetSource_GivenPublicProject(): void @@ -300,7 +300,7 @@ public function testGetSource_GivenPublicProject(): void 'reference' => $reference, ]; - $this->assertEquals($expected, $driver->getSource($reference)); + self::assertEquals($expected, $driver->getSource($reference)); } public function testGetTags(): void @@ -340,8 +340,8 @@ public function testGetTags(): void 'v2.0.0' => '8e8f60b3ec86d63733db3bd6371117a758027ec6', ]; - $this->assertEquals($expected, $driver->getTags()); - $this->assertEquals($expected, $driver->getTags(), 'Tags are cached'); + self::assertEquals($expected, $driver->getTags()); + self::assertEquals($expected, $driver->getTags(), 'Tags are cached'); } public function testGetPaginatedRefs(): void @@ -402,8 +402,8 @@ public function testGetPaginatedRefs(): void 'stagingdupe' => '502cffe49f136443f2059803f2e7192d1ac066cd', ]; - $this->assertEquals($expected, $driver->getBranches()); - $this->assertEquals($expected, $driver->getBranches(), 'Branches are cached'); + self::assertEquals($expected, $driver->getBranches()); + self::assertEquals($expected, $driver->getBranches(), 'Branches are cached'); } public function testGetBranches(): void @@ -444,8 +444,8 @@ public function testGetBranches(): void 'staging' => '502cffe49f136443f2059803f2e7192d1ac066cd', ]; - $this->assertEquals($expected, $driver->getBranches()); - $this->assertEquals($expected, $driver->getBranches(), 'Branches are cached'); + self::assertEquals($expected, $driver->getBranches()); + self::assertEquals($expected, $driver->getBranches(), 'Branches are cached'); } /** @@ -454,7 +454,7 @@ public function testGetBranches(): void */ public function testSupports(string $url, bool $expected): void { - $this->assertSame($expected, GitLabDriver::supports($this->io, $this->config, $url)); + self::assertSame($expected, GitLabDriver::supports($this->io, $this->config, $url)); } public static function dataForTestSupports(): array @@ -510,7 +510,7 @@ public function testGitlabSubDirectory(): void $driver = new GitLabDriver(['url' => $url], $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); + self::assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); } public function testGitlabSubGroup(): void @@ -542,7 +542,7 @@ public function testGitlabSubGroup(): void $driver = new GitLabDriver(['url' => $url], $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); + self::assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); } public function testGitlabSubDirectorySubGroup(): void @@ -574,7 +574,7 @@ public function testGitlabSubDirectorySubGroup(): void $driver = new GitLabDriver(['url' => $url], $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); + self::assertEquals($apiUrl, $driver->getApiUrl(), 'API URL is derived from the repository URL'); } public function testForwardsOptions(): void @@ -645,7 +645,7 @@ public function testProtocolOverrideRepositoryUrlGeneration(): void $config->merge(['config' => ['gitlab-protocol' => 'http']]); $driver = new GitLabDriver(['url' => $url], $this->io, $config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals('https://gitlab.com/mygroup/myproject.git', $driver->getRepositoryUrl(), 'Repository URL matches config request for http not git'); + self::assertEquals('https://gitlab.com/mygroup/myproject.git', $driver->getRepositoryUrl(), 'Repository URL matches config request for http not git'); } /** diff --git a/tests/Composer/Test/Repository/Vcs/HgDriverTest.php b/tests/Composer/Test/Repository/Vcs/HgDriverTest.php index f1f047de5950..b4e912af17bb 100644 --- a/tests/Composer/Test/Repository/Vcs/HgDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/HgDriverTest.php @@ -50,7 +50,7 @@ protected function tearDown(): void */ public function testSupports(string $repositoryUrl): void { - $this->assertTrue( + self::assertTrue( HgDriver::supports($this->io, $this->config, $repositoryUrl) ); } @@ -85,15 +85,15 @@ public function testGetBranchesFilterInvalidBranchNames(): void $process ->expects([[ - 'cmd' => 'hg branches', + 'cmd' => ['hg', 'branches'], 'stdout' => $stdout, ], [ - 'cmd' => 'hg bookmarks', + 'cmd' => ['hg', 'bookmarks'], 'stdout' => $stdout1, ]]); $branches = $driver->getBranches(); - $this->assertSame([ + self::assertSame([ 'help' => 'dbf6c8acb641', 'default' => 'dbf6c8acb640', ], $branches); @@ -106,7 +106,7 @@ public function testFileGetContentInvalidIdentifier(): void $process = $this->getProcessExecutorMock(); $driver = new HgDriver(['url' => 'https://example.org/acme.git'], $this->io, $this->config, $this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(), $process); - $this->assertNull($driver->getFileContent('file.txt', 'h')); + self::assertNull($driver->getFileContent('file.txt', 'h')); $driver->getFileContent('file.txt', '-h'); } diff --git a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php index b6165ebab155..357a544a4be2 100644 --- a/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/PerforceDriverTest.php @@ -132,9 +132,9 @@ public function testInitializeCapturesVariablesFromRepoConfig(): void { $driver = new PerforceDriver($this->repoConfig, $this->io, $this->config, $this->httpDownloader, $this->process); $driver->initialize(); - $this->assertEquals(self::TEST_URL, $driver->getUrl()); - $this->assertEquals(self::TEST_DEPOT, $driver->getDepot()); - $this->assertEquals(self::TEST_BRANCH, $driver->getBranch()); + self::assertEquals(self::TEST_URL, $driver->getUrl()); + self::assertEquals(self::TEST_DEPOT, $driver->getDepot()); + self::assertEquals(self::TEST_BRANCH, $driver->getBranch()); } public function testInitializeLogsInAndConnectsClient(): void @@ -157,7 +157,7 @@ public function testHasComposerFileReturnsFalseOnNoComposerFile(): void $this->perforce->expects($this->any())->method('getComposerInformation')->with($this->equalTo($formatted_depot_path))->will($this->returnValue([])); $this->driver->initialize(); $result = $this->driver->hasComposerFile($identifier); - $this->assertFalse($result); + self::assertFalse($result); } /** @@ -171,7 +171,7 @@ public function testHasComposerFileReturnsTrueWithOneOrMoreComposerFiles(): void $this->perforce->expects($this->any())->method('getComposerInformation')->with($this->equalTo($formatted_depot_path))->will($this->returnValue([''])); $this->driver->initialize(); $result = $this->driver->hasComposerFile($identifier); - $this->assertTrue($result); + self::assertTrue($result); } /** @@ -182,7 +182,7 @@ public function testHasComposerFileReturnsTrueWithOneOrMoreComposerFiles(): void public function testSupportsReturnsFalseNoDeepCheck(): void { $this->expectOutputString(''); - $this->assertFalse(PerforceDriver::supports($this->io, $this->config, 'existing.url')); + self::assertFalse(PerforceDriver::supports($this->io, $this->config, 'existing.url')); } public function testCleanup(): void diff --git a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php index 3232f2d4e8c1..7e0b72ba95ba 100644 --- a/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/SvnDriverTest.php @@ -60,12 +60,7 @@ public function testWrongCredentialsInUrl(): void $output .= " rejected Basic challenge (https://corp.svn.local/)"; $process = $this->getProcessExecutorMock(); - $authedCommand = sprintf( - 'svn ls --verbose --non-interactive --username %s --password %s -- %s', - ProcessExecutor::escape('till'), - ProcessExecutor::escape('secret'), - ProcessExecutor::escape('https://till:secret@corp.svn.local/repo/trunk') - ); + $authedCommand = ['svn', 'ls', '--verbose', '--non-interactive', '--username', 'till', '--password', 'secret', '--', 'https://till:secret@corp.svn.local/repo/trunk']; $process->expects([ ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], @@ -73,7 +68,7 @@ public function testWrongCredentialsInUrl(): void ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], ['cmd' => $authedCommand, 'return' => 1, 'stderr' => $output], - ['cmd' => 'svn --version', 'return' => 0, 'stdout' => '1.2.3'], + ['cmd' => ['svn', '--version'], 'return' => 0, 'stdout' => '1.2.3'], ], true); $repoConfig = [ @@ -101,6 +96,6 @@ public function testSupport(string $url, bool $assertion): void { $config = new Config(); $result = SvnDriver::supports($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $config, $url); - $this->assertEquals($assertion, $result); + self::assertEquals($assertion, $result); } } diff --git a/tests/Composer/Test/Repository/VcsRepositoryTest.php b/tests/Composer/Test/Repository/VcsRepositoryTest.php index dc219371c513..429b12b30764 100644 --- a/tests/Composer/Test/Repository/VcsRepositoryTest.php +++ b/tests/Composer/Test/Repository/VcsRepositoryTest.php @@ -94,6 +94,9 @@ protected function initialize(): void $exec('git add foo'); $exec('git commit -m change-a'); + // add foo#bar branch which should result in dev-foo+bar + $exec('git branch foo#bar'); + // add version to composer.json $exec('git checkout master'); $composer['version'] = '1.0.0'; @@ -154,6 +157,7 @@ public function testLoadVersions(): void '1.1.x-dev' => true, 'dev-feature-b' => true, 'dev-feature/a-1.0-B' => true, + 'dev-foo+bar' => true, 'dev-master' => true, '9999999-dev' => true, // alias of dev-master ]; @@ -177,6 +181,6 @@ public function testLoadVersions(): void } } - $this->assertEmpty($expected, 'Missing versions: '.implode(', ', array_keys($expected))); + self::assertEmpty($expected, 'Missing versions: '.implode(', ', array_keys($expected))); } } diff --git a/tests/Composer/Test/Script/EventTest.php b/tests/Composer/Test/Script/EventTest.php index c085ad110fec..f62b96cb4c78 100644 --- a/tests/Composer/Test/Script/EventTest.php +++ b/tests/Composer/Test/Script/EventTest.php @@ -28,15 +28,14 @@ public function testEventSetsOriginatingEvent(): void $scriptEvent = new Event('test', $composer, $io, true); - $this->assertNull( + self::assertNull( $scriptEvent->getOriginatingEvent(), 'originatingEvent is initialized as null' ); $scriptEvent->setOriginatingEvent($originatingEvent); - // @phpstan-ignore-next-line - $this->assertSame( + self::assertSame( $originatingEvent, $scriptEvent->getOriginatingEvent(), 'getOriginatingEvent() SHOULD return test event' @@ -55,13 +54,13 @@ public function testEventCalculatesNestedOriginatingEvent(): void $scriptEvent = new Event('test', $composer, $io, true); $scriptEvent->setOriginatingEvent($intermediateEvent); - $this->assertNotSame( + self::assertNotSame( $intermediateEvent, $scriptEvent->getOriginatingEvent(), 'getOriginatingEvent() SHOULD NOT return intermediate events' ); - $this->assertSame( + self::assertSame( $originatingEvent, $scriptEvent->getOriginatingEvent(), 'getOriginatingEvent() SHOULD return upper-most event' diff --git a/tests/Composer/Test/TestCase.php b/tests/Composer/Test/TestCase.php index b1c4c23d710f..0fc9a1acc9a8 100644 --- a/tests/Composer/Test/TestCase.php +++ b/tests/Composer/Test/TestCase.php @@ -102,7 +102,7 @@ public static function getUniqueTmpDirectory(): string $root = sys_get_temp_dir(); do { - $unique = $root . DIRECTORY_SEPARATOR . uniqid('composer-test-' . random_int(1000, 9000)); + $unique = $root . DIRECTORY_SEPARATOR . 'composer-test-' . bin2hex(random_bytes(10)); if (!file_exists($unique) && Silencer::call('mkdir', $unique, 0777)) { return realpath($unique); @@ -355,7 +355,6 @@ protected static function getCmd(string $cmd): string { if (Platform::isWindows()) { $cmd = Preg::replaceCallback("/('[^']*')/", static function ($m) { - assert(is_string($m[1])); // Double-quotes are used only when needed $char = (strpbrk($m[1], " \t^&|<>()") !== false || $m[1] === "''") ? '"' : ''; diff --git a/tests/Composer/Test/Util/AuthHelperTest.php b/tests/Composer/Test/Util/AuthHelperTest.php index ee5802321c90..0be55944fcc0 100644 --- a/tests/Composer/Test/Util/AuthHelperTest.php +++ b/tests/Composer/Test/Util/AuthHelperTest.php @@ -57,7 +57,7 @@ public function testAddAuthenticationHeaderWithoutAuthCredentials(): void ->with($origin) ->willReturn(false); - $this->assertSame( + self::assertSame( $headers, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -80,7 +80,7 @@ public function testAddAuthenticationHeaderWithBearerPassword(): void $expectedHeaders = array_merge($headers, ['Authorization: Bearer ' . $auth['username']]); - $this->assertSame( + self::assertSame( $expectedHeaders, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -107,7 +107,7 @@ public function testAddAuthenticationHeaderWithGithubToken(): void $expectedHeaders = array_merge($headers, ['Authorization: token ' . $auth['username']]); - $this->assertSame( + self::assertSame( $expectedHeaders, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -139,7 +139,7 @@ public function testAddAuthenticationHeaderWithGitlabOathToken(): void $expectedHeaders = array_merge($headers, ['Authorization: Bearer ' . $auth['username']]); - $this->assertSame( + self::assertSame( $expectedHeaders, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -182,7 +182,7 @@ public function testAddAuthenticationHeaderWithGitlabPrivateToken(string $passwo $expectedHeaders = array_merge($headers, ['PRIVATE-TOKEN: ' . $auth['username']]); - $this->assertSame( + self::assertSame( $expectedHeaders, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -214,7 +214,7 @@ public function testAddAuthenticationHeaderWithBitbucketOathToken(): void $expectedHeaders = array_merge($headers, ['Authorization: Bearer ' . $auth['password']]); - $this->assertSame( + self::assertSame( $expectedHeaders, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -250,7 +250,7 @@ public function testAddAuthenticationHeaderWithBitbucketPublicUrl(string $url): ->with('gitlab-domains') ->willReturn([]); - $this->assertSame( + self::assertSame( $headers, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -320,7 +320,7 @@ public function testAddAuthenticationHeaderWithBasicHttpAuthentication(string $u ['Authorization: Basic ' . base64_encode($auth['username'] . ':' . $auth['password'])] ); - $this->assertSame( + self::assertSame( $expectedHeaders, $this->authHelper->addAuthenticationHeader($headers, $origin, $url) ); @@ -331,12 +331,12 @@ public function testAddAuthenticationHeaderWithBasicHttpAuthentication(string $u */ public function testIsPublicBitBucketDownloadWithBitbucketPublicUrl(string $url): void { - $this->assertTrue($this->authHelper->isPublicBitBucketDownload($url)); + self::assertTrue($this->authHelper->isPublicBitBucketDownload($url)); } public function testIsPublicBitBucketDownloadWithNonBitbucketPublicUrl(): void { - $this->assertFalse( + self::assertFalse( $this->authHelper->isPublicBitBucketDownload( 'https://bitbucket.org/site/oauth2/authorize' ) @@ -592,12 +592,12 @@ function ($repositoryName) use (&$getAuthenticationReturnValues) { $result1 = $this->authHelper->promptAuthIfNeeded('https://bitbucket.org/workspace/repo1/get/hash1.zip', $origin, 401, 'HTTP/2 401 '); $result2 = $this->authHelper->promptAuthIfNeeded('https://bitbucket.org/workspace/repo2/get/hash2.zip', $origin, 401, 'HTTP/2 401 '); - $this->assertSame( + self::assertSame( $expectedResult, $result1 ); - $this->assertSame( + self::assertSame( $expectedResult, $result2 ); diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index dfaac651dff9..8b977e56f45f 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -102,7 +102,7 @@ public function testRequestAccessTokenWithValidOAuthConsumer(): void $this->setExpectationsForStoringAccessToken(); - $this->assertEquals( + self::assertEquals( $this->token, $this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret) ); @@ -124,7 +124,7 @@ public function testRequestAccessTokenWithValidOAuthConsumerAndValidStoredAccess ] ); - $this->assertEquals( + self::assertEquals( $this->token, $this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret) ); @@ -178,7 +178,7 @@ public function testRequestAccessTokenWithValidOAuthConsumerAndExpiredAccessToke $this->setExpectationsForStoringAccessToken(); - $this->assertEquals( + self::assertEquals( $this->token, $this->bitbucket->requestToken($this->origin, $this->consumer_key, $this->consumer_secret) ); @@ -222,7 +222,7 @@ public function testRequestAccessTokenWithUsernameAndPassword(): void ->with('bitbucket-oauth') ->willReturn(null); - $this->assertEquals('', $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); + self::assertEquals('', $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); } public function testRequestAccessTokenWithUsernameAndPasswordWithUnauthorizedResponse(): void @@ -252,7 +252,7 @@ public function testRequestAccessTokenWithUsernameAndPasswordWithUnauthorizedRes ) ->willThrowException(new \Composer\Downloader\TransportException('HTTP/1.1 401 UNAUTHORIZED', 401)); - $this->assertEquals('', $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); + self::assertEquals('', $this->bitbucket->requestToken($this->origin, $this->username, $this->password)); } public function testRequestAccessTokenWithUsernameAndPasswordWithNotFoundResponse(): void @@ -315,7 +315,7 @@ public function testUsernamePasswordAuthenticationFlow(): void $this->setExpectationsForStoringAccessToken(true); - $this->assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + self::assertTrue($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } public function testAuthorizeOAuthInteractivelyWithEmptyUsername(): void @@ -329,7 +329,7 @@ public function testAuthorizeOAuthInteractivelyWithEmptyUsername(): void ['ask' => 'Consumer Key (hidden): ', 'reply' => ''], ]); - $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + self::assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } public function testAuthorizeOAuthInteractivelyWithEmptyPassword(): void @@ -345,7 +345,7 @@ public function testAuthorizeOAuthInteractivelyWithEmptyPassword(): void ['ask' => 'Consumer Secret (hidden): ', 'reply' => ''], ]); - $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + self::assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } public function testAuthorizeOAuthInteractivelyWithRequestAccessTokenFailure(): void @@ -378,7 +378,7 @@ public function testAuthorizeOAuthInteractivelyWithRequestAccessTokenFailure(): ) ); - $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); + self::assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } private function setExpectationsForStoringAccessToken(bool $removeBasicAuth = false): void @@ -418,7 +418,7 @@ private function setExpectationsForStoringAccessToken(bool $removeBasicAuth = fa public function testGetTokenWithoutAccessToken(): void { - $this->assertSame('', $this->bitbucket->getToken()); + self::assertSame('', $this->bitbucket->getToken()); } /** @@ -426,12 +426,12 @@ public function testGetTokenWithoutAccessToken(): void */ public function testGetTokenWithAccessToken(Bitbucket $bitbucket): void { - $this->assertSame($this->token, $bitbucket->getToken()); + self::assertSame($this->token, $bitbucket->getToken()); } public function testAuthorizeOAuthWithWrongOriginUrl(): void { - $this->assertFalse($this->bitbucket->authorizeOAuth('non-' . $this->origin)); + self::assertFalse($this->bitbucket->authorizeOAuth('non-' . $this->origin)); } public function testAuthorizeOAuthWithoutAvailableGitConfigToken(): void @@ -441,7 +441,7 @@ public function testAuthorizeOAuthWithoutAvailableGitConfigToken(): void $bitbucket = new Bitbucket($this->io, $this->config, $process, $this->httpDownloader, $this->time); - $this->assertFalse($bitbucket->authorizeOAuth($this->origin)); + self::assertFalse($bitbucket->authorizeOAuth($this->origin)); } public function testAuthorizeOAuthWithAvailableGitConfigToken(): void @@ -450,6 +450,6 @@ public function testAuthorizeOAuthWithAvailableGitConfigToken(): void $bitbucket = new Bitbucket($this->io, $this->config, $process, $this->httpDownloader, $this->time); - $this->assertTrue($bitbucket->authorizeOAuth($this->origin)); + self::assertTrue($bitbucket->authorizeOAuth($this->origin)); } } diff --git a/tests/Composer/Test/Util/ConfigValidatorTest.php b/tests/Composer/Test/Util/ConfigValidatorTest.php index c6d2bbfde3da..885390efc1a4 100644 --- a/tests/Composer/Test/Util/ConfigValidatorTest.php +++ b/tests/Composer/Test/Util/ConfigValidatorTest.php @@ -29,7 +29,7 @@ public function testConfigValidatorCommitRefWarning(): void $configValidator = new ConfigValidator(new NullIO()); [, , $warnings] = $configValidator->validate(__DIR__ . '/Fixtures/composer_commit-ref.json'); - $this->assertContains( + self::assertContains( 'The package "some/package" is pointing to a commit-ref, this is bad practice and can cause unforeseen issues.', $warnings ); @@ -40,7 +40,7 @@ public function testConfigValidatorWarnsOnScriptDescriptionForNonexistentScript( $configValidator = new ConfigValidator(new NullIO()); [, , $warnings] = $configValidator->validate(__DIR__ . '/Fixtures/composer_scripts-descriptions.json'); - $this->assertContains( + self::assertContains( 'Description for non-existent script "phpcsxxx" found in "scripts-descriptions"', $warnings ); @@ -51,7 +51,7 @@ public function testConfigValidatorWarnsOnScriptAliasForNonexistentScript(): voi $configValidator = new ConfigValidator(new NullIO()); [, , $warnings] = $configValidator->validate(__DIR__ . '/Fixtures/composer_scripts-aliases.json'); - $this->assertContains( + self::assertContains( 'Aliases for non-existent script "phpcsxxx" found in "scripts-aliases"', $warnings ); @@ -62,15 +62,15 @@ public function testConfigValidatorWarnsOnUnnecessaryProvideReplace(): void $configValidator = new ConfigValidator(new NullIO()); [, , $warnings] = $configValidator->validate(__DIR__ . '/Fixtures/composer_provide-replace-requirements.json'); - $this->assertContains( + self::assertContains( 'The package a/a in require is also listed in provide which satisfies the requirement. Remove it from provide if you wish to install it.', $warnings ); - $this->assertContains( + self::assertContains( 'The package b/b in require is also listed in replace which satisfies the requirement. Remove it from replace if you wish to install it.', $warnings ); - $this->assertContains( + self::assertContains( 'The package c/c in require-dev is also listed in provide which satisfies the requirement. Remove it from provide if you wish to install it.', $warnings ); diff --git a/tests/Composer/Test/Util/ErrorHandlerTest.php b/tests/Composer/Test/Util/ErrorHandlerTest.php index a177f6fe0e9d..ae89d83b2867 100644 --- a/tests/Composer/Test/Util/ErrorHandlerTest.php +++ b/tests/Composer/Test/Util/ErrorHandlerTest.php @@ -36,7 +36,7 @@ protected function tearDown(): void */ public function testErrorHandlerCaptureNotice(): void { - if (PHP_VERSION_ID >= 80000) { + if (\PHP_VERSION_ID >= 80000) { self::expectException('\ErrorException'); self::expectExceptionMessage('Undefined array key "baz"'); } else { @@ -45,7 +45,7 @@ public function testErrorHandlerCaptureNotice(): void } $array = ['foo' => 'bar']; - // @phpstan-ignore-next-line + // @phpstan-ignore offsetAccess.notFound, expr.resultUnused $array['baz']; } @@ -54,7 +54,7 @@ public function testErrorHandlerCaptureNotice(): void */ public function testErrorHandlerCaptureWarning(): void { - if (PHP_VERSION_ID >= 80000) { + if (\PHP_VERSION_ID >= 80000) { self::expectException('TypeError'); self::expectExceptionMessage('array_merge'); } else { @@ -62,7 +62,7 @@ public function testErrorHandlerCaptureWarning(): void self::expectExceptionMessage('array_merge'); } - // @phpstan-ignore-next-line + // @phpstan-ignore function.resultUnused, argument.type array_merge([], 'string'); } diff --git a/tests/Composer/Test/Util/FilesystemTest.php b/tests/Composer/Test/Util/FilesystemTest.php index 83a28d75b5a1..e947e4fe9e03 100644 --- a/tests/Composer/Test/Util/FilesystemTest.php +++ b/tests/Composer/Test/Util/FilesystemTest.php @@ -54,10 +54,10 @@ protected function tearDown(): void /** * @dataProvider providePathCouplesAsCode */ - public function testFindShortestPathCode(string $a, string $b, bool $directory, string $expected, bool $static = false): void + public function testFindShortestPathCode(string $a, string $b, bool $directory, string $expected, bool $static = false, bool $preferRelative = false): void { $fs = new Filesystem; - $this->assertEquals($expected, $fs->findShortestPathCode($a, $b, $directory, $static)); + self::assertEquals($expected, $fs->findShortestPathCode($a, $b, $directory, $static, $preferRelative)); } public static function providePathCouplesAsCode(): array @@ -77,6 +77,7 @@ public static function providePathCouplesAsCode(): array ['/foo/bar', '/foo/baz', true, "dirname(__DIR__).'/baz'"], ['/foo/bin/run', '/foo/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"], ['/foo/bin/run', '/bar/bin/run', true, "'/bar/bin/run'"], + ['/app/vendor/foo/bar', '/lib', true, "dirname(dirname(dirname(dirname(__DIR__)))).'/lib'", false, true], ['/bin/run', '/bin/run', true, "__DIR__"], ['c:/bin/run', 'C:\\bin/run', true, "__DIR__"], ['c:/bin/run', 'c:/vendor/acme/bin/run', true, "dirname(dirname(__DIR__)).'/vendor/acme/bin/run'"], @@ -113,10 +114,10 @@ public static function providePathCouplesAsCode(): array /** * @dataProvider providePathCouples */ - public function testFindShortestPath(string $a, string $b, string $expected, bool $directory = false): void + public function testFindShortestPath(string $a, string $b, string $expected, bool $directory = false, bool $preferRelative = false): void { $fs = new Filesystem; - $this->assertEquals($expected, $fs->findShortestPath($a, $b, $directory)); + self::assertEquals($expected, $fs->findShortestPath($a, $b, $directory, $preferRelative)); } public static function providePathCouples(): array @@ -152,6 +153,7 @@ public static function providePathCouples(): array ['C:/Temp', 'c:\Temp\..\..\test', "../test", true], ['C:/Temp/../..', 'c:\Temp\..\..\test', "./test", true], ['C:/Temp/../..', 'D:\Temp\..\..\test', "D:/test", true], + ['/app/vendor/foo/bar', '/lib', '../../../../lib', true, true], ['/tmp', '/tmp/../../test', '../test', true], ['/tmp', '/test', '../test', true], ['/foo/bar', '/foo/bar_vendor', '../bar_vendor', true], @@ -171,8 +173,8 @@ public function testRemoveDirectoryPhp(): void file_put_contents($this->workingDir . "/level1/level2/hello.txt", "hello world"); $fs = new Filesystem; - $this->assertTrue($fs->removeDirectoryPhp($this->workingDir)); - $this->assertFileDoesNotExist($this->workingDir . "/level1/level2/hello.txt"); + self::assertTrue($fs->removeDirectoryPhp($this->workingDir)); + self::assertFileDoesNotExist($this->workingDir . "/level1/level2/hello.txt"); } public function testFileSize(): void @@ -180,7 +182,7 @@ public function testFileSize(): void file_put_contents($this->testFile, 'Hello'); $fs = new Filesystem; - $this->assertGreaterThanOrEqual(5, $fs->size($this->testFile)); + self::assertGreaterThanOrEqual(5, $fs->size($this->testFile)); } public function testDirectorySize(): void @@ -190,7 +192,7 @@ public function testDirectorySize(): void file_put_contents($this->workingDir."/file2.txt", 'World'); $fs = new Filesystem; - $this->assertGreaterThanOrEqual(10, $fs->size($this->workingDir)); + self::assertGreaterThanOrEqual(10, $fs->size($this->workingDir)); } /** @@ -199,7 +201,7 @@ public function testDirectorySize(): void public function testNormalizePath(string $expected, string $actual): void { $fs = new Filesystem; - $this->assertEquals($expected, $fs->normalizePath($actual)); + self::assertEquals($expected, $fs->normalizePath($actual)); } public static function provideNormalizedPaths(): array @@ -251,8 +253,8 @@ public function testUnlinkSymlinkedDirectory(): void $fs = new Filesystem(); $result = $fs->unlink($symlinked); - $this->assertTrue($result); - $this->assertFileDoesNotExist($symlinked); + self::assertTrue($result); + self::assertFileDoesNotExist($symlinked); } /** @@ -283,9 +285,9 @@ public function testRemoveSymlinkedDirectoryWithTrailingSlash(): void $fs = new Filesystem(); $result = $fs->removeDirectory($symlinkedTrailingSlash); - $this->assertTrue($result); - $this->assertFileDoesNotExist($symlinkedTrailingSlash); - $this->assertFileDoesNotExist($symlinked); + self::assertTrue($result); + self::assertFileDoesNotExist($symlinkedTrailingSlash); + self::assertFileDoesNotExist($symlinked); } public function testJunctions(): void @@ -295,8 +297,8 @@ public function testJunctions(): void // Non-Windows systems do not support this and will return false on all tests, and an exception on creation if (!defined('PHP_WINDOWS_VERSION_BUILD')) { - $this->assertFalse($fs->isJunction($this->workingDir)); - $this->assertFalse($fs->removeJunction($this->workingDir)); + self::assertFalse($fs->isJunction($this->workingDir)); + self::assertFalse($fs->removeJunction($this->workingDir)); self::expectException('LogicException'); self::expectExceptionMessage('not available on non-Windows platform'); } @@ -306,16 +308,16 @@ public function testJunctions(): void // Create and detect junction $fs->junction($target, $junction); - $this->assertTrue($fs->isJunction($junction), $junction . ': is a junction'); - $this->assertFalse($fs->isJunction($target), $target . ': is not a junction'); - $this->assertTrue($fs->isJunction($target . '/../../junction'), $target . '/../../junction: is a junction'); - $this->assertFalse($fs->isJunction($junction . '/../real'), $junction . '/../real: is not a junction'); - $this->assertTrue($fs->isJunction($junction . '/../junction'), $junction . '/../junction: is a junction'); + self::assertTrue($fs->isJunction($junction), $junction . ': is a junction'); + self::assertFalse($fs->isJunction($target), $target . ': is not a junction'); + self::assertTrue($fs->isJunction($target . '/../../junction'), $target . '/../../junction: is a junction'); + self::assertFalse($fs->isJunction($junction . '/../real'), $junction . '/../real: is not a junction'); + self::assertTrue($fs->isJunction($junction . '/../junction'), $junction . '/../junction: is a junction'); // Remove junction - $this->assertDirectoryExists($junction, $junction . ' is a directory'); - $this->assertTrue($fs->removeJunction($junction), $junction . ' has been removed'); - $this->assertDirectoryDoesNotExist($junction, $junction . ' is not a directory'); + self::assertDirectoryExists($junction, $junction . ' is a directory'); + self::assertTrue($fs->removeJunction($junction), $junction . ' has been removed'); + self::assertDirectoryDoesNotExist($junction, $junction . ' is not a directory'); } public function testOverrideJunctions(): void @@ -335,19 +337,19 @@ public function testOverrideJunctions(): void $fs->junction($old_target, $junction); $fs->junction($target, $junction); - $this->assertTrue($fs->isJunction($junction), $junction.': is a junction'); - $this->assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction'); + self::assertTrue($fs->isJunction($junction), $junction.': is a junction'); + self::assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction'); //Remove junction - $this->assertTrue($fs->removeJunction($junction), $junction . ' has been removed'); + self::assertTrue($fs->removeJunction($junction), $junction . ' has been removed'); // Override broken junction $fs->junction($old_target, $junction); $fs->removeDirectory($old_target); $fs->junction($target, $junction); - $this->assertTrue($fs->isJunction($junction), $junction.': is a junction'); - $this->assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction'); + self::assertTrue($fs->isJunction($junction), $junction.': is a junction'); + self::assertTrue($fs->isJunction($target.'/../../junction'), $target.'/../../junction: is a junction'); } public function testCopy(): void @@ -362,17 +364,17 @@ public function testCopy(): void $fs = new Filesystem(); $result1 = $fs->copy($this->workingDir . '/foo', $this->workingDir . '/foop'); - $this->assertTrue($result1, 'Copying directory failed.'); - $this->assertDirectoryExists($this->workingDir . '/foop', 'Not a directory: ' . $this->workingDir . '/foop'); - $this->assertDirectoryExists($this->workingDir . '/foop/bar', 'Not a directory: ' . $this->workingDir . '/foop/bar'); - $this->assertDirectoryExists($this->workingDir . '/foop/baz', 'Not a directory: ' . $this->workingDir . '/foop/baz'); - $this->assertFileExists($this->workingDir . '/foop/foo.file', 'Not a file: ' . $this->workingDir . '/foop/foo.file'); - $this->assertFileExists($this->workingDir . '/foop/bar/foobar.file', 'Not a file: ' . $this->workingDir . '/foop/bar/foobar.file'); - $this->assertFileExists($this->workingDir . '/foop/baz/foobaz.file', 'Not a file: ' . $this->workingDir . '/foop/baz/foobaz.file'); + self::assertTrue($result1, 'Copying directory failed.'); + self::assertDirectoryExists($this->workingDir . '/foop', 'Not a directory: ' . $this->workingDir . '/foop'); + self::assertDirectoryExists($this->workingDir . '/foop/bar', 'Not a directory: ' . $this->workingDir . '/foop/bar'); + self::assertDirectoryExists($this->workingDir . '/foop/baz', 'Not a directory: ' . $this->workingDir . '/foop/baz'); + self::assertFileExists($this->workingDir . '/foop/foo.file', 'Not a file: ' . $this->workingDir . '/foop/foo.file'); + self::assertFileExists($this->workingDir . '/foop/bar/foobar.file', 'Not a file: ' . $this->workingDir . '/foop/bar/foobar.file'); + self::assertFileExists($this->workingDir . '/foop/baz/foobaz.file', 'Not a file: ' . $this->workingDir . '/foop/baz/foobaz.file'); $result2 = $fs->copy($this->testFile, $this->workingDir . '/testfile.file'); - $this->assertTrue($result2); - $this->assertFileExists($this->workingDir . '/testfile.file'); + self::assertTrue($result2); + self::assertFileExists($this->workingDir . '/testfile.file'); } public function testCopyThenRemove(): void @@ -387,14 +389,14 @@ public function testCopyThenRemove(): void $fs = new Filesystem(); $fs->copyThenRemove($this->testFile, $this->workingDir . '/testfile.file'); - $this->assertFileDoesNotExist($this->testFile, 'Still a file: ' . $this->testFile); + self::assertFileDoesNotExist($this->testFile, 'Still a file: ' . $this->testFile); $fs->copyThenRemove($this->workingDir . '/foo', $this->workingDir . '/foop'); - $this->assertFileDoesNotExist($this->workingDir . '/foo/baz/foobaz.file', 'Still a file: ' . $this->workingDir . '/foo/baz/foobaz.file'); - $this->assertFileDoesNotExist($this->workingDir . '/foo/bar/foobar.file', 'Still a file: ' . $this->workingDir . '/foo/bar/foobar.file'); - $this->assertFileDoesNotExist($this->workingDir . '/foo/foo.file', 'Still a file: ' . $this->workingDir . '/foo/foo.file'); - $this->assertDirectoryDoesNotExist($this->workingDir . '/foo/baz', 'Still a directory: ' . $this->workingDir . '/foo/baz'); - $this->assertDirectoryDoesNotExist($this->workingDir . '/foo/bar', 'Still a directory: ' . $this->workingDir . '/foo/bar'); - $this->assertDirectoryDoesNotExist($this->workingDir . '/foo', 'Still a directory: ' . $this->workingDir . '/foo'); + self::assertFileDoesNotExist($this->workingDir . '/foo/baz/foobaz.file', 'Still a file: ' . $this->workingDir . '/foo/baz/foobaz.file'); + self::assertFileDoesNotExist($this->workingDir . '/foo/bar/foobar.file', 'Still a file: ' . $this->workingDir . '/foo/bar/foobar.file'); + self::assertFileDoesNotExist($this->workingDir . '/foo/foo.file', 'Still a file: ' . $this->workingDir . '/foo/foo.file'); + self::assertDirectoryDoesNotExist($this->workingDir . '/foo/baz', 'Still a directory: ' . $this->workingDir . '/foo/baz'); + self::assertDirectoryDoesNotExist($this->workingDir . '/foo/bar', 'Still a directory: ' . $this->workingDir . '/foo/bar'); + self::assertDirectoryDoesNotExist($this->workingDir . '/foo', 'Still a directory: ' . $this->workingDir . '/foo'); } } diff --git a/tests/Composer/Test/Util/GitHubTest.php b/tests/Composer/Test/Util/GitHubTest.php index d9e0e541209b..7ef74b934199 100644 --- a/tests/Composer/Test/Util/GitHubTest.php +++ b/tests/Composer/Test/Util/GitHubTest.php @@ -55,7 +55,7 @@ public function testUsernamePasswordAuthenticationFlow(): void $github = new GitHub($io, $config, null, $httpDownloader); - $this->assertTrue($github->authorizeOAuthInteractively($this->origin, $this->message)); + self::assertTrue($github->authorizeOAuthInteractively($this->origin, $this->message)); } public function testUsernamePasswordFailure(): void @@ -80,7 +80,7 @@ public function testUsernamePasswordFailure(): void $github = new GitHub($io, $config, null, $httpDownloader); - $this->assertFalse($github->authorizeOAuthInteractively($this->origin)); + self::assertFalse($github->authorizeOAuthInteractively($this->origin)); } /** diff --git a/tests/Composer/Test/Util/GitLabTest.php b/tests/Composer/Test/Util/GitLabTest.php index 5c8cc9443e5d..095cd747a838 100644 --- a/tests/Composer/Test/Util/GitLabTest.php +++ b/tests/Composer/Test/Util/GitLabTest.php @@ -57,7 +57,7 @@ public function testUsernamePasswordAuthenticationFlow(): void $gitLab = new GitLab($io, $config, null, $httpDownloader); - $this->assertTrue($gitLab->authorizeOAuthInteractively('http', $this->origin, $this->message)); + self::assertTrue($gitLab->authorizeOAuthInteractively('http', $this->origin, $this->message)); } public function testUsernamePasswordFailure(): void diff --git a/tests/Composer/Test/Util/GitTest.php b/tests/Composer/Test/Util/GitTest.php index 9a0ec1e8feb8..98936459d8f6 100644 --- a/tests/Composer/Test/Util/GitTest.php +++ b/tests/Composer/Test/Util/GitTest.php @@ -14,6 +14,7 @@ use Composer\Config; use Composer\IO\IOInterface; +use Composer\Test\Mock\HttpDownloaderMock; use Composer\Util\Filesystem; use Composer\Util\Git; use Composer\Test\Mock\ProcessExecutorMock; @@ -47,7 +48,7 @@ protected function setUp(): void public function testRunCommandPublicGitHubRepositoryNotInitialClone(string $protocol, string $expectedUrl): void { $commandCallable = function ($url) use ($expectedUrl): string { - $this->assertSame($expectedUrl, $url); + self::assertSame($expectedUrl, $url); return 'git command'; }; @@ -56,6 +57,7 @@ public function testRunCommandPublicGitHubRepositoryNotInitialClone(string $prot $this->process->expects(['git command'], true); + // @phpstan-ignore method.deprecated $this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true); } @@ -72,7 +74,7 @@ public function testRunCommandPrivateGitHubRepositoryNotInitialCloneNotInteracti self::expectException('RuntimeException'); $commandCallable = function ($url): string { - $this->assertSame('https://github.com/acme/repo', $url); + self::assertSame('https://github.com/acme/repo', $url); return 'git command'; }; @@ -81,9 +83,10 @@ public function testRunCommandPrivateGitHubRepositoryNotInitialCloneNotInteracti $this->process->expects([ ['cmd' => 'git command', 'return' => 1], - ['cmd' => 'git --version', 'return' => 0], + ['cmd' => ['git', '--version'], 'return' => 0], ], true); + // @phpstan-ignore method.deprecated $this->git->runCommand($commandCallable, 'https://github.com/acme/repo', null, true); } @@ -123,9 +126,169 @@ public function testRunCommandPrivateGitHubRepositoryNotInitialCloneNotInteracti ->with($this->equalTo('github.com')) ->willReturn(['username' => 'token', 'password' => $gitHubToken]); + // @phpstan-ignore method.deprecated $this->git->runCommand($commandCallable, $gitUrl, null, true); } + /** + * @dataProvider privateBitbucketWithCredentialsProvider + */ + public function testRunCommandPrivateBitbucketRepositoryNotInitialCloneNotInteractiveWithAuthentication(string $gitUrl, ?string $bitbucketToken, string $expectedUrl, int $expectedFailuresBeforeSuccess, int $bitbucket_git_auth_calls = 0): void + { + $commandCallable = static function ($url) use ($expectedUrl): string { + if ($url !== $expectedUrl) { + return 'git command failing'; + } + + return 'git command ok'; + }; + + $this->config + ->method('get') + ->willReturnMap([ + ['gitlab-domains', 0, ['gitlab.com']], + ['github-domains', 0, ['github.com']], + ]); + + $expectedCalls = array_fill(0, $expectedFailuresBeforeSuccess, ['cmd' => 'git command failing', 'return' => 1]); + if ($bitbucket_git_auth_calls > 0) { + // When we are testing what happens without auth saved, and URLs + // with https, there will also be an attempt to find the token in + // the git config for the folder and repo, locally. + $additional_calls = array_fill(0, $bitbucket_git_auth_calls, ['cmd' => ['git', 'config', 'bitbucket.accesstoken'], 'return' => 1]); + foreach ($additional_calls as $call) { + $expectedCalls[] = $call; + } + } + $expectedCalls[] = ['cmd' => 'git command ok', 'return' => 0]; + + $this->process->expects($expectedCalls, true); + + $this->io + ->method('isInteractive') + ->willReturn(false); + + if (null !== $bitbucketToken) { + $this->io + ->expects($this->atLeastOnce()) + ->method('hasAuthentication') + ->with($this->equalTo('bitbucket.org')) + ->willReturn(true); + $this->io + ->expects($this->atLeastOnce()) + ->method('getAuthentication') + ->with($this->equalTo('bitbucket.org')) + ->willReturn(['username' => 'token', 'password' => $bitbucketToken]); + } + // @phpstan-ignore method.deprecated + $this->git->runCommand($commandCallable, $gitUrl, null, true); + } + + /** + * @dataProvider privateBitbucketWithOauthProvider + * + * @param string $gitUrl + * @param string $expectedUrl + * @param array{'username': string, 'password': string}[] $initial_config + */ + public function testRunCommandPrivateBitbucketRepositoryNotInitialCloneInteractiveWithOauth(string $gitUrl, string $expectedUrl, array $initial_config = []): void + { + $commandCallable = static function ($url) use ($expectedUrl): string { + if ($url !== $expectedUrl) { + return 'git command failing'; + } + + return 'git command ok'; + }; + + $expectedCalls = []; + $expectedCalls[] = ['cmd' => 'git command failing', 'return' => 1]; + if (count($initial_config) > 0) { + $expectedCalls[] = ['cmd' => 'git command failing', 'return' => 1]; + } else { + $expectedCalls[] = ['cmd' => ['git', 'config', 'bitbucket.accesstoken'], 'return' => 1]; + } + $expectedCalls[] = ['cmd' => 'git command ok', 'return' => 0]; + $this->process->expects($expectedCalls, true); + + $this->config + ->method('get') + ->willReturnMap([ + ['gitlab-domains', 0, ['gitlab.com']], + ['github-domains', 0, ['github.com']], + ]); + + $this->io + ->method('isInteractive') + ->willReturn(true); + + $this->io + ->method('askConfirmation') + ->willReturnCallback(function () { + return true; + }); + $this->io->method('askAndHideAnswer') + ->willReturnCallback(function ($question) { + switch ($question) { + case 'Consumer Key (hidden): ': + return 'my-consumer-key'; + case 'Consumer Secret (hidden): ': + return 'my-consumer-secret'; + } + return ''; + }); + + $this->io + ->method('hasAuthentication') + ->with($this->equalTo('bitbucket.org')) + ->willReturnCallback(function ($repositoryName) use (&$initial_config) { + return isset($initial_config[$repositoryName]); + }); + $this->io + ->method('setAuthentication') + ->willReturnCallback(function (string $repositoryName, string $username, ?string $password = null) use (&$initial_config) { + $initial_config[$repositoryName] = ['username' => $username, 'password' => $password]; + }); + $this->io + ->method('getAuthentication') + ->willReturnCallback(function (string $repositoryName) use (&$initial_config) { + if (isset($initial_config[$repositoryName])) { + return $initial_config[$repositoryName]; + } + + return ['username' => null, 'password' => null]; + }); + $downloader_mock = $this->getHttpDownloaderMock(); + $downloader_mock->expects([ + ['url' => 'https://bitbucket.org/site/oauth2/access_token', 'status' => 200, 'body' => '{"expires_in": 600, "access_token": "my-access-token"}'] + ]); + $this->git->setHttpDownloader($downloader_mock); + // @phpstan-ignore method.deprecated + $this->git->runCommand($commandCallable, $gitUrl, null, true); + } + + public static function privateBitbucketWithOauthProvider(): array + { + return [ + ['git@bitbucket.org:acme/repo.git', 'https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git'], + ['https://bitbucket.org/acme/repo.git', 'https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git'], + ['https://bitbucket.org/acme/repo', 'https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git'], + ['git@bitbucket.org:acme/repo.git', 'https://x-token-auth:my-access-token@bitbucket.org/acme/repo.git', ['bitbucket.org' => ['username' => 'someuseralsoswappedfortoken', 'password' => 'little green men']]], + ]; + } + + public static function privateBitbucketWithCredentialsProvider(): array + { + return [ + ['git@bitbucket.org:acme/repo.git', 'MY_BITBUCKET_TOKEN', 'https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git', 1], + ['https://bitbucket.org/acme/repo', 'MY_BITBUCKET_TOKEN', 'https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git', 1], + ['https://bitbucket.org/acme/repo.git', 'MY_BITBUCKET_TOKEN', 'https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git', 1], + ['git@bitbucket.org:acme/repo.git', null, 'git@bitbucket.org:acme/repo.git', 0], + ['https://bitbucket.org/acme/repo', null, 'git@bitbucket.org:acme/repo.git', 1, 1], + ['https://bitbucket.org/acme/repo.git', null, 'git@bitbucket.org:acme/repo.git', 1, 1], + ]; + } + public static function privateGithubWithCredentialsProvider(): array { return [ diff --git a/tests/Composer/Test/Util/Http/ProxyHelperTest.php b/tests/Composer/Test/Util/Http/ProxyHelperTest.php deleted file mode 100644 index 8b9dfb1b106c..000000000000 --- a/tests/Composer/Test/Util/Http/ProxyHelperTest.php +++ /dev/null @@ -1,201 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Test\Util\Http; - -use Composer\Util\Http\ProxyHelper; -use Composer\Test\TestCase; - -class ProxyHelperTest extends TestCase -{ - protected function setUp(): void - { - unset( - $_SERVER['HTTP_PROXY'], - $_SERVER['http_proxy'], - $_SERVER['HTTPS_PROXY'], - $_SERVER['https_proxy'], - $_SERVER['NO_PROXY'], - $_SERVER['no_proxy'], - $_SERVER['CGI_HTTP_PROXY'] - ); - } - - protected function tearDown(): void - { - parent::tearDown(); - unset( - $_SERVER['HTTP_PROXY'], - $_SERVER['http_proxy'], - $_SERVER['HTTPS_PROXY'], - $_SERVER['https_proxy'], - $_SERVER['NO_PROXY'], - $_SERVER['no_proxy'], - $_SERVER['CGI_HTTP_PROXY'] - ); - } - - /** - * @dataProvider dataMalformed - */ - public function testThrowsOnMalformedUrl(string $url): void - { - $_SERVER['http_proxy'] = $url; - - self::expectException('RuntimeException'); - ProxyHelper::getProxyData(); - } - - public static function dataMalformed(): array - { - return [ - 'no-host' => ['localhost'], - 'no-port' => ['scheme://localhost'], - ]; - } - - /** - * @dataProvider dataFormatting - */ - public function testUrlFormatting(string $url, string $expected): void - { - $_SERVER['http_proxy'] = $url; - - [$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData(); - $this->assertSame($expected, $httpProxy); - } - - public static function dataFormatting(): array - { - // url, expected - return [ - 'lowercases-scheme' => ['HTTP://proxy.com:8888', 'http://proxy.com:8888'], - 'adds-http-port' => ['http://proxy.com', 'http://proxy.com:80'], - 'adds-https-port' => ['https://proxy.com', 'https://proxy.com:443'], - ]; - } - - /** - * @dataProvider dataCaseOverrides - * - * @param array $server - */ - public function testLowercaseOverridesUppercase(array $server, string $expected, int $index): void - { - $_SERVER = array_merge($_SERVER, $server); - - $list = ProxyHelper::getProxyData(); - $this->assertSame($expected, $list[$index]); - } - - public static function dataCaseOverrides(): array - { - // server, expected, list index - return [ - [['HTTP_PROXY' => 'http://upper.com', 'http_proxy' => 'http://lower.com'], 'http://lower.com:80', 0], - [['HTTPS_PROXY' => 'http://upper.com', 'https_proxy' => 'http://lower.com'], 'http://lower.com:80', 1], - [['NO_PROXY' => 'upper.com', 'no_proxy' => 'lower.com'], 'lower.com', 2], - ]; - } - - /** - * @dataProvider dataCGIOverrides - * - * @param array $server - */ - public function testCGIUpperCaseOverridesHttp(array $server, string $expected, int $index): void - { - $_SERVER = array_merge($_SERVER, $server); - - $list = ProxyHelper::getProxyData(); - $this->assertSame($expected, $list[$index]); - } - - public static function dataCGIOverrides(): array - { - // server, expected, list index - return [ - [['http_proxy' => 'http://http.com', 'CGI_HTTP_PROXY' => 'http://cgi.com'], 'http://cgi.com:80', 0], - [['http_proxy' => 'http://http.com', 'cgi_http_proxy' => 'http://cgi.com'], 'http://http.com:80', 0], - ]; - } - - public function testNoHttpsProxyUsesHttpProxy(): void - { - $_SERVER['http_proxy'] = 'http://http.com'; - - [$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData(); - $this->assertSame('http://http.com:80', $httpsProxy); - } - - public function testNoHttpProxyDoesNotUseHttpsProxy(): void - { - $_SERVER['https_proxy'] = 'http://https.com'; - - [$httpProxy, $httpsProxy, $noProxy] = ProxyHelper::getProxyData(); - $this->assertSame(null, $httpProxy); - } - - /** - * @dataProvider dataContextOptions - * - * @param array $expected - * - * @phpstan-param array{http: array{proxy: string, header?: string}} $expected - */ - public function testGetContextOptions(string $url, array $expected): void - { - $this->assertEquals($expected, ProxyHelper::getContextOptions($url)); - } - - public static function dataContextOptions(): array - { - // url, expected - return [ - ['http://proxy.com', ['http' => [ - 'proxy' => 'tcp://proxy.com:80', - ]]], - ['https://proxy.com', ['http' => [ - 'proxy' => 'ssl://proxy.com:443', - ]]], - ['http://user:p%40ss@proxy.com', ['http' => [ - 'proxy' => 'tcp://proxy.com:80', - 'header' => 'Proxy-Authorization: Basic dXNlcjpwQHNz', - ]]], - ]; - } - - /** - * @dataProvider dataRequestFullUri - * - * @param mixed[] $expected - */ - public function testSetRequestFullUri(string $requestUrl, array $expected): void - { - $options = []; - ProxyHelper::setRequestFullUri($requestUrl, $options); - - $this->assertEquals($expected, $options); - } - - public static function dataRequestFullUri(): array - { - $options = ['http' => ['request_fulluri' => true]]; - - // $requestUrl, expected - return [ - 'http' => ['http://repo.org', $options], - 'https' => ['https://repo.org', []], - 'no-scheme' => ['repo.org', []], - ]; - } -} diff --git a/tests/Composer/Test/Util/Http/ProxyItemTest.php b/tests/Composer/Test/Util/Http/ProxyItemTest.php new file mode 100644 index 000000000000..ccca76ec67d0 --- /dev/null +++ b/tests/Composer/Test/Util/Http/ProxyItemTest.php @@ -0,0 +1,72 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Util\Http; + +use Composer\Util\Http\ProxyItem; +use Composer\Test\TestCase; + +class ProxyItemTest extends TestCase +{ + /** + * @dataProvider dataMalformed + */ + public function testThrowsOnMalformedUrl(string $url): void + { + self::expectException('RuntimeException'); + $proxyItem = new ProxyItem($url, 'http_proxy'); + } + + /** + * @return array> + */ + public static function dataMalformed(): array + { + return [ + 'ws-r' => ["http://user\rname@localhost:80"], + 'ws-n' => ["http://user\nname@localhost:80"], + 'ws-t' => ["http://user\tname@localhost:80"], + 'no-host' => ['localhost'], + 'no-port' => ['scheme://localhost'], + 'port-0' => ['http://localhost:0'], + 'port-big' => ['http://localhost:65536'], + ]; + } + + /** + * @dataProvider dataFormatting + */ + public function testUrlFormatting(string $url, string $expected): void + { + $proxyItem = new ProxyItem($url, 'http_proxy'); + $proxy = $proxyItem->toRequestProxy('http'); + + self::assertSame($expected, $proxy->getStatus()); + } + + /** + * @return array> + */ + public static function dataFormatting(): array + { + // url, expected + return [ + 'none' => ['http://proxy.com:8888', 'http://proxy.com:8888'], + 'lowercases-scheme' => ['HTTP://proxy.com:8888', 'http://proxy.com:8888'], + 'adds-http-scheme' => ['proxy.com:80', 'http://proxy.com:80'], + 'adds-http-port' => ['http://proxy.com', 'http://proxy.com:80'], + 'adds-https-port' => ['https://proxy.com', 'https://proxy.com:443'], + 'removes-user' => ['http://user@proxy.com:6180', 'http://***@proxy.com:6180'], + 'removes-user-pass' => ['http://user:p%40ss@proxy.com:6180', 'http://***:***@proxy.com:6180'], + ]; + } +} diff --git a/tests/Composer/Test/Util/Http/ProxyManagerTest.php b/tests/Composer/Test/Util/Http/ProxyManagerTest.php index 6321df6be876..04c756b48ae7 100644 --- a/tests/Composer/Test/Util/Http/ProxyManagerTest.php +++ b/tests/Composer/Test/Util/Http/ProxyManagerTest.php @@ -15,6 +15,9 @@ use Composer\Util\Http\ProxyManager; use Composer\Test\TestCase; +/** + * @phpstan-import-type contextOptions from \Composer\Util\Http\RequestProxy + */ class ProxyManagerTest extends TestCase { protected function setUp(): void @@ -26,7 +29,8 @@ protected function setUp(): void $_SERVER['https_proxy'], $_SERVER['NO_PROXY'], $_SERVER['no_proxy'], - $_SERVER['CGI_HTTP_PROXY'] + $_SERVER['CGI_HTTP_PROXY'], + $_SERVER['cgi_http_proxy'] ); ProxyManager::reset(); } @@ -41,7 +45,8 @@ protected function tearDown(): void $_SERVER['https_proxy'], $_SERVER['NO_PROXY'], $_SERVER['no_proxy'], - $_SERVER['CGI_HTTP_PROXY'] + $_SERVER['CGI_HTTP_PROXY'], + $_SERVER['cgi_http_proxy'] ); ProxyManager::reset(); } @@ -49,54 +54,118 @@ protected function tearDown(): void public function testInstantiation(): void { $originalInstance = ProxyManager::getInstance(); - $this->assertInstanceOf('Composer\Util\Http\ProxyManager', $originalInstance); - $sameInstance = ProxyManager::getInstance(); - $this->assertTrue($originalInstance === $sameInstance); + self::assertTrue($originalInstance === $sameInstance); ProxyManager::reset(); $newInstance = ProxyManager::getInstance(); - $this->assertFalse($sameInstance === $newInstance); + self::assertFalse($sameInstance === $newInstance); } public function testGetProxyForRequestThrowsOnBadProxyUrl(): void { $_SERVER['http_proxy'] = 'localhost'; $proxyManager = ProxyManager::getInstance(); + self::expectException('Composer\Downloader\TransportException'); $proxyManager->getProxyForRequest('http://example.com'); } /** - * @dataProvider dataRequest + * @dataProvider dataCaseOverrides * - * @param array $server - * @param mixed[] $expectedOptions - * @param non-empty-string $url + * @param array $server + * @param non-empty-string $url */ - public function testGetProxyForRequest(array $server, string $url, string $expectedUrl, array $expectedOptions, bool $expectedSecure, string $expectedMessage): void + public function testLowercaseOverridesUppercase(array $server, string $url, string $expectedUrl): void { $_SERVER = array_merge($_SERVER, $server); $proxyManager = ProxyManager::getInstance(); $proxy = $proxyManager->getProxyForRequest($url); - $this->assertInstanceOf('Composer\Util\Http\RequestProxy', $proxy); + self::assertSame($expectedUrl, $proxy->getStatus()); + } - $this->assertSame($expectedUrl, $proxy->getUrl()); - $this->assertSame($expectedOptions, $proxy->getContextOptions()); - $this->assertSame($expectedSecure, $proxy->isSecure()); + /** + * @return list, 1: string, 2: string}> + */ + public static function dataCaseOverrides(): array + { + // server, url, expectedUrl + return [ + [['HTTP_PROXY' => 'http://upper.com', 'http_proxy' => 'http://lower.com'], 'http://repo.org', 'http://lower.com:80'], + [['CGI_HTTP_PROXY' => 'http://upper.com', 'cgi_http_proxy' => 'http://lower.com'], 'http://repo.org', 'http://lower.com:80'], + [['HTTPS_PROXY' => 'http://upper.com', 'https_proxy' => 'http://lower.com'], 'https://repo.org', 'http://lower.com:80'], + ]; + } - $message = $proxy->getFormattedUrl(); + /** + * @dataProvider dataCGIProxy + * + * @param array $server + */ + public function testCGIProxyIsOnlyUsedWhenNoHttpProxy(array $server, string $expectedUrl): void + { + $_SERVER = array_merge($_SERVER, $server); + $proxyManager = ProxyManager::getInstance(); - if ($expectedMessage) { - $condition = stripos($message, $expectedMessage) !== false; - } else { - $condition = $expectedMessage === $message; - } + $proxy = $proxyManager->getProxyForRequest('http://repo.org'); + self::assertSame($expectedUrl, $proxy->getStatus()); + } + + /** + * @return list, 1: string}> + */ + public static function dataCGIProxy(): array + { + // server, expectedUrl + return [ + [['CGI_HTTP_PROXY' => 'http://cgi.com:80'], 'http://cgi.com:80'], + [['http_proxy' => 'http://http.com:80', 'CGI_HTTP_PROXY' => 'http://cgi.com:80'], 'http://http.com:80'], + ]; + } + + public function testNoHttpProxyDoesNotUseHttpsProxy(): void + { + $_SERVER['https_proxy'] = 'https://proxy.com:443'; + $proxyManager = ProxyManager::getInstance(); + + $proxy = $proxyManager->getProxyForRequest('http://repo.org'); + self::assertSame('', $proxy->getStatus()); + } + + public function testNoHttpsProxyDoesNotUseHttpProxy(): void + { + $_SERVER['http_proxy'] = 'http://proxy.com:80'; + $proxyManager = ProxyManager::getInstance(); - $this->assertTrue($condition, 'lastProxy check'); + $proxy = $proxyManager->getProxyForRequest('https://repo.org'); + self::assertSame('', $proxy->getStatus()); } + /** + * @dataProvider dataRequest + * + * @param array $server + * @param non-empty-string $url + * @param ?contextOptions $options + */ + public function testGetProxyForRequest(array $server, string $url, ?array $options, string $status, bool $excluded): void + { + $_SERVER = array_merge($_SERVER, $server); + $proxyManager = ProxyManager::getInstance(); + + $proxy = $proxyManager->getProxyForRequest($url); + self::assertSame($options, $proxy->getContextOptions()); + self::assertSame($status, $proxy->getStatus()); + self::assertSame($excluded, $proxy->isExcludedByNoProxy()); + } + + /** + * Tests context options. curl options are tested in RequestProxyTest.php + * + * @return list, 1: string, 2: ?contextOptions, 3: string, 4: bool}> + */ public static function dataRequest(): array { $server = [ @@ -105,68 +174,26 @@ public static function dataRequest(): array 'no_proxy' => 'other.repo.org', ]; - // server, url, expectedUrl, expectedOptions, expectedSecure, expectedMessage + // server, url, options, status, excluded return [ - [[], 'http://repo.org', '', [], false, ''], - [$server, 'http://repo.org', 'http://user:p%40ss@proxy.com:80', + [[], 'http://repo.org', null, '', false], + [$server, 'http://repo.org', ['http' => [ 'proxy' => 'tcp://proxy.com:80', 'header' => 'Proxy-Authorization: Basic dXNlcjpwQHNz', 'request_fulluri' => true, ]], + 'http://***:***@proxy.com:80', false, - 'http://user:***@proxy.com:80', ], - [ - $server, 'https://repo.org', 'https://proxy.com:443', + [$server, 'https://repo.org', ['http' => [ 'proxy' => 'ssl://proxy.com:443', ]], - true, 'https://proxy.com:443', + false, ], - [$server, 'https://other.repo.org', '', [], false, 'no_proxy'], - ]; - } - - /** - * @dataProvider dataStatus - * - * @param array $server - */ - public function testGetStatus(array $server, bool $expectedStatus, ?string $expectedMessage): void - { - $_SERVER = array_merge($_SERVER, $server); - $proxyManager = ProxyManager::getInstance(); - $status = $proxyManager->isProxying(); - $message = $proxyManager->getFormattedProxy(); - - $this->assertSame($expectedStatus, $status); - - if ($expectedMessage !== null) { - $condition = stripos($message, $expectedMessage) !== false; - } else { - $condition = $expectedMessage === $message; - } - $this->assertTrue($condition, 'message check'); - } - - public static function dataStatus(): array - { - // server, expectedStatus, expectedMessage - return [ - [[], false, null], - [['http_proxy' => 'localhost'], false, 'malformed'], - [ - ['http_proxy' => 'http://user:p%40ss@proxy.com:80'], - true, - 'http=http://user:***@proxy.com:80', - ], - [ - ['http_proxy' => 'proxy.com:80', 'https_proxy' => 'proxy.com:80'], - true, - 'http=proxy.com:80, https=proxy.com:80', - ], + [$server, 'https://other.repo.org', null, 'excluded by no_proxy', true], ]; } } diff --git a/tests/Composer/Test/Util/Http/RequestProxyTest.php b/tests/Composer/Test/Util/Http/RequestProxyTest.php index 66a03fccb330..c01948598717 100644 --- a/tests/Composer/Test/Util/Http/RequestProxyTest.php +++ b/tests/Composer/Test/Util/Http/RequestProxyTest.php @@ -17,45 +17,174 @@ class RequestProxyTest extends TestCase { + public function testFactoryNone(): void + { + $proxy = RequestProxy::none(); + + $options = extension_loaded('curl') ? [CURLOPT_PROXY => ''] : []; + self::assertSame($options, $proxy->getCurlOptions([])); + self::assertNull($proxy->getContextOptions()); + self::assertSame('', $proxy->getStatus()); + } + + public function testFactoryNoProxy(): void + { + $proxy = RequestProxy::noProxy(); + + $options = extension_loaded('curl') ? [CURLOPT_PROXY => ''] : []; + self::assertSame($options, $proxy->getCurlOptions([])); + self::assertNull($proxy->getContextOptions()); + self::assertSame('excluded by no_proxy', $proxy->getStatus()); + } + /** * @dataProvider dataSecure + * + * @param ?non-empty-string $url */ - public function testIsSecure(string $url, bool $expectedSecure): void + public function testIsSecure(?string $url, bool $expected): void { - $proxy = new RequestProxy($url, [], ''); - - $this->assertSame($expectedSecure, $proxy->isSecure()); + $proxy = new RequestProxy($url, null, null, null); + self::assertSame($expected, $proxy->isSecure()); } + /** + * @return array + */ public static function dataSecure(): array { - // url, secure + // url, expected return [ 'basic' => ['http://proxy.com:80', false], 'secure' => ['https://proxy.com:443', true], - 'none' => ['', false], + 'none' => [null, false], ]; } + public function testGetStatusThrowsOnBadFormatSpecifier(): void + { + $proxy = new RequestProxy('http://proxy.com:80', null, null, 'http://proxy.com:80'); + self::expectException('InvalidArgumentException'); + $proxy->getStatus('using proxy'); + } + /** - * @dataProvider dataProxyUrl + * @dataProvider dataStatus + * + * @param ?non-empty-string $url */ - public function testGetFormattedUrlFormat(string $url, string $format, string $expected): void + public function testGetStatus(?string $url, ?string $format, string $expected): void { - $proxy = new RequestProxy($url, [], $url); + $proxy = new RequestProxy($url, null, null, $url); - $message = $proxy->getFormattedUrl($format); - $this->assertSame($expected, $message); + if ($format === null) { + // try with and without optional param + self::assertSame($expected, $proxy->getStatus()); + self::assertSame($expected, $proxy->getStatus($format)); + } else { + self::assertSame($expected, $proxy->getStatus($format)); + } } - public static function dataProxyUrl(): array + /** + * @return array + */ + public static function dataStatus(): array { $format = 'proxy (%s)'; // url, format, expected return [ - ['', $format, ''], - ['http://proxy.com:80', $format, 'proxy (http://proxy.com:80)'], + 'no-proxy' => [null, $format, ''], + 'null-format' => ['http://proxy.com:80', null, 'http://proxy.com:80'], + 'with-format' => ['http://proxy.com:80', $format, 'proxy (http://proxy.com:80)'], + ]; + } + + /** + * This test avoids HTTPS proxies so that it can be run on PHP < 7.3 + * + * @requires extension curl + * @dataProvider dataCurlOptions + * + * @param ?non-empty-string $url + * @param ?non-empty-string $auth + * @param array $expected + */ + public function testGetCurlOptions(?string $url, ?string $auth, array $expected): void + { + $proxy = new RequestProxy($url, $auth, null, null); + self::assertSame($expected, $proxy->getCurlOptions([])); + } + + /** + * @return list}> + */ + public static function dataCurlOptions(): array + { + // url, auth, expected + return [ + [null, null, [CURLOPT_PROXY => '']], + ['http://proxy.com:80', null, + [ + CURLOPT_PROXY => 'http://proxy.com:80', + CURLOPT_NOPROXY => '', + ], + ], + ['http://proxy.com:80', 'user:p%40ss', + [ + CURLOPT_PROXY => 'http://proxy.com:80', + CURLOPT_NOPROXY => '', + CURLOPT_PROXYAUTH => CURLAUTH_BASIC, + CURLOPT_PROXYUSERPWD => 'user:p%40ss', + ], + ], + ]; + } + + /** + * @requires PHP >= 7.3.0 + * @requires extension curl >= 7.52.0 + * @dataProvider dataCurlSSLOptions + * + * @param non-empty-string $url + * @param ?non-empty-string $auth + * @param array $sslOptions + * @param array $expected + */ + public function testGetCurlOptionsWithSSL(string $url, ?string $auth, array $sslOptions, array $expected): void + { + $proxy = new RequestProxy($url, $auth, null, null); + self::assertSame($expected, $proxy->getCurlOptions($sslOptions)); + } + + /** + * @return list, 3: array}> + */ + public static function dataCurlSSLOptions(): array + { + // for PHPStan on PHP < 7.3 + $caInfo = 10246; // CURLOPT_PROXY_CAINFO + $caPath = 10247; // CURLOPT_PROXY_CAPATH + + // url, auth, sslOptions, expected + return [ + ['https://proxy.com:443', null, ['cafile' => '/certs/bundle.pem'], + [ + CURLOPT_PROXY => 'https://proxy.com:443', + CURLOPT_NOPROXY => '', + $caInfo => '/certs/bundle.pem', + ], + ], + ['https://proxy.com:443', 'user:p%40ss', ['capath' => '/certs'], + [ + CURLOPT_PROXY => 'https://proxy.com:443', + CURLOPT_NOPROXY => '', + CURLOPT_PROXYAUTH => CURLAUTH_BASIC, + CURLOPT_PROXYUSERPWD => 'user:p%40ss', + $caPath => '/certs', + ], + ], ]; } } diff --git a/tests/Composer/Test/Util/HttpDownloaderTest.php b/tests/Composer/Test/Util/HttpDownloaderTest.php index 294063902a1e..aa550b500d29 100644 --- a/tests/Composer/Test/Util/HttpDownloaderTest.php +++ b/tests/Composer/Test/Util/HttpDownloaderTest.php @@ -49,7 +49,7 @@ public function testCaptureAuthenticationParamsFromUrl(): void try { $fs->get('https://user:pass@github.com/composer/composer/404'); } catch (\Composer\Downloader\TransportException $e) { - $this->assertNotEquals(200, $e->getCode()); + self::assertNotEquals(200, $e->getCode()); } } @@ -57,7 +57,7 @@ public function testOutputWarnings(): void { $io = new BufferIO(); HttpDownloader::outputWarnings($io, '$URL', []); - $this->assertSame('', $io->getOutput()); + self::assertSame('', $io->getOutput()); HttpDownloader::outputWarnings($io, '$URL', [ 'warning' => 'old warning msg', 'warning-versions' => '>=2.0', @@ -74,7 +74,7 @@ public function testOutputWarnings(): void ]); // the tag are consumed by the OutputFormatter, but not as that is not a default output format - $this->assertSame( + self::assertSame( 'Warning from $URL: old warning msg'.PHP_EOL. 'Info from $URL: old info msg'.PHP_EOL. 'Warning from $URL: visible warning'.PHP_EOL. diff --git a/tests/Composer/Test/Util/IniHelperTest.php b/tests/Composer/Test/Util/IniHelperTest.php index fbfde3a3d46a..9750ab8c092c 100644 --- a/tests/Composer/Test/Util/IniHelperTest.php +++ b/tests/Composer/Test/Util/IniHelperTest.php @@ -33,8 +33,8 @@ public function testWithNoIni(): void ]; $this->setEnv($paths); - $this->assertStringContainsString('does not exist', IniHelper::getMessage()); - $this->assertEquals($paths, IniHelper::getAll()); + self::assertStringContainsString('does not exist', IniHelper::getMessage()); + self::assertEquals($paths, IniHelper::getAll()); } public function testWithLoadedIniOnly(): void @@ -44,7 +44,7 @@ public function testWithLoadedIniOnly(): void ]; $this->setEnv($paths); - $this->assertStringContainsString('loaded.ini', IniHelper::getMessage()); + self::assertStringContainsString('loaded.ini', IniHelper::getMessage()); } public function testWithLoadedIniAndAdditional(): void @@ -56,8 +56,8 @@ public function testWithLoadedIniAndAdditional(): void ]; $this->setEnv($paths); - $this->assertStringContainsString('multiple ini files', IniHelper::getMessage()); - $this->assertEquals($paths, IniHelper::getAll()); + self::assertStringContainsString('multiple ini files', IniHelper::getMessage()); + self::assertEquals($paths, IniHelper::getAll()); } public function testWithoutLoadedIniAndAdditional(): void @@ -69,8 +69,8 @@ public function testWithoutLoadedIniAndAdditional(): void ]; $this->setEnv($paths); - $this->assertStringContainsString('multiple ini files', IniHelper::getMessage()); - $this->assertEquals($paths, IniHelper::getAll()); + self::assertStringContainsString('multiple ini files', IniHelper::getMessage()); + self::assertEquals($paths, IniHelper::getAll()); } public static function setUpBeforeClass(): void diff --git a/tests/Composer/Test/Util/MetadataMinifierTest.php b/tests/Composer/Test/Util/MetadataMinifierTest.php index d0961d5fde50..46e91a991e06 100644 --- a/tests/Composer/Test/Util/MetadataMinifierTest.php +++ b/tests/Composer/Test/Util/MetadataMinifierTest.php @@ -39,7 +39,7 @@ public function testMinifyExpand(): void $source = [$dumper->dump($package1), $dumper->dump($package2), $dumper->dump($package3)]; - $this->assertSame($minified, MetadataMinifier::minify($source)); - $this->assertSame($source, MetadataMinifier::expand($minified)); + self::assertSame($minified, MetadataMinifier::minify($source)); + self::assertSame($source, MetadataMinifier::expand($minified)); } } diff --git a/tests/Composer/Test/Util/NoProxyPatternTest.php b/tests/Composer/Test/Util/NoProxyPatternTest.php index 91c00f3ca2fb..6e639110c8e8 100644 --- a/tests/Composer/Test/Util/NoProxyPatternTest.php +++ b/tests/Composer/Test/Util/NoProxyPatternTest.php @@ -24,7 +24,7 @@ public function testHostName(string $noproxy, string $url, bool $expected): void { $matcher = new NoProxyPattern($noproxy); $url = $this->getUrl($url); - $this->assertEquals($expected, $matcher->test($url)); + self::assertEquals($expected, $matcher->test($url)); } public static function dataHostName(): array @@ -51,7 +51,7 @@ public function testIpAddress(string $noproxy, string $url, bool $expected): voi { $matcher = new NoProxyPattern($noproxy); $url = $this->getUrl($url); - $this->assertEquals($expected, $matcher->test($url)); + self::assertEquals($expected, $matcher->test($url)); } public static function dataIpAddress(): array @@ -76,7 +76,7 @@ public function testIpRange(string $noproxy, string $url, bool $expected): void { $matcher = new NoProxyPattern($noproxy); $url = $this->getUrl($url); - $this->assertEquals($expected, $matcher->test($url)); + self::assertEquals($expected, $matcher->test($url)); } public static function dataIpRange(): array @@ -101,7 +101,7 @@ public function testPort(string $noproxy, string $url, bool $expected): void { $matcher = new NoProxyPattern($noproxy); $url = $this->getUrl($url); - $this->assertEquals($expected, $matcher->test($url)); + self::assertEquals($expected, $matcher->test($url)); } public static function dataPort(): array diff --git a/tests/Composer/Test/Util/PerforceTest.php b/tests/Composer/Test/Util/PerforceTest.php index 6fd37e559d84..99a908c57775 100644 --- a/tests/Composer/Test/Util/PerforceTest.php +++ b/tests/Composer/Test/Util/PerforceTest.php @@ -78,7 +78,7 @@ public function testGetClientWithoutStream(): void $client = $this->perforce->getClient(); $expected = 'composer_perforce_TEST_depot'; - $this->assertEquals($expected, $client); + self::assertEquals($expected, $client); } public function testGetClientFromStream(): void @@ -88,13 +88,13 @@ public function testGetClientFromStream(): void $client = $this->perforce->getClient(); $expected = 'composer_perforce_TEST_depot_branch'; - $this->assertEquals($expected, $client); + self::assertEquals($expected, $client); } public function testGetStreamWithoutStream(): void { $stream = $this->perforce->getStream(); - $this->assertEquals("//depot", $stream); + self::assertEquals("//depot", $stream); } public function testGetStreamWithStream(): void @@ -102,26 +102,26 @@ public function testGetStreamWithStream(): void $this->setPerforceToStream(); $stream = $this->perforce->getStream(); - $this->assertEquals('//depot/branch', $stream); + self::assertEquals('//depot/branch', $stream); } public function testGetStreamWithoutLabelWithStreamWithoutLabel(): void { $stream = $this->perforce->getStreamWithoutLabel('//depot/branch'); - $this->assertEquals('//depot/branch', $stream); + self::assertEquals('//depot/branch', $stream); } public function testGetStreamWithoutLabelWithStreamWithLabel(): void { $stream = $this->perforce->getStreamWithoutLabel('//depot/branching@label'); - $this->assertEquals('//depot/branching', $stream); + self::assertEquals('//depot/branching', $stream); } public function testGetClientSpec(): void { $clientSpec = $this->perforce->getP4ClientSpec(); $expected = 'path/composer_perforce_TEST_depot.p4.spec'; - $this->assertEquals($expected, $clientSpec); + self::assertEquals($expected, $clientSpec); } public function testGenerateP4Command(): void @@ -129,13 +129,13 @@ public function testGenerateP4Command(): void $command = 'do something'; $p4Command = $this->perforce->generateP4Command($command); $expected = 'p4 -u user -c composer_perforce_TEST_depot -p port do something'; - $this->assertEquals($expected, $p4Command); + self::assertEquals($expected, $p4Command); } public function testQueryP4UserWithUserAlreadySet(): void { $this->perforce->queryP4user(); - $this->assertEquals(self::TEST_P4USER, $this->perforce->getUser()); + self::assertEquals(self::TEST_P4USER, $this->perforce->getUser()); } public function testQueryP4UserWithUserSetInP4VariablesWithWindowsOS(): void @@ -148,7 +148,7 @@ public function testQueryP4UserWithUserSetInP4VariablesWithWindowsOS(): void ); $this->perforce->queryP4user(); - $this->assertEquals('TEST_P4VARIABLE_USER', $this->perforce->getUser()); + self::assertEquals('TEST_P4VARIABLE_USER', $this->perforce->getUser()); } public function testQueryP4UserWithUserSetInP4VariablesNotWindowsOS(): void @@ -162,7 +162,7 @@ public function testQueryP4UserWithUserSetInP4VariablesNotWindowsOS(): void ); $this->perforce->queryP4user(); - $this->assertEquals('TEST_P4VARIABLE_USER', $this->perforce->getUser()); + self::assertEquals('TEST_P4VARIABLE_USER', $this->perforce->getUser()); } public function testQueryP4UserQueriesForUser(): void @@ -173,7 +173,7 @@ public function testQueryP4UserQueriesForUser(): void ->with($this->equalTo($expectedQuestion)) ->willReturn('TEST_QUERY_USER'); $this->perforce->queryP4user(); - $this->assertEquals('TEST_QUERY_USER', $this->perforce->getUser()); + self::assertEquals('TEST_QUERY_USER', $this->perforce->getUser()); } public function testQueryP4UserStoresResponseToQueryForUserWithWindows(): void @@ -228,7 +228,7 @@ public function testQueryP4PasswordWithPasswordAlreadySet(): void ]; $this->perforce = new Perforce($repoConfig, 'port', 'path', $this->processExecutor, false, $this->getMockIOInterface()); $password = $this->perforce->queryP4Password(); - $this->assertEquals('TEST_PASSWORD', $password); + self::assertEquals('TEST_PASSWORD', $password); } public function testQueryP4PasswordWithPasswordSetInP4VariablesWithWindowsOS(): void @@ -241,7 +241,7 @@ public function testQueryP4PasswordWithPasswordSetInP4VariablesWithWindowsOS(): ); $password = $this->perforce->queryP4Password(); - $this->assertEquals('TEST_P4VARIABLE_PASSWORD', $password); + self::assertEquals('TEST_P4VARIABLE_PASSWORD', $password); } public function testQueryP4PasswordWithPasswordSetInP4VariablesNotWindowsOS(): void @@ -254,7 +254,7 @@ public function testQueryP4PasswordWithPasswordSetInP4VariablesNotWindowsOS(): v ); $password = $this->perforce->queryP4Password(); - $this->assertEquals('TEST_P4VARIABLE_PASSWORD', $password); + self::assertEquals('TEST_P4VARIABLE_PASSWORD', $password); } public function testQueryP4PasswordQueriesForPassword(): void @@ -266,7 +266,7 @@ public function testQueryP4PasswordQueriesForPassword(): void ->willReturn('TEST_QUERY_PASSWORD'); $password = $this->perforce->queryP4Password(); - $this->assertEquals('TEST_QUERY_PASSWORD', $password); + self::assertEquals('TEST_QUERY_PASSWORD', $password); } public function testWriteP4ClientSpecWithoutStream(): void @@ -282,9 +282,9 @@ public function testWriteP4ClientSpecWithoutStream(): void $expectedArray = $this->getExpectedClientSpec(false); try { foreach ($expectedArray as $expected) { - $this->assertStringStartsWith($expected, fgets($stream)); + self::assertStringStartsWith($expected, (string) fgets($stream)); } - $this->assertFalse(fgets($stream)); + self::assertFalse(fgets($stream)); } catch (\Exception $e) { fclose($stream); throw $e; @@ -306,9 +306,9 @@ public function testWriteP4ClientSpecWithStream(): void $expectedArray = $this->getExpectedClientSpec(true); try { foreach ($expectedArray as $expected) { - $this->assertStringStartsWith($expected, fgets($stream)); + self::assertStringStartsWith($expected, (string) fgets($stream)); } - $this->assertFalse(fgets($stream)); + self::assertFalse(fgets($stream)); } catch (\Exception $e) { fclose($stream); throw $e; @@ -328,7 +328,7 @@ public function testIsLoggedIn(): void public function testConnectClient(): void { $this->processExecutor->expects( - ['p4 -u user -c composer_perforce_TEST_depot -p port client -i < path/composer_perforce_TEST_depot.p4.spec'], + ['p4 -u user -c composer_perforce_TEST_depot -p port client -i < '.ProcessExecutor::escape('path/composer_perforce_TEST_depot.p4.spec')], true ); @@ -354,7 +354,7 @@ public function testGetBranchesWithStream(): void ); $branches = $this->perforce->getBranches(); - $this->assertEquals('//depot/branch@1234', $branches['master']); + self::assertEquals('//depot/branch@1234', $branches['master']); } public function testGetBranchesWithoutStream(): void @@ -370,7 +370,7 @@ public function testGetBranchesWithoutStream(): void ); $branches = $this->perforce->getBranches(); - $this->assertEquals('//depot@5678', $branches['master']); + self::assertEquals('//depot@5678', $branches['master']); } public function testGetTagsWithoutStream(): void @@ -386,8 +386,8 @@ public function testGetTagsWithoutStream(): void ); $tags = $this->perforce->getTags(); - $this->assertEquals('//depot@0.0.1', $tags['0.0.1']); - $this->assertEquals('//depot@0.0.2', $tags['0.0.2']); + self::assertEquals('//depot@0.0.1', $tags['0.0.1']); + self::assertEquals('//depot@0.0.2', $tags['0.0.2']); } public function testGetTagsWithStream(): void @@ -405,15 +405,15 @@ public function testGetTagsWithStream(): void ); $tags = $this->perforce->getTags(); - $this->assertEquals('//depot/branch@0.0.1', $tags['0.0.1']); - $this->assertEquals('//depot/branch@0.0.2', $tags['0.0.2']); + self::assertEquals('//depot/branch@0.0.1', $tags['0.0.1']); + self::assertEquals('//depot/branch@0.0.2', $tags['0.0.2']); } public function testCheckStreamWithoutStream(): void { $result = $this->perforce->checkStream(); - $this->assertFalse($result); - $this->assertFalse($this->perforce->isStream()); + self::assertFalse($result); + self::assertFalse($this->perforce->isStream()); } public function testCheckStreamWithStream(): void @@ -429,8 +429,8 @@ public function testCheckStreamWithStream(): void ); $result = $this->perforce->checkStream(); - $this->assertTrue($result); - $this->assertTrue($this->perforce->isStream()); + self::assertTrue($result); + self::assertTrue($this->perforce->isStream()); } public function testGetComposerInformationWithoutLabelWithoutStream(): void @@ -452,7 +452,7 @@ public function testGetComposerInformationWithoutLabelWithoutStream(): void 'minimum-stability' => 'dev', 'autoload' => ['psr-0' => []], ]; - $this->assertEquals($expected, $result); + self::assertEquals($expected, $result); } public function testGetComposerInformationWithLabelWithoutStream(): void @@ -479,7 +479,7 @@ public function testGetComposerInformationWithLabelWithoutStream(): void 'minimum-stability' => 'dev', 'autoload' => ['psr-0' => []], ]; - $this->assertEquals($expected, $result); + self::assertEquals($expected, $result); } public function testGetComposerInformationWithoutLabelWithStream(): void @@ -504,7 +504,7 @@ public function testGetComposerInformationWithoutLabelWithStream(): void 'minimum-stability' => 'dev', 'autoload' => ['psr-0' => []], ]; - $this->assertEquals($expected, $result); + self::assertEquals($expected, $result); } public function testGetComposerInformationWithLabelWithStream(): void @@ -533,7 +533,7 @@ public function testGetComposerInformationWithLabelWithStream(): void 'minimum-stability' => 'dev', 'autoload' => ['psr-0' => []], ]; - $this->assertEquals($expected, $result); + self::assertEquals($expected, $result); } public function testSyncCodeBaseWithoutStream(): void @@ -561,12 +561,14 @@ public function testSyncCodeBaseWithStream(): void public function testCheckServerExists(): void { $this->processExecutor->expects( - ['p4 -p '.ProcessExecutor::escape('perforce.does.exist:port').' info -s'], + [ + ['p4', '-p', 'perforce.does.exist:port', 'info', '-s'] + ], true ); $result = $this->perforce->checkServerExists('perforce.does.exist:port', $this->processExecutor); - $this->assertTrue($result); + self::assertTrue($result); } /** @@ -578,14 +580,14 @@ public function testCheckServerClientError(): void { $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $expectedCommand = 'p4 -p '.ProcessExecutor::escape('perforce.does.exist:port').' info -s'; + $expectedCommand = ['p4', '-p', 'perforce.does.exist:port', 'info', '-s']; $processExecutor->expects($this->once()) ->method('execute') ->with($this->equalTo($expectedCommand), $this->equalTo(null)) ->willReturn(127); $result = $this->perforce->checkServerExists('perforce.does.exist:port', $processExecutor); - $this->assertFalse($result); + self::assertFalse($result); } public static function getComposerJson(): string diff --git a/tests/Composer/Test/Util/PlatformTest.php b/tests/Composer/Test/Util/PlatformTest.php index b6be7b8974eb..bbaa4ec82a9e 100644 --- a/tests/Composer/Test/Util/PlatformTest.php +++ b/tests/Composer/Test/Util/PlatformTest.php @@ -25,15 +25,15 @@ class PlatformTest extends TestCase public function testExpandPath(): void { putenv('TESTENV=/home/test'); - $this->assertEquals('/home/test/myPath', Platform::expandPath('%TESTENV%/myPath')); - $this->assertEquals('/home/test/myPath', Platform::expandPath('$TESTENV/myPath')); - $this->assertEquals((getenv('HOME') ?: getenv('USERPROFILE')) . '/test', Platform::expandPath('~/test')); + self::assertEquals('/home/test/myPath', Platform::expandPath('%TESTENV%/myPath')); + self::assertEquals('/home/test/myPath', Platform::expandPath('$TESTENV/myPath')); + self::assertEquals((getenv('HOME') ?: getenv('USERPROFILE')) . '/test', Platform::expandPath('~/test')); } public function testIsWindows(): void { // Compare 2 common tests for Windows to the built-in Windows test - $this->assertEquals(('\\' === DIRECTORY_SEPARATOR), Platform::isWindows()); - $this->assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), Platform::isWindows()); + self::assertEquals(('\\' === DIRECTORY_SEPARATOR), Platform::isWindows()); + self::assertEquals(defined('PHP_WINDOWS_VERSION_MAJOR'), Platform::isWindows()); } } diff --git a/tests/Composer/Test/Util/ProcessExecutorTest.php b/tests/Composer/Test/Util/ProcessExecutorTest.php index a8455c7ab3af..006f2321fd06 100644 --- a/tests/Composer/Test/Util/ProcessExecutorTest.php +++ b/tests/Composer/Test/Util/ProcessExecutorTest.php @@ -29,7 +29,7 @@ public function testExecuteCapturesOutput(): void { $process = new ProcessExecutor; $process->execute('echo foo', $output); - $this->assertEquals("foo".PHP_EOL, $output); + self::assertEquals("foo".PHP_EOL, $output); } public function testExecuteOutputsIfNotCaptured(): void @@ -38,7 +38,7 @@ public function testExecuteOutputsIfNotCaptured(): void ob_start(); $process->execute('echo foo'); $output = ob_get_clean(); - $this->assertEquals("foo".PHP_EOL, $output); + self::assertEquals("foo".PHP_EOL, $output); } public function testUseIOIsNotNullAndIfNotCaptured(): void @@ -56,14 +56,14 @@ public function testExecuteCapturesStderr(): void { $process = new ProcessExecutor; $process->execute('cat foo', $output); - $this->assertNotNull($process->getErrorOutput()); + self::assertStringContainsString('foo: No such file or directory', $process->getErrorOutput()); } public function testTimeout(): void { ProcessExecutor::setTimeout(1); $process = new ProcessExecutor; - $this->assertEquals(1, $process->getTimeout()); + self::assertEquals(1, $process->getTimeout()); ProcessExecutor::setTimeout(60); } @@ -74,7 +74,7 @@ public function testHidePasswords(string $command, string $expectedCommandOutput { $process = new ProcessExecutor($buffer = new BufferIO('', StreamOutput::VERBOSITY_DEBUG)); $process->execute($command, $output); - $this->assertEquals('Executing command (CWD): ' . $expectedCommandOutput, trim($buffer->getOutput())); + self::assertEquals('Executing command (CWD): ' . $expectedCommandOutput, trim($buffer->getOutput())); } public static function hidePasswordProvider(): array @@ -92,18 +92,18 @@ public function testDoesntHidePorts(): void { $process = new ProcessExecutor($buffer = new BufferIO('', StreamOutput::VERBOSITY_DEBUG)); $process->execute('echo https://localhost:1234/', $output); - $this->assertEquals('Executing command (CWD): echo https://localhost:1234/', trim($buffer->getOutput())); + self::assertEquals('Executing command (CWD): echo https://localhost:1234/', trim($buffer->getOutput())); } public function testSplitLines(): void { $process = new ProcessExecutor; - $this->assertEquals([], $process->splitLines('')); - $this->assertEquals([], $process->splitLines(null)); - $this->assertEquals(['foo'], $process->splitLines('foo')); - $this->assertEquals(['foo', 'bar'], $process->splitLines("foo\nbar")); - $this->assertEquals(['foo', 'bar'], $process->splitLines("foo\r\nbar")); - $this->assertEquals(['foo', 'bar'], $process->splitLines("foo\r\nbar\n")); + self::assertEquals([], $process->splitLines('')); + self::assertEquals([], $process->splitLines(null)); + self::assertEquals(['foo'], $process->splitLines('foo')); + self::assertEquals(['foo', 'bar'], $process->splitLines("foo\nbar")); + self::assertEquals(['foo', 'bar'], $process->splitLines("foo\r\nbar")); + self::assertEquals(['foo', 'bar'], $process->splitLines("foo\r\nbar\n")); } public function testConsoleIODoesNotFormatSymfonyConsoleStyle(): void @@ -112,7 +112,7 @@ public function testConsoleIODoesNotFormatSymfonyConsoleStyle(): void $process = new ProcessExecutor(new ConsoleIO(new ArrayInput([]), $output, new HelperSet([]))); $process->execute('php -ddisplay_errors=0 -derror_reporting=0 -r "echo \'foo\'.PHP_EOL;"'); - $this->assertSame('foo'.PHP_EOL, $output->fetch()); + self::assertSame('foo'.PHP_EOL, $output->fetch()); } public function testExecuteAsyncCancel(): void @@ -121,12 +121,12 @@ public function testExecuteAsyncCancel(): void $process->enableAsync(); $start = microtime(true); $promise = $process->executeAsync('sleep 2'); - $this->assertEquals(1, $process->countActiveJobs()); + self::assertEquals(1, $process->countActiveJobs()); $promise->cancel(); - $this->assertEquals(0, $process->countActiveJobs()); + self::assertEquals(0, $process->countActiveJobs()); $process->wait(); $end = microtime(true); - $this->assertTrue($end - $start < 2, 'Canceling took longer than it should, lasted '.($end - $start)); + self::assertTrue($end - $start < 2, 'Canceling took longer than it should, lasted '.($end - $start)); } /** @@ -139,7 +139,7 @@ public function testExecuteAsyncCancel(): void public function testEscapeArgument($argument, string $win, string $unix): void { $expected = defined('PHP_WINDOWS_VERSION_BUILD') ? $win : $unix; - $this->assertSame($expected, ProcessExecutor::escape($argument)); + self::assertSame($expected, ProcessExecutor::escape($argument)); } /** diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index d673cd2b01c8..048ccb0f8358 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -34,7 +34,7 @@ public function testGetOptionsForUrl(): void ; $res = $this->callGetOptionsForUrl($io, ['http://example.org', []]); - $this->assertTrue(isset($res['http']['header']) && is_array($res['http']['header']), 'getOptions must return an array with headers'); + self::assertTrue(isset($res['http']['header']) && is_array($res['http']['header']), 'getOptions must return an array with headers'); } public function testGetOptionsForUrlWithAuthorization(): void @@ -59,7 +59,7 @@ public function testGetOptionsForUrlWithAuthorization(): void $found = true; } } - $this->assertTrue($found, 'getOptions must have an Authorization header'); + self::assertTrue($found, 'getOptions must have an Authorization header'); } public function testGetOptionsForUrlWithStreamOptions(): void @@ -82,7 +82,7 @@ public function testGetOptionsForUrlWithStreamOptions(): void ]]; $res = $this->callGetOptionsForUrl($io, ['https://example.org', []], $streamOptions); - $this->assertTrue( + self::assertTrue( isset($res['ssl'], $res['ssl']['allow_self_signed']) && true === $res['ssl']['allow_self_signed'], 'getOptions must return an array with a allow_self_signed set to true' ); @@ -108,7 +108,7 @@ public function testGetOptionsForUrlWithCallOptionsKeepsHeader(): void ]]; $res = $this->callGetOptionsForUrl($io, ['https://example.org', $streamOptions]); - $this->assertTrue(isset($res['http']['header']), 'getOptions must return an array with a http.header key'); + self::assertTrue(isset($res['http']['header']), 'getOptions must return an array with a http.header key'); $found = false; foreach ($res['http']['header'] as $header) { @@ -117,15 +117,15 @@ public function testGetOptionsForUrlWithCallOptionsKeepsHeader(): void } } - $this->assertTrue($found, 'getOptions must have a Foo: bar header'); - $this->assertGreaterThan(1, count($res['http']['header'])); + self::assertTrue($found, 'getOptions must have a Foo: bar header'); + self::assertGreaterThan(1, count($res['http']['header'])); } public function testCallbackGetFileSize(): void { $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); $this->callCallbackGet($fs, STREAM_NOTIFY_FILE_SIZE_IS, 0, '', 0, 0, 20); - $this->assertAttributeEqualsCustom(20, 'bytesMax', $fs); + self::assertAttributeEqualsCustom(20, 'bytesMax', $fs); } public function testCallbackGetNotifyProgress(): void @@ -141,22 +141,24 @@ public function testCallbackGetNotifyProgress(): void $this->setAttribute($fs, 'progress', true); $this->callCallbackGet($fs, STREAM_NOTIFY_PROGRESS, 0, '', 0, 10, 20); - $this->assertAttributeEqualsCustom(50, 'lastProgress', $fs); + self::assertAttributeEqualsCustom(50, 'lastProgress', $fs); } + /** + * @doesNotPerformAssertions + */ public function testCallbackGetPassesThrough404(): void { $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); $this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0); - $this->assertTrue(true, 'callbackGet must pass through 404'); } public function testGetContents(): void { $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); - $this->assertStringContainsString('testGetContents', $fs->getContents('http://example.org', 'file://'.__FILE__)); + self::assertStringContainsString('testGetContents', (string) $fs->getContents('http://example.org', 'file://'.__FILE__)); } public function testCopy(): void @@ -164,9 +166,9 @@ public function testCopy(): void $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); $file = $this->createTempFile(); - $this->assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file)); - $this->assertFileExists($file); - $this->assertStringContainsString('testCopy', file_get_contents($file)); + self::assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file)); + self::assertFileExists($file); + self::assertStringContainsString('testCopy', (string) file_get_contents($file)); unlink($file); } @@ -231,9 +233,9 @@ public function testCopyWithSuccessOnRetry(): void ['retry-auth-failure' => true] ); - $this->assertTrue($copyResult); - $this->assertFileExists($file); - $this->assertStringContainsString('Copied', file_get_contents($file)); + self::assertTrue($copyResult); + self::assertFileExists($file); + self::assertStringContainsString('Copied', (string) file_get_contents($file)); unlink($file); } @@ -247,16 +249,16 @@ public function testGetOptionsForUrlCreatesSecureTlsDefaults(): void $res = $this->callGetOptionsForUrl($io, ['example.org', ['ssl' => ['cafile' => '/some/path/file.crt']]], [], 'http://www.example.org'); - $this->assertTrue(isset($res['ssl']['ciphers'])); - $this->assertMatchesRegularExpression('|!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA|', $res['ssl']['ciphers']); - $this->assertTrue($res['ssl']['verify_peer']); - $this->assertTrue($res['ssl']['SNI_enabled']); - $this->assertEquals(7, $res['ssl']['verify_depth']); - $this->assertEquals('/some/path/file.crt', $res['ssl']['cafile']); + self::assertTrue(isset($res['ssl']['ciphers'])); + self::assertMatchesRegularExpression('|!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA|', $res['ssl']['ciphers']); + self::assertTrue($res['ssl']['verify_peer']); + self::assertTrue($res['ssl']['SNI_enabled']); + self::assertEquals(7, $res['ssl']['verify_depth']); + self::assertEquals('/some/path/file.crt', $res['ssl']['cafile']); if (version_compare(PHP_VERSION, '5.4.13') >= 0) { - $this->assertTrue($res['ssl']['disable_compression']); + self::assertTrue($res['ssl']['disable_compression']); } else { - $this->assertFalse(isset($res['ssl']['disable_compression'])); + self::assertFalse(isset($res['ssl']['disable_compression'])); } } @@ -291,7 +293,7 @@ public function testBitBucketPublicDownload(string $url, string $contents): void $result = $rfs->getContents($hostname, $url, false); - $this->assertEquals($contents, $result); + self::assertEquals($contents, $result); } /** @@ -331,8 +333,8 @@ public function testBitBucketPublicDownloadWithAuthConfigured(string $url, strin $result = $rfs->getContents($hostname, $url, false); - $this->assertEquals($contents, $result); - $this->assertEquals(['bitbucket.org', 'bbuseruploads.s3.amazonaws.com'], $domains); + self::assertEquals($contents, $result); + self::assertEquals(['bitbucket.org', 'bbuseruploads.s3.amazonaws.com'], $domains); } /** @@ -399,7 +401,7 @@ private function assertAttributeEqualsCustom($value, string $attribute, $object) { $attr = new ReflectionProperty($object, $attribute); $attr->setAccessible(true); - $this->assertSame($value, $attr->getValue($object)); + self::assertSame($value, $attr->getValue($object)); } /** diff --git a/tests/Composer/Test/Util/SilencerTest.php b/tests/Composer/Test/Util/SilencerTest.php index 9161dae60123..270b9b1118e1 100644 --- a/tests/Composer/Test/Util/SilencerTest.php +++ b/tests/Composer/Test/Util/SilencerTest.php @@ -40,10 +40,10 @@ public function testSilencer(): void return $a * $b * $c; }, 2, 3, 4); - $this->assertEquals(24, $result); + self::assertEquals(24, $result); // Check the error reporting setting was restored correctly - $this->assertEquals($before, error_reporting()); + self::assertEquals($before, error_reporting()); } /** diff --git a/tests/Composer/Test/Util/StreamContextFactoryTest.php b/tests/Composer/Test/Util/StreamContextFactoryTest.php index 4d875122d4da..bb89cefd597d 100644 --- a/tests/Composer/Test/Util/StreamContextFactoryTest.php +++ b/tests/Composer/Test/Util/StreamContextFactoryTest.php @@ -45,8 +45,8 @@ public function testGetContext(array $expectedOptions, array $defaultOptions, ar $options = stream_context_get_options($context); $params = stream_context_get_params($context); - $this->assertEquals($expectedOptions, $options); - $this->assertEquals($expectedParams, $params); + self::assertEquals($expectedOptions, $options); + self::assertEquals($expectedParams, $params); } public static function dataGetContext(): array @@ -72,7 +72,7 @@ public function testHttpProxy(): void $context = StreamContextFactory::getContext('http://example.org', ['http' => ['method' => 'GET', 'header' => 'User-Agent: foo']]); $options = stream_context_get_options($context); - $this->assertEquals(['http' => [ + self::assertEquals(['http' => [ 'proxy' => 'tcp://proxyserver.net:3128', 'request_fulluri' => true, 'method' => 'GET', @@ -90,7 +90,7 @@ public function testHttpProxyWithNoProxy(): void $context = StreamContextFactory::getContext('http://example.org', ['http' => ['method' => 'GET', 'header' => 'User-Agent: foo']]); $options = stream_context_get_options($context); - $this->assertEquals(['http' => [ + self::assertEquals(['http' => [ 'method' => 'GET', 'max_redirects' => 20, 'follow_location' => 1, @@ -106,7 +106,7 @@ public function testHttpProxyWithNoProxyWildcard(): void $context = StreamContextFactory::getContext('http://example.org', ['http' => ['method' => 'GET', 'header' => 'User-Agent: foo']]); $options = stream_context_get_options($context); - $this->assertEquals(['http' => [ + self::assertEquals(['http' => [ 'method' => 'GET', 'max_redirects' => 20, 'follow_location' => 1, @@ -121,7 +121,7 @@ public function testOptionsArePreserved(): void $context = StreamContextFactory::getContext('http://example.org', ['http' => ['method' => 'GET', 'header' => ['User-Agent: foo', "X-Foo: bar"], 'request_fulluri' => false]]); $options = stream_context_get_options($context); - $this->assertEquals(['http' => [ + self::assertEquals(['http' => [ 'proxy' => 'tcp://proxyserver.net:3128', 'request_fulluri' => false, 'method' => 'GET', @@ -133,12 +133,12 @@ public function testOptionsArePreserved(): void public function testHttpProxyWithoutPort(): void { - $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net'; + $_SERVER['https_proxy'] = 'http://username:password@proxyserver.net'; $context = StreamContextFactory::getContext('https://example.org', ['http' => ['method' => 'GET', 'header' => 'User-Agent: foo']]); $options = stream_context_get_options($context); - $this->assertEquals(['http' => [ + self::assertEquals(['http' => [ 'proxy' => 'tcp://proxyserver.net:80', 'method' => 'GET', 'header' => ['User-Agent: foo', "Proxy-Authorization: Basic " . base64_encode('username:password')], @@ -172,7 +172,7 @@ public function testSSLProxy(string $expected, string $proxy): void $context = StreamContextFactory::getContext('http://example.org', ['http' => ['header' => 'User-Agent: foo']]); $options = stream_context_get_options($context); - $this->assertEquals(['http' => [ + self::assertEquals(['http' => [ 'proxy' => $expected, 'request_fulluri' => true, 'max_redirects' => 20, @@ -184,7 +184,7 @@ public function testSSLProxy(string $expected, string $proxy): void StreamContextFactory::getContext('http://example.org'); $this->fail(); } catch (\RuntimeException $e) { - $this->assertInstanceOf('Composer\Downloader\TransportException', $e); + self::assertInstanceOf('Composer\Downloader\TransportException', $e); } } } @@ -216,18 +216,18 @@ public function testEnsureThatfixHttpHeaderFieldMovesContentTypeToEndOfOptions() ]; $context = StreamContextFactory::getContext('http://example.org', $options); $ctxoptions = stream_context_get_options($context); - $this->assertEquals(end($expectedOptions['http']['header']), end($ctxoptions['http']['header'])); + self::assertEquals(end($expectedOptions['http']['header']), end($ctxoptions['http']['header'])); } public function testInitOptionsDoesIncludeProxyAuthHeaders(): void { - $_SERVER['http_proxy'] = 'http://username:password@proxyserver.net:3128/'; + $_SERVER['https_proxy'] = 'http://username:password@proxyserver.net:3128/'; $options = []; $options = StreamContextFactory::initOptions('https://example.org', $options); $headers = implode(' ', $options['http']['header']); - $this->assertTrue(false !== stripos($headers, 'Proxy-Authorization')); + self::assertTrue(false !== stripos($headers, 'Proxy-Authorization')); } public function testInitOptionsForCurlDoesNotIncludeProxyAuthHeaders(): void @@ -242,6 +242,6 @@ public function testInitOptionsForCurlDoesNotIncludeProxyAuthHeaders(): void $options = StreamContextFactory::initOptions('https://example.org', $options, true); $headers = implode(' ', $options['http']['header']); - $this->assertFalse(stripos($headers, 'Proxy-Authorization')); + self::assertFalse(stripos($headers, 'Proxy-Authorization')); } } diff --git a/tests/Composer/Test/Util/SvnTest.php b/tests/Composer/Test/Util/SvnTest.php index cf9f5f0adf0f..b17f722d3e92 100644 --- a/tests/Composer/Test/Util/SvnTest.php +++ b/tests/Composer/Test/Util/SvnTest.php @@ -23,25 +23,25 @@ class SvnTest extends TestCase * Test the credential string. * * @param string $url The SVN url. - * @param string $expect The expectation for the test. + * @param non-empty-list $expect The expectation for the test. * * @dataProvider urlProvider */ - public function testCredentials(string $url, string $expect): void + public function testCredentials(string $url, array $expect): void { $svn = new Svn($url, new NullIO, new Config()); - $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs'); $reflMethod->setAccessible(true); - $this->assertEquals($expect, $reflMethod->invoke($svn)); + self::assertEquals($expect, $reflMethod->invoke($svn)); } public static function urlProvider(): array { return [ - ['http://till:test@svn.example.org/', self::getCmd(" --username 'till' --password 'test' ")], - ['http://svn.apache.org/', ''], - ['svn://johndoe@example.org', self::getCmd(" --username 'johndoe' --password '' ")], + ['http://till:test@svn.example.org/', ['--username', 'till', '--password', 'test']], + ['http://svn.apache.org/', []], + ['svn://johndoe@example.org', ['--username', 'johndoe', '--password', '']], ]; } @@ -53,9 +53,9 @@ public function testInteractiveString(): void $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCommand'); $reflMethod->setAccessible(true); - $this->assertEquals( - self::getCmd("svn ls --non-interactive -- 'http://svn.example.org'"), - $reflMethod->invokeArgs($svn, ['svn ls', $url]) + self::assertEquals( + ['svn', 'ls', '--non-interactive', '--', 'http://svn.example.org'], + $reflMethod->invokeArgs($svn, [['svn', 'ls'], $url]) ); } @@ -73,10 +73,10 @@ public function testCredentialsFromConfig(): void ]); $svn = new Svn($url, new NullIO, $config); - $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs'); $reflMethod->setAccessible(true); - $this->assertEquals(self::getCmd(" --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); + self::assertEquals(['--username', 'foo', '--password', 'bar'], $reflMethod->invoke($svn)); } public function testCredentialsFromConfigWithCacheCredentialsTrue(): void @@ -96,10 +96,10 @@ public function testCredentialsFromConfigWithCacheCredentialsTrue(): void $svn = new Svn($url, new NullIO, $config); $svn->setCacheCredentials(true); - $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs'); $reflMethod->setAccessible(true); - $this->assertEquals(self::getCmd(" --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); + self::assertEquals(['--username', 'foo', '--password', 'bar'], $reflMethod->invoke($svn)); } public function testCredentialsFromConfigWithCacheCredentialsFalse(): void @@ -119,9 +119,9 @@ public function testCredentialsFromConfigWithCacheCredentialsFalse(): void $svn = new Svn($url, new NullIO, $config); $svn->setCacheCredentials(false); - $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialString'); + $reflMethod = new \ReflectionMethod('Composer\\Util\\Svn', 'getCredentialArgs'); $reflMethod->setAccessible(true); - $this->assertEquals(self::getCmd(" --no-auth-cache --username 'foo' --password 'bar' "), $reflMethod->invoke($svn)); + self::assertEquals(['--no-auth-cache', '--username', 'foo', '--password', 'bar'], $reflMethod->invoke($svn)); } } diff --git a/tests/Composer/Test/Util/TarTest.php b/tests/Composer/Test/Util/TarTest.php index 86c7564cd321..cba1d501b684 100644 --- a/tests/Composer/Test/Util/TarTest.php +++ b/tests/Composer/Test/Util/TarTest.php @@ -24,13 +24,13 @@ public function testReturnsNullifTheTarIsNotFound(): void { $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/invalid.zip'); - $this->assertNull($result); + self::assertNull($result); } public function testReturnsNullIfTheTarIsEmpty(): void { $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/empty.tar.gz'); - $this->assertNull($result); + self::assertNull($result); } public function testThrowsExceptionIfTheTarHasNoComposerJson(): void @@ -48,13 +48,13 @@ public function testThrowsExceptionIfTheComposerJsonIsInASubSubfolder(): void public function testReturnsComposerJsonInTarRoot(): void { $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/root.tar.gz'); - $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + self::assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); } public function testReturnsComposerJsonInFirstFolder(): void { $result = Tar::getComposerJson(__DIR__.'/Fixtures/Tar/folder.tar.gz'); - $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + self::assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); } public function testMultipleTopLevelDirsIsInvalid(): void diff --git a/tests/Composer/Test/Util/TlsHelperTest.php b/tests/Composer/Test/Util/TlsHelperTest.php index bda7f53ce1c8..64c0c4cc5e00 100644 --- a/tests/Composer/Test/Util/TlsHelperTest.php +++ b/tests/Composer/Test/Util/TlsHelperTest.php @@ -27,15 +27,15 @@ public function testCheckCertificateHost(bool $expectedResult, string $hostname, $certificate['subject']['commonName'] = $expectedCn = array_shift($certNames); $certificate['extensions']['subjectAltName'] = $certNames ? 'DNS:'.implode(',DNS:', $certNames) : ''; - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.deprecatedClass $result = TlsHelper::checkCertificateHost($certificate, $hostname, $foundCn); if (true === $expectedResult) { - $this->assertTrue($result); - $this->assertSame($expectedCn, $foundCn); + self::assertTrue($result); + self::assertSame($expectedCn, $foundCn); } else { - $this->assertFalse($result); - $this->assertNull($foundCn); + self::assertFalse($result); + self::assertNull($foundCn); } } @@ -70,12 +70,12 @@ public function testGetCertificateNames(): void $certificate['subject']['commonName'] = 'example.net'; $certificate['extensions']['subjectAltName'] = 'DNS: example.com, IP: 127.0.0.1, DNS: getcomposer.org, Junk: blah, DNS: composer.example.org'; - // @phpstan-ignore-next-line + // @phpstan-ignore staticMethod.deprecatedClass $names = TlsHelper::getCertificateNames($certificate); self::assertIsArray($names); - $this->assertSame('example.net', $names['cn']); - $this->assertSame([ + self::assertSame('example.net', $names['cn']); + self::assertSame([ 'example.com', 'getcomposer.org', 'composer.example.org', diff --git a/tests/Composer/Test/Util/UrlTest.php b/tests/Composer/Test/Util/UrlTest.php index 267b1c1bd7ad..04167904f5de 100644 --- a/tests/Composer/Test/Util/UrlTest.php +++ b/tests/Composer/Test/Util/UrlTest.php @@ -29,7 +29,7 @@ public function testUpdateDistReference(string $url, string $expectedUrl, array $config = new Config(); $config->merge(['config' => $conf]); - $this->assertSame($expectedUrl, Url::updateDistReference($config, $url, $ref)); + self::assertSame($expectedUrl, Url::updateDistReference($config, $url, $ref)); } public static function distRefsProvider(): array @@ -67,7 +67,7 @@ public static function distRefsProvider(): array */ public function testSanitize(string $expected, string $url): void { - $this->assertSame($expected, Url::sanitize($url)); + self::assertSame($expected, Url::sanitize($url)); } public static function sanitizeProvider(): array diff --git a/tests/Composer/Test/Util/ZipTest.php b/tests/Composer/Test/Util/ZipTest.php index 0af7ea89acd4..b1981de8679d 100644 --- a/tests/Composer/Test/Util/ZipTest.php +++ b/tests/Composer/Test/Util/ZipTest.php @@ -40,7 +40,7 @@ public function testReturnsNullifTheZipIsNotFound(): void $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/invalid.zip'); - $this->assertNull($result); + self::assertNull($result); } public function testReturnsNullIfTheZipIsEmpty(): void @@ -51,7 +51,7 @@ public function testReturnsNullIfTheZipIsEmpty(): void $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/empty.zip'); - $this->assertNull($result); + self::assertNull($result); } public function testThrowsExceptionIfTheZipHasNoComposerJson(): void @@ -86,7 +86,7 @@ public function testReturnsComposerJsonInZipRoot(): void $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/root.zip'); - $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + self::assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); } public function testReturnsComposerJsonInFirstFolder(): void @@ -96,7 +96,7 @@ public function testReturnsComposerJsonInFirstFolder(): void } $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/folder.zip'); - $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + self::assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); } public function testMultipleTopLevelDirsIsInvalid(): void @@ -119,6 +119,6 @@ public function testReturnsComposerJsonFromFirstSubfolder(): void $result = Zip::getComposerJson(__DIR__.'/Fixtures/Zip/single-sub.zip'); - $this->assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); + self::assertEquals("{\n \"name\": \"foo/bar\"\n}\n", $result); } }