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

Scrutinizer fixes #76

Merged
merged 9 commits into from
Aug 8, 2017
14 changes: 14 additions & 0 deletions src/ReflectionClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,18 @@ protected function __initialize()
{
parent::__construct($this->getName());
}

/**
* Create a ReflectionClass for a given class name.
*
* @param string $className
* The name of the class to create a reflection for.
*
* @return ReflectionClass
* The apropriate reflection object.
*/
protected function createReflectionForClass($className)
{
return class_exists($className, false) ? new parent($className) : new static($className);
}
}
8 changes: 8 additions & 0 deletions src/ReflectionFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class ReflectionFile
*/
public function __construct($fileName, $topLevelNodes = null)
{
if (!is_string($fileName)) {
Copy link

Choose a reason for hiding this comment

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

It seems, that you've started to change more code. I'd recommend doing this in another PRs.

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 was a change from my original commit from July 3rd before I ever issued this PR. I'm slightly confused.

Copy link

Choose a reason for hiding this comment

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

Interesting. I've reviewed only changes that Scrutinizer CI reported as fixes to existing issues. Probably this change fixed nothing of Scrutinizer CI reported bugs.

In either case I think it's too much to check filename to be a string. Likely nobody would be crazy enough to pass non-string in there.

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 only reason I made this addition was that it was flagged by Scrutinizer:

Documentation Bug introduced 30 days ago by @lisachenko
It seems like $fileName can also be of type array or Boolean. However, the property $fileName is declared as type string. Maybe add an additional type check?

I think this based on type variations in the calling functions, despite this function only ever being called with string. I expect the other change that might fix this is isolating the parameter into a distinct, string-only variable that is used when calling this method. I think this type check is the easier solution.

Copy link

Choose a reason for hiding this comment

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

Then better to:

  1. revert this change
  2. in another PR address cases, when method can be called with non-string 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.

The purpose of this PR was to address the Scrutinizer issues. This does that and it was in the PR from the beginning.

I'm confused why this is suddenly an issue?

Let me investigate the calling code to see if that is more easily addressed than this. I'll get back to you.

Copy link

Choose a reason for hiding this comment

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

  1. huge PRs are pain to review (this PR is becoming larger and larger)
  2. the solution you've chosen for Scrutinizer CI reported problem isn't correct one

The Scrutinizer CI told that multiple types could come in and you've restricted to type check instead of actually investigating places from where multiple types could come in and deciding on fixing these places instead. This could be a lots of changes and that's why I've suggested another PR.

P.S.
You don't need to put ALL fixing in single PR. Better to send smaller manageable PRs. Right now this PR has large discussion that already is hard to track what's going on.

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... I searched the code, and (outside of tests/) nothing within ParserReflection instantiates Go\ParserReflection\ReflectionFile, and ever use of it is annotated as string: The constructor is annotated @param string, the class property is annotated @var string, and the accessor is annotated @return string. I can only assume that this a plea for enforcing type safety rather than warning that badly typed data is being redirected here. I happen to be a fan of enforcing types where appropriate, so I see no harm in leaving this change, and the matching one for ReflectionFileNamespace intact.

👎 Keeping change in PR pending @lisachenko's approval.

Copy link

Choose a reason for hiding this comment

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

Enforcing something, that will never happen would only slow down whole code checking process. For that reason I'd rather not check unless somebody stumbles upon this and reports it.

Copy link

Choose a reason for hiding this comment

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

Let's keep it then. I hope this won't be a performance issue.

throw new \InvalidArgumentException(
sprintf(
'$fileName must be a string, but a %s was passed',
gettype($fileName)
)
);
}
$fileName = PathResolver::realpath($fileName);
$this->fileName = $fileName;
$this->topLevelNodes = $topLevelNodes ?: ReflectionEngine::parseFile($fileName);
Expand Down
8 changes: 8 additions & 0 deletions src/ReflectionFileNamespace.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ class ReflectionFileNamespace
*/
public function __construct($fileName, $namespaceName, Namespace_ $namespaceNode = null)
{
if (!is_string($fileName)) {
Copy link

Choose a reason for hiding this comment

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

It seems, that you've started to change more code. I'd recommend doing this in another PRs.

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 was a change from my original commit from July 3rd before I ever issued this PR. I'm slightly confused.

Copy link

Choose a reason for hiding this comment

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

Interesting. I've reviewed only changes that Scrutinizer CI reported as fixes to existing issues. Probably this change fixed nothing of Scrutinizer CI reported bugs.

In either case I think it's too much to check filename to be a string. Likely nobody would be crazy enough to pass non-string in there.

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 only reason I made this addition was that it was flagged by Scrutinizer:

Documentation Bug introduced 10 months ago by @lisachenko
It seems like $fileName can also be of type array or Boolean. However, the property $fileName is declared as type string. Maybe add an additional type check?

(Same comment as above)

Copy link

Choose a reason for hiding this comment

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

Then better to:

  1. revert this change
  2. in another PR address cases, when method can be called with non-string 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.

While there are 3 places in the code where Go\ParserReflection\ReflectionFileNamespace is instantiated, and two of them are called with filename coming from Object(Go\ParserReflection\ValueResolver\NodeExpressionResolver)->context->getFileName() where context isn't a strongly typed property, I believe the analysis from Go\ParserReflection\ReflectionFile's constructor still holds: It's a good idea to do type checking where appropriate, and I think this can stay, unless @lisachenko wants me to yank it.

Copy link

Choose a reason for hiding this comment

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

What is the 3rd place, can you post a link to that code line? If total call count in that 3rd place is much smaller than sum of other 2 places, then from performance point it would be better to type cast before calling this method instead of checking in constructor.

Same is valid for other similar place.

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... I'm happy to show you the three places that call this constructor, but the fact that you're asking indicates a failure to communicate on my part. I apologize for being unclear.

Here are the 3 places that call this constructor:

.../parser-reflection/src/ReflectionFile.php:

  153                  $namespaceName = $topLevelNode->name ? $topLevelNode->name->toString() : '';
  154  
  155:                 $namespaces[$namespaceName] = new ReflectionFileNamespace(
  156                      $this->fileName,
  157                      $namespaceName,

.../parser-reflection/src/ValueResolver/NodeExpressionResolver.php:

  231                  $fileName      = $this->context->getFileName();
  232                  $namespaceName = $this->resolveScalarMagicConstNamespace();
  233:                 $fileNamespace = new ReflectionFileNamespace($fileName, $namespaceName);
  234                  if ($fileNamespace->hasConstant($constantName)) {
  235                      $constantValue = $fileNamespace->getConstant($constantName);

...

  500              $namespaceName = $this->resolveScalarMagicConstNamespace();
  501  
  502:             $fileNamespace = new ReflectionFileNamespace($fileName, $namespaceName);
  503              return $fileNamespace->getClass($className);
  504          }

That said, I think this largely misses the point. Let me see if I can clear this up a little bit:

  • First, an apology: When you first brought these two changes up as issues, I think I took it a bit personally, and doubled down by adding unit tests for my type checking code. I was hoping that after trying to explain myself, why I made these changes would be clear, and I tried to defer to @lisachenko as an arbitrator. This basically blew up in my face, and I failed to address your concerns about these changes directly. I'm sorry. Mea culpa.

  • Relevance: The only reason I included these checks in this PR was because Scrutinizer flagged them as issues. While I try not to take anything that Scrutinizer says as gospel, in this case it has a point: These are public API entry points, and can be called with invalid arguments. Since we can't add PHP 7 type hints here without breaking PHP 5 compatibility, I felt it was appropriate to add these type checks to fix Scrutinizer's issue.

  • Performance: You have repeatedly brought up the concern of this check impacting performance, but in these two instances it just isn't applicable. Of course I agree that any operation takes some amount overhead. The problem is that these are checks that only occur on the construction of infrequently created objects that often result in loading and parsing a file. Even if the parsed file cache is 80% effective, and we're pulling data from a solid state drive, or even the OS cache, checking the data type of a single function parameter has to be at least 3 to 5 orders of magnitude smaller reading and parsing a file 20% of the time. I agree that it contributes to constructor complexity which impacts maintainability, and both of those need to be kept in check, but this has no appreciable impact on performance.

  • Addressing this from where the method is called: I think this is where our biggest misunderstanding lies.

    • My goal is to eliminate the issues being reported by Scrutinizer.
    • Scrutinizer is reporting two almost identical issues on two constructors with identically defined $fileName parameters.
    • One of those constructors is never called by this project's code, while the other is called three times.
    • If ensuring that all calling code was properly type hinted, then we should expect that the constructor that is never called (so all zero of its callers are properly type-hinted) would not trigger any Scrutinizer issue.
    • Since the uncalled constructor does trigger a Scrutinizer issue, I think it's safe to assume that ensuring the callers are properly type hinted (which I believe they already are) will have any impact on Scrutinizer reporting this issue.
    • Also, as this is a public API, I see no harm in a bit of sanity checking of parameters.
  • Summary: The decision is ultimately yours, so if you ask me to get rid of these two checks, and the unit tests to support them, I will, but I think that asking that really misses the point of this PR.

So, your choices are:

  1. I can leave these two if (is_string()) checks, along with the corresponding unit tests, in place.
  2. Or I can remove them.

Copy link

Choose a reason for hiding this comment

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

1 - keep.

If we try to imagine real life examples, then ReflectionFileNamespace class would only be created once per namespace. And I haven't seen any codebases, where unique namespace count is fare superior (or even equal) to class/interface/trait count in these namespaces.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for your quick response. I hope to have the final commit pushed this afternoon.

throw new \InvalidArgumentException(
sprintf(
'$fileName must be a string, but a %s was passed',
gettype($fileName)
)
);
}
$fileName = PathResolver::realpath($fileName);
if (!$namespaceNode) {
$namespaceNode = ReflectionEngine::parseFileNamespace($fileName, $namespaceName);
Expand Down
15 changes: 12 additions & 3 deletions src/ReflectionMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ public function ___debugInfo()
*/
public function __toString()
{
$hasReturnType = $this->hasReturnType();
$returnType = $this->getReturnType();
$hasReturnType = ($returnType !== null); // Internally same as: $this->hasReturnType();
Copy link

Choose a reason for hiding this comment

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

Not sure, that comment is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It came up in our discussion, and it wasn't obvious to me, so I added the comment.

Copy link

Choose a reason for hiding this comment

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

I thought that you will change

$hasReturnType    = ($returnType !== null); // Internally same as: $this->hasReturnType();

into

$hasReturnType    = $returnType !== null;

but you haven't.

Copy link
Contributor Author

@loren-osborn loren-osborn Aug 6, 2017

Choose a reason for hiding this comment

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

Adding the parentheses was inadvertent and I'll remove them, but I do think the comment is helpful. It's an explanation why we aren't calling hasReturnType(). @lisachenko seems to be deferring to you on this PR, so it's your call, but I think the comment has value.

if you disagree, I'll remove it; or if you prefer, I can put the comment on it's own line.

Update: To expedite things, I'll give you some unambiguous options ordered by my personal preference:

  1. Put the comment on its own line:
// Internally $this->getReturnType() !== null same as $this->hasReturnType()
$returnType       = $this->getReturnType();
$hasReturnType    = $returnType !== null;
  1. Just remove the parenthesis:
$returnType       = $this->getReturnType();
$hasReturnType    = $returnType !== null; // Internally same as: $this->hasReturnType();
  1. Strip the comment:
$returnType       = $this->getReturnType();
$hasReturnType    = $returnType !== null;

$paramsNeeded = $hasReturnType || $this->getNumberOfParameters() > 0;
$paramFormat = $paramsNeeded ? "\n\n - Parameters [%d] {%s\n }" : '';
$returnFormat = $hasReturnType ? "\n - Return [ %s ]" : '';
Expand All @@ -107,14 +108,22 @@ public function __toString()
$this->isFinal() ? ' final' : '',
$this->isStatic() ? ' static' : '',
$this->isAbstract() ? ' abstract' : '',
join(' ', \Reflection::getModifierNames($this->getModifiers() & 1792)),
join(
' ',
\Reflection::getModifierNames(
$this->getModifiers() &
(self::IS_PUBLIC | self::IS_PROTECTED | self::IS_PRIVATE)

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 20 spaces but found 24

Copy link

Choose a reason for hiding this comment

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

I guess this more expressive form of same thing? I wonder why you've added this 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.

Yeh... probably less relevant to this PR, but I generally hate non-expressive "magic numbers", and I can across this one while working on this PR. It probably doesn't belong, but I felt like it made the code clearer.

Copy link

Choose a reason for hiding this comment

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

OK, let's keep it then.

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 20 spaces but found 24

Copy link

Choose a reason for hiding this comment

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

Any way so solve this CS issue?

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 I read the issue correctly, @Nitpick-CI wants $this->getModifiers() intended the same as (self::IS_PUBLIC which I was concerned may be easily mistaken as a second argument to getModifierNames(). Despite not liking overly long lines, I think getting rid of the line break is the best option.

As above, unambiguous options in my personal order of preference:

  1. Remove the newline:
            join(
                ' ',
                \Reflection::getModifierNames(
                    $this->getModifiers() & (self::IS_PUBLIC | self::IS_PROTECTED | self::IS_PRIVATE)
                )
            ),
  1. Square up the two lines:
            join(
                ' ',
                \Reflection::getModifierNames(
                    $this->getModifiers() &
                    (self::IS_PUBLIC | self::IS_PROTECTED | self::IS_PRIVATE)
                )
            ),
  1. Leave them as-is:
            join(
                ' ',
                \Reflection::getModifierNames(
                    $this->getModifiers() &
                        (self::IS_PUBLIC | self::IS_PROTECTED | self::IS_PRIVATE)
                )
            ),

)
),
$this->getName(),
$this->getFileName(),
$this->getStartLine(),
$this->getEndLine(),
count($methodParameters),
$paramString,
$hasReturnType ? ReflectionType::convertToDisplayType($this->getReturnType()) : ''
( $returnType ?
ReflectionType::convertToDisplayType($returnType) :
'UNUSED: Prevents convertToDisplayType() being called.')
Copy link

Choose a reason for hiding this comment

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

😄 , what is that? Are you sure code is never executed under normal conditions?

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, I guess my "not quite a comment" explanatory string was unclear. When $returnType is non-null, this is definitely used. When $returnType is null, however, the '%s' substring that includes this in sprintf()'s return value, isn't included in the format string. If you can think of a clearer way to say this, I welcome suggestions.

Copy link

Choose a reason for hiding this comment

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

If UNUSED: Prevents convertToDisplayType() being called. text isn't actually displayed anywhere, then it doesn't make sense to write it.

If it's displayed, but same text isn't displayed in built-in reflection api, then this won't be compatible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is indeed not displayed. I can easily change it back to the empty string. I'm trying to decide if I need a comment there to replace it.

I admit that from a clarity standpoint, I don't like the way this function reads, but I think anything but an empty string probably just muddies the water. @aik099, what do you think?

);
}

Expand Down
2 changes: 1 addition & 1 deletion src/ReflectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public static function convertToDisplayType(\ReflectionType $type)
'int' => 'integer',
'bool' => 'boolean'
];
$displayType = $type->type;
$displayType = (string) $type;
Copy link

Choose a reason for hiding this comment

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

Are you sure, that according to coding standard there should be space between type name and variable?

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 don't like the way it looks either... it's what Scrutinizer was suggesting. I'm happy to loose the ugly space. It's your call.

Copy link

Choose a reason for hiding this comment

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

Please locate other type cast operators in the code (not added by you) and see which style is used most. Then use most used style.

if (isset($typeMap[$displayType])) {
$displayType = $typeMap[$displayType];
}
Expand Down
17 changes: 15 additions & 2 deletions src/Traits/ReflectionClassLikeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ public function getInterfaces()

/**
* {@inheritdoc}
* @param string $name
*/
public function getMethod($name)
{
Expand Down Expand Up @@ -442,7 +443,7 @@ public function getParentClass()
$extendsNode = $hasExtends ? $this->classLikeNode->$extendsField : null;
if ($extendsNode instanceof FullyQualified) {
$extendsName = $extendsNode->toString();
$parentClass = class_exists($extendsName, false) ? new parent($extendsName) : new static($extendsName);
$parentClass = $this->createReflectionForClass($extendsName);
}
$this->parentClass = $parentClass;
}
Expand Down Expand Up @@ -592,6 +593,7 @@ public function hasConstant($name)

/**
* {@inheritdoc}
* @param string $name
*/
public function hasMethod($name)
{
Expand Down Expand Up @@ -622,6 +624,7 @@ public function hasProperty($name)

/**
* {@inheritDoc}
* @param string $interfaceName
*/
public function implementsInterface($interfaceName)
{
Expand Down Expand Up @@ -698,7 +701,6 @@ public function isInstance($object)
}

$className = $this->getName();

return $className === get_class($object) || is_subclass_of($object, $className);
}

Expand Down Expand Up @@ -954,4 +956,15 @@ private function collectSelfConstants()
}
}
}

/**
* Create a ReflectionClass for a given class name.
*
* @param string $className
* The name of the class to create a reflection for.
*
* @return ReflectionClass
* The apropriate reflection object.
*/
abstract protected function createReflectionForClass($className);
}
18 changes: 18 additions & 0 deletions src/Traits/ReflectionFunctionLikeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ public function getStartLine()
*/
public function getStaticVariables()
{
// In nikic/PHP-Parser < 2.0.0 the default behavior is cloning
// nodes when traversing them. Passing FALSE to the constructor
// prevents this.
// In nikic/PHP-Parser >= 2.0.0 and < 3.0.0 the default behavior was
// changed to not clone nodes, but the parameter was retained as
// an option.
// In nikic/PHP-Parser >= 3.0.0 the option to clone nodes was removed
// as a constructor parameter, so Scrutinizer will pick this up as
// an issue. It is retained for legacy compatibility.
$nodeTraverser = new NodeTraverser(false);
$variablesCollector = new StaticVariablesCollector($this);
$nodeTraverser->addVisitor($variablesCollector);
Expand Down Expand Up @@ -274,6 +283,15 @@ public function isDeprecated()
*/
public function isGenerator()
{
// In nikic/PHP-Parser < 2.0.0 the default behavior is cloning
// nodes when traversing them. Passing FALSE to the constructor
// prevents this.
// In nikic/PHP-Parser >= 2.0.0 and < 3.0.0 the default behavior was
// changed to not clone nodes, but the parameter was retained as
// an option.
// In nikic/PHP-Parser >= 3.0.0 the option to clone nodes was removed
// as a constructor parameter, so Scrutinizer will pick this up as
// an issue. It is retained for legacy compatibility.
$nodeTraverser = new NodeTraverser(false);
$nodeDetector = new GeneratorDetector();
$nodeTraverser->addVisitor($nodeDetector);
Expand Down
38 changes: 28 additions & 10 deletions src/ValueResolver/NodeExpressionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,7 @@ protected function resolve(Node $node)
try {
++$this->nodeLevel;

$nodeType = $node->getType();
$methodName = 'resolve' . str_replace('_', '', $nodeType);
$methodName = $this->getDispatchMethodFor($node);
if (method_exists($this, $methodName)) {
$value = $this->$methodName($node);
}
Expand Down Expand Up @@ -142,7 +141,7 @@ protected function resolveScalarString(Scalar\String_ $node)
protected function resolveScalarMagicConstMethod()
{
if ($this->context instanceof \ReflectionMethod) {
$fullName = $this->context->getDeclaringClass()->getName() . '::' . $this->context->getShortName();
$fullName = $this->context->getDeclaringClass()->name . '::' . $this->context->getShortName();

return $fullName;
}
Expand Down Expand Up @@ -175,12 +174,12 @@ protected function resolveScalarMagicConstNamespace()
protected function resolveScalarMagicConstClass()
{
if ($this->context instanceof \ReflectionClass) {
return $this->context->getName();
return $this->context->name;
Copy link

Choose a reason for hiding this comment

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

Looks good.

}
if (method_exists($this->context, 'getDeclaringClass')) {
$declaringClass = $this->context->getDeclaringClass();
if ($declaringClass instanceof \ReflectionClass) {
return $declaringClass->getName();
return $declaringClass->name;
Copy link

Choose a reason for hiding this comment

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

Looks good.

}
}

Expand Down Expand Up @@ -213,7 +212,7 @@ protected function resolveScalarMagicConstLine(MagicConst\Line $node)
protected function resolveScalarMagicConstTrait()
{
if ($this->context instanceof \ReflectionClass && $this->context->isTrait()) {
return $this->context->getName();
return $this->context->name;
Copy link

Choose a reason for hiding this comment

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

Looks good.

}

return '';
Expand All @@ -224,8 +223,6 @@ protected function resolveExprConstFetch(Expr\ConstFetch $node)
$constantValue = null;
$isResolved = false;

/** @var ReflectionFileNamespace|null $fileNamespace */
$fileNamespace = null;
$isFQNConstant = $node->name instanceof Node\Name\FullyQualified;
$constantName = $node->name->toString();

Expand Down Expand Up @@ -256,7 +253,22 @@ protected function resolveExprConstFetch(Expr\ConstFetch $node)

protected function resolveExprClassConstFetch(Expr\ClassConstFetch $node)
{
$refClass = $this->fetchReflectionClass($node->class);
$classToReflect = $node->class;
Copy link

Choose a reason for hiding this comment

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

Doesn't seem to be related to Scrutinizer CI reported problems. Maybe belongs to separate PR with a description.

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 sure why this issue isn't showing up for you, nor why I can't find links to individual Scrutinizer issues, but this is to address the Scrutinizer issue reported for this line that reads:

Bug introduced 11 months ago by @lisachenko
It seems like $node->class can also be of type object<PhpParser\Node\Expr>; however, Go\ParserReflection\Valu...:fetchReflectionClass() does only seem to accept object<PhpParser\Node\Name>, maybe add an additional type check?

Copy link

Choose a reason for hiding this comment

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

Interesting. So class constant can be an expression. Seem to addition in PHP syntax, that PHP-Parser now supports. In either cases a test for that is needed too.

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, I know you can do a constant fetch from a class name stored in a variable. I'm unsure what other Exprs are valid. I believe ParserReflection uses this mostly for evaluating constants, so this might not be a valid code path as we are using it, but it is a valid value for $node->class in some circumstances.

As far as adding corresponding test cases, I agree, but I was lumping my test changes into a separate PR. (I'm shooting for 100% code coverage, but I've always been a cockeyed optimist.) I can add these tests to this PR after I've untangled my current repo branches. (I don't really want to do a stash right now.)

Copy link

@aik099 aik099 Jul 26, 2017

Choose a reason for hiding this comment

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

Yes, I know you can do a constant fetch from a class name stored in a variable. I'm unsure what other Exprs are valid.

I've meant Example #4 Constant expression example from http://php.net/manual/en/language.oop5.constants.php page:

const ONE = 1;

class foo {
    // As of PHP 5.6.0
    const TWO = ONE * 2;
    const THREE = ONE + self::TWO;
    const SENTENCE = 'The value of THREE is '.self::THREE;
}

This way you can create test for that kind of syntax.

As far as adding corresponding test cases, I agree, but I was lumping my test changes into a separate PR.

The tests for changed code needs to come as part of PR, where that code is changed. Later you can send another PR for improving code coverage.

Copy link
Contributor Author

@loren-osborn loren-osborn Jul 27, 2017

Choose a reason for hiding this comment

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

I agree we need to test for this case, but it isn't demonstrated in the example above. It would be something like this, which I'm doubting is valid PHP:

const LEFT = 'Foo';
const RIGHT = 'Bar';

class FooBar {
    const FILENAME = 'baz.php';
}

require((LEFT . RIGHT)::FILENAME);

Like I said... I'm unsure if this is valid PHP, but I think, given that PhpParser accepts an expression, and we need something that can evaluate at compile time, something like this would be a valid test, even if not legal PHP currently.

Opinions?

Thumbing up to remind myself to add test cases. 👍

Copy link

Choose a reason for hiding this comment

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

Anything that would force PHP-Parser to create Expr class would do. Code won't be ever evaluated by PHP itself and only needs to be syntactically valid.

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 (!($classToReflect instanceof Node\Name)) {
$classToReflect = $this->resolve($classToReflect) ?: $classToReflect;
if (!is_string($classToReflect)) {
$reason = 'Unable';
if ($classToReflect instanceof Expr) {
$methodName = $this->getDispatchMethodFor($classToReflect);
$reason = "Method " . __CLASS__ . "::{$methodName}() not found trying";
}
throw new ReflectionException("$reason to resolve class constant.");
}
// Strings evaluated as class names are always treated as fully
// qualified.
$classToReflect = new Node\Name\FullyQualified(ltrim($classToReflect, '\\'));
}
$refClass = $this->fetchReflectionClass($classToReflect);
$constantName = $node->name;

// special handling of ::class constants
Expand All @@ -265,7 +277,7 @@ protected function resolveExprClassConstFetch(Expr\ClassConstFetch $node)
}

$this->isConstant = true;
$this->constantName = (string)$node->class . '::' . $constantName;
$this->constantName = (string)$classToReflect . '::' . $constantName;

return $refClass->getConstant($constantName);
}
Expand Down Expand Up @@ -430,6 +442,12 @@ protected function resolveExprBinaryOpLogicalXor(Expr\BinaryOp\LogicalXor $node)
return $this->resolve($node->left) xor $this->resolve($node->right);
}

private function getDispatchMethodFor(Node $node)
{
$nodeType = $node->getType();
return 'resolve' . str_replace('_', '', $nodeType);
}

/**
* Utility method to fetch reflection class instance by name
*
Expand Down
9 changes: 4 additions & 5 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
use Go\ParserReflection\ReflectionEngine;

/**
* This file is used for automatic configuration of Go\ParserReflection\ReflectionEngine class
* This file is used for automatic configuration of
* Go\ParserReflection\ReflectionEngine class
*/
ReflectionEngine::init(new ComposerLocator());
// Polifyll for PHP<7.0
if (!class_exists(ReflectionType::class, false)) {
class ReflectionType {}
}

require(__DIR__ . '/polyfill.php');
Copy link

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm... I think this is a case of minor improvement in PSR-1 compliance for 1 extra small file load once, during execution. I think it's a worthwhile cost... and I think having Polyfills in a file called polyfill.php helps people know where to find them.

34 changes: 34 additions & 0 deletions src/polyfill.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
/**
* Parser Reflection API
*
* @copyright Copyright 2015, Lisachenko Alexander <[email protected]>
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/

/**
* This file is for ployfilling classes not defined in all supported
* versions of PHP, (i.e. PHP < 7).
*/
if (!class_exists(ReflectionType::class, false)) {
/* Dummy polyfill class */
class ReflectionType

Choose a reason for hiding this comment

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

Each class must be in a namespace of at least one level (a top-level vendor name)

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 obviously a non-issue for a polyfill class.

Choose a reason for hiding this comment

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

Each class must be in a namespace of at least one level (a top-level vendor name)

Choose a reason for hiding this comment

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

Each class must be in a namespace of at least one level (a top-level vendor name)

Choose a reason for hiding this comment

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

Each class must be in a namespace of at least one level (a top-level vendor name)

Choose a reason for hiding this comment

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

Each class must be in a namespace of at least one level (a top-level vendor name)

Choose a reason for hiding this comment

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

Each class must be in a namespace of at least one level (a top-level vendor name)

{
public function allowsNull()
{
return true;
}

public function isBuiltin()
{
return false;
}

public function __toString()
{
return '';
}
}
}
18 changes: 18 additions & 0 deletions tests/ReflectionFileNamespaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ protected function setUp()
include_once $fileName;
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage $fileName must be a string, but a array was passed
*/
public function testBadFilenameTypeArray()
{
new ReflectionFileNamespace([1, 3, 5, 7], 'BogusNamespace');
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage $fileName must be a string, but a object was passed
*/
public function testBadFilenameTypeObject()
{
new ReflectionFileNamespace(new \DateTime(), 'BogusNamespace');
}

public function testGetClass()
{
$refClass = $this->parsedRefFileNamespace->getClass('Unknown');
Expand Down
18 changes: 18 additions & 0 deletions tests/ReflectionFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ protected function setUp()
$this->parsedRefFile = $reflectionFile;
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage $fileName must be a string, but a array was passed
*/
public function testBadFilenameTypeArray()
{
new ReflectionFile([1, 3, 5, 7]);
}

/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage $fileName must be a string, but a object was passed
*/
public function testBadFilenameTypeObject()
{
new ReflectionFile(new \DateTime());
}

public function testGetName()
{
$fileName = $this->parsedRefFile->getName();
Expand Down
Loading