From 0cd02f62575b167b01ab33a610e788a6f8b11353 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Mar 2022 09:26:31 +0100 Subject: [PATCH 1/5] added phpstan ConfigReturnTypeExtension --- phpstan/config.neon | 6 +++ .../PHPStan/ConfigReturnTypeExtension.php | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/Composer/PHPStan/ConfigReturnTypeExtension.php diff --git a/phpstan/config.neon b/phpstan/config.neon index 5ac2cd37b..f09a2e4b7 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -58,3 +58,9 @@ parameters: - Composer\Composer::VERSION - Composer\Composer::RELEASE_DATE - Composer\Composer::SOURCE_VERSION + +services: + - + class: Composer\PHPStan\ConfigReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/src/Composer/PHPStan/ConfigReturnTypeExtension.php b/src/Composer/PHPStan/ConfigReturnTypeExtension.php new file mode 100644 index 000000000..73f1764bc --- /dev/null +++ b/src/Composer/PHPStan/ConfigReturnTypeExtension.php @@ -0,0 +1,50 @@ +getName()) === 'get'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $args = $methodCall->getArgs(); + + if (count($args) < 1) { + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } + + $keyType = $scope->getType($args[0]->value); + if ($keyType instanceof ConstantStringType) { + if ($keyType->getValue() == 'allow-plugins') { + return TypeCombinator::addNull( + new ArrayType(new StringType(), new BooleanType()) + ); + } + } + + return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); + } +} From 532166a892aeb2a09fdd94bbf8259b8e98eeaff4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Mar 2022 09:37:11 +0100 Subject: [PATCH 2/5] ws --- phpstan/config.neon | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/phpstan/config.neon b/phpstan/config.neon index f09a2e4b7..500e97ed4 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -60,7 +60,7 @@ parameters: - Composer\Composer::SOURCE_VERSION services: - - - class: Composer\PHPStan\ConfigReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: Composer\PHPStan\ConfigReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension From 6fdd9494efa2d5d638db1be6cb669668ced4fa4f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 28 Apr 2022 15:13:08 +0200 Subject: [PATCH 3/5] Implement config type parsing --- res/composer-schema.json | 2 +- .../PHPStan/ConfigReturnTypeExtension.php | 131 +++++++++++++++++- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/res/composer-schema.json b/res/composer-schema.json index 166a80820..3276cc025 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -326,7 +326,7 @@ }, "github-protocols": { "type": "array", - "description": "A list of protocols to use for github.com clones, in priority order, defaults to [\"git\", \"https\", \"http\"].", + "description": "A list of protocols to use for github.com clones, in priority order, defaults to [\"https\", \"ssh\", \"git\"].", "items": { "type": "string" } diff --git a/src/Composer/PHPStan/ConfigReturnTypeExtension.php b/src/Composer/PHPStan/ConfigReturnTypeExtension.php index 73f1764bc..1b99218ac 100644 --- a/src/Composer/PHPStan/ConfigReturnTypeExtension.php +++ b/src/Composer/PHPStan/ConfigReturnTypeExtension.php @@ -3,20 +3,39 @@ namespace Composer\PHPStan; use Composer\Config; +use Composer\Json\JsonFile; use PhpParser\Node\Expr\MethodCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; +use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\UnionType; -final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtension { +final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtension +{ + /** @var array */ + private $properties = []; + + public function __construct() + { + $schema = JsonFile::parseJson(file_get_contents(__DIR__.'/../../../res/composer-schema.json')); + foreach ($schema['properties']['config']['properties'] as $prop => $conf) { + $type = $this->parseType($conf, $prop); + + $this->properties[$prop] = $type; + } + } public function getClass(): string { @@ -38,13 +57,115 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio $keyType = $scope->getType($args[0]->value); if ($keyType instanceof ConstantStringType) { - if ($keyType->getValue() == 'allow-plugins') { - return TypeCombinator::addNull( - new ArrayType(new StringType(), new BooleanType()) - ); + if (isset($this->properties[$keyType->getValue()])) { + return $this->properties[$keyType->getValue()]; } } return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); } + + /** + * @param array $types + */ + private function parseType(array $def, string $path): Type + { + if (isset($def['type'])) { + $types = []; + foreach ((array) $def['type'] as $type) { + switch ($type) { + case 'integer': + if (in_array($path, ['process-timeout', 'cache-ttl', 'cache-files-ttl'], true)) { + $types[] = IntegerRangeType::createAllGreaterThan(0); + } else { + $types[] = new IntegerType(); + } + break; + + case 'string': + if ($path === 'discard-changes') { + $types[] = new ConstantStringType('stash'); + } elseif ($path === 'use-parent-dir') { + $types[] = new ConstantStringType('prompt'); + } elseif ($path === 'store-auths') { + $types[] = new ConstantStringType('prompt'); + } elseif ($path === 'platform-check') { + $types[] = new ConstantStringType('php-only'); + } elseif ($path === 'github-protocols') { + $types[] = new UnionType([new ConstantStringType('git'), new ConstantStringType('https'), new ConstantStringType('ssh'), new ConstantStringType('http')]); + } elseif (str_starts_with($path, 'preferred-install')) { + $types[] = new UnionType([new ConstantStringType('source'), new ConstantStringType('dist'), new ConstantStringType('auto')]); + } else { + $types[] = new StringType(); + } + break; + + case 'boolean': + $types[] = new BooleanType(); + break; + + case 'object': + $addlPropType = null; + if (isset($def['additionalProperties'])) { + $addlPropType = $this->parseType($def['additionalProperties'], $path.'.additionalProperties'); + } + + if (isset($def['properties'])) { + $keyNames = []; + $valTypes = []; + foreach ($def['properties'] as $propName => $propdef) { + $keyNames[] = new ConstantStringType($propName); + $valType = $this->parseType($propdef, $path.'.'.$propName); + if (!isset($def['required']) || !in_array($propName, $def['required'])) { + $valType = TypeCombinator::addNull($valType); + } + $valTypes[] = $valType; + } + + if ($addlPropType !== null) { + $types[] = new ArrayType(TypeCombinator::union(new StringType(), ...$keyNames), TypeCombinator::union($addlPropType, ...$valTypes)); + } else { + $types[] = new ConstantArrayType($keyNames, $valTypes); + } + } else { + $types[] = new ArrayType(new StringType(), $addlPropType ?? new MixedType()); + } + break; + + case 'array': + if (isset($def['items'])) { + $valType = $this->parseType($def['items'], $path.'.items'); + } else { + $valType = new MixedType(); + } + + $types[] = new ArrayType(new IntegerType(), $valType); + break; + + default: + $types[] = new MixedType(); + } + } + } elseif (isset($def['enum'])) { + $types[] = TypeCombinator::union(...array_map(function (string $value): ConstantStringType { + return new ConstantStringType($value); + }, $def['enum'])); + } else { + $types = [new MixedType()]; + } + + $type = \count($types) === 1 ? $types[0] : TypeCombinator::union(...$types); + + // allow-plugins defaults to null until July 1st 2022 for some BC hackery, but after that it is not nullable anymore + if ($path === 'allow-plugins' && time() < strtotime('2022-07-01')) { + $type = TypeCombinator::addNull($type); + } + + // default null props + if (in_array($path, ['autoloader-suffix', 'gitlab-protocol'], true)) { + $type = TypeCombinator::addNull($type); + } + + return $type; + } } From bd6403a6bef36fdbefdb287dc7e26286d5e06e5e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 28 Apr 2022 21:20:40 +0200 Subject: [PATCH 4/5] Fix newly surfaced errors --- res/composer-schema.json | 25 +++++- src/Composer/Autoload/AutoloadGenerator.php | 6 +- src/Composer/Command/DiagnoseCommand.php | 2 +- src/Composer/Command/RemoveCommand.php | 2 +- src/Composer/Config.php | 14 ++-- src/Composer/Downloader/FileDownloader.php | 2 +- src/Composer/Factory.php | 6 +- src/Composer/IO/BaseIO.php | 6 +- .../PHPStan/ConfigReturnTypeExtension.php | 32 +++++--- src/Composer/Package/Locker.php | 24 +++--- src/Composer/Repository/Vcs/FossilDriver.php | 2 +- src/Composer/Repository/Vcs/GitDriver.php | 2 +- src/Composer/Repository/Vcs/GitLabDriver.php | 4 +- src/Composer/Repository/Vcs/HgDriver.php | 2 +- .../Repository/Vcs/PerforceDriver.php | 2 +- src/Composer/Util/Git.php | 3 - src/Composer/Util/GitLab.php | 4 +- src/Composer/Util/Url.php | 5 +- .../Test/Autoload/AutoloadGeneratorTest.php | 3 + .../Test/Repository/PathRepositoryTest.php | 78 +++++++------------ 20 files changed, 115 insertions(+), 109 deletions(-) diff --git a/res/composer-schema.json b/res/composer-schema.json index 3276cc025..2c410b262 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -289,7 +289,7 @@ "properties": { "platform": { "type": "object", - "description": "This is an object of package name (keys) and version (values) that will be used to mock the platform packages on this machine.", + "description": "This is an object of package name (keys) and version (values) that will be used to mock the platform packages on this machine, the version can be set to false to make it appear like the package is not present.", "additionalProperties": { "type": ["string", "boolean"] } @@ -347,9 +347,20 @@ }, "gitlab-token": { "type": "object", - "description": "An object of domain name => gitlab private tokens, typically {\"gitlab.com\":\"\"}.", + "description": "An object of domain name => gitlab private tokens, typically {\"gitlab.com\":\"\"}, or an object with username and token keys.", "additionalProperties": { - "type": "string" + "type": ["string", "object"], + "required": ["username", "token"], + "properties": { + "username": { + "type": "string", + "description": "The username used for GitLab authentication" + }, + "token": { + "type": "string", + "description": "The token used for GitLab authentication" + } + } } }, "gitlab-protocol": { @@ -512,6 +523,14 @@ "consumer-secret": { "type": "string", "description": "The consumer-secret used for OAuth authentication" + }, + "access-token": { + "type": "string", + "description": "The OAuth token retrieved from Bitbucket's API, this is written by Composer and you should not set it nor modify it." + }, + "access-token-expiration": { + "type": "integer", + "description": "The generated token's expiration timestamp, this is written by Composer and you should not set it nor modify it." } } } diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 3496e1dc4..554b1cdcd 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -202,7 +202,7 @@ class AutoloadGenerator // See https://bugs.php.net/bug.php?id=72738 $basePath = $filesystem->normalizePath(realpath(realpath(Platform::getCwd()))); $vendorPath = $filesystem->normalizePath(realpath(realpath($config->get('vendor-dir')))); - $useGlobalIncludePath = (bool) $config->get('use-include-path'); + $useGlobalIncludePath = $config->get('use-include-path'); $prependAutoloader = $config->get('prepend-autoloader') === false ? 'false' : 'true'; $targetDir = $vendorPath.'/'.$targetDir; $filesystem->ensureDirectoryExists($targetDir); @@ -408,7 +408,7 @@ EOF; unlink($includeFilesFilePath); } $filesystem->filePutContentsIfModified($targetDir.'/autoload_static.php', $this->getStaticFile($suffix, $targetDir, $vendorPath, $basePath)); - $checkPlatform = $config->get('platform-check') && !($this->platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter); + $checkPlatform = $config->get('platform-check') !== false && !($this->platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter); $platformCheckContent = null; if ($checkPlatform) { $platformCheckContent = $this->getPlatformCheck($packageMap, $config->get('platform-check'), $devPackageNames); @@ -429,7 +429,7 @@ EOF; if ($this->runScripts) { $this->eventDispatcher->dispatchScript(ScriptEvents::POST_AUTOLOAD_DUMP, $this->devMode, array(), array( - 'optimize' => (bool) $scanPsrPackages, + 'optimize' => $scanPsrPackages, )); } diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index cb49c2d24..ce5488291 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -114,7 +114,7 @@ EOT $this->outputResult($this->checkHttpProxy()); } - if ($oauth = $config->get('github-oauth')) { + if (count($oauth = $config->get('github-oauth')) > 0) { foreach ($oauth as $domain => $token) { $io->write('Checking '.$domain.' oauth access: ', false); $this->outputResult($this->checkGithubOauth($domain, $token)); diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 89e7bc6fc..9d1fa4965 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -237,7 +237,7 @@ EOT $allowPlugins = $composer->getConfig()->get('allow-plugins'); $removedPlugins = is_array($allowPlugins) ? array_intersect(array_keys($allowPlugins), $packages) : []; - if (!$dryRun && count($removedPlugins) > 0) { + if (!$dryRun && is_array($allowPlugins) && count($removedPlugins) > 0) { if (count($allowPlugins) === count($removedPlugins)) { $json->removeConfigSetting('allow-plugins'); } else { diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 182797431..166d9cbf6 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -301,6 +301,10 @@ class Config $this->setSourceOfConfigValue($val, $key, $env); } + if ($key === 'process-timeout') { + return max(0, false !== $val ? (int) $val : $this->config[$key]); + } + $val = rtrim((string) $this->process(false !== $val ? $val : $this->config[$key], $flags), '/\\'); $val = Platform::expandPath($val); @@ -339,7 +343,7 @@ class Config // ints without env var support case 'cache-ttl': - return (int) $this->config[$key]; + return max(0, (int) $this->config[$key]); // numbers with kb/mb/gb support, without env var support case 'cache-files-maxsize': @@ -348,7 +352,7 @@ class Config "Could not parse the value of '$key': {$this->config[$key]}" ); } - $size = $matches[1]; + $size = (float) $matches[1]; if (isset($matches[2])) { switch (strtolower($matches[2])) { case 'g': @@ -365,15 +369,15 @@ class Config } } - return $size; + return max(0, (int) $size); // special cases below case 'cache-files-ttl': if (isset($this->config[$key])) { - return (int) $this->config[$key]; + return max(0, (int) $this->config[$key]); } - return (int) $this->config['cache-ttl']; + return $this->get('cache-ttl'); case 'home': return rtrim($this->process(Platform::expandPath($this->config[$key]), $flags), '/\\'); diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 38aff1a4d..960fcbf58 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -94,7 +94,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface if ($this->cache && $this->cache->gcIsNecessary()) { $this->io->writeError('Running cache garbage collection', true, IOInterface::VERY_VERBOSE); - $this->cache->gc((int) $config->get('cache-files-ttl'), (int) $config->get('cache-files-maxsize')); + $this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize')); } } diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index d84d9ee88..a63dd0ab3 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -190,7 +190,7 @@ class Factory } $config->setConfigSource(new JsonConfigSource($file)); - $htaccessProtect = (bool) $config->get('htaccess-protect'); + $htaccessProtect = $config->get('htaccess-protect'); if ($htaccessProtect) { // Protect directory against web access. Since HOME could be // the www-data's user home and be web-accessible it is a @@ -645,10 +645,10 @@ class Factory } $httpDownloaderOptions = array(); if ($disableTls === false) { - if ($config->get('cafile')) { + if ('' !== $config->get('cafile')) { $httpDownloaderOptions['ssl']['cafile'] = $config->get('cafile'); } - if ($config->get('capath')) { + if ('' !== $config->get('capath')) { $httpDownloaderOptions['ssl']['capath'] = $config->get('capath'); } $httpDownloaderOptions = array_replace_recursive($httpDownloaderOptions, $options); diff --git a/src/Composer/IO/BaseIO.php b/src/Composer/IO/BaseIO.php index 4f9bc1da9..56715b709 100644 --- a/src/Composer/IO/BaseIO.php +++ b/src/Composer/IO/BaseIO.php @@ -141,8 +141,8 @@ abstract class BaseIO implements IOInterface } foreach ($gitlabToken as $domain => $token) { - $username = is_array($token) && array_key_exists("username", $token) ? $token["username"] : $token; - $password = is_array($token) && array_key_exists("token", $token) ? $token["token"] : 'private-token'; + $username = is_array($token) ? $token["username"] : $token; + $password = is_array($token) ? $token["token"] : 'private-token'; $this->checkAndSetAuthentication($domain, $username, $password); } @@ -156,7 +156,7 @@ abstract class BaseIO implements IOInterface } // setup process timeout - ProcessExecutor::setTimeout((int) $config->get('process-timeout')); + ProcessExecutor::setTimeout($config->get('process-timeout')); } public function emergency($message, array $context = array()): void diff --git a/src/Composer/PHPStan/ConfigReturnTypeExtension.php b/src/Composer/PHPStan/ConfigReturnTypeExtension.php index 1b99218ac..fc1de219e 100644 --- a/src/Composer/PHPStan/ConfigReturnTypeExtension.php +++ b/src/Composer/PHPStan/ConfigReturnTypeExtension.php @@ -11,6 +11,7 @@ use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\IntegerRangeType; @@ -29,7 +30,10 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio public function __construct() { - $schema = JsonFile::parseJson(file_get_contents(__DIR__.'/../../../res/composer-schema.json')); + $schema = JsonFile::parseJson((string) file_get_contents(__DIR__.'/../../../res/composer-schema.json')); + /** + * @var string $prop + */ foreach ($schema['properties']['config']['properties'] as $prop => $conf) { $type = $this->parseType($conf, $prop); @@ -66,7 +70,7 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio } /** - * @param array $types + * @param array $def */ private function parseType(array $def, string $path): Type { @@ -75,15 +79,17 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio foreach ((array) $def['type'] as $type) { switch ($type) { case 'integer': - if (in_array($path, ['process-timeout', 'cache-ttl', 'cache-files-ttl'], true)) { - $types[] = IntegerRangeType::createAllGreaterThan(0); + if (in_array($path, ['process-timeout', 'cache-ttl', 'cache-files-ttl', 'cache-files-maxsize'], true)) { + $types[] = IntegerRangeType::createAllGreaterThanOrEqualTo(0); } else { $types[] = new IntegerType(); } break; case 'string': - if ($path === 'discard-changes') { + if ($path === 'cache-files-maxsize') { + // passthru, skip as it is always converted to int + } elseif ($path === 'discard-changes') { $types[] = new ConstantStringType('stash'); } elseif ($path === 'use-parent-dir') { $types[] = new ConstantStringType('prompt'); @@ -101,7 +107,11 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio break; case 'boolean': - $types[] = new BooleanType(); + if ($path === 'platform.additionalProperties') { + $types[] = new ConstantBooleanType(false); + } else { + $types[] = new BooleanType(); + } break; case 'object': @@ -116,7 +126,7 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio foreach ($def['properties'] as $propName => $propdef) { $keyNames[] = new ConstantStringType($propName); $valType = $this->parseType($propdef, $path.'.'.$propName); - if (!isset($def['required']) || !in_array($propName, $def['required'])) { + if (!isset($def['required']) || !in_array($propName, $def['required'], true)) { $valType = TypeCombinator::addNull($valType); } $valTypes[] = $valType; @@ -146,16 +156,16 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio $types[] = new MixedType(); } } + + $type = TypeCombinator::union(...$types); } elseif (isset($def['enum'])) { - $types[] = TypeCombinator::union(...array_map(function (string $value): ConstantStringType { + $type = TypeCombinator::union(...array_map(function (string $value): ConstantStringType { return new ConstantStringType($value); }, $def['enum'])); } else { - $types = [new MixedType()]; + $type = new MixedType(); } - $type = \count($types) === 1 ? $types[0] : TypeCombinator::union(...$types); - // allow-plugins defaults to null until July 1st 2022 for some BC hackery, but after that it is not nullable anymore if ($path === 'allow-plugins' && time() < strtotime('2022-07-01')) { $type = TypeCombinator::addNull($type); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index b9fd396df..1763f42b4 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -337,17 +337,17 @@ class Locker /** * Locks provided data into lockfile. * - * @param PackageInterface[] $packages array of packages - * @param PackageInterface[]|null $devPackages array of dev packages or null if installed without --dev - * @param array $platformReqs array of package name => constraint for required platform packages - * @param array $platformDevReqs array of package name => constraint for dev-required platform packages - * @param string[][] $aliases array of aliases - * @param string $minimumStability - * @param array $stabilityFlags - * @param bool $preferStable - * @param bool $preferLowest - * @param array $platformOverrides - * @param bool $write Whether to actually write data to disk, useful in tests and for --dry-run + * @param PackageInterface[] $packages array of packages + * @param PackageInterface[]|null $devPackages array of dev packages or null if installed without --dev + * @param array $platformReqs array of package name => constraint for required platform packages + * @param array $platformDevReqs array of package name => constraint for dev-required platform packages + * @param string[][] $aliases array of aliases + * @param string $minimumStability + * @param array $stabilityFlags + * @param bool $preferStable + * @param bool $preferLowest + * @param array $platformOverrides + * @param bool $write Whether to actually write data to disk, useful in tests and for --dry-run * * @return bool * @@ -386,7 +386,7 @@ class Locker $lock['platform'] = $platformReqs; $lock['platform-dev'] = $platformDevReqs; - if ($platformOverrides) { + if (\count($platformOverrides) > 0) { $lock['platform-overrides'] = $platformOverrides; } $lock['plugin-api-version'] = PluginInterface::PLUGIN_API_VERSION; diff --git a/src/Composer/Repository/Vcs/FossilDriver.php b/src/Composer/Repository/Vcs/FossilDriver.php index 86ee0307b..32d54bf67 100644 --- a/src/Composer/Repository/Vcs/FossilDriver.php +++ b/src/Composer/Repository/Vcs/FossilDriver.php @@ -51,7 +51,7 @@ class FossilDriver extends VcsDriver if (Filesystem::isLocalPath($this->url) && is_dir($this->url)) { $this->checkoutDir = $this->url; } else { - if (!Cache::isUsable((string) $this->config->get('cache-repo-dir')) || !Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { + if (!Cache::isUsable($this->config->get('cache-repo-dir')) || !Cache::isUsable($this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('FossilDriver requires a usable cache directory, and it looks like you set it to be disabled'); } diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index 6b4635c3a..71fc6b241 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -49,7 +49,7 @@ class GitDriver extends VcsDriver $this->repoDir = $this->url; $cacheUrl = realpath($this->url); } else { - if (!Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { + if (!Cache::isUsable($this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('GitDriver requires a usable cache directory, and it looks like you set it to be disabled'); } diff --git a/src/Composer/Repository/Vcs/GitLabDriver.php b/src/Composer/Repository/Vcs/GitLabDriver.php index 47dee5e4a..7f2db1f55 100644 --- a/src/Composer/Repository/Vcs/GitLabDriver.php +++ b/src/Composer/Repository/Vcs/GitLabDriver.php @@ -111,7 +111,7 @@ class GitLabDriver extends VcsDriver } $this->originUrl = $origin; - if ($protocol = $this->config->get('gitlab-protocol')) { + if (is_string($protocol = $this->config->get('gitlab-protocol'))) { // https treated as a synonym for http. if (!in_array($protocol, array('git', 'http', 'https'))) { throw new \RuntimeException('gitlab-protocol must be one of git, http.'); @@ -582,7 +582,7 @@ class GitLabDriver extends VcsDriver $guessedDomain = !empty($match['domain']) ? $match['domain'] : $match['domain2']; $urlParts = explode('/', $match['parts']); - if (false === self::determineOrigin((array) $config->get('gitlab-domains'), $guessedDomain, $urlParts, $match['port'])) { + if (false === self::determineOrigin($config->get('gitlab-domains'), $guessedDomain, $urlParts, $match['port'])) { return false; } diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php index 33515f00e..f99790223 100644 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -42,7 +42,7 @@ class HgDriver extends VcsDriver if (Filesystem::isLocalPath($this->url)) { $this->repoDir = $this->url; } else { - if (!Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { + if (!Cache::isUsable($this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('HgDriver requires a usable cache directory, and it looks like you set it to be disabled'); } diff --git a/src/Composer/Repository/Vcs/PerforceDriver.php b/src/Composer/Repository/Vcs/PerforceDriver.php index 92459fdab..396c94894 100644 --- a/src/Composer/Repository/Vcs/PerforceDriver.php +++ b/src/Composer/Repository/Vcs/PerforceDriver.php @@ -62,7 +62,7 @@ class PerforceDriver extends VcsDriver return; } - if (!Cache::isUsable((string) $this->config->get('cache-vcs-dir'))) { + if (!Cache::isUsable($this->config->get('cache-vcs-dir'))) { throw new \RuntimeException('PerforceDriver requires a usable cache directory, and it looks like you set it to be disabled'); } diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 7bf62fcff..9348b109e 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -74,9 +74,6 @@ class Git } $protocols = $this->config->get('github-protocols'); - if (!is_array($protocols)) { - throw new \RuntimeException('Config value "github-protocols" must be an array, got ' . gettype($protocols)); - } // public github, autoswitch protocols if (Preg::isMatch('{^(?:https?|git)://' . self::getGitHubDomainsRegex($this->config) . '/(.*)}', $url, $match)) { $messages = array(); diff --git a/src/Composer/Util/GitLab.php b/src/Composer/Util/GitLab.php index 8f453efaa..267c2b526 100644 --- a/src/Composer/Util/GitLab.php +++ b/src/Composer/Util/GitLab.php @@ -90,8 +90,8 @@ class GitLab } if (isset($token)) { - $username = is_array($token) && array_key_exists("username", $token) ? $token["username"] : $token; - $password = is_array($token) && array_key_exists("token", $token) ? $token["token"] : 'private-token'; + $username = is_array($token) ? $token["username"] : $token; + $password = is_array($token) ? $token["token"] : 'private-token'; $this->io->setAuthentication($originUrl, $username, $password); return true; diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index c30e4199d..04c006560 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -90,9 +90,8 @@ class Url // Gitlab can be installed in a non-root context (i.e. gitlab.com/foo). When downloading archives the originUrl // is the host without the path, so we look for the registered gitlab-domains matching the host here if ( - is_array($config->get('gitlab-domains')) - && false === strpos($origin, '/') - && !in_array($origin, $config->get('gitlab-domains')) + false === strpos($origin, '/') + && !in_array($origin, $config->get('gitlab-domains'), true) ) { foreach ($config->get('gitlab-domains') as $gitlabDomain) { if (0 === strpos($gitlabDomain, $origin)) { diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index 5eaa8ee80..73e46ed07 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -104,6 +104,9 @@ class AutoloadGeneratorTest extends TestCase 'platform-check' => function (): bool { return true; }, + 'use-include-path' => function (): bool { + return false; + }, ); $this->config->expects($this->atLeastOnce()) diff --git a/tests/Composer/Test/Repository/PathRepositoryTest.php b/tests/Composer/Test/Repository/PathRepositoryTest.php index b82837d93..5c30aa339 100644 --- a/tests/Composer/Test/Repository/PathRepositoryTest.php +++ b/tests/Composer/Test/Repository/PathRepositoryTest.php @@ -14,33 +14,26 @@ namespace Composer\Test\Repository; use Composer\Repository\PathRepository; use Composer\Test\TestCase; +use Composer\Util\HttpDownloader; +use Composer\Util\Loop; use Composer\Util\Platform; +use Composer\Util\ProcessExecutor; class PathRepositoryTest extends TestCase { public function testLoadPackageFromFileSystemWithIncorrectPath(): void { self::expectException('RuntimeException'); - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', 'missing')); - $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $repositoryUrl)); $repository->getPackages(); } public function testLoadPackageFromFileSystemWithVersion(): void { - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); - $versionGuesser = null; - $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', 'with-version')); - $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $repositoryUrl)); $repository->getPackages(); $this->assertSame(1, $repository->count()); @@ -49,14 +42,8 @@ class PathRepositoryTest extends TestCase public function testLoadPackageFromFileSystemWithoutVersion(): void { - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); - $versionGuesser = null; - $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', 'without-version')); - $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $repositoryUrl)); $packages = $repository->getPackages(); $this->assertGreaterThanOrEqual(1, $repository->count()); @@ -70,14 +57,8 @@ class PathRepositoryTest extends TestCase public function testLoadPackageFromFileSystemWithWildcard(): void { - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); - $versionGuesser = null; - $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', '*')); - $repository = new PathRepository(array('url' => $repositoryUrl), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $repositoryUrl)); $packages = $repository->getPackages(); $names = array(); @@ -95,12 +76,6 @@ class PathRepositoryTest extends TestCase public function testLoadPackageWithExplicitVersions(): void { - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); - $versionGuesser = null; - $options = array( 'versions' => array( 'test/path-unversioned' => '4.3.2.1', @@ -108,7 +83,7 @@ class PathRepositoryTest extends TestCase ), ); $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', '*')); - $repository = new PathRepository(array('url' => $repositoryUrl, 'options' => $options), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $repositoryUrl, 'options' => $options)); $packages = $repository->getPackages(); $versions = array(); @@ -130,12 +105,6 @@ class PathRepositoryTest extends TestCase */ public function testUrlRemainsRelative(): void { - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); - $versionGuesser = null; - // realpath() does not fully expand the paths // PHP Bug https://bugs.php.net/bug.php?id=72642 $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(realpath(realpath(__DIR__)), 'Fixtures', 'path', 'with-version')); @@ -143,7 +112,7 @@ class PathRepositoryTest extends TestCase // PHP Bug https://bugs.php.net/bug.php?id=73797 $relativeUrl = ltrim(substr($repositoryUrl, strlen(realpath(realpath(Platform::getCwd())))), DIRECTORY_SEPARATOR); - $repository = new PathRepository(array('url' => $relativeUrl), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $relativeUrl)); $packages = $repository->getPackages(); $this->assertSame(1, $repository->count()); @@ -158,16 +127,11 @@ class PathRepositoryTest extends TestCase public function testReferenceNone(): void { - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); - $options = array( 'reference' => 'none', ); $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', '*')); - $repository = new PathRepository(array('url' => $repositoryUrl, 'options' => $options), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $repositoryUrl, 'options' => $options)); $packages = $repository->getPackages(); $this->assertGreaterThanOrEqual(2, $repository->count()); @@ -179,17 +143,12 @@ class PathRepositoryTest extends TestCase public function testReferenceConfig(): void { - $ioInterface = $this->getMockBuilder('Composer\IO\IOInterface') - ->getMock(); - - $config = new \Composer\Config(); - $options = array( 'reference' => 'config', 'relative' => true, ); $repositoryUrl = implode(DIRECTORY_SEPARATOR, array(__DIR__, 'Fixtures', 'path', '*')); - $repository = new PathRepository(array('url' => $repositoryUrl, 'options' => $options), $ioInterface, $config); + $repository = $this->createPathRepo(array('url' => $repositoryUrl, 'options' => $options)); $packages = $repository->getPackages(); $this->assertGreaterThanOrEqual(2, $repository->count()); @@ -201,4 +160,19 @@ class PathRepositoryTest extends TestCase ); } } + + /** + * @param array $options + */ + private function createPathRepo(array $options): PathRepository + { + $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + + $config = new \Composer\Config(); + $proc = new ProcessExecutor(); + $loop = new Loop(new HttpDownloader($io, $config), $proc); + + + return new PathRepository($options, $io, $config, null, null, $proc); + } } From 474d0dc82dfeaa8ac09a87224e5aa037ab964493 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 29 Apr 2022 09:04:02 +0200 Subject: [PATCH 5/5] Extract phpstan extension config into its own file --- phpstan/config.neon | 7 +------ phpstan/rules.neon | 10 ++++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 phpstan/rules.neon diff --git a/phpstan/config.neon b/phpstan/config.neon index 500e97ed4..a07471575 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -4,6 +4,7 @@ includes: - ../vendor/phpstan/phpstan-strict-rules/rules.neon - ../vendor/phpstan/phpstan-symfony/extension.neon - ../vendor/phpstan/phpstan-symfony/rules.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 @@ -58,9 +59,3 @@ parameters: - Composer\Composer::VERSION - Composer\Composer::RELEASE_DATE - Composer\Composer::SOURCE_VERSION - -services: - - - class: Composer\PHPStan\ConfigReturnTypeExtension - tags: - - phpstan.broker.dynamicMethodReturnTypeExtension diff --git a/phpstan/rules.neon b/phpstan/rules.neon new file mode 100644 index 000000000..8d81b0dd3 --- /dev/null +++ b/phpstan/rules.neon @@ -0,0 +1,10 @@ +# Composer-specific PHPStan extensions +# +# These can be reused by third party packages by including 'vendor/composer/composer/phpstan/rules.neon' +# in your phpstan config + +services: + - + class: Composer\PHPStan\ConfigReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension