Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude abstract methods from pointcut matching #335 #337

Conversation

TheCelavi
Copy link
Contributor

This is a first, working idea. I guess there is a better approach. There are no tests written yet, however, I have tested internally and this do fixes my problem that I have described.

Solution seams un-natural and hackish. I will explain in code general idea.

@lisachenko I would like from you to review it and propose a better/more elegant solution for this fix implementation.

Thanks.

/**
* Pointcut that filters out all class method and properties which are not declared within class
*/
class ClassDeclaresPointcut implements Pointcut
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Idea is to have a pointcut matcher that will filter out all non-declared members and properties that are matched.

Copy link
Member

Choose a reason for hiding this comment

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

Looks like it is already implemented in MatchInheritedPointcut 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, so it seams....

*/
public function getKind()
{
return PointFilter::KIND_METHOD | PointFilter::KIND_PROPERTY;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As I have understood - this will ensure that this pointcut matcher will be applied ONLY if subject of matching is class method and class property, right?

Otherwise, matches function will not be invoked, right?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, you are right, point filter effectively reduce the number of checks for concrete point filter. It's not ideal, but it's very efficient. KIND_METHOD filters only methods, KIND_PROPERTY filters properties in the class.

For future versions of framework I want to put this logic into matches() method. To be in one single place.

*/
public function parse(TokenStream $stream)
{
return new AndPointcut(parent::parse($stream), new ClassDeclaresPointcut());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, so this is why I think that this solution is kinda hackish...

Whatever our parser gets as a pointcut filter, it has to be additionally filtered. So it has to be applied last.

parent::parse($stream) -> will provide a pointcut that will match something, and that something can be a collection of properties and methods, right?

So, whatever methods and properties we got, we have to apply ClassDeclaresPointcut matcher to filter out non-declared properties and methods.

I guess that Grammar should give us this expression, is that even possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, crap, I forgot that we can add a node visitor that would modify AST and provide us with same result: https://github.com/jakubledl/dissect/blob/master/docs/ast.md#traversing-the-ast

I can write node visitor to modify AST in order to move this logic from PointcutParser to node visitor and grammar.

However - still need from you the verification of idea so I can proceed (write node visitor, tests, etc...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, seams wrong -> I should only wrap original Pointcut if getKind() is PROPERTY or METHOD, right?

Copy link
Member

Choose a reason for hiding this comment

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

You shouldn't touch the PointcutParser class at all, it's final :) Instead, all changes should be implemented in grammar and AST transforming, see PointcutGrammar class for examples how AST will be converted to the pointcut.

@lisachenko
Copy link
Member

Hi, Nikola! Thank you for researching this! But correct me if I'm wrong: you want to exclude all parent methods that aren't declared in current class, right?

If it's what you want to achieve, then there is special syntax for that: && !matchInherited(). You can see this syntax in PointcutParser test case

            // This will match all methods, but not inherited
            ['execution(public **->*(*)) && !matchInherited()'],

@TheCelavi
Copy link
Contributor Author

Yes. Ok, I got confused here for sure - why !matchInherited() is not applied by default?

Why would weaving of method/property occur on a class that does not declares it?

@TheCelavi
Copy link
Contributor Author

TheCelavi commented Aug 9, 2017

Ok, this is the issue of understanding what is the bug and what is not. This proposal is related to #335.

Lack of declaration existence check leads to issue #335

For me - there is no logic to weave method/property that is not declared within the class. If there is - I believe that it should be explicitly requested by pointcut expression, ie. !matchInherited() should be by default.

This was my usecase:

Interface X {
    public function foo();
}

abstract class Y implements X {

}

class Z extends Y {

    public function foo() {
    }
}

Booth Z and Y classes are weaved. It should be only Z. And that should be by default, right?

@lisachenko
Copy link
Member

Previously developers asked me why there is no matching for parent methods, issue about matching for parent methods was described in #131 and later I was implemented logic to match parent properties and methods, see #272 and #274. New logic was introduced in PR #279.

This was logical for me to match methods from parent classes during weaving. And filtration should be performed via pointcut definition.

Regarding your question about switching matching to declared members only be default - it's discussable. Maybe we can introduce new option for the kernel to allow/disable matching with parent members for classes?

@TheCelavi
Copy link
Contributor Author

TheCelavi commented Aug 9, 2017

Ok, THANK YOU - now we are getting somewhere. In conclusion, we can not change this behaviour, it would result with BC break.

Conclusion is: weaving will occur always on all matching methods/properties, regardless if it is defined in class or some of the parent class (unless ! matchInherited() is specified). This behaviour must be documented! So, this is on me.

Ok, so we have a bug, that is for sure, which is easy to define now:

  1. Pointcut MUST NOT match abstract method of class
  2. Pointcut MUST NOT match method of class that is declared in interface

Simple - what is not implemented in class hierarchy, can not be weaved.

Agree?

(if we can identify bug and agree upon what is a bug - we can solve it)

@lisachenko
Copy link
Member

lisachenko commented Aug 9, 2017

Pointcut MUST NOT match abstract method of class

Agree

Pointcut MUST NOT match method of class that is declared in interface

Actually, it's the same as the first check, because method from an interface will be abstract too in reflection. So resulting statement will be: "Pointcut MUST NOT match abstract methods"

@@ -47,6 +47,7 @@ class ModifierMatcherFilter implements PointFilter
public function __construct($initialMask = 0)
{
$this->andMask = $initialMask;
$this->notMask = \ReflectionMethod::IS_ABSTRACT;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@lisachenko Ok, it seams that you are right, modifier filter should have default value of \ReflectionMethod::IS_ABSTRACT for notMask .

Test passes, but what is worries me is that there are no a lot of integration/functional tests where we can test what we actually get (like what I did for console commands).

What do you think about writing some functional tests that would have real-life fixtures for verification of weaving?

Copy link
Member

Choose a reason for hiding this comment

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

I feel myself shame for tests :) I'd vote for unit tests for each class and only several functional tests for checking that whole system is working as expected and still able to transform files and perform weaving.

For this concrete class we can add just simple test case that modifier matcher filter by default doesn't match abstract methods.

One more caution: modifier filter is also used for property matching. Of course, "not abstract" for properties will be always true. But if we will use ModiferFilter for properties with negation, then it can be very unpredictable :)

@@ -166,6 +166,11 @@ private function getAdvicesFromAdvisor(

$mask = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED;
foreach ($class->getMethods($mask) as $method) {
// abstract methods can not be weaved
Copy link
Contributor Author

Choose a reason for hiding this comment

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

One more caution: modifier filter is also used for property matching. Of course, "not abstract" for properties will be always true. But if we will use ModiferFilter for properties with negation, then it can be very unpredictable :)

You are soooo right! Ok, how about this? Simply - advice matcher will skip abstract methods for weaving, regardless of pointcut expression used?

Copy link
Member

Choose a reason for hiding this comment

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

This solution is perfect! 👍

@lisachenko lisachenko changed the title Proposal for review of fix for #335 Exclude abstract methods from pointcut matching #335 Aug 10, 2017
Copy link
Member

@lisachenko lisachenko left a comment

Choose a reason for hiding this comment

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

Looking good now


abstract class AbstractBar implements FooInterface
{
public abstract function doSomethingElse();

Choose a reason for hiding this comment

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

The abstract declaration must precede the visibility declaration


protected static function exec($command, $args = '')
{
$process = new Process(sprintf('php %s %s %s %s',

Choose a reason for hiding this comment

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

Opening parenthesis of a multi-line function call must be the last content on the line

Copy link
Member

Choose a reason for hiding this comment

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

Better to follow PSR here and make it one-liner or extract sprintf part of expression into separate variable.

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Process;

abstract class BaseFunctionalTest extends TestCase
Copy link
Contributor Author

@TheCelavi TheCelavi Aug 11, 2017

Choose a reason for hiding this comment

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

@lisachenko When you find a time, please review tests as well.

I have cleaned up code a little bit, setup a base for functional tests, write a required test as well as modified (cleaned up) command tests...

I have one more thing on my mind, however, even with this setup it is going to be quite easy to write a bunch of functional tests that we need.


use Go\Aop\Aspect;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation as Pointcut;
Copy link
Member

Choose a reason for hiding this comment

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

👍 nice import of namespace name

protected static $projectDir = __DIR__.'/../../Fixtures/project';
protected static $aspectCacheDir = __DIR__.'/../../Fixtures/project/var/cache/aspect';
protected static $consolePath = __DIR__.'/../../Fixtures/project/bin/console';
protected static $frontControllerPath = __DIR__.'/../../Fixtures/project/web/index.php';
Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer to have these assignments aligned... It will be more readable.

Can we also replace these static properties with simple class properties?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not recommended at all. In order to be compatible with static calls, such as public static function setUpBeforeClass() they must be static, that is, must be accessible in static context.

Best thing what I can do is to define them as constants, if you prefer that more.


protected static function exec($command, $args = '')
{
$process = new Process(sprintf('php %s %s %s %s',
Copy link
Member

Choose a reason for hiding this comment

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

Better to follow PSR here and make it one-liner or extract sprintf part of expression into separate variable.

@lisachenko lisachenko merged commit e9dace6 into goaop:2.x Aug 11, 2017
@lisachenko
Copy link
Member

Oops, merged without squashing changes. Going to clean manually...

lisachenko added a commit that referenced this pull request Aug 11, 2017
…g-class-non-declared-methods-and-properties

AdviceMatcher should reject abstract method as fix for #335
Cleaned up tests, base for functional tests setup, basic test for #335 written.
@lisachenko
Copy link
Member

Merged into master as well.

Added declare(strict_types = 1); to all new files for master branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants