Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce callable object intersection #9599

Merged
merged 8 commits into from
Apr 9, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\Scalar;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TCallableObject;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TIterable;
Expand Down Expand Up @@ -750,6 +751,7 @@ public static function handleIterable(
foreach ($iterator_atomic_types as $iterator_atomic_type) {
if ($iterator_atomic_type instanceof TTemplateParam
|| $iterator_atomic_type instanceof TObjectWithProperties
|| $iterator_atomic_type instanceof TCallableObject
) {
throw new UnexpectedValueException('Shouldn’t get a generic param here');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -723,8 +723,17 @@ private static function getAnalyzeNamedExpression(
),
$statements_analyzer->getSuppressedIssues(),
);
} elseif ($var_type_part instanceof TCallableObject
|| $var_type_part instanceof TCallableString
} elseif ($var_type_part instanceof TCallableObject) {
$has_valid_function_call_type = true;
self::analyzeInvokeCall(
$statements_analyzer,
$stmt,
$real_stmt,
$function_name,
$context,
$var_type_part,
);
} elseif ($var_type_part instanceof TCallableString
|| ($var_type_part instanceof TNamedObject && $var_type_part->value === 'Closure')
|| ($var_type_part instanceof TObjectWithProperties && isset($var_type_part->methods['__invoke']))
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
use Psalm\Storage\ClassLikeStorage;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TCallableObject;
use Psalm\Type\Atomic\TClosure;
use Psalm\Type\Atomic\TEmptyMixed;
use Psalm\Type\Atomic\TFalse;
Expand Down Expand Up @@ -104,6 +106,18 @@ public static function analyze(

$source = $statements_analyzer->getSource();

if ($lhs_type_part instanceof TCallableObject) {
self::handleCallableObject(
$statements_analyzer,
$stmt,
$context,
$lhs_type_part->callable,
$result,
$inferred_template_result,
);
return;
}

if (!$lhs_type_part instanceof TNamedObject) {
self::handleInvalidClass(
$statements_analyzer,
Expand Down Expand Up @@ -891,4 +905,55 @@ private static function handleRegularMixins(
$fq_class_name,
];
}

private static function handleCallableObject(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\MethodCall $stmt,
Context $context,
?TCallable $lhs_type_part_callable,
AtomicMethodCallAnalysisResult $result,
?TemplateResult $inferred_template_result = null
): void {
$method_id = 'object::__invoke';
$result->existent_method_ids[] = $method_id;
$result->has_valid_method_call_type = true;

if ($lhs_type_part_callable !== null) {
$result->return_type = $lhs_type_part_callable->return_type ?? Type::getMixed();
$callableArgumentCount = count($lhs_type_part_callable->params ?? []);
$providedArgumentsCount = count($stmt->getArgs());

if ($callableArgumentCount > $providedArgumentsCount) {
$result->too_few_arguments = true;
$result->too_few_arguments_method_ids[] = new MethodIdentifier('callable-object', '__invoke');
} elseif ($providedArgumentsCount > $callableArgumentCount) {
$result->too_many_arguments = true;
$result->too_many_arguments_method_ids[] = new MethodIdentifier('callable-object', '__invoke');
}

$template_result = $inferred_template_result ?? new TemplateResult([], []);

ArgumentsAnalyzer::analyze(
$statements_analyzer,
$stmt->getArgs(),
$lhs_type_part_callable->params,
$method_id,
false,
$context,
$template_result,
);

ArgumentsAnalyzer::checkArgumentsMatch(
$statements_analyzer,
$stmt->getArgs(),
$method_id,
$lhs_type_part_callable->params ?? [],
null,
null,
$template_result,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$context,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTemplateParamClass;
use Psalm\Type\Atomic\TUnknownClassString;
use Psalm\Type\TaintKind;
use Psalm\Type\Union;

Expand Down Expand Up @@ -777,9 +778,10 @@ private static function analyzeConstructorExpression(
) {
if (!$statements_analyzer->node_data->getType($stmt)) {
if ($lhs_type_part instanceof TClassString) {
$generated_type = $lhs_type_part->as_type
? $lhs_type_part->as_type
: new TObject();
$generated_type = $lhs_type_part->as_type ?? new TObject();
if ($lhs_type_part instanceof TUnknownClassString) {
$generated_type = $lhs_type_part->as_unknown_type ?? $generated_type;
}

if ($lhs_type_part->as_type
&& $codebase->classlikes->classExists($lhs_type_part->as_type->value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\Possibilities;
use Psalm\Type;
use Psalm\Type\Atomic\TCallableObject;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TObjectWithProperties;
use Psalm\Type\Atomic\TTemplateParam;
Expand Down Expand Up @@ -579,6 +580,7 @@ public static function getFunctionIdsFromCallableArg(
foreach ($type_part->extra_types as $extra_type) {
if ($extra_type instanceof TTemplateParam
|| $extra_type instanceof TObjectWithProperties
|| $extra_type instanceof TCallableObject
) {
throw new UnexpectedValueException('Shouldn’t get a generic param here');
}
Expand Down
11 changes: 8 additions & 3 deletions src/Psalm/Internal/Type/Comparator/ObjectComparator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Psalm\Codebase;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TCallableObject;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
Expand Down Expand Up @@ -90,6 +91,8 @@ public static function isShallowlyContainedBy(
$intersection_container_type_lower = 'object';
} elseif ($intersection_container_type instanceof TTemplateParam) {
$intersection_container_type_lower = null;
} elseif ($intersection_container_type instanceof TCallableObject) {
$intersection_container_type_lower = 'callable-object';
} else {
$container_was_static = $intersection_container_type->is_static;

Expand Down Expand Up @@ -134,7 +137,7 @@ public static function isShallowlyContainedBy(

/**
* @param TNamedObject|TTemplateParam|TIterable $type_part
* @return array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties>
* @return array<string, TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject>
boesing marked this conversation as resolved.
Show resolved Hide resolved
*/
private static function getIntersectionTypes(Atomic $type_part): array
{
Expand Down Expand Up @@ -166,8 +169,8 @@ private static function getIntersectionTypes(Atomic $type_part): array
}

/**
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $intersection_input_type
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $intersection_container_type
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject $intersection_input_type
* @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties|TCallableObject $intersection_container_type
*/
private static function isIntersectionShallowlyContainedBy(
Codebase $codebase,
Expand Down Expand Up @@ -268,6 +271,8 @@ private static function isIntersectionShallowlyContainedBy(
$intersection_input_type_lower = 'iterable';
} elseif ($intersection_input_type instanceof TObjectWithProperties) {
$intersection_input_type_lower = 'object';
} elseif ($intersection_input_type instanceof TCallableOBject) {
boesing marked this conversation as resolved.
Show resolved Hide resolved
$intersection_input_type_lower = 'callable-object';
} else {
$input_was_static = $intersection_input_type->is_static;

Expand Down
3 changes: 2 additions & 1 deletion src/Psalm/Internal/Type/SimpleAssertionReconciler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1603,7 +1603,8 @@ private static function reconcileObject(
if ($type->isObjectType()) {
$object_types[] = $type;
} elseif ($type instanceof TCallable) {
$object_types[] = new TCallableObject();
$callable_object = new TCallableObject($type->from_docblock, $type);
$object_types[] = $callable_object;
$redundant = false;
} elseif ($type instanceof TTemplateParam
&& $type->as->isMixed()
Expand Down
116 changes: 83 additions & 33 deletions src/Psalm/Internal/Type/TypeParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Psalm\Type\Atomic\TArrayKey;
use Psalm\Type\Atomic\TCallable;
use Psalm\Type\Atomic\TCallableKeyedArray;
use Psalm\Type\Atomic\TCallableObject;
use Psalm\Type\Atomic\TClassConstant;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TClassStringMap;
Expand Down Expand Up @@ -61,6 +62,7 @@
use Psalm\Type\Atomic\TTemplatePropertiesOf;
use Psalm\Type\Atomic\TTemplateValueOf;
use Psalm\Type\Atomic\TTypeAlias;
use Psalm\Type\Atomic\TUnknownClassString;
use Psalm\Type\Atomic\TValueOf;
use Psalm\Type\TypeNode;
use Psalm\Type\Union;
Expand Down Expand Up @@ -711,13 +713,25 @@ private static function getTypeFromGenericTree(

$types = [];
foreach ($generic_params[0]->getAtomicTypes() as $type) {
if (!$type instanceof TNamedObject) {
throw new TypeParseTreeException('Class string param should be a named object');
if ($type instanceof TNamedObject) {
$types[] = new TClassString($type->value, $type, false, false, false, $from_docblock);
continue;
}

$types[] = new TClassString($type->value, $type, false, false, false, $from_docblock);
if ($type instanceof TCallableObject) {
$types[] = new TUnknownClassString($type, false, $from_docblock);
continue;
}

throw new TypeParseTreeException('class-string param can only target to named or callable objects');
}

assert(
$types !== [],
'Since `Union` cannot be empty and all non-supported atomics lead to thrown exception,'
.' we can safely assert that the types array is non-empty.',
);

return new Union($types);
}

Expand Down Expand Up @@ -1196,46 +1210,72 @@ private static function getTypeFromIntersectionTree(
if ($keyed_intersection_types) {
return $first_type->setIntersectionTypes($keyed_intersection_types);
}
} else {
foreach ($intersection_types as $intersection_type) {
if (!$intersection_type instanceof TIterable
&& !$intersection_type instanceof TNamedObject
&& !$intersection_type instanceof TTemplateParam
&& !$intersection_type instanceof TObjectWithProperties
) {

return $first_type;
}

$callable_intersection = null;

foreach ($intersection_types as $intersection_type) {
if ($intersection_type instanceof TIterable
|| $intersection_type instanceof TNamedObject
|| $intersection_type instanceof TTemplateParam
|| $intersection_type instanceof TObjectWithProperties
) {
$keyed_intersection_types[self::extractIntersectionKey($intersection_type)] = $intersection_type;
continue;
}

if (get_class($intersection_type) === TObject::class) {
continue;
}

if ($intersection_type instanceof TCallable) {
if ($callable_intersection !== null) {
throw new TypeParseTreeException(
'Intersection types must be all objects, '
. get_class($intersection_type) . ' provided',
'The intersection type must not contain more than one callable type!',
);
}

$keyed_intersection_types[$intersection_type instanceof TIterable
? $intersection_type->getId()
: $intersection_type->getKey()] = $intersection_type;
$callable_intersection = $intersection_type;
continue;
}

$intersect_static = false;
throw new TypeParseTreeException(
'Intersection types must be all objects, '
. get_class($intersection_type) . ' provided',
);
}

if ($callable_intersection !== null) {
$callable_object_type = new TCallableObject(
$callable_intersection->from_docblock,
$callable_intersection,
);

if (isset($keyed_intersection_types['static'])) {
unset($keyed_intersection_types['static']);
$intersect_static = true;
}
$keyed_intersection_types[self::extractIntersectionKey($callable_object_type)] = $callable_object_type;
}

if (!$keyed_intersection_types && $intersect_static) {
return new TNamedObject('static', false, false, [], $from_docblock);
}
$intersect_static = false;

$first_type = array_shift($keyed_intersection_types);
if (isset($keyed_intersection_types['static'])) {
unset($keyed_intersection_types['static']);
$intersect_static = true;
}

if ($intersect_static
&& $first_type instanceof TNamedObject
) {
$first_type->is_static = true;
}
if (!$keyed_intersection_types && $intersect_static) {
return new TNamedObject('static', false, false, [], $from_docblock);
}

if ($keyed_intersection_types) {
return $first_type->setIntersectionTypes($keyed_intersection_types);
}
$first_type = array_shift($keyed_intersection_types);

if ($intersect_static
&& $first_type instanceof TNamedObject
) {
$first_type->is_static = true;
}

if ($keyed_intersection_types) {
return $first_type->setIntersectionTypes($keyed_intersection_types);
}

return $first_type;
Expand Down Expand Up @@ -1528,4 +1568,14 @@ private static function getTypeFromKeyedArrayTree(
$from_docblock,
);
}

/**
* @param TNamedObject|TObjectWithProperties|TCallableObject|TIterable|TTemplateParam $intersection_type
*/
private static function extractIntersectionKey(Atomic $intersection_type): string
{
return $intersection_type instanceof TIterable
? $intersection_type->getId()
: $intersection_type->getKey();
}
}
Loading