Skip to content

Commit a5c21e6

Browse files
committed
Fix PHP8.4 tests for ReflectionProperty features
1 parent 6fd4ba6 commit a5c21e6

File tree

7 files changed

+163
-5
lines changed

7 files changed

+163
-5
lines changed

.github/workflows/phpunit.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ name: "PHPUnit tests"
22

33
on:
44
pull_request:
5-
push:
65

76
jobs:
87
phpunit:

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
},
2424
"require": {
2525
"php": ">=8.2",
26-
"nikic/php-parser": "^5.0"
26+
"nikic/php-parser": "^5.4"
2727
},
2828
"require-dev": {
2929
"phpunit/phpunit": "^11.0.7",

src/ReflectionProperty.php

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,21 @@ public function getModifiers(): int
199199
if ($this->isReadOnly()) {
200200
$modifiers += self::IS_READONLY;
201201
}
202+
if (PHP_VERSION_ID >= 80400 && $this->isAbstract()) {
203+
$modifiers += self::IS_ABSTRACT;
204+
}
205+
if (PHP_VERSION_ID >= 80400 && $this->isFinal()) {
206+
$modifiers += self::IS_FINAL;
207+
}
208+
if (PHP_VERSION_ID >= 80400 && $this->isProtectedSet()) {
209+
$modifiers += self::IS_PROTECTED_SET;
210+
}
211+
if (PHP_VERSION_ID >= 80400 && $this->isPrivateSet()) {
212+
$modifiers += self::IS_PRIVATE_SET;
213+
}
202214

203215
// Handle PHP 8.4+ asymmetric visibility modifiers
204-
// Note: IS_PRIVATE_SET and IS_PROTECTED_SET are only added for properties with explicit
216+
// Note: IS_PRIVATE_SET and IS_PROTECTED_SET are only added for properties with explicit
205217
// asymmetric visibility syntax like "public private(set) $prop", not for regular readonly properties
206218
// TODO: Implement when nikic/php-parser supports asymmetric visibility syntax
207219

@@ -277,6 +289,18 @@ public function getDefaultValue(): mixed
277289
return $this->defaultValue;
278290
}
279291

292+
/**
293+
* @inheritDoc
294+
*/
295+
public function isAbstract(): bool
296+
{
297+
if ($this->propertyOrPromotedParam instanceof Property) {
298+
return $this->propertyOrPromotedParam->isAbstract();
299+
}
300+
301+
return false;
302+
}
303+
280304
/**
281305
* @inheritDoc
282306
*/
@@ -287,6 +311,22 @@ public function isDefault(): bool
287311
return true;
288312
}
289313

314+
/**
315+
* {@inheritDoc}
316+
*
317+
* @see Property::isFinal()
318+
*/
319+
public function isFinal(): bool
320+
{
321+
$explicitFinal = false;
322+
if ($this->propertyOrPromotedParam instanceof Property) {
323+
$explicitFinal = $this->propertyOrPromotedParam->isFinal();
324+
}
325+
326+
// Property with private(set) modifier is implicitly final
327+
return $explicitFinal || $this->isPrivateSet();
328+
}
329+
290330
/**
291331
* {@inheritDoc}
292332
*
@@ -298,6 +338,17 @@ public function isPrivate(): bool
298338
return $this->propertyOrPromotedParam->isPrivate();
299339
}
300340

341+
/**
342+
* @inheritDoc
343+
*
344+
* @see Property::isPrivateSet()
345+
* @see Param::isPrivateSet()
346+
*/
347+
public function isPrivateSet(): bool
348+
{
349+
return ($this->propertyOrPromotedParam->isPrivateSet() && !$this->propertyOrPromotedParam->isPrivate());
350+
}
351+
301352
/**
302353
* {@inheritDoc}
303354
*
@@ -309,6 +360,22 @@ public function isProtected(): bool
309360
return $this->propertyOrPromotedParam->isProtected();
310361
}
311362

363+
/**
364+
* @inheritDoc
365+
*
366+
* @see Property::isProtectedSet()
367+
* @see Param::isProtectedSet()
368+
*/
369+
public function isProtectedSet(): bool
370+
{
371+
/*
372+
* Behavior of readonly is to imply protected(set), not private(set).
373+
* A readonly property may still be explicitly declared private(set), in which case it will also be implicitly final
374+
*/
375+
return ($this->propertyOrPromotedParam->isProtectedSet() && !$this->propertyOrPromotedParam->isProtected())
376+
|| ($this->isPublic() && $this->isReadonly() && !$this->isPrivateSet() && !$this->propertyOrPromotedParam->isPublicSet());
377+
}
378+
312379
/**
313380
* {@inheritDoc}
314381
*
@@ -352,7 +419,6 @@ public function isReadOnly(): bool
352419
return $this->propertyOrPromotedParam->isReadonly() || $this->getDeclaringClass()->isReadOnly();
353420
}
354421

355-
356422
/**
357423
* {@inheritDoc}
358424
*/
@@ -369,6 +435,14 @@ public function isInitialized(?object $object = null): bool
369435
return $this->hasDefaultValue();
370436
}
371437

438+
/**
439+
* @inheritDoc
440+
*/
441+
public function isVirtual(): bool
442+
{
443+
return $this->propertyOrPromotedParam->isVirtual();
444+
}
445+
372446
/**
373447
* {@inheritDoc}
374448
*/

tests/AbstractTestCase.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ public static function getFilesToAnalyze(): \Generator
8585
if (PHP_VERSION_ID >= 80300) {
8686
yield 'PHP8.3' => [__DIR__ . '/Stub/FileWithClasses83.php'];
8787
}
88+
if (PHP_VERSION_ID >= 80400) {
89+
yield 'PHP8.4' => [__DIR__ . '/Stub/FileWithClasses84.php'];
90+
}
8891
}
8992

9093
/**

tests/ReflectionClassTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use Go\ParserReflection\Stub\ClassWithPhp50ConstantsAndInheritance;
77
use Go\ParserReflection\Stub\ClassWithPhp50MagicConstants;
8+
use Go\ParserReflection\Stub\ClassWithPhp84PropertyHooks;
89
use Go\ParserReflection\Stub\SimplePhp50ClassWithMethodsAndProperties;
910
use Go\ParserReflection\Stub\ClassWithPhp50ScalarConstants;
1011
use Go\ParserReflection\Stub\ClassWithPhp50FinalKeyword;
@@ -69,6 +70,12 @@ public function testReflectionGetterParity(
6970
"See https://github.com/goaop/parser-reflection/issues/132"
7071
);
7172
}
73+
if ($parsedClass->getName() === ClassWithPhp84PropertyHooks::class && in_array($getterName, ['isIterable', 'isIterateable'], true)) {
74+
$this->markTestSkipped(
75+
"isIterable for class with hooks returns true.\n" .
76+
"See https://github.com/php/php-src/issues/20217"
77+
);
78+
}
7279
$this->assertSame(
7380
$expectedValue,
7481
$actualValue,

tests/ReflectionPropertyTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,16 @@ public static function propertiesDataProvider(): \Generator
175175
*/
176176
protected static function getGettersToCheck(): array
177177
{
178-
return [
178+
$getters = [
179179
'isDefault', 'getName', 'getModifiers', 'getDocComment',
180180
'isPrivate', 'isProtected', 'isPublic', 'isStatic', 'isReadOnly', 'isInitialized',
181181
'hasType', 'hasDefaultValue', 'getDefaultValue', '__toString'
182182
];
183+
184+
if (PHP_VERSION_ID >= 80400) {
185+
array_push($getters, 'isAbstract', 'isProtectedSet', 'isPrivateSet', 'isFinal');
186+
}
187+
188+
return $getters;
183189
}
184190
}

tests/Stub/FileWithClasses84.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
/**
3+
* Parser Reflection API
4+
*
5+
* @copyright Copyright 2025, Lisachenko Alexander <[email protected]>
6+
*
7+
* This source file is subject to the license that is bundled
8+
* with this source code in the file LICENSE.
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace Go\ParserReflection\Stub;
13+
14+
/**
15+
* @see https://wiki.php.net/rfc/property-hooks
16+
*/
17+
18+
class ClassWithPhp84PropertyHooks
19+
{
20+
private string $backing = 'default';
21+
22+
public string $name {
23+
get => $this->backing;
24+
set => $this->backing = strtoupper($value);
25+
}
26+
}
27+
28+
/* Not supported yet
29+
interface InterfaceWithPhp84AbstractProperty
30+
{
31+
public string $name { get; }
32+
}
33+
*/
34+
35+
/**
36+
* https://wiki.php.net/rfc/asymmetric-visibility-v2
37+
*/
38+
class ClassWithPhp84AsymmetricVisibility
39+
{
40+
// These create a public-read, protected-write, write-once property.
41+
public protected(set) readonly string $explicitPublicWriteOnceProtectedProperty;
42+
public readonly string $implicitPublicReadonlyWriteOnceProperty;
43+
readonly string $implicitReadonlyWriteOnceProperty;
44+
45+
// These creates a public-read, private-set, write-once, final property.
46+
public private(set) readonly string $explicitPublicWriteOncePrivateProperty;
47+
private(set) readonly string $implicitPublicReadonlyWriteOncePrivateProperty;
48+
49+
// These create a public-read, public-write, write-once property.
50+
// While use cases for this configuration are likely few,
51+
// there's no intrinsic reason it should be forbidden.
52+
public public(set) readonly string $explicitPublicWriteOncePublicProperty;
53+
public(set) readonly string $implicitPublicReadonlyWriteOncePublicProperty;
54+
55+
// These create a private-read, private-write, write-once, final property.
56+
private private(set) readonly string $explicitPrivateWriteOncePrivateProperty;
57+
private readonly string $implicitPrivateReadonlyWriteOncePrivateProperty;
58+
59+
// These create a protected-read, protected-write, write-once property.
60+
protected protected(set) readonly string $explicitProtectedWriteOnceProtectedProperty;
61+
protected readonly string $implicitProtectedReadonlyWriteOnceProtectedProperty;
62+
63+
public function __construct(
64+
private(set) string $promotedPrivateSetStringProperty,
65+
protected(set) string $promotedProtectedSetStringProperty,
66+
protected private(set) int $promotedProtectedPrivateSetIntProperty,
67+
) {}
68+
69+
}

0 commit comments

Comments
 (0)