Skip to content

Commit

Permalink
Merge pull request #604 from PHPCSStandards/php-8.2/add-support-for-d…
Browse files Browse the repository at this point in the history
…nf-types

PHP 8.2: add support for DNF types
  • Loading branch information
jrfnl authored May 20, 2024
2 parents 4b29bc0 + a631970 commit f6b22f6
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 69 deletions.
6 changes: 5 additions & 1 deletion PHPCSUtils/BackCompat/BCFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ public static function getMethodParameters(File $phpcsFile, $stackPtr)
// it's likely to be an array which might have arguments in it. This
// could cause problems in our parsing below, so lets just skip to the
// end of it.
if (isset($tokens[$i]['parenthesis_opener']) === true) {
if ($tokens[$i]['code'] !== T_TYPE_OPEN_PARENTHESIS
&& isset($tokens[$i]['parenthesis_opener']) === true
) {
// Don't do this if it's the close parenthesis for the method.
if ($i !== $tokens[$i]['parenthesis_closer']) {
$i = ($tokens[$i]['parenthesis_closer'] + 1);
Expand Down Expand Up @@ -324,6 +326,8 @@ public static function getMethodParameters(File $phpcsFile, $stackPtr)
case T_NS_SEPARATOR:
case T_TYPE_UNION:
case T_TYPE_INTERSECTION:
case T_TYPE_OPEN_PARENTHESIS:
case T_TYPE_CLOSE_PARENTHESIS:
case T_FALSE:
case T_TRUE:
case T_NULL:
Expand Down
50 changes: 28 additions & 22 deletions PHPCSUtils/Tokens/Collections.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,14 +400,16 @@ final class Collections
* @var array<int|string, int|string>
*/
private static $parameterTypeTokens = [
\T_CALLABLE => \T_CALLABLE,
\T_SELF => \T_SELF,
\T_PARENT => \T_PARENT,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_NULL => \T_NULL,
\T_TYPE_UNION => \T_TYPE_UNION,
\T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION,
\T_CALLABLE => \T_CALLABLE,
\T_SELF => \T_SELF,
\T_PARENT => \T_PARENT,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_NULL => \T_NULL,
\T_TYPE_UNION => \T_TYPE_UNION,
\T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION,
\T_TYPE_OPEN_PARENTHESIS => \T_TYPE_OPEN_PARENTHESIS,
\T_TYPE_CLOSE_PARENTHESIS => \T_TYPE_CLOSE_PARENTHESIS,
];

/**
Expand Down Expand Up @@ -446,14 +448,16 @@ final class Collections
* @var array<int|string, int|string>
*/
private static $propertyTypeTokens = [
\T_CALLABLE => \T_CALLABLE, // Not allowed in PHP, but in this list to allow for (flagging) code errors.
\T_SELF => \T_SELF,
\T_PARENT => \T_PARENT,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_NULL => \T_NULL,
\T_TYPE_UNION => \T_TYPE_UNION,
\T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION,
\T_CALLABLE => \T_CALLABLE, // Not allowed in PHP, but in this list to allow for flagging code errors.
\T_SELF => \T_SELF,
\T_PARENT => \T_PARENT,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_NULL => \T_NULL,
\T_TYPE_UNION => \T_TYPE_UNION,
\T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION,
\T_TYPE_OPEN_PARENTHESIS => \T_TYPE_OPEN_PARENTHESIS,
\T_TYPE_CLOSE_PARENTHESIS => \T_TYPE_CLOSE_PARENTHESIS,
];

/**
Expand All @@ -464,12 +468,14 @@ final class Collections
* @var array<int|string, int|string>
*/
private static $returnTypeTokens = [
\T_CALLABLE => \T_CALLABLE,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_NULL => \T_NULL,
\T_TYPE_UNION => \T_TYPE_UNION,
\T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION,
\T_CALLABLE => \T_CALLABLE,
\T_FALSE => \T_FALSE,
\T_TRUE => \T_TRUE,
\T_NULL => \T_NULL,
\T_TYPE_UNION => \T_TYPE_UNION,
\T_TYPE_INTERSECTION => \T_TYPE_INTERSECTION,
\T_TYPE_OPEN_PARENTHESIS => \T_TYPE_OPEN_PARENTHESIS,
\T_TYPE_CLOSE_PARENTHESIS => \T_TYPE_CLOSE_PARENTHESIS,
];

/**
Expand Down
15 changes: 15 additions & 0 deletions Tests/BackCompat/BCFile/GetMemberPropertiesTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -339,3 +339,18 @@ class WhitespaceAndCommentsInTypes {
/* testIntersectionTypeWithWhitespaceAndComment */
public \Foo /*comment*/ & Bar $hasWhitespaceAndComment;
}

trait DNFTypes {
/* testPHP82DNFTypeStatic */
public static (Foo&\Bar)|bool $propA;

/* testPHP82DNFTypeReadonlyA */
protected readonly float|(Partially\Qualified&Traversable) $propB;

/* testPHP82DNFTypeReadonlyB */
private readonly (namespace\Foo&Bar)|string $propC;

/* testPHP82DNFTypeIllegalNullable */
// Intentional fatal error - nullable operator cannot be combined with DNF.
var ?(A&\Pck\B)|bool $propD;
}
53 changes: 53 additions & 0 deletions Tests/BackCompat/BCFile/GetMemberPropertiesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,59 @@ public static function dataGetMemberProperties()
'nullable_type' => false,
],
],

'php8.2-dnf-with-static' => [
'identifier' => '/* testPHP82DNFTypeStatic */',
'expected' => [
'scope' => 'public',
'scope_specified' => true,
'is_static' => true,
'is_readonly' => false,
'type' => '(Foo&\Bar)|bool',
'type_token' => ($php8Names === true) ? -8 : -9,
'type_end_token' => -2,
'nullable_type' => false,
],
],
'php8.2-dnf-with-readonly-1' => [
'identifier' => '/* testPHP82DNFTypeReadonlyA */',
'expected' => [
'scope' => 'protected',
'scope_specified' => true,
'is_static' => false,
'is_readonly' => true,
'type' => 'float|(Partially\Qualified&Traversable)',
'type_token' => ($php8Names === true) ? -8 : -10,
'type_end_token' => -2,
'nullable_type' => false,
],
],
'php8.2-dnf-with-readonly-2' => [
'identifier' => '/* testPHP82DNFTypeReadonlyB */',
'expected' => [
'scope' => 'private',
'scope_specified' => true,
'is_static' => false,
'is_readonly' => true,
'type' => '(namespace\Foo&Bar)|string',
'type_token' => ($php8Names === true) ? -8 : -10,
'type_end_token' => -2,
'nullable_type' => false,
],
],
'php8.2-dnf-with-illegal-nullable' => [
'identifier' => '/* testPHP82DNFTypeIllegalNullable */',
'expected' => [
'scope' => 'public',
'scope_specified' => false,
'is_static' => false,
'is_readonly' => false,
'type' => '?(A&\Pck\B)|bool',
'type_token' => ($php8Names === true) ? -8 : -11,
'type_end_token' => -2,
'nullable_type' => true,
],
],
];
}

Expand Down
17 changes: 17 additions & 0 deletions Tests/BackCompat/BCFile/GetMethodParametersTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,23 @@ function newInInitializers(
\Package\TypeB $newToo = new \Package\TypeB(10, 'string'),
) {}

/* testPHP82DNFTypes */
function dnfTypes(
#[MyAttribute]
false|(Foo&Bar)|true $obj1,
(\Boo&\Pck\Bar)|(Boo&Baz) $obj2 = new Boo()
) {}

/* testPHP82DNFTypesWithSpreadOperatorAndReference */
function dnfInGlobalFunctionWithSpreadAndReference((Countable&MeMe)|iterable &$paramA, true|(Foo&Bar) ...$paramB) {}

/* testPHP82DNFTypesIllegalNullable */
// Intentional fatal error - nullable operator cannot be combined with DNF.
$dnf_closure = function (? ( MyClassA & /*comment*/ \Package\MyClassB & \Package\MyClassC ) $var): void {};

/* testPHP82DNFTypesInArrow */
$dnf_arrow = fn((Hi&Ho)|FALSE &...$range): string => $a;

/* testFunctionCallFnPHPCS353-354 */
$value = $obj->fn(true);

Expand Down
151 changes: 151 additions & 0 deletions Tests/BackCompat/BCFile/GetMethodParametersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2601,6 +2601,157 @@ public function testPHP81NewInInitializers()
$this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected);
}

/**
* Verify recognition of 8.2 DNF parameter type declarations.
*
* @return void
*/
public function testPHP82DNFTypes()
{
$php8Names = parent::usesPhp8NameTokens();

// Offsets are relative to the T_FUNCTION token.
$expected = [];
$expected[0] = [
'token' => 21,
'name' => '$obj1',
'content' => '#[MyAttribute]
false|(Foo&Bar)|true $obj1',
'has_attributes' => true,
'pass_by_reference' => false,
'reference_token' => false,
'variable_length' => false,
'variadic_token' => false,
'type_hint' => 'false|(Foo&Bar)|true',
'type_hint_token' => 11,
'type_hint_end_token' => 19,
'nullable_type' => false,
'comma_token' => 22,
];
$expected[1] = [
'token' => ($php8Names === true) ? 37 : 41,
'name' => '$obj2',
'content' => '(\Boo&\Pck\Bar)|(Boo&Baz) $obj2 = new Boo()',
'default' => 'new Boo()',
'default_token' => ($php8Names === true) ? 41 : 45,
'default_equal_token' => ($php8Names === true) ? 39 : 43,
'has_attributes' => false,
'pass_by_reference' => false,
'reference_token' => false,
'variable_length' => false,
'variadic_token' => false,
'type_hint' => '(\Boo&\Pck\Bar)|(Boo&Baz)',
'type_hint_token' => 25,
'type_hint_end_token' => ($php8Names === true) ? 35 : 39,
'nullable_type' => false,
'comma_token' => false,
];

$this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected);
}

/**
* Verify recognition of PHP 8.2 DNF parameter type declarations when the variable
* has either a spread operator or a reference.
*
* @return void
*/
public function testPHP82DNFTypesWithSpreadOperatorAndReference()
{
// Offsets are relative to the T_FUNCTION token.
$expected = [];
$expected[0] = [
'token' => 13,
'name' => '$paramA',
'content' => '(Countable&MeMe)|iterable &$paramA',
'has_attributes' => false,
'pass_by_reference' => true,
'reference_token' => 12,
'variable_length' => false,
'variadic_token' => false,
'type_hint' => '(Countable&MeMe)|iterable',
'type_hint_token' => 4,
'type_hint_end_token' => 10,
'nullable_type' => false,
'comma_token' => 14,
];
$expected[1] = [
'token' => 25,
'name' => '$paramB',
'content' => 'true|(Foo&Bar) ...$paramB',
'has_attributes' => false,
'pass_by_reference' => false,
'reference_token' => false,
'variable_length' => true,
'variadic_token' => 24,
'type_hint' => 'true|(Foo&Bar)',
'type_hint_token' => 16,
'type_hint_end_token' => 22,
'nullable_type' => false,
'comma_token' => false,
];

$this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected);
}

/**
* Verify recognition of PHP 8.2 DNF parameter type declarations using the nullability operator (not allowed).
*
* @return void
*/
public function testPHP82DNFTypesIllegalNullable()
{
$php8Names = parent::usesPhp8NameTokens();

// Offsets are relative to the T_FUNCTION token.
$expected = [];
$expected[0] = [
'token' => ($php8Names === true) ? 21 : 27,
'name' => '$var',
'content' => '? ( MyClassA & /*comment*/ \Package\MyClassB & \Package\MyClassC ) $var',
'has_attributes' => false,
'pass_by_reference' => false,
'reference_token' => false,
'variable_length' => false,
'variadic_token' => false,
'type_hint' => '?(MyClassA&\Package\MyClassB&\Package\MyClassC)',
'type_hint_token' => 5,
'type_hint_end_token' => ($php8Names === true) ? 19 : 25,
'nullable_type' => true,
'comma_token' => false,
];

$this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected);
}

/**
* Verify recognition of PHP 8.2 DNF parameter type declarations in an arrow function.
*
* @return void
*/
public function testPHP82DNFTypesInArrow()
{
// Offsets are relative to the T_FUNCTION token.
$expected = [];
$expected[0] = [
'token' => 12,
'name' => '$range',
'content' => '(Hi&Ho)|FALSE &...$range',
'has_attributes' => false,
'pass_by_reference' => true,
'reference_token' => 10,
'variable_length' => true,
'variadic_token' => 11,
'type_hint' => '(Hi&Ho)|FALSE',
'type_hint_token' => 2,
'type_hint_end_token' => 8,
'nullable_type' => false,
'comma_token' => false,
];

$this->getMethodParametersTestHelper('/* ' . __FUNCTION__ . ' */', $expected);
}

/**
* Verify handling of a closure.
*
Expand Down
19 changes: 19 additions & 0 deletions Tests/BackCompat/BCFile/GetMethodPropertiesTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,25 @@ function pseudoTypeTrue(): ?true {}
// Intentional fatal error - Type contains both true and false, bool should be used instead, but that's not the concern of the method.
function pseudoTypeFalseAndTrue(): true|false {}

/* testPHP82DNFType */
function hasDNFType() : bool|(Foo&Bar)|string {}

abstract class AbstractClass {
/* testPHP82DNFTypeAbstractMethod */
abstract protected function abstractMethodDNFType() : float|(Foo&Bar);
}

/* testPHP82DNFTypeIllegalNullable */
// Intentional fatal error - nullable operator cannot be combined with DNF.
function illegalNullableDNF(): ?(A&\Pck\B)|bool {}

/* testPHP82DNFTypeClosure */
$closure = function() : object|(namespace\Foo&Countable) {};

/* testPHP82DNFTypeFn */
// Intentional fatal error - void type cannot be combined with DNF.
$arrow = fn() : null|(Partially\Qualified&Traversable)|void => do_something();

/* testNotAFunction */
return true;

Expand Down
Loading

0 comments on commit f6b22f6

Please sign in to comment.