Skip to content
Draft
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
54 changes: 54 additions & 0 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,60 @@ jobs:
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}

mutation-tests-phpstan:
name: "Mutation tests with PHPStan"

runs-on: ${{ matrix.operating-system }}

strategy:
matrix:
dependencies:
- "locked"
php-version:
- "8.4"
operating-system:
- "ubuntu-latest"

steps:
- name: "Checkout"
uses: "actions/checkout@v4"

- name: "Install PHP"
uses: "shivammathur/[email protected]"
with:
coverage: "xdebug"
php-version: "${{ matrix.php-version }}"
extensions: intl, sodium
ini-values: memory_limit=-1, zend.assertions=1

- name: "Install dependencies"
uses: "ramsey/[email protected]"
with:
dependency-versions: "${{ matrix.dependencies }}"

- name: "Install CI dependencies"
uses: "ramsey/[email protected]"
with:
dependency-versions: "${{ matrix.dependencies }}"
working-directory: "tools"
custom-cache-suffix: "ci"

- name: "Warmup PHPStan"
run: "tools/vendor/bin/phpstan analyse --memory-limit=-1"

- name: "Infection"
run: "tools/vendor/bin/phpstan-mutant-killer-infection-runner --phpstan-config phpstan.neon --log log.txt --threads=$(nproc)"
env:
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}

- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: log.txt
path: log.txt
if-no-files-found: error

compatibility:
name: "Test Compatibility"

Expand Down
4 changes: 1 addition & 3 deletions src/Reflection/ReflectionMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@
/** @return int-mask-of<ReflectionMethodAdapter::IS_*> */
private function computeModifiers(MethodNode|Node\PropertyHook $node): int
{
$modifiers = 0;
$modifiers = $node->isFinal() ? CoreReflectionMethod::IS_FINAL : 0;

if ($node instanceof MethodNode) {
$modifiers += $node->isStatic() ? CoreReflectionMethod::IS_STATIC : 0;
Expand All @@ -317,8 +317,6 @@
$modifiers += $node->isAbstract() ? CoreReflectionMethod::IS_ABSTRACT : 0;
}

$modifiers += $node->isFinal() ? CoreReflectionMethod::IS_FINAL : 0;

return $modifiers;
}

Expand Down Expand Up @@ -348,7 +346,7 @@
*/
public function isAbstract(): bool
{
return (bool) ($this->modifiers & CoreReflectionMethod::IS_ABSTRACT)

Check warning on line 349 in src/Reflection/ReflectionMethod.php

View workflow job for this annotation

GitHub Actions / Mutation tests with PHPStan (locked, 8.4, ubuntu-latest)

Escaped Mutant for Mutator "CastBool": @@ @@ */ public function isAbstract(): bool { - return (bool) ($this->modifiers & CoreReflectionMethod::IS_ABSTRACT) || $this->declaringClass->isInterface(); + return $this->modifiers & CoreReflectionMethod::IS_ABSTRACT || $this->declaringClass->isInterface(); } /** * Is the method final.
|| $this->declaringClass->isInterface();
}

Expand Down
3 changes: 2 additions & 1 deletion src/Reflection/ReflectionObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
/** @psalm-immutable */
class ReflectionObject extends ReflectionClass
{
protected function __construct(private Reflector $reflector, private ReflectionClass $reflectionClass, private object $object)
private function __construct(private Reflector $reflector, private ReflectionClass $reflectionClass, private object $object)
{
}

Expand Down Expand Up @@ -117,6 +117,7 @@ private function createPropertyNodeFromRuntimePropertyReflection(CoreReflectionP
{
$builder = new PropertyNodeBuilder($property->getName());
$builder->setDefault($property->getValue($instance));
// @infection-ignore-all MethodCallRemoval: The call is not necessary because the property is public by default, it's just for clarity
$builder->makePublic();

return $builder->getNode();
Expand Down
2 changes: 2 additions & 0 deletions src/Reflection/ReflectionProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ public function hasType(): bool

public function isVirtual(): bool
{
// @infection-ignore-all AssignCoalesce: It's just optimization
$this->cachedVirtual ??= $this->createCachedVirtual();

return $this->cachedVirtual;
Expand All @@ -621,6 +622,7 @@ public function getHook(ReflectionPropertyHookType $hookType): ReflectionMethod|
/** @return array{get?: ReflectionMethod, set?: ReflectionMethod} */
public function getHooks(): array
{
// @infection-ignore-all AssignCoalesce: It's just optimization
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can possibly disable the entire mutator? It will always catch optimizations

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(unless there's a way to declare these optimizations at type level, perhaps?)

$this->cachedHooks ??= $this->createCachedHooks();

return $this->cachedHooks;
Expand Down
39 changes: 39 additions & 0 deletions test/unit/Fixture/PropertyHooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,45 @@ class GetAndSetPropertyHook extends GetPropertyHook
}
}

class SetPropertyHook
{
public string $hook {
set (string $value) {
$this->hook = $value;
}
}
}

class SetAndGetPropertyHook extends SetPropertyHook
{
public string $hook {
get {
return 'hook';
}
}
}

class BothPropertyHooks
{
public string $hook {
get {
return 'hook';
}
set (string $value) {
$this->hook = $value;
}
}
}

class ExtendedHooks extends BothPropertyHooks
{
public string $hook {
get {
return 'hook2';
}
}
}

trait PropertyHookTrait
{
public string $hook {
Expand Down
11 changes: 11 additions & 0 deletions test/unit/Reflection/ReflectionClassConstantTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,34 @@ public function testDefaultVisibility(): void
{
$const = $this->getExampleConstant('MY_CONST_1');
self::assertTrue($const->isPublic());
self::assertFalse($const->isProtected());
self::assertFalse($const->isPrivate());
self::assertFalse($const->isFinal());
}

public function testOnlyPublicVisibility(): void
{
$const = $this->getExampleConstant('MY_CONST_3');
self::assertTrue($const->isPublic());
self::assertFalse($const->isProtected());
self::assertFalse($const->isPrivate());
self::assertFalse($const->isFinal());
}

public function testOnlyProtectedVisibility(): void
{
$const = $this->getExampleConstant('MY_CONST_4');
self::assertFalse($const->isPublic());
self::assertTrue($const->isProtected());
self::assertFalse($const->isPrivate());
self::assertFalse($const->isFinal());
}

public function testPrivateVisibility(): void
{
$const = $this->getExampleConstant('MY_CONST_5');
self::assertFalse($const->isPublic());
self::assertFalse($const->isProtected());
self::assertTrue($const->isPrivate());
self::assertFalse($const->isFinal());
}
Expand All @@ -94,7 +103,9 @@ public function testPublicFinal(): void
public function testProtectedFinal(): void
{
$const = $this->getExampleConstant('MY_CONST_7');
self::assertFalse($const->isPublic());
self::assertTrue($const->isProtected());
self::assertFalse($const->isPrivate());
self::assertTrue($const->isFinal());
}

Expand Down
2 changes: 2 additions & 0 deletions test/unit/Reflection/ReflectionClassTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Roave\BetterReflection\Reflection\ReflectionClassConstant;
use Roave\BetterReflection\Reflection\ReflectionMethod;
use Roave\BetterReflection\Reflection\ReflectionNamedType;
use Roave\BetterReflection\Reflection\ReflectionObject;
use Roave\BetterReflection\Reflection\ReflectionParameter;
use Roave\BetterReflection\Reflection\ReflectionProperty;
use Roave\BetterReflection\Reflection\ReflectionUnionType;
Expand Down Expand Up @@ -108,6 +109,7 @@
use function uniqid;

#[CoversClass(ReflectionClass::class)]
#[CoversClass(ReflectionObject::class)]
class ReflectionClassTest extends TestCase
{
private Locator $astLocator;
Expand Down
67 changes: 49 additions & 18 deletions test/unit/Reflection/ReflectionPropertyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,11 @@ public function testIsReadOnly(): void

$notReadOnlyProperty = $classInfo->getProperty('publicProperty');
self::assertFalse($notReadOnlyProperty->isReadOnly());
self::assertTrue($notReadOnlyProperty->isPublic());

$readOnlyProperty = $classInfo->getProperty('readOnlyProperty');
self::assertTrue($readOnlyProperty->isPublic());
self::assertTrue($readOnlyProperty->isReadOnly());
self::assertTrue($readOnlyProperty->isPublic());
}

public function testIsFinal(): void
Expand All @@ -204,6 +205,7 @@ public function testIsFinal(): void

$notReadOnlyProperty = $classInfo->getProperty('publicProperty');
self::assertFalse($notReadOnlyProperty->isFinal());
self::assertTrue($notReadOnlyProperty->isPublic());

$finalPublicProperty = $classInfo->getProperty('finalPublicProperty');
self::assertTrue($finalPublicProperty->isFinal());
Expand All @@ -215,6 +217,7 @@ public function testIsNotAbstract(): void
$classInfo = $this->reflector->reflectClass(ExampleClass::class);

$notAbstractProperty = $classInfo->getProperty('publicProperty');
self::assertTrue($notAbstractProperty->isPublic());
self::assertFalse($notAbstractProperty->isAbstract());
}

Expand All @@ -227,6 +230,7 @@ public function testIsReadOnlyInReadOnlyClass(): void
$classInfo = $reflector->reflectClass('\\Roave\\BetterReflectionTest\\Fixture\\ReadOnlyClass');

$property = $classInfo->getProperty('property');
self::assertTrue($property->isPrivate());
self::assertTrue($property->isReadOnly());
}

Expand Down Expand Up @@ -265,25 +269,32 @@ public function testGetDocCommentReturnsNullWithNoComment(): void
self::assertNull($property->getDocComment());
}

/** @return list<array{0: non-empty-string, 1: int-mask-of<ReflectionPropertyAdapter::IS_*>}> */
/** @return list<array{0: non-empty-string, 1: class-string, 2: non-empty-string, 3: int-mask-of<ReflectionPropertyAdapter::IS_*>}> */
public static function modifierProvider(): array
{
return [
['publicProperty', CoreReflectionProperty::IS_PUBLIC],
['protectedProperty', CoreReflectionProperty::IS_PROTECTED],
['privateProperty', CoreReflectionProperty::IS_PRIVATE],
['publicStaticProperty', CoreReflectionProperty::IS_PUBLIC | CoreReflectionProperty::IS_STATIC],
['readOnlyProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_READONLY | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY],
['finalPublicProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY],
[__DIR__ . '/../Fixture/ExampleClass.php', 'Roave\BetterReflectionTest\Fixture\ExampleClass', 'publicProperty', CoreReflectionProperty::IS_PUBLIC],
[__DIR__ . '/../Fixture/ExampleClass.php', 'Roave\BetterReflectionTest\Fixture\ExampleClass', 'protectedProperty', CoreReflectionProperty::IS_PROTECTED],
[__DIR__ . '/../Fixture/ExampleClass.php', 'Roave\BetterReflectionTest\Fixture\ExampleClass', 'privateProperty', CoreReflectionProperty::IS_PRIVATE],
[__DIR__ . '/../Fixture/ExampleClass.php', 'Roave\BetterReflectionTest\Fixture\ExampleClass', 'publicStaticProperty', CoreReflectionProperty::IS_PUBLIC | CoreReflectionProperty::IS_STATIC],
[__DIR__ . '/../Fixture/ExampleClass.php', 'Roave\BetterReflectionTest\Fixture\ExampleClass', 'readOnlyProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_READONLY | ReflectionPropertyAdapter::IS_PROTECTED_SET_COMPATIBILITY],
[__DIR__ . '/../Fixture/ExampleClass.php', 'Roave\BetterReflectionTest\Fixture\ExampleClass', 'finalPublicProperty', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY],
[__DIR__ . '/../Fixture/PropertyHooks.php', 'Roave\BetterReflectionTest\Fixture\AbstractPropertyHooks', 'hook', CoreReflectionProperty::IS_PUBLIC | ReflectionPropertyAdapter::IS_ABSTRACT_COMPATIBILITY | ReflectionPropertyAdapter::IS_VIRTUAL_COMPATIBILITY],
];
}

/** @param non-empty-string $propertyName */
/**
* @param non-empty-string $fileName
* @param class-string $className
* @param non-empty-string $propertyName
*/
#[DataProvider('modifierProvider')]
public function testGetModifiers(string $propertyName, int $expectedModifier): void
public function testGetModifiers(string $fileName, string $className, string $propertyName, int $expectedModifier): void
{
$classInfo = $this->reflector->reflectClass(ExampleClass::class);
$property = $classInfo->getProperty($propertyName);
$reflector = new DefaultReflector(new SingleFileSourceLocator($fileName, $this->astLocator));
$classInfo = $reflector->reflectClass($className);

$property = $classInfo->getProperty($propertyName);

self::assertSame($expectedModifier, $property->getModifiers());
}
Expand Down Expand Up @@ -972,25 +983,29 @@ public function testIsPrivateSet(): void
self::assertTrue($protectedPrivateSet->isPrivateSet());
}

/** @return list<array{0: non-empty-string, 1: bool}> */
/** @return list<array{0: non-empty-string, 1: bool, 2: int-mask-of<ReflectionPropertyAdapter::IS_*>}> */
public static function asymmetricVisibilityImplicitFinalProvider(): array
{
return [
['publicPrivateSetIsFinal', true],
['protectedPrivateSetIsFinal', true],
['privatePrivateSetIsNotFinal', false],
['publicPrivateSetIsFinal', true, ReflectionPropertyAdapter::IS_PUBLIC | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY],
['protectedPrivateSetIsFinal', true, ReflectionPropertyAdapter::IS_PROTECTED | ReflectionPropertyAdapter::IS_PRIVATE_SET_COMPATIBILITY | ReflectionPropertyAdapter::IS_FINAL_COMPATIBILITY],
['privatePrivateSetIsNotFinal', false, ReflectionPropertyAdapter::IS_PRIVATE],
];
}

/** @param non-empty-string $propertyName */
/**
* @param non-empty-string $propertyName
* @param int-mask-of<ReflectionPropertyAdapter::IS_*> $modifiers
*/
#[DataProvider('asymmetricVisibilityImplicitFinalProvider')]
public function testAsymmetricVisibilityImplicitFinal(string $propertyName, bool $isFinal): void
public function testAsymmetricVisibilityImplicitFinal(string $propertyName, bool $isFinal, int $modifiers): void
{
$reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/AsymmetricVisibilityImplicitFinal.php', $this->astLocator));
$classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\AsymmetricVisibilityImplicitFinal');
$property = $classInfo->getProperty($propertyName);

self::assertSame($isFinal, $property->isFinal());
self::assertSame($modifiers, $property->getModifiers());
}

/** @return list<array{0: non-empty-string, 1: bool}> */
Expand Down Expand Up @@ -1023,6 +1038,7 @@ public function testIsAbstract(): void

$hookProperty = $classInfo->getProperty('hook');
self::assertTrue($hookProperty->isAbstract());
self::assertTrue($hookProperty->isPublic());
}

public function testIsAbstractInInterface(): void
Expand All @@ -1032,6 +1048,7 @@ public function testIsAbstractInInterface(): void

$abstractProperty = $classInfo->getProperty('abstractPropertyFromInterface');
self::assertTrue($abstractProperty->isAbstract());
self::assertTrue($abstractProperty->isPublic());
}

public function testNoHooks(): void
Expand Down Expand Up @@ -1212,6 +1229,20 @@ public function testExtendingHooks(): void
self::assertSame('Roave\BetterReflectionTest\Fixture\GetAndSetPropertyHook', $getAndSetHookProperty->getHook(ReflectionPropertyHookType::Set)->getDeclaringClass()->getName());
}

public function testExtendingHooks2(): void
{
$reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator));

$classInfo = $reflector->reflectClass('Roave\BetterReflectionTest\Fixture\ExtendedHooks');

$getAndSetHookProperty = $classInfo->getProperty('hook');
self::assertCount(2, $getAndSetHookProperty->getHooks());
self::assertTrue($getAndSetHookProperty->hasHook(ReflectionPropertyHookType::Get));
self::assertTrue($getAndSetHookProperty->hasHook(ReflectionPropertyHookType::Set));
self::assertSame('Roave\BetterReflectionTest\Fixture\ExtendedHooks', $getAndSetHookProperty->getHook(ReflectionPropertyHookType::Get)->getDeclaringClass()->getName());
self::assertSame('Roave\BetterReflectionTest\Fixture\BothPropertyHooks', $getAndSetHookProperty->getHook(ReflectionPropertyHookType::Set)->getDeclaringClass()->getName());
}

public function testUseHookFromTrait(): void
{
$reflector = new DefaultReflector(new SingleFileSourceLocator(__DIR__ . '/../Fixture/PropertyHooks.php', $this->astLocator));
Expand Down
7 changes: 5 additions & 2 deletions tools/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"phpstan/phpstan-phpunit": "^2.0.6",
"vimeo/psalm": "^6.11.0",
"roave/backward-compatibility-check": "^8.13.0",
"roave/infection-static-analysis-plugin": "^1.37.0"
"roave/infection-static-analysis-plugin": "^1.38.0",
"phpstan/mutant-killer-infection-runner": "1.0.x-dev"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a note here: let's get a stable (even if 0.x) release out, before going with this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not probably going to release this package, there's a bug that causes PHPStan not be called for some mutants and I don't know why. I talked to Maks at PHPers over the weekend and the official support in Infection should land very soon.

We will continue working on this, mostly for performance improvements, but there's no reason to delay the feature in Infection itself.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

offtop:

I talked to Maks at PHPers over the weekend and the official support in Infection should land very soon.

confirm, I do my best on bringing it to Infection natively, I think by the end of the week or early next week we will have at least opened PR with draft implementation.

Unfortunately, to support static analysis, I have to prepare other things to allow proper parallelization of PHPStan processes, so some architecture changes are required on Infection side first, thus it's not quick.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think by the end of the week or early next week we will have at least opened PR with draft implementation.

Just to be clear: nobody is chasing anybody :-)

What matters most is that there's a plan 👍

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPD: #1510

},
"config": {
"allow-plugins": {
Expand All @@ -20,5 +21,7 @@
"psr-4": {
"Roave\\BetterReflection\\": "../src"
}
}
},
"minimum-stability": "dev",
"prefer-stable": true
}
Loading
Loading