Skip to content

Commit

Permalink
fix: Add PHP version checks / more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
petewalker committed Jan 21, 2022
1 parent a3bdf1b commit 299eca4
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 8 deletions.
17 changes: 9 additions & 8 deletions src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
Expand Up @@ -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;
Expand Down
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php
Expand Up @@ -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;
Expand All @@ -31,6 +34,7 @@ class TypeHintResolver
*/
public static function resolve(
PhpParser\NodeAbstract $hint,
CodeLocation $code_location,
Codebase $codebase,
FileStorage $file_storage,
?ClassLikeStorage $classlike_storage,
Expand All @@ -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,
Expand All @@ -67,16 +81,37 @@ 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,
$aliases,
$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);
}

Expand Down
42 changes: 42 additions & 0 deletions tests/NativeIntersectionsTest.php
Expand Up @@ -103,6 +103,48 @@ function test(A&B $in): void {
'ignored_issues' => [],
'php_version' => '8.1'
],
'intersectionsNotAllowedWithUnions' => [
'<?php
interface A {
}
interface B {
}
interface C {
}
function foo (A&B|C $test): A&B|C {
return $test;
}',
'error_message' => 'ParseError',
[],
false,
'8.1'
],
'intersectionsNotAllowedWithNonClasses' => [
'<?php
interface A {
}
function foo (A&string $test): A&string {
return $test;
}',
'error_message' => 'ParseError',
[],
false,
'8.1'
],
'intersectionsNotAllowedInPHP80' => [
'<?php
interface A {
}
interface B {
}
function foo (A&B $test): A&B {
return $test;
}',
'error_message' => 'ParseError',
[],
false,
'8.0'
],
];
}
}
119 changes: 119 additions & 0 deletions tests/NativeUnionsTest.php
@@ -0,0 +1,119 @@
<?php

namespace Psalm\Tests;

use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;

class NativeUnionsTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;

/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[],php_version?:string}>
*/
public function providerValidCodeParse(): iterable
{
return [
'nativeTypeUnionInConstructor' => [
'<?php
interface A {
}
interface B {
}
class Foo {
public function __construct(private A|B $self) {}
public function self(): A|B
{
return $this->self;
}
}',
'assertions' => [],
'error_levels' => [],
'php_version' => '8.0'
],
'nativeTypeUnionAsArgument' => [
'<?php
interface A {
function foo(): void;
}
interface B {
function foo(): void;
}
class C implements A {
function foo(): void {
}
}
function test(A|B $in): void {
$in->foo();
}
test(new C());
',
'assertions' => [],
'error_levels' => [],
'php_version' => '8.0'
],
'unionAndNullableEquivalent' => [
'<?php
function test(string|null $in): ?string {
return $in;
}
',
'assertions' => [],
'error_levels' => [],
'php_version' => '8.0'
],
];
}

/**
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'invalidNativeUnionArgument' => [
'<?php
function test(string|null $in): string|null {
return $in;
}
test(2);
',
'error_message' => 'InvalidScalarArgument',
[],
false,
'8.0'
],
'mismatchDocblockNativeUnionArgument' => [
'<?php
/**
* @param string|null $in
*/
function test(int|bool $in): bool {
return !!$in;
}
',
'error_message' => 'MismatchingDocblockParamType',
[],
false,
'8.0'
],
'unionsNotAllowedInPHP74' => [
'<?php
interface A {
}
interface B {
}
function foo (A|B $test): A&B {
return $test;
}',
'error_message' => 'ParseError',
[],
false,
'7.4'
],
];
}
}

0 comments on commit 299eca4

Please sign in to comment.