Skip to content

Commit

Permalink
Merge pull request #1699 from PHPCompatibility/php-8.4/removedoptiona…
Browse files Browse the repository at this point in the history
…lbeforerequired-update-for-implicitly-nullable

PHP 8.4 | FunctionDeclarations/RemovedOptionalBeforeRequiredParam: flag implicitly nullable parameters
  • Loading branch information
wimg committed Apr 7, 2024
2 parents 88e77df + 6d7802c commit 7cd518e
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function bar(string $a, <em>int $b = 0</em>) {}
// Exception: non-nullable typed parameters
// with a default value of `null` are still
// allowed.
// allowed prior to PHP 8.4.
function nullDefault(<em>Foo $a = null</em>, $b) {}
]]>
</code>
Expand All @@ -37,4 +37,24 @@ function typed(<em>string|null $a = null</em>, int $b) {}
]]>
</code>
</code_comparison>
<standard>
<![CDATA[
Declaring a function with an implicitly nullable, typed parameter before a required parameter, is deprecated since PHP 8.4.
Aside from making the parameter type explicitly nullable, the default value of `null` should be removed as well.
Alternatively, parameters after the implicitly nullable parameter could be made optional.
]]>
</standard>
<code_comparison>
<code title="Cross-version compatible: required parameter is declared as nullable and doesn't have a default value.">
<![CDATA[
function foo(<em>?Countable</em> $a, $b) {}
]]>
</code>
<code title="PHP &lt; 8.4: required parameter is not declared as explicitly nullable and has a null default value.">
<![CDATA[
function foo(<em>Countable</em> $a = <em>null</em>, $b) {}
]]>
</code>
</code_comparison>
</documentation>
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,18 @@
* While deprecated since PHP 8.0, optional parameters with an union type which includes null
* and a null default value, and found before a required parameter, are only flagged since PHP 8.3.
*
* And as of PHP 8.4, parameters of the form "Type $param = null" before a required parameter are
* now also deprecated.
*
* PHP version 8.0
* PHP version 8.1
* PHP version 8.3
* PHP version 8.4
*
* @link https://github.com/php/php-src/blob/69888c3ff1f2301ead8e37b23ff8481d475e29d2/UPGRADING#L145-L151
* @link https://github.com/php/php-src/commit/c939bd2f10b41bced49eb5bf12d48c3cf64f984a
* @link https://github.com/php/php-src/commit/68ef3938f42aefa3881c268b12b3c0f1ecc5888d
* @link https://wiki.php.net/rfc/deprecate-implicitly-nullable-types
*
* @since 10.0.0
*/
Expand Down Expand Up @@ -65,6 +70,13 @@ class RemovedOptionalBeforeRequiredParamSniff extends Sniff
*/
const PHP83_MSG = 'Declaring an optional parameter with a null stand-alone type or a union type including null before a required parameter is soft deprecated since PHP 8.0 and hard deprecated since PHP 8.3';

/**
* Base message for the PHP 8.4 deprecation.
*
* @var string
*/
const PHP84_MSG = 'Declaring an optional parameter with a non-nullable type and a null default value before a required parameter is deprecated since PHP 8.4';

/**
* Message template for detailed information about the deprecation.
*
Expand All @@ -73,7 +85,7 @@ class RemovedOptionalBeforeRequiredParamSniff extends Sniff
const MSG_DETAILS = ' Parameter %1$s is optional, while parameter %2$s is required. The %1$s parameter is implicitly treated as a required parameter.';

/**
* Tokens allowed in the default value.
* Tokens allowed in the default value (until PHP 8.4).
*
* This property will be enriched in the register() method.
*
Expand Down Expand Up @@ -161,15 +173,6 @@ public function process(File $phpcsFile, $stackPtr)
}
}

// Check if it's typed with a non-nullable type and has a null default value, in which case we can ignore it.
if ($param['type_hint'] !== ''
&& $param['nullable_type'] === false
&& $hasNullType === false
&& ($hasNull !== false && $hasNonNull === false)
) {
continue;
}

// Found an optional parameter with a required param after it.
$error = self::PHP80_MSG . self::MSG_DETAILS;
$code = 'Deprecated80';
Expand All @@ -178,7 +181,7 @@ public function process(File $phpcsFile, $stackPtr)
$requiredParam,
];

if ($hasNull !== false) {
if ($hasNull !== false && $hasNonNull === false) {
if ($param['nullable_type'] === true) {
// Skip flagging the issue if the codebase doesn't need to run on PHP 8.1+.
if (ScannedCode::shouldRunOnOrAbove('8.1') === false) {
Expand All @@ -196,6 +199,14 @@ public function process(File $phpcsFile, $stackPtr)

$error = self::PHP83_MSG . self::MSG_DETAILS;
$code = 'Deprecated83';
} elseif ($param['type_hint'] !== '') {
// Skip flagging the issue if the codebase doesn't need to run on PHP 8.4+.
if (ScannedCode::shouldRunOnOrAbove('8.4') === false) {
continue;
}

$error = self::PHP84_MSG . self::MSG_DETAILS;
$code = 'Deprecated84';
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/
function requiredBeforeOptional($a, $b, $c = null, $d = true) {}
function requiredBeforeOptionalWithTypes(?int $a, string $b, callable $c = NULL, bool $d = /*comment*/ true) {}
function requiredTypedWithNullDefaultBeforeRequired(Foo $a = /* comment */ null, int $b = null, $c, $d) {}


/*
* Deprecated in PHP 8.0.
Expand Down Expand Up @@ -64,8 +64,8 @@ function newInInitializers(
/*
* Deprecated in PHP 8.0, but only flagged as of PHP 8.1.
*/
function nonNullableTypedWithNullDefaultValueBeforeRequired(T1 $a, T2 $b = null, T3 $c) {} // This is fine.
$nullableTypedWithNullDefaultValueBeforeOptional = function (T1 $a, ?T2 $b = null, T3 $c = null) {}; // This is fine.
function nonNullableTypedWithNullDefaultValueBeforeRequired(T1 $a, T2 $b = null, T3 $c) {} // This is fine until PHP 8.4.
$nullableTypedWithNullDefaultValueBeforeOptional = function (T1 $a, ?T2 $b = null, T3 $c = null) {}; // This is fine, even on PHP 8.4.

// Deprecated as of PHP 8.0, but only emits a deprecation notice in PHP itself as of PHP 8.1.
function nullableTypedOptionalBeforeRequired(Okay $a, ?NotOkay $b = /* comment */ null, Required $c) {}
Expand All @@ -78,9 +78,9 @@ class ConstructorPropertyPromotionWithNullableType {
/*
* Deprecated in PHP 8.0, but only flagged as of PHP 8.3.
*/
function nullLastUnionTypedWithNullDefaultValueBeforeOptional(T1 $a, T2|null $b = null, T3 $c = null) {} // This is fine.
$nullFirstUnionTypedWithNullDefaultValueBeforeOptional = fn(T1 $a, null|T2 $b = null, T3 $c = null) => dosomething(); // This is fine.
$nonNullableIntersectionTypeWithNullDefaultBeforeRequired = function ( Foo&Bar $c = null, $d) {}; // This is fine.
function nullLastUnionTypedWithNullDefaultValueBeforeOptional(T1 $a, T2|null $b = null, T3 $c = null) {} // This is fine, even on PHP 8.4.
$nullFirstUnionTypedWithNullDefaultValueBeforeOptional = fn(T1 $a, null|T2 $b = null, T3 $c = null) => dosomething(); // This is fine, even on PHP 8.4.
$nonNullableIntersectionTypeWithNullDefaultBeforeRequired = function ( Foo&Bar $c = null, $d) {}; // This is fine until PHP 8.4.

// Deprecated as of PHP 8.0, but only emits a deprecation notice in PHP itself as of PHP 8.3.
function nullLastUnionTypedWithNullDefaultValueBeforeRequired(Okay $a, NotOkay|null $b = null, Required $c) {}
Expand All @@ -95,13 +95,36 @@ class ConstructorPropertyPromotionWithUnionTypedWithNull {
public function __construct(public null|NotOkay $prop = null, $b) {}
}

/*
* Deprecated in PHP 8.4.
*/
// These should still be fine, even on PHP 8.4.
function untypedWithDefaultValue($a = null, $b = true) {} // = mixed type.
$explicitlyNullableWithDefaultValue = function (?T $var = null) {};
function explicitlyNullableUnionWithDefaultValue(T|null $var = null, int $var2 = 10) {}
$explicitNullableTypeNull = fn (null $var = null): Type => new Type;
// Will trigger implicitly nullable deprecation for $call, but that's not the concern of this sniff.
function explicitNullableTypeMixed(mixed $var = null, callable $call = null) {}

// Deprecated as of PHP 8.4.
function typedWithNullDefaultBeforeRequired(
Foo $a = /* comment */ null,
int $b = null,
$c,
$d,
) {} // Deprecated x2.
function implicitNullableTypeClass(T $var = null, $c) {}
$implicitNullableTypeString = function (string $var = null, $c) {};
function implicitNullableTypeUnion(string|int $var = null, $c) {}

// Combination: contains parameters for all deprecations.
function combinationOfAllDeprecations(
Okay $a,
NotOkay|null $b = null, // PHP 8.3 deprecation.
?NotOkay $c = null, // PHP 8.1 deprecation.
int $d = 10, // PHP 8.0 deprecation.
Required $e,
ShouldBeNullable $b = null, // PHP 8.4 deprecation.
NotOkay|null $c = null, // PHP 8.3 deprecation.
?NotOkay $d = null, // PHP 8.1 deprecation.
int $e = 10, // PHP 8.0 deprecation.
Required $f,
) {}

// Intentional parse error. This has to be the last test in the file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ class RemovedOptionalBeforeRequiredParamUnitTest extends BaseSniffTestCase
*/
const PHP83_MSG = 'Declaring an optional parameter with a null stand-alone type or a union type including null before a required parameter is soft deprecated since PHP 8.0 and hard deprecated since PHP 8.3';

/**
* Base message for the PHP 8.4 deprecation.
*
* @var string
*/
const PHP84_MSG = 'Declaring an optional parameter with a non-nullable type and a null default value before a required parameter is deprecated since PHP 8.4';

/**
* Verify that the sniff throws a warning for optional parameters before required.
*
Expand Down Expand Up @@ -82,7 +89,7 @@ public static function dataRemovedOptionalBeforeRequiredParam80()
[57],
[58],
[59],
[103],
[126],
];
}

Expand Down Expand Up @@ -139,7 +146,7 @@ public static function dataNoFalsePositives80()
// Deprecated, but only flagged as of PHP 8.1.
$cases['line 71 - deprecated in PHP 8.1'] = [71];
$cases['line 75 - deprecated in PHP 8.1'] = [75];
$cases['line 102 - deprecated in PHP 8.1'] = [102];
$cases['line 125 - deprecated in PHP 8.1'] = [125];

// Not deprecated, false positive checks for PHP 8.3 deprecation.
$cases['line 81 - related to PHP 8.3 deprecation'] = [81];
Expand All @@ -154,10 +161,27 @@ public static function dataNoFalsePositives80()
$cases['line 90 - deprecated in PHP 8.3'] = [90];
$cases['line 91 - deprecated in PHP 8.3'] = [91];
$cases['line 95 - deprecated in PHP 8.3'] = [95];
$cases['line 101 - deprecated in PHP 8.3'] = [101];
$cases['line 124 - deprecated in PHP 8.3'] = [124];

// Not deprecated, false positive checks for PHP 8.4 deprecation.
$cases['line 102 - related to PHP 8.4 deprecation'] = [102];
$cases['line 103 - related to PHP 8.4 deprecation'] = [103];
$cases['line 104 - related to PHP 8.4 deprecation'] = [104];
$cases['line 105 - related to PHP 8.4 deprecation'] = [105];
$cases['line 107 - related to PHP 8.4 deprecation'] = [107];
$cases['line 113 - related to PHP 8.4 deprecation'] = [113];
$cases['line 114 - related to PHP 8.4 deprecation'] = [114];

// Deprecated as of PHP 8.4.
$cases['line 111 - deprecated in PHP 8.4'] = [111];
$cases['line 112 - deprecated in PHP 8.4'] = [112];
$cases['line 116 - deprecated in PHP 8.4'] = [116];
$cases['line 117 - deprecated in PHP 8.4'] = [117];
$cases['line 118 - deprecated in PHP 8.4'] = [118];
$cases['line 123 - deprecated in PHP 8.4'] = [123];

// Add parse error test case.
$cases['line 108 - parse error'] = [108];
$cases['line 131 - parse error'] = [131];

return $cases;
}
Expand Down Expand Up @@ -191,7 +215,7 @@ public static function dataRemovedOptionalBeforeRequiredParam81()
$data = self::dataRemovedOptionalBeforeRequiredParam80();
$data[] = [71, self::PHP81_MSG];
$data[] = [75, self::PHP81_MSG];
$data[] = [102, self::PHP81_MSG];
$data[] = [125, self::PHP81_MSG];
return $data;
}

Expand Down Expand Up @@ -224,7 +248,7 @@ public static function dataNoFalsePositives81()
unset(
$cases['line 71 - deprecated in PHP 8.1'],
$cases['line 75 - deprecated in PHP 8.1'],
$cases['line 102 - deprecated in PHP 8.1']
$cases['line 125 - deprecated in PHP 8.1']
);

return $cases;
Expand Down Expand Up @@ -264,7 +288,7 @@ public static function dataRemovedOptionalBeforeRequiredParam83()
$data[] = [90, self::PHP83_MSG];
$data[] = [91, self::PHP83_MSG];
$data[] = [95, self::PHP83_MSG];
$data[] = [101, self::PHP83_MSG];
$data[] = [124, self::PHP83_MSG];
return $data;
}

Expand Down Expand Up @@ -302,7 +326,85 @@ public static function dataNoFalsePositives83()
$cases['line 90 - deprecated in PHP 8.3'],
$cases['line 91 - deprecated in PHP 8.3'],
$cases['line 95 - deprecated in PHP 8.3'],
$cases['line 101 - deprecated in PHP 8.3']
$cases['line 124 - deprecated in PHP 8.3']
);

return $cases;
}


/**
* Verify that the sniff throws a warning for optional parameters with a union type which includes null before required.
*
* @dataProvider dataRemovedOptionalBeforeRequiredParam84
*
* @param int $line The line number where a warning is expected.
* @param string $msg The expected warning message.
*
* @return void
*/
public function testRemovedOptionalBeforeRequiredParam84($line, $msg = self::PHP80_MSG)
{
$file = $this->sniffFile(__FILE__, '8.4');
$this->assertWarning($file, $line, $msg);
}

/**
* Data provider.
*
* @see testRemovedOptionalBeforeRequiredParam84()
*
* @return array
*/
public static function dataRemovedOptionalBeforeRequiredParam84()
{
$data = self::dataRemovedOptionalBeforeRequiredParam83();
$data[] = [67, self::PHP84_MSG];
$data[] = [83, self::PHP84_MSG];
$data[] = [111, self::PHP84_MSG];
$data[] = [112, self::PHP84_MSG];
$data[] = [116, self::PHP84_MSG];
$data[] = [117, self::PHP84_MSG];
$data[] = [118, self::PHP84_MSG];
$data[] = [123, self::PHP84_MSG];
return $data;
}


/**
* Verify the sniff does not throw false positives for valid code.
*
* @dataProvider dataNoFalsePositives84
*
* @param int $line The line number.
*
* @return void
*/
public function testNoFalsePositives84($line)
{
$file = $this->sniffFile(__FILE__, '8.4');
$this->assertNoViolation($file, $line);
}

/**
* Data provider.
*
* @see testNoFalsePositives84()
*
* @return array
*/
public static function dataNoFalsePositives84()
{
$cases = self::dataNoFalsePositives83();
unset(
$cases['line 67 - related to PHP 8.1 deprecation'],
$cases['line 83 - related to PHP 8.3 deprecation'],
$cases['line 111 - deprecated in PHP 8.4'],
$cases['line 112 - deprecated in PHP 8.4'],
$cases['line 116 - deprecated in PHP 8.4'],
$cases['line 117 - deprecated in PHP 8.4'],
$cases['line 118 - deprecated in PHP 8.4'],
$cases['line 123 - deprecated in PHP 8.4']
);

return $cases;
Expand Down

0 comments on commit 7cd518e

Please sign in to comment.