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

[WIP] Native reflection pass-through #82

Closed

Conversation

loren-osborn
Copy link
Contributor

@loren-osborn loren-osborn commented Sep 24, 2017

(This is a reissue of #81 to hide initial cleanup noise.)

This patch allows ParsedReflections to fall back to native implementation when dealing with built-in functions that have no parsable source code. This was proposed by @lisachenko in #17.

I think this implementation is complete, but it's still a bit rough. I especially want to move the utility functions I use in my tests into their own class(es) and give them their own tests. Go\ParserReflection\TestCaseBase::getStringificationOf() is one such bit of nastiness that should have it's own tests.

Thanks for any feedback.

squashed from:
    Work in progress: Passthrough                                cd98cc6
    Work In Progress: Computer B0RKed! :-(                       9d53b25
    Finished majority of passthrough code.                       3a8d144
    Minor test generalization (to include builtin classes)       d5966b9
    Finished breaking out getClassesToAnalyze() into a separate  d89bcd0
        method.
    BROKEN: work in progress... added builtin classes to test.   a778110
    BROKEN: good progress...                                     fbe9a31
        Some rough edges to find and fix.
    Changes before laptop died                                   ae9cb03
    Merge branch 'master' into losborn_passThrough               6c98c4d
    minor fix before bed.                                        d91682c
    Passes all tests again.                                      58d897a
        Still have some loose ends to tie up.
    Work in Progress: adding builtin functions to test set       5cc5e66
    BROKEN: Work in Progress: adding builtin functions to tests  46078b2
    All tests pass again: added passthrough for Functions.       cee9d42
        Need to:
          * Add ReflectionExtension code to emit proper classes.
          * Refactor
    BROKEN: Started adding ReflectionExtension tests.            8320983
    Broken: Minimal progress                                     3a18b93
    Broken: minor fixes... (from ancient spare computer)         ab79fda
    Merge branch 'master' into losborn_passThrough               673dd9a
    Tests pass.                                                  d7e98cd
        Still need to implement ReflectionExtension
        specialization.
    Complete proxy implementation.                               586c528
        Still need to do some refactoring.
    Try to placate Scrutinizer.                                  6f974ef
    more scrutinizer fixes                                       6b75c57
    more scrutinizer                                             ca58eb1
(caught one *REAL* bug... yeah!)
@loren-osborn
Copy link
Contributor Author

@Bob4ever, if we go in this direction, this will probably obsolete #77.

@loren-osborn
Copy link
Contributor Author

My schedule is still pretty unpredictable for the time being, but @lisachenko, can you give me a sense of how close this is to what you had envisioned for an API passthrough? Do you see anything particular you might want me to change? I know this is a pretty big patch, and I certainly don't expect a code review on something still in progress. I'm simply wanting your overall impressions.

Thanks

@lisachenko
Copy link
Member

Wow! It's a huge job! Thanks!

From my first quick view, it's what I planned to do previously, need to look deeper into PR.

*/
namespace Go\ParserReflection;

interface IReflection
Copy link
Member

Choose a reason for hiding this comment

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

Please, use PSR-2 naming: interfaces should have Interface suffix in their names.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The purpose of this interface is intended to be more than it currently appears to be. Because we don't have multiple inheritance the idea was that each Reflection class would have an IReflection... interface, and that each of these would extend the parent of the original Reflection class. The purpose being that you could easily type-hint any parser-reflection class, with all compatible subtypes by using, for instance IReflectionFunctionAbstract, which would be implemented by all subtypes that are parser-reflection classes. I thought a full "Interface" prefix for this purpose would become too cumbersome. Also, native Reflections already have a Reflector interface, so, if I didn't want to go in this direction, I probably would have named this Reflector instead. I also didn't want the compatibility headaches of renaming the interface later.

Interested to hear your feedback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

...or perhaps you want me to rename this Reflector, and add an empty derived interface (IReflection or ReflectionInterface or Interfaces\Reflection or whatever you think is best) when I add all its brethren? (Kicking the van down the road so to speak.)

Copy link
Member

Choose a reason for hiding this comment

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

My POV is to name it as ParserReflection\ReflectionInterface and leave all stuff in this interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

Still tempted to add Go\ParserReflection\Reflector in the future for consistency, but I see no need for that in this PR.

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, I'm finally digging into making these changes (as time is available), and a concern came to mind: It looks like some of the ReflectionType values in PHP 7.0 are being broken out into subclasses in PHP 7.2. If this trend continues, we may see ReflectionTrait and ReflectionInterface as derived classes of ReflectionClass in the not too distant future. If we wish to mirror this class heirarchy, this would cause a name collision for us. Perhaps we should change IReflection to Reflector which is already in PHP as an interface?

(Just a thought. In the meantime, I've already made the change you suggested. [Not pushed yet.] )


interface IReflection
{
public function wasIncluded();
Copy link
Member

Choose a reason for hiding this comment

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

why do we need such a method/interface?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Interface I explained above. The method was something I felt was intrinsic to the feature that parser-reflection classes offer: The ability to Reflect a construct before it's included (any built-in construct is already "included"). I believe I do use this in part of my of my passthrough implementation, but I thought knowing if a construct was already defined was intrinsic to what a parser-reflection class is.

This is also why I think adding a factory mechanism, which this patch is the first step towards, (and you indicated that you are sitting on the fence about) will be such a powerful feature. It allows seemlessly layering these application specific derived classes into the existing ecosystem of reflection classes.

I look forward to your thoughts.

Copy link
Member

Choose a reason for hiding this comment

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

I like your explanation, so let's go with wasIncluded method in the interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

}

/**
* Has class been loaded by PHP.
Copy link
Member

Choose a reason for hiding this comment

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

Incorrect doc-block

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you give me an example of the proper format? What did I miss? -Thanks.

Copy link
Member

Choose a reason for hiding this comment

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

This method relates to the function, so it should be related to the function and not to the class :) So, naming should be "Has function/method been loaded by PHP"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That looks like a typo/copy-paste error... I've been using the name "construct" to refer to functions/classes/constants/variables, but I'm open to a better name.

$isUserDefined = true;
if ($this->wasIncluded()) {
$nativeRef = new BaseReflectionFunction($functionName);
$isUserDefined = $nativeRef->isUserDefined();
Copy link
Member

Choose a reason for hiding this comment

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

Assignment alignment missed, same applied to many places.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wasn't sure if you had a preference there. I'm happy to fix. Not always sure when applicable, but I'll see what I can do.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe it's my personal taste but when I'm looking at aligned code it reads more natural and simpler.

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 prefer it too, but we might want a rule of thumb to ad consistency. (i.e. Min number of consecutive assignments to align. How to mix with broken lines. Should they cascade into nested blocks. etc.)

For now I will only align 2 or more consecutive assignments with no intervening broken lines unless you have a more specific preference.

@@ -341,4 +399,18 @@ private function getClassMethodNode()
{
return $this->functionLikeNode;
}

/**
* Has class been loaded by PHP.
Copy link
Member

Choose a reason for hiding this comment

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

Incorrect doc-block

} else {
$this->initializeInternalReflection();
// We need the native implementation of getInterfaceNames() here.
$ifaceNames = parent::getInterfaceNames();
Copy link
Member

Choose a reason for hiding this comment

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

Please, do not cut variable names, because readability will be lower in that case, so ifaceNames => parentInterfaceNames

$this->initializeInternalReflection();
$methodRefs = parent::getMethods();
$this->methods = [];
foreach ($methodRefs as $eachMethod) {
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 suggest eachMethod => reflectionMethod

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 Sounds good. I wanted to avoid using $methodRefs with $methodRef, as that would be rather hard to distinguish.

if ($orig->isDefaultValueAvailable() || $orig->isOptional()) {
if ($orig->isDefaultValueAvailable()) {
if (method_exists($orig, 'isDefaultValueConstant') && $orig->isDefaultValueConstant()) {
$constNameParts = explode('::', $orig->getDefaultValueConstantName(), 2);
Copy link
Member

Choose a reason for hiding this comment

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

This block looks very suspicious - why do you need to parse constant name by hands? It's a task for NodeExpressionResolver to resolve such values.

Copy link
Member

Choose a reason for hiding this comment

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

Instead of doing this by hand, just ask parser reflection for this file+class+method+param, it will look at existing AST and will search for definition.

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 fine with moving part of this to NodeExpressionResolver, but it seems built to process nodes rather than strings. How would you recommend I delegate this to the parser? My gut reaction would be to give it something like:

sprintf('<?php function foo($bar = %s) {}', $orig->getDefaultValueConstantName())

then pluck out the resulting AST nodes for the default value. Is there a cleaner way?

$argKeys = array_keys($filenameArgList);
$fileName = $filenameArgList[$argKeys[0]];
$resolvedFileName = stream_resolve_include_path($fileName);
$fileNode = ReflectionEngine::parseFile($resolvedFileName);
Copy link
Member

Choose a reason for hiding this comment

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

Please, enable assignment alignment to improve readability

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'll look for a Sublime plugin that does that... what IDE(s)/editor(s) do you use?

return array_pop($nameParts);
}

protected function getStringificationOf($value, $maxLen = 128)
Copy link
Member

Choose a reason for hiding this comment

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

Wow! Why do we need such a complex function? I'd vote to drop it and replace with something more general, without dark magic inside...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method is most of why this PR is still [WIP] right now... it is there exclusively for test failure readability, so I think it adds minimal risk, but needs its own unit tests, and should be moved into a class in tests/Support, or somewhere similar.

Opinions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me amend that.... I intend to use this function for future tests, not just test failure reporting, but it is currently only used for test error reporting.

Copy link
Contributor Author

@loren-osborn loren-osborn Dec 31, 2017

Choose a reason for hiding this comment

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

@lisachenko, I did some exploring and found PHPUnit uses SebastianBergmann\Exporter\Exporter to avoid this very mess. I see no reason I can't do similarly. I'm (slowly) working on extracting a PR from this one that refactors the test suite and will address this there.

Updated: The code I had originally based this on (at least conceptually) was from Codeception here: https://github.com/Codeception/Codeception/blob/baef1675a1f06e07d1dd63b46147616235020fdb/src/Codeception/Step.php#L94

@lisachenko
Copy link
Member

Hi, @loren-osborn! Sorry for the delay, was on vacation.

Could we proceed with this PR?

I also want to ask you and @aik099 if you want to become library developer with rights to push changes into master branch.

$nativeRef = new BaseReflectionClass($fullClassName);
$isUserDefined = $nativeRef->isUserDefined();
}
if ($isUserDefined) {
Copy link

Choose a reason for hiding this comment

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

Previously there wasn't any special handling for internal classes here and likely attempt to resolve them would fail. Do we have a test to cover this behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this code is much less clear than I realized. I will wrap this logic into a new private method isParsedNodeMissing(). I believe the tests I added cover this new case, but I will verify this. 📝 To understand why this new logic is necessary, let me contrast the old and new behavior:

The Old Behavior

  • If the parsed Node wasn't supplied:
    • Have the ReflectionEngine find it using the locator
      • If the locator can't find the class source code, throw a (hopefully fatal) exception.

Now we need to support classes that may not have PHP source code. Keep in mind "user defined" simply means PHP source code exists. So I changed this to:

The New Behavior

  • If no parsed Node is supplied:
    • The class is considered user defined unless the class is already defined in PHP.
    • If the class is already defined in PHP:
      • The class is user defined if native ReflectionClass says it is.
    • If the class is user defined:
      • Ask ReflectionEngine to load the PHP using the locator as before.

This way we aren't searching for PHP source files for built in classes. Does this clarify the new logic at all?

Copy link

Choose a reason for hiding this comment

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

It does. I agree, that logic for detecting missing $classLikeNode should be extracted to separate method.

Copy link
Contributor Author

@loren-osborn loren-osborn Nov 18, 2017

Choose a reason for hiding this comment

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

@aik099, I wanted to share the results of verifying all the logical cases were being covered: There was, unfortunately one case that I'm not currently testing. I need to add tests for files that haven't been included by PHP when the test is performed. (I had already been thinking how to do this properly, but I will come up with a stop-gap measure for now.) Currently we include all the Stub files, so we can compare the results to the behavior of the builtin reflection classes.

  • Add tests for code not included or required by PHP at test time.

Copy link
Contributor Author

@loren-osborn loren-osborn Dec 4, 2017

Choose a reason for hiding this comment

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

Added cases to the test suite for namespace Go\ParserReflection\Stub\NeverIncluded which mirrors Go\ParserReflection\Stub, but is never included or required by PHP.

: new static($implementName);
$interfaces[$implementName] = $interface;
$implementName = $implementNode->toString();
$interfaces[$implementName] = new static($implementName);
Copy link

Choose a reason for hiding this comment

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

I see the parent/static thing was removed.

  1. What was it for and why it's not needed anymore?
  2. Any tests covering changed piece?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. It used to be needed, because Go\ParserReflection\ReflectionClass didn't previously support built in classes (or interfaces in this case), but now it does.
  2. This new situation should already be under test, but I will verify: 📝

Copy link

Choose a reason for hiding this comment

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

OK.

? new parent($traitName)
: new static($traitName);
$traits[$traitName] = $trait;
$traits[$traitName] = new static($traitName);
Copy link

Choose a reason for hiding this comment

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

I see the parent/static thing was removed.

  1. What was it for and why it's not needed anymore?
  2. Any tests covering changed piece?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(See last answer)

Copy link

Choose a reason for hiding this comment

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

OK.

* @return ReflectionClass
* The apropriate reflection object.
*/
protected function createReflectionForClass($className)
Copy link

Choose a reason for hiding this comment

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

Why this method was removed? If I'm not mistaken, then it's used in \Go\ParserReflection\Traits\ReflectionClassLikeTrait::getParentClass method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that we can reflect builtin classes, this is no longer needed.

Copy link

Choose a reason for hiding this comment

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

OK.

// to behave exactly like \ReflectionExtension, but return
// Go\ParserReflection\ReflectionFunction and
// Go\ParserReflection\ReflectionClass where apropriate.
return new ReflectionExtension($extName);
Copy link

Choose a reason for hiding this comment

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

Is it tested?

To actually test it you need to know what extension are loaded into PHP and try to reflect class provided by them.

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 included tests for two seemingly ubiquitous extensions. I believe I setup the tests to skip if not loaded... not sure I have coverage on this particular method, but I will verify. 👍 📝

@@ -98,7 +124,7 @@ public function getTypeNode()
public function ___debugInfo()
{
return array(
'name' => isset($this->propertyNode) ? $this->propertyNode->name : 'unknown',
'name' => isset($this->propertyName) ? $this->propertyName : 'unknown',
Copy link

Choose a reason for hiding this comment

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

The isset won't get you desired result, because $this->propertyName is now always set (thanks to your changes) from mandatory constructor argument.

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 have to investigate further, but you're probably right. If I can simplify this, I will. 👍

@@ -53,7 +60,7 @@ public function getClosureScopeClass()
{
$this->initializeInternalReflection();

return forward_static_call('parent::getClosureScopeClass');
return parent::getClosureScopeClass();
Copy link

Choose a reason for hiding this comment

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

Why change 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.

This is just blatantly wrong. This is an instance method, not a static method.

Copy link

Choose a reason for hiding this comment

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

Why this was working before and needs to be changed now? Maybe this was workaround for older PHP versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now these four calls to forward_static_call() are a bug fix, and distinct from the rest of this PR, so I'd be happy to move this to a separate PR.

  • Basically any call to forward_static_call() within an instance method is a bug.
  • Two of these dealt with static properties of a ReflectionClass's class, so that confusion is very understandable.
  • After some searching I found the commits that added these:
    • Origin of ReflectionFunctionLikeTrait forward_static_call()s in methods getClosureThis() and getClosureScopeClass() commit 240d5b5
    • Origin of ReflectionClassLikeTrait forward_static_call()s in methods setStaticPropertyValue()and getStaticPropertyValue() commit 9503a19
  • @lisachenko, any recollection from these commits you can share?
  • Not sure why these weren't caught before. I suspect they are not being properly tested?

Copy link

Choose a reason for hiding this comment

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

👍 for moving this into separate PR.

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 other thing, for this PR: the native implementation returns ?ReflectionClass, so the result needs to be reinstantiated as a Go\ParserReflection\ReflectionClass before it's returned, unless it's null.

Copy link

Choose a reason for hiding this comment

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

No big deal. Good catch.

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... doesn't appear that the old code has any bad behavior, but I moved this into PR #86.

@@ -63,33 +70,59 @@ public function getClosureThis()
{
$this->initializeInternalReflection();

return forward_static_call('parent::getClosureThis');
return parent::getClosureThis();
Copy link

Choose a reason for hiding this comment

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

Why change 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.

As above, it's an instance method.

Copy link

Choose a reason for hiding this comment

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

As above, why this was needed before?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(See above... Move to new PR?)

Copy link

Choose a reason for hiding this comment

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

👍 for moving this into separate PR.

}

public function getDocComment()
{
if (!$this->functionLikeNode) {
Copy link

Choose a reason for hiding this comment

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

I see lots of similar checks for internal reflection initialization. Maybe we can create a proxy classes that would use __call method where optional initialization of internal reflection would be made.

That would greatly reduce copy/paste of such code, but I'm not sure if excessive usage of __call method would result in performance decrease.

Copy link
Contributor Author

@loren-osborn loren-osborn Oct 30, 2017

Choose a reason for hiding this comment

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

I'm thinking similarly. I believe __call() is pretty expensive, but I believe this can be simplified. I'll look into it further. 👍

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 have some thoughts on this I want to explore. No promises, but we'll see.

Copy link

Choose a reason for hiding this comment

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

OK.

} else {
return null;
if (PHP_VERSION_ID < 70000) {
Copy link

Choose a reason for hiding this comment

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

This check won't help you, because we can use PHP5 to parse PHP7 files and vice-versa.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we're in this else block there is no PHP source code to analyze, and we're only querying the PHP binary and any loaded extensions.

@loren-osborn
Copy link
Contributor Author

loren-osborn commented Oct 17, 2017

Ok... just dashing off a quick note... I'd fallen off the radar a bit myself for a bit, and hobbling by with a Windows XP laptop currently, so somewhat underequipped to test any PHP 7 code at the the moment, except on my iMac at home. I do have a bunch more PRs I want to contribute, but had to focus on other areas of my life recently.

@lisachenko, I greatly appreciate the offer to be a library contributor, and am happy to help any way I can. Prudence dictates that I should still rely on your very helpful peer reviews, if that isn't inconvienient.

@aik099, thanks for the second round of review comments. I will respond as soon as I am able.

Give me a day or two to get back up to speed on this PR.

@aik099
Copy link

aik099 commented Oct 17, 2017

@loren-osborn , sure, take your time. Whenever you're ready just add a comment so that I can know that push/commit round was made a ready to be reviewed.

@loren-osborn
Copy link
Contributor Author

@aik099, based on your review comments, I wanted to give some overall context for this PR that may be unclear:

The goal of this PR is to bring feature pairity to Go\ParserReflection classes with their native equivalents for functions/classes/methods/etc. that are compiled into PHP (and therefore have no parsable PHP source code.) Because these functions/classes/methods/etc. previously weren't supported by Go\ParserReflection classes, any interaction (such as inheritance) between built in and user defined functions/classes/methods/etc. needed to be special cased. As this PR makes this no longer the case, many of these special cases disappear.

Of course, we don't get this new flexibility for free, so we have lots of new special cases for most Go\ParserReflection classes, for cases when there is no PHP source code. In these cases, parsed node properties, like $this->functionLikeNode and $this->classLikeNode, are null.

As far as testing all of this new functionality, I did add tests for these cases, in general, but I need to go back and check each of the conditions you pointed out individually, which I fully intend to do. I will use a 📝 to remind myself of tests I need to go back and verify, as well as pointing out any tests that I am already aware are missing/insufficient.

These are simply overall responses to the issues I saw you raise. I'll go back now and respond to your individual points. These are the outstanding issues I'm already aware of, but I appreciate any feedback you have on these as well:

Known Outstanding Issues:

  • Already pointed out to @lisachenko:
    • Pull non-testing code out of test classes.
    • Put this test-support code under test.
    • Refactor this test-support code.
  • Previously undiscussed issues:
    • Add ability to construct Go\ParserReflection\ReflectionParameter with string and string|int arguments respectively$function and $parameter like native ReflectionParameter.
  • Issues for separate PR:
    • Major refactor, especially of test code, (hopefully) bringing 100% code coverage.

@aik099
Copy link

aik099 commented Oct 30, 2017

The goal of this PR is to bring feature pairity to Go\ParserReflection classes with their native equivalents for functions/classes/methods/etc. that are compiled into PHP (and therefore have no parsable PHP source code.)

I thought it was solving #16. Does it as well?

Of course, we don't get this new flexibility for free, so we have lots of new special cases for most Go\ParserReflection classes, for cases when there is no PHP source code. In these cases, parsed node properties, like $this->functionLikeNode and $this->classLikeNode, are null.

I understand now. Thanks.

Copy link

@aik099 aik099 left a comment

Choose a reason for hiding this comment

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

Next time, when you plan to answer several comments please click on Start a review button instead of Add single comment and at the end of the process submit your review as Comment type. This nice feature of GitHub allows to send single notification email for all comments instead of separate email for each.

$nativeRef = new BaseReflectionClass($fullClassName);
$isUserDefined = $nativeRef->isUserDefined();
}
if ($isUserDefined) {
Copy link

Choose a reason for hiding this comment

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

It does. I agree, that logic for detecting missing $classLikeNode should be extracted to separate method.

: new static($implementName);
$interfaces[$implementName] = $interface;
$implementName = $implementNode->toString();
$interfaces[$implementName] = new static($implementName);
Copy link

Choose a reason for hiding this comment

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

OK.

? new parent($traitName)
: new static($traitName);
$traits[$traitName] = $trait;
$traits[$traitName] = new static($traitName);
Copy link

Choose a reason for hiding this comment

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

OK.

*
* @var string|\Closure
*/
private $functionName;
Copy link

Choose a reason for hiding this comment

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

OK. Make sure you've add tests for cases, where it's not initialized then. I guess tests, when it's initialized are already there.

*
* @var string
*/
private $methodName;
Copy link

Choose a reason for hiding this comment

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

(As above:) OK. Make sure you've add tests for cases, where it's not initialized then. I guess tests, when it's initialized are already there.

@@ -345,7 +354,7 @@ public function isDefaultValueConstant()
*/
public function isOptional()
{
return $this->isVariadic() || ($this->isDefaultValueAvailable() && $this->haveSiblingsDefalutValues());
return $this->isVariadic() || ($this->isDefaultValueSet() && $this->haveSiblingsDefalutValues());
Copy link

Choose a reason for hiding this comment

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

Better to move this into separate PR for optional parameter without default value handling (part 1).

*
* @return bool
*/
protected function isDefaultValueSet()
Copy link

Choose a reason for hiding this comment

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

Better to move this into separate PR for optional parameter without default value handling (part 2).

@@ -53,7 +60,7 @@ public function getClosureScopeClass()
{
$this->initializeInternalReflection();

return forward_static_call('parent::getClosureScopeClass');
return parent::getClosureScopeClass();
Copy link

Choose a reason for hiding this comment

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

Why this was working before and needs to be changed now? Maybe this was workaround for older PHP versions?

@@ -63,33 +70,59 @@ public function getClosureThis()
{
$this->initializeInternalReflection();

return forward_static_call('parent::getClosureThis');
return parent::getClosureThis();
Copy link

Choose a reason for hiding this comment

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

As above, why this was needed before?

* @return ReflectionClass
* The apropriate reflection object.
*/
protected function createReflectionForClass($className)
Copy link

Choose a reason for hiding this comment

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

OK.

Copy link
Contributor Author

@loren-osborn loren-osborn left a comment

Choose a reason for hiding this comment

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

I apologize for the abundance of notifications last time around. "Start a new review" sounded a bit misleading to me.

@@ -224,6 +250,10 @@ public function invokeArgs($object, array $args)
*/
public function isAbstract()
{
if (!$this->functionLikeNode) {
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 somewhat concerned about this. There is already a method that does this: !$this->isUserDefined(). We don't actually want to call this, since as I stated elsewhere, it calls into the parent class to be sure, but that is probably the method we should use. I could remove the extra (safeguard) logic from isUserDefined(), but I'm not sure how I feel about this. Unfortunately I'm not sure if this is clear given your previous confusion. I could really use any suggestion you have to keep this repeated logic simple, clear and specific.

@@ -89,7 +89,7 @@ public function __construct(
$this->parameterIndex = $parameterIndex;
$this->declaringFunction = $declaringFunction;

if ($this->isDefaultValueAvailable()) {
if ($this->isDefaultValueSet()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here I do specifically disagree with you. First off, the ?string is a "required nullable type". This is almost the opposite of the "optional parameter with no default value", which only appears to exist in builtin functions and methods. Secondly, we're doing the same proxying we do throughout this PR; We just accomplish it differently. Everywhere else in this PR, if we don't have source code for a function/method/class/etc. we call the method on the parent class. It would be rather dIfficult to initialize the ReflectionParameter objects properly for methods with the API as it is, so instead, we query all the user visible properties, and build a parse node that will give have the correctly reflected properties, and build a Go\ParserReflection\ReflectionParameter from the synthetic ParamNode we synthesized. This works great except for optional parameters with no default value, which there is no syntax for. What we do is add an attribute to indicate this behavior, and check for it in ReflectionParameter; which are the changes you're commenting on. Look at the synthetic ParamNode generation code at ReflectionFunctionLikeTrait::getRefParameter().

@@ -165,6 +165,11 @@ public function __toString()
*/
public function allowsNull()
{
// Allow builtin types to override
if ($this->parameterNode->getAttribute('prohibit_null', false)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Disagree... see above.)

@@ -329,7 +336,9 @@ public function isCallable()
*/
public function isDefaultValueAvailable()
{
return isset($this->parameterNode->default);
return
isset($this->parameterNode->default) &&
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Disagree... see above.)

@@ -345,7 +354,7 @@ public function isDefaultValueConstant()
*/
public function isOptional()
{
return $this->isVariadic() || ($this->isDefaultValueAvailable() && $this->haveSiblingsDefalutValues());
return $this->isVariadic() || ($this->isDefaultValueSet() && $this->haveSiblingsDefalutValues());
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(Disagree... see above.)

@@ -53,7 +60,7 @@ public function getClosureScopeClass()
{
$this->initializeInternalReflection();

return forward_static_call('parent::getClosureScopeClass');
return parent::getClosureScopeClass();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now these four calls to forward_static_call() are a bug fix, and distinct from the rest of this PR, so I'd be happy to move this to a separate PR.

  • Basically any call to forward_static_call() within an instance method is a bug.
  • Two of these dealt with static properties of a ReflectionClass's class, so that confusion is very understandable.
  • After some searching I found the commits that added these:
    • Origin of ReflectionFunctionLikeTrait forward_static_call()s in methods getClosureThis() and getClosureScopeClass() commit 240d5b5
    • Origin of ReflectionClassLikeTrait forward_static_call()s in methods setStaticPropertyValue()and getStaticPropertyValue() commit 9503a19
  • @lisachenko, any recollection from these commits you can share?
  • Not sure why these weren't caught before. I suspect they are not being properly tested?

@@ -63,33 +70,59 @@ public function getClosureThis()
{
$this->initializeInternalReflection();

return forward_static_call('parent::getClosureThis');
return parent::getClosureThis();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(See above... Move to new PR?)

*
* @var string|\Closure
*/
private $functionName;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 📝 (Note to self:) Verify test cases!!!

@@ -89,7 +89,7 @@ public function __construct(
$this->parameterIndex = $parameterIndex;
$this->declaringFunction = $declaringFunction;

if ($this->isDefaultValueAvailable()) {
if ($this->isDefaultValueSet()) {
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 did some thinking on this. I think I'm able to do the same thing with a more minor change to ReflectionParameter. Let me take another crack at this.

Everything I said above is still valid, but I think I have a more subtle solution with the same behavior.

}

public function getDocComment()
{
if (!$this->functionLikeNode) {
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 have some thoughts on this I want to explore. No promises, but we'll see.

@loren-osborn
Copy link
Contributor Author

@aik099, I forgot about your asking about #16. The way I read #16, yes it will, even though I generally thought of this PR as solving #17, although indirectly.

@loren-osborn
Copy link
Contributor Author

@aik099, continuing the discussion from #86:

Part of the reason I'm not quite so scared of it, is this is my second pass at this change, and it gave me no problems previously. I agree it's a bigger delta than I remember. One thing that I probably would have included in this PR, that I think will be it's own PR now, is explicit closure support. (I may add protection against using ReflectionFunction with a closure to this PR, but merely for the purpose of defensive programming.)

On the subject of a closure support PR, I think it would be useful to add two methods to ReflectionFunctionLikeTrait:

  • The first for purposes of traversal, (that would have a work-alike method inReflectionFileNamespace) would return an array of all closures defined within the function, method or file's namespace. (It may be a good idea to have aggregator methods in ReflectionClass and ReflectionFile For this new method.)
  • The second I'm a bit less clear on: I'm thinking a static analysis equivalent of getClosureThis(). I'm not quite so sure what that would be though. I'm pretty sure it would return null whenever isClosure() is false, and whenever a closure isn't defined within a non-static method, but I'm not sure if it should just return getClosureScopeClass() the rest of the time, or something a bit more stateful, without having to instantiate (and load) the class definition.

What do you think? This is only a half-formed idea at this point.

@aik099
Copy link

aik099 commented Nov 16, 2017

The first for purposes of traversal, (that would have a work-alike method inReflectionFileNamespace) would return an array of all closures defined within the function, method or file's namespace. (It may be a good idea to have aggregator methods in ReflectionClass and ReflectionFile For this new method.)

  1. Is PHP built-in reflection capable if this?
  2. For what purposes this functionality can be used?

The second I'm a bit less clear on: I'm thinking a static analysis equivalent of getClosureThis(). I'm not quite so sure what that would be though. I'm pretty sure it would return null whenever isClosure() is false, and whenever a closure isn't defined within a non-static method, but I'm not sure if it should just return getClosureScopeClass() the rest of the time, or something a bit more stateful, without having to instantiate (and load) the class definition.

In real reflection (e.g. created from object) that would probably point back to object to which reflected method belongs. But when you're doing it statistically you have no object to return from that method. The only thing you can return is another reflection of what you expect $this to be, but then return type won't be compatible with what built-in reflection method returns.

@loren-osborn
Copy link
Contributor Author

The first for purposes of traversal, (that would have a work-alike method in ReflectionFileNamespace) would return an array of all closures defined within the function, method or file's namespace. (It may be a good idea to have aggregator methods in ReflectionClass and ReflectionFile For this new method.)

  1. Is PHP built-in reflection capable if this?

No, these would be discovery methods like getFunctions(), getClasses() and getConstants() in ReflectionFileNamespace.

  1. For what purposes this functionality can be used?

The intent, is being able to inspect closures that might be returned to (or otherwise interact with) calling code. The static analysis to determine which closures may be returned would be well outside the scope of this module, so the thinking was to simply return an array of all closures defined within the scope being queried. My biggest concern about doing this, is that closures are intrinsically anonymous, so there is no logical way to organize them in a returned array (except order of occurrence, which is likely to change.)

The second I'm a bit less clear on: I'm thinking a static analysis equivalent of getClosureThis(). I'm not quite so sure what that would be though. I'm pretty sure it would return null whenever isClosure() is false, and whenever a closure isn't defined within a non-static method, but I'm not sure if it should just return getClosureScopeClass() the rest of the time, or something a bit more stateful, without having to instantiate (and load) the class definition.

In real reflection (e.g. created from object) that would probably point back to object to which reflected method belongs. But when you're doing it statistically you have no object to return from that method. The only thing you can return is another reflection of what you expect $this to be, but then return type won't be compatible with what built-in reflection method returns.

I think I was slightly misunderstood, but mostly because I believe I was being too ambitious:

  • First, I think the correct solution here would be adding a method getClosureThisClass(), which would return the same value as getClosureScopeClass(), except that it would return null when the closure is defined within a static method of a class.
    • It would essentially be equivilant to:
      (
          ($closureReflection->isClosure() && $closureReflection->getClosureThis()) ?
          new ReflectionClass(get_class($closureReflection->getClosureThis())) :
          null
      )
  • Second, I think you misunderstood what I meant by a "more stateful" value than a ReflectionClass because (in hindsight) what I had in mind was so far beyond the scope of what we would want in this module. I think I could explain best by example:
    • Imagine the following code fragment within a instance method we're introspecting:
      if ($this->getFred() < 7) {
          return (function () { return $this->getGeorge() + 3; } );
      }
    • In this case, a sufficiently advanced (bloated) static analyzer could deduce, if we had a ReflectionFunction of the closure the fragment above returns, some yet-to-be-defined static analog to getClosureThis() could return something that the object it represents would have a method getFred(), that returned a value less than 7.
    • Even ignoring the question of how this state could be presented to the caller, the depth of static analysis required for this would be daunting, and is well beyond what we might want in this composer module.
    • (Summary) This is well outside anything we would want to attempt here.

@aik099
Copy link

aik099 commented Nov 17, 2017

To summarize better not to implement any of #82 (comment), because it will be out of scope of this library and any kind of reflection library shouldn't be doing static analysis of that kind.

@loren-osborn
Copy link
Contributor Author

loren-osborn commented Nov 17, 2017

@aik099, basically, I mostly agree with you, and was really sitting on the fence on most of that except I think adding getClosureThisClass() is quite narrow in scope, doesn't require any static analysis, and provides a staticly relevant alternative to getClosureThis(). (The only reason this method isn't a trivial wrapper over existing functionality, is that it needs to determine if it was defined within a non-static method of a class and which class.) Even seeing the utility of this method, I see it as a low-priority nice-to-have feature, and of limited importance.

@loren-osborn
Copy link
Contributor Author

My latest changes by no means address all raised concerns yet. I just wanted to share my progress so far. I regret that this PR is taking so long. New in these changes is testing against classes that have actually never been loaded by the PHP interpreter. Much need of refactoring all around.

if ($phpExt) {
$origin .= ':' . $phpExt->getName();
}
}

Choose a reason for hiding this comment

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

Expected 1 space after closing brace; newline found

throw new \InvalidArgumentException(
sprintf(
"Option 'supportedBuiltinTypeHints's element %s " .
"isn't a valid typehint string.",

Choose a reason for hiding this comment

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

Multi-line function call not indented correctly; expected 28 spaces but found 32


public function enterNode(Node $node)
{
if (

Choose a reason for hiding this comment

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

Expected 0 spaces after opening bracket; newline found

throw new \InvalidArgumentException(
sprintf(
"Option 'supportedBuiltinTypeHints's element %s " .
"isn't a valid typehint string.",

Choose a reason for hiding this comment

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

Multi-line function call not indented correctly; expected 28 spaces but found 32


public function enterNode(Node $node)
{
if (

Choose a reason for hiding this comment

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

Expected 0 spaces after opening bracket; newline found

throw new \InvalidArgumentException(
sprintf(
"Option 'supportedBuiltinTypeHints's element %s " .
"isn't a valid typehint string.",

Choose a reason for hiding this comment

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

Multi-line function call not indented correctly; expected 28 spaces but found 32


public function enterNode(Node $node)
{
if (

Choose a reason for hiding this comment

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

Expected 0 spaces after opening bracket; newline found

throw new \InvalidArgumentException(
sprintf(
"Option 'supportedBuiltinTypeHints's element %s " .
"isn't a valid typehint string.",

Choose a reason for hiding this comment

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

Multi-line function call not indented correctly; expected 28 spaces but found 32


public function enterNode(Node $node)
{
if (

Choose a reason for hiding this comment

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

Expected 0 spaces after opening bracket; newline found

throw new \InvalidArgumentException(
sprintf(
"Option 'supportedBuiltinTypeHints's element %s " .
"isn't a valid typehint string.",

Choose a reason for hiding this comment

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

Multi-line function call not indented correctly; expected 28 spaces but found 32


public function enterNode(Node $node)
{
if (

Choose a reason for hiding this comment

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

Expected 0 spaces after opening bracket; newline found

throw new \InvalidArgumentException(
sprintf(
"Option 'supportedBuiltinTypeHints's element %s " .
"isn't a valid typehint string.",

Choose a reason for hiding this comment

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

Multi-line function call not indented correctly; expected 28 spaces but found 32


public function enterNode(Node $node)
{
if (

Choose a reason for hiding this comment

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

Expected 0 spaces after opening bracket; newline found

@loren-osborn
Copy link
Contributor Author

loren-osborn commented Dec 14, 2017

It looks like nikic/PHP-Parser#449 is more an issue of simulating different versions of the PHP parser. Different typehints were added in different PHP versions and PhpParser runs either in latest PHP versions of 5 and 7, but no obvious way to differentiate PHP 7.1 from PHP 7.2 for instance.

@scrutinizer-notifier
Copy link

The inspection completed: 13 new issues, 47 updated code elements

*/
namespace Go\ParserReflection;

interface ReflectionInterface
Copy link
Contributor Author

@loren-osborn loren-osborn Jan 22, 2018

Choose a reason for hiding this comment

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

@lisachenko, I'm kind of knee-deep in alligators here, trying to finish and untangle this into two workable PRs, but I wanted to give you an update relevant to the Go\ParserReflection\ReflectionInterface interface I added. I added it believing that \Reflection was the parent class of all Reflection classes. I have since discovered (at least in PHP 7) this isn't the case. The \Reflector interface, however is implemented by all Reflection classes (except, ironically, the purely-static \Reflection class). So the new interface Go\ParserReflection\ReflectionInterface should clearly be renamed either Go\ParserReflection\ReflectorInterface or simply Go\ParserReflection\Reflector (as \Reflector is already an interface). I welcome your input.

I am continuing work on this PR here: loren-osborn:losborn_testDirReorgAndPassThrough which will be split into two distinct PRs.

@lisachenko
Copy link
Member

One year later :)

Guys, could we have clean PR to merge for 2.0?

@loren-osborn
Copy link
Contributor Author

@lisachenko and team, I’m sorry for dragging my feet on this... (It has been a crazy year.) I’ll try to get something cleaned up in the next 2 weeks. (Then again, I’ve been away from this so long i’m almost starting over mentally on this, so any ideas anyone has will likely help me get moving in the right direction.)

@icanhazstring
Copy link
Member

@loren-osborn don't stress ;)
And even then, mentally starting over here is maybe not even a bad thing. Since things evolve over time. Maybe you find something to improve over your old implementation ;)

@lisachenko
Copy link
Member

Closing this one as it's 2020 year )

@lisachenko lisachenko closed this Jan 16, 2020
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.

None yet

6 participants