.github/dependabot.yml 0000644 00000000224 14641221054 0010731 0 ustar 00 version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
time: "22:00"
open-pull-requests-limit: 10
.github/workflows/phpunit.yml 0000644 00000003017 14641221054 0012353 0 ustar 00 name: "PHPUnit tests"
on:
pull_request:
push:
jobs:
phpunit:
name: "PHPUnit tests"
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
dependencies:
- "lowest"
- "highest"
php-version:
- "8.2"
- "8.3"
operating-system:
- "ubuntu-latest"
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1
tools: composer:v2, cs2pr
- name: "Cache dependencies"
uses: "actions/cache@v2"
with:
path: |
~/.composer/cache
vendor
key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}"
- name: "Install lowest dependencies"
if: ${{ matrix.dependencies == 'lowest' }}
run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest"
- name: "Install highest dependencies"
if: ${{ matrix.dependencies == 'highest' }}
run: "composer update --no-interaction --no-progress --no-suggest"
- name: "Install locked dependencies"
if: ${{ matrix.dependencies == 'locked' }}
run: "composer install --no-interaction --no-progress --no-suggest"
- name: "Tests"
run: "vendor/bin/phpunit"
.gitignore 0000644 00000000055 14641221054 0006533 0 ustar 00 /vendor/
composer.lock
/.phpunit.result.cache .scrutinizer.yml 0000644 00000001517 14641221054 0007731 0 ustar 00 before_commands:
- "composer install --no-dev --prefer-source"
tools:
external_code_coverage:
timeout: 1200
php_code_coverage:
enabled: true
php_code_sniffer:
enabled: true
config:
standard: PSR2
filter:
paths: ["src/*"]
php_cpd:
enabled: true
excluded_dirs: ["docs", "examples", "tests", "vendor"]
php_cs_fixer:
enabled: true
config:
level: all
filter:
paths: ["src/*"]
php_loc:
enabled: true
excluded_dirs: ["docs", "examples", "tests", "vendor"]
php_pdepend:
enabled: true
excluded_dirs: ["docs", "examples", "tests", "vendor"]
php_analyzer:
enabled: true
filter:
paths: ["src/*"]
sensiolabs_security_checker: true
LICENSE 0000644 00000002055 14641221054 0005552 0 ustar 00 Copyright (c) 2015-2024 Alexander Lisachenko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
README.md 0000644 00000011777 14641221054 0006037 0 ustar 00 Parser Reflection API Library
-----------------
This library is **deprecated**. Please use [BetterReflection](https://github.com/Roave/BetterReflection).
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 automatic proxy creation and much more.
[](https://scrutinizer-ci.com/g/goaop/parser-reflection/build-status/master)
[](https://scrutinizer-ci.com/g/goaop/parser-reflection/?branch=master)
[](https://packagist.org/packages/goaop/parser-reflection)
[](https://packagist.org/packages/goaop/parser-reflection)
[](https://scrutinizer-ci.com/g/goaop/parser-reflection/?branch=master)
[](https://php.net/)
[](https://packagist.org/packages/goaop/parser-reflection)
Installation
------------
Library can be installed with Composer. Installation is quite easy:
```bash
$ composer require goaop/parser-reflection
```
Composer will install the library to your project's `vendor/goaop/parser-reflection` directory.
Usage
------------
### Initialization
Prior to the first use library can be optionally initialized. If you use Composer for installing packages and loading classes,
then you shouldn't worry about initialization, library will be initialized automatically.
If project uses a custom autoloader then you should follow the next steps:
1. Create a new class that implements `\Go\ParserReflection\LocatorInterface`
2. Create an instance of that class and pass it to the `ReflectionEngine::init()` method for initial configuration
### Reflecting concrete classes/methods/properties without loading them
Just use `Go\ParserReflection` package reflection classes like traditional ones:
```php
$parsedClass = new \Go\ParserReflection\ReflectionClass(SomeClass::class);
var_dump($parsedClass->getMethods());
$parsedMethod = new \Go\ParserReflection\ReflectionMethod(SomeClass::class, 'someMethod');
echo (string)$parsedMethod;
```
Or you can use an additional classes [`ReflectionFile`][0] and [`ReflectionFileNamespace`][1] to analyse a raw PHP files:
```php
$parsedFile = new \Go\ParserReflection\ReflectionFile('SomeClass.php');
$fileNameSpaces = $parsedFile->getFileNamespaces();
// We can iterate over namespaces in the file
foreach ($fileNameSpaces as $namespace) {
$classes = $namespace->getClasses();
// Iterate over the classes in the namespace
foreach ($classes as $class) {
echo "Found class: ", $class->getName(), PHP_EOL;
// Now let's show all methods in the class
foreach ($class->getMethods() as $method) {
echo "Found class method: ", $class->getName(), '::', $method->getName(), PHP_EOL;
}
// ...all properties in the class
foreach ($class->getProperties() as $property) {
echo "Found class property: ", $class->getName(), '->', $property->getName(), PHP_EOL;
}
}
}
```
How it works?
------------
To understand how library works let's look at what happens during the call to the `new \Go\ParserReflection\ReflectionClass(SomeClass::class)`
* `\Go\ParserReflection\ReflectionClass` asks reflection engine to give an AST node for the given class name
* Reflection engine asks a locator to locate a filename for the given class
* `ComposerLocator` instance asks the Composer to find a filename for the given class and returns this result back to the reflection engine
* Reflection engine loads the content of file and passes it to the [PHP-Parser](https://github.com/nikic/PHP-Parser) for tokenization and processing
* PHP-Parser returns an AST (Abstract Syntax Tree)
* Reflection engine then analyse this AST to extract specific nodes and wrap them into corresponding reflection classes.
Compatibility
------------
All parser reflection classes extend PHP internal reflection classes, this means that you can use `\Go\ParserReflection\ReflectionClass` instance in any place that asks for `\ReflectionClass` instance. All reflection methods should be compatible with original ones, providing an except methods that requires object manipulation, such as `invoke()`, `invokeArgs()`, `setAccessible()`, etc. These methods will trigger the process of class loading and switching to the internal reflection.
[0]: docs/reflection_file.md
[1]: docs/reflection_file_namespace.md
composer.json 0000644 00000001471 14641221054 0007270 0 ustar 00 {
"name": "goaop/parser-reflection",
"description": "Provides reflection information, based on raw source",
"license": "MIT",
"authors": [
{
"name": "Alexander Lisachenko",
"email": "lisachenko.it@gmail.com"
}
],
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"Go\\ParserReflection\\": "src"
},
"files": ["src/bootstrap.php"],
"exclude-from-classmap": ["/tests/"]
},
"autoload-dev": {
"psr-4": {
"Go\\ParserReflection\\": "tests"
}
},
"require": {
"php": ">=8.2",
"nikic/php-parser": "^5.0"
},
"require-dev": {
"phpunit/phpunit": "^10.5.8",
"tracy/tracy": "^2.10",
"rector/rector": "^1.0",
"rector/rector-php-parser": "^0.14.0"
},
"extra": {
"branch-alias": {
"dev-master": "4.x-dev"
}
}
}
docs/README.md 0000644 00000002222 14641221054 0006750 0 ustar 00 ParserReflection API Reference
==============
Introduction
--------
PHP comes with a complete reflection API that adds the ability to reverse-engineer classes, interfaces, functions, methods and extensions. Additionally, the reflection API offers ways to retrieve doc comments for functions, classes and methods.
However, this reflection API requires concrete element to be loaded into the memory. Once element is loaded, it cannot be changed, modified or updated without special extensions.
`goaop\parser-reflection` packet is a user-land implementation of reflection API that is fully compatible with internal one, but doesn't require an element (e.g. class or function) to be loaded into the PHP. Only the source code is required for performing the raw analysis.
Reference
---------
- [`ReflectionClass`](reflection_class.md)
- [`ReflectionFile`](reflection_file.md)
- [`ReflectionFileNamespace`](reflection_file_namespace.md)
- [`ReflectionFunction`](reflection_function.md)
- [`ReflectionMethod`](reflection_method.md)
- [`ReflectionParameter`](reflection_parameter.md)
- [`ReflectionProperty`](reflection_property.md)
- [`ReflectionType`](reflection_type.md)
docs/reflection_class.md 0000644 00000001477 14641221054 0011345 0 ustar 00 ReflectionClass
==============
The `ReflectionClass` class reports an information about a class. This class is available in the standard PHP, so for any questions, please look at documentation for [`ReflectionClass`][0]
But be careful, that several methods require the class to be loaded into the memory, otherwise an exception will be thrown.
List of methods, that require class to be loaded
---------
- ReflectionClass::newInstance — Creates a new class instance from given arguments.
- ReflectionClass::newInstanceArgs — Creates a new class instance from given arguments.
- ReflectionClass::newInstanceWithoutConstructor — Creates a new class instance without invoking the constructor.
- ReflectionClass::setStaticPropertyValue — Sets static property value
[0]: http://php.net/manual/en/class.reflectionclass.php
docs/reflection_file.md 0000644 00000003132 14641221054 0011145 0 ustar 00 ReflectionFile
==============
The `ReflectionFile` class reports an information about a file with valid PHP source code. This class is not available in the standard PHP
API
---
```php
class ReflectionFile
{
public function __construct($fileName, $topLevelNodes = null) {}
public function getFileNamespace($namespaceName) {}
public function getFileNamespaces() {}
public function getName() {}
public function hasFileNamespace($namespaceName) {}
public function isStrictMode() {}
}
```
Methods
-------
- `ReflectionFile::__construct($fileName, $topLevelNodes = null)`
Constructs an instance of `ReflectionFile` object for given `fileName`. Can accept custom AST-tree nodes as optional parameter.
- `ReflectionFile::getFileNamespace($namespaceName)`
Returns a [`ReflectionFileNamespace`][0] instance for the specified namespace in a file. If you don't know an exact name of namespace in the file, then use `getFileNamespaces()` method to get a full list of namespaces for inspection.
- `ReflectionFile::getFileNamespaces()`
Returns an array with available namespaces in the file. Each `namespace {}` section will be represented as a single instance of [`ReflectionFileNamespace`][0] in this list.
- `ReflectionFile::getName()`
Returns a string with the name of file
- `ReflectionFile::hasFileNamespace($namespaceName)`
Checks if requested namespace is present in the file or not. Returns `true` if present.
- `ReflectionFile::isStrictMode()`
Checks if current file has enabled strict mode via `declare(strict_types=1)` for PHP>=7.0
[0]: reflection_file_namespace.md
docs/reflection_file_namespace.md 0000644 00000007122 14641221054 0013164 0 ustar 00 ReflectionFileNamespace
==============
The `ReflectionFileNamespace` class reports an information about a namespace in the single file. This class is not available in the standard PHP
API
---
```php
class ReflectionFileNamespace
{
public function __construct($fileName, $namespaceName, Namespace_ $namespaceNode = null) {}
public function getClass($className) {}
public function getClasses() {}
public function getConstant($constantName) {}
public function getConstants() {}
public function getDocComment() {}
public function getEndLine() {}
public function getFileName() {}
public function getFunction($functionName) {}
public function getFunctions() {}
public function getName() {}
public function getNamespaceAliases() {}
public function getStartLine() {}
public function hasClass($className) {}
public function hasConstant($constantName) {}
public function hasFunction($functionName) {}
}
```
Methods
-------
- `ReflectionFileNamespace::__construct($fileName, $namespaceName, Namespace_ $namespaceNode = null)`
Constructs an instance of `ReflectionFileNamespace` object for given `fileName` and `namespaceName`. Can accept custom `Namespace_` node as optional parameter.
- `ReflectionFileNamespace::getClass($className)`
Returns the concrete [`ReflectionClass`][0] from the file namespace or `false` if there is no such a class in the current namespace
- `ReflectionFileNamespace::getClasses()`
Returns an array with available classes in the namespace. Each class/trait/interface definition will be represented as a single instance of [`ReflectionClass`][0] in this list.
- `ReflectionFileNamespace::getConstant($constantName)`
Returns a value for the constant with name `$constantName` in the file namespace or `false` if there is no such a constant in the current namespace.
- `ReflectionFileNamespace::getConstants()`
Returns an array with all available constants in the namespace.
- `ReflectionFileNamespace::getDocComment()`
Returns a doc-block section for file namespace if present, otherwise `false`
- `ReflectionFileNamespace::getEndLine()`
Returns a end line for the namespace. Be careful, this value is not correct for simple `namespace Name;` definitions.
- `ReflectionFileNamespace::getFileName()`
Returns a string with the name of file
- `ReflectionFileNamespace::getFunction($functionName)`
Returns the concrete [`ReflectionFunction`][1] from the file namespace or `false` if there is no such a function in the current namespace
- `ReflectionFileNamespace::getFunctions()`
Returns an array with available functions in the namespace. Each function definition will be represented as a single instance of [`ReflectionFunction`][1] in this list.
- `ReflectionFile::getName()`
Returns a string with the name of current file namespace
- `ReflectionFileNamespace::getNamespaceAliases()`
Returns an array with all imported class namespaces and aliases for them in the current namespace.
- `ReflectionFileNamespace::getStartLine()`
Returns a start line for the namespace.
- `ReflectionFileNamespace::hasClass($className)`
Checks if the requested class is present in the file namespace or not. Returns `true` if present.
- `ReflectionFileNamespace::hasConstant($constantName)`
Checks if the requested constant is present in the file namespace or not. Returns `true` if present.
- `ReflectionFileNamespace::hasFunction($functionName)`
Checks if the requested function is present in the file namespace or not. Returns `true` if present.
[0]: reflection_class.md
[1]: reflection_function.md
docs/reflection_function.md 0000644 00000001260 14641221054 0012053 0 ustar 00 ReflectionFunction
==============
The `ReflectionFunction` class reports an information about a function. This class is available in the standard PHP, so for any questions, please look at documentation for [`ReflectionFunction`][0]
But be careful, that several methods require the function to be loaded into the memory, otherwise an exception will be thrown.
List of methods, that require function to be loaded
---------
- ReflectionFunction::getClosure — Returns a dynamically created closure for the function
- ReflectionFunction::invoke — Invokes function
- ReflectionFunction::invokeArgs — Invokes function args
[0]: http://php.net/manual/en/class.reflectionfunction.php
docs/reflection_method.md 0000644 00000001371 14641221054 0011511 0 ustar 00 ReflectionMethod
==============
The `ReflectionMethod` class reports an information about a method. This class is available in the standard PHP, so for any questions, please look at documentation for [`ReflectionMethod`][0]
But be careful, that several methods require the method and class to be loaded into the memory, otherwise an exception will be thrown.
List of methods, that require method and a corresponding class to be loaded
---------
- ReflectionMethod::getClosure — Returns a dynamically created closure for the method
- ReflectionMethod::invoke — Invokes method
- ReflectionMethod::invokeArgs — Invokes method args
- ReflectionMethod::setAccessible — Set method accessibility
[0]: http://php.net/manual/en/class.reflectionmethod.php
docs/reflection_parameter.md 0000644 00000000455 14641221054 0012213 0 ustar 00 ReflectionParameter
==============
The `ReflectionParameter` class reports an information about an parameter. This class is available in the standard PHP, so for any questions, please look at documentation for [`ReflectionParameter`][0]
[0]: http://php.net/manual/en/class.reflectionparameter.php
docs/reflection_property.md 0000644 00000001205 14641221054 0012111 0 ustar 00 ReflectionProperty
==============
The `ReflectionProperty` class reports an information about a property. This class is available in the standard PHP, so for any questions, please look at documentation for [`ReflectionProperty`][0]
But be careful, that several methods require the property and class to be loaded into the memory, otherwise an exception will be thrown.
List of methods, that require property and a corresponding class to be loaded
---------
- ReflectionProperty::setAccessible — Set property accessibility
- ReflectionProperty::setValue — Set property value
[0]: http://php.net/manual/en/class.reflectionproperty.php
docs/reflection_type.md 0000644 00000000423 14641221054 0011207 0 ustar 00 ReflectionType
==============
The `ReflectionType` class reports an information about a type. This class is available in the standard PHP, so for any questions, please look at documentation for [`ReflectionType`][0]
[0]: http://php.net/manual/en/class.reflectiontype.php
nitpick.json 0000644 00000000117 14641221054 0007076 0 ustar 00 {
"ignore": [
"src/*",
"tests/*",
"docs/*"
]
}
phpunit.xml.dist 0000644 00000001015 14641221054 0007713 0 ustar 00
./tests/./src/
rector.php 0000644 00000002121 14641221054 0006546 0 ustar 00 withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
// uncomment to reach your current PHP version
// ->withPhpSets()
->withRules([
AddVoidReturnTypeWhereNoReturnRector::class,
RemoveUselessParamTagRector::class,
RemoveUselessReturnTagRector::class,
PublicConstantVisibilityRector::class,
ClosureToArrowFunctionRector::class,
])
->withSets([
PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES,
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PhpParserSetList::PHP_PARSER_50
]);
src/Instrument/PathResolver.php 0000644 00000005376 14641221054 0012644 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Instrument;
/**
* Special class for resolving path for different file systems, wrappers, etc
*
* @see http://stackoverflow.com/questions/4049856/replace-phps-realpath/4050444
* @see http://bugs.php.net/bug.php?id=52769
*
* @link https://github.com/goaop/framework/blob/master/src/Instrument/PathResolver.php
*/
class PathResolver
{
/**
* Custom replacement for realpath() and stream_resolve_include_path()
*
* @param string|array $somePath Path without normalization or array of paths
* @param bool $shouldCheckExistence Flag for checking existence of resolved filename
*
* @return array|bool|string
*/
public static function realpath($somePath, $shouldCheckExistence = false)
{
// Do not resolve empty string/false/arrays into the current path
if (!$somePath) {
return $somePath;
}
if (is_array($somePath)) {
return array_map([__CLASS__, __FUNCTION__], $somePath);
}
// Trick to get scheme name and path in one action. If no scheme, then there will be only one part
$components = explode('://', $somePath, 2);
[$pathScheme, $path] = isset($components[1]) ? $components : [null, $components[0]];
// Optimization to bypass complex logic for simple paths (eg. not in phar archives)
if (!$pathScheme && ($fastPath = stream_resolve_include_path($somePath))) {
return $fastPath;
}
$isRelative = !$pathScheme && ($path[0] !== '/') && ($path[1] !== ':');
if ($isRelative) {
$path = getcwd() . DIRECTORY_SEPARATOR . $path;
}
// resolve path parts (single dot, double dot and double delimiters)
$path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
if (strpos($path, '.') !== false) {
$parts = explode(DIRECTORY_SEPARATOR, $path);
$absolutes = [];
foreach ($parts as $part) {
if ('.' === $part) {
continue;
}
if ('..' === $part) {
array_pop($absolutes);
} else {
$absolutes[] = $part;
}
}
$path = implode(DIRECTORY_SEPARATOR, $absolutes);
}
if ($pathScheme) {
$path = "{$pathScheme}://{$path}";
}
if ($shouldCheckExistence && !file_exists($path)) {
return false;
}
return $path;
}
}
src/Locator/CallableLocator.php 0000644 00000001721 14641221054 0012472 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Locator;
use Go\ParserReflection\LocatorInterface;
/**
* Locator, that can find a file for the given class name by asking composer
* @see \Go\ParserReflection\Locator\CallableLocatorTest
*/
class CallableLocator implements LocatorInterface
{
/**
* @var callable
*/
private $callable;
public function __construct(callable $callable)
{
$this->callable = $callable;
}
/**
* Returns a path to the file for given class name
*
* @param string $className Name of the class
*/
public function locateClass(string $className): false|string
{
return call_user_func($this->callable, ltrim($className, '\\'));
}
}
src/Locator/ComposerLocator.php 0000644 00000003304 14641221054 0012561 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Locator;
use Composer\Autoload\ClassLoader;
use Go\ParserReflection\Instrument\PathResolver;
use Go\ParserReflection\LocatorInterface;
use Go\ParserReflection\ReflectionException;
/**
* Locator, that can find a file for the given class name by asking composer
* @see \Go\ParserReflection\Locator\ComposerLocatorTest
*/
class ComposerLocator implements LocatorInterface
{
/**
* @var ClassLoader
*/
private $loader;
public function __construct(ClassLoader $composerLoader = null)
{
if ($composerLoader === null) {
$loaders = spl_autoload_functions();
foreach ($loaders as $loader) {
if (is_array($loader) && $loader[0] instanceof ClassLoader) {
$composerLoader = $loader[0];
break;
}
}
if ($composerLoader === null) {
throw new ReflectionException('Can not found a correct composer loader');
}
}
$this->loader = $composerLoader;
}
/**
* Returns a path to the file for given class name
*
* @param string $className Name of the class
**/
public function locateClass(string $className): false|string
{
$filePath = $this->loader->findFile(ltrim($className, '\\'));
if (!empty($filePath)) {
$filePath = PathResolver::realpath($filePath);
}
return $filePath;
}
}
src/LocatorInterface.php 0000644 00000001175 14641221054 0011273 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
/**
* Locator is responsible to return a file name for given item, typically class
*/
interface LocatorInterface
{
/**
* Returns a path to the file for given class name
*
* @param string $className Name of the class (with or without leading '\' FQCN)
*/
public function locateClass(string $className): false|string;
}
src/NodeVisitor/GeneratorDetector.php 0000644 00000002015 14641221054 0013726 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\NodeVisitor;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
/**
* Visitor to check if the method body
*/
class GeneratorDetector extends NodeVisitorAbstract
{
private bool $isGenerator = false;
/**
* {@inheritDoc}
*/
public function enterNode(Node $node)
{
// There may be internal generators in closures, we do not need to look at them
if ($node instanceof Node\Expr\Closure) {
return self::DONT_TRAVERSE_CHILDREN;
}
if ($node instanceof Node\Expr\Yield_ || $node instanceof Node\Expr\YieldFrom) {
$this->isGenerator = true;
}
return null;
}
public function isGenerator(): bool
{
return $this->isGenerator;
}
}
src/NodeVisitor/RootNamespaceNormalizer.php 0000644 00000003277 14641221054 0015124 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\NodeVisitor;
use PhpParser\Node\Stmt\Declare_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\NodeVisitorAbstract;
/**
* Visitor to normalize the root namespace for the files without the namespace (root namespace)
*
* File->Namespace->Statements
*/
class RootNamespaceNormalizer extends NodeVisitorAbstract
{
/**
* {@inheritdoc}
*/
public function beforeTraverse(array $nodes)
{
// namespaces can be only top-level nodes, so we can scan them directly
foreach ($nodes as $topLevelNode) {
if ($topLevelNode instanceof Namespace_) {
// file has namespace in it, nothing to change, returning null
return null;
}
}
// if we don't have a namespaces at all, this is global namespace, wrap everything in it, except declares
$lastDeclareOffset = 0;
foreach ($nodes as $lastDeclareOffset => $node) {
if (!$node instanceof Declare_) {
// $declareOffset now stores the position of first non-declare node statement
break;
}
}
// Wrap all statements into the namespace block
$globalNamespaceNode = new Namespace_(null, array_slice($nodes, $lastDeclareOffset));
// Replace top-level nodes with namespaced node
array_splice($nodes, $lastDeclareOffset, count($nodes), [$globalNamespaceNode]);
return $nodes;
}
}
src/NodeVisitor/StaticVariablesCollector.php 0000644 00000004454 14641221054 0015246 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\NodeVisitor;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
/**
* Visitor to collect static variables in the method/function body and resolve them
*/
class StaticVariablesCollector extends NodeVisitorAbstract
{
/**
* Reflection context, eg. ReflectionClass, ReflectionMethod, etc
*/
private mixed $context;
private array $staticVariables = [];
/**
* Default constructor
*
* @param mixed $context Reflection context, eg. ReflectionClass, ReflectionMethod, etc
*/
public function __construct(mixed $context)
{
$this->context = $context;
}
/**
* {@inheritDoc}
*/
public function enterNode(Node $node)
{
// There may be internal closures, we do not need to look at them
if ($node instanceof Node\Expr\Closure) {
return self::DONT_TRAVERSE_CHILDREN;
}
if ($node instanceof Node\Stmt\Static_) {
$expressionSolver = new NodeExpressionResolver($this->context);
$staticVariables = $node->vars;
foreach ($staticVariables as $staticVariable) {
$expr = $staticVariable->default;
if ($expr) {
$expressionSolver->process($expr);
$value = $expressionSolver->getValue();
} else {
$value = null;
}
if ($staticVariable->var->name instanceof Node\Expr) {
$expressionSolver->process($staticVariable->var->name);
$name = $expressionSolver->getValue();
} else {
$name = $staticVariable->var->name;
}
$this->staticVariables[$name] = $value;
}
}
return null;
}
/**
* Returns an associative map of static variables in the method/function body
*/
public function getStaticVariables(): array
{
return $this->staticVariables;
}
}
src/ReflectionAttribute.php 0000644 00000005566 14641221054 0012035 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use PhpParser\Node;
use PhpParser\Node\Param;
use PhpParser\Node\PropertyItem;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Property;
use ReflectionAttribute as BaseReflectionAttribute;
/**
* ref original usage https://3v4l.org/duaQI
*/
class ReflectionAttribute extends BaseReflectionAttribute
{
public function __construct(
private string $attributeName,
private ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionClassConstant|ReflectionFunction|ReflectionParameter $reflector,
private array $arguments,
private bool $isRepeated,
) {
}
public function getNode(): Node\Attribute
{
/** @var Class_|ClassMethod|PropertyItem|ClassConst|Function_|Param $node */
$node = $this->reflector->getNode();
// attrGroups only exists in Property Stmt
if ($node instanceof PropertyItem) {
$node = $this->reflector->getTypeNode();
}
$nodeExpressionResolver = new NodeExpressionResolver($this);
foreach ($node->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
if ($attr->name->toString() !== $this->attributeName) {
continue;
}
$arguments = [];
foreach ($attr->args as $arg) {
$nodeExpressionResolver->process($arg->value);
$arguments[] = $nodeExpressionResolver->getValue();
}
if ($arguments !== $this->arguments) {
continue;
}
return $attr;
}
}
throw new ReflectionException('ReflectionAttribute should be initiated from Go\ParserReflection Reflection classes');
}
public function isRepeated(): bool
{
return $this->isRepeated;
}
/**
* {@inheritDoc}
*/
public function getArguments(): array
{
return $this->arguments;
}
/**
* {@inheritDoc}
*/
public function getName(): string
{
return $this->attributeName;
}
/**
* {@inheritDoc}
*/
public function getTarget(): int
{
throw new \RuntimeException(sprintf('cannot get target from %s', $this::class));
}
/**
* {@inheritDoc}
*/
public function newInstance(): object
{
throw new \RuntimeException(sprintf('cannot create new instance from %s', $this::class));
}
}
src/ReflectionClass.php 0000644 00000012757 14641221054 0011137 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Traits\AttributeResolverTrait;
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
use Go\ParserReflection\Traits\ReflectionClassLikeTrait;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\TraitUse;
use ReflectionClass as InternalReflectionClass;
/**
* AST-based reflection class
* @see \Go\ParserReflection\ReflectionClassTest
*/
class ReflectionClass extends InternalReflectionClass
{
use InternalPropertiesEmulationTrait;
use ReflectionClassLikeTrait;
use AttributeResolverTrait;
/**
* Initializes reflection instance
*
* @param object|string $argument Class name or instance of object
* @param ?ClassLike $classLikeNode AST node for class
*/
public function __construct(object|string $argument, ?ClassLike $classLikeNode = null)
{
$fullClassName = is_object($argument) ? get_class($argument) : ltrim($argument, '\\');
$namespaceParts = explode('\\', $fullClassName);
$this->className = array_pop($namespaceParts);
// Let's unset original read-only property to have a control over it via __get
unset($this->name);
$this->namespaceName = implode('\\', $namespaceParts);
$this->classLikeNode = $classLikeNode ?: ReflectionEngine::parseClass($fullClassName);
}
/**
* Parses interfaces from the concrete class node
*
* @return InternalReflectionClass[] List of reflections of interfaces
*/
public static function collectInterfacesFromClassNode(ClassLike $classLikeNode): array
{
$interfaces = [];
$isInterface = $classLikeNode instanceof Interface_;
$interfaceField = $isInterface ? 'extends' : 'implements';
$hasExplicitInterfaces = in_array($interfaceField, $classLikeNode->getSubNodeNames(), true);
$implementsList = $hasExplicitInterfaces ? $classLikeNode->$interfaceField : [];
if (count($implementsList) > 0) {
foreach ($implementsList as $implementNode) {
if ($implementNode instanceof FullyQualified) {
$implementName = $implementNode->toString();
$interface = interface_exists($implementName, false)
? new parent($implementName)
: new static($implementName);
$interfaces[$implementName] = $interface;
}
}
}
// All Enum classes has implicit interface(s) added by PHP
if ($classLikeNode instanceof Enum_) {
// @see https://php.watch/versions/8.1/enums#enum-BackedEnum
$interfacesToAdd = isset($classLikeNode->scalarType)
? [\UnitEnum::class, \BackedEnum::class] // PHP Uses exactly this order, not reversed by parent!
: [\UnitEnum::class];
foreach ($interfacesToAdd as $interfaceToAdd) {
$interfaces[$interfaceToAdd] = new parent($interfaceToAdd);
}
}
return $interfaces;
}
/**
* Parses traits from the concrete class node
*
* @param array $traitAdaptations List of method adaptations
*
* @return InternalReflectionClass[] List of reflections of traits
*/
public static function collectTraitsFromClassNode(ClassLike $classLikeNode, array &$traitAdaptations): array
{
$traits = [];
if (!empty($classLikeNode->stmts)) {
foreach ($classLikeNode->stmts as $classLevelNode) {
if ($classLevelNode instanceof TraitUse) {
foreach ($classLevelNode->traits as $classTraitName) {
if ($classTraitName instanceof FullyQualified) {
$traitName = $classTraitName->toString();
$trait = trait_exists($traitName, false)
? new parent($traitName)
: new static($traitName);
$traits[$traitName] = $trait;
}
}
$traitAdaptations = $classLevelNode->adaptations;
}
}
}
return $traits;
}
/**
* Emulating original behaviour of reflection
*/
public function __debugInfo(): array
{
return [
'name' => $this->getName()
];
}
/**
* Returns an AST-node for class
*/
public function getNode(): ?ClassLike
{
return $this->classLikeNode;
}
/**
* Implementation of internal reflection initialization
*/
protected function __initialize(): void
{
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 InternalReflectionClass The appropriate reflection object.
*/
protected function createReflectionForClass(string $className): InternalReflectionClass
{
return class_exists($className, false) ? new parent($className) : new static($className);
}
}
src/ReflectionClassConstant.php 0000644 00000020014 14641221054 0012632 0 ustar 00
*
* 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\Resolver\TypeExpressionResolver;
use Go\ParserReflection\Traits\AttributeResolverTrait;
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use PhpParser\Node;
use PhpParser\Node\Const_;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\EnumCase;
use Reflection;
use ReflectionClassConstant as BaseReflectionClassConstant;
/**
* @see \Go\ParserReflection\ReflectionClassConstantTest
*/
class ReflectionClassConstant extends BaseReflectionClassConstant
{
use InternalPropertiesEmulationTrait;
use AttributeResolverTrait;
/**
* Concrete class constant node
*/
private ClassConst|EnumCase $classConstOrEnumCaseNode;
private Const_|EnumCase $constOrEnumCaseNode;
private string $className;
private mixed $value = null;
private \ReflectionUnionType|\ReflectionNamedType|\ReflectionIntersectionType|null $type = null;
/**
* Parses class constants from the concrete class node
*
* @return ReflectionClassConstant[]
*/
public static function collectFromClassNode(ClassLike $classLikeNode, string $reflectionClassFQN): array
{
$classConstants = [];
foreach ($classLikeNode->stmts as $classLevelNode) {
if ($classLevelNode instanceof ClassConst) {
foreach ($classLevelNode->consts as $const) {
$classConstName = $const->name->toString();
$classConstants[$classConstName] = new static(
$reflectionClassFQN,
$classConstName,
$classLevelNode,
$const
);
}
}
// Enum cases are reported as constants too
if ($classLevelNode instanceof Node\Stmt\EnumCase) {
$enumCaseName = $classLevelNode->name->toString();
$classConstants[$enumCaseName] = new static (
$reflectionClassFQN,
$enumCaseName,
$classLevelNode,
$classLevelNode
);
}
}
return $classConstants;
}
/**
* Initializes a reflection for the class constant
*/
public function __construct(
string $className,
string $classConstantName,
ClassConst|EnumCase|null $classConstNode = null,
Const_|EnumCase|null $constNode = null
) {
$this->className = ltrim($className, '\\');
if (!$classConstNode) {
[$classConstNode, $constNode] = ReflectionEngine::parseClassConstant($className, $classConstantName);
}
// Let's unset original read-only property to have a control over it via __get
unset($this->name, $this->class);
$this->classConstOrEnumCaseNode = $classConstNode;
$this->constOrEnumCaseNode = $constNode;
$expressionSolver = new NodeExpressionResolver($this->getDeclaringClass());
// We can statically resolve value only fot ClassConst, as for EnumCase we need to have object itself as default
if ($classConstNode instanceof ClassConst) {
$expressionSolver->process($this->constOrEnumCaseNode->value);
$this->value = $expressionSolver->getValue();
}
if ($this->hasType()) {
// If we have null value, this handled internally as nullable type too
$hasDefaultNull = $this->getValue() === null;
$typeResolver = new TypeExpressionResolver($this->getDeclaringClass());
$typeResolver->process($this->classConstOrEnumCaseNode->type, $hasDefaultNull);
$this->type = $typeResolver->getType();
}
}
/**
* Emulating original behaviour of reflection
*/
public function __debugInfo(): array
{
return [
'name' => $this->getName(),
'class' => $this->className
];
}
/**
* @inheritDoc
*/
public function getDeclaringClass(): \ReflectionClass
{
return new ReflectionClass($this->className);
}
/**
* @inheritDoc
*/
public function getDocComment(): string|false
{
$docBlock = $this->classConstOrEnumCaseNode->getDocComment();
return $docBlock ? $docBlock->getText() : false;
}
/**
* @inheritDoc
*/
public function getModifiers(): int
{
$modifiers = 0;
if ($this->isPublic()) {
$modifiers += BaseReflectionClassConstant::IS_PUBLIC;
}
if ($this->isProtected()) {
$modifiers += BaseReflectionClassConstant::IS_PROTECTED;
}
if ($this->isPrivate()) {
$modifiers += BaseReflectionClassConstant::IS_PRIVATE;
}
if ($this->isFinal()) {
$modifiers += BaseReflectionClassConstant::IS_FINAL;
}
return $modifiers;
}
/**
* @inheritDoc
*/
public function getName(): string
{
return $this->constOrEnumCaseNode->name->toString();
}
/**
* @inheritDoc
*/
public function getValue(): mixed
{
return $this->value;
}
/**
* @inheritDoc
*/
public function isEnumCase(): bool
{
return $this->classConstOrEnumCaseNode instanceof EnumCase;
}
/**
* @inheritDoc
*/
public function isPrivate(): bool
{
return $this->classConstOrEnumCaseNode instanceof ClassConst && $this->classConstOrEnumCaseNode->isPrivate();
}
/**
* @inheritDoc
*/
public function isProtected(): bool
{
return $this->classConstOrEnumCaseNode instanceof ClassConst && $this->classConstOrEnumCaseNode->isProtected();
}
/**
* @inheritDoc
*/
public function isPublic(): bool
{
$isPublicClassConst = $this->classConstOrEnumCaseNode instanceof ClassConst && $this->classConstOrEnumCaseNode->isPublic();
$isEnumCase = $this->classConstOrEnumCaseNode instanceof EnumCase;
return $isPublicClassConst || $isEnumCase;
}
/**
* @inheritDoc
*/
public function isFinal(): bool
{
return $this->classConstOrEnumCaseNode instanceof ClassConst && $this->classConstOrEnumCaseNode->isFinal();
}
/**
* @inheritDoc
*/
public function hasType(): bool
{
return $this->classConstOrEnumCaseNode instanceof ClassConst && isset($this->classConstOrEnumCaseNode->type);
}
public function getType(): ?\ReflectionType
{
return $this->type;
}
/**
* @inheritDoc
*/
public function __toString(): string
{
# Starting from PHP7.3 gettype returns different names, need to remap them
static $typeMap = [
'integer' => 'int',
'boolean' => 'bool',
'double' => 'float',
];
$value = $this->isEnumCase() ? 'Object' : $this->getValue();
if (!$this->hasType()) {
$type = gettype($value);
if (isset($typeMap[$type])) {
$type = $typeMap[$type];
}
$type = strtolower($type);
if ($this->isEnumCase()) {
$type = $this->className;
}
$valueType = new ReflectionType($type, false);
} else {
$valueType = $this->type;
}
return sprintf(
"Constant [ %s %s %s ] { %s }\n",
implode(' ', Reflection::getModifierNames($this->getModifiers())),
ReflectionType::convertToDisplayType($valueType),
$this->getName(),
is_object($value) ? 'Object' : $value
);
}
public function getNode(): ClassConst|EnumCase
{
return $this->classConstOrEnumCaseNode;
}
}
src/ReflectionEngine.php 0000644 00000021014 14641221054 0011261 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Instrument\PathResolver;
use Go\ParserReflection\NodeVisitor\RootNamespaceNormalizer;
use InvalidArgumentException;
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_;
use PhpParser\Node\Stmt\Property;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser;
use PhpParser\ParserFactory;
/**
* AST-based reflection engine, powered by PHP-Parser
*/
class ReflectionEngine
{
protected static ?LocatorInterface $locator;
/**
* @var Node[][]
*/
protected static array $parsedFiles = [];
protected static ?int $maximumCachedFiles;
protected static Parser $parser;
protected static NodeTraverser $traverser;
private function __construct() {}
public static function init(LocatorInterface $locator): void
{
self::$parser = (new ParserFactory())->createForHostVersion();
self::$traverser = $traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver());
$traverser->addVisitor(new RootNamespaceNormalizer());
self::$locator = $locator;
}
/**
* Limits number of files, that can be cached at any given moment
*/
public static function setMaximumCachedFiles(int $newLimit): void
{
self::$maximumCachedFiles = $newLimit;
if (count(self::$parsedFiles) > $newLimit) {
self::$parsedFiles = array_slice(self::$parsedFiles, 0, $newLimit);
}
}
/**
* Locates a file name for class
*/
public static function locateClassFile(string $fullClassName): string
{
if (class_exists($fullClassName, false)
|| interface_exists($fullClassName, false)
|| trait_exists($fullClassName, false)
) {
$refClass = new \ReflectionClass($fullClassName);
$classFileName = $refClass->getFileName();
} else {
$classFileName = self::$locator->locateClass($fullClassName);
}
if (!$classFileName) {
throw new InvalidArgumentException("Class $fullClassName was not found by locator");
}
return $classFileName;
}
/**
* Tries to parse a class by name using LocatorInterface
*/
public static function parseClass(string $fullClassName): ClassLike
{
$classFileName = self::locateClassFile($fullClassName);
$namespaceParts = explode('\\', $fullClassName);
$className = array_pop($namespaceParts);
$namespaceName = implode('\\', $namespaceParts);
// we have a namespace node somewhere
$namespace = self::parseFileNamespace($classFileName, $namespaceName);
$namespaceNodes = $namespace->stmts;
$namespaceNode = self::findClassLikeNodeByClassName($namespaceNodes, $className);
if ($namespaceNode instanceof ClassLike) {
$namespaceNode->setAttribute('fileName', $classFileName);
return $namespaceNode;
}
throw new InvalidArgumentException("Class $fullClassName was not found in the $classFileName");
}
/**
* Loop through an array and find a ClassLike statement by the given class name.
*
* If an if statement like `if (false) {` is found, the class will also be search inside that if statement.
* This relies on the guide of greg0ire on how to deprecate a type.
*
* @see https://dev.to/greg0ire/how-to-deprecate-a-type-in-php-48cf
*/
protected static function findClassLikeNodeByClassName(array $nodes, string $className): ?ClassLike
{
foreach ($nodes as $node) {
if ($node instanceof ClassLike && $node->name->toString() == $className) {
return $node;
}
if ($node instanceof Node\Stmt\If_
&& $node->cond instanceof Node\Expr\ConstFetch
&& isset($node->cond->name->parts[0])
&& $node->cond->name->parts[0] === 'false'
) {
$result = self::findClassLikeNodeByClassName($node->stmts, $className);
if ($result instanceof ClassLike) {
return $result;
}
}
}
return null;
}
/**
* Parses class method
*/
public static function parseClassMethod(string $fullClassName, string $methodName): ClassMethod
{
$class = self::parseClass($fullClassName);
$classNodes = $class->stmts;
foreach ($classNodes as $classLevelNode) {
if ($classLevelNode instanceof ClassMethod && $classLevelNode->name->toString() === $methodName) {
return $classLevelNode;
}
}
throw new InvalidArgumentException("Method $methodName was not found in the $fullClassName");
}
/**
* Parses class property
*
* @return array Pair of [Property and PropertyItem] nodes
*/
public static function parseClassProperty(string $fullClassName, string $propertyName): array
{
$class = self::parseClass($fullClassName);
$classNodes = $class->stmts;
foreach ($classNodes as $classLevelNode) {
if ($classLevelNode instanceof Property) {
foreach ($classLevelNode->props as $classProperty) {
if ($classProperty->name->toString() === $propertyName) {
return [$classLevelNode, $classProperty];
}
}
}
}
throw new InvalidArgumentException("Property $propertyName was not found in the $fullClassName");
}
/**
* Parses class constants
*
* @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
*
* @param string|null $fileContent Optional content of the file
*
* @return Node[]
*/
public static function parseFile(string $fileName, ?string $fileContent = null): array
{
$fileName = PathResolver::realpath($fileName);
if (isset(self::$parsedFiles[$fileName]) && !isset($fileContent)) {
return self::$parsedFiles[$fileName];
}
if (isset(self::$maximumCachedFiles) && (count(self::$parsedFiles) === self::$maximumCachedFiles)) {
array_shift(self::$parsedFiles);
}
if (!isset($fileContent)) {
$fileContent = file_get_contents($fileName);
}
$treeNodes = self::$parser->parse($fileContent);
$treeNodes = self::$traverser->traverse($treeNodes);
self::$parsedFiles[$fileName] = $treeNodes;
return $treeNodes;
}
/**
* Parses a file namespace and returns an AST for it
*
* @throws ReflectionException
*/
public static function parseFileNamespace(string $fileName, string $namespaceName): Namespace_
{
$topLevelNodes = self::parseFile($fileName);
// namespaces can be only top-level nodes, so we can scan them directly
foreach ($topLevelNodes as $topLevelNode) {
if (!$topLevelNode instanceof Namespace_) {
continue;
}
$topLevelNodeName = $topLevelNode->name ? $topLevelNode->name->toString() : '';
if (ltrim($topLevelNodeName, '\\') === trim($namespaceName, '\\')) {
return $topLevelNode;
}
}
throw new ReflectionException("Namespace $namespaceName was not found in the file $fileName");
}
public static function getParser(): Parser
{
return self::$parser;
}
}
src/ReflectionException.php 0000644 00000000664 14641221054 0012022 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use ReflectionException as BaseException;
/**
* General parser-level reflection exception
*/
class ReflectionException extends BaseException
{
}
src/ReflectionFile.php 0000644 00000010120 14641221054 0010727 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Instrument\PathResolver;
use PhpParser\Node;
use PhpParser\Node\Stmt\Namespace_;
/**
* AST-based reflector for the source file
* @see \Go\ParserReflection\ReflectionFileTest
*/
class ReflectionFile
{
/**
* Name of the file for reflection
*/
protected string $fileName;
/**
* List of namespaces in the file
*
* @var ReflectionFileNamespace[]
*/
protected array $fileNamespaces;
/**
* Top-level nodes for the file
*
* @var Node[]
*/
private array $topLevelNodes;
/**
* ReflectionFile constructor.
*
* @param null|Node[] $topLevelNodes Optional corresponding list of AST nodes for that file
*/
public function __construct(string $fileName, ?array $topLevelNodes = null)
{
$fileName = PathResolver::realpath($fileName);
$this->fileName = $fileName;
$this->topLevelNodes = $topLevelNodes ?: ReflectionEngine::parseFile($fileName);
}
/**
* Returns a namespace from the file or false if no such a namespace
*
* @throws ReflectionException If namespace doesn't exists in the file
*/
public function getFileNamespace(string $namespaceName): ReflectionFileNamespace
{
if ($this->hasFileNamespace($namespaceName)) {
return $this->fileNamespaces[$namespaceName];
}
throw new ReflectionException("Could not find the namespace " . $namespaceName . " in the file " . $this->fileName);
}
/**
* Gets the list of namespaces in the file
*
* @return ReflectionFileNamespace[]
*/
public function getFileNamespaces(): array
{
if (!isset($this->fileNamespaces)) {
$this->fileNamespaces = $this->findFileNamespaces();
}
return $this->fileNamespaces;
}
/**
* Returns the name of current reflected file
*/
public function getName(): string
{
return $this->fileName;
}
/**
* Returns an AST-nodes for file
*
* @return Node[]
*/
public function getNodes(): array
{
return $this->topLevelNodes;
}
/**
* Returns the presence of namespace in the file
*/
public function hasFileNamespace(string $namespaceName): bool
{
$namespaces = $this->getFileNamespaces();
return isset($namespaces[$namespaceName]);
}
/**
* Checks if the current file is in strict mode
*/
public function isStrictMode(): bool
{
// declare statement for the strict_types can be only top-level node
$topLevelNode = reset($this->topLevelNodes);
if (!$topLevelNode instanceof Node\Stmt\Declare_) {
return false;
}
$declareStatement = reset($topLevelNode->declares);
$isStrictTypeKey = $declareStatement->key->toString() === 'strict_types';
$isScalarValue = $declareStatement->value instanceof Node\Scalar\Int_;
$isStrictMode = $isStrictTypeKey && $isScalarValue && $declareStatement->value->value === 1;
return $isStrictMode;
}
/**
* Searches for file namespaces in the given AST
*
* @return ReflectionFileNamespace[]
*/
private function findFileNamespaces(): array
{
$namespaces = [];
// namespaces can be only top-level nodes, so we can scan them directly
foreach ($this->topLevelNodes as $topLevelNode) {
if ($topLevelNode instanceof Namespace_) {
$namespaceName = $topLevelNode->name ? $topLevelNode->name->toString() : '';
$namespaces[$namespaceName] = new ReflectionFileNamespace(
$this->fileName,
$namespaceName,
$topLevelNode
);
}
}
return $namespaces;
}
}
src/ReflectionFileNamespace.php 0000644 00000030315 14641221054 0012554 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Instrument\PathResolver;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Const_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Use_;
/**
* AST-based reflection for the concrete namespace in the file
* @see \Go\ParserReflection\ReflectionFileNamespaceTest
*/
class ReflectionFileNamespace
{
/**
* List of classes in the namespace
*
* @var ReflectionClass[]
*/
protected array $fileClasses;
/**
* List of functions in the namespace
*
* @var ReflectionFunction[]
*/
protected array $fileFunctions;
/**
* Map of constants in the namespace
*/
protected array $fileConstants;
/**
* Map of constants in the namespace including defined via "define(...)"
*/
protected array $fileConstantsWithDefined;
/**
* List of imported namespaces (aliases)
*/
protected array $fileNamespaceAliases;
/**
* Namespace node
*/
private Namespace_ $namespaceNode;
/**
* Name of the file
*/
private string $fileName;
/**
* File namespace constructor
*
* @param Namespace_|null $namespaceNode Optional AST-node for this namespace block
*/
public function __construct(string $fileName, string $namespaceName, ?Namespace_ $namespaceNode = null)
{
$fileName = PathResolver::realpath($fileName);
if (!isset($namespaceNode)) {
$namespaceNode = ReflectionEngine::parseFileNamespace($fileName, $namespaceName);
}
$this->namespaceNode = $namespaceNode;
$this->fileName = $fileName;
}
/**
* Returns the concrete class from the file namespace
*
* @throws ReflectionException if there is no such class
*/
public function getClass(string $className): ReflectionClass
{
if (!$this->hasClass($className)) {
throw new ReflectionException("Could not find the class " . $className . " in the file " . $this->fileName);
}
return $this->fileClasses[$className];
}
/**
* Gets list of classes in the namespace
*
* @return ReflectionClass[]
*/
public function getClasses(): array
{
if (!isset($this->fileClasses)) {
$this->fileClasses = $this->findClasses();
}
return $this->fileClasses;
}
/**
* Returns a value for the given constant
*
* @throws ReflectionException if there is no such constant in the file's namespace
*/
public function getConstant(string $constantName): mixed
{
if (!$this->hasConstant($constantName)) {
throw new ReflectionException("Could not find the constant " . $constantName . " in the file " . $this->fileName);
}
return $this->fileConstants[$constantName];
}
/**
* Returns a list of defined constants in the namespace
*
* @param bool $withDefined Include constants defined via "define(...)" in results.
*/
public function getConstants(bool $withDefined = false): array
{
if ($withDefined) {
if (!isset($this->fileConstantsWithDefined)) {
$this->fileConstantsWithDefined = $this->findConstants(true);
}
return $this->fileConstantsWithDefined;
}
if (!isset($this->fileConstants)) {
$this->fileConstants = $this->findConstants();
}
return $this->fileConstants;
}
/**
* Gets doc comments from a namespace node if it exists, otherwise "false"
*/
public function getDocComment(): string|false
{
$docComment = false;
$comments = $this->namespaceNode->getAttribute('comments');
if ($comments) {
$docComment = (string)$comments[0];
}
return $docComment;
}
/**
* Gets starting line number or false if information is not available
*/
public function getEndLine(): int|false
{
if ($this->namespaceNode->hasAttribute('endLine')) {
return $this->namespaceNode->getAttribute('endLine');
}
return false;
}
/**
* Returns the name of file
*/
public function getFileName(): string
{
return $this->fileName;
}
/**
* Returns the concrete function from the file namespace
*
* @throws ReflectionException if there is no function
*/
public function getFunction(string $functionName): ReflectionFunction
{
if (!$this->hasFunction($functionName)) {
throw new ReflectionException("Could not find the function " . $functionName . " in the file " . $this->fileName);
}
return $this->fileFunctions[$functionName];
}
/**
* Gets list of functions in the namespace
*
* @return ReflectionFunction[]
*/
public function getFunctions(): array
{
if (!isset($this->fileFunctions)) {
$this->fileFunctions = $this->findFunctions();
}
return $this->fileFunctions;
}
/**
* Gets namespace name
*/
public function getName(): string
{
$nameNode = $this->namespaceNode->name;
return $nameNode ? $nameNode->toString() : '';
}
/**
* Returns a list of namespace aliases
*/
public function getNamespaceAliases(): array
{
if (!isset($this->fileNamespaceAliases)) {
$this->fileNamespaceAliases = $this->findNamespaceAliases();
}
return $this->fileNamespaceAliases;
}
/**
* Returns an AST-node for namespace
*/
public function getNode(): Namespace_
{
return $this->namespaceNode;
}
/**
* Helper method to access last token position for namespace
*
* This method is useful because namespace can be declared with braces or without them
*/
public function getLastTokenPosition(): int
{
$endNamespaceTokenPosition = $this->namespaceNode->getAttribute('endTokenPos');
/** @var Node $lastNamespaceNode */
$lastNamespaceNode = end($this->namespaceNode->stmts);
$endStatementTokenPosition = $lastNamespaceNode->getAttribute('endTokenPos');
return max($endNamespaceTokenPosition, $endStatementTokenPosition);
}
/**
* Gets starting line number or false if information is not available
*/
public function getStartLine(): int|false
{
if ($this->namespaceNode->hasAttribute('startLine')) {
return $this->namespaceNode->getAttribute('startLine');
}
return false;
}
/**
* Checks if the given class is present in this file namespace
*/
public function hasClass(string $className): bool
{
$classes = $this->getClasses();
return isset($classes[$className]);
}
/**
* Checks if the given constant is present in this file namespace
*/
public function hasConstant(string $constantName): bool
{
$constants = $this->getConstants();
return isset($constants[$constantName]);
}
/**
* Checks if the given function is present in this file namespace
*/
public function hasFunction(string $functionName): bool
{
$functions = $this->getFunctions();
return isset($functions[$functionName]);
}
/**
* Searches for classes in the given AST
*
* @return ReflectionClass[]
*/
private function findClasses(): array
{
$classes = [];
$namespaceName = $this->getName();
// classes can be only top-level nodes in the namespace, so we can scan them directly
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) {
if ($namespaceLevelNode instanceof ClassLike) {
$classShortName = $namespaceLevelNode->name->toString();
$className = $namespaceName ? $namespaceName .'\\' . $classShortName : $classShortName;
$namespaceLevelNode->setAttribute('fileName', $this->fileName);
$classes[$className] = new ReflectionClass($className, $namespaceLevelNode);
}
}
return $classes;
}
/**
* Searches for functions in the given AST
*
* @return ReflectionFunction[]
*/
private function findFunctions(): array
{
$functions = [];
$namespaceName = $this->getName();
// functions can be only top-level nodes in the namespace, so we can scan them directly
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) {
if ($namespaceLevelNode instanceof Function_) {
$funcShortName = $namespaceLevelNode->name->toString();
$functionName = $namespaceName ? $namespaceName .'\\' . $funcShortName : $funcShortName;
$namespaceLevelNode->setAttribute('fileName', $this->fileName);
$functions[$funcShortName] = new ReflectionFunction($functionName, $namespaceLevelNode);
}
}
return $functions;
}
/**
* Searches for constants in the given AST
*
* @param bool $withDefined Include constants defined via "define(...)" in results.
*/
private function findConstants(bool $withDefined = false): array
{
$constants = [];
$expressionSolver = new NodeExpressionResolver($this);
// constants can be only top-level nodes in the namespace, so we can scan them directly
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) {
if ($namespaceLevelNode instanceof Const_) {
$nodeConstants = $namespaceLevelNode->consts;
if (!empty($nodeConstants)) {
foreach ($nodeConstants as $nodeConstant) {
$expressionSolver->process($nodeConstant->value);
$constants[$nodeConstant->name->toString()] = $expressionSolver->getValue();
}
}
}
}
if ($withDefined) {
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) {
if ($namespaceLevelNode instanceof Expression
&& $namespaceLevelNode->expr instanceof FuncCall
&& $namespaceLevelNode->expr->name instanceof Name
&& (string)$namespaceLevelNode->expr->name === 'define'
) {
try {
$functionCallNode = $namespaceLevelNode->expr;
$expressionSolver->process($functionCallNode->args[0]->value);
$constantName = $expressionSolver->getValue();
$expressionSolver->process($functionCallNode->args[1]->value);
$constantValue = $expressionSolver->getValue();
$constants[$constantName] = $constantValue;
} catch (\Throwable) {
// Ignore all possible errors during evaluation of runtime constants defined in the code
}
}
}
}
return $constants;
}
/**
* Searches for namespace aliases for the current block
*/
private function findNamespaceAliases(): array
{
$namespaceAliases = [];
// aliases can be only top-level nodes in the namespace, so we can scan them directly
foreach ($this->namespaceNode->stmts as $namespaceLevelNode) {
if ($namespaceLevelNode instanceof Use_) {
$useAliases = $namespaceLevelNode->uses;
if (!empty($useAliases)) {
foreach ($useAliases as $useNode) {
$namespaceAliases[$useNode->name->toString()] = $useNode->getAlias()->toString();
}
}
}
}
return $namespaceAliases;
}
}
src/ReflectionFunction.php 0000644 00000007321 14641221054 0011646 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Closure;
use Go\ParserReflection\Traits\AttributeResolverTrait;
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
use Go\ParserReflection\Traits\ReflectionFunctionLikeTrait;
use JetBrains\PhpStorm\Deprecated;
use PhpParser\Node\Stmt\Function_;
use ReflectionFunction as BaseReflectionFunction;
/**
* AST-based reflection for function
* @see \Go\ParserReflection\ReflectionFunctionTest
*/
class ReflectionFunction extends BaseReflectionFunction
{
use InternalPropertiesEmulationTrait;
use ReflectionFunctionLikeTrait;
use AttributeResolverTrait;
/**
* Initializes reflection instance for given AST-node
*
* @param string $functionName The name of the function to reflect.
* @param Function_ $functionNode Function node AST
*/
public function __construct(string $functionName, Function_ $functionNode)
{
$namespaceParts = explode('\\', $functionName);
// Remove the last one part with function name
array_pop($namespaceParts);
$this->namespaceName = implode('\\', $namespaceParts);
$this->functionLikeNode = $functionNode;
unset($this->name);
}
/**
* Emulating original behaviour of reflection
*/
public function __debugInfo(): array
{
$nodeName = 'unknown';
if ($this->functionLikeNode instanceof Function_) {
$nodeName = $this->functionLikeNode->name->toString();
}
return ['name' => $nodeName];
}
/**
* Returns an AST-node for function
*/
public function getNode(): Function_
{
return $this->functionLikeNode;
}
/**
* {@inheritDoc}
*/
public function getClosure(): \Closure
{
$this->initializeInternalReflection();
return parent::getClosure();
}
/**
* {@inheritDoc}
*/
public function invoke(mixed ...$args): mixed
{
$this->initializeInternalReflection();
return parent::invoke(...$args);
}
/**
* {@inheritDoc}
*/
public function invokeArgs(array $args): mixed
{
$this->initializeInternalReflection();
return parent::invokeArgs($args);
}
/**
* Checks if function is disabled
*
* Only internal functions can be disabled using disable_functions directive.
* User-defined functions are unaffected.
*/
#[Deprecated('ReflectionFunction::isDisabled() is deprecated', since: "8.0")]
public function isDisabled(): bool
{
return false;
}
/**
* Returns textual representation of function
*/
public function __toString(): string
{
$paramFormat = ($this->getNumberOfParameters() > 0) ? "\n\n - Parameters [%d] {%s\n }" : '';
$reflectionFormat = "%sFunction [ function %s ] {\n @@ %s %d - %d{$paramFormat}\n}\n";
return sprintf(
$reflectionFormat,
$this->getDocComment() ? $this->getDocComment() . "\n" : '',
$this->getName(),
$this->getFileName(),
$this->getStartLine(),
$this->getEndLine(),
count($this->getParameters()),
array_reduce($this->getParameters(), static fn($str, ReflectionParameter $param) => $str . "\n " . $param, '')
);
}
/**
* Implementation of internal reflection initialization
*/
protected function __initialize(): void
{
parent::__construct($this->getName());
}
}
src/ReflectionIntersectionType.php 0000644 00000003660 14641221054 0013373 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use ReflectionIntersectionType as BaseReflectionIntersectionType;
/**
* ReflectionIntersectionType implementation
*/
class ReflectionIntersectionType extends BaseReflectionIntersectionType
{
/**
* @var ReflectionNamedType[]
*/
private readonly array $types;
/**
* Initializes reflection data
*/
public function __construct(ReflectionNamedType ...$types)
{
$this->types = $types;
}
/**
* @inheritDoc
*/
public function allowsNull(): bool
{
// @see https://php.watch/versions/8.1/intersection-types
// Intersection Types only allow pure intersection types; composite types with nullable or Union Types are not
// allowed, and results in a syntax error.
//
// Intersection Types only support class and interface names as intersection members.
// Scalar types, array, void, mixed, callable, never, iterable, null, and other types are not allowed.
return false;
}
/**
* @inheritDoc
*/
public function __toString(): string
{
$stringTypes = array_map(fn(\ReflectionNamedType $namedType) => (string) $namedType, $this->types);
// Special iterable type is already union Traversable|array, thus should be replaced
$iterableIndex = array_search('iterable', $stringTypes, true);
if ($iterableIndex !== false) {
unset($stringTypes[$iterableIndex]);
array_push($stringTypes, 'Traversable', 'array');
}
return join('&', $stringTypes);
}
/**
* @inheritDoc
*/
public function getTypes(): array
{
return $this->types;
}
}
src/ReflectionMethod.php 0000644 00000031701 14641221054 0011300 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Traits\AttributeResolverTrait;
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
use Go\ParserReflection\Traits\ReflectionFunctionLikeTrait;
use JetBrains\PhpStorm\Deprecated;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\UnionType;
use Reflection;
use ReflectionMethod as BaseReflectionMethod;
/**
* AST-based reflection for the method in a class
* @see \Go\ParserReflection\ReflectionMethodTest
*/
class ReflectionMethod extends BaseReflectionMethod
{
use InternalPropertiesEmulationTrait;
use ReflectionFunctionLikeTrait;
use AttributeResolverTrait;
/**
* Name of the class
*/
private string $className;
/**
* Optional declaring class reference
*/
private ?ReflectionClass $declaringClass;
/**
* Initializes reflection instance for the method node
*
* @param ?ClassMethod $classMethodNode AST-node for method
* @param ?ReflectionClass $declaringClass Optional declaring class
*/
public function __construct(
string $className,
string $methodName,
?ClassMethod $classMethodNode = null,
?ReflectionClass $declaringClass = null
) {
//for some reason, ReflectionMethod->getNamespaceName in php always returns '', so we shouldn't use it too
$this->className = ltrim($className, '\\');
$this->declaringClass = $declaringClass;
$this->functionLikeNode = $classMethodNode ?: ReflectionEngine::parseClassMethod($className, $methodName);
// Let's unset original read-only properties to have a control over them via __get
unset($this->name, $this->class);
}
/**
* Returns an AST-node for method
*/
public function getNode(): ClassMethod
{
return $this->functionLikeNode;
}
/**
* Emulating original behaviour of reflection
*/
public function __debugInfo(): array
{
return [
'name' => $this->getClassMethodNode()->name->toString(),
'class' => $this->className
];
}
/**
* Returns the string representation of the Reflection method object.
*
* @link http://php.net/manual/en/reflectionmethod.tostring.php
*/
public function __toString(): string
{
// Internally $this->getReturnType() !== null is the same as $this->hasReturnType()
$returnType = $this->getReturnType();
$hasReturnType = $returnType !== null;
$paramsNeeded = $hasReturnType || $this->getNumberOfParameters() > 0;
$paramFormat = $paramsNeeded ? "\n\n - Parameters [%d] {%s\n }" : '';
$returnFormat = $hasReturnType ? "\n - Return [ %s ]" : '';
$methodParameters = $this->getParameters();
$protoString = '';
if ($this->hasPrototype()) {
$prototype = $this->getPrototype();
$prototypeClass = $prototype->getDeclaringClass()->name;
$parentClass = $this->getDeclaringClass()->getParentClass();
// If we have the same method in parent, then we override it as well, otherwise it is prototype
$overrideProto = $parentClass && $parentClass->hasMethod($this->getName());
if ($overrideProto) {
$protoString .= ", overwrites {$prototypeClass}";
}
$protoString .= ", prototype {$prototypeClass}";
}
$fileString = '';
if ($this->getFileName()) {
$fileString .= "\n @@ " . $this->getFileName();
$fileString .= ' ' . $this->getStartLine();
$fileString .= ' - ' . $this->getEndLine();
}
$paramString = '';
$indentation = str_repeat(' ', 4);
foreach ($methodParameters as $methodParameter) {
$paramString .= "\n{$indentation}" . $methodParameter;
}
return sprintf(
"%sMethod [ <%s%s%s>%s%s%s %s method %s ] {%s{$paramFormat}{$returnFormat}\n}\n",
$this->getDocComment() ? $this->getDocComment() . "\n" : '',
$this->isInternal() ? 'internal' : 'user',
$protoString,
$this->isConstructor() ? ', ctor' : '',
$this->isFinal() ? ' final' : '',
$this->isStatic() ? ' static' : '',
$this->isAbstract() ? ' abstract' : '',
implode(
' ',
Reflection::getModifierNames(
$this->getModifiers() & (self::IS_PUBLIC | self::IS_PROTECTED | self::IS_PRIVATE)
)
),
$this->getName(),
$fileString,
count($methodParameters),
$paramString,
$returnType ? ReflectionType::convertToDisplayType($returnType) : ''
);
}
/**
* {@inheritDoc}
*/
public function getClosure($object = null): \Closure
{
$this->initializeInternalReflection();
return parent::getClosure($object);
}
/**
* {@inheritDoc}
*/
public function getDeclaringClass(): \ReflectionClass
{
return $this->declaringClass ?? new ReflectionClass($this->className);
}
/**
* {@inheritDoc}
*/
public function getModifiers(): int
{
$modifiers = 0;
if ($this->isPublic()) {
$modifiers += self::IS_PUBLIC;
}
if ($this->isProtected()) {
$modifiers += self::IS_PROTECTED;
}
if ($this->isPrivate()) {
$modifiers += self::IS_PRIVATE;
}
if ($this->isAbstract()) {
$modifiers += self::IS_ABSTRACT;
}
if ($this->isFinal()) {
$modifiers += self::IS_FINAL;
}
if ($this->isStatic()) {
$modifiers += self::IS_STATIC;
}
return $modifiers;
}
/**
* {@inheritDoc}
*/
public function getPrototype(): \ReflectionMethod
{
$allKnownParents = [];
$parent = $this->getDeclaringClass()->getParentClass();
if ($parent instanceof \ReflectionClass) {
$allKnownParents[] = $parent;
}
$allKnownParents = array_merge($allKnownParents, $this->getDeclaringClass()->getInterfaces());
$methodName = $this->getName();
foreach ($allKnownParents as $knownParent) {
if ($knownParent->hasMethod($methodName)) {
return $knownParent->getMethod($methodName);
}
}
throw new ReflectionException("Method " . $this->getDeclaringClass()->getName() . "::" . $methodName . "() does not have prototype");
}
public function hasPrototype(): bool
{
$allKnownParents = [];
$parent = $this->getDeclaringClass()->getParentClass();
if ($parent instanceof \ReflectionClass) {
$allKnownParents[] = $parent;
}
$allKnownParents = array_merge($allKnownParents, $this->getDeclaringClass()->getInterfaces());
foreach ($allKnownParents as $knownParent) {
if ($knownParent->hasMethod($this->getName())) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
public function invoke(?object $object, mixed ...$args): mixed
{
$this->initializeInternalReflection();
return parent::invoke($object, ...$args);
}
/**
* {@inheritDoc}
*/
public function invokeArgs(?object $object, array $args): mixed
{
$this->initializeInternalReflection();
return parent::invokeArgs($object, $args);
}
/**
* {@inheritDoc}
*/
public function isAbstract(): bool
{
return $this->getDeclaringClass()->isInterface() || $this->getClassMethodNode()->isAbstract();
}
/**
* {@inheritDoc}
*/
public function isConstructor(): bool
{
return $this->getClassMethodNode()->name->toLowerString() === '__construct';
}
/**
* {@inheritDoc}
*/
public function isDestructor(): bool
{
return $this->getClassMethodNode()->name->toLowerString() === '__destruct';
}
/**
* {@inheritDoc}
*/
public function isFinal(): bool
{
return $this->getClassMethodNode()->isFinal();
}
/**
* {@inheritDoc}
*/
public function isPrivate(): bool
{
return $this->getClassMethodNode()->isPrivate();
}
/**
* {@inheritDoc}
*/
public function isProtected(): bool
{
return $this->getClassMethodNode()->isProtected();
}
/**
* {@inheritDoc}
*/
public function isPublic(): bool
{
return $this->getClassMethodNode()->isPublic();
}
/**
* {@inheritDoc}
*/
public function isStatic(): bool
{
return $this->getClassMethodNode()->isStatic();
}
/**
* {@inheritDoc}
*/
#[Deprecated(reason: "Usage of ReflectionMethod::setAccessible() has no effect.", since: "8.1")]
public function setAccessible(bool $accessible): void
{
}
/**
* Parses methods from the concrete class node
*
* @param ClassLike $classLikeNode Class-like node
* @param ReflectionClass $reflectionClass Reflection of the class
*
* @return ReflectionMethod[]
*/
public static function collectFromClassNode(ClassLike $classLikeNode, ReflectionClass $reflectionClass): array
{
$methods = [];
foreach ($classLikeNode->stmts as $classLevelNode) {
if ($classLevelNode instanceof ClassMethod) {
$classLevelNode->setAttribute('fileName', $classLikeNode->getAttribute('fileName'));
$methodName = $classLevelNode->name->toString();
$methods[$methodName] = new ReflectionMethod(
$reflectionClass->name,
$methodName,
$classLevelNode,
$reflectionClass
);
}
}
// Enum has special `cases` (and `from`/`tryFrom` for Backed Enums) methods
if ($classLikeNode instanceof Enum_) {
$methods['cases'] = self::createEnumCasesMethod($reflectionClass);
// Backed enum methods emulation
if (isset($classLikeNode->scalarType)) {
$methods['from'] = self::createEnumFromMethod($reflectionClass);
$methods['tryFrom'] = self::createEnumTryFromMethod($reflectionClass);
}
}
return $methods;
}
/**
* Implementation of internal reflection initialization
*/
protected function __initialize(): void
{
parent::__construct($this->className, $this->getName());
}
/**
* Ad-Hoc constructor of Enum `cases` method, which emulates PHP behaviour
*/
private static function createEnumCasesMethod(ReflectionClass $reflectionClass): ReflectionMethod
{
$casesMethodNode = (new \PhpParser\Builder\Method('cases'))
->makeStatic()
->makePublic()
->setReturnType('array')
->getNode();
return new static(
$reflectionClass->name,
'cases',
$casesMethodNode,
$reflectionClass
);
}
private static function createEnumFromMethod(ReflectionClass $reflectionClass): ReflectionMethod
{
$valueParam = (new \PhpParser\Builder\Param('value'))
->setType(new UnionType([new Identifier('string'), new Identifier('int')]))
->getNode();
$fromMethodNode = (new \PhpParser\Builder\Method('from'))
->makeStatic()
->makePublic()
->addParam($valueParam)
->setReturnType('static')
->getNode();
return new static(
$reflectionClass->name,
'from',
$fromMethodNode,
$reflectionClass
);
}
private static function createEnumTryFromMethod(ReflectionClass $reflectionClass): ReflectionMethod
{
$valueParam = (new \PhpParser\Builder\Param('value'))
->setType(new UnionType([new Identifier('string'), new Identifier('int')]))
->getNode();
$fromMethodNode = (new \PhpParser\Builder\Method('tryFrom'))
->makeStatic()
->makePublic()
->addParam($valueParam)
->setReturnType('?static')
->getNode();
return new static(
$reflectionClass->name,
'tryFrom',
$fromMethodNode,
$reflectionClass
);
}
/**
* Returns ClassMethod node to prevent all possible type checks with instanceof
*/
private function getClassMethodNode(): ClassMethod
{
return $this->functionLikeNode;
}
}
src/ReflectionNamedType.php 0000644 00000002357 14641221054 0011753 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use ReflectionNamedType as BaseReflectionNamedType;
/**
* ReflectionNamedType implementation
*/
class ReflectionNamedType extends BaseReflectionNamedType
{
/**
* Initializes reflection data
*/
public function __construct(
private readonly string $type,
private readonly bool $allowsNull,
private readonly bool $isBuiltin
) {}
/**
* @inheritDoc
*/
public function allowsNull(): bool
{
return $this->allowsNull;
}
/**
* @inheritDoc
*/
public function isBuiltin(): bool
{
return $this->isBuiltin;
}
/**
* @inheritDoc
*/
public function __toString(): string
{
$allowsNull = $this->allowsNull && !in_array($this->type,['null', 'mixed'], true);
return $allowsNull ? '?' . $this->type : $this->type;
}
/**
* @inheritDoc
*/
public function getName(): string
{
return $this->type;
}
}
src/ReflectionParameter.php 0000644 00000025557 14641221054 0012014 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Traits\AttributeResolverTrait;
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use Go\ParserReflection\Resolver\TypeExpressionResolver;
use JetBrains\PhpStorm\Deprecated;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\PrettyPrinter\Standard;
use ReflectionFunctionAbstract;
use ReflectionParameter as BaseReflectionParameter;
/**
* AST-based reflection for method/function parameter
* @see \Go\ParserReflection\ReflectionParameterTest
*/
class ReflectionParameter extends BaseReflectionParameter
{
use InternalPropertiesEmulationTrait;
use AttributeResolverTrait;
/**
* Reflection function or method
*/
private ReflectionFunctionAbstract $declaringFunction;
/**
* Stores the default value for node (if present)
*/
private mixed $defaultValue;
/**
* Whether or not default value is constant
*/
private bool $isDefaultValueConstant = false;
/**
* Name of the constant of default value
*
* @see $isDefaultValueConstant
*/
private ?string $defaultValueConstantName;
/**
* Index of parameter in the list
*/
private int $parameterIndex;
/**
* Concrete parameter node
*/
private Param $parameterNode;
private bool $isDefaultValueConstExpr;
private ?string $defaultValueConstExpr;
private \ReflectionUnionType|\ReflectionNamedType|\ReflectionIntersectionType|null $type = null;
/**
* Initializes a reflection for the property
*/
public function __construct(
string|array $unusedFunctionName,
string $parameterName,
Param $parameterNode,
int $parameterIndex,
ReflectionFunctionAbstract $declaringFunction
) {
// Let's unset original read-only property to have a control over it via __get
unset($this->name);
$this->parameterNode = $parameterNode;
$this->parameterIndex = $parameterIndex;
$this->declaringFunction = $declaringFunction;
if ($declaringFunction instanceof \ReflectionMethod) {
$context = $declaringFunction->getDeclaringClass();
} else {
$context = $declaringFunction;
}
if ($this->isDefaultValueAvailable()) {
$expressionSolver = new NodeExpressionResolver($context);
$expressionSolver->process($this->parameterNode->default);
$this->defaultValue = $expressionSolver->getValue();
$this->isDefaultValueConstant = $expressionSolver->isConstant();
$this->defaultValueConstantName = $expressionSolver->getConstantName();
$this->isDefaultValueConstExpr = $expressionSolver->isConstExpression();
$this->defaultValueConstExpr = $expressionSolver->getConstExpression();
}
if ($this->hasType()) {
// If we have null value, this handled internally as nullable type too
$hasDefaultNull = $this->isDefaultValueAvailable() && $this->getDefaultValue() === null;
$typeResolver = new TypeExpressionResolver($this->getDeclaringClass());
$typeResolver->process($this->parameterNode->type, $hasDefaultNull);
$this->type = $typeResolver->getType();
}
}
/**
* Returns an AST-node for parameter
*/
public function getNode(): Param
{
return $this->parameterNode;
}
/**
* Emulating original behaviour of reflection
*/
public function __debugInfo(): array
{
return [
'name' => (string)$this->parameterNode->var->name,
];
}
/**
* Returns string representation of this parameter.
*/
public function __toString(): string
{
$parameterType = $this->getType();
$isOptional = $this->isOptional();
$hasDefaultValue = $this->isDefaultValueAvailable();
$defaultValue = '';
if ($hasDefaultValue) {
// For constant fetch expressions, PHP renders now expression
if ($this->isDefaultValueConstExpr) {
$defaultValue = $this->defaultValueConstExpr;
} elseif ($this->isDefaultValueConstant){
$defaultValue = $this->defaultValueConstantName;
} else {
$defaultValue = var_export($this->getDefaultValue(), true);
}
}
return sprintf(
'Parameter #%d [ %s %s%s%s$%s%s ]',
$this->parameterIndex,
$isOptional ? '' : '',
$parameterType ? ReflectionType::convertToDisplayType($parameterType) . ' ' : '',
$this->isVariadic() ? '...' : '',
$this->isPassedByReference() ? '&' : '',
$this->getName(),
($isOptional && $hasDefaultValue) ? (' = ' . $defaultValue) : ''
);
}
/**
* {@inheritDoc}
*/
public function allowsNull(): bool
{
// All non-typed parameters allows null by default
if (!$this->hasType()) {
return true;
}
return $this->type->allowsNull();
}
/**
* {@inheritDoc}
*/
public function canBePassedByValue(): bool
{
return !$this->isPassedByReference();
}
/**
* @inheritDoc
*/
#[Deprecated(reason: "Use ReflectionParameter::getType() and the ReflectionType APIs should be used instead.", since: "8.0")]
public function getClass(): ?\ReflectionClass
{
$parameterType = $this->parameterNode->type;
if ($parameterType instanceof Identifier && $parameterType->name === 'iterable') {
// This is how PHP represents iterable pseudo-class
$parameterType = new Name\FullyQualified(\Traversable::class);
}
if ($parameterType instanceof Name) {
if (!$parameterType instanceof Name\FullyQualified) {
$parameterTypeName = $parameterType->toString();
if ('self' === $parameterTypeName) {
return $this->getDeclaringClass();
}
if ('parent' === $parameterTypeName) {
return $this->getDeclaringClass()->getParentClass();
}
throw new ReflectionException("Can not resolve a class name for parameter");
}
$className = $parameterType->toString();
$classOrInterfaceExists = class_exists($className, false) || interface_exists($className, false);
return $classOrInterfaceExists ? new \ReflectionClass($className) : new ReflectionClass($className);
}
return null;
}
/**
* {@inheritDoc}
*/
public function getDeclaringClass(): ?\ReflectionClass
{
if ($this->declaringFunction instanceof \ReflectionMethod) {
return $this->declaringFunction->getDeclaringClass();
}
return null;
}
/**
* {@inheritDoc}
*/
public function getDeclaringFunction(): ReflectionFunctionAbstract
{
return $this->declaringFunction;
}
/**
* {@inheritDoc}
*/
public function getDefaultValue(): mixed
{
if (!$this->isDefaultValueAvailable()) {
throw new ReflectionException('Internal error: Failed to retrieve the default value');
}
return $this->defaultValue;
}
/**
* {@inheritDoc}
*/
public function getDefaultValueConstantName(): null|string
{
if (!$this->isDefaultValueAvailable()) {
throw new ReflectionException('Internal error: Failed to retrieve the default value');
}
return $this->defaultValueConstantName;
}
/**
* @inheritDoc
*/
public function getName(): string
{
return (string)$this->parameterNode->var->name;
}
/**
* {@inheritDoc}
*/
public function getPosition(): int
{
return $this->parameterIndex;
}
/**
* @inheritDoc
*/
public function getType(): \ReflectionNamedType|\ReflectionUnionType|\ReflectionIntersectionType|null
{
return $this->type;
}
/**
* @inheritDoc
*/
public function hasType(): bool
{
return isset($this->parameterNode->type);
}
/**
* @inheritDoc
*/
#[Deprecated(reason: "Use ReflectionParameter::getType() instead.", since: "8.0")]
public function isArray(): bool
{
$type = $this->parameterNode->type;
return ($type instanceof Identifier) && 'array' === $type->name;
}
/**
* @inheritDoc
*/
#[Deprecated(reason: "Use ReflectionParameter::getType() instead.", since: "8.0")]
public function isCallable(): bool
{
$type = $this->parameterNode->type;
return ($type instanceof Identifier) && 'callable' === $type->name;
}
/**
* @inheritDoc
*/
public function isDefaultValueAvailable(): bool
{
return isset($this->parameterNode->default) && $this->allSiblingsAreOptional();
}
/**
* {@inheritDoc}
*/
public function isDefaultValueConstant(): bool
{
return $this->isDefaultValueConstant;
}
/**
* {@inheritDoc}
*/
public function isOptional(): bool
{
return $this->isVariadic() || $this->isDefaultValueAvailable();
}
/**
* @inheritDoc
*/
public function isPassedByReference(): bool
{
return $this->parameterNode->byRef;
}
/**
* @inheritDoc
*/
public function isPromoted(): bool
{
return $this->parameterNode->isPromoted();
}
/**
* @inheritDoc
*/
public function isVariadic(): bool
{
return $this->parameterNode->variadic;
}
/**
* Returns true if all following parameters are optional (either have values or variadic)
*/
private function allSiblingsAreOptional(): bool
{
// start from PHP 8.1, isDefaultValueAvailable() returns false if next parameter is required
// see https://github.com/php/php-src/issues/8090
$parameters = $this->declaringFunction->getNode()->getParams();
for ($nextParamIndex = $this->parameterIndex + 1; $nextParamIndex < count($parameters); ++$nextParamIndex) {
if (!isset($parameters[$nextParamIndex]->default) && !$parameters[$nextParamIndex]->variadic) {
return false;
}
}
return true;
}
}
src/ReflectionProperty.php 0000644 00000033204 14641221054 0011704 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use Go\ParserReflection\Traits\AttributeResolverTrait;
use Go\ParserReflection\Traits\InitializationTrait;
use Go\ParserReflection\Traits\InternalPropertiesEmulationTrait;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use Go\ParserReflection\Resolver\TypeExpressionResolver;
use JetBrains\PhpStorm\Deprecated;
use PhpParser\Node\Identifier;
use PhpParser\Node\Param;
use PhpParser\Node\PropertyItem;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Property;
use Reflection;
use ReflectionProperty as BaseReflectionProperty;
/**
* AST-based reflection for class property
* @see \Go\ParserReflection\ReflectionPropertyTest
*/
class ReflectionProperty extends BaseReflectionProperty
{
use InitializationTrait;
use InternalPropertiesEmulationTrait;
use AttributeResolverTrait;
private Property|Param $propertyOrPromotedParam;
private PropertyItem|Param $propertyItemOrPromotedParam;
/**
* Name of the class
*/
private string $className;
private \ReflectionUnionType|\ReflectionNamedType|\ReflectionIntersectionType|null $type = null;
private mixed $defaultValue = null;
private bool $isDefaultValueConstant = false;
private ?string $defaultValueConstantName;
private bool $isDefaultValueConstExpr = false;
private ?string $defaultValueConstExpr;
/**
* Initializes a reflection for the property
*
* @param Property|Param|null $propertyOrPromotedParam Property type definition node
* @param PropertyItem|Param|null $propertyItemOrPromotedParam Concrete property definition (value, name)
* @throws ReflectionException
*/
public function __construct(
string $className,
string $propertyName,
Property|Param $propertyOrPromotedParam = null,
PropertyItem|Param $propertyItemOrPromotedParam = null
) {
$this->className = ltrim($className, '\\');
if (!$propertyOrPromotedParam || !$propertyItemOrPromotedParam) {
[$propertyOrPromotedParam, $propertyItemOrPromotedParam] = ReflectionEngine::parseClassProperty($className, $propertyName);
}
$this->propertyOrPromotedParam = $propertyOrPromotedParam;
$this->propertyItemOrPromotedParam = $propertyItemOrPromotedParam;
// Both PropertyItem and Param has `default` property
if (isset($this->propertyItemOrPromotedParam->default) && $this->hasDefaultValue()) {
$expressionSolver = new NodeExpressionResolver($this->getDeclaringClass());
$expressionSolver->process($this->propertyItemOrPromotedParam->default);
$this->defaultValue = $expressionSolver->getValue();
$this->isDefaultValueConstant = $expressionSolver->isConstant();
$this->defaultValueConstantName = $expressionSolver->getConstantName();
$this->isDefaultValueConstExpr = $expressionSolver->isConstExpression();
$this->defaultValueConstExpr = $expressionSolver->getConstExpression();
}
if ($this->hasType()) {
// If we have null value, this handled internally as nullable type too
$hasDefaultNull = $this->hasDefaultValue() && $this->getDefaultValue() === null;
$typeResolver = new TypeExpressionResolver($this->getDeclaringClass());
$typeResolver->process($this->propertyOrPromotedParam->type, $hasDefaultNull);
$this->type = $typeResolver->getType();
}
// Let's unset original read-only properties to have a control over them via __get
unset($this->name, $this->class);
}
/**
* Returns an AST-node for property item
*/
public function getNode(): PropertyItem|Param
{
return $this->propertyItemOrPromotedParam;
}
/**
* Returns an AST-node for property type
*/
public function getTypeNode(): Property|Param
{
return $this->propertyOrPromotedParam;
}
/**
* Emulating original behaviour of reflection
*/
public function __debugInfo(): array
{
return [
'name' => $this->getName(),
'class' => $this->className
];
}
/**
* Return string representation of this little old property.
*/
public function __toString(): string
{
$propertyType = $this->getType();
$hasDefaultValue = $this->hasDefaultValue();
$defaultValue = '';
if ($hasDefaultValue) {
// For constant fetch expressions, PHP renders now expression
if ($this->isDefaultValueConstant) {
$defaultValue = $this->defaultValueConstantName;
} elseif (is_array($this->getDefaultValue())) {
$defaultValue = $this->defaultValueConstExpr;
} else {
$defaultValue = var_export($this->getDefaultValue(), true);
}
}
return sprintf(
"Property [ %s %s$%s%s ]\n",
implode(' ', Reflection::getModifierNames($this->getModifiers())),
$propertyType ? ReflectionType::convertToDisplayType($propertyType) . ' ' : '',
$this->getName(),
$hasDefaultValue ? (' = ' . $defaultValue) : ''
);
}
/**
* {@inheritDoc}
*/
public function getDeclaringClass(): \ReflectionClass
{
return new ReflectionClass($this->className);
}
/**
* @inheritDoc
*/
public function getDocComment(): string|false
{
$docBlock = $this->propertyOrPromotedParam->getDocComment();
return $docBlock ? $docBlock->getText() : false;
}
/**
* {@inheritDoc}
*/
public function getModifiers(): int
{
$modifiers = 0;
if ($this->isPublic()) {
$modifiers += self::IS_PUBLIC;
}
if ($this->isProtected()) {
$modifiers += self::IS_PROTECTED;
}
if ($this->isPrivate()) {
$modifiers += self::IS_PRIVATE;
}
if ($this->isStatic()) {
$modifiers += self::IS_STATIC;
}
if ($this->isReadOnly()) {
$modifiers += self::IS_READONLY;
}
return $modifiers;
}
/**
* @inheritDoc
*/
public function getName(): string
{
$node = $this->propertyItemOrPromotedParam;
return match (true) {
$node instanceof PropertyItem => $node->name->toString(),
$node instanceof Param => (string) $node->var->name,
default => 'unknown'
};
}
/**
* @inheritDoc
*/
public function getValue(object|null $object = null): mixed
{
if (!isset($object)) {
return $this->getDefaultValue();
}
// With object we should call original reflection to determine property value
$this->initializeInternalReflection();
return parent::getValue($object);
}
/**
* @inheritDoc
*/
public function getType(): \ReflectionNamedType|\ReflectionUnionType|\ReflectionIntersectionType|null
{
return $this->type;
}
/**
* @inheritDoc
*
* @see Property::$type
* @see Param::$type
*/
public function hasType(): bool
{
return isset($this->propertyOrPromotedParam->type);
}
/**
* @inheritDoc
*
* @see PropertyItem::$default
* @see Param::$default
*
* @see https://bugs.php.net/bug.php?id=81386 For corner-case with promoted properties and default values
*/
public function hasDefaultValue(): bool
{
return (isset($this->propertyItemOrPromotedParam->default) && !$this->isPromoted()) || !$this->hasType();
}
/**
* @inheritDoc
*/
public function getDefaultValue(): mixed
{
return $this->defaultValue;
}
/**
* @inheritDoc
*/
public function isDefault(): bool
{
// TRUE if the property was declared at compile-time
return true;
}
/**
* {@inheritDoc}
*
* @see Property::isPrivate()
* @see Param::isPrivate()
*/
public function isPrivate(): bool
{
return $this->propertyOrPromotedParam->isPrivate();
}
/**
* {@inheritDoc}
*
* @see Property::isProtected()
* @see Param::isProtected()
*/
public function isProtected(): bool
{
return $this->propertyOrPromotedParam->isProtected();
}
/**
* {@inheritDoc}
*
* @see Property::isPublic()
* @see Param::isPublic()
*/
public function isPublic(): bool
{
return $this->propertyOrPromotedParam->isPublic();
}
/**
* {@inheritDoc}
*
* @see Property::isStatic()
*/
public function isStatic(): bool
{
// All promoted properties are dynamic and not static
return !$this->isPromoted() && $this->propertyOrPromotedParam->isStatic();
}
/**
* {@inheritDoc}
*
* @see Param::isPromoted()
*/
public function isPromoted(): bool
{
return $this->propertyOrPromotedParam instanceof Param;
}
/**
* {@inheritDoc}
*
* @see Property::isReadonly()
* @see Param::isReadonly()
*/
public function isReadOnly(): bool
{
return $this->propertyOrPromotedParam->isReadonly() || $this->getDeclaringClass()->isReadOnly();
}
/**
* {@inheritDoc}
*/
public function isInitialized(?object $object = null): bool
{
// If we have already object, we should proceed to original implementation
if (isset($object)) {
$this->initializeInternalReflection();
return parent::isInitialized($object);
}
// For static properties, we could check if we have default value
return $this->hasDefaultValue();
}
/**
* {@inheritDoc}
*/
#[Deprecated(reason: 'This method is no-op starting from PHP 8.1', since: '8.1')]
public function setAccessible(bool $accessible): void
{
}
/**
* @inheritDoc
*/
public function setValue(mixed $objectOrValue, mixed $value = null): void
{
$this->initializeInternalReflection();
parent::setValue($objectOrValue, $value);
}
/**
* Parses properties from the concrete class node
*
* @param ClassLike $classLikeNode Class-like node
* @param string $fullClassName FQN of the class
*
* @return ReflectionProperty[]
*/
public static function collectFromClassNode(ClassLike $classLikeNode, string $fullClassName): array
{
$properties = [];
foreach ($classLikeNode->stmts as $classLevelNode) {
// Old-fashioned properties
if ($classLevelNode instanceof Property) {
foreach ($classLevelNode->props as $classPropertyNode) {
$propertyName = $classPropertyNode->name->toString();
$properties[$propertyName] = new static(
$fullClassName,
$propertyName,
$classLevelNode,
$classPropertyNode
);
}
}
// We might also have promoted properties inside constructor
if ($classLevelNode instanceof ClassMethod && $classLevelNode->name->toString() === '__construct') {
foreach ($classLevelNode->getParams() as $paramNode) {
if ($paramNode->isPromoted()) {
$propertyName = (string) $paramNode->var->name;
$properties[$propertyName] = new static(
$fullClassName,
$propertyName,
$paramNode,
$paramNode
);
}
}
}
}
// Enum has special `name` (and `value` for Backed Enums) properties
if ($classLikeNode instanceof Enum_) {
$properties['name'] = self::createEnumNameProperty($fullClassName);
if (isset($classLikeNode->scalarType)) {
$valueProperty = self::createEnumValueProperty($classLikeNode, $fullClassName);
$properties['value'] = $valueProperty;
}
}
return $properties;
}
/**
* Implementation of internal reflection initialization
*/
protected function __initialize(): void
{
parent::__construct($this->className, $this->getName());
}
private static function createEnumNameProperty(string $fullClassName): ReflectionProperty
{
$namePropertyNode = (new \PhpParser\Builder\Property('name'))
->makeReadonly()
->makePublic()
->setType('string')
->getNode();
return new static(
$fullClassName,
'name',
$namePropertyNode,
$namePropertyNode->props[0]
);
}
private static function createEnumValueProperty(Enum_ $classLikeNode, string $fullClassName): ReflectionProperty
{
$valuePropertyNode = (new \PhpParser\Builder\Property('value'))
->makeReadonly()
->makePublic()
->setType($classLikeNode->scalarType)
->getNode();
return new static(
$fullClassName,
'value',
$valuePropertyNode,
$valuePropertyNode->props[0]
);
}
}
src/ReflectionType.php 0000644 00000003461 14641221054 0011003 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use ReflectionNamedType;
use ReflectionType as BaseReflectionType;
use ReflectionUnionType;
/**
* ReflectionType implementation
* @see \Go\ParserReflection\ReflectionTypeTest
*/
class ReflectionType extends BaseReflectionType
{
/**
* If type allows null or not
*/
private bool $allowsNull;
/**
* Type name
*/
private string $type;
/**
* Initializes reflection data
*/
public function __construct(string $type, bool $allowsNull)
{
$this->type = $type;
$this->allowsNull = $allowsNull;
}
/**
* @inheritDoc
*/
public function allowsNull(): bool
{
return $this->allowsNull;
}
/**
* @inheritDoc
*/
public function __toString(): string
{
return $this->type;
}
/**
* PHP reflection has it's own rules, so 'int' type will be displayed as 'integer', etc...
*
* @see https://3v4l.org/nZFiT
*/
public static function convertToDisplayType(BaseReflectionType $type): string
{
if ($type instanceof ReflectionNamedType) {
$displayType = $type->getName();
} else {
$displayType = (string)$type;
}
$displayType = ltrim($displayType, '\\');
$specialNullableTypes = in_array($displayType, ['mixed', 'null'], true);
if ($type->allowsNull() && !$type instanceof ReflectionUnionType && !$specialNullableTypes) {
$displayType = '?' . $displayType;
}
return $displayType;
}
}
src/ReflectionUnionType.php 0000644 00000006270 14641221054 0012015 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use ReflectionUnionType as BaseReflectionUnionType;
/**
* ReflectionUnionType implementation
*/
class ReflectionUnionType extends BaseReflectionUnionType
{
/**
* @var ReflectionNamedType[]|ReflectionIntersectionType[]
*/
private readonly array $types;
/**
* Initializes reflection data
*/
public function __construct(ReflectionNamedType|ReflectionIntersectionType ...$types)
{
$this->types = $types;
}
/**
* @inheritDoc
*/
public function allowsNull(): bool
{
// Nullable if we have at least one nullable type
foreach ($this->types as $type) {
if ($type->allowsNull()) {
return true;
}
}
return false;
}
/**
* @inheritDoc
*/
public function __toString(): string
{
$stringTypes = array_map(function(ReflectionNamedType|ReflectionIntersectionType $type) {
return match (true) {
$type instanceof ReflectionNamedType => $type->getName(),
$type instanceof ReflectionIntersectionType => '(' . $type . ')',
};
}, $this->types);
$allowsNull = array_reduce(
$this->types,
fn(bool $allowsNull, \ReflectionType $type) => $allowsNull || $type->allowsNull(),
false
);
$hasExplicitNull = array_reduce(
$this->types,
fn(bool $hasExplicitNull, \ReflectionType $type) => $hasExplicitNull || in_array((string) $type, ['null', 'mixed']),
false
);
if ($allowsNull && !$hasExplicitNull) {
$stringTypes[] = 'null';
}
// Special iterable type is already union Traversable|array, thus should be replaced
$iterableIndex = array_search('iterable', $stringTypes, true);
if ($iterableIndex !== false) {
unset($stringTypes[$iterableIndex]);
array_push($stringTypes, 'Traversable', 'array');
}
// PHP has own scheme of ordering of built-in types to follow
usort($stringTypes, function(string $first, string $second): int {
static $internalTypesOrder = ['object', 'array', 'string', 'int', 'float', 'bool', 'false', 'null'];
$firstOrder = array_search($first, $internalTypesOrder, true);
$secondOrder = array_search($second, $internalTypesOrder, true);
if ($firstOrder !== false && $secondOrder !== false) {
return $firstOrder <=> $secondOrder;
}
if ($firstOrder !== false && $secondOrder === false) {
return 1;
}
if ($firstOrder === false && $secondOrder !== false) {
return -1;
}
return 0;
});
return join('|', $stringTypes);
}
/**
* @inheritDoc
*/
public function getTypes(): array
{
return $this->types;
}
}
src/Resolver/NodeExpressionResolver.php 0000644 00000050553 14641221054 0014343 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Resolver;
use Go\ParserReflection\ReflectionClass;
use Go\ParserReflection\ReflectionException;
use Go\ParserReflection\ReflectionFileNamespace;
use PhpParser\Node;
use PhpParser\Node\Const_;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\Float_;
use PhpParser\Node\Scalar\Int_;
use PhpParser\Node\Scalar\MagicConst\Line;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\PrettyPrinter\Standard;
use ReflectionFunctionAbstract;
use ReflectionMethod;
/**
* Tries to resolve expression into value
* @see \Go\ParserReflection\Resolver\NodeExpressionResolverTest
*/
class NodeExpressionResolver
{
/**
* List of exception for constant fetch
*/
private static array $notConstants = [
'true' => true,
'false' => true,
'null' => true,
];
/**
* Current reflection context for parsing
*/
private
\ReflectionClass|\ReflectionFunction|\ReflectionMethod|\ReflectionClassConstant|
\ReflectionParameter|\ReflectionAttribute|\ReflectionProperty|ReflectionFileNamespace|null $context;
/**
* Flag if given expression is constant
*/
private bool $isConstant = false;
/**
* If given expression is constant-like (used mostly for dumping string representation) of node in reflection
*/
private bool $isConstExpr = false;
/**
* Name of the constant (if present), used to collect references to constants for misc places
* @see $isConstant
*/
private ?string $constantName;
/**
* Node resolving level, 1 = top-level
*/
private int $nodeLevel = 0;
/**
* @var Node[]
*/
private array $nodeStack = [];
private mixed $value;
public function __construct($context)
{
$this->context = $context;
}
public function getConstantName(): ?string
{
return $this->constantName;
}
public function getValue(): mixed
{
return $this->value;
}
public function isConstant(): bool
{
return $this->isConstant;
}
public function isConstExpression(): bool
{
return $this->isConstExpr;
}
public function getConstExpression(): ?string
{
$expression = null;
if ($this->isConstExpr) {
// Clone node to avoid possible side-effects
$node = clone $this->nodeStack[$this->nodeLevel];
if ($node instanceof Expr\ConstFetch) {
if ($node->name->isFullyQualified()) {
// For full-qualified names we would like to remove leading "\"
$node->name = new Name(ltrim($node->name->toString(), '\\'));
} else {
// For relative names we would like to add namespace prefix
$node->name = new Name($this->resolveScalarMagicConstNamespace() . '\\' . $node->name->toString());
}
}
// All long array nodes are pretty-printed by PHP in short format
if ($node instanceof Expr\Array_ && $node->getAttribute('kind') === Expr\Array_::KIND_LONG) {
$node->setAttribute('kind', Expr\Array_::KIND_SHORT);
}
$printer = new Standard(['shortArraySyntax' => true]);
$expression = $printer->prettyPrintExpr($node);
}
return $expression;
}
/**
* @throws ReflectionException If node could not be resolved
*/
final public function process(Node $node): void
{
$this->nodeLevel = 0;
$this->nodeStack = [$node]; // Always keep the root node
$this->isConstant = false;
$this->isConstExpr = false;
$this->constantName = null;
$this->value = $this->resolve($node);
}
/**
* Recursively resolves node into valid value
*
* @throws ReflectionException If couldn't resolve value for given Node
*/
final protected function resolve(Node $node): mixed
{
$value = null;
try {
$this->nodeStack[] = $node;
++$this->nodeLevel;
if ($this->nodeLevel > 1 && $this->isConstant) {
$this->isConstant = false;
$this->constantName = null;
}
$methodName = $this->getDispatchMethodFor($node);
if (!method_exists($this, $methodName)) {
throw new ReflectionException("Could not find handler for the " . __CLASS__ . "::{$methodName} method");
}
$value = $this->$methodName($node);
} finally {
array_pop($this->nodeStack);
--$this->nodeLevel;
}
return $value;
}
protected function resolveStmtExpression(Expression $node): mixed
{
// Just unwrap "expr;" statements.
return $this->resolve($node->expr);
}
protected function resolveNameFullyQualified(Name\FullyQualified $node): string
{
return $node->toString();
}
protected function resolveIdentifier(Node\Identifier $node): string
{
return $node->toString();
}
/**
* @throws \Throwable In case of any errors during function evaluation
*/
protected function resolveExprFuncCall(Expr\FuncCall $node): mixed
{
$functionName = $this->resolve($node->name);
$resolvedArgs = [];
foreach ($node->args as $argumentNode) {
$value = $this->resolve($argumentNode->value);
// if function uses named arguments, then unpack argument name first
if (isset($argumentNode->name)) {
$name = $this->resolve($argumentNode->name);
$resolvedArgs[$name] = $value;
} else {
// otherwise simply add argument to the list
$resolvedArgs[] = $value;
}
}
$reflectedFunction = new \ReflectionFunction($functionName);
if (!$reflectedFunction->isInternal()) {
throw new ReflectionException("Only internal PHP functions can be evaluated safely");
}
return $reflectedFunction->invoke(...$resolvedArgs);
}
protected function resolveScalarFloat(Float_ $node): float
{
return $node->value;
}
protected function resolveScalarInt(Int_ $node): int
{
return $node->value;
}
protected function resolveScalarString(String_ $node): string
{
return $node->value;
}
/**
* @throws ReflectionException If not in the context of parsing method body
*/
protected function resolveScalarMagicConstMethod(): string
{
if (!$this->context instanceof ReflectionMethod) {
throw new ReflectionException("Could not resolve __METHOD__ without method context");
}
return $this->context->getDeclaringClass()->name . '::' . $this->context->getShortName();
}
/**
* @throws ReflectionException If not in the context of parsing function body
*/
protected function resolveScalarMagicConstFunction(): string
{
if (!$this->context instanceof ReflectionFunctionAbstract) {
throw new ReflectionException("Could not resolve __FUNCTION__ without function context");
}
return $this->context->getName();
}
/**
* @throws ReflectionException If not inside ReflectionFileNamespace or context doesn't have getNamespaceName()
*/
protected function resolveScalarMagicConstNamespace(): string
{
if ($this->context instanceof ReflectionFileNamespace) {
return $this->context->getName();
}
if (!method_exists($this->context, 'getNamespaceName')) {
throw new ReflectionException("Could not resolve __NAMESPACE__ without having getNamespaceName");
}
return $this->context->getNamespaceName();
}
/**
* @throws ReflectionException If not inside ReflectionClass or class children nodes
*/
protected function resolveScalarMagicConstClass(): string
{
if ($this->context instanceof \ReflectionClass) {
return $this->context->name;
}
if (!method_exists($this->context, 'getDeclaringClass')) {
throw new ReflectionException("Could not resolve __CLASS__ without having getDeclaringClass");
}
$declaringClass = $this->context->getDeclaringClass();
return $declaringClass->name;
}
/**
* @throws ReflectionException If ReflectionContext doesn't have getFileName
*/
protected function resolveScalarMagicConstDir(): string
{
if (!method_exists($this->context, 'getFileName')) {
throw new ReflectionException("Could not resolve __DIR__ without having getFileName");
}
return dirname($this->context->getFileName());
}
/**
* @throws ReflectionException If ReflectionContext doesn't have getFileName
*/
protected function resolveScalarMagicConstFile(): string
{
if (!method_exists($this->context, 'getFileName')) {
throw new ReflectionException("Could not resolve __FILE__ without having getFileName");
}
return $this->context->getFileName();
}
protected function resolveScalarMagicConstLine(Line $node): int
{
return $node->getStartLine();
}
/**
* @throws ReflectionException If not inside trait context
*/
protected function resolveScalarMagicConstTrait(): string
{
if (!$this->context instanceof \ReflectionClass || !$this->context->isTrait()) {
throw new ReflectionException("Could not resolve __TRAIT__ without trait context");
}
return $this->context->name;
}
protected function resolveExprConstFetch(Expr\ConstFetch $node)
{
$constantValue = null;
$isResolved = false;
$isFQNConstant = $node->name instanceof Node\Name\FullyQualified;
$constantName = $node->name->toString();
if (!$isFQNConstant && method_exists($this->context, 'getFileName')) {
$fileName = $this->context->getFileName();
$namespaceName = $this->resolveScalarMagicConstNamespace();
$fileNamespace = new ReflectionFileNamespace($fileName, $namespaceName);
if ($fileNamespace->hasConstant($constantName)) {
$constantValue = $fileNamespace->getConstant($constantName);
$constantName = $fileNamespace->getName() . '\\' . $constantName;
$isResolved = true;
}
}
$isRealConstant = !isset(self::$notConstants[$constantName]);
if (!$isResolved && defined($constantName)) {
$constantValue = constant($constantName);
if (!$isFQNConstant) {
$constantName = $this->context->getNamespaceName() . '\\' . $constantName;
}
}
if ($this->nodeLevel === 1 && $isRealConstant) {
$this->isConstant = true;
$this->isConstExpr = true;
$this->constantName = $constantName;
}
return $constantValue;
}
protected function resolveExprClassConstFetch(Expr\ClassConstFetch $node)
{
$classToReflect = $node->class;
if (!($classToReflect instanceof Node\Name)) {
$classToReflect = $this->resolve($classToReflect);
if (!is_string($classToReflect)) {
throw new ReflectionException("Unable 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 instanceof Expr\Error) ? '' : $node->name->toString();
// special handling of ::class constants
if ('class' === $constantName) {
return $refClass->getName();
}
$this->isConstant = true;
$this->isConstExpr = true;
$this->constantName = $classToReflect . '::' . $constantName;
return $refClass->getConstant($constantName);
}
protected function resolveExprArray(Expr\Array_ $node): array
{
// For array expressions we would like to have pretty-printed output too
$this->isConstExpr = true;
$result = [];
foreach ($node->items as $itemIndex => $arrayItem) {
$itemValue = $this->resolve($arrayItem->value);
$itemKey = isset($arrayItem->key) ? $this->resolve($arrayItem->key) : $itemIndex;
$result[$itemKey] = $itemValue;
}
return $result;
}
protected function resolveExprBinaryOpPlus(Expr\BinaryOp\Plus $node): int|float|array
{
return $this->resolve($node->left) + $this->resolve($node->right);
}
protected function resolveExprBinaryOpMinus(Expr\BinaryOp\Minus $node): int|float
{
return $this->resolve($node->left) - $this->resolve($node->right);
}
protected function resolveExprBinaryOpMul(Expr\BinaryOp\Mul $node): int|float
{
return $this->resolve($node->left) * $this->resolve($node->right);
}
protected function resolveExprBinaryOpPow(Expr\BinaryOp\Pow $node): int|float
{
return $this->resolve($node->left) ** $this->resolve($node->right);
}
protected function resolveExprBinaryOpDiv(Expr\BinaryOp\Div $node): int|float
{
return $this->resolve($node->left) / $this->resolve($node->right);
}
/**
* Operands of modulo are converted to int before processing
*
* @see https://www.php.net/manual/en/language.operators.arithmetic.php#language.operators.arithmetic
*/
protected function resolveExprBinaryOpMod(Expr\BinaryOp\Mod $node): int
{
return $this->resolve($node->left) % $this->resolve($node->right);
}
protected function resolveExprBooleanNot(Expr\BooleanNot $node): bool
{
return !$this->resolve($node->expr);
}
protected function resolveExprBitwiseNot(Expr\BitwiseNot $node): int|string
{
return ~$this->resolve($node->expr);
}
protected function resolveExprBinaryOpBitwiseOr(Expr\BinaryOp\BitwiseOr $node): int
{
return $this->resolve($node->left) | $this->resolve($node->right);
}
protected function resolveExprBinaryOpBitwiseAnd(Expr\BinaryOp\BitwiseAnd $node): int
{
return $this->resolve($node->left) & $this->resolve($node->right);
}
protected function resolveExprBinaryOpBitwiseXor(Expr\BinaryOp\BitwiseXor $node): int
{
return $this->resolve($node->left) ^ $this->resolve($node->right);
}
protected function resolveExprBinaryOpShiftLeft(Expr\BinaryOp\ShiftLeft $node): int
{
return $this->resolve($node->left) << $this->resolve($node->right);
}
protected function resolveExprBinaryOpShiftRight(Expr\BinaryOp\ShiftRight $node): int
{
return $this->resolve($node->left) >> $this->resolve($node->right);
}
protected function resolveExprBinaryOpConcat(Expr\BinaryOp\Concat $node): string
{
return $this->resolve($node->left) . $this->resolve($node->right);
}
protected function resolveExprTernary(Expr\Ternary $node): mixed
{
if (isset($node->if)) {
// Full syntax $a ? $b : $c;
return $this->resolve($node->cond) ? $this->resolve($node->if) : $this->resolve($node->else);
}
return $this->resolve($node->cond) ?: $this->resolve($node->else);
}
protected function resolveExprBinaryOpSmallerOrEqual(Expr\BinaryOp\SmallerOrEqual $node): bool
{
return $this->resolve($node->left) <= $this->resolve($node->right);
}
protected function resolveExprBinaryOpGreaterOrEqual(Expr\BinaryOp\GreaterOrEqual $node): bool
{
return $this->resolve($node->left) >= $this->resolve($node->right);
}
protected function resolveExprBinaryOpEqual(Expr\BinaryOp\Equal $node): bool
{
/** @noinspection TypeUnsafeComparisonInspection */
return $this->resolve($node->left) == $this->resolve($node->right);
}
protected function resolveExprBinaryOpNotEqual(Expr\BinaryOp\NotEqual $node): bool
{
/** @noinspection TypeUnsafeComparisonInspection */
return $this->resolve($node->left) != $this->resolve($node->right);
}
protected function resolveExprBinaryOpSmaller(Expr\BinaryOp\Smaller $node): bool
{
return $this->resolve($node->left) < $this->resolve($node->right);
}
protected function resolveExprBinaryOpGreater(Expr\BinaryOp\Greater $node): bool
{
return $this->resolve($node->left) > $this->resolve($node->right);
}
protected function resolveExprBinaryOpIdentical(Expr\BinaryOp\Identical $node): bool
{
return $this->resolve($node->left) === $this->resolve($node->right);
}
protected function resolveExprBinaryOpNotIdentical(Expr\BinaryOp\NotIdentical $node): bool
{
return $this->resolve($node->left) !== $this->resolve($node->right);
}
protected function resolveExprBinaryOpBooleanAnd(Expr\BinaryOp\BooleanAnd $node): bool
{
return $this->resolve($node->left) && $this->resolve($node->right);
}
protected function resolveExprBinaryOpLogicalAnd(Expr\BinaryOp\LogicalAnd $node): bool
{
return $this->resolve($node->left) and $this->resolve($node->right);
}
protected function resolveExprBinaryOpBooleanOr(Expr\BinaryOp\BooleanOr $node): bool
{
return $this->resolve($node->left) || $this->resolve($node->right);
}
protected function resolveExprBinaryOpLogicalOr(Expr\BinaryOp\LogicalOr $node): bool
{
return $this->resolve($node->left) or $this->resolve($node->right);
}
protected function resolveExprBinaryOpLogicalXor(Expr\BinaryOp\LogicalXor $node): bool
{
return $this->resolve($node->left) xor $this->resolve($node->right);
}
private function getDispatchMethodFor(Node $node): string
{
$nodeType = $node->getType();
return 'resolve' . str_replace('_', '', $nodeType);
}
/**
* Utility method to fetch reflection class instance by name
*
* Supports:
* 'self' keyword
* 'parent' keyword
* not-FQN class names
*
* @param Node\Name $node Class name node
*
* @return bool|\ReflectionClass
*
* @throws ReflectionException
*/
private function fetchReflectionClass(Node\Name $node)
{
$className = $node->toString();
$isFQNClass = $node instanceof Node\Name\FullyQualified;
if ($isFQNClass) {
// check to see if the class is already loaded and is safe to use
// PHP's ReflectionClass to determine if the class is user defined
if (class_exists($className, false)) {
$refClass = new \ReflectionClass($className);
if (!$refClass->isUserDefined()) {
return $refClass;
}
}
return new ReflectionClass($className);
}
if ('self' === $className) {
if ($this->context instanceof \ReflectionClass) {
return $this->context;
}
if (method_exists($this->context, 'getDeclaringClass')) {
return $this->context->getDeclaringClass();
}
}
if ('parent' === $className) {
if ($this->context instanceof \ReflectionClass) {
return $this->context->getParentClass();
}
if (method_exists($this->context, 'getDeclaringClass')) {
return $this->context->getDeclaringClass()
->getParentClass()
;
}
}
if (method_exists($this->context, 'getFileName')) {
/** @var ReflectionFileNamespace|null $fileNamespace */
$fileName = $this->context->getFileName();
$namespaceName = $this->resolveScalarMagicConstNamespace();
$fileNamespace = new ReflectionFileNamespace($fileName, $namespaceName);
return $fileNamespace->getClass($className);
}
throw new ReflectionException("Can not resolve class $className");
}
}
src/Resolver/TypeExpressionResolver.php 0000644 00000015460 14641221054 0014375 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Resolver;
use Go\ParserReflection\ReflectionClass;
use Go\ParserReflection\ReflectionException;
use Go\ParserReflection\ReflectionFileNamespace;
use Go\ParserReflection\ReflectionIntersectionType;
use Go\ParserReflection\ReflectionNamedType;
use Go\ParserReflection\ReflectionUnionType;
use PhpParser\Node;
use PhpParser\Node\Const_;
use PhpParser\Node\Expr;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\MagicConst\Line;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\PrettyPrinter\Standard;
/**
* Tries to resolve expression into value
*/
class TypeExpressionResolver
{
/**
* Current reflection context for parsing
*/
private
\ReflectionClass|\ReflectionFunction|\ReflectionMethod|\ReflectionClassConstant|
\ReflectionParameter|\ReflectionAttribute|\ReflectionProperty|ReflectionFileNamespace|null $context;
/**
* Whether this type has explicit null value set
*/
private bool $hasDefaultNull = false;
/**
* Node resolving level, 1 = top-level
*/
private int $nodeLevel = 0;
/**
* @var Node[]
*/
private array $nodeStack = [];
private \ReflectionNamedType|\ReflectionUnionType|\ReflectionIntersectionType|null $type;
public function __construct($context)
{
$this->context = $context;
}
/**
* @throws ReflectionException If node could not be resolved
*/
final public function process(Node $node, bool $hasDefaultNull): void
{
$this->hasDefaultNull = $hasDefaultNull;
$this->nodeLevel = 0;
$this->nodeStack = [$node]; // Always keep the root node
$this->type = $this->resolve($node);
}
final public function getType(): \ReflectionNamedType|\ReflectionUnionType|\ReflectionIntersectionType|null
{
return $this->type;
}
/**
* Recursively resolves node into valid type
*
* @throws ReflectionException If couldn't resolve value for given Node
*/
final protected function resolve(Node $node): mixed
{
$type = null;
try {
$this->nodeStack[] = $node;
++$this->nodeLevel;
$methodName = $this->getDispatchMethodFor($node);
if (!method_exists($this, $methodName)) {
throw new ReflectionException("Could not find handler for the " . __CLASS__ . "::{$methodName} method");
}
$type = $this->$methodName($node);
} finally {
array_pop($this->nodeStack);
--$this->nodeLevel;
}
return $type;
}
private function resolveUnionType(Node\UnionType $unionType): ReflectionUnionType
{
$resolvedTypes = array_map(
fn(Identifier|IntersectionType|Name $singleType) => $this->resolve($singleType),
$unionType->types
);
return new ReflectionUnionType(...$resolvedTypes);
}
private function resolveIntersectionType(Node\IntersectionType $intersectionType): ReflectionIntersectionType
{
$resolvedTypes = array_map(
fn(Identifier|IntersectionType|Name $singleType) => $this->resolve($singleType),
$intersectionType->types
);
return new ReflectionIntersectionType(...$resolvedTypes);
}
private function resolveNullableType(Node\NullableType $node): ReflectionNamedType
{
$type = $this->resolve($node->type);
return new ReflectionNamedType($type->getName(), true, false);
}
private function resolveIdentifier(Node\Identifier $node): ReflectionNamedType
{
$typeString = $node->toString();
$allowsNull = $this->hasDefaultNull || in_array($typeString, ['null', 'mixed'], true);
return new ReflectionNamedType($typeString, $allowsNull, true);
}
private function resolveName(Name $node): ReflectionNamedType
{
return new ReflectionNamedType($node->toString(), $this->hasDefaultNull, false);
}
private function resolveNameFullyQualified(Name\FullyQualified $node): ReflectionNamedType
{
return new ReflectionNamedType((string) $node, $this->hasDefaultNull, false);
}
private function getDispatchMethodFor(Node $node): string
{
$nodeType = $node->getType();
return 'resolve' . str_replace('_', '', $nodeType);
}
/**
* Utility method to fetch reflection class instance by name
*
* Supports:
* 'self' keyword
* 'parent' keyword
* not-FQN class names
*
* @param Node\Name $node Class name node
*
* @return bool|\ReflectionClass
*
* @throws ReflectionException
*/
private function fetchReflectionClass(Node\Name $node)
{
$className = $node->toString();
$isFQNClass = $node instanceof Node\Name\FullyQualified;
if ($isFQNClass) {
// check to see if the class is already loaded and is safe to use
// PHP's ReflectionClass to determine if the class is user defined
if (class_exists($className, false)) {
$refClass = new \ReflectionClass($className);
if (!$refClass->isUserDefined()) {
return $refClass;
}
}
return new ReflectionClass($className);
}
if ('self' === $className) {
if ($this->context instanceof \ReflectionClass) {
return $this->context;
}
if (method_exists($this->context, 'getDeclaringClass')) {
return $this->context->getDeclaringClass();
}
}
if ('parent' === $className) {
if ($this->context instanceof \ReflectionClass) {
return $this->context->getParentClass();
}
if (method_exists($this->context, 'getDeclaringClass')) {
return $this->context->getDeclaringClass()
->getParentClass()
;
}
}
if (method_exists($this->context, 'getFileName')) {
/** @var ReflectionFileNamespace|null $fileNamespace */
$fileName = $this->context->getFileName();
$namespaceName = $this->resolveScalarMagicConstNamespace();
$fileNamespace = new ReflectionFileNamespace($fileName, $namespaceName);
return $fileNamespace->getClass($className);
}
throw new ReflectionException("Can not resolve class $className");
}
}
src/Traits/AttributeResolverTrait.php 0000644 00000004165 14641221054 0014010 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Traits;
use Go\ParserReflection\ReflectionAttribute;
use Go\ParserReflection\ReflectionProperty;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
trait AttributeResolverTrait
{
/**
* @return ReflectionAttribute[]
*/
public function getAttributes(?string $name = null, int $flags = 0): array
{
if ($this instanceof ReflectionProperty) {
$node = $this->getTypeNode();
} else {
$node = $this->getNode();
}
$attributes = [];
$nodeExpressionResolver = new NodeExpressionResolver($this);
foreach ($node->attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
$arguments = [];
foreach ($attr->args as $arg) {
$nodeExpressionResolver->process($arg->value);
$arguments[] = $nodeExpressionResolver->getValue();
}
if ($name === null) {
$attributes[] = new ReflectionAttribute($attr->name->toString(), $this, $arguments, $this->isAttributeRepeated($attr->name->toString(), $node->attrGroups));
continue;
}
if ($name !== $attr->name->toString()) {
continue;
}
$attributes[] = new ReflectionAttribute($name, $this, $arguments, $this->isAttributeRepeated($name, $node->attrGroups));
}
}
return $attributes;
}
private function isAttributeRepeated(string $attributeName, array $attrGroups): bool
{
$count = 0;
foreach ($attrGroups as $attrGroup) {
foreach ($attrGroup->attrs as $attr) {
if ($attr->name->toString() === $attributeName) {
++$count;
}
}
}
return $count >= 2;
}
}
src/Traits/InitializationTrait.php 0000644 00000002005 14641221054 0013301 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Traits;
trait InitializationTrait
{
/**
* Is internal reflection is initialized or not
*/
private bool $isInitialized = false;
/**
* Initializes internal reflection for calling misc runtime methods
*/
public function initializeInternalReflection(): void
{
if (!$this->isInitialized) {
$this->__initialize();
$this->isInitialized = true;
}
}
/**
* Returns the status of initialization status for internal object
*/
public function __isInitialized(): bool
{
return $this->isInitialized;
}
/**
* Implementation of internal reflection initialization
*/
abstract protected function __initialize(): void;
}
src/Traits/InternalPropertiesEmulationTrait.php 0000644 00000002074 14641221054 0016027 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Traits;
/**
* Class for emulating internal properties behaviour
*/
trait InternalPropertiesEmulationTrait
{
/**
* Magic method that should be defined to provide an info about internal properties
*/
abstract public function __debugInfo(): array;
/**
* Magic implementation of properties
*
* @param string $propertyName Name of the property to return
*
* @return null|mixed
*/
public function __get(string $propertyName)
{
$internalInfo = $this->__debugInfo();
if (!isset($internalInfo[$propertyName])) {
$className = get_class($this);
trigger_error("Undefined property {$className}::\${$propertyName}");
return null;
}
return $internalInfo[$propertyName];
}
}
src/Traits/ReflectionClassLikeTrait.php 0000644 00000070443 14641221054 0014212 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Traits;
use Closure;
use Go\ParserReflection\ReflectionClass;
use Go\ParserReflection\ReflectionClassConstant;
use Go\ParserReflection\ReflectionException;
use Go\ParserReflection\ReflectionMethod;
use Go\ParserReflection\ReflectionProperty;
use Go\ParserReflection\Resolver\NodeExpressionResolver;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Enum_;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\Node\Stmt\TraitUseAdaptation;
use ReflectionExtension;
use ReflectionObject;
use RuntimeException;
use function func_num_args;
/**
* General class-like reflection
*/
trait ReflectionClassLikeTrait
{
use InitializationTrait;
protected ClassLike $classLikeNode;
/**
* Short name of the class, without namespace
*/
protected string $className;
/**
* List of all constants from the class or null if not initialized yet
*/
protected ?array $constants;
/**
* Interfaces or null if not initialized yet
*
* @var \ReflectionClass[]|null
*/
protected ?array $interfaceClasses;
/**
* List of traits or null if not initialized yet
*
* @var \ReflectionClass[]|null
*/
protected ?array $traits;
/**
* Additional list of trait adaptations
*
* @var TraitUseAdaptation[]
*/
protected array $traitAdaptations = [];
/**
* @var ReflectionMethod[]
*/
protected ?array $methods;
/**
* Namespace name
*/
protected string $namespaceName = '';
/**
* Parent class, or false if not present, null if uninitialized yet
*/
protected null|\ReflectionClass|false $parentClass;
/**
* @var ReflectionProperty[]
*/
protected ?array $properties;
/**
* @var ReflectionClassConstant[]
*/
protected ?array $classConstants;
/**
* Returns the string representation of the ReflectionClass object.
*/
public function __toString(): string
{
$isObject = $this instanceof ReflectionObject;
$staticProperties = $staticMethods = $defaultProperties = $dynamicProperties = $methods = [];
$format = "%s%s [ %sclass %s%s%s ] {\n";
$format .= " @@ %s %d-%d\n\n";
$format .= " - Constants [%d] {%s\n }\n\n";
$format .= " - Static properties [%d] {%s\n }\n\n";
$format .= " - Static methods [%d] {%s\n }\n\n";
$format .= " - Properties [%d] {%s\n }\n\n";
$format .= ($isObject ? " - Dynamic properties [%d] {%s\n }\n\n" : '%s%s');
$format .= " - Methods [%d] {%s\n }\n";
$format .= "}\n";
foreach ($this->getProperties() as $property) {
if ($property->isStatic()) {
$staticProperties[] = $property;
} elseif ($property->isDefault()) {
$defaultProperties[] = $property;
} else {
$dynamicProperties[] = $property;
}
}
foreach ($this->getMethods() as $method) {
if ($method->isStatic()) {
$staticMethods[] = $method;
} else {
$methods[] = $method;
}
}
$buildString = static function (array $items, $indentLevel = 4) {
if (!count($items)) {
return '';
}
$indent = "\n" . str_repeat(' ', $indentLevel);
return $indent . implode($indent, explode("\n", implode("\n", $items)));
};
$buildConstants = static function (array $items, $indentLevel = 4) {
$str = '';
foreach ($items as $name => $value) {
$str .= "\n" . str_repeat(' ', $indentLevel);
$str .= sprintf(
'Constant [ %s %s ] { %s }',
gettype($value),
$name,
$value
);
}
return $str;
};
$interfaceNames = $this->getInterfaceNames();
$parentClass = $this->getParentClass();
$modifiers = '';
if ($this->isAbstract()) {
$modifiers = 'abstract ';
} elseif ($this->isFinal()) {
$modifiers = 'final ';
}
$string = sprintf(
$format,
$this->getDocComment() ? $this->getDocComment() . "\n" : '',
($isObject ? 'Object of class' : 'Class'),
$modifiers,
$this->getName(),
false !== $parentClass ? (' extends ' . $parentClass->getName()) : '',
$interfaceNames ? (' implements ' . implode(', ', $interfaceNames)) : '',
$this->getFileName(),
$this->getStartLine(),
$this->getEndLine(),
count($this->getConstants()),
$buildConstants($this->getConstants()),
count($staticProperties),
$buildString($staticProperties),
count($staticMethods),
$buildString($staticMethods),
count($defaultProperties),
$buildString($defaultProperties),
$isObject ? count($dynamicProperties) : '',
$isObject ? $buildString($dynamicProperties) : '',
count($methods),
$buildString($methods)
);
return $string;
}
/**
* {@inheritDoc}
*/
public function getConstant(string $name): mixed
{
if ($this->hasConstant($name)) {
return $this->constants[$name];
}
return false;
}
/**
* {@inheritDoc}
*/
public function getConstants(?int $filter = null): array
{
if (!isset($this->constants)) {
$this->constants = $this->recursiveCollect(
function (array &$result, \ReflectionClass $instance) {
$result += $instance->getConstants();
}
);
$this->collectSelfConstants();
}
return $this->constants;
}
/**
* {@inheritDoc}
*/
public function getConstructor(): ?ReflectionMethod
{
try {
$constructor = $this->getMethod('__construct');
} catch (\ReflectionException) {
$constructor = null;
}
return $constructor;
}
/**
* Gets default properties from a class (including inherited properties).
*
* @link http://php.net/manual/en/reflectionclass.getdefaultproperties.php
*
* @return array An array of default properties, with the key being the name of the property and the value being
* the default value of the property or NULL if the property doesn't have a default value
*/
public function getDefaultProperties(): array
{
$defaultValues = [];
$properties = $this->getProperties();
$staticOrder = [true, false];
foreach ($staticOrder as $shouldBeStatic) {
foreach ($properties as $property) {
$isStaticProperty = $property->isStatic();
if ($shouldBeStatic !== $isStaticProperty) {
continue;
}
$propertyName = $property->getName();
$isInternalReflection = $property::class === \ReflectionProperty::class;
if (!$property->hasDefaultValue()) {
continue;
}
if (!$isInternalReflection || $isStaticProperty) {
$defaultValues[$propertyName] = $property->getValue();
} elseif (!$isStaticProperty) {
// Internal reflection and dynamic property
$classProperties = $property->getDeclaringClass()->getDefaultProperties();
$defaultValues[$propertyName] = $classProperties[$propertyName];
}
}
}
return $defaultValues;
}
/**
* {@inheritDoc}
*/
public function getDocComment(): string|false
{
$docComment = $this->classLikeNode->getDocComment();
return $docComment ? $docComment->getText() : false;
}
public function getEndLine(): int|false
{
return $this->classLikeNode->getAttribute('endLine');
}
public function getExtension(): ?ReflectionExtension
{
return null;
}
public function getExtensionName(): string|false
{
return false;
}
public function getFileName(): string|false
{
return $this->classLikeNode->getAttribute('fileName');
}
/**
* {@inheritDoc}
*/
public function getInterfaceNames(): array
{
return array_keys($this->getInterfaces());
}
/**
* {@inheritDoc}
*/
public function getInterfaces(): array
{
if (!isset($this->interfaceClasses)) {
$this->interfaceClasses = $this->recursiveCollect(
function (array &$result, \ReflectionClass $instance) {
if ($instance->isInterface()) {
$result[$instance->name] = $instance;
}
$result += $instance->getInterfaces();
}
);
}
return $this->interfaceClasses;
}
/**
* {@inheritdoc}
*
* @return ReflectionMethod
*/
public function getMethod(string $name): \ReflectionMethod
{
foreach ($this->getMethods() as $method) {
if ($method->getName() === $name) {
return $method;
}
}
throw new ReflectionException("Method " . $this->getName() . "::" . $name . " does not exist");
}
/**
* {@inheritdoc}
*
* @return ReflectionMethod[]
*/
public function getMethods(int|null $filter = null): array
{
if (!isset($this->methods)) {
$directMethods = ReflectionMethod::collectFromClassNode($this->classLikeNode, $this);
$parentMethods = $this->recursiveCollect(
function (array &$result, \ReflectionClass $instance, $isParent) {
$reflectionMethods = [];
foreach ($instance->getMethods() as $reflectionMethod) {
if (!$isParent || !$reflectionMethod->isPrivate()) {
$reflectionMethods[$reflectionMethod->name] = $reflectionMethod;
}
}
$result += $reflectionMethods;
}
);
$methods = $directMethods + $parentMethods;
$this->methods = $methods;
}
if (!isset($filter)) {
return array_values($this->methods);
}
$methods = [];
foreach ($this->methods as $method) {
if (!($filter & $method->getModifiers())) {
continue;
}
$methods[] = $method;
}
return $methods;
}
/**
* Returns a bitfield of the access modifiers for this class.
*
* @link http://php.net/manual/en/reflectionclass.getmodifiers.php
*
* NB: this method is not fully compatible with original value because of hidden internal constants
*/
public function getModifiers(): int
{
$modifiers = 0;
if ($this->isFinal()) {
$modifiers += \ReflectionClass::IS_FINAL;
}
if ($this->isReadOnly()) {
$modifiers += \ReflectionClass::IS_READONLY;
}
if ($this->classLikeNode instanceof Class_ && $this->classLikeNode->isAbstract()) {
$modifiers += \ReflectionClass::IS_EXPLICIT_ABSTRACT;
}
if ($this->isInterface()) {
$abstractMethods = $this->getMethods();
} else {
$abstractMethods = $this->getMethods(\ReflectionMethod::IS_ABSTRACT);
}
if (!empty($abstractMethods)) {
$modifiers += \ReflectionClass::IS_IMPLICIT_ABSTRACT;
}
return $modifiers;
}
/**
* {@inheritDoc}
*/
public function getName(): string
{
$namespaceName = $this->namespaceName ? $this->namespaceName . '\\' : '';
return $namespaceName . $this->getShortName();
}
/**
* {@inheritDoc}
*/
public function getNamespaceName(): string
{
return $this->namespaceName;
}
/**
* {@inheritDoc}
*/
public function getParentClass(): \ReflectionClass|false
{
if (!isset($this->parentClass)) {
static $extendsField = 'extends';
$parentClass = false;
$hasExtends = in_array($extendsField, $this->classLikeNode->getSubNodeNames(), true);
$extendsNode = $hasExtends ? $this->classLikeNode->$extendsField : null;
if ($extendsNode instanceof FullyQualified) {
$extendsName = $extendsNode->toString();
$parentClass = $this->createReflectionForClass($extendsName);
}
$this->parentClass = $parentClass;
}
return $this->parentClass;
}
/**
* Retrieves reflected properties.
*
* @inheritDoc
*
* @return ReflectionProperty[]
*/
public function getProperties(int|null $filter = null): array
{
if (!isset($this->properties)) {
$directProperties = ReflectionProperty::collectFromClassNode($this->classLikeNode, $this->getName());
$parentProperties = $this->recursiveCollect(
function (array &$result, \ReflectionClass $instance, $isParent) {
$reflectionProperties = [];
foreach ($instance->getProperties() as $reflectionProperty) {
if (!$isParent || !$reflectionProperty->isPrivate()) {
$reflectionProperties[$reflectionProperty->name] = $reflectionProperty;
}
}
$result += $reflectionProperties;
}
);
$properties = $directProperties + $parentProperties;
$this->properties = $properties;
}
// Without filter we can just return the full list
if (!isset($filter)) {
return array_values($this->properties);
}
$properties = [];
foreach ($this->properties as $property) {
if (!($filter & $property->getModifiers())) {
continue;
}
$properties[] = $property;
}
return $properties;
}
/**
* {@inheritdoc}
*/
public function getProperty(string $name): \ReflectionProperty
{
$properties = $this->getProperties();
foreach ($properties as $property) {
if ($property->getName() === $name) {
return $property;
}
}
throw new ReflectionException("Property " . $this->getName() . "::" . $name . " does not exist");
}
/**
* @inheritDoc
*/
public function getReflectionConstant(string $name): \ReflectionClassConstant|false
{
$classConstants = $this->getReflectionConstants();
foreach ($classConstants as $classConstant) {
if ($classConstant->getName() === $name) {
return $classConstant;
}
}
return false;
}
/**
* @inheritDoc
*/
public function getReflectionConstants(?int $filter = null): array
{
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}
*/
public function getShortName(): string
{
return $this->className;
}
public function getStartLine(): int|false
{
if ($this->classLikeNode->attrGroups !== []) {
$attrGroups = $this->classLikeNode->attrGroups;
$lastAttrGroupsEndLine = end($attrGroups)->getAttribute('endLine');
return $lastAttrGroupsEndLine + 1;
}
return $this->classLikeNode->getAttribute('startLine');
}
/**
* Returns an array of trait aliases
*
* @link http://php.net/manual/en/reflectionclass.gettraitaliases.php
*
* @return array an array with new method names in keys and original names (in the format
* "TraitName::original") in values.
*/
public function getTraitAliases(): array
{
$aliases = [];
$traits = $this->getTraits();
foreach ($this->traitAdaptations as $adaptation) {
if ($adaptation instanceof TraitUseAdaptation\Alias) {
$methodName = $adaptation->method;
$traitName = null;
foreach ($traits as $trait) {
if ($trait->hasMethod($methodName)) {
$traitName = $trait->getName();
break;
}
}
$aliases[$adaptation->newName] = $traitName . '::' . $methodName;
}
}
return $aliases;
}
/**
* Returns an array of names of traits used by this class
*
* @link http://php.net/manual/en/reflectionclass.gettraitnames.php
*/
public function getTraitNames(): array
{
return array_keys($this->getTraits());
}
/**
* Returns an array of traits used by this class
*
* @link http://php.net/manual/en/reflectionclass.gettraits.php
*
* @return \ReflectionClass[]
*/
public function getTraits(): array
{
if (!isset($this->traits)) {
$traitAdaptations = [];
$this->traits = ReflectionClass::collectTraitsFromClassNode(
$this->classLikeNode,
$traitAdaptations
);
$this->traitAdaptations = $traitAdaptations;
}
return $this->traits;
}
/**
* {@inheritDoc}
*/
public function hasConstant(string $name): bool
{
$constants = $this->getConstants();
return isset($constants[$name]) || array_key_exists($name, $constants);
}
/**
* {@inheritDoc}
*/
public function hasMethod(string $name): bool
{
$methods = $this->getMethods();
foreach ($methods as $method) {
if ($method->getName() === $name) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
public function hasProperty(string $name): bool
{
$properties = $this->getProperties();
foreach ($properties as $property) {
if ($property->getName() === $name) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
public function implementsInterface(\ReflectionClass|string $interfaceName): bool
{
$allInterfaces = $this->getInterfaces();
if ($interfaceName instanceof \ReflectionClass) {
return isset($allInterfaces[$interfaceName->getName()]);
} else {
return isset($allInterfaces[$interfaceName]);
}
}
/**
* {@inheritDoc}
*/
public function inNamespace(): bool
{
return !empty($this->namespaceName);
}
/**
* {@inheritDoc}
*/
public function isAbstract(): bool
{
if ($this->classLikeNode instanceof Class_ && $this->classLikeNode->isAbstract()) {
return true;
}
if ($this->isInterface() && !empty($this->getMethods())) {
return true;
}
return false;
}
/**
* Currently, anonymous classes aren't supported for parsed reflection
*/
public function isAnonymous(): bool
{
return false;
}
/**
* {@inheritDoc}
*/
public function isCloneable(): bool
{
if ($this->isInterface() || $this->isTrait() || $this->isAbstract() || $this->isEnum()) {
return false;
}
if ($this->hasMethod('__clone')) {
return $this->getMethod('__clone')->isPublic();
}
return true;
}
/**
* {@inheritDoc}
*/
public function isFinal(): bool
{
$isFinalClass = $this->classLikeNode instanceof Class_ && $this->classLikeNode->isFinal();
return $isFinalClass || $this->isEnum();
}
/**
* {@inheritDoc}
*/
public function isReadOnly(): bool
{
return $this->classLikeNode instanceof Class_ && $this->classLikeNode->isReadonly();
}
/**
* {@inheritDoc}
*/
public function isInstance(object $object): bool
{
$className = $this->getName();
return $className === $object::class || is_subclass_of($object, $className);
}
/**
* {@inheritDoc}
*/
public function isInstantiable(): bool
{
if ($this->isInterface() || $this->isTrait() || $this->isAbstract() || $this->isEnum()) {
return false;
}
if (null === ($constructor = $this->getConstructor())) {
return true;
}
return $constructor->isPublic();
}
/**
* {@inheritDoc}
*/
public function isInterface(): bool
{
return $this->classLikeNode instanceof Interface_;
}
/**
* {@inheritDoc}
*/
public function isInternal(): bool
{
// never can be an internal method
return false;
}
/**
* {@inheritDoc}
*/
public function isIterateable(): bool
{
return $this->isIterable();
}
/**
* {@inheritDoc}
*/
public function isIterable(): bool
{
return $this->implementsInterface('Traversable');
}
/**
* {@inheritDoc}
*/
public function isSubclassOf(\ReflectionClass|string $class): bool
{
if ($class instanceof \ReflectionClass) {
$className = $class->name;
} else {
$className = $class;
}
if (!$this->classLikeNode instanceof Class_) {
return false;
}
$extends = $this->classLikeNode->extends ?? null;
if ($extends && $extends->toString() === $className) {
return true;
}
$parent = $this->getParentClass();
return false === $parent ? false : $parent->isSubclassOf($class);
}
/**
* {@inheritDoc}
*/
public function isEnum(): bool
{
return $this->classLikeNode instanceof Enum_;
}
/**
* {@inheritDoc}
*/
public function isTrait(): bool
{
return $this->classLikeNode instanceof Trait_;
}
/**
* {@inheritDoc}
*/
public function isUserDefined(): bool
{
// always defined by user, because we parse the source code
return true;
}
/**
* Gets static properties
*
* @link http://php.net/manual/en/reflectionclass.getstaticproperties.php
*/
public function getStaticProperties(): array
{
// In runtime static properties can be changed in any time
if ($this->__isInitialized()) {
return parent::getStaticProperties();
}
$properties = [];
$reflectionProperties = $this->getProperties(\ReflectionProperty::IS_STATIC);
foreach ($reflectionProperties as $reflectionProperty) {
$properties[$reflectionProperty->getName()] = $reflectionProperty->getValue();
}
return $properties;
}
/**
* Gets static property value
*
* @inheritDoc
*/
public function getStaticPropertyValue(string $name, mixed $default = null): mixed
{
$properties = $this->getStaticProperties();
$propertyExists = array_key_exists($name, $properties);
if (!$propertyExists && func_num_args() === 1) {
throw new ReflectionException("Static property does not exist and no default value is given");
}
return $propertyExists ? $properties[$name] : $default;
}
/**
* Creates a new class instance from given arguments.
*
* @link http://php.net/manual/en/reflectionclass.newinstance.php
*
* @param mixed $args Accepts a variable number of arguments which are passed to the class constructor
*/
public function newInstance(...$args): object
{
$this->initializeInternalReflection();
return parent::newInstance(...$args);
}
/**
* Creates a new class instance from given arguments.
*
* @link http://php.net/manual/en/reflectionclass.newinstanceargs.php
*
* @param array $args The parameters to be passed to the class constructor as an array.
*/
public function newInstanceArgs(array $args = []): ?object
{
$this->initializeInternalReflection();
return parent::newInstanceArgs($args);
}
/**
* Creates a new class instance without invoking the constructor.
*
* @link http://php.net/manual/en/reflectionclass.newinstancewithoutconstructor.php
*/
public function newInstanceWithoutConstructor(): object
{
$this->initializeInternalReflection();
return parent::newInstanceWithoutConstructor();
}
/**
* Sets static property value
*
* @link http://php.net/manual/en/reflectionclass.setstaticpropertyvalue.php
*
* @param string $name Property name
* @param mixed $value New property value
*/
public function setStaticPropertyValue(string $name, mixed $value): void
{
$this->initializeInternalReflection();
parent::setStaticPropertyValue($name, $value);
}
private function recursiveCollect(Closure $collector): array
{
$result = [];
$isParent = true;
$traits = $this->getTraits();
foreach ($traits as $trait) {
$collector($result, $trait, !$isParent);
}
$parentClass = $this->getParentClass();
if ($parentClass) {
$collector($result, $parentClass, $isParent);
}
$interfaces = ReflectionClass::collectInterfacesFromClassNode($this->classLikeNode);
foreach ($interfaces as $interface) {
$collector($result, $interface, $isParent);
}
return $result;
}
/**
* Collects list of constants from the class itself
*/
private function collectSelfConstants(): void
{
$expressionSolver = new NodeExpressionResolver($this);
$localConstants = [];
// constants can be only top-level nodes in the class, so we can scan them directly
foreach ($this->classLikeNode->stmts as $classLevelNode) {
if ($classLevelNode instanceof ClassConst) {
$nodeConstants = $classLevelNode->consts;
if (!empty($nodeConstants)) {
foreach ($nodeConstants as $nodeConstant) {
$expressionSolver->process($nodeConstant->value);
$localConstants[$nodeConstant->name->toString()] = $expressionSolver->getValue();
$this->constants = $localConstants + $this->constants;
}
}
}
}
}
/**
* Create a ReflectionClass for a given class name.
*
* @param string $className The name of the class to create a reflection for.
*
* @return \ReflectionClass The appropriate reflection object.
*/
abstract protected function createReflectionForClass(string $className): \ReflectionClass;
}
src/Traits/ReflectionFunctionLikeTrait.php 0000644 00000020735 14641221054 0014731 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Traits;
use Go\ParserReflection\NodeVisitor\GeneratorDetector;
use Go\ParserReflection\NodeVisitor\StaticVariablesCollector;
use Go\ParserReflection\ReflectionMethod;
use Go\ParserReflection\ReflectionParameter;
use Go\ParserReflection\Resolver\TypeExpressionResolver;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Identifier;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Function_;
use PhpParser\NodeTraverser;
use ReflectionException;
use ReflectionExtension;
/**
* General trait for all function-like reflections
*/
trait ReflectionFunctionLikeTrait
{
use InitializationTrait;
protected FunctionLike|Function_|ClassMethod $functionLikeNode;
/**
* Namespace name
*/
protected string $namespaceName = '';
/**
* @var ReflectionParameter[]
*/
protected array $parameters;
/**
* {@inheritDoc}
*/
public function getClosureScopeClass(): ?\ReflectionClass
{
$this->initializeInternalReflection();
return parent::getClosureScopeClass();
}
/**
* {@inheritDoc}
*/
public function getClosureThis(): ?object
{
$this->initializeInternalReflection();
return parent::getClosureThis();
}
public function getDocComment(): string|false
{
$docComment = $this->functionLikeNode->getDocComment();
return $docComment ? $docComment->getText() : false;
}
public function getEndLine(): int|false
{
if ($this->functionLikeNode->hasAttribute('endLine')) {
return $this->functionLikeNode->getAttribute('endLine');
}
return false;
}
public function getExtension(): ?ReflectionExtension
{
return null;
}
public function getExtensionName(): string|false
{
return false;
}
public function getFileName(): string|false
{
if ($this->functionLikeNode->hasAttribute('fileName')) {
return $this->functionLikeNode->getAttribute('fileName');
}
return false;
}
/**
* {@inheritDoc}
*/
public function getName(): string
{
if ($this->functionLikeNode instanceof Function_ || $this->functionLikeNode instanceof ClassMethod) {
$functionName = $this->functionLikeNode->name->toString();
return $this->namespaceName ? $this->namespaceName . '\\' . $functionName : $functionName;
}
return '';
}
/**
* {@inheritDoc}
*/
public function getNamespaceName(): string
{
return $this->namespaceName;
}
/**
* Get the number of parameters that a function defines, both optional and required.
*
* @link http://php.net/manual/en/reflectionfunctionabstract.getnumberofparameters.php
*/
public function getNumberOfParameters(): int
{
return count($this->functionLikeNode->getParams());
}
/**
* Get the number of required parameters that a function defines.
*
* @link http://php.net/manual/en/reflectionfunctionabstract.getnumberofrequiredparameters.php
*/
public function getNumberOfRequiredParameters(): int
{
$requiredParameters = 0;
foreach ($this->getParameters() as $parameter) {
if (!$parameter->isOptional()) {
$requiredParameters++;
}
}
return $requiredParameters;
}
/**
* {@inheritDoc}
*/
public function getParameters(): array
{
if (!isset($this->parameters)) {
$parameters = [];
foreach ($this->functionLikeNode->getParams() as $parameterIndex => $parameterNode) {
$reflectionParameter = new ReflectionParameter(
$this->getName(),
(string)$parameterNode->var->name,
$parameterNode,
$parameterIndex,
$this
);
$parameters[] = $reflectionParameter;
}
$this->parameters = $parameters;
}
return $this->parameters;
}
/**
* Gets the specified return type of a function
*
* @link http://php.net/manual/en/reflectionfunctionabstract.getreturntype.php
*/
public function getReturnType(): \ReflectionNamedType|\ReflectionUnionType|\ReflectionIntersectionType|null
{
if ($this->hasReturnType()) {
$typeResolver = new TypeExpressionResolver($this);
$typeResolver->process($this->functionLikeNode->getReturnType(), false);
return $typeResolver->getType();
}
return null;
}
/**
* {@inheritDoc}
*/
public function getShortName(): string
{
if ($this->functionLikeNode instanceof Function_ || $this->functionLikeNode instanceof ClassMethod) {
return $this->functionLikeNode->name->toString();
}
throw new ReflectionException('unable to get short name');
}
public function getStartLine(): int|false
{
if ($this->functionLikeNode->attrGroups !== []) {
$attrGroups = $this->functionLikeNode->attrGroups;
$lastAttrGroupsEndLine = end($attrGroups)->getAttribute('endLine');
return $lastAttrGroupsEndLine + 1;
}
if ($this->functionLikeNode->hasAttribute('startLine')) {
return $this->functionLikeNode->getAttribute('startLine');
}
return false;
}
/**
* {@inheritDoc}
*/
public function getStaticVariables(): array
{
$nodeTraverser = new NodeTraverser();
$variablesCollector = new StaticVariablesCollector($this);
$nodeTraverser->addVisitor($variablesCollector);
/* @see https://github.com/nikic/PHP-Parser/issues/235 */
$nodeTraverser->traverse($this->functionLikeNode->getStmts() ?: []);
return $variablesCollector->getStaticVariables();
}
/**
* Checks if the function has a specified return type
*
*
* @link http://php.net/manual/en/reflectionfunctionabstract.hasreturntype.php
*/
public function hasReturnType(): bool
{
$returnType = $this->functionLikeNode->getReturnType();
return isset($returnType);
}
/**
* {@inheritDoc}
*/
public function inNamespace(): bool
{
return !empty($this->namespaceName);
}
/**
* {@inheritDoc}
*/
public function isClosure(): bool
{
return $this->functionLikeNode instanceof Closure;
}
/**
* {@inheritDoc}
*/
public function isDeprecated(): bool
{
// user-land method/function/closure can not be deprecated
return false;
}
/**
* {@inheritDoc}
*/
public function isGenerator(): bool
{
$nodeTraverser = new NodeTraverser();
$nodeDetector = new GeneratorDetector();
$nodeTraverser->addVisitor($nodeDetector);
/* @see https://github.com/nikic/PHP-Parser/issues/235 */
$nodeTraverser->traverse($this->functionLikeNode->getStmts() ?: []);
return $nodeDetector->isGenerator();
}
/**
* {@inheritDoc}
*/
public function isInternal(): bool
{
// never can be an internal method, except for the Enum magic methods
$isEnumMethod = $this instanceof ReflectionMethod && $this->getDeclaringClass()->isEnum();
return $isEnumMethod && in_array($this->getName(), ['cases', 'tryFrom', 'from']);
}
/**
* {@inheritDoc}
*/
public function isUserDefined(): bool
{
// always user-defined method, except for the Enum magic methods
$isEnumMethod = $this instanceof ReflectionMethod && $this->getDeclaringClass()->isEnum();
return !($isEnumMethod && in_array($this->getName(), ['cases', 'tryFrom', 'from']));
}
/**
* {@inheritDoc}
*/
public function isVariadic(): bool
{
foreach ($this->getParameters() as $parameter) {
if ($parameter->isVariadic()) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
public function returnsReference(): bool
{
return $this->functionLikeNode->returnsByRef();
}
}
src/bootstrap.php 0000644 00000000767 14641221054 0010072 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
use Go\ParserReflection\Locator\ComposerLocator;
use Go\ParserReflection\ReflectionEngine;
/**
* This file is used for automatic configuration of
* Go\ParserReflection\ReflectionEngine class
*/
ReflectionEngine::init(new ComposerLocator());
tests/AbstractTestCase.php 0000644 00000015240 14641221054 0011617 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection;
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
use PHPUnit\Framework\TestCase;
use Go\ParserReflection\Stub\SimplePhp50AbstractClassWithMethods;
abstract class AbstractTestCase extends TestCase
{
protected const DEFAULT_STUB_FILENAME = '/Stub/FileWithClasses55.php';
protected ReflectionFile $parsedRefFile;
protected ReflectionFileNamespace $parsedRefFileNamespace;
protected ?ReflectionClass $parsedRefClass = null;
/**
* Name of the class to compare
*/
protected static string $reflectionClassToTest = \Reflection::class;
/**
* Name of the class to load for default tests
*/
protected static string $defaultClassToLoad = SimplePhp50AbstractClassWithMethods::class;
#[DoesNotPerformAssertions]
final public function testCoverAllMethods(): void
{
$allInternalMethods = get_class_methods(static::$reflectionClassToTest);
$allMissedMethods = [];
foreach ($allInternalMethods as $internalMethodName) {
$refMethod = new \ReflectionMethod(__NAMESPACE__ . '\\' . static::$reflectionClassToTest, $internalMethodName);
$definerClass = $refMethod->getDeclaringClass()->getName();
if (!str_starts_with($definerClass, __NAMESPACE__)) {
$allMissedMethods[] = $internalMethodName;
}
}
if (count($allMissedMethods) > 0) {
$this->markTestIncomplete('Methods ' . join(', ', $allMissedMethods) . ' are not implemented for ' . static::$reflectionClassToTest);
}
}
/**
* Provides a list of files for analysis
*/
public static function getFilesToAnalyze(): \Generator
{
yield 'PHP5.5' => [
__DIR__ . '/Stub/FileWithClasses55.php',
__DIR__ . '/Stub/FileWithFunctions55.php',
__DIR__ . '/Stub/FileWithParameters55.php',
];
yield 'PHP5.6' => [
__DIR__ . '/Stub/FileWithClasses56.php',
__DIR__ . '/Stub/FileWithParameters56.php'
];
yield 'PHP7.0' => [
__DIR__ . '/Stub/FileWithClasses70.php',
__DIR__ . '/Stub/FileWithParameters70.php',
__DIR__ . '/Stub/FileWithFunctions70.php',
];
yield 'PHP7.1' => [__DIR__ . '/Stub/FileWithClasses71.php'];
yield 'PHP7.2' => [__DIR__ . '/Stub/FileWithClasses72.php'];
yield 'PHP7.4' => [__DIR__ . '/Stub/FileWithClasses74.php'];
yield 'PHP8.0' => [
__DIR__ . '/Stub/FileWithClasses80.php',
__DIR__ . '/Stub/FileWithParameters80.php',
__DIR__ . '/Stub/FileWithFunction80.php',
];
yield 'PHP8.1' => [__DIR__ . '/Stub/FileWithClasses81.php'];
yield 'PHP8.2' => [__DIR__ . '/Stub/FileWithClasses82.php'];
if (PHP_VERSION_ID >= 80300) {
yield 'PHP8.3' => [__DIR__ . '/Stub/FileWithClasses83.php'];
}
}
/**
* Provides generator list in the form [ParsedFile, ParsedFileNamespace]
*/
public static function getFileNamespacesToAnalyze(): \Generator
{
foreach (static::getFilesToAnalyze() as $prefix => $fileList) {
foreach ($fileList as $fileName) {
$fileName = stream_resolve_include_path($fileName);
$fileNode = ReflectionEngine::parseFile($fileName);
$reflectionFile = new ReflectionFile($fileName, $fileNode);
include_once $fileName;
foreach ($reflectionFile->getFileNamespaces() as $fileNamespace) {
yield $prefix => [$reflectionFile, $fileNamespace];
}
}
}
}
/**
* Provides generator list in the form [ParsedClass, ReflectionClass]
*/
public static function classesDataProvider(): \Generator
{
foreach (static::getFileNamespacesToAnalyze() as $prefix => [$reflectionFile, $fileNamespace]) {
foreach ($fileNamespace->getClasses() as $parsedClass) {
$refClass = new \ReflectionClass($parsedClass->getName());
yield $prefix . ' ' . $refClass->getName() => [$parsedClass, $refClass];
}
}
}
/**
* Provides full test-case list in the form [ParsedClass, ReflectionMethod, \ReflectionMethod to check]
*/
public static function methodsDataProvider(): \Generator
{
foreach (self::classesDataProvider() as $prefix => [$parsedClass, $refClass]) {
foreach ($refClass->getMethods() as $classMethod) {
$parsedMethod = $parsedClass->getMethod($classMethod->getName());
$fullMethodName = $parsedClass->getName() . '->' . $classMethod->getName() . '()';
yield $prefix . ' ' . $fullMethodName => [
$parsedClass,
$parsedMethod,
$classMethod
];
}
}
}
/**
* Provides generator list in the form [ParsedFunction, ReflectionFunction]
*/
public static function functionsDataProvider(): \Generator
{
foreach (static::getFileNamespacesToAnalyze() as $prefix => [$reflectionFile, $fileNamespace]) {
foreach ($fileNamespace->getFunctions() as $parsedFunction) {
$refFunction = new \ReflectionFunction($parsedFunction->getName());
yield $prefix . ' ' . $refFunction ->getName() => [$parsedFunction, $refFunction];
}
}
}
/**
* Returns list of ReflectionMethod getters that be checked directly without additional arguments
*/
abstract static protected function getGettersToCheck(): array;
/**
* Setups file for parsing
*/
protected function setUpFile(string $fileName): void
{
$fileName = stream_resolve_include_path($fileName);
$fileNode = ReflectionEngine::parseFile($fileName);
$reflectionFile = new ReflectionFile($fileName, $fileNode);
$this->parsedRefFile = $reflectionFile;
$parsedFileNamespace = $reflectionFile->getFileNamespace('Go\ParserReflection\Stub');
$this->parsedRefFileNamespace = $parsedFileNamespace;
if ($parsedFileNamespace->hasClass(static::$defaultClassToLoad)) {
$this->parsedRefClass = $parsedFileNamespace->getClass(static::$defaultClassToLoad);
}
include_once $fileName;
}
protected function setUp(): void
{
$this->setUpFile(__DIR__ . static::DEFAULT_STUB_FILENAME);
}
}
tests/Locator/CallableLocatorTest.php 0000644 00000000726 14641221054 0013711 0 ustar 00 ltrim($class, '\\') . '.php';
$locator = new CallableLocator($callable);
$this->assertSame('class.php', $locator->locateClass('class'));
$this->assertSame('class.php', $locator->locateClass('\class'));
}
}
tests/Locator/ComposerLocatorTest.php 0000644 00000001767 14641221054 0014007 0 ustar 00 assertSame(
$reflectionClass->getFileName(),
$locator->locateClass(ReflectionClass::class)
);
$this->assertSame(
$reflectionClass->getFileName(),
$locator->locateClass('\\' . ReflectionClass::class)
);
}
public function testLocateClassWithAttributes(): void
{
ReflectionEngine::init(new ComposerLocator());
$parsedClass = new \Go\ParserReflection\ReflectionClass(\Go\ParserReflection\Stub\RandomClassWithAttribute::class);
$this->assertIsArray($parsedClass->getAttributes());
}
}
tests/ReflectionAttributesTest.php 0000644 00000017534 14641221054 0013431 0 ustar 00 setUpFile(__DIR__ . '/Stub/FileWithFunction80.php');
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$function = $fileNamespace->getFunction('function_with_attribute');
$attributes = $function->getAttributes();
$originalReflection = new \ReflectionFunction('Go\ParserReflection\Stub\function_with_attribute');
foreach ($attributes as $attribute) {
$originalAttribute = current($originalReflection->getAttributes($attribute->getName()));
$this->assertInstanceOf(ReflectionAttribute::class, $attribute);
$this->assertInstanceOf(Attribute::class, $attribute->getNode());
$this->assertSame($originalAttribute->getName(), $attribute->getName());
$this->assertSame($originalAttribute->getArguments(), $attribute->getArguments());
$this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated());
}
}
public function testGetAttributeOnClassMethod(): void
{
$this->setUpFile(__DIR__ . '/Stub/FileWithClasses80.php');
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$class = $fileNamespace->getClass(ClassPHP80WithAttribute::class);
foreach ($class->getMethods() as $method) {
$attributes = $method->getAttributes();
$originalReflection = new \ReflectionMethod(ClassPHP80WithAttribute::class, $method->getName());
foreach ($attributes as $attribute) {
$originalAttribute = current($originalReflection->getAttributes($attribute->getName()));
$this->assertInstanceOf(ReflectionAttribute::class, $attribute);
$this->assertInstanceOf(Attribute::class, $attribute->getNode());
$this->assertSame($originalAttribute->getName(), $attribute->getName());
$this->assertSame($originalAttribute->getArguments(), $attribute->getArguments());
$this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated());
}
$this->assertSame($originalReflection->__toString(), $method->__toString());
}
}
public function testGetAttributeOnParameters(): void
{
$this->setUpFile(__DIR__ . '/Stub/FileWithParameters80.php');
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$parameters = $fileNamespace->getFunction('authenticate')->getParameters();
foreach ($parameters as $parameter) {
$attributes = $parameter->getAttributes();
$originalReflection = new \ReflectionParameter('Go\ParserReflection\Stub\authenticate', $parameter->getName());
foreach ($attributes as $attribute) {
$originalAttribute = current($originalReflection->getAttributes($attribute->getName()));
$this->assertInstanceOf(ReflectionAttribute::class, $attribute);
$this->assertInstanceOf(Attribute::class, $attribute->getNode());
$this->assertSame($originalAttribute->getName(), $attribute->getName());
$this->assertSame($originalAttribute->getArguments(), $attribute->getArguments());
$this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated());
}
$this->assertSame($originalReflection->__toString(), $parameter->__toString());
}
}
public function testGetAttributeOnClassConst(): void
{
$this->setUpFile(__DIR__ . '/Stub/FileWithClasses80.php');
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$constants = $fileNamespace->getClass(ClassPHP80WithAttribute::class)->getConstants();
foreach (array_keys($constants) as $constant) {
$reflectionClassConst = new ReflectionClassConstant(ClassPHP80WithAttribute::class, $constant);
$attributes = $reflectionClassConst->getAttributes();
$originalReflection = new \ReflectionClassConstant(ClassPHP80WithAttribute::class, $constant);
foreach ($attributes as $key => $attribute) {
$originalAttributes = $originalReflection->getAttributes($attribute->getName());
$this->assertInstanceOf(ReflectionAttribute::class, $attribute);
$this->assertInstanceOf(Attribute::class, $attribute->getNode());
$this->assertSame(current($originalAttributes)->getName(), $attribute->getName());
$this->assertSame(current($originalAttributes)->isRepeated(), $attribute->isRepeated());
// test repeated on constant stub
if ($attribute->isRepeated()) {
$this->assertSame($originalAttributes[$key]->getArguments(), $attribute->getArguments());
}
}
$this->assertSame($originalReflection->__toString(), $reflectionClassConst->__toString());
}
}
public function testGetAttributeOnClass(): void
{
$this->setUpFile(__DIR__ . '/Stub/FileWithClasses80.php');
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$class = $fileNamespace->getClass(ClassPHP80WithAttribute::class);
$attributes = $class->getAttributes();
$originalReflection = new \ReflectionClass($class->getName());
foreach ($attributes as $attribute) {
$originalAttribute = current($originalReflection->getAttributes($attribute->getName()));
$this->assertInstanceOf(ReflectionAttribute::class, $attribute);
$this->assertInstanceOf(Attribute::class, $attribute->getNode());
$this->assertSame($originalAttribute->getName(), $attribute->getName());
$this->assertSame($originalAttribute->getArguments(), $attribute->getArguments());
$this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated());
}
}
public function testGetAttributeOnProperty(): void
{
$this->setUpFile(__DIR__ . '/Stub/FileWithClasses80.php');
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$properties = $fileNamespace->getClass(ClassPHP80WithAttribute::class)->getProperties();
foreach ($properties as $property) {
$attributes = $property->getAttributes();
$originalReflection = new \ReflectionProperty(ClassPHP80WithAttribute::class, $property->getName());
foreach ($attributes as $attribute) {
$originalAttribute = current($originalReflection->getAttributes($attribute->getName()));
$this->assertInstanceOf(ReflectionAttribute::class, $attribute);
$this->assertInstanceOf(Attribute::class, $attribute->getNode());
$this->assertSame($originalAttribute->getName(), $attribute->getName());
$this->assertSame($originalAttribute->getArguments(), $attribute->getArguments());
$this->assertSame($originalAttribute->isRepeated(), $attribute->isRepeated());
}
$this->assertSame($originalReflection->__toString(), $property->__toString());
}
}
/**
* Setups file for parsing
*/
private function setUpFile(string $fileName): void
{
$fileName = stream_resolve_include_path($fileName);
$fileNode = ReflectionEngine::parseFile($fileName);
$reflectionFile = new ReflectionFile($fileName, $fileNode);
$this->parsedRefFile = $reflectionFile;
include_once $fileName;
}
}
tests/ReflectionClassConstantTest.php 0000644 00000007035 14641221054 0014055 0 ustar 00 getName();
$className = $parsedClass->getName();
$parsedConstant = $parsedClass->getReflectionConstant($constantName);
$expectedValue = $refClassConstant->$getterName();
$actualValue = $parsedConstant->$getterName();
// I would like to completely stop maintaining the __toString method
if ($expectedValue !== $actualValue && $getterName === '__toString') {
$this->markTestSkipped("__toString for constant {$className}::{$constantName} is not equal:\n{$expectedValue}{$actualValue}");
}
if ($getterName === 'getValue' && $parsedClass->isEnum()) {
$this->markTestSkipped("getValue() for Enum cases could not be resolved, see https://github.com/goaop/parser-reflection/issues/132");
}
$this->assertSame(
$expectedValue,
$actualValue,
"$getterName() for constant $className::$constantName() should be equal"
);
}
/**
* Provides full test-case list in the form [ReflectionClass, \ReflectionClassConstant, getter name to check]
*/
public static function reflectionGetterDataProvider(): \Generator
{
$allNameGetters = self::getGettersToCheck();
foreach (self::classConstantsDataProvider() as $prefix => [$parsedClass, $classReflectionConstant]) {
foreach ($allNameGetters as $getterName) {
yield $prefix . ', ' . $getterName => [
$parsedClass,
$classReflectionConstant,
$getterName
];
}
}
}
/**
* Provides full test-case list in the form [ParsedClass, \ReflectionClassConstant to check]
*/
public static function classConstantsDataProvider(): \Generator
{
foreach (self::classesDataProvider() as $prefix => [$parsedClass, $refClass]) {
foreach ($refClass->getReflectionConstants() as $reflectionConstant) {
$fullConstantName = $parsedClass->getName() . '::' . $reflectionConstant->getName();
yield $prefix . ' ' . $fullConstantName => [
$parsedClass,
$reflectionConstant,
];
}
}
}
/**
* @inheritDoc
*/
static protected function getGettersToCheck(): array
{
$php83Getters = [];
if (PHP_VERSION_ID >= 80300) {
$php83Getters[] = 'hasType';
}
return [
'getDocComment', 'getModifiers', 'getName', 'getValue',
'isPrivate', 'isProtected', 'isPublic', 'isFinal', 'isEnumCase',
'__toString', ...$php83Getters
];
}
}
tests/ReflectionClassTest.php 0000644 00000035274 14641221054 0012351 0 ustar 00 getModifiers() & $mask;
$originalModifiers = $originalRefClass->getModifiers() & $mask;
$this->assertSame(
$originalModifiers,
$parsedModifiers,
"Modifiers for the {$parsedRefClass->name} should be equal"
);
}
/**
* Performs method-by-method comparison with original reflection
*/
#[DataProvider('reflectionGetterDataProvider')]
public function testReflectionGetterParity(
ReflectionClass $parsedClass,
\ReflectionClass $refClass,
string $getterName
): void {
$className = $parsedClass->getName();
$expectedValue = $refClass->$getterName();
$actualValue = $parsedClass->$getterName();
// I would like to completely stop maintaining the __toString method
if ($expectedValue !== $actualValue && $getterName === '__toString') {
$this->markTestSkipped("__toString for class {$className} is not equal:\n{$expectedValue}{$actualValue}");
}
// For Enum it isn't possible to statically resolve constants as well
if ($parsedClass->isEnum() && $getterName === 'getConstants') {
$this->markTestSkipped(
"getConstants for enum {$className} couldn't be resolved fully from AST.\n" .
"See https://github.com/goaop/parser-reflection/issues/132"
);
}
$this->assertSame(
$expectedValue,
$actualValue,
"{$getterName}() for class {$className} should be equal"
);
}
/**
* Provides full test-case list in the form [ReflectionClass, \ReflectionClass, getter name to check]
*/
public static function reflectionGetterDataProvider(): \Generator
{
$allNameGetters = self::getGettersToCheck();
foreach (self::classesDataProvider() as $prefix => [$parsedClass, $originalClass]) {
foreach ($allNameGetters as $getterName) {
yield $prefix . ', ' . $getterName => [
$parsedClass,
$originalClass,
$getterName
];
}
}
}
/**
* Tests getMethods() returns correct number of methods for the class
*/
#[DataProvider('classesDataProvider')]
public function testGetMethodCount(
ReflectionClass $parsedRefClass,
\ReflectionClass $originalRefClass
): void {
$parsedMethods = $parsedRefClass->getMethods();
$originalMethods = $originalRefClass->getMethods();
if ($parsedRefClass->getTraitAliases()) {
$this->markTestIncomplete("Adoptation methods for traits are not supported yet");
}
$this->assertCount(count($originalMethods), $parsedMethods);
}
/**
* Tests getReflectionConstants() returns correct number of reflectionConstants for the class
*/
#[DataProvider('classesDataProvider')]
public function testGetReflectionConstantCount(
ReflectionClass $parsedRefClass,
\ReflectionClass $originalRefClass
): void {
$parsedReflectionConstants = $parsedRefClass->getReflectionConstants();
$originalReflectionConstants = $originalRefClass->getReflectionConstants();
$this->assertCount(count($originalReflectionConstants), $parsedReflectionConstants);
}
/**
* Tests getProperties() returns correct number of properties for the class
*/
#[DataProvider('classesDataProvider')]
public function testGetProperties(
ReflectionClass $parsedRefClass,
\ReflectionClass $originalRefClass
): void {
$parsedProperties = $parsedRefClass->getProperties();
$originalProperties = $originalRefClass->getProperties();
$this->assertCount(count($originalProperties), $parsedProperties, "Count of properties for " . $originalRefClass->getName() . " should match");
}
/**
* Tests getInterfaces() returns correct number of interfaces for the class
*/
#[DataProvider('classesDataProvider')]
public function testGetInterfaces(
ReflectionClass $parsedRefClass,
\ReflectionClass $originalRefClass
): void {
$parsedInterfaces = $parsedRefClass->getInterfaces();
$originalInterfaces = $originalRefClass->getInterfaces();
$this->assertCount(count($originalInterfaces), $parsedInterfaces, "Count of interfaces for " . $originalRefClass->getName() . " should match");
}
/**
* Tests getConstructor() returns instance of ReflectionMethod for constructor
*/
#[DataProvider('classesDataProvider')]
public function testGetConstructor(
ReflectionClass $parsedRefClass,
\ReflectionClass $originalRefClass
): void {
$hasConstructor = $originalRefClass->hasMethod('__construct');
$this->assertSame(
$hasConstructor,
$parsedRefClass->hasMethod('__construct')
);
if ($hasConstructor) {
$parsedConstructor = $parsedRefClass->getConstructor();
$originalConstructor = $originalRefClass->getConstructor();
$this->assertSame($originalConstructor->getDeclaringClass()->name, $parsedConstructor->getDeclaringClass()->name);
}
}
/**
* Tests getParentClass() returns instance of ReflectionClass
*/
#[DataProvider('classesDataProvider')]
public function testGetParentClass(
ReflectionClass $parsedRefClass,
\ReflectionClass $originalRefClass
): void {
$originalParentClass = $originalRefClass->getParentClass();
$parsedParentClass = $parsedRefClass->getParentClass();
if (!$originalParentClass) {
$this->assertSame($originalParentClass, $parsedParentClass);
}
if ($originalParentClass) {
$this->assertSame($originalParentClass->getName(), $parsedParentClass->getName());
}
}
public function testNewInstanceMethod(): void
{
$parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp50FinalKeyword::class);
$instance = $parsedRefClass->newInstance();
$this->assertInstanceOf(ClassWithPhp50FinalKeyword::class, $instance);
$this->assertSame([], $instance->args);
}
public function testNewInstanceArgsMethod(): void
{
$someValueByRef = 5;
$arguments = [1, &$someValueByRef];
$parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp50FinalKeyword::class);
$instance = $parsedRefClass->newInstanceArgs($arguments);
$this->assertInstanceOf(ClassWithPhp50FinalKeyword::class, $instance);
$this->assertSame($arguments, $instance->args);
}
public function testNewInstanceWithoutConstructorMethod(): void
{
$arguments = [1, 2];
$parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp50FinalKeyword::class);
$instance = $parsedRefClass->newInstanceWithoutConstructor($arguments);
$this->assertInstanceOf(ClassWithPhp50FinalKeyword::class, $instance);
$this->assertSame([], $instance->args);
}
public function testSetStaticPropertyValueMethod(): void
{
$parsedRefClass1 = $this->parsedRefFileNamespace->getClass(ClassWithPhp50ConstantsAndInheritance::class);
$originalRefClass1 = new \ReflectionClass(ClassWithPhp50ConstantsAndInheritance::class);
$parsedRefClass2 = $this->parsedRefFileNamespace->getClass(ClassWithPhp50MagicConstants::class);
$originalRefClass2 = new \ReflectionClass(ClassWithPhp50MagicConstants::class);
$defaultProp1Value = $originalRefClass1->getStaticPropertyValue('h');
$defaultProp2Value = $originalRefClass2->getStaticPropertyValue('a');
$ex = null;
try {
$this->assertEqualsWithDelta(M_PI, $parsedRefClass1->getStaticPropertyValue('h'), 0.0001, 'Close to expected value of M_PI');
$this->assertEqualsWithDelta(M_PI, $originalRefClass1->getStaticPropertyValue('h'), 0.0001, 'Close to expected value of M_PI');
$this->assertEquals(
realpath(dirname(__DIR__ . parent::DEFAULT_STUB_FILENAME)),
realpath($parsedRefClass2->getStaticPropertyValue('a')),
'Expected value');
$this->assertEquals(
$originalRefClass2->getStaticPropertyValue('a'),
$parsedRefClass2->getStaticPropertyValue('a'),
'Same as native implementation');
$parsedRefClass1->setStaticPropertyValue('h', 'test');
$parsedRefClass2->setStaticPropertyValue('a', 'different value');
$this->assertSame('test', $parsedRefClass1->getStaticPropertyValue('h'));
$this->assertSame('test', $originalRefClass1->getStaticPropertyValue('h'));
$this->assertSame('different value', $parsedRefClass2->getStaticPropertyValue('a'));
$this->assertSame('different value', $originalRefClass2->getStaticPropertyValue('a'));
}
catch (\Exception $e) {
$ex = $e;
}
// I didn't want to write a tearDown() for one test.
$originalRefClass1->setStaticPropertyValue('h', $defaultProp1Value);
$originalRefClass2->setStaticPropertyValue('a', $defaultProp2Value);
if ($ex) {
throw $ex;
}
}
public function testGetMethodsFiltering(): void
{
$parsedRefClass = $this->parsedRefFileNamespace->getClass(SimplePhp50ClassWithMethodsAndProperties::class);
$originalRefClass = new \ReflectionClass(SimplePhp50ClassWithMethodsAndProperties::class);
$parsedMethods = $parsedRefClass->getMethods(\ReflectionMethod::IS_PUBLIC);
$originalMethods = $originalRefClass->getMethods(\ReflectionMethod::IS_PUBLIC);
$this->assertCount(count($originalMethods), $parsedMethods);
$parsedMethods = $parsedRefClass->getMethods(\ReflectionMethod::IS_PRIVATE | \ReflectionMethod::IS_STATIC);
$originalMethods = $originalRefClass->getMethods(\ReflectionMethod::IS_PRIVATE | \ReflectionMethod::IS_STATIC);
$this->assertCount(count($originalMethods), $parsedMethods);
}
public function testDirectMethods(): void
{
$parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp50ImplicitAbstractKeyword::class);
$originalRefClass = new \ReflectionClass(ClassWithPhp50ImplicitAbstractKeyword::class);
$this->assertEquals($originalRefClass->hasMethod('test'), $parsedRefClass->hasMethod('test'));
$this->assertCount(count($originalRefClass->getMethods()), $parsedRefClass->getMethods());
$originalMethodName = $originalRefClass->getMethod('test')->getName();
$parsedMethodName = $parsedRefClass->getMethod('test')->getName();
$this->assertSame($originalMethodName, $parsedMethodName);
}
public function testInheritedMethods(): void
{
$this->markTestIncomplete("See https://github.com/goaop/parser-reflection/issues/55");
$parsedRefClass = $this->parsedRefFileNamespace->getClass(SimplePhp50AbstractClassInheritance::class);
$originalRefClass = new \ReflectionClass(SimplePhp50AbstractClassInheritance::class);
$this->assertEquals($originalRefClass->hasMethod('test'), $parsedRefClass->hasMethod('test'));
}
public function testHasConstant(): void
{
$parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp50ScalarConstants::class);
$originalRefClass = new \ReflectionClass(ClassWithPhp50ScalarConstants::class);
$this->assertSame($originalRefClass->hasConstant('D'), $parsedRefClass->hasConstant('D'));
$this->assertSame($originalRefClass->hasConstant('E'), $parsedRefClass->hasConstant('E'));
}
public function testGetConstant(): void
{
$parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp50ScalarConstants::class);
$originalRefClass = new \ReflectionClass(ClassWithPhp50ScalarConstants::class);
$this->assertSame($originalRefClass->getConstant('D'), $parsedRefClass->getConstant('D'));
$this->assertSame($originalRefClass->getConstant('E'), $parsedRefClass->getConstant('E'));
}
public function testGetReflectionConstant(): void
{
$parsedRefClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp50ScalarConstants::class);
$originalRefClass = new \ReflectionClass(ClassWithPhp50ScalarConstants::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
*/
protected static function getGettersToCheck(): array
{
return [
'getFileName', 'getStartLine', 'getEndLine', 'getDocComment', 'getExtension', 'getExtensionName',
'getName', 'getNamespaceName', 'getShortName', 'inNamespace',
'isAbstract', 'isCloneable', 'isFinal', 'isInstantiable', 'isReadOnly',
'isInterface', 'isInternal', 'isIterateable', 'isIterable', 'isTrait', 'isUserDefined',
'getConstants', 'getTraitNames', 'getInterfaceNames', 'getStaticProperties',
'getDefaultProperties', 'getTraitAliases', 'isEnum'
];
}
}
tests/ReflectionFileNamespaceTest.php 0000644 00000020054 14641221054 0013766 0 ustar 00 getFileNamespace('Go\ParserReflection\Stub');
$this->parsedRefFileNamespace = $parsedFileNamespace;
include_once $fileName;
}
public function testGetClass(): void
{
$refClass = $this->parsedRefFileNamespace->getClass(TestNamespaceClassFoo::class);
$this->assertInstanceOf(\ReflectionClass::class, $refClass);
$this->assertSame(TestNamespaceClassFoo::class, $refClass->name);
$this->expectException(ReflectionException::class);
$this->expectExceptionMessageMatches('/^Could not find the class Unknown in the file/');
$this->parsedRefFileNamespace->getClass('Unknown');
}
public function testGetClasses(): void
{
$refClasses = $this->parsedRefFileNamespace->getClasses();
$this->assertCount(2, $refClasses);
}
public function testGetConstant(): void
{
$constValue = $this->parsedRefFileNamespace->getConstant('NAMESPACE_NAME');
$this->assertNotFalse($constValue);
$this->assertSame(\Go\ParserReflection\Stub\NAMESPACE_NAME, $constValue);
$this->expectException(ReflectionException::class);
$this->expectExceptionMessageMatches('/^Could not find the constant Unknown in the file/');
$this->parsedRefFileNamespace->getConstant('Unknown');
}
public function testGetConstants(): void
{
$constValue = $this->parsedRefFileNamespace->getConstants(true);
$this->assertEquals(
array(
'START_MARKER' => 9,
'NAMESPACE_NAME' => 'Go\ParserReflection\Stub',
'FILE_NAME' => __DIR__ . self::STUB_FILE,
'END_MARKER' => 26,
'INT_CONST' => 5,
),
$constValue
);
$constValue = $this->parsedRefFileNamespace->getConstants(false);
$this->assertNotFalse($constValue);
$this->assertEquals(
array(
'START_MARKER' => 9,
'NAMESPACE_NAME' => 'Go\ParserReflection\Stub',
'FILE_NAME' => __DIR__ . self::STUB_FILE,
'END_MARKER' => 26,
),
$constValue
);
}
public function testGetConstantsCacheIndependence(): void
{
$globalConstants = $this->parsedRefFileNamespace->getConstants(true);
$this->assertArrayHasKey('FILE_NAME', $globalConstants, 'Namespaced constant found.');
$this->assertArrayHasKey('INT_CONST', $globalConstants, 'Global constant found.');
$constants = $this->parsedRefFileNamespace->getConstants();
$this->assertArrayHasKey('FILE_NAME', $constants, 'Namespaced constant found.');
$this->assertArrayNotHasKey('INT_CONST', $constants, 'Global constant not found.');
$this->assertNotEmpty($this->parsedRefFileNamespace->getConstant('FILE_NAME'), 'Namespaced constant found.');
$this->assertTrue($this->parsedRefFileNamespace->hasConstant('FILE_NAME'), 'Namespaced constant found.');
$this->assertFalse($this->parsedRefFileNamespace->hasConstant('INT_CONST'), 'Global constant not found.');
$this->expectException(ReflectionException::class);
$this->expectExceptionMessageMatches('/^Could not find the constant INT_CONST in the file/');
$this->parsedRefFileNamespace->getConstant('INT_CONST');
}
public function testGetGlobalConstants(): void
{
$fileName = stream_resolve_include_path(__DIR__ . self::STUB_GLOBAL_FILE);
$reflectionFile = new ReflectionFile($fileName);
$reflectionFileNamespace = $reflectionFile->getFileNamespace('');
$this->assertSame(
[
// Scalar types are handled.
'INT_CONST' => 5,
'STRING_CONST' => 'text',
'BOOLEAN_CONST' => true,
// Expressions are handled partially.
'EXPRESSION_CONST' => false,
'FUNCTION_CONST' => mktime(hour: 12, minute: 33, second: 00),
'AAAAAAAAAA' => true
],
$reflectionFileNamespace->getConstants(true)
);
}
public function testGetDocComment(): void
{
$docComment = $this->parsedRefFileNamespace->getDocComment();
$this->assertNotEmpty($docComment);
}
public function testGetEndLine(): void
{
$endLine = $this->parsedRefFileNamespace->getEndLine();
$this->assertSame(\Go\ParserReflection\Stub\END_MARKER + 1, $endLine);
}
public function testGetFileName(): void
{
$fileName = $this->parsedRefFileNamespace->getFileName();
$this->assertEquals(\Go\ParserReflection\Stub\FILE_NAME, $fileName);
}
public function testGetFunction(): void
{
$refFunction = $this->parsedRefFileNamespace->getFunction('testFunctionBar');
$this->assertInstanceOf(\ReflectionFunction::class, $refFunction);
$this->assertSame('testFunctionBar', $refFunction->name);
$this->expectException(ReflectionException::class);
$this->expectExceptionMessageMatches('/^Could not find the function Unknown in the file/');
$this->parsedRefFileNamespace->getFunction('Unknown');
}
public function testGetFunctions(): void
{
$refFunctions = $this->parsedRefFileNamespace->getFunctions();
$this->assertCount(1, $refFunctions);
}
public function testGetName(): void
{
$namespaceName = $this->parsedRefFileNamespace->getName();
$this->assertSame(\Go\ParserReflection\Stub\NAMESPACE_NAME, $namespaceName);
}
public function testGetNameOfGlobalNamespace(): void
{
$fileName = stream_resolve_include_path(__DIR__ . self::STUB_GLOBAL_FILE);
$reflectionFile = new ReflectionFile($fileName);
$reflectionFileNamespace = $reflectionFile->getFileNamespace('');
$this->assertSame('', $reflectionFileNamespace->getName());
}
public function testGetNamespaceAliases(): void
{
$expectedAliases = [
'SomeClass\WithoutAlias' => 'WithoutAlias',
'ReflectionClass' => 'UnusedReflectionClass',
'PhpParser\Node' => 'UnusedNode',
'PhpParser\Node\Expr' => 'UnusedNodeExpr'
];
$realAliases = $this->parsedRefFileNamespace->getNamespaceAliases();
$this->assertSame($expectedAliases, $realAliases);
}
public function testGetStartLine(): void
{
$startLine = $this->parsedRefFileNamespace->getStartLine();
$this->assertSame(\Go\ParserReflection\Stub\START_MARKER - 2, $startLine);
}
public function testHasClass(): void
{
$hasClass = $this->parsedRefFileNamespace->hasClass('Unknown');
$this->assertFalse($hasClass);
$hasClass = $this->parsedRefFileNamespace->hasClass(TestNamespaceClassFoo::class);
$this->assertTrue($hasClass);
}
public function testHasConstant(): void
{
$hasConstant = $this->parsedRefFileNamespace->hasConstant('Unknown');
$this->assertFalse($hasConstant);
$hasConstant = $this->parsedRefFileNamespace->hasConstant('NAMESPACE_NAME');
$this->assertTrue($hasConstant);
}
public function testHasFunction(): void
{
$hasFunction = $this->parsedRefFileNamespace->hasFunction('Unknown');
$this->assertFalse($hasFunction);
$hasFunction = $this->parsedRefFileNamespace->hasFunction('testFunctionBar');
$this->assertTrue($hasFunction);
}
}
tests/ReflectionFileTest.php 0000644 00000007200 14641221054 0012147 0 ustar 00 parsedRefFile = $reflectionFile;
}
public function testGetName(): void
{
$fileName = $this->parsedRefFile->getName();
$expectedName = stream_resolve_include_path(__DIR__ . self::STUB_FILE);
$this->assertEquals($expectedName, $fileName);
}
public function testGetFileNamespaces(): void
{
$reflectionFileNamespaces = $this->parsedRefFile->getFileNamespaces();
$this->assertCount(3, $reflectionFileNamespaces);
}
public function testGetFileNamespace(): void
{
$reflectionFileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$this->assertInstanceOf(ReflectionFileNamespace::class, $reflectionFileNamespace);
$this->expectException(ReflectionException::class);
$this->expectExceptionMessageMatches('/^Could not find the namespace Unknown in the file/');
$this->parsedRefFile->getFileNamespace('Unknown');
}
public function testHasFileNamespace(): void
{
$hasFileNamespace = $this->parsedRefFile->hasFileNamespace('Go\ParserReflection\Stub');
$this->assertTrue($hasFileNamespace);
$hasFileNamespace = $this->parsedRefFile->hasFileNamespace('Unknown');
$this->assertFalse($hasFileNamespace);
}
public function testGetGlobalFileNamespace(): void
{
$fileName = stream_resolve_include_path(__DIR__ . self::STUB_GLOBAL_FILE);
$reflectionFile = new ReflectionFile($fileName);
$reflectionFileNamespace = $reflectionFile->getFileNamespace('');
$this->assertInstanceOf(ReflectionFileNamespace::class, $reflectionFileNamespace);
}
/**
* Tests if strict mode detected correctly
*/
#[\PHPUnit\Framework\Attributes\DataProvider('fileNameProvider')]
public function testIsStrictType(string $fileName, bool $shouldBeStrict): void
{
$fileName = stream_resolve_include_path(__DIR__ . $fileName);
$reflectionFile = new ReflectionFile($fileName);
$this->assertSame($shouldBeStrict, $reflectionFile->isStrictMode());
}
public static function fileNameProvider(): \Iterator
{
yield '/Stub/FileWithClasses56.php' => ['/Stub/FileWithClasses56.php', false];
yield '/Stub/FileWithClasses70.php' => ['/Stub/FileWithClasses70.php', false];
yield '/Stub/FileWithClasses71.php' => ['/Stub/FileWithClasses71.php', true];
yield '/Stub/FileWithGlobalNamespace.php' => ['/Stub/FileWithGlobalNamespace.php', true];
}
public function testGetInterfaceNamesWithExtends(): void
{
$fileName = __DIR__ . '/Stub/Issue44/ClassWithoutNamespace.php';
require_once __DIR__ . '/Stub/Issue44/Locator.php';
ReflectionEngine::init(new Locator());
$reflectedFile = new ReflectionFile($fileName);
$namespaces = $reflectedFile->getFileNamespaces();
$namespace = array_pop($namespaces);
$classes = $namespace->getClasses();
$class = array_pop($classes);
$interfaceNames = $class->getInterfaceNames();
$this->assertSame([], $interfaceNames);
}
}
tests/ReflectionFunctionTest.php 0000644 00000011427 14641221054 0013063 0 ustar 00 getName();
$expectedValue = $refFunction->$getterName();
$actualValue = $parsedFunction->$getterName();
// I would like to completely stop maintaining the __toString method
if ($expectedValue !== $actualValue && $getterName === '__toString') {
$this->markTestSkipped("__toString for function {$functionName}() is not equal:\n{$expectedValue}{$actualValue}");
}
$this->assertSame(
$expectedValue,
$actualValue,
"$getterName() for function {$functionName}() should be equal"
);
}
public function testGetClosureMethod(): void
{
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$refFunc = $fileNamespace->getFunction('noGeneratorFunc');
$closure = $refFunc->getClosure();
$this->assertInstanceOf(\Closure::class, $closure);
$retValue = $closure();
$this->assertSame(100, $retValue);
}
public function testInvokeMethod(): void
{
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$refFunc = $fileNamespace->getFunction('funcWithReturnArgs');
$retValue = $refFunc->invoke(1, 2, 3);
$this->assertSame([1, 2, 3], $retValue);
}
public function testInvokeArgsMethod(): void
{
$fileNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$refFunc = $fileNamespace->getFunction('funcWithReturnArgs');
$retValue = $refFunc->invokeArgs([1, 2, 3]);
$this->assertSame([1, 2, 3], $retValue);
}
#[DataProvider('functionsDataProvider')]
public function testGetReturnTypeMethod(
ReflectionFunction $parsedRefFunction, \ReflectionFunction $originalRefFunction
): void {
$functionName = $parsedRefFunction->getName();
$hasReturnType = $originalRefFunction->hasReturnType();
$this->assertSame(
$originalRefFunction->hasReturnType(),
$parsedRefFunction->hasReturnType(),
"Presence of return type for function {$functionName} should be equal"
);
if ($hasReturnType) {
$parsedReturnType = $parsedRefFunction->getReturnType();
$originalReturnType = $originalRefFunction->getReturnType();
$this->assertSame($originalReturnType->allowsNull(), $parsedReturnType->allowsNull());
$this->assertSame($originalReturnType->__toString(), $parsedReturnType->__toString());
} else {
$this->assertSame(
$originalRefFunction->getReturnType(),
$parsedRefFunction->getReturnType()
);
}
}
/**
* Provides full test-case list in the form [ReflectionFunction, \ReflectionFunction, getter name to check]
*/
public static function reflectionGetterDataProvider(): \Generator
{
$allNameGetters = self::getGettersToCheck();
foreach (self::functionsDataProvider() as $prefix => [$parsedFunction, $refFunction]) {
foreach ($allNameGetters as $getterName) {
yield $prefix . ', ' . $getterName => [
$parsedFunction,
$refFunction,
$getterName
];
}
}
}
/**
* @inheritDoc
*/
static protected function getGettersToCheck(): array
{
return [
'getStartLine', 'getEndLine', 'getDocComment', 'getExtension', 'getExtensionName',
'getName', 'getNamespaceName', 'getShortName', 'inNamespace', 'getStaticVariables',
'getNumberOfParameters', 'getNumberOfRequiredParameters',
'returnsReference', 'getClosureScopeClass', 'getClosureThis', 'hasReturnType', '__toString'
];
}
}
tests/ReflectionMethodTest.php 0000644 00000015270 14641221054 0012516 0 ustar 00 getName();
$className = $parsedClass->getName();
$expectedValue = $refMethod->$getterName();
$actualValue = $parsedMethod->$getterName();
// I would like to completely stop maintaining the __toString method
if ($expectedValue !== $actualValue && $getterName === '__toString') {
$this->markTestSkipped("__toString for method {$className}::{$methodName} is not equal:\n{$expectedValue}{$actualValue}");
}
$this->assertSame(
$expectedValue,
$actualValue,
"$getterName() for method $className->$methodName() should be equal"
);
}
public function testGetClosureMethod(): void
{
$refMethod = $this->parsedRefClass->getMethod('funcWithDocAndBody');
$closure = $refMethod->getClosure(null);
$this->assertInstanceOf(\Closure::class, $closure);
$retValue = $closure();
$this->assertSame('hello', $retValue);
}
public function testInvokeMethod(): void
{
$refMethod = $this->parsedRefClass->getMethod('funcWithReturnArgs');
$retValue = $refMethod->invoke(null, 1, 2, 3);
$this->assertSame([1, 2, 3], $retValue);
}
public function testInvokeArgsMethod(): void
{
$refMethod = $this->parsedRefClass->getMethod('funcWithReturnArgs');
$retValue = $refMethod->invokeArgs(null, [1, 2, 3]);
$this->assertSame([1, 2, 3], $retValue);
}
public function testCallProtectedMethod(): void
{
$refMethod = $this->parsedRefClass->getMethod('protectedStaticFunc');
$retValue = $refMethod->invokeArgs(null, []);
$this->assertEquals(null, $retValue);
}
#[DataProvider('methodsDataProvider')]
public function testDebugInfoMethod(
ReflectionClass $parsedRefClass,
\ReflectionMethod|ReflectionMethod $parsedRefMethod,
\ReflectionMethod $originalRefMethod
): void {
$methodName = $originalRefMethod->getName();
$className = $parsedRefClass->getName();
if (!$parsedRefMethod instanceof ReflectionMethod) {
$this->markTestSkipped("Native reflection method {$className}->{$methodName}() represented similarly");
}
$expectedValue = (array) $originalRefMethod;
$this->assertSame($expectedValue, $parsedRefMethod->__debugInfo());
}
#[DataProvider('methodsDataProvider')]
public function testReturnTypeMethods(
ReflectionClass $parsedRefClass,
\ReflectionMethod|ReflectionMethod $parsedRefMethod,
\ReflectionMethod $originalRefMethod
): void {
$methodName = $originalRefMethod->getName();
$className = $parsedRefClass->getName();
$hasType = $parsedRefMethod->hasReturnType();
$this->assertSame(
$originalRefMethod->hasReturnType(),
$hasType,
"Presence of return type for method {$className}:{$methodName} should be equal"
);
$message= "Type information for {$className}::$methodName() not equals to the original reflection";
if ($hasType) {
$parsedType = $parsedRefMethod->getReturnType();
$originalType = $originalRefMethod->getReturnType();
$this->assertSame($originalType->allowsNull(), $parsedType->allowsNull(), $message);
$this->assertSame($originalType->__toString(), $parsedType->__toString(), $message);
} else {
$this->assertSame(
$originalRefMethod->getReturnType(),
$parsedRefMethod->getReturnType(),
$message
);
}
}
#[DataProvider('methodsDataProvider')]
public function testPrototypeMethods(
ReflectionClass $parsedRefClass,
\ReflectionMethod|ReflectionMethod $parsedRefMethod,
\ReflectionMethod $originalRefMethod
): void {
$methodName = $originalRefMethod->getName();
$className = $parsedRefClass->getName();
$hasPrototype = $parsedRefMethod->hasPrototype();
$this->assertSame(
$originalRefMethod->hasPrototype(),
$hasPrototype,
"Presence of prototype for method {$className}:{$methodName} should be equal"
);
$message= "Prototype information for {$className}::$methodName() not equals to the original reflection";
if ($hasPrototype) {
$parsedPrototype = $parsedRefMethod->getPrototype();
$originalPrototype = $originalRefMethod->getPrototype();
$this->assertSame($originalPrototype->getDeclaringClass()->getName(), $parsedPrototype->getDeclaringClass()->getName(), $message);
}
}
/**
* Provides full test-case list in the form [ReflectionClass, ReflectionMethod, \ReflectionMethod, getter name to check]
*/
public static function reflectionGetterDataProvider(): \Generator
{
$allNameGetters = self::getGettersToCheck();
foreach (self::methodsDataProvider() as $prefix => [$parsedClass, $parsedMethod, $classMethod]) {
foreach ($allNameGetters as $getterName) {
yield $prefix . ', ' . $getterName => [
$parsedClass,
$parsedMethod,
$classMethod,
$getterName
];
}
}
}
/**
* Returns list of ReflectionMethod getters that be checked directly without additional arguments
*/
protected static function getGettersToCheck(): array
{
return [
'getStartLine', 'getEndLine', 'getDocComment', 'getExtension', 'getExtensionName', 'getName',
'getNamespaceName', 'getShortName', 'inNamespace', 'getStaticVariables', 'isClosure', 'isDeprecated',
'isInternal', 'isUserDefined', 'isAbstract', 'isConstructor', 'isDestructor', 'isFinal', 'isPrivate',
'isProtected', 'isPublic', 'isStatic', 'isVariadic', 'isGenerator', 'getNumberOfParameters',
'getNumberOfRequiredParameters', 'returnsReference', 'getClosureScopeClass', 'getClosureThis',
'hasReturnType', '__toString'
];
}
}
tests/ReflectionParameterTest.php 0000644 00000022467 14641221054 0013224 0 ustar 00 getName();
if ($parsedFunctionAbstract instanceof \ReflectionMethod) {
$functionName = [$parsedFunctionAbstract->class, $parsedFunctionAbstract->getName()];
} else {
$functionName = $parsedFunctionAbstract->getName();
}
$expectedValue = $originalRefParameter->$getterName();
$actualValue = $parsedParameter->$getterName();
$displayableName = is_array($functionName) ? join ('->', $functionName) : $functionName;
// I would like to completely stop maintaining the __toString method
if ($expectedValue !== $actualValue && $getterName === '__toString') {
$this->markTestSkipped("__toString for parameter {$displayableName}(\${$parameterName}) is not equal:\n{$expectedValue}\n{$actualValue}");
}
$this->assertSame(
$expectedValue,
$actualValue,
"{$getterName}() for parameter {$displayableName}(\${$parameterName}) should be equal"
);
}
#[DataProvider('parametersDataProvider')]
public function testGetClassMethod(
\ReflectionFunctionAbstract $parsedFunction,
ReflectionParameter $parsedParameter,
\ReflectionParameter $originalRefParameter
): void {
$originalParamClass = $originalRefParameter->getClass();
$parsedParamClass = $parsedParameter->getClass();
if (isset($originalParamClass)) {
$this->assertNotNull($parsedParamClass, "Original param class is: {$originalParamClass->name}");
$this->assertSame($originalParamClass->getName(), $parsedParamClass->getName());
} else {
$this->assertNull($parsedParamClass);
}
}
#[DataProvider('parametersDataProvider')]
public function testGetDeclaringClassMethod(
\ReflectionFunctionAbstract $parsedFunction,
ReflectionParameter $parsedParameter,
\ReflectionParameter $originalRefParameter
): void {
$originalDeclaringClass = $originalRefParameter->getDeclaringClass();
$parsedDeclaringClass = $parsedParameter->getDeclaringClass();
if (isset($originalDeclaringClass)) {
$this->assertSame($originalDeclaringClass->getName(), $parsedDeclaringClass->getName());
} else {
$this->assertNull($parsedDeclaringClass);
}
}
#[DataProvider('parametersDataProvider')]
public function testDebugInfoMethod(
\ReflectionFunctionAbstract $parsedFunction,
ReflectionParameter $parsedParameter,
\ReflectionParameter $originalRefParameter
): void {
$expectedValue = (array) $originalRefParameter;
$this->assertSame($expectedValue, $parsedParameter->__debugInfo());
}
/**
* @param string $getterName Name of the getter to call
*/
#[DataProvider('listOfDefaultGetters')]
public function testGetDefaultValueThrowsAnException(string $getterName): void
{
$originalException = null;
$parsedException = null;
try {
$originalRefParameter = new \ReflectionParameter('Go\ParserReflection\Stub\miscParameters', 'arrayParam');
$originalRefParameter->$getterName();
} catch (\ReflectionException $e) {
$originalException = $e;
}
try {
$parsedNamespace = $this->parsedRefFile->getFileNamespace('Go\ParserReflection\Stub');
$parsedFunction = $parsedNamespace->getFunction('miscParameters');
$parsedRefParameters = $parsedFunction->getParameters();
$parsedRefParameter = $parsedRefParameters[0];
$parsedRefParameter->$getterName();
} catch (\ReflectionException $e) {
$parsedException = $e;
}
$this->assertInstanceOf(\ReflectionException::class, $originalException);
$this->assertInstanceOf(\ReflectionException::class, $parsedException);
$this->assertSame($originalException->getMessage(), $parsedException->getMessage());
}
public static function listOfDefaultGetters(): \Iterator
{
yield ['getDefaultValue'];
yield ['getDefaultValueConstantName'];
}
#[DataProvider('parametersDataProvider')]
public function testGetTypeMethod(
\ReflectionFunctionAbstract $parsedFunctionAbstract,
ReflectionParameter $parsedParameter,
\ReflectionParameter $originalRefParameter
): void {
$functionName = $parsedFunctionAbstract->getName();
$parameterName = $parsedParameter->getName();
$hasType = $originalRefParameter->hasType();
$this->assertSame(
$hasType,
$parsedParameter->hasType(),
"Presence of type for parameter {$functionName}:{$parameterName} should be equal"
);
$message= "Parameter $functionName:$parameterName not equals to the original reflection";
if ($hasType) {
$parsedReturnType = $parsedParameter->getType();
$originalReturnType = $originalRefParameter->getType();
$this->assertSame($originalReturnType->allowsNull(), $parsedReturnType->allowsNull(), $message);
$this->assertSame($originalReturnType->__toString(), $parsedReturnType->__toString(), $message);
} else {
$this->assertSame(
$originalRefParameter->getType(),
$parsedParameter->getType(),
$message
);
}
}
/**
* Provides full test-case list in the form [ReflectionClass, \ReflectionMethod, getter name to check]
*/
public static function reflectionGetterDataProvider(): \Generator
{
static $onlyWithDefaultValues = [
'getDefaultValue', 'getDefaultValueConstantName', 'isDefaultValueConstant'
];
$allNameGetters = self::getGettersToCheck();
foreach (self::parametersDataProvider() as $prefix => [$parsedFunction, $parsedParameter, $originalParameter]) {
foreach ($allNameGetters as $getterName) {
// We should ignore some methods if there isn't default value
$isDefaultValueAvailable = $originalParameter->isDefaultValueAvailable();
if (!$isDefaultValueAvailable && in_array($getterName, $onlyWithDefaultValues)) {
continue;
}
yield $prefix . ', ' . $getterName => [
$parsedFunction,
$parsedParameter,
$originalParameter,
$getterName
];
}
}
}
/**
* Provides generator list in the form [ReflectionFunctionAbstract, ReflectionParameter, \ReflectionParameter to check]
*/
public static function parametersDataProvider(): \Generator
{
foreach (self::methodsDataProvider() as $prefix => [$parsedClass, $parsedClassMethod]) {
if (get_class($parsedClassMethod) === \ReflectionMethod::class) {
// We don't want test again parent parameters from parent methods, which already loaded
continue;
}
foreach ($parsedClassMethod->getParameters() as $parsedMethodParameter) {
$paramName = $parsedMethodParameter->getName();
$refParameter = new \ReflectionParameter([$parsedClass->getName(), $parsedClassMethod->getName()], $paramName);
yield $prefix . ' ' . '($' . $paramName . ')' => [
$parsedClassMethod,
$parsedMethodParameter,
$refParameter
];
}
}
foreach (self::functionsDataProvider() as $prefix => [$parsedFunction, $refFunction]) {
foreach ($parsedFunction->getParameters() as $parsedFunctionParameter) {
$paramName = $parsedFunctionParameter->getName();
$refParameter = new \ReflectionParameter($parsedFunction->getName(), $paramName);
yield $prefix . ' ' . '($' . $paramName . ')' => [
$parsedFunction,
$parsedFunctionParameter,
$refParameter
];
}
}
}
/**
* @inheritDoc
*/
static protected function getGettersToCheck(): array
{
return [
'isOptional', 'isPassedByReference', 'isDefaultValueAvailable',
'getPosition', 'canBePassedByValue', 'allowsNull', 'getDefaultValue', 'getDefaultValueConstantName',
'isDefaultValueConstant', 'isVariadic', 'isPromoted', 'hasType', '__toString'
];
}
}
tests/ReflectionPropertyTest.php 0000644 00000016632 14641221054 0013125 0 ustar 00 getName();
$className = $parsedClass->getName();
$parsedProperty = $parsedClass->getProperty($propertyName);
$expectedValue = $refProperty->$getterName();
$actualValue = $parsedProperty->$getterName();
// I would like to completely stop maintaining the __toString method
if ($expectedValue !== $actualValue && $getterName === '__toString') {
$this->markTestSkipped("__toString for property {$className}->{$propertyName} is not equal:\n{$expectedValue}{$actualValue}");
}
$this->assertSame(
$expectedValue,
$actualValue,
"{$getterName}() for property {$className}->{$propertyName} should be equal"
);
}
/**
* Provides full test-case list in the form [ReflectionClass, \ReflectionProperty, getter name to check]
*/
public static function reflectionGetterDataProvider(): \Generator
{
$allNameGetters = self::getGettersToCheck();
foreach (self::propertiesDataProvider() as $prefix => [$parsedClass, $classProperty]) {
foreach ($allNameGetters as $getterName) {
// We could check isInitialized only for static properties
if ($getterName === 'isInitialized' && !$classProperty->isStatic()) {
continue;
}
yield $prefix . ', ' . $getterName => [
$parsedClass,
$classProperty,
$getterName
];
}
}
}
#[DataProvider('propertiesDataProvider')]
public function testGetDefaultValue(ReflectionClass $parsedRefClass, \ReflectionProperty $originalRefProperty): void
{
$propertyName = $originalRefProperty->getName();
$parsedProperty = $parsedRefClass->getProperty($propertyName);
$className = $parsedRefClass->getName();
$this->assertSame(
$originalRefProperty->hasDefaultValue(),
$parsedProperty->hasDefaultValue(),
"Presence of default value for property {$className}:{$propertyName} should be equal"
);
if ($originalRefProperty->isStatic()) {
$actualValue = $parsedProperty->getValue();
$this->assertSame($originalRefProperty->getValue(), $actualValue);
} elseif ($originalRefProperty->hasDefaultValue() && $parsedRefClass->isInstantiable()) {
$instance = $parsedRefClass->newInstanceWithoutConstructor();
$actualValue = $parsedProperty->getValue(); // Let's try not to fallback to internal reflection, pure AST
$this->assertSame($originalRefProperty->getValue($instance), $actualValue);
}
}
public function testGetSetValueForObjectMethods(): void
{
$parsedProperty = $this->parsedRefClass->getProperty('protectedProperty');
$className = $this->parsedRefClass->getName();
$obj = new $className;
$value = $parsedProperty->getValue($obj);
$this->assertSame('a', $value);
$parsedProperty->setValue($obj, 43);
$value = $parsedProperty->getValue($obj);
$this->assertSame(43, $value);
}
public function testCompatibilityWithOriginalConstructor(): void
{
$parsedRefProperty = new ReflectionProperty($this->parsedRefClass->getName(), 'publicStaticProperty');
$originalValue = $parsedRefProperty->getValue();
$this->assertSame(M_PI, $originalValue);
}
#[DataProvider('propertiesDataProvider')]
public function testDebugInfoMethod(ReflectionClass $parsedRefClass, \ReflectionProperty $originalRefProperty): void
{
$propertyName = $originalRefProperty->getName();
$className = $parsedRefClass->getName();
$parsedRefProperty = $parsedRefClass->getProperty($propertyName);
if (!$parsedRefProperty instanceof ReflectionProperty) {
$this->markTestSkipped("Native reflection property {$className}->{$propertyName} represented similarly");
}
$expectedValue = (array) $originalRefProperty;
$this->assertSame($expectedValue, $parsedRefProperty->__debugInfo());
}
#[DataProvider('propertiesDataProvider')]
public function testGetTypeMethod(ReflectionClass $parsedRefClass, \ReflectionProperty $originalRefProperty): void
{
$propertyName = $originalRefProperty->getName();
$className = $parsedRefClass->getName();
$parsedRefProperty = $parsedRefClass->getProperty($propertyName);
$hasType = $parsedRefProperty->hasType();
$this->assertSame(
$originalRefProperty->hasType(),
$hasType,
"Presence of type for property {$className}:{$propertyName} should be equal"
);
$message= "Type information for {$className}::$propertyName not equals to the original reflection";
if ($hasType) {
$parsedType = $parsedRefProperty->getType();
$originalType = $originalRefProperty->getType();
$this->assertSame($originalType->allowsNull(), $parsedType->allowsNull(), $message);
$this->assertSame($originalType->__toString(), $parsedType->__toString(), $message);
} else {
$this->assertSame(
$originalRefProperty->getType(),
$parsedRefProperty->getType(),
$message
);
}
}
/**
* Provides full test-case list in the form [ParsedClass, \ReflectionProperty to check]
*/
public static function propertiesDataProvider(): \Generator
{
foreach (self::classesDataProvider() as $prefix => [$parsedClass, $refClass]) {
foreach ($refClass->getProperties() as $classProperty) {
$fullPropertyName = $parsedClass->getName() . '->' . $classProperty->getName();
yield $prefix . ' ' . $fullPropertyName => [
$parsedClass,
$classProperty,
];
}
}
}
/**
* Returns list of ReflectionMethod getters that be checked directly without additional arguments
*/
protected static function getGettersToCheck(): array
{
return [
'isDefault', 'getName', 'getModifiers', 'getDocComment',
'isPrivate', 'isProtected', 'isPublic', 'isStatic', 'isReadOnly', 'isInitialized',
'hasType', 'hasDefaultValue', 'getDefaultValue', '__toString'
];
}
}
tests/ReflectionTypeTest.php 0000644 00000011633 14641221054 0012216 0 ustar 00 getMethod('acceptsDefaultString');
$this->assertInstanceOf(\ReflectionMethod::class, $nativeMethodRef);
$nativeParamRefArr = $nativeMethodRef->getParameters();
$this->assertCount(2, $nativeParamRefArr);
$this->assertInstanceOf(\ReflectionParameter::class, $nativeParamRefArr[0]);
$nativeTypeRef = $nativeParamRefArr[0]->getType();
$this->assertSame('string', $nativeTypeRef->getName());
$this->assertStringNotContainsString('\\', get_class($nativeTypeRef));
$this->assertInstanceOf(\ReflectionType::class, $nativeTypeRef);
$this->assertSame('string', \Go\ParserReflection\ReflectionType::convertToDisplayType($nativeTypeRef));
}
/**
* Testing convertToDisplayType() with native \ReflectionType
*
* We're already testing it with Go\ParserReflection\ReflectionType
* elsewhere.
*/
public function testTypeConvertToDisplayTypeWithNullableNativeType(): void
{
$nativeClassRef = new \ReflectionClass('Go\\ParserReflection\\Stub\\ClassWithPhp71NullableScalarTypeHints');
$nativeMethodRef = $nativeClassRef->getMethod('acceptsDefaultString');
$this->assertInstanceOf(\ReflectionMethod::class, $nativeMethodRef);
$nativeParamRefArr = $nativeMethodRef->getParameters();
$this->assertCount(2, $nativeParamRefArr);
$this->assertInstanceOf(\ReflectionParameter::class, $nativeParamRefArr[0]);
$nativeTypeRef = $nativeParamRefArr[0]->getType();
$this->assertSame('string', $nativeTypeRef->getName());
$this->assertStringNotContainsString('\\', get_class($nativeTypeRef));
$this->assertInstanceOf(\ReflectionType::class, $nativeTypeRef);
$this->assertSame('?string', \Go\ParserReflection\ReflectionType::convertToDisplayType($nativeTypeRef));
}
/**
* Testing convertToDisplayType() with native \ReflectionType
*
* We're already testing it with Go\ParserReflection\ReflectionType
* elsewhere.
*/
public function testTypeConvertToDisplayTypeImplicitlyNullable(): void
{
$nativeClassRef = new \ReflectionClass('Go\\ParserReflection\\Stub\\ClassWithPhp70ScalarTypeHints');
$nativeMethodRef = $nativeClassRef->getMethod('acceptsStringDefaultToNull');
$this->assertInstanceOf(\ReflectionMethod::class, $nativeMethodRef);
$nativeParamRefArr = $nativeMethodRef->getParameters();
$this->assertCount(1, $nativeParamRefArr);
$this->assertInstanceOf(\ReflectionParameter::class, $nativeParamRefArr[0]);
$nativeTypeRef = $nativeParamRefArr[0]->getType();
$this->assertTrue($nativeTypeRef->allowsNull());
$this->assertSame('string', $nativeTypeRef->getName());
$this->assertStringNotContainsString('\\', get_class($nativeTypeRef));
$this->assertInstanceOf(\ReflectionType::class, $nativeTypeRef);
$this->assertSame('?string', \Go\ParserReflection\ReflectionType::convertToDisplayType($nativeTypeRef));
}
/**
* Testing convertToDisplayType() with native \ReflectionType
*
* We're already testing it with Go\ParserReflection\ReflectionType
* elsewhere.
*/
public function testTypeConvertToDisplayTypeImplicitlyUnionNullable(): void
{
$nativeClassRef = new \ReflectionClass('Go\\ParserReflection\\Stub\\ClassWithPhp80Features');
$nativeMethodRef = $nativeClassRef->getMethod('acceptsStringArrayDefaultToNull');
$this->assertInstanceOf(\ReflectionMethod::class, $nativeMethodRef);
$nativeParamRefArr = $nativeMethodRef->getParameters();
$this->assertCount(1, $nativeParamRefArr);
$this->assertInstanceOf(\ReflectionParameter::class, $nativeParamRefArr[0]);
$nativeTypeRef = $nativeParamRefArr[0]->getType();
$this->assertTrue($nativeTypeRef->allowsNull());
$this->assertSame('array|string|null', (string) $nativeTypeRef);
$this->assertStringNotContainsString('\\', get_class($nativeTypeRef));
$this->assertInstanceOf(\ReflectionType::class, $nativeTypeRef);
$this->assertSame('array|string|null', \Go\ParserReflection\ReflectionType::convertToDisplayType($nativeTypeRef));
}
}
tests/Resolver/NodeExpressionResolverTest.php 0000644 00000005055 14641221054 0015553 0 ustar 00 parser = (new ParserFactory)->createForHostVersion();
}
/**
* Testing passing PhpParser\Node\Expr as class for constant fetch
*
* We're already testing constant fetch with a explicit class name
* elsewhere.
*/
public function testResolveConstFetchFromExpressionAsClass(): void
{
$expressionNodeTree = $this->parser->parse("process($expressionNodeTree[0]);
$this->assertSame(\DateTime::ATOM, $expressionSolver->getValue());
$this->assertTrue($expressionSolver->isConstant());
$this->assertSame('DateTime::ATOM', $expressionSolver->getConstantName());
}
/**
* Testing passing PhpParser\Node\Expr as class for constant fetch
*
* Evaluating a run-time value like a variable should throw an exception.
*/
public function testResolveConstFetchFromVariableAsClass(): void
{
$this->expectException(\Go\ParserReflection\ReflectionException::class);
$this->expectExceptionMessage('Could not find handler for the Go\ParserReflection\Resolver\NodeExpressionResolver::resolveExprVariable method');
$expressionNodeTree = $this->parser->parse("process($expressionNodeTree[0]);
}
/**
* Testing passing non-expression as class for constant fetch
*
* Non-expressions should be invalid.
*/
public function testResolveConstFetchFromNonExprAsClass(): void
{
$this->expectException(\Go\ParserReflection\ReflectionException::class);
$this->expectExceptionMessage('Could not find handler for the Go\ParserReflection\Resolver\NodeExpressionResolver::resolveStmtIf method');
$expressionNodeTree = $this->parser->parse("parser->parse("expr->class = $notAnExpressionNodeTree[0];
$expressionSolver = new NodeExpressionResolver(NULL);
$expressionSolver->process($expressionNodeTree[0]);
}
}
tests/Stub/FileWithClasses55.php 0000644 00000013473 14641221054 0012546 0 ustar 00 args = array_slice(array($a, &$b), 0, func_num_args());
}
}
class SimplePhp50BaseClass
{
protected static function prototypeMethod()
{
return __CLASS__;
}
}
/**
* @link https://bugs.php.net/bug.php?id=70957 self::class can not be resolved with reflection for abstract class
*/
abstract class SimplePhp50AbstractClassWithMethods extends SimplePhp50BaseClass
{
public const TEST = 5;
public function __construct(){}
public function __destruct(){}
public function explicitPublicFunc(){}
function implicitPublicFunc(){}
protected function protectedFunc(){}
private function privateFunc(){}
static function staticFunc(){}
protected static function protectedStaticFunc(){}
abstract function abstractFunc();
final function finalFunc(){}
/**
* @return string
*/
public static function funcWithDocAndBody()
{
static $a =5, $test = '1234';
return 'hello';
}
public static function funcWithReturnArgs($a, $b = 100, $c = 10.0)
{
return [$a, $b, $c];
}
public static function prototypeMethod()
{
return __CLASS__;
}
/**
* @return \Generator
*/
public function generatorYieldFunc()
{
$index = 0;
while ($index < 1e3) {
yield $index;
}
}
/**
* @return int
*/
public function noGeneratorFunc()
{
$gen = function () {
yield 10;
};
return 10;
}
private function testParam($a, $b = null, $d = self::TEST, $d2 = self::TEST . '_append', $e = M_PI, $f = \M_PI, $g = ['test'], $h = false, $i = true, $j = array('yoo')) {}
}
class SimplePhp50ClassWithProperties
{
private $privateProperty = 123;
protected $protectedProperty = 'a';
public $publicProperty = 42.0;
/**
* Some message to test docBlock
*
* @var int
*/
private static $privateStaticProperty = 1;
protected static $protectedStaticProperty = 'foo';
public static $publicStaticProperty = M_PI;
}
abstract class SimplePhp50ClassWithMethodsAndProperties
{
public $publicProperty;
protected $protectedProperty;
private $privateProperty;
static public $staticPublicProperty;
static protected $staticProtectedProperty;
static private $staticPrivateProperty;
public function publicMethod() {}
protected function protectedMethod() {}
private function privateMethod() {}
static public function publicStaticMethod() {}
static protected function protectedStaticMethod() {}
static private function privateStaticMethod() {}
abstract public function publicAbstractMethod();
abstract protected function protectedAbstractMethod();
final public function publicFinalMethod() {}
final protected function protectedFinalMethod() {}
}
/**
* @see https://www.php.net/manual/en/language.oop5.interfaces.php
*/
interface SimplePhp50Interface {}
interface SimplePhp50InterfaceWithMethod {
function foo();
}
/**
* @see https://www.php.net/manual/en/language.oop5.traits.php
*/
trait SimplePhp54Trait
{
function foo() { return __CLASS__; }
}
trait SimplePhp54ConflictedTrait
{
function foo() { return 'BAZ'; }
}
class SimplePhp50ClassInheritance extends ClassWithPhp50ExplicitAbstractKeyword {}
abstract class SimplePhp50AbstractClassInheritance extends ClassWithPhp50ImplicitAbstractKeyword
{
public $b = 'bar1';
public $d = 'foobar';
private $e = 'foobaz';
}
class ClassWithPhp50Interface implements SimplePhp50Interface {}
class ClassWithPhp54Trait
{
use SimplePhp54Trait;
}
/*
* Current implementation doesn't support trait adaptation,
* @see https://github.com/goaop/parser-reflection/issues/54
*
class ClassWithTraitAndAdaptation
{
use SimpleTrait {
foo as protected fooBar;
foo as private fooBaz;
}
}
class ClassWithTraitAndConflict
{
use SimpleTrait, ConflictedSimpleTrait {
foo as protected fooBar;
ConflictedSimpleTrait::foo insteadof SimpleTrait;
}
}
*/
/*
* Logic of prototype methods for interface and traits was changed since 7.0.6
* @see https://github.com/goaop/parser-reflection/issues/56
class ClassWithTraitAndInterface implements SimplePhp50InterfaceWithMethod
{
use SimplePhp54Trait;
}
*/
class SimplePhp50NoCloneableClass
{
private function __clone() {}
}
class SimplePhp50NoInstantiableClass
{
private function __construct() {}
}
class ClassWithPhp50ScalarConstants
{
const A = 10, A1 = 11;
const B = 42.0;
const C = 'foo';
const D = false;
const E = null;
}
class ClassWithPhp50MagicConstants
{
const A = __DIR__;
const B = __FILE__;
const C = __NAMESPACE__;
const D = __CLASS__;
const E = __LINE__;
public static $a = self::A;
protected static $b = self::B;
private static $c = self::C;
}
const NS_CONST = 'test';
class ClassWithPhp50ConstantsAndInheritance extends ClassWithPhp50MagicConstants
{
const A = 'overridden';
const H = M_PI;
const J = NS_CONST;
public static $h = self::H;
}
trait TraitWithPhp54Properties
{
private $a = 'foo';
protected $b = 'bar';
public $c = 'baz';
private static $as = 1;
protected static $bs = __TRAIT__;
public static $cs = 'foo';
}
tests/Stub/FileWithClasses56.php 0000644 00000004655 14641221054 0012551 0 ustar 00 > - Bitwise Shift Right
* . - Concatenation
* ?: - Ternary Operator
* <= - Smaller or Equal
* => - Greater or Equal
* == - Equal
* != - Not Equal
* < - Smaller
* > - Greater
* === - Identical
* !== - Not Identical
* && / and - Boolean AND
* || / or - Boolean OR
* xor - Boolean XOR
*
* Also supported is grouping static operations: (1 + 2) * 3.
*
* @see https://wiki.php.net/rfc/const_scalar_exprs
*/
class ClassWithPhp56ConstantExpressions
{
const ADDITION = ONE + 1;
const SUBTRACTION = 2 - ONE;
const MULTIPLICATION = ONE * 2;
const DIVISION = ONE / 2;
const MODULUS = ONE % 2;
const BOOLEAN_NEGATION = !ONE;
const BITWISE_NEGATION = ~ONE;
const BITWISE_OR = ONE | 2;
const BITWISE_AND = ONE & 2;
const BITWISE_XOR = ONE ^ 2;
const BITWISE_SHIFT_LEFT = ONE << 3;
const BITWISE_SHIFT_RIGHT = ONE >> 3;
const CONCATENATION = 'Value of shift left is ' . self::BITWISE_SHIFT_LEFT;
const TERNARY_OPERATOR = true ?: false;
const SMALLER_OR_EQUAL = ONE <= 2;
const GREATER_OR_EQUAL = ONE >= 2;
const EQUAL = ONE == true;
const NOT_EQUAL = ONE != 2;
const SMALLER = ONE < 2;
const GREATER = ONE > 2;
const IDENTICAL = ONE === true;
const NOT_IDENTICAL = ONE !== true;
const BOOLEAN_AND = ONE && false;
const LOGICAL_AND = ONE and false;
const BOOLEAN_OR = ONE || true;
const LOGICAL_OR = ONE or true;
const LOGICAL_XOR = ONE xor true;
const GROUPING = (ONE + 2) * 3;
const REFERENCE = self::ADDITION;
}
tests/Stub/FileWithClasses70.php 0000644 00000002254 14641221054 0012536 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
declare(strict_types=0);
namespace Go\ParserReflection\Stub;
// PHP 7+ code
use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P};
class ClassWithPhp70ScalarTypeHints
{
public function acceptsInteger(int $value) {}
public function acceptsString(string $value) {}
public function acceptsFloat(float $value) {}
public function acceptsBool(bool $value) {}
public function acceptsVariadicInteger(int ...$values) {}
public function acceptsDefaultString(string $class = ReflectionMethod::class, string $name = P::class) {}
public function acceptsStringDefaultToNull(string $someName = null) {}
}
class ClassWithPhp70ReturnTypeHints
{
public function returnsInteger() : int {}
public function returnsString() : string {}
public function returnsFloat() : float {}
public function returnsBool() : bool {}
public function returnsObject() : ReflectionMethod {}
public function returnsNamedObject() : P {}
}
tests/Stub/FileWithClasses71.php 0000644 00000004771 14641221054 0012545 0 ustar 00
*
* 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\Stub;
use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P};
// PHP 7.1+ code
/**
* @see https://www.php.net/manual/en/migration71.new-features.php#migration71.new-features.nullable-types
*/
class ClassWithPhp71NullableScalarTypeHints
{
public function acceptsInteger(?int $value) {}
public function acceptsString(?string $value) {}
public function acceptsFloat(?float $value) {}
public function acceptsBool(?bool $value) {}
public function acceptsVariadicInteger(?int ...$values) {}
public function acceptsDefaultString(?string $class = ReflectionMethod::class, ?string $name = P::class) {}
}
class ClassWithPhp71NullableReturnTypeHints
{
public function returnsInteger() : ?int {}
public function returnsString() : ?string {}
public function returnsFloat() : ?float {}
public function returnsBool() : ?bool {}
public function returnsObject() : ?ReflectionMethod {}
public function returnsNamedObject() : ?P {}
}
/**
* @see https://www.php.net/manual/en/migration71.new-features.php#migration71.new-features.class-constant-visibility
*/
class ClassWithPhp71ClassConstantVisibility
{
/**
* 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;
}
/**
* @see https://www.php.net/manual/en/migration71.new-features.php#migration71.new-features.void-functions
*/
class ClassWithPhp71VoidReturnType
{
public function returnsVoid(): void {}
}
/**
* @see https://www.php.net/manual/en/migration71.new-features.php#migration71.new-features.void-functions
*/
function functionWithPhp71VoidReturnType(): void {}
/**
* @see https://www.php.net/manual/en/language.types.iterable.php
*/
class ClassWithPhp71IterablePseudoType
{
public function acceptsIterable(iterable $iterable) : iterable {}
}
/**
* @see https://www.php.net/manual/en/language.types.iterable.php
*/
function functionWithPhp71IterableType(iterable $iterable) : iterable {}
tests/Stub/FileWithClasses72.php 0000644 00000002451 14641221054 0012537 0 ustar 00
*
* 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\Stub;
use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P};
// PHP 7.2+ code https://www.php.net/manual/en/migration72.php
/**
* @see https://wiki.php.net/rfc/object-typehint
*/
class ClassWithPhp72ObjectTypeHints
{
public function acceptsObject(object $object) {}
public function acceptsNullableObject(?object $object) {}
public function returnsObject(): object {}
public function returnsNullableObject(): ?object {}
}
/**
* @see https://www.php.net/manual/en/migration72.new-features.php#migration72.new-features.param-type-widening
* @see https://wiki.php.net/rfc/parameter-no-type-variance
*/
class ClassWithPhp72ParameterTypeWidening extends ClassWithPhp72ObjectTypeHints
{
public function acceptsObject($object) {}
}
/**
* @see https://wiki.php.net/rfc/parameter-no-type-variance
* @see https://www.php.net/manual/en/language.oop5.variance.php
*/
class ClassWithPhp72ReturnTypeNarrowing extends ClassWithPhp72ObjectTypeHints
{
public function returnsObject(): \Traversable {}
} tests/Stub/FileWithClasses74.php 0000644 00000003113 14641221054 0012535 0 ustar 00
*
* 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\Stub;
use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P};
// PHP 7.4+ code https://www.php.net/manual/en/migration74.php
/**
* @see https://php.watch/versions/7.4/typed-properties
*/
class ClassWithPhp74TypedProperties
{
public int $uninitializedIntProperty;
public string $uninitializedStringProperty;
public bool $uninitializedBoolProperty;
public float $uninitializedFloatProperty;
public array $uninitializedArrayProperty;
public object $uninitializedObjectProperty;
public \DateTime $uninitializedClassProperty;
public self $uninitializedSelfProperty;
public int $initializedIntProperty = 10;
public string $initializedStringProperty = 'foo';
public bool $initializedBoolProperty = true;
public float $initializedFloatProperty = 42.0;
public array $initializedArrayProperty = [10, 20];
public ?object $initializedNullableObjectProperty = null;
public static int $initializedStaticIntProperty = 10;
public static string $initializedStaticStringProperty = 'foo';
public static bool $initializedStaticBoolProperty = true;
public static float $initializedStaticFloatProperty = 42.0;
public static array $initializedStaticArrayProperty = [10, 20];
public static ?object $initializedStaticNullableObjectProperty = null;
}
tests/Stub/FileWithClasses80.php 0000644 00000006153 14641221054 0012541 0 ustar 00
*
* 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\Stub;
use Attribute;
use Go\ParserReflection\{ReflectionMethod, ReflectionProperty as P};
class ClassWithPhp80Features
{
public function acceptsStringArrayDefaultToNull(array|string $iterable = null) : array {}
}
/**
* @see https://php.watch/versions/8.0/named-parameters
*/
class ClassWithPHP80NamedCall
{
public static function foo(string $key1 = '', string $key2 = ''): string
{
return $key1 . ':' . $key2;
}
public static function namedCall(): array
{
return [
'key1' => self::foo(key1: 'bar'),
'key2' => self::foo(key2: 'baz'),
'keys' => self::foo(key1: 'A', key2: 'B'),
'reverseKeys' => self::foo(key2: 'A', key1: 'B'),
'unpack' => self::foo(...['key1' => 'C', 'key2' => 'D']),
];
}
}
/**
* @see https://php.watch/versions/8.0/attributes
*/
#[Attribute(Attribute::TARGET_ALL | Attribute::IS_REPEATABLE)]
readonly class ClassPHP80Attribute
{
private string $value;
public function __construct(string $value)
{
$this->value = $value;
}
public function getValue(): string
{
return $this->value;
}
}
/**
* @see https://php.watch/versions/8.0/attributes
*/
#[ClassPHP80Attribute('class')]
class ClassPHP80WithAttribute
{
#[ClassPHP80Attribute('first')]
#[ClassPHP80Attribute('second')]
public const PUBLIC_CONST = 1;
#[ClassPHP80Attribute('property')]
private string $privateProperty = 'foo';
#[ClassPHP80Attribute('method')]
public function bar(#[ClassPHP80Attribute('parameter')] $parameter)
{}
}
/**
* @see https://php.watch/versions/8.0/constructor-property-promotion
*/
class ClassPHP80WithPropertyPromotion
{
public function __construct(
private string $privateStringValue,
private $privateNonTypedValue,
protected int $protectedIntValue = 42,
public array $publicArrayValue = [M_PI, M_E],
) {}
}
/**
* @see https://php.watch/versions/8.0/union-types
*/
class ClassWithPHP80UnionTypes
{
public string|int|float|bool $scalarValue;
public array|object|null $complexValueOrNull = null;
/**
* Special case, internally iterable should be replaced with Traversable|array
*/
public iterable|object $iterableOrObject;
public static function returnsUnionType(): object|array|null {}
public static function acceptsUnionType(\stdClass|\Traversable|array $iterable): void {}
}
/**
* @see https://php.watch/versions/8.0/mixed-type
*/
class ClassWithPHP80MixedType
{
public mixed $someMixedPublicProperty;
public static function returnsMixed(): mixed {}
public static function acceptsMixed(mixed $value): void {}
}
/**
* @see https://php.watch/versions/8.0/static-return-type
*/
class ClassWithPHP80StaticReturnType
{
public static function create(): static {}
} tests/Stub/FileWithClasses81.php 0000644 00000004776 14641221054 0012553 0 ustar 00
*
* 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\Stub;
/**
* @see https://php.watch/versions/8.1/readonly
*/
class ClassWithPhp81ReadOnlyProperties
{
public readonly int $publicReadonlyInt;
protected readonly array $protectedReadonlyArray;
private readonly object $privateReadonlyObject;
}
/**
* @see https://php.watch/versions/8.1/enums
*/
enum SimplePhp81EnumWithSuit {
case Clubs;
case Diamonds;
case Hearts;
case Spades;
}
/**
* @see https://php.watch/versions/8.1/enums#enums-backed
*/
enum BackedPhp81EnumHTTPMethods: string
{
case GET = 'get';
case POST = 'post';
}
/**
* @see https://php.watch/versions/8.1/enums#enum-methods
*/
enum BackedPhp81EnumHTTPStatusWithMethod: int
{
case OK = 200;
case ACCESS_DENIED = 403;
case NOT_FOUND = 404;
public function label(): string {
return static::getLabel($this);
}
public static function getLabel(self $value): string {
return match ($value) {
self::OK => 'OK',
self::ACCESS_DENIED => 'Access Denied',
self::NOT_FOUND => 'Page Not Found',
};
}
}
/**
* @see https://php.watch/versions/8.1/intersection-types
*/
class ClassWithPhp81IntersectionType implements \Countable
{
private \Iterator&\Countable $countableIterator;
public function __construct(\Iterator&\Countable $countableIterator)
{
$this->countableIterator = $countableIterator;
}
public function count(): int
{
return count($this->countableIterator);
}
}
/**
* @see https://php.watch/versions/8.1/intersection-types
*/
function functionWithPhp81IntersectionType(\Iterator&\Countable $value): \Iterator&\Countable {
foreach($value as $val) {}
count($value);
return $value;
}
/**
* @see https://php.watch/versions/8.1/never-return-type
*/
class ClassWithPhp81NeverReturnType
{
public static function doThis(): never
{
throw new \RuntimeException('Not implemented');
}
}
/**
* @see https://php.watch/versions/8.1/never-return-type
*/
function functionWithPhp81NeverReturnType(): never
{
throw new \RuntimeException('Not implemented');
}
/**
* @see https://php.watch/versions/8.1/final-class-const
*/
class ClassWithPhp81FinalClassConst {
final public const TEST = '1';
}
tests/Stub/FileWithClasses82.php 0000644 00000004105 14641221054 0012536 0 ustar 00
*
* 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\Stub;
/**
* @see https://php.watch/versions/8.2/readonly-classes
*/
readonly class ClassWithPhp82ReadOnlyFlag
{
public int $publicInt;
}
/**
* @see https://php.watch/versions/8.2/dnf-types
*/
class ClassWithPhp82DNFType
{
private (JSONResponse&SuccessResponse)|HTMLResponse|string $respond;
public function __construct((JSONResponse&SuccessResponse)|HTMLResponse|string $respond)
{
$this->respond = $respond;
}
public function respond(): (JSONResponse&SuccessResponse)|HTMLResponse|string
{
return $this->respond;
}
}
/**
* @see https://php.watch/versions/8.2/null-false-types
* @see https://php.watch/versions/8.2/true-type
*/
class ClassWithPhp82NullFalseTypes
{
private true $isTrue = true;
private false $isFalse = false;
private null $isNull = null;
public function returnsFalse(): false
{
return false;
}
public function returnsTrue(): true
{
return true;
}
public function returnsNullExplicitly(): null
{
return null;
}
public function acceptsTrue(true $acceptsTrue): void {}
public function acceptsFalse(false $acceptsFalse): void {}
public function acceptsNull(null $acceptsNull): void {}
}
/**
* @see https://php.watch/versions/8.2/constants-in-traits
*/
trait TraitWithPhp82Constant
{
protected const CURRENT_VERSION = '2.6';
final protected const MIN_VERSION = '2.5';
protected function ensureVersion(): void
{
if (self::CURRENT_VERSION < self::MIN_VERSION) {
throw new \Exception('Current version is too old');
}
}
}
class ClassWithPhp82SensitiveAttribute
{
private string $secret;
public function __construct(#[\SensitiveParameter] string $secret = 'password')
{
$this->secret = $secret;
}
} tests/Stub/FileWithClasses83.php 0000644 00000002005 14641221054 0012534 0 ustar 00
*
* 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\Stub;
// PHP 8.3+ code https://www.php.net/manual/en/migration83.php
define('PHP83_OBJECT_CONSTANT', new \Exception('PHP83'));
/**
* @see https://php.watch/versions/8.3/typed-constants
*/
class ClassWithPhp83TypedConstants
{
public const int INT_CONSTANT = 10;
public const string STRING_CONSTANT = 'foo';
public const bool BOOL_CONSTANT = true;
public const float FLOAT_CONSTANT = 42.0;
public const array ARRAY_CONSTANT = [10, 20];
public const object OBJECT_CONSTANT = PHP83_OBJECT_CONSTANT;
public const \Throwable&\Stringable INTERSECTED_OBJECT_CONSTANT = PHP83_OBJECT_CONSTANT;
public const string|int UNION_OBJECT_CONSTANT = 'test';
final public const string FINAL_STRING_CONSTANT = 'final';
}
tests/Stub/FileWithFunction80.php 0000644 00000000535 14641221054 0012727 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Stub;
#[SomeAttribute]
function function_with_attribute()
{
}
tests/Stub/FileWithFunctions55.php 0000644 00000001270 14641221054 0013111 0 ustar 00 7);
define('FUNCTION_CONST', mktime(hour: 12, minute: 33, second: 00));
// Conditionally defined constant won't be detected.
if (true) {
define('TRUE_CONST', 1);
}
// Constants defined with dynamic names are partially supported
define(str_repeat('A', times: 10), true);
// Ignore function calls with dynamic function names.
$define('TEST', 5);
tests/Stub/FileWithNamespaces.php 0000644 00000001400 14641221054 0013041 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Stub;
function arrayVariadicParameters(array ...$acceptsArrays) {}
function callableVariadicParameters(callable ...$acceptsArrays) {}
function constantExpressionAsDefault($value = 0.5 * 2 * 10, $another = __FUNCTION__ . 'test', $anotherOne = ['test'], $anotherTwo = array('yoo')) {}
function constantExponentiation($value = 0.5 * 2**2) {}
tests/Stub/FileWithParameters70.php 0000644 00000001112 14641221054 0013234 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Stub;
function miscScalarParameters(
int $acceptsInteger,
string $acceptsString,
bool $acceptsBool,
int $acceptsVariadicInteger,
array $acceptsArray = ['some' => ['value' => 'for', 'array' => 'to dump']],
float $acceptsFloat = \INF,
string ...$acceptsDefaultString
) {
}
tests/Stub/FileWithParameters80.php 0000644 00000000610 14641221054 0013237 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Stub;
function authenticate(
string $username,
#[\SensitiveParameter]
string $password
) {
}
tests/Stub/Issue44/ClassWithNamespace.php 0000644 00000000126 14641221054 0014310 0 ustar 00
*
* This source file is subject to the license that is bundled
* with this source code in the file LICENSE.
*/
namespace Go\ParserReflection\Stub;
use JetBrains\PhpStorm\ArrayShape;
use JetBrains\PhpStorm\Deprecated;
#[Deprecated('some arg')]
class RandomClassWithAttribute
{
#[ArrayShape([
'token' => 'string',
'code' => 'integer'
])]
public $foo;
}
throw new \RuntimeException('test');