Skip to content

Add reflection classes to mutators. Do no mutate public method visibility if parent has the same one#67

Merged
maks-rafalko merged 8 commits intomasterfrom
reflection2
Mar 5, 2018
Merged

Add reflection classes to mutators. Do no mutate public method visibility if parent has the same one#67
maks-rafalko merged 8 commits intomasterfrom
reflection2

Conversation

@maks-rafalko
Copy link
Copy Markdown
Member

@maks-rafalko maks-rafalko commented Nov 6, 2017

This is an attempt to add native PHP Reflection to Mutator Operators. Fixes #63.

As a result:

  • Many useless mutants are not created anymore for PublicVisibility and ProtectedVisibility mutators (-130 mutants for infection itself).
  • Now we can start to think/implement new mutators that was not possible to implement before without Reflection.

Please note:

  • Now Infection loads project's autoload.php (see app/bootstrap.php) in order to instantiate \ReflectionClass() instances

Ho to prefix:

cd infection
composer install --no-dev
/path/to/php-scoper/bin/php-scoper add-prefix --output-dir=build-prefixed --force --prefix=Mutant

composer dump-autoload --working-dir=build-prefixed/ --classmap-authoritative --no-dev

sudo chmod +x build-prefixed/bin/infection

Feedback/review is much appreciated.


UPD: Finally, I was able to prefix Infection lib with PHP-Scoper without errors, got the same MSI/Covered MSI, number of mutations, mutated files with Prefixed PHAR as with the original source code.

@maks-rafalko maks-rafalko changed the title [WIP] Add reflection classes to mutators. Do no mutate protected method visibility if parent has the same one [WIP] Add reflection classes to mutators. Do no mutate public method visibility if parent has the same one Nov 6, 2017
@maks-rafalko maks-rafalko self-assigned this Nov 9, 2017
@BackEndTea
Copy link
Copy Markdown
Member

Both humbug/php-scoper#127 and nikic/PHP-Parser#441 are fixed now.

@theofidry
Copy link
Copy Markdown
Member

I would try PHP-Scoper a bit later and in a dedicated PR as I'm sure it's gonna be a work on its own. Infection may be fine itself, but guaranteed to find weird cases with PhpSpec & PHPUnit

@maks-rafalko
Copy link
Copy Markdown
Member Author

Both humbug/php-scoper#127 and nikic/PHP-Parser#441 are fixed now.

great. Will continue working on this PR next week

but guaranteed to find weird cases with PhpSpec & PHPUnit

it shouldn't affect prefixing Infection as I understand since we don't have such dependencies in --no-dev mode

@maks-rafalko maks-rafalko force-pushed the reflection2 branch 6 times, most recently from 92fb97e to fea4fd7 Compare February 6, 2018 20:02
@maks-rafalko
Copy link
Copy Markdown
Member Author

maks-rafalko commented Feb 6, 2018

@infection/core I think this is ready for review.

I've updated initial comment, please see details. If you have some time - could you please check it with some of your project(s)? With prefixed PHAR and as a dependency.

Not sure why builds are failing on PHP 7.2 with phpdbg. Didn't dig in yet.

@theofidry your feedback on PHP-Scoper usage is highly appreciated

@sidz
Copy link
Copy Markdown
Member

sidz commented Feb 10, 2018

@borNfreee I've run Infection from this PR under phpdbg (php7.2) and got the same issue with segfault. Potentially it's an issue with php itself. It'll be nice to submit an issue (if it's an php issue) to the php bug-tracker

@BackEndTea BackEndTea mentioned this pull request Feb 11, 2018
4 tasks
@maks-rafalko
Copy link
Copy Markdown
Member Author

Potentially it's an issue with php itself

I was able to reproduce it with PHP 7.2.2 today. It fails with phpdbg -qrr vendor/bin/phpunit but works with php vendor/bin/phpunit.

Will continue git-bisecting tomorrow.

Copy link
Copy Markdown
Member

@theofidry theofidry left a comment

Choose a reason for hiding this comment

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

I left some comments. I'll have to check that in more details as there's significant changes I need to pay more attention to it.

Regarding PHP Scoper I think it's mandatory to have an e2e test with the scoped file if you wanna integrate it. That said maybe this can be done in a separate PR.

Also I didn't see any change on how we are building the PHAR to pick up the scoped files, did I miss something?

scoper.inc.php Outdated
// heart contents.
//
// For more see: https://github.com/humbug/php-scoper#patchers
'patchers' => [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you can remove this entry as you're not using it atm

scoper.inc.php Outdated
// (the class name) as an argument and return a boolean (true meaning the class is going to prefixed).
//
// For more, see https://github.com/humbug/php-scoper#global-namespace-whitelisting
'global_namespace_whitelist' => [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we need this

scoper.inc.php Outdated
// that this does not work with functions or constants neither with classes belonging to the global namespace.
//
// Fore more see https://github.com/humbug/php-scoper#whitelist
'whitelist' => [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

neither this one

CONTENT;
}

private function getInterceptorNamespacePrefix()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you can add the string return type

$this->namespace = $node->name;
} elseif ($node instanceof Stmt\ClassLike) {
if (null !== $node->name) {
$node->fullyQualifiedClassName = Name::concat($this->namespace, $node->name);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this supposed to work for any class? Because if that's the case this won't be enough as you need to account for when an alias is used with the use statement (if any)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure I got it. This visitor's goal is to build FQCL for the current class, not for other imported (via use statement) classes.

When we have:

namespace Infection\Mutator;

use SomeClass;
use Some\OtherClass as Aliased;

class Plus {
}

this visitor should build a Infection\Mutator\Plus FQCN and save it to the node to fullyQualifiedClassName property.

It does not need to work with SomeClass, OtherClass, Aliased.

Then we use it to build reflection in ReflectionVisitor:

if ($node instanceof Node\Stmt\ClassLike) {
       $this->reflectionClass = new \ReflectionClass($node->fullyQualifiedClassName->toString());
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh right, I had my head too much in php-scoper. Should be alright then :)


$mutatedCode = $this->mutate($code);

$expectedMutatedCode = <<<'CODE'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think 'PHP' would be better here as IDEs can pick that up for inspections


public function test_it_does_not_mutate_if_parent_class_has_same_protected_method()
{
$code = file_get_contents(__DIR__ . '/../../Fixtures/Autoloaded/ProtectedVisibility/pv-same-method-parent.php');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

instead of having __DIR__ . '/../../Fixtures/Autoloaded[...] all the time everywhere maybe you can introduce a constant in the test case with the base path

Copy link
Copy Markdown
Member

@sidz sidz left a comment

Choose a reason for hiding this comment

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

Ok with me 👌

@BackEndTea
Copy link
Copy Markdown
Member

Now Infection loads project's autoload.php (see app/bootstrap.php) in order to instantiate \ReflectionClass() instances

I don't know how relevant this is, but maybe we can allow users to define the location of the autoload file in the infection config. Similar to how phpunit does things.

@maks-rafalko
Copy link
Copy Markdown
Member Author

maks-rafalko commented Feb 25, 2018

Finally, rebased on master, ready for review again.

Regarding PHPScoper config, I think we will use Theo's one from humbug/php-scoper#172 later (but here PHPParser has been upgraded, so not all patchers are needed)

Also, I think we will release 0.8.0 and this PR will be shipped with the next major release as it is not ready yet.

I don't know how relevant this is, but maybe we can allow users to define the location of the autoload file in the infection config. Similar to how phpunit does things.

what does it bring for us?

Copy link
Copy Markdown
Member

@sidz sidz left a comment

Choose a reason for hiding this comment

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

Still ok 👍


public function enterNode(Node $node)
{
if ($node instanceof Node\Stmt\ClassLike) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What happens here if we declare an anonymous class within our class?
Will all the nodes after that still be seen as instances of that anonymous class?

Copy link
Copy Markdown
Member

@BackEndTea BackEndTea Feb 26, 2018

Choose a reason for hiding this comment

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

It crashes on anonymous classes in my test cases.
This could be fixed by

    public function enterNode(Node $node)
    {
        if ($node instanceof Stmt\Namespace_) {
            $this->namespace = $node->name;
        } elseif ($node instanceof Stmt\ClassLike) {
            if (null !== $node->name) {
                $node->fullyQualifiedClassName = Name::concat($this->namespace, $node->name);
            } else {
                $node->fullyQualifiedClassName = null;
            }
        }
    }

For the FullyQualifiedClassNameVisitor

and

    
public function enterNode(Node $node)
    {
        if ($node instanceof Node\Stmt\ClassLike && $node->fullyQualifiedClassName !== null) {
            $this->reflectionClass = new \ReflectionClass($node->fullyQualifiedClassName->toString());
        }

in the ReflectionVisitor

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thank you @BackEndTea, I've added a test case.

@BackEndTea
Copy link
Copy Markdown
Member

what does it bring for us?

I realize now that it not that usefull for us. Phpunits version is for bootstrapping an application, not just autoloading

@BackEndTea
Copy link
Copy Markdown
Member

Looks good to me. Id just like to see a test for the reflection visitor that checks if the reflection class is correctly set.

…ibility if parent has the same one

Signed-off-by: borNfreee <b0rn@list.ru>

User the correct namespace for Interceptor when code is prefixed with random character(s)

Signed-off-by: borNfreee <b0rn@list.ru>

Rebase and refactor. Add more tests

Update PHPParser to 3.1.4

Do not analyze Fixture files by PHPStan as it loads the code and can't parse PHP7.1 syntax. Skip tests on PHP7.0 that requires 7.1+

Do not analyze Fixture files by PHPStan as it loads the code and can't parse PHP7.1 syntax. Skip tests on PHP7.0 that requires 7.1+

Add PHP-Scoper config. Decrease MSI since phpdbg results with the lower number than xdebug

Rebase, resolve conflicts

Remove empty files with PHP extension. This causes a segfault with phpdbg on PHP 7.2
Copy link
Copy Markdown
Member

@BackEndTea BackEndTea left a comment

Choose a reason for hiding this comment

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

Looks good to me 👍

return false;
}

if ($this->hasSameProtectedParentMethod($node)) {
Copy link
Copy Markdown
Member

@BackEndTea BackEndTea Feb 27, 2018

Choose a reason for hiding this comment

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

Just a small thing, but maybe its slightly faster to first check if the node is protected, before checking if it has a protected parent.

And ofcourse the same goes for public

@maks-rafalko
Copy link
Copy Markdown
Member Author

Ok, I think it's ready to be merged (this PR also unlocks Profiles implementation).

Thanks everyone for the review and help.

@maks-rafalko maks-rafalko merged commit 926372c into master Mar 5, 2018
@maks-rafalko maks-rafalko deleted the reflection2 branch March 5, 2018 19:35
@maks-rafalko maks-rafalko added this to the 0.9.0 milestone Mar 5, 2018
@maks-rafalko maks-rafalko changed the title [WIP] Add reflection classes to mutators. Do no mutate public method visibility if parent has the same one Add reflection classes to mutators. Do no mutate public method visibility if parent has the same one Mar 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Do not mutate function signature of inherited methods

4 participants