Skip to content

Commit

Permalink
PHP 8.4 - report deprecated implicitly nullable parameter types
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Sep 5, 2024
1 parent 9a88a77 commit 9bd027c
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 48 deletions.
52 changes: 5 additions & 47 deletions src/Dependency/ExportedNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
use PHPStan\Dependency\ExportedNode\ExportedTraitNode;
use PHPStan\Dependency\ExportedNode\ExportedTraitUseAdaptation;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Node\Printer\NodeTypePrinter;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\FileTypeMapper;
use function array_map;
use function implode;
use function is_string;

final class ExportedNodeResolver
Expand Down Expand Up @@ -165,7 +165,7 @@ public function resolve(string $fileName, Node $node): ?RootExportedNode
$docComment !== null ? $docComment->getText() : null,
),
$node->byRef,
$this->printType($node->returnType),
NodeTypePrinter::printType($node->returnType),
$this->exportParameterNodes($node->params),
$this->exportAttributeNodes($node->attrGroups),
);
Expand All @@ -174,48 +174,6 @@ public function resolve(string $fileName, Node $node): ?RootExportedNode
return null;
}

/**
* @param Node\Identifier|Node\Name|Node\ComplexType|null $type
*/
private function printType($type): ?string
{
if ($type === null) {
return null;
}

if ($type instanceof Node\NullableType) {
return '?' . $this->printType($type->type);
}

if ($type instanceof Node\UnionType) {
return implode('|', array_map(function ($innerType): string {
$printedType = $this->printType($innerType);
if ($printedType === null) {
throw new ShouldNotHappenException();
}

return $printedType;
}, $type->types));
}

if ($type instanceof Node\IntersectionType) {
return implode('&', array_map(function ($innerType): string {
$printedType = $this->printType($innerType);
if ($printedType === null) {
throw new ShouldNotHappenException();
}

return $printedType;
}, $type->types));
}

if ($type instanceof Node\Identifier || $type instanceof Name) {
return $type->toString();
}

throw new ShouldNotHappenException();
}

/**
* @param Node\Param[] $params
* @return ExportedParameterNode[]
Expand Down Expand Up @@ -243,7 +201,7 @@ private function exportParameterNodes(array $params): array
}
$nodes[] = new ExportedParameterNode(
$param->var->name,
$this->printType($type),
NodeTypePrinter::printType($type),
$param->byRef,
$param->variadic,
$param->default !== null,
Expand Down Expand Up @@ -321,7 +279,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string
$node->isAbstract(),
$node->isFinal(),
$node->isStatic(),
$this->printType($node->returnType),
NodeTypePrinter::printType($node->returnType),
$this->exportParameterNodes($node->params),
$this->exportAttributeNodes($node->attrGroups),
);
Expand All @@ -343,7 +301,7 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string
null,
$docComment !== null ? $docComment->getText() : null,
),
$this->printType($node->type),
NodeTypePrinter::printType($node->type),
$node->isPublic(),
$node->isPrivate(),
$node->isStatic(),
Expand Down
52 changes: 52 additions & 0 deletions src/Node/Printer/NodeTypePrinter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php declare(strict_types = 1);

namespace PHPStan\Node\Printer;

use PhpParser\Node;
use PHPStan\ShouldNotHappenException;
use function array_map;
use function implode;

final class NodeTypePrinter
{

public static function printType(Node\Name|Node\Identifier|Node\ComplexType|null $type): ?string
{
if ($type === null) {
return null;
}

if ($type instanceof Node\NullableType) {
return '?' . self::printType($type->type);
}

if ($type instanceof Node\UnionType) {
return implode('|', array_map(static function ($innerType): string {
$printedType = self::printType($innerType);
if ($printedType === null) {
throw new ShouldNotHappenException();
}

return $printedType;
}, $type->types));
}

if ($type instanceof Node\IntersectionType) {
return implode('&', array_map(static function ($innerType): string {
$printedType = self::printType($innerType);
if ($printedType === null) {
throw new ShouldNotHappenException();
}

return $printedType;
}, $type->types));
}

if ($type instanceof Node\Identifier || $type instanceof Node\Name) {
return $type->toString();
}

throw new ShouldNotHappenException();
}

}
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -343,4 +343,9 @@ public function highlightStringDoesNotReturnFalse(): bool
return $this->versionId >= 80400;
}

public function deprecatesImplicitlyNullableParameterTypes(): bool
{
return $this->versionId >= 80400;
}

}
85 changes: 84 additions & 1 deletion src/Rules/FunctionDefinitionCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Rules;

use PhpParser\Node;
use PhpParser\Node\ComplexType;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\FunctionLike;
Expand All @@ -15,6 +16,7 @@
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\UnionType;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Printer\NodeTypePrinter;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParameterReflection;
Expand All @@ -41,6 +43,7 @@
use function in_array;
use function is_string;
use function sprintf;
use function strtolower;

final class FunctionDefinitionCheck
{
Expand Down Expand Up @@ -103,7 +106,7 @@ public function checkAnonymousFunction(
{
$errors = [];
$unionTypeReported = false;
foreach ($parameters as $param) {
foreach ($parameters as $i => $param) {
if ($param->type === null) {
continue;
}
Expand All @@ -123,6 +126,18 @@ public function checkAnonymousFunction(
if (!$param->var instanceof Variable || !is_string($param->var->name)) {
throw new ShouldNotHappenException();
}

$implicitlyNullableTypeError = $this->checkImplicitlyNullableType(
$param->type,
$param->default,
$i + 1,
$param->getStartLine(),
$param->var->name,
);
if ($implicitlyNullableTypeError !== null) {
$errors[] = $implicitlyNullableTypeError;
}

$type = $scope->getFunctionType($param->type, false, false);
if ($type->isVoid()->yes()) {
$errors[] = RuleErrorBuilder::message(sprintf($parameterMessage, $param->var->name, 'void'))
Expand Down Expand Up @@ -333,6 +348,18 @@ private function checkParametersAcceptor(
}
}

foreach ($parameterNodes as $i => $parameterNode) {
if (!$parameterNode->var instanceof Variable || !is_string($parameterNode->var->name)) {
throw new ShouldNotHappenException();
}
$implicitlyNullableTypeError = $this->checkImplicitlyNullableType($parameterNode->type, $parameterNode->default, $i + 1, $parameterNode->getStartLine(), $parameterNode->var->name);
if ($implicitlyNullableTypeError === null) {
continue;
}

$errors[] = $implicitlyNullableTypeError;
}

if ($this->phpVersion->deprecatesRequiredParameterAfterOptional()) {
$errors = array_merge($errors, $this->checkRequiredParameterAfterOptional($parameterNodes));
}
Expand Down Expand Up @@ -654,4 +681,60 @@ private function getReturnTypeReferencedClasses(ParametersAcceptor $parametersAc
);
}

private function checkImplicitlyNullableType(
Identifier|Name|ComplexType|null $type,
?Node\Expr $default,
int $order,
int $line,
string $name,
): ?IdentifierRuleError
{
if (!$default instanceof ConstFetch) {
return null;
}

if ($default->name->toLowerString() !== 'null') {
return null;
}

if ($type === null) {
return null;
}

if ($type instanceof NullableType || $type instanceof IntersectionType) {
return null;
}

if (!$this->phpVersion->deprecatesImplicitlyNullableParameterTypes()) {
return null;
}

if ($type instanceof Identifier && strtolower($type->name) === 'mixed') {
return null;
}
if ($type instanceof Name && $type->toLowerString() === 'mixed') {
return null;
}

if ($type instanceof UnionType) {
foreach ($type->types as $innerType) {
if ($innerType instanceof Identifier && strtolower($innerType->name) === 'null') {
return null;
}
if ($innerType instanceof Name && $innerType->toLowerString() === 'null') {
return null;
}
}
}

return RuleErrorBuilder::message(sprintf(
'Deprecated in PHP 8.4: Parameter #%d $%s (%s) is implicitly nullable via default value null.',
$order,
$name,
NodeTypePrinter::printType($type),
))->line($line)
->identifier('parameter.implicitlyNullable')
->build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,26 @@ public function testIntersectionTypes(int $phpVersion, array $errors): void
$this->analyse([__DIR__ . '/data/closure-intersection-types.php'], $errors);
}

public function testDeprecatedImplicitlyNullableParameterType(): void
{
if (PHP_VERSION_ID < 80400) {
self::markTestSkipped('Test requires PHP 8.4.');
}

$this->analyse([__DIR__ . '/data/closure-implicitly-nullable.php'], [
[
'Deprecated in PHP 8.4: Parameter #3 $c (int) is implicitly nullable via default value null.',
13,
],
[
'Deprecated in PHP 8.4: Parameter #5 $e (int|string) is implicitly nullable via default value null.',
15,
],
[
'Deprecated in PHP 8.4: Parameter #7 $g (stdClass) is implicitly nullable via default value null.',
17,
],
]);
}

}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Functions/data/closure-implicitly-nullable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php // lint >= 8.0

namespace ClosureImplicitNullable;

class Foo
{

public function doFoo(): void
{
$c = function (
$a = null,
int $b = 1,
int $c = null,
mixed $d = null,
int|string $e = null,
int|string|null $f = null,
\stdClass $g = null,
?\stdClass $h = null,
): void {

};
}

}
22 changes: 22 additions & 0 deletions tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -526,4 +526,26 @@ public function testSelfOut(): void
]);
}

public function testDeprecatedImplicitlyNullableParameterType(): void
{
if (PHP_VERSION_ID < 80400) {
self::markTestSkipped('Test requires PHP 8.4.');
}

$this->analyse([__DIR__ . '/data/method-implicitly-nullable.php'], [
[
'Deprecated in PHP 8.4: Parameter #3 $c (int) is implicitly nullable via default value null.',
13,
],
[
'Deprecated in PHP 8.4: Parameter #5 $e (int|string) is implicitly nullable via default value null.',
15,
],
[
'Deprecated in PHP 8.4: Parameter #7 $g (stdClass) is implicitly nullable via default value null.',
17,
],
]);
}

}
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/Methods/data/method-implicitly-nullable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace MethodImplicitNullable;

use stdClass;

class Foo
{

public function doFoo(
$a = null,
int $b = 1,
int $c = null,
mixed $d = null,
int|string $e = null,
int|string|null $f = null,
stdClass $g = null,
?stdClass $h = null,
): void
{
}

}

0 comments on commit 9bd027c

Please sign in to comment.