Implement config type parsing

main
Jordi Boggiano 2 years ago
parent 532166a892
commit 6fdd9494ef
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC

@ -326,7 +326,7 @@
}, },
"github-protocols": { "github-protocols": {
"type": "array", "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": { "items": {
"type": "string" "type": "string"
} }

@ -3,20 +3,39 @@
namespace Composer\PHPStan; namespace Composer\PHPStan;
use Composer\Config; use Composer\Config;
use Composer\Json\JsonFile;
use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope; use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType; use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType; use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension; use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType; use PHPStan\Type\StringType;
use PHPStan\Type\Type; use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtension { final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
/** @var array<string, \PHPStan\Type\Type> */
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 public function getClass(): string
{ {
@ -38,13 +57,115 @@ final class ConfigReturnTypeExtension implements DynamicMethodReturnTypeExtensio
$keyType = $scope->getType($args[0]->value); $keyType = $scope->getType($args[0]->value);
if ($keyType instanceof ConstantStringType) { if ($keyType instanceof ConstantStringType) {
if ($keyType->getValue() == 'allow-plugins') { if (isset($this->properties[$keyType->getValue()])) {
return TypeCombinator::addNull( return $this->properties[$keyType->getValue()];
new ArrayType(new StringType(), new BooleanType())
);
} }
} }
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
} }
/**
* @param array<mixed> $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;
}
} }

Loading…
Cancel
Save