Skip to content

Commit

Permalink
Merge pull request #1714 from PHPCompatibility/php-8.2/detect-dnf-types
Browse files Browse the repository at this point in the history
PHP 8.2 | Detect DNF types in all type declaration related sniffs
  • Loading branch information
wimg committed Jun 7, 2024
2 parents c2858c4 + 4123a52 commit 5a0ad8d
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ To start contributing, fork the repository, create a new branch in your fork, ma

Please make sure that your pull request contains unit tests covering what's being addressed by it.

* All code should be compatible with PHPCS >= 3.9.0.
* All code should be compatible with PHPCS >= 3.10.0.
* All code should be compatible with PHP 5.4 to PHP nightly.
* All code should comply with the PHPCompatibility coding standards.
The [ruleset used by PHPCompatibility](https://github.com/PHPCSStandards/PHPCSDevCS) is largely based on PSR-12 with minor variations and some additional checks for array layout and documentation and such.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:

include:
- php: '7.2'
phpcs_version: '^3.9.0'
phpcs_version: '^3.10.0'
custom_ini: true
experimental: false

Expand Down
18 changes: 15 additions & 3 deletions PHPCompatibility/Sniffs/Classes/NewTypedPropertiesSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* - Since PHP 8.1, intersection types are supported for class/interface names.
* - Since PHP 8.2, `false` and `null` can be used as stand-alone types.
* - Since PHP 8.2, the `true` sub-type is available.
* - Since PHP 8.2, disjunctive normal form types are available.
*
* PHP version 7.4+
*
Expand All @@ -37,6 +38,7 @@
* @link https://wiki.php.net/rfc/pure-intersection-types
* @link https://wiki.php.net/rfc/null-false-standalone-types
* @link https://wiki.php.net/rfc/true-type
* @link https://wiki.php.net/rfc/dnf_types
*
* @since 9.2.0
*/
Expand Down Expand Up @@ -239,9 +241,10 @@ protected function checkType(File $phpcsFile, $typeToken, $typeInfo)
[$origType]
);
} else {
$types = \preg_split('`[|&]`', $type, -1, \PREG_SPLIT_NO_EMPTY);
$isUnionType = (\strpos($type, '|') !== false);
$isIntersectionType = (\strpos($type, '&') !== false);
$types = \preg_split('`[|&()]`', $type, -1, \PREG_SPLIT_NO_EMPTY);
$isUnionType = (\strpos($type, '|') !== false && \strpos($type, '(') === false);
$isIntersectionType = (\strpos($type, '&') !== false && \strpos($type, '(') === false);
$isDNFType = \strpos($type, '(') !== false;

if (ScannedCode::shouldRunOnOrBelow('7.4') === true && $isUnionType === true) {
$phpcsFile->addError(
Expand All @@ -261,6 +264,15 @@ protected function checkType(File $phpcsFile, $typeToken, $typeInfo)
);
}

if (ScannedCode::shouldRunOnOrBelow('8.1') === true && $isDNFType === true) {
$phpcsFile->addError(
'Disjunctive Normal Form types are not present in PHP version 8.1 or earlier. Found: %s',
$typeToken,
'DNFTypeFound',
[$origType]
);
}

foreach ($types as $type) {
if (isset($this->newTypes[$type])) {
$itemInfo = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
* - Since PHP 8.1, intersection types are supported for class/interface names.
* - Since PHP 8.2, `false` and `null` can be used as stand-alone types.
* - Since PHP 8.2, the `true` sub-type is available.
* - Since PHP 8.2, disjunctive normal form types are available.
*
* Additionally, this sniff does a cursory check for typical invalid type declarations,
* such as:
Expand All @@ -55,6 +56,7 @@
* @link https://wiki.php.net/rfc/pure-intersection-types
* @link https://wiki.php.net/rfc/null-false-standalone-types
* @link https://wiki.php.net/rfc/true-type
* @link https://wiki.php.net/rfc/dnf_types
*
* @since 7.0.0
* @since 7.1.0 Now extends the `AbstractNewFeatureSniff` instead of the base `Sniff` class.
Expand Down Expand Up @@ -232,9 +234,10 @@ public function process(File $phpcsFile, $stackPtr)
// Strip off potential nullable indication.
$typeHint = \ltrim($param['type_hint'], '?');
$typeHint = \strtolower($typeHint);
$types = \preg_split('`[|&]`', $typeHint, -1, \PREG_SPLIT_NO_EMPTY);
$isUnionType = (\strpos($typeHint, '|') !== false);
$isIntersectionType = (\strpos($typeHint, '&') !== false);
$types = \preg_split('`[|&()]`', $typeHint, -1, \PREG_SPLIT_NO_EMPTY);
$isUnionType = (\strpos($typeHint, '|') !== false && \strpos($typeHint, '(') === false);
$isIntersectionType = (\strpos($typeHint, '&') !== false && \strpos($typeHint, '(') === false);
$isDNFType = \strpos($typeHint, '(') !== false;

if ($supportsPHP7 === true && $isUnionType === true) {
$phpcsFile->addError(
Expand All @@ -254,6 +257,15 @@ public function process(File $phpcsFile, $stackPtr)
);
}

if ($supportsPHP81 === true && $isDNFType === true) {
$phpcsFile->addError(
'Disjunctive Normal Form types are not present in PHP version 8.1 or earlier. Found: %s',
$param['token'],
'DNFTypeFound',
[$param['type_hint']]
);
}

foreach ($types as $type) {
if (isset($this->newTypes[$type])) {
$itemInfo = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
* - Since PHP 8.1, intersection types are supported for class/interface names.
* - Since PHP 8.2, `false` and `null` can be used as stand-alone types.
* - Since PHP 8.2, the `true` sub-type is available.
* - Since PHP 8.2, disjunctive normal form types are available.
*
* PHP version 7.0+
*
Expand All @@ -46,6 +47,7 @@
* @link https://wiki.php.net/rfc/pure-intersection-types
* @link https://wiki.php.net/rfc/null-false-standalone-types
* @link https://wiki.php.net/rfc/true-type
* @link https://wiki.php.net/rfc/dnf_types
*
* @since 7.0.0
* @since 7.1.0 Now extends the `AbstractNewFeatureSniff` instead of the base `Sniff` class.
Expand Down Expand Up @@ -210,9 +212,10 @@ public function process(File $phpcsFile, $stackPtr)
$returnType = \ltrim($properties['return_type'], '?'); // Trim off potential nullability.
$returnType = \strtolower($returnType);
$returnTypeToken = $properties['return_type_token'];
$types = \preg_split('`[|&]`', $returnType, -1, \PREG_SPLIT_NO_EMPTY);
$isUnionType = (\strpos($returnType, '|') !== false);
$isIntersectionType = (\strpos($returnType, '&') !== false);
$types = \preg_split('`[|&()]`', $returnType, -1, \PREG_SPLIT_NO_EMPTY);
$isUnionType = (\strpos($returnType, '|') !== false && \strpos($returnType, '(') === false);
$isIntersectionType = (\strpos($returnType, '&') !== false && \strpos($returnType, '(') === false);
$isDNFType = \strpos($returnType, '(') !== false;

if (ScannedCode::shouldRunOnOrBelow('7.4') === true && $isUnionType === true) {
$phpcsFile->addError(
Expand All @@ -232,6 +235,15 @@ public function process(File $phpcsFile, $stackPtr)
);
}

if (ScannedCode::shouldRunOnOrBelow('8.1') === true && $isDNFType === true) {
$phpcsFile->addError(
'Disjunctive Normal Form types are not present in PHP version 8.1 or earlier. Found: %s',
$returnTypeToken,
'DNFTypeFound',
[$properties['return_type']]
);
}

foreach ($types as $type) {
if (isset($this->newTypes[$type]) === true) {
$itemInfo = [
Expand Down
15 changes: 15 additions & 0 deletions PHPCompatibility/Tests/Classes/NewTypedPropertiesUnitTest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,18 @@ class EdgeCase {
class ShouldBeDetected {
public int $property;
}

// PHP 8.2 DNF types.
class DNFTypes {
public (Foo&Bar)|null $dnf1;
public readonly (A&B)|(C&D) $dnf2;

// Intentional fatal error - &/| reversed, but that's not the concern of the sniff.
public B&(D|W) $illegalDnf;

// Intentional fatal error - segments are not unique, but that's not the concern of the sniff.
public (A&B)|(B&A) $duplicateNotAllowed;

// Intentional fatal error - segments which are strict subsets of others are disallowed, but that's not the concern of the sniff.
public (A&self)|A $nonSubsetNotAllowedPlusSelf;
}
45 changes: 45 additions & 0 deletions PHPCompatibility/Tests/Classes/NewTypedPropertiesUnitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ public static function dataNewTypedProperties()
[161],
[165],
[170],
[175],
[176],
[179],
[182],
[185],
];
}

Expand Down Expand Up @@ -231,6 +236,7 @@ public static function dataNewTypedPropertyTypes()
['false', '7.4', 99, '8.0'],
['mixed', '7.4', 116, '8.0', false],
['true', '8.1', 147, '8.2'],
['null', '7.4', 175, '8.2'],
];
}

Expand Down Expand Up @@ -338,6 +344,7 @@ public static function dataInvalidNonUnionNullFalseType()
return [
[93, 'null'],
[96, 'false'],
[175, 'null'],
];
}

Expand Down Expand Up @@ -413,6 +420,44 @@ public static function dataNewIntersectionTypes()
}


/**
* Verify that an error is thrown for DNF types.
*
* @dataProvider dataNewDNFTypes
*
* @param string $type The declared type.
* @param int $line The line number where the error is expected.
*
* @return void
*/
public function testNewDNFTypes($type, $line)
{
$file = $this->sniffFile(__FILE__, '8.1');
$this->assertError($file, $line, "Disjunctive Normal Form types are not present in PHP version 8.1 or earlier. Found: $type");

$file = $this->sniffFile(__FILE__, '8.2');
$this->assertNoViolation($file, $line);
}

/**
* Data provider.
*
* @see testNewDNFTypes()
*
* @return array
*/
public static function dataNewDNFTypes()
{
return [
['(Foo&Bar)|null', 175],
['(A&B)|(C&D)', 176],
['B&(D|W)', 179],
['(A&B)|(B&A)', 182],
['(A&self)|A', 185],
];
}


/*
* `testNoViolationsInFileOnValidVersion` test omitted as this sniff will also throw warnings/errors
* about invalid typed properties.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,19 @@ enum Suit implements Colorful
A&B $intersection,
): mixed {}
}

// PHP 8.2 DNF types.
function NOTDnfTypeParamDefault($param = (CONST_A & CONST_B) | CONST_C) {}

function DNFTypes((Foo&Bar)|null $a, int|(\A&\B) $b) {}
$closure = function($a, (A&B)|(C&D) $b) {};

// Intentional fatal error - &/| reversed, but that's not the concern of the sniff.
$arrow = fn(B&(D|W)|null $a) => $a;

// Intentional fatal error - segments are not unique, but that's not the concern of the sniff.
$arrow = fn((A&B)|(B&A) $a) => $a;

// Intentional fatal error - segments which are strict subsets of others are disallowed, but that's not the concern of the sniff.
// Intentional fatal error - using self type outside a class is invalid.
$arrow = fn((A&self)|A $a) => $a;
Loading

0 comments on commit 5a0ad8d

Please sign in to comment.