From 6fdd9494efa2d5d638db1be6cb669668ced4fa4f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 28 Apr 2022 15:13:08 +0200 Subject: [PATCH] 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; + } }