Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 534295b

Browse files
jordikroonnicolas-grekas
authored andcommitted
[PropertyInfo][Serializer] Skip methods that look like getters but return void or never
1 parent 6e70fbe commit 534295b

17 files changed

Lines changed: 268 additions & 37 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyAccess\Tests\Fixtures;
13+
14+
class TestIgnoreVoidAccessor
15+
{
16+
public bool $setValue = false;
17+
public bool $setterValue = false;
18+
public bool $neverValue = false;
19+
public bool $normalValue = false;
20+
21+
public function setValue(): void
22+
{
23+
$this->setValue = true;
24+
}
25+
26+
public function setSetterValue(): void
27+
{
28+
$this->setterValue = true;
29+
}
30+
31+
public function setNeverValue(): never
32+
{
33+
// Simulate a setter that does not return anything and exits
34+
$this->neverValue = true;
35+
exit;
36+
}
37+
38+
public function setUndefinedValue(): void
39+
{
40+
// This method is intentionally left empty to simulate a missing setter
41+
}
42+
}

src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassSetValue;
3333
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassTypedProperty;
3434
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestClassTypeErrorInsideCall;
35+
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestIgnoreVoidAccessor;
3536
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestPublicPropertyDynamicallyCreated;
3637
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestPublicPropertyGetterOnObject;
3738
use Symfony\Component\PropertyAccess\Tests\Fixtures\TestPublicPropertyGetterOnObjectMagicGet;
@@ -995,6 +996,33 @@ public function testGetValueGetterThrowsExceptionIfUninitializedWithoutLazyGhost
995996
$this->propertyAccessor->getValue(new UninitializedObjectProperty(), 'privateUninitialized');
996997
}
997998

999+
/**
1000+
* @dataProvider voidAccessorProvider
1001+
*/
1002+
public function testIgnoreVoidAccessor(string $property, mixed $value)
1003+
{
1004+
$object = new TestIgnoreVoidAccessor();
1005+
if (null === $value) {
1006+
$this->expectException(NoSuchPropertyException::class);
1007+
}
1008+
1009+
$this->assertSame(
1010+
$value,
1011+
$this->propertyAccessor->getValue($object, $property),
1012+
);
1013+
}
1014+
1015+
public static function voidAccessorProvider()
1016+
{
1017+
return [
1018+
['setValue', false],
1019+
['setterValue', false],
1020+
['neverValue', false],
1021+
['normalValue', false],
1022+
['undefinedValue', null],
1023+
];
1024+
}
1025+
9981026
public function testGetValuePropertyThrowsExceptionIfUninitializedWithLazyGhost()
9991027
{
10001028
$this->expectException(UninitializedPropertyException::class);

src/Symfony/Component/PropertyInfo/Extractor/PhpDocExtractor.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,19 +290,24 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i
290290
{
291291
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
292292
$prefix = null;
293+
$method = null;
293294

294295
foreach ($prefixes as $prefix) {
295296
$methodName = $prefix.$ucFirstProperty;
296297

297298
try {
298-
$reflectionMethod = new \ReflectionMethod($class, $methodName);
299-
if ($reflectionMethod->isStatic()) {
299+
$method = new \ReflectionMethod($class, $methodName);
300+
if ($method->isStatic()) {
301+
continue;
302+
}
303+
304+
if (self::ACCESSOR === $type && \in_array((string) $method->getReturnType(), ['void', 'never'], true)) {
300305
continue;
301306
}
302307

303308
if (
304-
(self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters())
305-
|| (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
309+
(self::ACCESSOR === $type && !$method->getNumberOfRequiredParameters())
310+
|| (self::MUTATOR === $type && $method->getNumberOfParameters() >= 1)
306311
) {
307312
break;
308313
}
@@ -311,11 +316,11 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i
311316
}
312317
}
313318

314-
if (!isset($reflectionMethod)) {
319+
if (!$method) {
315320
return null;
316321
}
317322

318-
$reflector = $reflectionMethod->getDeclaringClass();
323+
$reflector = $method->getDeclaringClass();
319324

320325
foreach ($reflector->getTraits() as $trait) {
321326
if ($trait->hasMethod($methodName)) {
@@ -324,7 +329,7 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i
324329
}
325330

326331
try {
327-
return [$this->docBlockFactory->create($reflectionMethod, $this->createFromReflector($reflector)), $prefix];
332+
return [$this->docBlockFactory->create($method, $this->createFromReflector($reflector)), $prefix];
328333
} catch (\InvalidArgumentException|\RuntimeException) {
329334
return null;
330335
}

src/Symfony/Component/PropertyInfo/Extractor/PhpStanExtractor.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -278,19 +278,24 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i
278278
{
279279
$prefixes = self::ACCESSOR === $type ? $this->accessorPrefixes : $this->mutatorPrefixes;
280280
$prefix = null;
281+
$method = null;
281282

282283
foreach ($prefixes as $prefix) {
283284
$methodName = $prefix.$ucFirstProperty;
284285

285286
try {
286-
$reflectionMethod = new \ReflectionMethod($class, $methodName);
287-
if ($reflectionMethod->isStatic()) {
287+
$method = new \ReflectionMethod($class, $methodName);
288+
if ($method->isStatic()) {
289+
continue;
290+
}
291+
292+
if (self::ACCESSOR === $type && \in_array((string) $method->getReturnType(), ['void', 'never'], true)) {
288293
continue;
289294
}
290295

291296
if (
292-
(self::ACCESSOR === $type && 0 === $reflectionMethod->getNumberOfRequiredParameters())
293-
|| (self::MUTATOR === $type && $reflectionMethod->getNumberOfParameters() >= 1)
297+
(self::ACCESSOR === $type && !$method->getNumberOfRequiredParameters())
298+
|| (self::MUTATOR === $type && $method->getNumberOfParameters() >= 1)
294299
) {
295300
break;
296301
}
@@ -299,17 +304,17 @@ private function getDocBlockFromMethod(string $class, string $ucFirstProperty, i
299304
}
300305
}
301306

302-
if (!isset($reflectionMethod)) {
307+
if (!$method) {
303308
return null;
304309
}
305310

306-
if (null === $rawDocNode = $reflectionMethod->getDocComment() ?: null) {
311+
if (null === $rawDocNode = $method->getDocComment() ?: null) {
307312
return null;
308313
}
309314

310315
$phpDocNode = $this->getPhpDocNode($rawDocNode);
311316

312-
return [$phpDocNode, $prefix, $reflectionMethod->class];
317+
return [$phpDocNode, $prefix, $method->class];
313318
}
314319

315320
private function getPhpDocNode(string $rawDocNode): PhpDocNode

src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -261,19 +261,13 @@ public function getReadInfo(string $class, string $property, array $context = []
261261
foreach ($this->accessorPrefixes as $prefix) {
262262
$methodName = $prefix.$camelProp;
263263

264-
if ($reflClass->hasMethod($methodName) && $reflClass->getMethod($methodName)->getModifiers() & $this->methodReflectionFlags && !$reflClass->getMethod($methodName)->getNumberOfRequiredParameters()) {
265-
$method = $reflClass->getMethod($methodName);
266-
267-
return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisibilityForMethod($method), $method->isStatic(), false);
264+
if ($reflClass->hasMethod($methodName) && ($m = $reflClass->getMethod($methodName))->getModifiers() & $this->methodReflectionFlags && !$m->getNumberOfRequiredParameters() && !\in_array((string) $m->getReturnType(), ['void', 'never'], true)) {
265+
return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, $this->getReadVisibilityForMethod($m), $m->isStatic(), false);
268266
}
269267
}
270268

271-
if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($reflClass->getMethod($getsetter)->getModifiers() & $this->methodReflectionFlags)) {
272-
$method = $reflClass->getMethod($getsetter);
273-
// Only consider jQuery-style accessors when they don't require parameters
274-
if (!$method->getNumberOfRequiredParameters()) {
275-
return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisibilityForMethod($method), $method->isStatic(), false);
276-
}
269+
if ($allowGetterSetter && $reflClass->hasMethod($getsetter) && ($m = $reflClass->getMethod($getsetter))->getModifiers() & $this->methodReflectionFlags && !$m->getNumberOfRequiredParameters() && !\in_array((string) $m->getReturnType(), ['void', 'never'], true)) {
270+
return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $getsetter, $this->getReadVisibilityForMethod($m), $m->isStatic(), false);
277271
}
278272

279273
if ($allowMagicGet && $reflClass->hasMethod('__get') && (($r = $reflClass->getMethod('__get'))->getModifiers() & $this->methodReflectionFlags)) {

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpDocExtractorTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\PropertyInfo\Tests\Fixtures\PseudoTypeDummy;
2323
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait;
2424
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait;
25+
use Symfony\Component\PropertyInfo\Tests\Fixtures\VoidNeverReturnTypeDummy;
2526
use Symfony\Component\PropertyInfo\Type;
2627

2728
/**
@@ -476,6 +477,18 @@ public static function promotedPropertyProvider(): array
476477
['promotedAndMutated', [new Type(Type::BUILTIN_TYPE_STRING)]],
477478
];
478479
}
480+
481+
public function testSkipVoidNeverReturnTypeAccessors()
482+
{
483+
// Methods that return void or never should be skipped, so no types should be extracted
484+
$this->assertNull($this->extractor->getTypes(VoidNeverReturnTypeDummy::class, 'voidProperty'));
485+
$this->assertNull($this->extractor->getTypes(VoidNeverReturnTypeDummy::class, 'neverProperty'));
486+
// Normal getter should still work
487+
$types = $this->extractor->getTypes(VoidNeverReturnTypeDummy::class, 'normalProperty');
488+
$this->assertNotNull($types);
489+
$this->assertCount(1, $types);
490+
$this->assertEquals(Type::BUILTIN_TYPE_STRING, $types[0]->getBuiltinType());
491+
}
479492
}
480493

481494
class EmptyDocBlock

src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\AnotherNamespace\DummyInAnotherNamespace;
2929
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsedInTrait;
3030
use Symfony\Component\PropertyInfo\Tests\Fixtures\TraitUsage\DummyUsingTrait;
31+
use Symfony\Component\PropertyInfo\Tests\Fixtures\VoidNeverReturnTypeDummy;
3132
use Symfony\Component\PropertyInfo\Type;
3233

3334
require_once __DIR__.'/../Fixtures/Extractor/DummyNamespace.php';
@@ -586,6 +587,13 @@ class: Dummy::class,
586587
],
587588
];
588589
}
590+
591+
public function testSkipVoidNeverReturnTypeAccessors()
592+
{
593+
$this->assertNull($this->extractor->getTypes(VoidNeverReturnTypeDummy::class, 'voidProperty'));
594+
$this->assertNull($this->extractor->getTypes(VoidNeverReturnTypeDummy::class, 'neverProperty'));
595+
$this->assertEquals([new Type(Type::BUILTIN_TYPE_STRING)], $this->extractor->getTypes(VoidNeverReturnTypeDummy::class, 'normalProperty'));
596+
}
589597
}
590598

591599
class PhpStanOmittedParamTagTypeDocBlock

src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use Symfony\Component\PropertyInfo\Tests\Fixtures\SnakeCaseDummy;
3131
use Symfony\Component\PropertyInfo\Tests\Fixtures\UnderscoreDummy;
3232
use Symfony\Component\PropertyInfo\Tests\Fixtures\VirtualProperties;
33+
use Symfony\Component\PropertyInfo\Tests\Fixtures\VoidNeverReturnTypeDummy;
3334
use Symfony\Component\PropertyInfo\Type;
3435

3536
/**
@@ -560,7 +561,7 @@ public static function readAccessorProvider(): array
560561
[Dummy::class, 'static', true, PropertyReadInfo::TYPE_METHOD, 'getStatic', PropertyReadInfo::VISIBILITY_PUBLIC, true],
561562
[Dummy::class, 'foo', true, PropertyReadInfo::TYPE_PROPERTY, 'foo', PropertyReadInfo::VISIBILITY_PUBLIC, false],
562563
[Php71Dummy::class, 'foo', true, PropertyReadInfo::TYPE_METHOD, 'getFoo', PropertyReadInfo::VISIBILITY_PUBLIC, false],
563-
[Php71Dummy::class, 'buz', true, PropertyReadInfo::TYPE_METHOD, 'getBuz', PropertyReadInfo::VISIBILITY_PUBLIC, false],
564+
[Php71Dummy::class, 'buz', false, null, null, null, null],
564565
[UnderscoreDummy::class, '_', true, PropertyReadInfo::TYPE_METHOD, 'get_', PropertyReadInfo::VISIBILITY_PUBLIC, false],
565566
[UnderscoreDummy::class, '__', true, PropertyReadInfo::TYPE_METHOD, 'get__', PropertyReadInfo::VISIBILITY_PUBLIC, false],
566567
[UnderscoreDummy::class, 'foo_bar', false, null, null, null, null],
@@ -827,4 +828,13 @@ public static function camelizeProvider(): iterable
827828
yield 'no underscore' => ['fooBar', 'FooBar'];
828829
yield 'pascal case' => ['FooBar', 'FooBar'];
829830
}
831+
832+
public function testSkipVoidNeverReturnTypeAccessors()
833+
{
834+
$this->assertFalse($this->extractor->isReadable(VoidNeverReturnTypeDummy::class, 'voidProperty'));
835+
$this->assertFalse($this->extractor->isReadable(VoidNeverReturnTypeDummy::class, 'neverProperty'));
836+
$this->assertTrue($this->extractor->isReadable(VoidNeverReturnTypeDummy::class, 'normalProperty'));
837+
$this->assertNull($this->extractor->getReadInfo(VoidNeverReturnTypeDummy::class, 'voidProperty'));
838+
$this->assertNull($this->extractor->getReadInfo(VoidNeverReturnTypeDummy::class, 'neverProperty'));
839+
}
830840
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
13+
14+
class VoidNeverReturnTypeDummy
15+
{
16+
public string $normalProperty = 'value';
17+
18+
/**
19+
* @return string
20+
*/
21+
public function getNormalProperty(): string
22+
{
23+
return $this->normalProperty;
24+
}
25+
26+
public function getVoidProperty(): void
27+
{
28+
// This looks like a getter but returns void, should be ignored
29+
}
30+
31+
public function getNeverProperty(): never
32+
{
33+
// This looks like a getter but returns never, should be ignored
34+
throw new \Exception('Never returns');
35+
}
36+
37+
public function setValue(): void
38+
{
39+
// This looks like a setter but has no parameters, should be ignored as accessor
40+
}
41+
42+
public function setNeverValue(): never
43+
{
44+
// This looks like a setter but has no parameters and returns never, should be ignored as accessor
45+
throw new \Exception('Never returns');
46+
}
47+
}
48+

src/Symfony/Component/Serializer/Mapping/Loader/AttributeLoader.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,12 @@ public function loadClassMetadata(ClassMetadataInterface $classMetadata): bool
125125
}
126126
$name = $method->name;
127127

128-
if (0 === stripos($name, 'get') && $method->getNumberOfRequiredParameters()) {
128+
if (0 !== stripos($name, 'set') && ($method->getNumberOfRequiredParameters() || \in_array((string) $method->getReturnType(), ['void', 'never'], true))) {
129129
continue; /* matches the BC behavior in `Symfony\Component\Serializer\Normalizer\ObjectNormalizer::extractAttributes` */
130130
}
131131

132132
$accessorOrMutator = match ($name[0]) {
133-
's' => str_starts_with($name, 'set') && isset($name[$i = 3]),
133+
's' => str_starts_with($name, 'set') && isset($name[$i = 3]) && $method->getNumberOfParameters(),
134134
'g' => str_starts_with($name, 'get') && isset($name[$i = 3]),
135135
'h' => str_starts_with($name, 'has') && isset($name[$i = 3]),
136136
'c' => str_starts_with($name, 'can') && isset($name[$i = 3]),

0 commit comments

Comments
 (0)