1
0
Fork 0

Implement config type parsing

pull/10635/head
Jordi Boggiano 2022-04-28 15:13:08 +02:00
parent 532166a892
commit 6fdd9494ef
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC
2 changed files with 127 additions and 6 deletions

View File

@ -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"
}

View File

@ -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<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
{
@ -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<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;
}
}