From a3bdf1ba8ce51cf6193d14fc44010c00ae8fe99f Mon Sep 17 00:00:00 2001 From: Pete Walker Date: Fri, 21 Jan 2022 12:59:55 +0000 Subject: [PATCH 1/6] feat: Handle native intersection types Adds native intersection type handling to psalm, removing the previous `UnexpectedValueException`. Where an intersection is found in the parse tree, the types are resolved using the existing `Type::intersectUnionTypes` function, which I assume is being used when they're encountered in the existing docblock annotations. I've added a handful of tests to cover this, but they're certainly not exhaustive. Are there any specific edge cases I should target? This change feels way too simple... so apologies if I've missed something fundamental..! --- .../Reflector/ClassLikeNodeScanner.php | 8 +- .../Reflector/FunctionLikeNodeScanner.php | 15 +-- .../PhpVisitor/Reflector/TypeHintResolver.php | 44 ++++++- src/Psalm/Type.php | 20 +++- tests/NativeIntersectionsTest.php | 108 ++++++++++++++++++ 5 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 tests/NativeIntersectionsTest.php diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 9dffdfe8026..4ae30b5f4db 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -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; @@ -1485,14 +1486,11 @@ 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 */ + /** @var Identifier|IntersectionType|Name|NullableType|UnionType $parser_property_type */ $signature_type = TypeHintResolver::resolve( $parser_property_type, - $this->codebase->scanner, + $this->codebase, $this->file_storage, $this->storage, $this->aliases, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index f1caac12d17..077445c09e8 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -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_; @@ -425,14 +426,11 @@ 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, + $this->codebase, $this->file_storage, $this->classlike_storage, $this->aliases, @@ -823,14 +821,11 @@ 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, + $this->codebase, $this->file_storage, $this->classlike_storage, $this->aliases, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index e594ac9afb1..f11b97a87c9 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -3,9 +3,14 @@ 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\Codebase; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; -use Psalm\Internal\Codebase\Scanner as CodebaseScanner; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FileStorage; use Psalm\Type; @@ -22,11 +27,11 @@ 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, + Codebase $codebase, FileStorage $file_storage, ?ClassLikeStorage $classlike_storage, Aliases $aliases, @@ -42,7 +47,7 @@ public static function resolve( foreach ($hint->types as $atomic_typehint) { $resolved_type = self::resolve( $atomic_typehint, - $scanner, + $codebase, $file_storage, $classlike_storage, $aliases, @@ -55,6 +60,33 @@ public static function resolve( return $type; } + if ($hint instanceof PhpParser\Node\IntersectionType) { + $type = null; + + if (!$hint->types) { + throw new UnexpectedValueException('bad'); + } + + foreach ($hint->types as $atomic_typehint) { + $resolved_type = self::resolve( + $atomic_typehint, + $codebase, + $file_storage, + $classlike_storage, + $aliases, + $analysis_php_version_id + ); + + $type = Type::intersectUnionTypes($resolved_type, $type, $codebase); + } + + if ($type === null) { + throw new UnexpectedValueException('bad'); + } + + return $type; + } + $is_nullable = false; if ($hint instanceof PhpParser\Node\NullableType) { @@ -69,7 +101,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]); @@ -87,7 +119,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; } } diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index e6e988f842a..40a2edf7b45 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -552,10 +552,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(); diff --git a/tests/NativeIntersectionsTest.php b/tests/NativeIntersectionsTest.php new file mode 100644 index 00000000000..443cf2ba329 --- /dev/null +++ b/tests/NativeIntersectionsTest.php @@ -0,0 +1,108 @@ +,ignored_issues?:list}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'nativeTypeIntersectionInConstructor' => [ + 'code' => 'self; + } + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1' + ], + 'nativeTypeIntersectionAsArgument' => [ + 'code' => 'foo(); + } + test(new C()); + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1' + ], + ]; + } + + /** + * @return iterable,php_version?:string}> + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'invalidNativeIntersectionArgument' => [ + 'code' => 'foo(); + } + test(new C()); + ', + 'error_message' => 'InvalidArgument', + 'ignored_issues' => [], + 'php_version' => '8.1' + ], + 'mismatchDocblockNativeIntersectionArgument' => [ + 'code' => 'foo(); + } + ', + 'error_message' => 'MismatchingDocblockParamType', + 'ignored_issues' => [], + 'php_version' => '8.1' + ], + ]; + } +} From 299eca4daa08664cdd4af8127b3934663d974f63 Mon Sep 17 00:00:00 2001 From: Pete Walker Date: Fri, 21 Jan 2022 17:31:48 +0000 Subject: [PATCH 2/6] fix: Add PHP version checks / more tests --- .../Reflector/ClassLikeNodeScanner.php | 17 +-- .../Reflector/FunctionLikeNodeScanner.php | 8 ++ .../PhpVisitor/Reflector/TypeHintResolver.php | 35 ++++++ tests/NativeIntersectionsTest.php | 42 +++++++ tests/NativeUnionsTest.php | 119 ++++++++++++++++++ 5 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 tests/NativeUnionsTest.php diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 4ae30b5f4db..4c77a4ae1be 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1488,22 +1488,23 @@ private function visitPropertyDeclaration( $parser_property_type = $stmt->type; /** @var Identifier|IntersectionType|Name|NullableType|UnionType $parser_property_type */ + $signature_type_location = new CodeLocation( + $this->file_scanner, + $parser_property_type, + null, + 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 ); - - $signature_type_location = new CodeLocation( - $this->file_scanner, - $parser_property_type, - null, - false, - CodeLocation::FUNCTION_RETURN_TYPE - ); } $doc_var_group_type = $var_comment->type ?? null; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 077445c09e8..b67940138ef 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -430,6 +430,10 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $storage->return_type = TypeHintResolver::resolve( $original_type, + new CodeLocation( + $this->file_scanner, + $original_type + ), $this->codebase, $this->file_storage, $this->classlike_storage, @@ -825,6 +829,10 @@ private function getTranslatedFunctionParam( $param_type = TypeHintResolver::resolve( $param_typehint, + new CodeLocation( + $this->file_scanner, + $param_typehint + ), $this->codebase, $this->file_storage, $this->classlike_storage, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index f11b97a87c9..8293f70b315 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -10,7 +10,10 @@ use PhpParser\Node\UnionType; use Psalm\Aliases; use Psalm\Codebase; +use Psalm\CodeLocation; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; +use Psalm\Issue\ParseError; +use Psalm\IssueBuffer; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FileStorage; use Psalm\Type; @@ -31,6 +34,7 @@ class TypeHintResolver */ public static function resolve( PhpParser\NodeAbstract $hint, + CodeLocation $code_location, Codebase $codebase, FileStorage $file_storage, ?ClassLikeStorage $classlike_storage, @@ -44,9 +48,19 @@ public static function resolve( throw new UnexpectedValueException('bad'); } + if ($analysis_php_version_id < 8_00_00) { + IssueBuffer::add( + new ParseError( + 'Union types are not supported in PHP < 8', + $code_location + ) + ); + } + foreach ($hint->types as $atomic_typehint) { $resolved_type = self::resolve( $atomic_typehint, + $code_location, $codebase, $file_storage, $classlike_storage, @@ -67,9 +81,20 @@ public static function resolve( throw new UnexpectedValueException('bad'); } + if ($analysis_php_version_id < 8_10_00) { + IssueBuffer::add( + new ParseError( + 'Intersection types are not supported in PHP < 8.1', + $code_location + ) + ); + return false; + } + foreach ($hint->types as $atomic_typehint) { $resolved_type = self::resolve( $atomic_typehint, + $code_location, $codebase, $file_storage, $classlike_storage, @@ -77,6 +102,16 @@ public static function resolve( $analysis_php_version_id ); + if ($resolved_type->hasScalarType()) { + IssueBuffer::add( + new ParseError( + 'Intersection types cannot contain scalar types', + $code_location + ) + ); + return null; + } + $type = Type::intersectUnionTypes($resolved_type, $type, $codebase); } diff --git a/tests/NativeIntersectionsTest.php b/tests/NativeIntersectionsTest.php index 443cf2ba329..f16f57fe51f 100644 --- a/tests/NativeIntersectionsTest.php +++ b/tests/NativeIntersectionsTest.php @@ -103,6 +103,48 @@ function test(A&B $in): void { 'ignored_issues' => [], 'php_version' => '8.1' ], + 'intersectionsNotAllowedWithUnions' => [ + ' 'ParseError', + [], + false, + '8.1' + ], + 'intersectionsNotAllowedWithNonClasses' => [ + ' 'ParseError', + [], + false, + '8.1' + ], + 'intersectionsNotAllowedInPHP80' => [ + ' 'ParseError', + [], + false, + '8.0' + ], ]; } } diff --git a/tests/NativeUnionsTest.php b/tests/NativeUnionsTest.php new file mode 100644 index 00000000000..6ee8ef7129c --- /dev/null +++ b/tests/NativeUnionsTest.php @@ -0,0 +1,119 @@ +,error_levels?:string[],php_version?:string}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'nativeTypeUnionInConstructor' => [ + 'self; + } + }', + 'assertions' => [], + 'error_levels' => [], + 'php_version' => '8.0' + ], + 'nativeTypeUnionAsArgument' => [ + 'foo(); + } + test(new C()); + ', + 'assertions' => [], + 'error_levels' => [], + 'php_version' => '8.0' + ], + 'unionAndNullableEquivalent' => [ + ' [], + 'error_levels' => [], + 'php_version' => '8.0' + ], + ]; + } + + /** + * @return iterable + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'invalidNativeUnionArgument' => [ + ' 'InvalidScalarArgument', + [], + false, + '8.0' + ], + 'mismatchDocblockNativeUnionArgument' => [ + ' 'MismatchingDocblockParamType', + [], + false, + '8.0' + ], + 'unionsNotAllowedInPHP74' => [ + ' 'ParseError', + [], + false, + '7.4' + ], + ]; + } +} From e2ebfd2ce3740eb3467b96bf7ed515a912ebaaab Mon Sep 17 00:00:00 2001 From: Pete Walker Date: Fri, 21 Jan 2022 17:35:21 +0000 Subject: [PATCH 3/6] Trigger CI From 41a3807693ab5fc2fd978927ca2147a41080def3 Mon Sep 17 00:00:00 2001 From: Pete Walker Date: Fri, 21 Jan 2022 17:39:01 +0000 Subject: [PATCH 4/6] fix: Lint issue --- src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 8293f70b315..4772542d3ff 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -9,8 +9,8 @@ use PhpParser\Node\NullableType; use PhpParser\Node\UnionType; use Psalm\Aliases; -use Psalm\Codebase; use Psalm\CodeLocation; +use Psalm\Codebase; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Issue\ParseError; use Psalm\IssueBuffer; From 057a99c9f12a46ab1bf11ab5771f777ef1fcef72 Mon Sep 17 00:00:00 2001 From: Pete Walker Date: Sat, 22 Jan 2022 00:00:45 +0000 Subject: [PATCH 5/6] fix: Tests / minor bug --- .../PhpVisitor/Reflector/TypeHintResolver.php | 10 +++--- tests/BinaryOperationTest.php | 5 ++- tests/InterfaceTest.php | 7 +++-- tests/NativeIntersectionsTest.php | 23 ++++++-------- tests/NativeUnionsTest.php | 31 +++++++++---------- tests/PropertyTypeTest.php | 7 +++-- tests/Template/ClassTemplateExtendsTest.php | 4 ++- tests/Template/ClassTemplateTest.php | 11 ++++++- .../AssignmentInConditionalTest.php | 2 ++ .../RedundantConditionTest.php | 12 +++++-- tests/TypeReconciliation/TypeAlgebraTest.php | 7 +++-- tests/UnusedVariableTest.php | 13 +++++--- 12 files changed, 80 insertions(+), 52 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 4772542d3ff..45a2f0f1d12 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -49,7 +49,7 @@ public static function resolve( } if ($analysis_php_version_id < 8_00_00) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Union types are not supported in PHP < 8', $code_location @@ -81,14 +81,13 @@ public static function resolve( throw new UnexpectedValueException('bad'); } - if ($analysis_php_version_id < 8_10_00) { - IssueBuffer::add( + if ($analysis_php_version_id < 8_01_00) { + IssueBuffer::maybeAdd( new ParseError( 'Intersection types are not supported in PHP < 8.1', $code_location ) ); - return false; } foreach ($hint->types as $atomic_typehint) { @@ -103,13 +102,12 @@ public static function resolve( ); if ($resolved_type->hasScalarType()) { - IssueBuffer::add( + IssueBuffer::maybeAdd( new ParseError( 'Intersection types cannot contain scalar types', $code_location ) ); - return null; } $type = Type::intersectUnionTypes($resolved_type, $type, $codebase); diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 63af0a13a53..2f2e37f197d 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -260,7 +260,7 @@ function takesI(I $i): void } /** - * @return iterable,ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -818,6 +818,9 @@ function scope(array $a): int|float { return 0; } }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'NumericStringIncrementLiteral' => [ 'code' => ',ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -719,7 +719,10 @@ function takesAorB(SomeClass|SomeInterface $some): void { if ($some instanceof SomeInterface) { $some->doStuff(); } - }' + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], ]; } diff --git a/tests/NativeIntersectionsTest.php b/tests/NativeIntersectionsTest.php index f16f57fe51f..d64587fc4ae 100644 --- a/tests/NativeIntersectionsTest.php +++ b/tests/NativeIntersectionsTest.php @@ -11,7 +11,7 @@ class NativeIntersectionsTest extends TestCase use ValidCodeAnalysisTestTrait; /** - * @return iterable,ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -104,7 +104,7 @@ function test(A&B $in): void { 'php_version' => '8.1' ], 'intersectionsNotAllowedWithUnions' => [ - ' ' 'ParseError', - [], - false, - '8.1' + 'ignored_issues' => [], + 'php_version' => '8.1' ], 'intersectionsNotAllowedWithNonClasses' => [ - ' ' 'ParseError', - [], - false, - '8.1' + 'ignored_issues' => [], + 'php_version' => '8.1' ], 'intersectionsNotAllowedInPHP80' => [ - ' ' 'ParseError', - [], - false, - '8.0' + 'ignored_issues' => [], + 'php_version' => '8.0' ], ]; } diff --git a/tests/NativeUnionsTest.php b/tests/NativeUnionsTest.php index 6ee8ef7129c..679c6d6bf51 100644 --- a/tests/NativeUnionsTest.php +++ b/tests/NativeUnionsTest.php @@ -11,13 +11,13 @@ class NativeUnionsTest extends TestCase use ValidCodeAnalysisTestTrait; /** - * @return iterable,error_levels?:string[],php_version?:string}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { return [ 'nativeTypeUnionInConstructor' => [ - ' ' '8.0' ], 'nativeTypeUnionAsArgument' => [ - ' ' '8.0' ], 'unionAndNullableEquivalent' => [ - ' ' + * @return iterable,php_version?:string}> */ public function providerInvalidCodeParse(): iterable { return [ 'invalidNativeUnionArgument' => [ - ' ' 'InvalidScalarArgument', - [], - false, - '8.0' + 'ignored_issues' => [], + 'php_version' => '8.0' ], 'mismatchDocblockNativeUnionArgument' => [ - ' ' 'MismatchingDocblockParamType', - [], - false, - '8.0' + 'ignored_issues' => [], + 'php_version' => '8.0' ], 'unionsNotAllowedInPHP74' => [ - ' ' 'ParseError', - [], - false, - '7.4' + 'ignored_issues' => [], + 'php_version' => '7.4' ], ]; } diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index fed1037eccd..8154402bd8c 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -346,7 +346,7 @@ public function getX(bool $b): int { } /** - * @return iterable,ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -2157,7 +2157,10 @@ public function __construct() { $a = new A(); if ($a->i === 3) {} - if ($a->i === "foo") {}' + if ($a->i === "foo") {}', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'setClassStringOfStatic' => [ 'code' => ',ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -4545,6 +4545,8 @@ class Secondary {} '$c3' => 'c3', '$resultC3' => 'RealE|Secondary', ], + 'ignored_issues' => [], + 'php_version' => '8.0', ], ]; } diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 96ef16745ca..4c058f6eb43 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -14,7 +14,7 @@ class ClassTemplateTest extends TestCase use ValidCodeAnalysisTestTrait; /** - * @return iterable,ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -3490,6 +3490,9 @@ class Dog {} function foo(Collection $c, Collection $d): object { return rand(0, 1) ? $c->get() : $d->get(); }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'allowCovariantBoundsMismatchDifferentContainers' => [ 'code' => 'get() : $d->get(); }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'allowCovariantBoundsMismatchContainerAndObject' => [ 'code' => 'get(); }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'allowCompatibleGenerics' => [ 'code' => ' 'InvalidReturnStatement', + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'assignmentInBranchOfAndReferencedAfterIf' => [ 'code' => ',ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -819,7 +819,10 @@ function bar(string $f) : void { function normalizeValue(bool|int|float|string $value): void { assert(is_string($value)); - }' + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'NumericCanBeFalsy' => [ 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'NumericCanBeNotIntOrNotFloat' => [ 'code' => ',ignored_issues?:list}> + * @return iterable,ignored_issues?:list,php_version?:string}> */ public function providerValidCodeParse(): iterable { @@ -1037,7 +1037,10 @@ function foo(?B $b, ?C $c): B|C { } return $c; - }' + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', ], 'dependentType' => [ 'code' => 'project_analyzer->setPhpVersion('7.4', 'tests'); $this->project_analyzer->getCodebase()->reportUnusedVariables(); } @@ -46,13 +45,15 @@ public function setUp(): void * @param array $error_levels * */ - public function testValidCode($code, array $error_levels = []): void + public function testValidCode($code, array $error_levels = [], string $php_version = '7.4'): void { $test_name = $this->getTestName(); if (strpos($test_name, 'SKIPPED-') !== false) { $this->markTestSkipped('Skipped due to a bug.'); } + $this->project_analyzer->setPhpVersion($php_version, 'tests'); + $file_path = self::$src_dir_path . 'somefile.php'; $this->addFile( @@ -84,6 +85,8 @@ public function testInvalidCode($code, $error_message, $error_levels = []): void $this->expectException(CodeException::class); $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/'); + $this->project_analyzer->setPhpVersion('7.4', 'tests'); + $file_path = self::$src_dir_path . 'somefile.php'; foreach ($error_levels as $error_level) { @@ -99,7 +102,7 @@ public function testInvalidCode($code, $error_message, $error_levels = []): void } /** - * @return array}> + * @return array,php_version?:string}> */ public function providerValidCodeParse(): array { @@ -2371,7 +2374,9 @@ function bar(): int { return 2; } } - }' + }', + 'error_levels' => [], + 'php_version' => '8.0', ], 'concatWithUnknownProperty' => [ 'code' => ' Date: Sat, 22 Jan 2022 10:12:48 +0000 Subject: [PATCH 6/6] chore: Better exception messages --- .../Internal/PhpVisitor/Reflector/TypeHintResolver.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 45a2f0f1d12..9265d3c8b15 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -45,7 +45,7 @@ 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) { @@ -78,7 +78,7 @@ public static function resolve( $type = null; if (!$hint->types) { - throw new UnexpectedValueException('bad'); + throw new UnexpectedValueException('Intersection type should not be empty'); } if ($analysis_php_version_id < 8_01_00) { @@ -114,7 +114,7 @@ public static function resolve( } if ($type === null) { - throw new UnexpectedValueException('bad'); + throw new UnexpectedValueException('Intersection type could not be resolved'); } return $type;