diff --git a/.gitattributes b/.gitattributes index e28d16514..bd516c568 100644 --- a/.gitattributes +++ b/.gitattributes @@ -24,3 +24,6 @@ /PORTING_INFO export-ignore /README.md export-ignore /UPGRADE-2.0.md export-ignore + +# Ref https://github.com/composer/composer/issues/11507 +/phpstan/rules.neon -export-ignore diff --git a/.gitignore b/.gitignore index 3db374fdf..fce10c34f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ phpunit.xml .vagrant Vagrantfile .idea +.vscode .php-cs-fixer.cache diff --git a/README.md b/README.md index 6a7d02282..09a1ab73e 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,24 @@ PHP versions 5.3.2 - 8.1 are still supported via the LTS releases of Composer (2 run the installer or the `self-update` command the appropriate Composer version for your PHP should be automatically selected. +#### Binary dependencies + +- `7z` (or `7zz`) +- `unzip` (if `7z` is missing) +- `gzip` +- `tar` +- `unrar` +- `xz` +- Git (`git`) +- Mercurial (`hg`) +- Fossil (`fossil`) +- Perforce (`p4`) +- Subversion (`svn`) + +It's important to note that the need for these binary dependencies may vary +depending on individual use cases. However, for most users, only 2 dependencies +are essential for Composer: `7z` (or `7zz` or `unzip`), and `git`. + Authors ------- diff --git a/composer.json b/composer.json index 2bd532acc..36dbfe22d 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "composer/ca-bundle": "^1.0", "composer/class-map-generator": "^1.0", "composer/metadata-minifier": "^1.0", - "composer/semver": "^3.0", + "composer/semver": "^3.2.5", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", "justinrainbow/json-schema": "^5.2.11", diff --git a/composer.lock b/composer.lock index 760cdabff..39b48033b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cc535e8c9fc8f1414a1cede463898453", + "content-hash": "c50c89580fa044b7523cb55c2d557c87", "packages": [ { "name": "composer/ca-bundle", @@ -84,22 +84,22 @@ }, { "name": "composer/class-map-generator", - "version": "1.0.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "1e1cb2b791facb2dfe32932a7718cf2571187513" + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/1e1cb2b791facb2dfe32932a7718cf2571187513", - "reference": "1e1cb2b791facb2dfe32932a7718cf2571187513", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/953cc4ea32e0c31f2185549c7d216d7921f03da9", + "reference": "953cc4ea32e0c31f2185549c7d216d7921f03da9", "shasum": "" }, "require": { - "composer/pcre": "^2 || ^3", + "composer/pcre": "^2.1 || ^3.1", "php": "^7.2 || ^8.0", - "symfony/finder": "^4.4 || ^5.3 || ^6" + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" }, "require-dev": { "phpstan/phpstan": "^1.6", @@ -137,7 +137,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.0.0" + "source": "https://github.com/composer/class-map-generator/tree/1.1.0" }, "funding": [ { @@ -153,7 +153,7 @@ "type": "tidelift" } ], - "time": "2022-06-19T11:31:27+00:00" + "time": "2023-06-30T13:58:57+00:00" }, { "name": "composer/metadata-minifier", @@ -1103,16 +1103,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.23", + "version": "v5.4.25", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b2f79d86cd9e7de0fff6d03baa80eaed7a5f38b5" + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b2f79d86cd9e7de0fff6d03baa80eaed7a5f38b5", - "reference": "b2f79d86cd9e7de0fff6d03baa80eaed7a5f38b5", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", + "reference": "0ce3a62c9579a53358d3a7eb6b3dfb79789a6364", "shasum": "" }, "require": { @@ -1147,7 +1147,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.23" + "source": "https://github.com/symfony/filesystem/tree/v5.4.25" }, "funding": [ { @@ -1163,7 +1163,7 @@ "type": "tidelift" } ], - "time": "2023-03-02T11:38:35+00:00" + "time": "2023-05-31T13:04:02+00:00" }, { "name": "symfony/finder", @@ -2034,16 +2034,16 @@ "packages-dev": [ { "name": "phpstan/phpstan", - "version": "1.10.16", + "version": "1.10.25", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "352bdbb960bb523e3d71b834862589f910921c23" + "reference": "578f4e70d117f9a90699324c555922800ac38d8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/352bdbb960bb523e3d71b834862589f910921c23", - "reference": "352bdbb960bb523e3d71b834862589f910921c23", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/578f4e70d117f9a90699324c555922800ac38d8c", + "reference": "578f4e70d117f9a90699324c555922800ac38d8c", "shasum": "" }, "require": { @@ -2092,7 +2092,7 @@ "type": "tidelift" } ], - "time": "2023-06-05T08:21:46+00:00" + "time": "2023-07-06T12:11:37+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -2316,16 +2316,16 @@ }, { "name": "symfony/phpunit-bridge", - "version": "v6.3.0", + "version": "v6.3.1", "source": { "type": "git", "url": "https://github.com/symfony/phpunit-bridge.git", - "reference": "f8d75b4d9bf7243979b2c2e5e6cd73f03e10579f" + "reference": "0b0bf59b0d9bd1422145a123a67fb12af546ef0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/f8d75b4d9bf7243979b2c2e5e6cd73f03e10579f", - "reference": "f8d75b4d9bf7243979b2c2e5e6cd73f03e10579f", + "url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/0b0bf59b0d9bd1422145a123a67fb12af546ef0d", + "reference": "0b0bf59b0d9bd1422145a123a67fb12af546ef0d", "shasum": "" }, "require": { @@ -2377,7 +2377,7 @@ "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.0" + "source": "https://github.com/symfony/phpunit-bridge/tree/v6.3.1" }, "funding": [ { @@ -2393,7 +2393,7 @@ "type": "tidelift" } ], - "time": "2023-05-30T09:01:24+00:00" + "time": "2023-06-23T13:25:16+00:00" } ], "aliases": [], diff --git a/doc/00-intro.md b/doc/00-intro.md index ad43c94f6..7c08bd380 100644 --- a/doc/00-intro.md +++ b/doc/00-intro.md @@ -39,8 +39,14 @@ a legacy PHP version. A few sensitive php settings and compile flags are also required, but when using the installer you will be warned about any incompatibilities. -To install packages from sources instead of plain zip archives, you will need -git, svn, fossil or hg depending on how the package is version-controlled. +Composer needs several supporting applications to work effectively, making the +process of handling package dependencies more efficient. For decompressing +files, Composer relies on tools like `7z` (or `7zz`), `gzip`, `tar`, `unrar`, +`unzip` and `xz`. As for version control systems, Composer integrates seamlessly +with Fossil, Git, Mercurial, Perforce and Subversion, thereby ensuring the +application's smooth operation and management of library repositories. Before +using Composer, ensure that these dependencies are correctly installed on your +system. Composer is multi-platform and we strive to make it run equally well on Windows, Linux and macOS. diff --git a/doc/04-schema.md b/doc/04-schema.md index 8cdb3117a..de22c551a 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -35,7 +35,7 @@ separated by `/`. Examples: * igorw/event-source The name must be lowercase and consist of words separated by `-`, `.` or `_`. -The complete name should match `^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]?|-{0,2})[a-z0-9]+)*$`. +The complete name should match `^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9](([_.]|-{1,2})?[a-z0-9]+)*$`. The `name` property is required for published packages (libraries). diff --git a/doc/articles/plugins.md b/doc/articles/plugins.md index 818011887..019ab5d45 100644 --- a/doc/articles/plugins.md +++ b/doc/articles/plugins.md @@ -284,6 +284,10 @@ Now the `custom-plugin-command` is available alongside Composer commands. Plugins for an event can be run manually by the `run-script` command. This works the same way as [running scripts manually](scripts.md#running-scripts-manually). +If it is another type of plugin the best way to test it is probably using a [path repository](../05-repositories.md#path) +to require the plugin in a test project, and then `rm -rf vendor && composer update` +every time you want to install/run it again. + ## Using Plugins Plugin packages are automatically loaded as soon as they are installed and will diff --git a/phpstan/baseline-8.1.neon b/phpstan/baseline-8.1.neon index 29e52ed7c..26137b6ca 100644 --- a/phpstan/baseline-8.1.neon +++ b/phpstan/baseline-8.1.neon @@ -85,11 +85,6 @@ parameters: count: 1 path: ../src/Composer/Downloader/GzipDownloader.php - - - message: "#^Parameter \\#1 \\$string of function rawurldecode expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - message: "#^Parameter \\#3 \\$length of function substr expects int\\|null, int\\<0, max\\>\\|false given\\.$#" count: 1 diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index 3597f9fee..19f0d228f 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -645,26 +645,11 @@ parameters: count: 1 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Cannot call method getId\\(\\) on Composer\\\\Package\\\\BasePackage\\|int\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Cannot call method getInstallationManager\\(\\) on Composer\\\\Composer\\|null\\.$#" count: 2 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Cannot call method getPrettyVersion\\(\\) on Composer\\\\Package\\\\BasePackage\\|int\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - - - message: "#^Cannot call method getVersion\\(\\) on Composer\\\\Package\\\\BasePackage\\|int\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Foreach overwrites \\$packages with its value variable\\.$#" count: 1 @@ -675,11 +660,6 @@ parameters: count: 1 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Method Composer\\\\Command\\\\ShowCommand\\:\\:getPackage\\(\\) should return array\\{Composer\\\\Package\\\\CompletePackageInterface\\|null, array\\\\} but returns array\\{Composer\\\\Package\\\\BasePackage\\|int\\|null, array\\\\}\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Only booleans are allowed in &&, Composer\\\\Composer\\|null given on the right side\\.$#" count: 1 @@ -720,11 +700,6 @@ parameters: count: 2 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Package\\\\BasePackage\\|int\\|null given\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Only booleans are allowed in a negated boolean, Composer\\\\Repository\\\\RepositorySet\\|null given\\.$#" count: 1 @@ -805,11 +780,6 @@ parameters: count: 2 path: ../src/Composer/Command/ShowCommand.php - - - message: "#^Parameter \\#1 \\$package of method Composer\\\\Repository\\\\CompositeRepository\\:\\:hasPackage\\(\\) expects Composer\\\\Package\\\\PackageInterface, Composer\\\\Package\\\\BasePackage\\|int given\\.$#" - count: 1 - path: ../src/Composer/Command/ShowCommand.php - - message: "#^Parameter \\#1 \\$str of function strtok expects string, array\\|string given\\.$#" count: 1 @@ -1930,36 +1900,11 @@ parameters: count: 1 path: ../src/Composer/Downloader/VcsDownloader.php - - - message: "#^Parameter \\#1 \\$haystack of function strpos expects string, string\\|false given\\.$#" - count: 2 - path: ../src/Composer/Downloader/VcsDownloader.php - - message: "#^Parameter \\#1 \\$package of method Composer\\\\Downloader\\\\VcsDownloader\\:\\:cleanChanges\\(\\) expects Composer\\\\Package\\\\PackageInterface, Composer\\\\Package\\\\PackageInterface\\|null given\\.$#" count: 1 path: ../src/Composer/Downloader/VcsDownloader.php - - - message: "#^Parameter \\#1 \\$path of function realpath expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - - - message: "#^Parameter \\#1 \\$path of static method Composer\\\\Util\\\\Filesystem\\:\\:isLocalPath\\(\\) expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - - - message: "#^Parameter \\#1 \\$str of function rawurldecode expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - - - message: "#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#" - count: 1 - path: ../src/Composer/Downloader/VcsDownloader.php - - message: "#^Parameter \\#2 \\$toReference of method Composer\\\\Downloader\\\\VcsDownloader\\:\\:getCommitLogs\\(\\) expects string, string\\|null given\\.$#" count: 1 @@ -2500,11 +2445,6 @@ parameters: count: 1 path: ../src/Composer/Installer/SuggestedPackagesReporter.php - - - message: "#^Method Composer\\\\Json\\\\JsonFile\\:\\:encode\\(\\) should return string but returns string\\|false\\.$#" - count: 1 - path: ../src/Composer/Json/JsonFile.php - - message: "#^Only booleans are allowed in &&, Composer\\\\IO\\\\IOInterface\\|null given on the left side\\.$#" count: 1 diff --git a/src/Composer/Autoload/PhpFileCleaner.php b/src/Composer/Autoload/PhpFileCleaner.php deleted file mode 100644 index 0c00f878b..000000000 --- a/src/Composer/Autoload/PhpFileCleaner.php +++ /dev/null @@ -1,247 +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\Autoload; - -use Composer\Pcre\Preg; - -/** - * @author Jordi Boggiano - * @internal - */ -class PhpFileCleaner -{ - /** @var array */ - private static $typeConfig; - - /** @var non-empty-string */ - private static $restPattern; - - /** - * @readonly - * @var string - */ - private $contents; - - /** - * @readonly - * @var int - */ - private $len; - - /** - * @readonly - * @var int - */ - private $maxMatches; - - /** @var int */ - private $index = 0; - - /** - * @param string[] $types - */ - public static function setTypeConfig(array $types): void - { - foreach ($types as $type) { - self::$typeConfig[$type[0]] = [ - 'name' => $type, - 'length' => \strlen($type), - 'pattern' => '{.\b(?])'.$type.'\s++[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+}Ais', - ]; - } - - self::$restPattern = '{[^?"\'contents = $contents; - $this->len = \strlen($this->contents); - $this->maxMatches = $maxMatches; - } - - public function clean(): string - { - $clean = ''; - - while ($this->index < $this->len) { - $this->skipToPhp(); - $clean .= 'index < $this->len) { - $char = $this->contents[$this->index]; - if ($char === '?' && $this->peek('>')) { - $clean .= '?>'; - $this->index += 2; - continue 2; - } - - if ($char === '"') { - $this->skipString('"'); - $clean .= 'null'; - continue; - } - - if ($char === "'") { - $this->skipString("'"); - $clean .= 'null'; - continue; - } - - if ($char === "<" && $this->peek('<') && $this->match('{<<<[ \t]*+([\'"]?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*+)\\1(?:\r\n|\n|\r)}A', $match)) { - $this->index += \strlen($match[0]); - $this->skipHeredoc($match[2]); - $clean .= 'null'; - continue; - } - - if ($char === '/') { - if ($this->peek('/')) { - $this->skipToNewline(); - continue; - } - if ($this->peek('*')) { - $this->skipComment(); - continue; - } - } - - if ($this->maxMatches === 1 && isset(self::$typeConfig[$char])) { - $type = self::$typeConfig[$char]; - if ( - \substr($this->contents, $this->index, $type['length']) === $type['name'] - && Preg::isMatch($type['pattern'], $this->contents, $match, 0, $this->index - 1) - ) { - $clean .= $match[0]; - - return $clean; - } - } - - $this->index += 1; - if ($this->match(self::$restPattern, $match)) { - $clean .= $char . $match[0]; - $this->index += \strlen($match[0]); - } else { - $clean .= $char; - } - } - } - - return $clean; - } - - private function skipToPhp(): void - { - while ($this->index < $this->len) { - if ($this->contents[$this->index] === '<' && $this->peek('?')) { - $this->index += 2; - break; - } - - $this->index += 1; - } - } - - private function skipString(string $delimiter): void - { - $this->index += 1; - while ($this->index < $this->len) { - if ($this->contents[$this->index] === '\\' && ($this->peek('\\') || $this->peek($delimiter))) { - $this->index += 2; - continue; - } - if ($this->contents[$this->index] === $delimiter) { - $this->index += 1; - break; - } - $this->index += 1; - } - } - - private function skipComment(): void - { - $this->index += 2; - while ($this->index < $this->len) { - if ($this->contents[$this->index] === '*' && $this->peek('/')) { - $this->index += 2; - break; - } - - $this->index += 1; - } - } - - private function skipToNewline(): void - { - while ($this->index < $this->len) { - if ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n") { - return; - } - $this->index += 1; - } - } - - private function skipHeredoc(string $delimiter): void - { - $firstDelimiterChar = $delimiter[0]; - $delimiterLength = \strlen($delimiter); - $delimiterPattern = '{'.preg_quote($delimiter).'(?![a-zA-Z0-9_\x80-\xff])}A'; - - while ($this->index < $this->len) { - // check if we find the delimiter after some spaces/tabs - switch ($this->contents[$this->index]) { - case "\t": - case " ": - $this->index += 1; - continue 2; - case $firstDelimiterChar: - if ( - \substr($this->contents, $this->index, $delimiterLength) === $delimiter - && $this->match($delimiterPattern) - ) { - $this->index += $delimiterLength; - - return; - } - break; - } - - // skip the rest of the line - while ($this->index < $this->len) { - $this->skipToNewline(); - - // skip newlines - while ($this->index < $this->len && ($this->contents[$this->index] === "\r" || $this->contents[$this->index] === "\n")) { - $this->index += 1; - } - - break; - } - } - } - - private function peek(string $char): bool - { - return $this->index + 1 < $this->len && $this->contents[$this->index + 1] === $char; - } - - /** - * @param non-empty-string $regex - * @param null|array $match - */ - private function match($regex, ?array &$match = null): bool - { - return Preg::isMatch($regex, $this->contents, $match, 0, $this->index); - } -} diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 82662093a..d3b690eca 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -24,10 +24,12 @@ use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryFactory; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; +use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Package\Version\VersionParser; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Composer\Util\PackageInfo; /** * Base implementation for commands mapping dependency relationships. @@ -180,7 +182,9 @@ abstract class BaseDependencyCommand extends BaseCommand } $doubles[$unique] = true; $version = $package->getPrettyVersion() === RootPackage::DEFAULT_PRETTY_VERSION ? '-' : $package->getPrettyVersion(); - $rows[] = [$package->getPrettyName(), $version, $link->getDescription(), sprintf('%s (%s)', $link->getTarget(), $link->getPrettyConstraint())]; + $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) { $queue = array_merge($queue, $children); } @@ -229,7 +233,9 @@ abstract class BaseDependencyCommand extends BaseCommand $prevColor = $this->colors[($level - 1) % count($this->colors)]; $isLast = (++$idx === $count); $versionText = $package->getPrettyVersion() === RootPackage::DEFAULT_PRETTY_VERSION ? '' : $package->getPrettyVersion(); - $packageText = rtrim(sprintf('<%s>%s %s', $color, $package->getPrettyName(), $versionText)); + $packageUrl = PackageInfo::getViewSourceOrHomepageUrl($package); + $nameWithLink = $packageUrl !== null ? '' . $package->getPrettyName() . '' : $package->getPrettyName(); + $packageText = rtrim(sprintf('<%s>%s %s', $color, $nameWithLink, $versionText)); $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))); diff --git a/src/Composer/Command/PackageDiscoveryTrait.php b/src/Composer/Command/PackageDiscoveryTrait.php index 1e31abaa3..d95e06de8 100644 --- a/src/Composer/Command/PackageDiscoveryTrait.php +++ b/src/Composer/Command/PackageDiscoveryTrait.php @@ -15,6 +15,7 @@ namespace Composer\Command; use Composer\Factory; use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; +use Composer\IO\IOInterface; use Composer\Package\CompletePackageInterface; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; @@ -100,7 +101,7 @@ trait PackageDiscoveryTrait if (!isset($requirement['version'])) { // determine the best version automatically - [$name, $version] = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $fixed); + [$name, $version] = $this->findBestVersionAndNameForPackage($this->getIO(), $input, $requirement['name'], $platformRepo, $preferredStability, $fixed); // replace package name from packagist.org $requirement['name'] = $name; @@ -243,7 +244,7 @@ trait PackageDiscoveryTrait ); if (false === $constraint) { - [, $constraint] = $this->findBestVersionAndNameForPackage($input, $package, $platformRepo, $preferredStability); + [, $constraint] = $this->findBestVersionAndNameForPackage($this->getIO(), $input, $package, $platformRepo, $preferredStability); $io->writeError(sprintf( 'Using version %s for %s', @@ -273,7 +274,7 @@ trait PackageDiscoveryTrait * @throws \InvalidArgumentException * @return array{string, string} name version */ - private function findBestVersionAndNameForPackage(InputInterface $input, string $name, ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', bool $fixed = false): array + private function findBestVersionAndNameForPackage(IOInterface $io, InputInterface $input, string $name, ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', bool $fixed = false): array { // handle ignore-platform-reqs flag if present if ($input->hasOption('ignore-platform-reqs') && $input->hasOption('ignore-platform-req')) { @@ -358,6 +359,13 @@ trait PackageDiscoveryTrait )); } + if ($input->isInteractive()) { + $result = $io->select("Could not find package $name.\nPick one of these or leave empty to abort:", $similar, false, 1); + if ($result !== false) { + return $this->findBestVersionAndNameForPackage($io, $input, $similar[$result], $platformRepo, $preferredStability, $fixed); + } + } + throw new \InvalidArgumentException(sprintf( "Could not find package %s.\n\nDid you mean " . (count($similar) > 1 ? 'one of these' : 'this') . "?\n %s", $name, diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index d7bd2c75a..603a26e9c 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -756,10 +756,14 @@ EOT } // select preferred package according to policy rules - if (!$matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { + if (null === $matchedPackage && $matches && $preferred = $policy->selectPreferredPackages($pool, $matches)) { $matchedPackage = $pool->literalToPackage($preferred[0]); } + if ($matchedPackage !== null && !$matchedPackage instanceof CompletePackageInterface) { + throw new \LogicException('ShowCommand::getPackage can only work with CompletePackageInterface, but got '.get_class($matchedPackage)); + } + return [$matchedPackage, $versions]; } @@ -1251,6 +1255,9 @@ EOT $colorIdent = $level % count($this->colors); $color = $this->colors[$colorIdent]; + assert(is_string($require['name'])); + assert(is_string($require['version'])); + $circularWarn = in_array( $require['name'], $currentTree, diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 5dfeb18c8..21b76ec3a 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -557,6 +557,19 @@ class Problem */ protected static function constraintToText(?ConstraintInterface $constraint = null): string { + if ($constraint instanceof Constraint && $constraint->getOperator() === Constraint::STR_OP_EQ && !str_starts_with($constraint->getVersion(), 'dev-')) { + if (!Preg::isMatch('{^\d+(?:\.\d+)*$}', $constraint->getPrettyString())) { + return ' '.$constraint->getPrettyString() .' (exact version match)'; + } + + $versions = [$constraint->getPrettyString()]; + for ($i = 3 - substr_count($versions[0], '.'); $i > 0; $i--) { + $versions[] = end($versions) . '.0'; + } + + return ' ' . $constraint->getPrettyString() . ' (exact version match: ' . (count($versions) > 1 ? implode(', ', array_slice($versions, 0, -1)) . ' or ' . end($versions) : $versions[0]) . ')'; + } + return $constraint ? ' '.$constraint->getPrettyString() : ''; } } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index fa6dfa73b..0c989a82d 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -436,7 +436,12 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ protected function getFileName(PackageInterface $package, string $path): string { - return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . md5($package . spl_object_hash($package)) . '.' . $this->getDistPath($package, PATHINFO_EXTENSION), '.'); + $extension = $this->getDistPath($package, PATHINFO_EXTENSION); + if ($extension === '') { + $extension = $package->getDistType(); + } + + return rtrim($this->config->get('vendor-dir') . '/composer/tmp-' . md5($package . spl_object_hash($package)) . '.' . $extension, '.'); } /** diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index dc871ff8b..b4f08afdd 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -42,12 +42,16 @@ class JsonFile public const COMPOSER_SCHEMA_PATH = __DIR__ . '/../../../res/composer-schema.json'; + public const INDENT_DEFAULT = ' '; + /** @var string */ private $path; /** @var ?HttpDownloader */ private $httpDownloader; /** @var ?IOInterface */ private $io; + /** @var string */ + private $indent = self::INDENT_DEFAULT; /** * Initializes json file reader/parser. @@ -117,6 +121,8 @@ class JsonFile throw new \RuntimeException('Could not read '.$this->path); } + $this->indent = self::detectIndenting($json); + return static::parseJson($json, $this->path); } @@ -131,7 +137,7 @@ class JsonFile public function write(array $hash, int $options = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) { if ($this->path === 'php://memory') { - file_put_contents($this->path, static::encode($hash, $options)); + file_put_contents($this->path, static::encode($hash, $options, $this->indent)); return; } @@ -153,7 +159,7 @@ class JsonFile $retries = 3; while ($retries--) { try { - $this->filePutContentsIfModified($this->path, static::encode($hash, $options). ($options & JSON_PRETTY_PRINT ? "\n" : '')); + $this->filePutContentsIfModified($this->path, static::encode($hash, $options, $this->indent). ($options & JSON_PRETTY_PRINT ? "\n" : '')); break; } catch (\Exception $e) { if ($retries > 0) { @@ -262,15 +268,28 @@ class JsonFile * * @param mixed $data Data to encode into a formatted JSON string * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + * @param string $indent Indentation string * @return string Encoded json */ - public static function encode($data, int $options = 448) + public static function encode($data, int $options = 448, string $indent = self::INDENT_DEFAULT): string { $json = json_encode($data, $options); + if (false === $json) { self::throwEncodeError(json_last_error()); } + if (($options & JSON_PRETTY_PRINT) > 0 && $indent !== self::INDENT_DEFAULT ) { + // Pretty printing and not using default indentation + return Preg::replaceCallback( + '#^ {4,}#m', + static function ($match) use ($indent): string { + return str_repeat($indent, (int)(strlen($match[0] ?? '') / 4)); + }, + $json + ); + } + return $json; } @@ -279,6 +298,7 @@ class JsonFile * * @param int $code return code of json_last_error function * @throws \RuntimeException + * @return never */ private static function throwEncodeError(int $code): void { @@ -364,4 +384,12 @@ class JsonFile $result->getDetails()); } } + + public static function detectIndenting(?string $json): string + { + if (Preg::isMatchStrictGroups('#^([ \t]+)"#m', $json ?? '', $match)) { + return $match[1]; + } + return self::INDENT_DEFAULT; + } } diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index d6caffeba..8d6759671 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -561,10 +561,6 @@ class JsonManipulator protected function detectIndenting(): void { - if (Preg::isMatchStrictGroups('{^([ \t]+)"}m', $this->contents, $match)) { - $this->indent = $match[1]; - } else { - $this->indent = ' '; - } + $this->indent = JsonFile::detectIndenting($this->contents); } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index ea87e5c4b..0efb02e96 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -139,8 +139,13 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito { parent::__construct(); if (!Preg::isMatch('{^[\w.]+\??://}', $repoConfig['url'])) { - // assume http as the default protocol - $repoConfig['url'] = 'http://'.$repoConfig['url']; + if (($localFilePath = realpath($repoConfig['url'])) !== false) { + // it is a local path, add file scheme + $repoConfig['url'] = 'file://'.$localFilePath; + } else { + // otherwise, assume http as the default protocol + $repoConfig['url'] = 'http://'.$repoConfig['url']; + } } $repoConfig['url'] = rtrim($repoConfig['url'], '/'); if ($repoConfig['url'] === '') { diff --git a/src/Composer/Util/PackageInfo.php b/src/Composer/Util/PackageInfo.php index 0b2607ac9..e93c58447 100644 --- a/src/Composer/Util/PackageInfo.php +++ b/src/Composer/Util/PackageInfo.php @@ -19,7 +19,7 @@ class PackageInfo { public static function getViewSourceUrl(PackageInterface $package): ?string { - if ($package instanceof CompletePackageInterface && isset($package->getSupport()['source'])) { + if ($package instanceof CompletePackageInterface && isset($package->getSupport()['source']) && '' !== $package->getSupport()['source']) { return $package->getSupport()['source']; } @@ -28,6 +28,12 @@ class PackageInfo public static function getViewSourceOrHomepageUrl(PackageInterface $package): ?string { - return self::getViewSourceUrl($package) ?? ($package instanceof CompletePackageInterface ? $package->getHomepage() : null); + $url = self::getViewSourceUrl($package) ?? ($package instanceof CompletePackageInterface ? $package->getHomepage() : null); + + if ($url === '') { + return null; + } + + return $url; } } diff --git a/tests/Composer/Test/Command/ReinstallCommandTest.php b/tests/Composer/Test/Command/ReinstallCommandTest.php new file mode 100644 index 000000000..c25c00202 --- /dev/null +++ b/tests/Composer/Test/Command/ReinstallCommandTest.php @@ -0,0 +1,69 @@ + + * 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 Generator; + +class ReinstallCommandTest extends TestCase +{ + /** + * @dataProvider caseProvider + * @param array $packages + * @param string $expected + */ + public function testReinstallCommand(array $packages, string $expected): void + { + $this->initTempComposer([ + 'require' => [ + 'root/req' => '1.*', + 'root/anotherreq' => '2.*' + ] + ]); + + $rootReqPackage = self::getPackage('root/req'); + $anotherReqPackage = self::getPackage('root/anotherreq'); + $rootReqPackage->setType('metapackage'); + $anotherReqPackage->setType('metapackage'); + + $this->createComposerLock([$rootReqPackage], [$anotherReqPackage]); + $this->createInstalledJson([$rootReqPackage], [$anotherReqPackage]); + + $appTester = $this->getApplicationTester(); + $appTester->run([ + 'command' => 'reinstall', + '--no-progress' => true, + '--no-plugins' => true, + 'packages' => $packages + ]); + + $this->assertSame($expected, trim($appTester->getDisplay(true))); + } + + public function caseProvider(): Generator + { + yield 'reinstall a package' => [ + ['root/req', 'root/anotherreq'], +'- Removing root/req (1.0.0) + - Removing root/anotherreq (1.0.0) + - Installing root/anotherreq (1.0.0) + - Installing root/req (1.0.0)' + ]; + + yield 'reinstall a package that is not installed' => [ + ['root/unknownreq'], + 'Pattern "root/unknownreq" does not match any currently installed packages. +Found no packages to reinstall, aborting.' + ]; + } +} diff --git a/tests/Composer/Test/Fixtures/installer/alias-on-unloadable-package.test b/tests/Composer/Test/Fixtures/installer/alias-on-unloadable-package.test index 58cd7d818..0f9f2c5ef 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-on-unloadable-package.test +++ b/tests/Composer/Test/Fixtures/installer/alias-on-unloadable-package.test @@ -24,7 +24,6 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - Root composer.json requires a/aliased 3.0.2 as 3.0.3, found a/aliased[1.2.3] but it does not match the constraint. + - Root composer.json requires a/aliased 3.0.2 as 3.0.3 (exact version match), found a/aliased[1.2.3] but it does not match the constraint. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test b/tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test index 2c698a2a6..2cf503056 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test @@ -59,7 +59,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 3 - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. Problem 4 - - Root composer.json requires PHP extension ext-foobar 1.0.0 but it is missing from your system. Install or enable PHP's foobar extension. + - Root composer.json requires PHP extension ext-foobar 1.0.0 (exact version match: 1.0.0 or 1.0.0.0) but it is missing from your system. Install or enable PHP's foobar extension. Problem 5 - Root composer.json requires PHP extension ext-pcre ^8 but the ext-pcre package is disabled by your platform config. Enable it again with "composer config platform.ext-pcre --unset". Problem 6 diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 6b9c03630..610911bb9 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -113,7 +113,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 3 - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. Problem 4 - - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command. + - Root composer.json requires stable-requiree-excluded/pkg 1.0.1 (exact version match: 1.0.1 or 1.0.1.0), found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you list it as an argument for the update command. Problem 5 - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. Problem 6 @@ -121,7 +121,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 7 - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version installed (%s). Problem 8 - - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement. + - Root composer.json requires php 1 (exact version match: 1, 1.0, 1.0.0 or 1.0.0.0) but your php version (%s) does not satisfy that requirement. Problem 9 - Root composer.json requires package/found 2.* -> satisfiable by package/found[2.0.0]. - package/found 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. diff --git a/tests/Composer/Test/Json/Fixtures/tabs.json b/tests/Composer/Test/Json/Fixtures/tabs.json new file mode 100644 index 000000000..460b5331d --- /dev/null +++ b/tests/Composer/Test/Json/Fixtures/tabs.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/tests/Composer/Test/Json/JsonFileTest.php b/tests/Composer/Test/Json/JsonFileTest.php index 848a18def..dab7531fd 100644 --- a/tests/Composer/Test/Json/JsonFileTest.php +++ b/tests/Composer/Test/Json/JsonFileTest.php @@ -436,6 +436,29 @@ class JsonFileTest extends TestCase JsonFile::parseJson($data); } + public function testPreserveIndentationAfterRead(): void + { + copy(__DIR__.'/Fixtures/tabs.json', __DIR__.'/Fixtures/tabs2.json'); + $jsonFile = new JsonFile(__DIR__.'/Fixtures/tabs2.json'); + $data = $jsonFile->read(); + $jsonFile->write(['foo' => 'baz']); + + self::assertSame("{\n\t\"foo\": \"baz\"\n}\n", file_get_contents(__DIR__.'/Fixtures/tabs2.json')); + + unlink(__DIR__.'/Fixtures/tabs2.json'); + } + + public function testOverwritesIndentationByDefault(): void + { + copy(__DIR__.'/Fixtures/tabs.json', __DIR__.'/Fixtures/tabs2.json'); + $jsonFile = new JsonFile(__DIR__.'/Fixtures/tabs2.json'); + $jsonFile->write(['foo' => 'baz']); + + self::assertSame("{\n \"foo\": \"baz\"\n}\n", file_get_contents(__DIR__.'/Fixtures/tabs2.json')); + + unlink(__DIR__.'/Fixtures/tabs2.json'); + } + private function expectParseException(string $text, string $json): void { try {