Skip to content

Commit

Permalink
Implement prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
danog committed Jan 19, 2024
1 parent 8ce89dc commit 9509e86
Show file tree
Hide file tree
Showing 17 changed files with 464 additions and 11 deletions.
62 changes: 62 additions & 0 deletions src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TSatisfiedBy;
use Psalm\Type\Atomic\TScalar;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateSatisfiedBy;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Atomic\TValueOf;
use Psalm\Type\Union;
Expand Down Expand Up @@ -105,6 +107,32 @@ public static function isContainedBy(
}
}

if ($input_type_part instanceof TSatisfiedBy) {
if ($container_type_part instanceof TSatisfiedBy) {
return UnionTypeComparator::isContainedBy(
$codebase,
$input_type_part->type->as_type,

Check failure on line 114 in src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullArgument

src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php:114:21: PossiblyNullArgument: Argument 2 of Psalm\Internal\Type\Comparator\UnionTypeComparator::isContainedBy cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 114 in src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullArgument

src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php:114:21: PossiblyNullArgument: Argument 2 of Psalm\Internal\Type\Comparator\UnionTypeComparator::isContainedBy cannot be null, possibly null value provided (see https://psalm.dev/078)
$container_type_part->type->as_type,

Check failure on line 115 in src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullArgument

src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php:115:21: PossiblyNullArgument: Argument 3 of Psalm\Internal\Type\Comparator\UnionTypeComparator::isContainedBy cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 115 in src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullArgument

src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php:115:21: PossiblyNullArgument: Argument 3 of Psalm\Internal\Type\Comparator\UnionTypeComparator::isContainedBy cannot be null, possibly null value provided (see https://psalm.dev/078)
false,
false,
null,
false,
false,
);
} else {
return UnionTypeComparator::isContainedBy(
$codebase,
$input_type_part->type->as_type,

Check failure on line 125 in src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullArgument

src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php:125:21: PossiblyNullArgument: Argument 2 of Psalm\Internal\Type\Comparator\UnionTypeComparator::isContainedBy cannot be null, possibly null value provided (see https://psalm.dev/078)

Check failure on line 125 in src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullArgument

src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php:125:21: PossiblyNullArgument: Argument 2 of Psalm\Internal\Type\Comparator\UnionTypeComparator::isContainedBy cannot be null, possibly null value provided (see https://psalm.dev/078)
new Union([$container_type_part]),
false,
false,
null,
false,
false,
);
}
}

if ($container_type_part instanceof TMixed
|| ($container_type_part instanceof TTemplateParam
&& $container_type_part->as->isMixed()
Expand Down Expand Up @@ -432,6 +460,18 @@ public static function isContainedBy(
);
}

if ($container_type_part instanceof TTemplateSatisfiedBy) {
if (!$input_type_part instanceof TTemplateSatisfiedBy) {
return false;
}

return UnionTypeComparator::isContainedBy(
$codebase,
$input_type_part->as,
$container_type_part->as,
);
}

if ($input_type_part instanceof TTemplateValueOf) {
$array_value_type = TValueOf::getValueType($input_type_part->as, $codebase);
if ($array_value_type === null) {
Expand All @@ -454,6 +494,28 @@ public static function isContainedBy(
return true;
}

if ($input_type_part instanceof TTemplateSatisfiedBy) {
$array_value_type = $input_type_part->as->as_type;
if ($array_value_type === null) {
return false;
}

foreach ($array_value_type->getAtomicTypes() as $array_value_atomic) {
if (!self::isContainedBy(
$codebase,
$array_value_atomic,
$container_type_part,
$allow_interface_equality,
$allow_float_int_equality,
$atomic_comparison_result,
)) {
return false;
}
}

return true;
}

if ($container_type_part instanceof TTemplateParam && $input_type_part instanceof TTemplateParam) {
return UnionTypeComparator::isContainedBy(
$codebase,
Expand Down
14 changes: 14 additions & 0 deletions src/Psalm/Internal/Type/ParseTree/TypeIsTree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Psalm\Internal\Type\ParseTree;

use Psalm\Internal\Type\ParseTree;

/**
* @internal
*/
final class TypeIsTree extends ParseTree
{
}
8 changes: 4 additions & 4 deletions src/Psalm/Internal/Type/ParseTreeCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -562,11 +562,11 @@ private function handleColon(): void
return;
}

if (!$this->current_leaf instanceof Value) {
throw new TypeParseTreeException('Unexpected LHS of property');
}

if ($current_parent instanceof KeyedArrayTree) {
if (!$this->current_leaf instanceof Value) {
throw new TypeParseTreeException('Unexpected LHS of property');
}

$prev_token = $this->t > 0 ? $this->type_tokens[$this->t - 1] : null;

$new_parent_leaf = new KeyedArrayPropertyTree($this->current_leaf->value, $current_parent);
Expand Down
5 changes: 5 additions & 0 deletions src/Psalm/Internal/Type/SimpleAssertionReconciler.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TResource;
use Psalm\Type\Atomic\TSatisfiedBy;
use Psalm\Type\Atomic\TScalar;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParam;
Expand Down Expand Up @@ -291,6 +292,10 @@ public static function reconcile(

$assertion_type = $assertion->getAtomicType();

while ($assertion_type instanceof TSatisfiedBy) {
$assertion_type = $assertion_type->type->as_type;
}

if ($assertion_type instanceof TObject) {
return self::reconcileObject(
$codebase,
Expand Down
25 changes: 22 additions & 3 deletions src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TPropertiesOf;
use Psalm\Type\Atomic\TSatisfiedBy;
use Psalm\Type\Atomic\TTemplateIndexedAccess;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTemplatePropertiesOf;
use Psalm\Type\Atomic\TTemplateSatisfiedBy;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Atomic\TValueOf;
use Psalm\Type\Union;
Expand Down Expand Up @@ -173,6 +175,7 @@ public static function replace(
}
} elseif ($atomic_type instanceof TTemplateKeyOf
|| $atomic_type instanceof TTemplateValueOf
|| $atomic_type instanceof TTemplateSatisfiedBy
) {
$new_type = self::replaceTemplateKeyOfValueOf(
$codebase,
Expand Down Expand Up @@ -234,12 +237,22 @@ public static function replace(
throw new UnexpectedValueException('This array should be full');
}

return $union->getBuilder()->setTypes(
$builder = $union->getBuilder()->setTypes(
TypeCombiner::combine(
$atomic_types,
$codebase,
)->getAtomicTypes(),
)->freeze();
);

if ($union->as_type !== null) {
$builder->as_type = self::replace(

Check failure on line 248 in src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php

View workflow job for this annotation

GitHub Actions / build

ImpurePropertyAssignment

src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php:248:13: ImpurePropertyAssignment: Cannot assign to a property from a mutation-free context (see https://psalm.dev/204)

Check failure on line 248 in src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php

View workflow job for this annotation

GitHub Actions / build

ImpurePropertyAssignment

src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php:248:13: ImpurePropertyAssignment: Cannot assign to a property from a mutation-free context (see https://psalm.dev/204)
$union->as_type,
$template_result,
$codebase
);
}

return $builder->freeze();
}

/**
Expand Down Expand Up @@ -333,7 +346,7 @@ private static function replaceTemplateParam(
}

/**
* @param TTemplateKeyOf|TTemplateValueOf $atomic_type
* @param TTemplateKeyOf|TTemplateValueOf|TTemplateSatisfiedBy $atomic_type
* @param array<string, array<string, non-empty-list<TemplateBound>>> $inferred_lower_bounds
*/
private static function replaceTemplateKeyOfValueOf(
Expand Down Expand Up @@ -362,6 +375,12 @@ private static function replaceTemplateKeyOfValueOf(
return new TValueOf($template_type);
}

if ($atomic_type instanceof TTemplateSatisfiedBy
&& $template_type->as_type !== null
) {
return new TSatisfiedBy($template_type);
}

return null;
}

Expand Down
10 changes: 9 additions & 1 deletion src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTemplatePropertiesOf;
use Psalm\Type\Atomic\TTemplateSatisfiedBy;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Union;

Expand Down Expand Up @@ -324,7 +325,8 @@ private static function handleAtomicStandin(
}

if ($atomic_type instanceof TTemplateKeyOf
|| $atomic_type instanceof TTemplateValueOf) {
|| $atomic_type instanceof TTemplateValueOf
|| $atomic_type instanceof TTemplateSatisfiedBy) {
if (!$replace) {
return [$atomic_type];
}
Expand All @@ -344,6 +346,12 @@ private static function handleAtomicStandin(
}

if ($template_type) {
if ($atomic_type instanceof TTemplateSatisfiedBy) {
if ($template_type->as_type === null) {
return [$atomic_type];
}
return $template_type->as_type->getAtomicTypes();

Check failure on line 353 in src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php

View workflow job for this annotation

GitHub Actions / build

LessSpecificReturnStatement

src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php:353:28: LessSpecificReturnStatement: The type 'non-empty-array<string, Psalm\Type\Atomic>' is more general than the declared return type 'list<Psalm\Type\Atomic>' for Psalm\Internal\Type\TemplateStandinTypeReplacer::handleAtomicStandin (see https://psalm.dev/129)

Check failure on line 353 in src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php

View workflow job for this annotation

GitHub Actions / build

LessSpecificReturnStatement

src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php:353:28: LessSpecificReturnStatement: The type 'non-empty-array<string, Psalm\Type\Atomic>' is more general than the declared return type 'list<Psalm\Type\Atomic>' for Psalm\Internal\Type\TemplateStandinTypeReplacer::handleAtomicStandin (see https://psalm.dev/129)
}
foreach ($template_type->getAtomicTypes() as $template_atomic) {
if (!$template_atomic instanceof TKeyedArray
&& !$template_atomic instanceof TArray
Expand Down
56 changes: 56 additions & 0 deletions src/Psalm/Internal/Type/TypeExpander.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TPropertiesOf;
use Psalm\Type\Atomic\TSatisfiedBy;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTypeAlias;
use Psalm\Type\Atomic\TValueOf;
Expand Down Expand Up @@ -355,6 +356,22 @@ public static function expandAtomic(
);
}

if ($return_type instanceof TSatisfiedBy) {
return self::expandTSatisfiedBy(
$codebase,
$return_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final,
$expand_generic,
$expand_templates,
$throw_on_unresolvable_constant,
);
}

if ($return_type instanceof TIntMask) {
if (!$evaluate_class_constants) {
return [new TInt()];
Expand Down Expand Up @@ -1096,4 +1113,43 @@ private static function expandKeyOfValueOf(

return array_values($new_return_types->getAtomicTypes());
}


/**
* @return non-empty-list<Atomic>
*/
private static function expandTSatisfiedBy(
Codebase $codebase,
TSatisfiedBy &$return_type,
?string $self_class,
string|TNamedObject|TTemplateParam|null $static_class_type,
?string $parent_class,
bool $evaluate_class_constants = true,
bool $evaluate_conditional_types = false,
bool $final = false,
bool $expand_generic = false,
bool $expand_templates = false,
bool $throw_on_unresolvable_constant = false,
): array {
// Expand class constants to their atomics
$type_atomics = [];
foreach ($return_type->type->as_type->getAtomicTypes() as $type_param) {

Check failure on line 1136 in src/Psalm/Internal/Type/TypeExpander.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullReference

src/Psalm/Internal/Type/TypeExpander.php:1136:47: PossiblyNullReference: Cannot call method getAtomicTypes on possibly null value (see https://psalm.dev/083)

Check failure on line 1136 in src/Psalm/Internal/Type/TypeExpander.php

View workflow job for this annotation

GitHub Actions / build

PossiblyNullReference

src/Psalm/Internal/Type/TypeExpander.php:1136:47: PossiblyNullReference: Cannot call method getAtomicTypes on possibly null value (see https://psalm.dev/083)
$type_param_expanded = self::expandAtomic(
$codebase,
$type_param,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final,
$expand_generic,
$expand_templates,
$throw_on_unresolvable_constant,
);
$type_atomics = [...$type_atomics, ...$type_param_expanded];
}

return $type_atomics;
}
}
32 changes: 32 additions & 0 deletions src/Psalm/Internal/Type/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@
use Psalm\Type\Atomic\TObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TPropertiesOf;
use Psalm\Type\Atomic\TSatisfiedBy;
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateIndexedAccess;
use Psalm\Type\Atomic\TTemplateKeyOf;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TTemplatePropertiesOf;
use Psalm\Type\Atomic\TTemplateSatisfiedBy;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Atomic\TTypeAlias;
use Psalm\Type\Atomic\TUnknownClassString;
Expand Down Expand Up @@ -726,8 +728,10 @@ private static function getTypeFromGenericTree(
|| $atomic_type instanceof TTemplateValueOf
|| $atomic_type instanceof TTemplateKeyOf
|| $atomic_type instanceof TTemplateParamClass
|| $atomic_type instanceof TTemplateSatisfiedBy
|| $atomic_type instanceof TTypeAlias
|| $atomic_type instanceof TValueOf
|| $atomic_type instanceof TSatisfiedBy
|| $atomic_type instanceof TConditional
|| $atomic_type instanceof TKeyOf
|| !$from_docblock
Expand Down Expand Up @@ -965,6 +969,34 @@ private static function getTypeFromGenericTree(
return new TValueOf($generic_params[0]);
}

if ($generic_type_value === 'satisfied-by') {
$param_name = $generic_params[0]->getId(false);

if (isset($template_type_map[$param_name])
&& ($defining_class = array_key_first($template_type_map[$param_name])) !== null
) {
return new TTemplateSatisfiedBy(
$param_name,
$defining_class,
$generic_params[0],
$from_docblock,
);
}

if (!$generic_params[0]->as_type
&& !$generic_params[0] instanceof TTypeAlias
&& !$generic_params[0] instanceof TValueOf
&& !$generic_params[0] instanceof TKeyOf
&& !$generic_params[0] instanceof TPropertiesOf
) {
throw new TypeParseTreeException(
'Untemplated satisfied-by param ' . $param_name . ' should have an upper bound!',
);
}

return new TSatisfiedBy($generic_params[0]);
}

if ($generic_type_value === 'int-mask') {
$atomic_types = [];

Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/Type/TypeTokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ final class TypeTokenizer
'public-properties-of' => true,
'protected-properties-of' => true,
'private-properties-of' => true,
'satisfied-by' => true,
'non-empty-countable' => true,
'list' => true,
'non-empty-list' => true,
Expand Down
Loading

0 comments on commit 9509e86

Please sign in to comment.