diff --git a/.travis.yml b/.travis.yml index 1c7bc4ee..11e19e8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 7.0 - 7.1 - 7.2 - hhvm diff --git a/README.md b/README.md index 5eccb58c..4984b0c3 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ Parser Reflection API Library Parser Reflection API library provides a set of classes that extend original internal Reflection classes, but powered by [PHP-Parser](https://github.com/nikic/PHP-Parser) library thus allowing to create a reflection instance without loading classes into the memory. -This library can be used for analysing the source code for PHP versions 5.5, 5.6, 7.0; for automatic proxy creation and much more. +This library can be used for analysing the source code for PHP versions 7.1, 7.2, 7.3; for automatic proxy creation and much more. [![Build Status](https://scrutinizer-ci.com/g/goaop/parser-reflection/badges/build.png?b=master)](https://scrutinizer-ci.com/g/goaop/parser-reflection/build-status/master) [![Code Coverage](https://scrutinizer-ci.com/g/goaop/parser-reflection/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/goaop/parser-reflection/?branch=master) [![Total Downloads](https://img.shields.io/packagist/dt/goaop/parser-reflection.svg)](https://packagist.org/packages/goaop/parser-reflection) [![Daily Downloads](https://img.shields.io/packagist/dd/goaop/parser-reflection.svg)](https://packagist.org/packages/goaop/parser-reflection) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/goaop/parser-reflection/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/goaop/parser-reflection/?branch=master) -[![SensioLabs Insight](https://img.shields.io/sensiolabs/i/1fdfee9c-839a-4209-a2f2-42dadc859621.svg)](https://insight.sensiolabs.com/projects/1fdfee9c-839a-4209-a2f2-42dadc859621)[![Minimum PHP Version](http://img.shields.io/badge/php-%3E%3D%205.5-8892BF.svg)](https://php.net/) +[![SensioLabs Insight](https://img.shields.io/sensiolabs/i/1fdfee9c-839a-4209-a2f2-42dadc859621.svg)](https://insight.sensiolabs.com/projects/1fdfee9c-839a-4209-a2f2-42dadc859621)[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.1-8892BF.svg)](https://php.net/) [![License](https://img.shields.io/packagist/l/goaop/parser-reflection.svg)](https://packagist.org/packages/goaop/parser-reflection) Installation diff --git a/composer.json b/composer.json index 7ee34347..5bb4ade8 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } }, "require": { - "php": ">=7.0", + "php": ">=7.1", "nikic/php-parser": "^4.0" }, "require-dev": { diff --git a/src/ReflectionClassConstant.php b/src/ReflectionClassConstant.php new file mode 100644 index 00000000..62249ced --- /dev/null +++ b/src/ReflectionClassConstant.php @@ -0,0 +1,189 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ +declare(strict_types=1); + +namespace Go\ParserReflection; + +use Go\ParserReflection\ValueResolver\NodeExpressionResolver; +use PhpParser\Node\Const_; +use PhpParser\Node\Stmt\ClassConst; +use PhpParser\Node\Stmt\ClassLike; +use \ReflectionClassConstant as BaseReflectionClassConstant; + +class ReflectionClassConstant extends BaseReflectionClassConstant +{ + /** + * Concrete class constant node + * + * @var ClassConst + */ + private $classConstantNode; + + /** + * @var Const_ + */ + private $constNode; + + /** + * Name of the class + * + * @var string + */ + private $className; + + /** + * Parses class constants from the concrete class node + * + * @param ClassLike $classLikeNode Class-like node + * @param string $reflectionClassName FQN of the class + * + * @return array|ReflectionClassConstant[] + */ + public static function collectFromClassNode(ClassLike $classLikeNode, string $reflectionClassName): array + { + $classConstants = []; + + foreach ($classLikeNode->stmts as $classLevelNode) { + if ($classLevelNode instanceof ClassConst) { + foreach ($classLevelNode->consts as $const) { + $classConstName = $const->name->toString(); + $classConstants[$classConstName] = new ReflectionClassConstant( + $reflectionClassName, + $classConstName, + $classLevelNode, + $const + ); + } + } + } + + return $classConstants; + } + + /** + * Initializes a reflection for the class constant + * + * @param string $className Name of the class + * @param string $classConstantName Name of the class constant to reflect + * @param ClassConst $classConstNode ClassConstant definition node + * @param Const_|null $constNode Concrete const definition node + */ + public function __construct( + string $className, + string $classConstantName, + ClassConst $classConstNode = null, + Const_ $constNode = null + ) { + $this->className = ltrim($className, '\\'); + + if (!$classConstNode || !$constNode) { + list($classConstNode, $constNode) = ReflectionEngine::parseClassConstant($className, $classConstantName); + } + + $this->classConstantNode = $classConstNode; + $this->constNode = $constNode; + } + + /** + * @inheritDoc + */ + public function getDeclaringClass() + { + return new ReflectionClass($this->className); + } + + /** + * @inheritDoc + */ + public function getDocComment() + { + $docBlock = $this->classConstantNode->getDocComment(); + + return $docBlock ? $docBlock->getText() : false; + } + + /** + * @inheritDoc + */ + public function getModifiers() + { + $modifiers = 0; + if ($this->isPublic()) { + $modifiers += ReflectionMethod::IS_PUBLIC; + } + if ($this->isProtected()) { + $modifiers += ReflectionMethod::IS_PROTECTED; + } + if ($this->isPrivate()) { + $modifiers += ReflectionMethod::IS_PRIVATE; + } + + return $modifiers; + } + + /** + * @inheritDoc + */ + public function getName() + { + return $this->constNode->name->toString(); + } + + /** + * @inheritDoc + */ + public function getValue() + { + $solver = new NodeExpressionResolver($this->getDeclaringClass()); + $solver->process($this->constNode->value); + return $solver->getValue(); + } + + /** + * @inheritDoc + */ + public function isPrivate() + { + return $this->classConstantNode->isPrivate(); + } + + /** + * @inheritDoc + */ + public function isProtected() + { + return $this->classConstantNode->isProtected(); + } + + /** + * @inheritDoc + */ + public function isPublic() + { + return $this->classConstantNode->isPublic(); + } + + /** + * @inheritDoc + */ + public function __toString() + { + $value = $this->getValue(); + $valueType = new ReflectionType(gettype($value), null, true); + + return sprintf( + "Constant [ %s %s %s ] { %s }\n", + implode(' ', \Reflection::getModifierNames($this->getModifiers())), + strtolower((string) ReflectionType::convertToDisplayType($valueType)), + $this->getName(), + (string) $value + ); + } +} diff --git a/src/ReflectionEngine.php b/src/ReflectionEngine.php index 8813c08f..5298db64 100644 --- a/src/ReflectionEngine.php +++ b/src/ReflectionEngine.php @@ -14,6 +14,7 @@ use Go\ParserReflection\NodeVisitor\RootNamespaceNormalizer; use PhpParser\Lexer; use PhpParser\Node; +use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Namespace_; @@ -199,6 +200,31 @@ public static function parseClassProperty($fullClassName, $propertyName) throw new \InvalidArgumentException("Property $propertyName was not found in the $fullClassName"); } + /** + * Parses class constants + * + * @param string $fullClassName + * @param string $constantName + * @return array Pair of [ClassConst and Const_] nodes + */ + public static function parseClassConstant(string $fullClassName, string $constantName): array + { + $class = self::parseClass($fullClassName); + $classNodes = $class->stmts; + + foreach ($classNodes as $classLevelNode) { + if ($classLevelNode instanceof ClassConst) { + foreach ($classLevelNode->consts as $classConst) { + if ($classConst->name->toString() === $constantName) { + return [$classLevelNode, $classConst]; + } + } + } + } + + throw new \InvalidArgumentException("ClassConstant $constantName was not found in the $fullClassName"); + } + /** * Parses a file and returns an AST for it * diff --git a/src/Traits/ReflectionClassLikeTrait.php b/src/Traits/ReflectionClassLikeTrait.php index 35b0391a..01ed9a7f 100644 --- a/src/Traits/ReflectionClassLikeTrait.php +++ b/src/Traits/ReflectionClassLikeTrait.php @@ -11,6 +11,7 @@ namespace Go\ParserReflection\Traits; use Go\ParserReflection\ReflectionClass; +use Go\ParserReflection\ReflectionClassConstant; use Go\ParserReflection\ReflectionException; use Go\ParserReflection\ReflectionMethod; use Go\ParserReflection\ReflectionProperty; @@ -94,6 +95,11 @@ trait ReflectionClassLikeTrait */ protected $properties; + /** + * @var array|ReflectionClassConstant[] + */ + protected $classConstants; + /** * Returns the string representation of the ReflectionClass object. * @@ -508,6 +514,45 @@ public function getProperty($name) return false; } + /** + * @inheritDoc + */ + public function getReflectionConstant($name) + { + $classConstants = $this->getReflectionConstants(); + foreach ($classConstants as $classConstant) { + if ($classConstant->getName() == $name) { + return $classConstant; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function getReflectionConstants() + { + if (!isset($this->classConstants)) { + $directClassConstants = ReflectionClassConstant::collectFromClassNode($this->classLikeNode, $this->getName()); + $parentClassConstants = $this->recursiveCollect(function (array &$result, \ReflectionClass $instance, $isParent) { + $reflectionClassConstants = []; + foreach ($instance->getReflectionConstants() as $reflectionClassConstant) { + if (!$isParent || !$reflectionClassConstant->isPrivate()) { + $reflectionClassConstants[$reflectionClassConstant->name] = $reflectionClassConstant; + } + } + $result += $reflectionClassConstants; + }); + $classConstants = $directClassConstants + $parentClassConstants; + + $this->classConstants = $classConstants; + } + + return array_values($this->classConstants); + } + /** * {@inheritDoc} */ diff --git a/src/bootstrap.php b/src/bootstrap.php index 803d845b..997211dd 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -16,5 +16,3 @@ * Go\ParserReflection\ReflectionEngine class */ ReflectionEngine::init(new ComposerLocator()); - -require(__DIR__ . '/polyfill.php'); diff --git a/src/polyfill.php b/src/polyfill.php deleted file mode 100644 index 22644580..00000000 --- a/src/polyfill.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * 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 - { - public function allowsNull() - { - return true; - } - - public function isBuiltin() - { - return false; - } - - public function __toString() - { - return ''; - } - } -} diff --git a/tests/ReflectionClassConstantTest.php b/tests/ReflectionClassConstantTest.php new file mode 100644 index 00000000..4fd3c7d1 --- /dev/null +++ b/tests/ReflectionClassConstantTest.php @@ -0,0 +1,105 @@ +setUpFile(__DIR__ . '/Stub/FileWithClasses71.php'); + } + + public function testGeneralInfoGetters() + { + $allNameGetters = [ + 'getDocComment', + 'getModifiers', + 'getName', + 'getValue', + 'isPrivate', + 'isProtected', + 'isPublic', + '__toString' + ]; + + foreach ($this->parsedRefFile->getFileNamespaces() as $fileNamespace) { + foreach ($fileNamespace->getClasses() as $refClass) { + $className = $refClass->getName(); + foreach ($refClass->getReflectionConstants() as $refReflectionConstant) { + $classConstantName = $refReflectionConstant->getName(); + $originalRefParameter = new \ReflectionClassConstant($className, $classConstantName); + foreach ($allNameGetters as $getterName) { + $expectedValue = $originalRefParameter->$getterName(); + $actualValue = $refReflectionConstant->$getterName(); + $this->assertSame( + $expectedValue, + $actualValue, + "{$getterName}() for parameter {$className}::{$classConstantName} should be equal" + ); + } + } + } + } + } + + public function testGetClassConstant() + { + $parsedNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub'); + $parsedClass = $parsedNamespace->getClass(ClassWithPhp71Features::class); + + $classConstants = $parsedClass->getReflectionConstants(); + $this->assertSame($classConstants[0], $parsedClass->getReflectionConstant('PUBLIC_CONST_A')); + $this->assertSame($classConstants[1], $parsedClass->getReflectionConstant('PUBLIC_CONST_B')); + $this->assertSame($classConstants[2], $parsedClass->getReflectionConstant('PROTECTED_CONST')); + $this->assertSame($classConstants[3], $parsedClass->getReflectionConstant('PRIVATE_CONST')); + $this->assertSame($classConstants[4], $parsedClass->getReflectionConstant('CALCULATED_CONST')); + } + + public function testCoverAllMethods() + { + $allInternalMethods = get_class_methods(\ReflectionClassConstant::class); + $allMissedMethods = []; + + foreach ($allInternalMethods as $internalMethodName) { + if ('export' === $internalMethodName) { + continue; + } + $refMethod = new \ReflectionMethod(ReflectionClassConstant::class, $internalMethodName); + $definerClass = $refMethod->getDeclaringClass()->getName(); + if (strpos($definerClass, 'Go\\ParserReflection') !== 0) { + $allMissedMethods[] = $internalMethodName; + } + } + + if ($allMissedMethods) { + $this->markTestIncomplete('Methods ' . implode($allMissedMethods, ', ') . ' are not implemented'); + } + } + + /** + * Setups file for parsing + * + * @param string $fileName File name to use + */ + private function setUpFile($fileName) + { + $fileName = stream_resolve_include_path($fileName); + $fileNode = ReflectionEngine::parseFile($fileName); + + $reflectionFile = new ReflectionFile($fileName, $fileNode); + $this->parsedRefFile = $reflectionFile; + + include_once $fileName; + } +} diff --git a/tests/ReflectionClassTest.php b/tests/ReflectionClassTest.php index 3f6e92c7..52d91914 100644 --- a/tests/ReflectionClassTest.php +++ b/tests/ReflectionClassTest.php @@ -127,6 +127,28 @@ public function testGetMethodCount($fileName) } } + /** + * Tests getReflectionConstants() returns correct number of reflectionConstants for the class + * + * @dataProvider getFilesToAnalyze + * + * @param string $fileName File name to test + */ + public function testGetReflectionConstantCount($fileName) + { + $this->setUpFile($fileName); + $parsedClasses = $this->parsedRefFileNamespace->getClasses(); + + foreach ($parsedClasses as $parsedRefClass) { + $originalRefClass = new \ReflectionClass($parsedRefClass->getName()); + $parsedReflectionConstants = $parsedRefClass->getReflectionConstants(); + $originalReflectionConstants = $originalRefClass->getReflectionConstants(); + $this->assertCount(count($originalReflectionConstants), $parsedReflectionConstants); + } + } + + + /** * Tests getProperties() returns correct number of properties for the class * @@ -272,6 +294,22 @@ public function testGetConstant() $this->assertSame($originalRefClass->getConstant('E'), $parsedRefClass->getConstant('E')); } + public function testGetReflectionConstant() + { + $parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithScalarConstants::class); + $originalRefClass = new \ReflectionClass(ClassWithScalarConstants::class); + + $this->assertFalse($parsedRefClass->getReflectionConstant('NOT_EXISTING')); + $this->assertSame( + (string) $originalRefClass->getReflectionConstant('D'), + (string) $parsedRefClass->getReflectionConstant('D') + ); + $this->assertSame( + (string) $originalRefClass->getReflectionConstant('E'), + (string) $parsedRefClass->getReflectionConstant('E') + ); + } + /** * Returns list of ReflectionMethod getters that be checked directly without additional arguments * diff --git a/tests/ReflectionTypeTest.php b/tests/ReflectionTypeTest.php index c165437c..39dab937 100644 --- a/tests/ReflectionTypeTest.php +++ b/tests/ReflectionTypeTest.php @@ -18,8 +18,6 @@ protected function setUp() * * We're already testing it with Go\ParserReflection\ReflectionType * elsewhere. - * - * @requires PHP 7.0.0 */ public function testTypeConvertToDisplayTypeWithNativeType() { @@ -41,8 +39,6 @@ public function testTypeConvertToDisplayTypeWithNativeType() * * We're already testing it with Go\ParserReflection\ReflectionType * elsewhere. - * - * @requires PHP 7.1.0 */ public function testTypeConvertToDisplayTypeWithNullableNativeType() { @@ -64,8 +60,6 @@ public function testTypeConvertToDisplayTypeWithNullableNativeType() * * We're already testing it with Go\ParserReflection\ReflectionType * elsewhere. - * - * @requires PHP 7.0.0 */ public function testTypeConvertToDisplayTypeImplicitlyNullable() { diff --git a/tests/Stub/FileWithClasses71.php b/tests/Stub/FileWithClasses71.php index 6005e048..559ddba3 100644 --- a/tests/Stub/FileWithClasses71.php +++ b/tests/Stub/FileWithClasses71.php @@ -37,11 +37,28 @@ public function returnsNamedObject() : ?P {} class ClassWithPhp71Features { + /** + * Description for PUBLIC_CONST_A + */ const PUBLIC_CONST_A = 1; + + /** + * Description for PUBLIC_CONST_B + */ public const PUBLIC_CONST_B = 2; + + /** + * Description for PROTECTED_CONST + */ protected const PROTECTED_CONST = 3; + + /** + * Description for PRIVATE_CONST + */ private const PRIVATE_CONST = 4; + public const CALCULATED_CONST = 1 + 1; + public function returnsVoid() : void {} public function acceptsIterable(iterable $iterable) : iterable {} }