Skip to content

Commit

Permalink
Merge pull request #7454 from petewalker/feat/native-intersections
Browse files Browse the repository at this point in the history
feat: Handle native intersection types
  • Loading branch information
orklah committed Jan 22, 2022
2 parents 142b85a + be6ce77 commit d1a946c
Show file tree
Hide file tree
Showing 15 changed files with 430 additions and 48 deletions.
25 changes: 12 additions & 13 deletions src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
Expand Up @@ -8,6 +8,7 @@
use PhpParser;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\UnionType;
Expand Down Expand Up @@ -1485,19 +1486,7 @@ private function visitPropertyDeclaration(

if ($stmt->type) {
$parser_property_type = $stmt->type;
if ($parser_property_type instanceof PhpParser\Node\IntersectionType) {
throw new UnexpectedValueException('Intersection types not yet supported');
}
/** @var Identifier|Name|NullableType|UnionType $parser_property_type */

$signature_type = TypeHintResolver::resolve(
$parser_property_type,
$this->codebase->scanner,
$this->file_storage,
$this->storage,
$this->aliases,
$this->codebase->analysis_php_version_id
);
/** @var Identifier|IntersectionType|Name|NullableType|UnionType $parser_property_type */

$signature_type_location = new CodeLocation(
$this->file_scanner,
Expand All @@ -1506,6 +1495,16 @@ private function visitPropertyDeclaration(
false,
CodeLocation::FUNCTION_RETURN_TYPE
);

$signature_type = TypeHintResolver::resolve(
$parser_property_type,
$signature_type_location,
$this->codebase,
$this->file_storage,
$this->storage,
$this->aliases,
$this->codebase->analysis_php_version_id
);
}

$doc_var_group_type = $var_comment->type ?? null;
Expand Down
23 changes: 13 additions & 10 deletions src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php
Expand Up @@ -5,6 +5,7 @@
use LogicException;
use PhpParser;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\Class_;
Expand Down Expand Up @@ -426,14 +427,15 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal

if ($parser_return_type) {
$original_type = $parser_return_type;
if ($original_type instanceof PhpParser\Node\IntersectionType) {
throw new UnexpectedValueException('Intersection types not yet supported');
}
/** @var Identifier|Name|NullableType|UnionType $original_type */
/** @var Identifier|IntersectionType|Name|NullableType|UnionType $original_type */

$storage->return_type = TypeHintResolver::resolve(
$original_type,
$this->codebase->scanner,
new CodeLocation(
$this->file_scanner,
$original_type
),
$this->codebase,
$this->file_storage,
$this->classlike_storage,
$this->aliases,
Expand Down Expand Up @@ -824,14 +826,15 @@ private function getTranslatedFunctionParam(
$param_typehint = $param->type;

if ($param_typehint) {
if ($param_typehint instanceof PhpParser\Node\IntersectionType) {
throw new UnexpectedValueException('Intersection types not yet supported');
}
/** @var Identifier|Name|NullableType|UnionType $param_typehint */
/** @var Identifier|IntersectionType|Name|NullableType|UnionType $param_typehint */

$param_type = TypeHintResolver::resolve(
$param_typehint,
$this->codebase->scanner,
new CodeLocation(
$this->file_scanner,
$param_typehint
),
$this->codebase,
$this->file_storage,
$this->classlike_storage,
$this->aliases,
Expand Down
79 changes: 72 additions & 7 deletions src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php
Expand Up @@ -3,9 +3,17 @@
namespace Psalm\Internal\PhpVisitor\Reflector;

use PhpParser;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\UnionType;
use Psalm\Aliases;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Codebase\Scanner as CodebaseScanner;
use Psalm\Issue\ParseError;
use Psalm\IssueBuffer;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FileStorage;
use Psalm\Type;
Expand All @@ -22,11 +30,12 @@
class TypeHintResolver
{
/**
* @param PhpParser\Node\Identifier|PhpParser\Node\Name|PhpParser\Node\NullableType|PhpParser\Node\UnionType $hint
* @param Identifier|IntersectionType|Name|NullableType|UnionType $hint
*/
public static function resolve(
PhpParser\NodeAbstract $hint,
CodebaseScanner $scanner,
CodeLocation $code_location,
Codebase $codebase,
FileStorage $file_storage,
?ClassLikeStorage $classlike_storage,
Aliases $aliases,
Expand All @@ -36,13 +45,23 @@ public static function resolve(
$type = null;

if (!$hint->types) {
throw new UnexpectedValueException('bad');
throw new UnexpectedValueException('Union type should not be empty');
}

if ($analysis_php_version_id < 8_00_00) {
IssueBuffer::maybeAdd(
new ParseError(
'Union types are not supported in PHP < 8',
$code_location
)
);
}

foreach ($hint->types as $atomic_typehint) {
$resolved_type = self::resolve(
$atomic_typehint,
$scanner,
$code_location,
$codebase,
$file_storage,
$classlike_storage,
$aliases,
Expand All @@ -55,6 +74,52 @@ public static function resolve(
return $type;
}

if ($hint instanceof PhpParser\Node\IntersectionType) {
$type = null;

if (!$hint->types) {
throw new UnexpectedValueException('Intersection type should not be empty');
}

if ($analysis_php_version_id < 8_01_00) {
IssueBuffer::maybeAdd(
new ParseError(
'Intersection types are not supported in PHP < 8.1',
$code_location
)
);
}

foreach ($hint->types as $atomic_typehint) {
$resolved_type = self::resolve(
$atomic_typehint,
$code_location,
$codebase,
$file_storage,
$classlike_storage,
$aliases,
$analysis_php_version_id
);

if ($resolved_type->hasScalarType()) {
IssueBuffer::maybeAdd(
new ParseError(
'Intersection types cannot contain scalar types',
$code_location
)
);
}

$type = Type::intersectUnionTypes($resolved_type, $type, $codebase);
}

if ($type === null) {
throw new UnexpectedValueException('Intersection type could not be resolved');
}

return $type;
}

$is_nullable = false;

if ($hint instanceof PhpParser\Node\NullableType) {
Expand All @@ -69,7 +134,7 @@ public static function resolve(
} elseif ($hint instanceof PhpParser\Node\Name\FullyQualified) {
$fq_type_string = (string)$hint;

$scanner->queueClassLikeForScanning($fq_type_string);
$codebase->scanner->queueClassLikeForScanning($fq_type_string);
$file_storage->referenced_classlikes[strtolower($fq_type_string)] = $fq_type_string;
} else {
$lower_hint = strtolower($hint->parts[0]);
Expand All @@ -87,7 +152,7 @@ public static function resolve(
$type_string = implode('\\', $hint->parts);
$fq_type_string = ClassLikeAnalyzer::getFQCLNFromNameObject($hint, $aliases);

$scanner->queueClassLikeForScanning($fq_type_string);
$codebase->scanner->queueClassLikeForScanning($fq_type_string);
$file_storage->referenced_classlikes[strtolower($fq_type_string)] = $fq_type_string;
}
}
Expand Down
20 changes: 18 additions & 2 deletions src/Psalm/Type.php
Expand Up @@ -551,10 +551,26 @@ public static function combineUnionTypes(
*
*/
public static function intersectUnionTypes(
Union $type_1,
Union $type_2,
?Union $type_1,
?Union $type_2,
Codebase $codebase
): ?Union {
if ($type_2 === null && $type_1 === null) {
throw new UnexpectedValueException('At least one type must be provided to combine');
}

if ($type_1 === null) {
return $type_2;
}

if ($type_2 === null) {
return $type_1;
}

if ($type_1 === $type_2) {
return $type_1;
}

$intersection_performed = false;
$type_1_mixed = $type_1->isMixed();
$type_2_mixed = $type_2->isMixed();
Expand Down
5 changes: 4 additions & 1 deletion tests/BinaryOperationTest.php
Expand Up @@ -260,7 +260,7 @@ function takesI(I $i): void
}

/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>}>
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
Expand Down Expand Up @@ -818,6 +818,9 @@ function scope(array $a): int|float {
return 0;
}
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
'NumericStringIncrementLiteral' => [
'code' => '<?php
Expand Down
7 changes: 5 additions & 2 deletions tests/InterfaceTest.php
Expand Up @@ -13,7 +13,7 @@ class InterfaceTest extends TestCase
use ValidCodeAnalysisTestTrait;

/**
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>}>
* @return iterable<string,array{code:string,assertions?:array<string,string>,ignored_issues?:list<string>,php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
Expand Down Expand Up @@ -719,7 +719,10 @@ function takesAorB(SomeClass|SomeInterface $some): void {
if ($some instanceof SomeInterface) {
$some->doStuff();
}
}'
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0',
],
];
}
Expand Down

0 comments on commit d1a946c

Please sign in to comment.