Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 57 additions & 28 deletions build/PHPStan/Build/RequiredPhpVersionVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Php\PhpVersion;
use function in_array;
use function strtolower;

Expand All @@ -17,15 +18,6 @@
*/
final class RequiredPhpVersionVisitor extends NodeVisitorAbstract
{

public const PHP_8_0 = 80000;

private const PHP_8_1 = 80100;
private const PHP_8_2 = 80200;
private const PHP_8_3 = 80300;
private const PHP_8_4 = 80400;
private const PHP_8_5 = 80500;

private ?int $requiredVersionId = null;

private ?string $reason = null;
Expand All @@ -51,64 +43,64 @@ public function getReasonLine(): ?int
public function enterNode(Node $node): ?Node
{
if ($node instanceof Node\Stmt\Enum_) {
$this->require(self::PHP_8_1, 'enums', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsEnums(), 'enums', $node);
}

if ($node instanceof Node\Expr\BinaryOp\Pipe) {
$this->require(self::PHP_8_5, 'the pipe operator', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsPipeOperator(), 'the pipe operator', $node);
}

if ($node instanceof Node\PropertyHook) {
$this->require(self::PHP_8_4, 'property hooks', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsPropertyHooks(), 'property hooks', $node);
}

if ($node instanceof Node\IntersectionType) {
$this->require(self::PHP_8_1, 'pure intersection types', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsPureIntersectionTypes(), 'pure intersection types', $node);
}

if ($node instanceof Node\UnionType) {
$this->require(self::PHP_8_0, 'union types', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsNativeUnionTypes(), 'union types', $node);
foreach ($node->types as $innerType) {
if ($innerType instanceof Node\IntersectionType) {
$this->require(self::PHP_8_2, 'disjunctive normal form types', $innerType);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsDisjunctiveNormalForm(), 'disjunctive normal form types', $innerType);
}
if (!($innerType instanceof Node\Identifier) || strtolower($innerType->name) !== 'true') {
continue;
}

$this->require(self::PHP_8_2, 'the standalone "true" type', $innerType);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsTrueFalseNullStandaloneType(), 'the standalone "true" type', $innerType);
}
}

if ($node instanceof Node\Stmt\Class_ && ($node->flags & Modifiers::READONLY) !== 0) {
$this->require(self::PHP_8_2, 'readonly classes', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsReadOnlyClasses(), 'readonly classes', $node);
}

if ($node instanceof Node\Stmt\Property && ($node->flags & Modifiers::READONLY) !== 0) {
$this->require(self::PHP_8_1, 'readonly properties', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsReadOnlyProperties(), 'readonly properties', $node);
}

if ($node instanceof Node\Param && $node->flags !== 0) {
$this->require(self::PHP_8_0, 'promoted properties', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsPromotedProperties(), 'promoted properties', $node);
}

if ($node instanceof Node\Param && ($node->flags & Modifiers::READONLY) !== 0) {
$this->require(self::PHP_8_1, 'readonly promoted properties', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsReadOnlyProperties(), 'readonly promoted properties', $node);
}

if (
($node instanceof Node\Param || $node instanceof Node\Stmt\Property)
&& ($node->flags & Modifiers::VISIBILITY_SET_MASK) !== 0
) {
$this->require(self::PHP_8_4, 'asymmetric visibility', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsAsymmetricVisibility(), 'asymmetric visibility', $node);
}

if ($node instanceof Node\Stmt\ClassConst && $node->type !== null) {
$this->require(self::PHP_8_3, 'typed class constants', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsNativeTypesInClassConstants(), 'typed class constants', $node);
}

if ($node instanceof Node\Expr\ClassConstFetch && $node->name instanceof Node\Expr) {
$this->require(self::PHP_8_3, 'dynamic class constant fetch', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsDynamicClassConstantFetch(), 'dynamic class constant fetch', $node);
}

if (
Expand All @@ -119,14 +111,14 @@ public function enterNode(Node $node): ?Node
) {
foreach ($node->args as $arg) {
if ($arg instanceof Node\VariadicPlaceholder) {
$this->require(self::PHP_8_1, 'first-class callable syntax', $arg);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsFirstClassCallables(), 'first-class callable syntax', $arg);
break;
}
}
}

if ($node instanceof Node\Arg && $node->name !== null) {
$this->require(self::PHP_8_0, 'named arguments', $node);
$this->require(fn(PhpVersion $phpVersion) => $phpVersion->supportsNamedArguments(), 'named arguments', $node);
}

$this->checkStandaloneType($node);
Expand All @@ -146,7 +138,11 @@ private function checkStandaloneType(Node $node): void
return;
}

$this->require(self::PHP_8_2, 'standalone "null", "false" or "true" types', $type);
$this->require(
fn(PhpVersion $phpVersion) => $phpVersion->supportsTrueFalseNullStandaloneType(),
'standalone "null", "false" or "true" types',
$type
);
}

private function checkMixedType(Node $node): void
Expand All @@ -160,7 +156,11 @@ private function checkMixedType(Node $node): void
return;
}

$this->require(self::PHP_8_0, 'the mixed type', $type);
$this->require(
fn(PhpVersion $phpVersion) => $phpVersion->supportsNativeMixed(),
'the mixed type',
$type
);
}

private function getDeclaredType(Node $node): ?Node
Expand All @@ -185,8 +185,13 @@ private function getDeclaredType(Node $node): ?Node
return null;
}

private function require(int $versionId, string $reason, Node $node): void
/**
* @param callable(PhpVersion $phpVersion): bool $callable
*/
private function require(callable $callable, string $reason, Node $node): void
{
$versionId = $this->findPhpVersion($callable);

if ($this->requiredVersionId !== null && $this->requiredVersionId >= $versionId) {
return;
}
Expand All @@ -196,4 +201,28 @@ private function require(int $versionId, string $reason, Node $node): void
$this->reasonLine = $node->getStartLine();
}

/**
* @param callable(PhpVersion $phpVersion): bool $callable
*/
private function findPhpVersion(callable $callable): int
{
$phpVersionIds = [
new PhpVersion(70400),
new PhpVersion(80000),
new PhpVersion(80100),
new PhpVersion(80200),
new PhpVersion(80300),
new PhpVersion(80400),
new PhpVersion(80500)
];

foreach($phpVersionIds as $phpVersion) {
if ($callable($phpVersion)) {
return $phpVersion->getVersionId();
}
}

throw new \PHPStan\ShouldNotHappenException();
}

}
15 changes: 15 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ public function supportsNativeUnionTypes(): bool
return $this->versionId >= 80000;
}

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

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

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

public function deprecatesRequiredParameterAfterOptional(): bool
{
return $this->versionId >= 80000;
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Build/RequiredPhpVersionCommentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function testFixtureHasRequiredLintComment(string $file): void
// version-specific function signature) may therefore use it; the author owns
// that choice. Parse-breaking features (enums, hooks, ...) are still rejected.
if (
$requiredVersionId === RequiredPhpVersionVisitor::PHP_8_0
$requiredVersionId === 80000
&& self::hasUpperBoundLintConstraint($code)
) {
$this->expectNotToPerformAssertions();
Expand Down
Loading