Skip to content

Commit dfe1c16

Browse files
lisachenkoclaude
andauthored
feature: Bump phpstan to the level10, enable PHP8.5 (#156)
* feat: Initialize Claude for the repository * feat: use phpstan in the pipeline with level:5 * chore: fix all PHPStan level 5 errors - Fix type mismatches in NodeExpressionResolver, TypeExpressionResolver, ReflectionClassLikeTrait, and ReflectionFunctionLikeTrait - Remove unused constructor parameter from TypeExpressionResolver - Fix ClassLike property access with proper instanceof narrowing in ReflectionClass - Fix array key types and dead code in ReflectionClassLikeTrait - Add ignoreErrors for unset.possiblyHookedProperty in final classes ReflectionFunction and ReflectionMethod (false positive: final classes cannot have subclasses with hooks) - Replace array_reduce with foreach to fix callable type inference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: fix PHPStan level 6 errors Add missing PHPDoc type annotations for generic types and iterable value types across all source files: - Add @extends ReflectionClass<object> and ReflectionAttribute<object> - Type all array<K, V> properties and return types - Add ReflectionClass<object> generics to method return types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: fix PHPStan level 7 errors Fix actual type errors surfaced at level 7: - Fix PathResolver::realpath() return type to reflect recursive return - Add instanceof guards before accessing Arg::$value and Const_::$value - Fix ReflectionAttribute::getTypeNode() union type check - Add false-check around fetchReflectionClass() calls in NodeExpressionResolver - Fix constants property key type (int|string instead of string) - Add @return class-string and non-empty-string overrides for getName/getShortName - Fix getNamespaceName() method.notFound by guarding against ReflectionAttribute Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: fix PHPStan level 8 errors Fix null-safety and type-safety issues: - Add null guards for ReflectionEngine::$locator before use - Fix NodeTraverser::traverse() call with null-safe ?? [] - Add null checks before TypeExpressionResolver::process() calls - Add null check for Identifier::toString() in findClassLikeNodeByClassName - Fix offsetAccess.notFound in PathResolver by guarding string|null - Add null guards around method_exists() calls in NodeExpressionResolver - Fix $constOrEnumCaseNode null assignment in ReflectionClassConstant Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: fix PHPStan level 9 errors Fix mixed-type propagation from PhpParser Node::getAttribute() calls: - Use hasAttribute() + explicit casts for getStartLine/getEndLine/getFileName - Add instanceof Name guards before calling toString() on getAttribute() results - Fix array key types by guarding with is_string()/is_int() checks - Fix StaticVariablesCollector context type to match NodeExpressionResolver - Fix sprintf with mixed value in ReflectionClassConstant - Fix TypeExpressionResolver return types with explicit typed returns Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: fix PHPStan level 10 errors Fix remaining mixed-type and type-safety issues at level 10: - Add abstract getNodeForAttributes() to AttributeResolverTrait for typed node access - Fix CallableLocator return type with explicit string|false narrowing - Fix ReflectionClassConstant::gettype() usage with proper array key narrowing - Fix NodeExpressionResolver getAttribute() mixed results with type guards - Fix ReflectionFileNamespace getAttribute() calls with proper int|false returns - Fix ReflectionUnionType array_search with proper array type - Filter Stringable objects in ReflectionClassLikeTrait::__toString() buildString Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve self/parent type hints to actual class names Native PHP reflection resolves 'self' to the declaring class FQCN and 'parent' to the parent class FQCN in type hints. The parser-based TypeExpressionResolver was returning the literals 'self'/'parent'. - Add optional $selfClassName/$parentClassName to TypeExpressionResolver - Add getDeclaringClassNameForTypes()/getParentClassNameForTypes() hook methods to ReflectionFunctionLikeTrait (default null for functions) - Override both hooks in ReflectionMethod with the declaring class names - Pass declaring class context when constructing TypeExpressionResolver in ReflectionParameter, ReflectionProperty, ReflectionClassConstant - Keep 'static' unresolved (preserved as-is by native PHP reflection) Fixes all 4 previously failing PHPUnit tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: Remove the treatPhpDocTypesAsCertain: false for phpstan * fix: Remove old source code that causes deprecation warnings * fix: address PR review comments - Remove class_alias() from resolveAsClassString() and resolveAttributeClassName() (global side-effect; class names from AST are semantically class-strings by construction). Add ignoreErrors entries for these two known PHPStan limitations. - Convert silent continue statements to explicit ReflectionException throws in NodeExpressionResolver (variadic placeholders, non-scalar named arg keys, non-scalar array keys) and ReflectionFileNamespace (define() with unsupported args). - Fix PathResolver::realpath() unsafe $path[1] indexing — use regex-based Windows drive letter detection (matches PR suggestion). - Fix ReflectionProperty::setValue() to check isStatic() before validating object arg, consistent with native ReflectionProperty behavior. - Remove always-true PHP_VERSION_ID >= 80400 guards in ReflectionProperty::getModifiers() since composer.json now requires PHP >= 8.4. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): Cover misc deprecations * fix: resolve self/parent types only on PHP 8.5+ to match native behavior PHP 8.5 changed ReflectionNamedType::getName() to return the actual FQCN for 'self' and 'parent' type hints, whereas PHP 8.4 and earlier return the keywords as-is. Gate the resolution behind PHP_VERSION_ID >= 80500 so the library mirrors native reflection behavior on all supported versions. Fixes CI failures on PHP 8.4 builds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(property): restore single-arg setValue() for static properties setValue(\$value) with one argument is the canonical PHP 8.4+ way to set a static property value. The previous implementation silently forwarded null as the value because \$value defaulted to null. Also update CLAUDE.md to reflect the actual PHP >=8.4 requirement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dcd0363 commit dfe1c16

33 files changed

Lines changed: 1033 additions & 347 deletions

.github/workflows/phpstan.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: "PHPStan analysis"
2+
3+
permissions:
4+
contents: read
5+
6+
on:
7+
pull_request:
8+
push:
9+
branches:
10+
- master
11+
12+
jobs:
13+
build:
14+
name: "PHPStan analysis - PHP8.4"
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: "Checkout"
18+
uses: actions/checkout@v4
19+
- name: "Install PHP"
20+
uses: shivammathur/setup-php@v2
21+
with:
22+
php-version: "8.4"
23+
ini-values: memory_limit=-1
24+
tools: composer:v2
25+
- name: "Cache dependencies"
26+
uses: actions/cache@v4
27+
with:
28+
path: |
29+
~/.composer/cache
30+
vendor
31+
key: "php-8.4"
32+
restore-keys: "php-8.4"
33+
- name: "Install dependencies"
34+
run: "composer install --no-interaction --no-progress"
35+
- name: "Static analysis"
36+
run: "vendor/bin/phpstan analyze --memory-limit=1G"

.github/workflows/phpunit.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@ jobs:
1515
- "lowest"
1616
- "highest"
1717
php-version:
18+
- "8.5"
1819
- "8.4"
19-
- "8.3"
20-
- "8.2"
2120
operating-system:
2221
- "ubuntu-latest"
2322

CLAUDE.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
Parser Reflection is a **deprecated** PHP library (deprecated in favor of [BetterReflection](https://github.com/Roave/BetterReflection)) that extends PHP's internal reflection classes using nikic/PHP-Parser for static analysis. It reflects PHP code without loading classes into memory by parsing source files into an AST.
8+
9+
Requires PHP >=8.4. Namespace: `Go\ParserReflection\`.
10+
11+
## Commands
12+
13+
```bash
14+
# Install dependencies (slow locally — see note below)
15+
composer install --prefer-source --no-interaction
16+
17+
# Run tests (~6 seconds, ~10,500 tests)
18+
vendor/bin/phpunit
19+
20+
# Run a single test file
21+
vendor/bin/phpunit tests/ReflectionClassTest.php
22+
23+
# Run a specific test method
24+
vendor/bin/phpunit --filter testMethodName
25+
26+
# Static analysis (~5 seconds, 18 known existing errors are normal)
27+
vendor/bin/phpstan analyse src --no-progress
28+
```
29+
30+
> **Note on `composer install` locally**: due to GitHub API rate limits, use `--prefer-source` and set a long timeout: `composer config --global process-timeout 2000`. In CI, standard `composer install` works fine with GitHub tokens.
31+
32+
## Architecture
33+
34+
### Request flow
35+
36+
When you call `new ReflectionClass('SomeClass')`:
37+
1. `ReflectionClass` asks `ReflectionEngine` for the class's AST node
38+
2. `ReflectionEngine` uses the registered `LocatorInterface` to find the file
39+
3. The file is parsed by PHP-Parser into an AST
40+
4. Two node visitors run: `NameResolver` (resolves FQCNs) and `RootNamespaceNormalizer` (normalizes global namespace)
41+
5. The resulting `ClassLike` AST node is stored in `ReflectionEngine::$parsedFiles` (in-memory LRU cache)
42+
6. The node is wrapped in the appropriate reflection class
43+
44+
### Key components
45+
46+
- **`ReflectionEngine`** (`src/ReflectionEngine.php`) — static class; central hub. Owns the PHP-Parser instance, AST cache, and locator. Entry points: `parseFile()`, `parseClass()`, `parseClassMethod()`, etc.
47+
- **`LocatorInterface`** / **`ComposerLocator`** — pluggable class file finder. `ComposerLocator` delegates to Composer's classmap/autoloader. `bootstrap.php` auto-registers `ComposerLocator` on load.
48+
- **Reflection classes** (`src/Reflection*.php`) — each extends its PHP internal counterpart (e.g. `ReflectionClass extends \ReflectionClass`) and holds an AST node. Methods that require a live object (e.g. `invoke()`) trigger actual class loading and fall back to native reflection.
49+
- **Traits** (`src/Traits/`) — shared logic extracted to avoid duplication:
50+
- `ReflectionClassLikeTrait` — used by `ReflectionClass`; implements most class inspection methods against the AST
51+
- `ReflectionFunctionLikeTrait` — shared by `ReflectionMethod` and `ReflectionFunction`
52+
- `InitializationTrait` — lazy initialization of AST node from engine
53+
- `InternalPropertiesEmulationTrait` — makes `var_dump`/serialization look like native reflection
54+
- `AttributeResolverTrait` — resolves PHP 8 attributes from AST nodes
55+
- **Resolvers** (`src/Resolver/`) — `NodeExpressionResolver` evaluates constant expressions in the AST (used for default values, constants). `TypeExpressionResolver` resolves type AST nodes into reflection type objects.
56+
- **`ReflectionFile` / `ReflectionFileNamespace`** — library-specific (not in native PHP reflection). Allow reflecting arbitrary PHP files and iterating their namespaces, classes, functions without knowing class names in advance.
57+
58+
### Test structure
59+
60+
Tests in `tests/` mirror the reflection class names (e.g. `ReflectionClassTest.php`). PHP version-specific stub files in `tests/Stub/` (e.g. `FileWithClasses84.php`) contain the PHP code being reflected. Tests extend `AbstractTestCase` which sets up the `ReflectionEngine` with a `ComposerLocator`.
61+
62+
### CI
63+
64+
GitHub Actions (`.github/workflows/phpunit.yml`) runs PHPUnit on PHP 8.2, 8.3, 8.4 with both lowest and highest dependency versions.

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@
2222
}
2323
},
2424
"require": {
25-
"php": ">=8.2",
25+
"php": ">=8.4",
2626
"nikic/php-parser": "^5.4"
2727
},
2828
"require-dev": {
2929
"phpunit/phpunit": "^11.0.7",
30+
"phpstan/phpstan": "^2.0",
3031
"tracy/tracy": "^2.10",
31-
"rector/rector": "^1.0",
32-
"rector/rector-php-parser": "^0.14.0"
32+
"rector/rector": "^2.0"
3333
},
3434
"extra": {
3535
"branch-alias": {

phpstan.neon

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
parameters:
2+
level: 10
3+
paths:
4+
- src
5+
ignoreErrors:
6+
# Both classes are final, so "might have hooks in a subclass" is a false positive
7+
- identifier: unset.possiblyHookedProperty
8+
path: src/ReflectionFunction.php
9+
- identifier: unset.possiblyHookedProperty
10+
path: src/ReflectionMethod.php
11+
# Class names from the AST are semantically class-strings by construction (they come from
12+
# parsed PHP class declarations), but PHPStan cannot verify this without autoloading, which
13+
# would violate the library's contract of reflecting code without loading classes.
14+
- identifier: return.type
15+
path: src/Traits/ReflectionClassLikeTrait.php
16+
message: '#resolveAsClassString#'
17+
- identifier: return.type
18+
path: src/Traits/AttributeResolverTrait.php
19+
message: '#resolveAttributeClassName#'

src/Instrument/PathResolver.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ class PathResolver
2626
/**
2727
* Custom replacement for realpath() and stream_resolve_include_path()
2828
*
29-
* @param string|array $somePath Path without normalization or array of paths
30-
* @param bool $shouldCheckExistence Flag for checking existence of resolved filename
29+
* @param string|array<int, string> $somePath Path without normalization or array of paths
30+
* @param bool $shouldCheckExistence Flag for checking existence of resolved filename
3131
*
32-
* @return array|bool|string
32+
* @return ($somePath is array ? array<int, string|false> : string|false)
3333
*/
3434
public static function realpath($somePath, $shouldCheckExistence = false)
3535
{
@@ -50,13 +50,14 @@ public static function realpath($somePath, $shouldCheckExistence = false)
5050
return $fastPath;
5151
}
5252

53-
$isRelative = !$pathScheme && ($path[0] !== '/') && ($path[1] !== ':');
53+
$isWindowsAbsolutePath = $path !== null && strlen($path) > 1 && preg_match('/^[A-Za-z]:/', $path) === 1;
54+
$isRelative = !$pathScheme && $path !== null && !str_starts_with($path, '/') && !$isWindowsAbsolutePath;
5455
if ($isRelative) {
5556
$path = getcwd() . DIRECTORY_SEPARATOR . $path;
5657
}
5758

5859
// resolve path parts (single dot, double dot and double delimiters)
59-
$path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
60+
$path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path ?? '');
6061
if (strpos($path, '.') !== false) {
6162
$parts = explode(DIRECTORY_SEPARATOR, $path);
6263
$absolutes = [];

src/Locator/CallableLocator.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,18 @@
1212

1313
namespace Go\ParserReflection\Locator;
1414

15+
use Closure;
1516
use Go\ParserReflection\LocatorInterface;
1617

1718
/**
1819
* Locator, that can find a file for the given class name by asking composer
1920
* @see \Go\ParserReflection\Locator\CallableLocatorTest
2021
*/
21-
class CallableLocator implements LocatorInterface
22+
final readonly class CallableLocator implements LocatorInterface
2223
{
23-
/**
24-
* @var callable
25-
*/
26-
private $callable;
2724

28-
public function __construct(callable $callable)
25+
public function __construct(private Closure $callable)
2926
{
30-
$this->callable = $callable;
3127
}
3228

3329
/**
@@ -37,6 +33,8 @@ public function __construct(callable $callable)
3733
*/
3834
public function locateClass(string $className): false|string
3935
{
40-
return call_user_func($this->callable, ltrim($className, '\\'));
36+
$result = ($this->callable)(ltrim($className, '\\'));
37+
38+
return is_string($result) ? $result : false;
4139
}
4240
}

src/Locator/ComposerLocator.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@
2323
*/
2424
class ComposerLocator implements LocatorInterface
2525
{
26-
/**
27-
* @var ClassLoader
28-
*/
29-
private $loader;
26+
27+
private ClassLoader $loader;
3028

3129
public function __construct(?ClassLoader $composerLoader = null)
3230
{
@@ -54,7 +52,8 @@ public function locateClass(string $className): false|string
5452
{
5553
$filePath = $this->loader->findFile(ltrim($className, '\\'));
5654
if (!empty($filePath)) {
57-
$filePath = PathResolver::realpath($filePath);
55+
$resolvedPath = PathResolver::realpath($filePath);
56+
$filePath = is_string($resolvedPath) ? $resolvedPath : false;
5857
}
5958

6059
return $filePath;

src/NodeVisitor/RootNamespaceNormalizer.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace Go\ParserReflection\NodeVisitor;
1414

15+
use PhpParser\Node\Stmt;
1516
use PhpParser\Node\Stmt\Declare_;
1617
use PhpParser\Node\Stmt\Namespace_;
1718
use PhpParser\NodeVisitorAbstract;
@@ -45,7 +46,11 @@ public function beforeTraverse(array $nodes)
4546
}
4647
}
4748
// Wrap all statements into the namespace block
48-
$globalNamespaceNode = new Namespace_(null, array_slice($nodes, $lastDeclareOffset));
49+
$stmts = array_values(array_filter(
50+
array_slice($nodes, $lastDeclareOffset),
51+
static fn ($node) => $node instanceof Stmt
52+
));
53+
$globalNamespaceNode = new Namespace_(null, $stmts);
4954
// Replace top-level nodes with namespaced node
5055
array_splice($nodes, $lastDeclareOffset, count($nodes), [$globalNamespaceNode]);
5156

src/NodeVisitor/StaticVariablesCollector.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace Go\ParserReflection\NodeVisitor;
1414

15+
use Go\ParserReflection\ReflectionFileNamespace;
1516
use Go\ParserReflection\Resolver\NodeExpressionResolver;
1617
use PhpParser\Node;
1718
use PhpParser\NodeVisitorAbstract;
@@ -23,17 +24,22 @@ class StaticVariablesCollector extends NodeVisitorAbstract
2324
{
2425
/**
2526
* Reflection context, eg. ReflectionClass, ReflectionMethod, etc
27+
*
28+
* @var \ReflectionClass<object>|\ReflectionFunction|\ReflectionMethod|\ReflectionClassConstant|\ReflectionParameter|\ReflectionAttribute<object>|\ReflectionProperty|ReflectionFileNamespace|null
2629
*/
27-
private mixed $context;
30+
private \ReflectionClass|\ReflectionFunction|\ReflectionMethod|\ReflectionClassConstant|\ReflectionParameter|\ReflectionAttribute|\ReflectionProperty|ReflectionFileNamespace|null $context;
2831

32+
/**
33+
* @var array<string, mixed>
34+
*/
2935
private array $staticVariables = [];
3036

3137
/**
3238
* Default constructor
3339
*
34-
* @param mixed $context Reflection context, eg. ReflectionClass, ReflectionMethod, etc
40+
* @param \ReflectionClass<object>|\ReflectionFunction|\ReflectionMethod|\ReflectionClassConstant|\ReflectionParameter|\ReflectionAttribute<object>|\ReflectionProperty|ReflectionFileNamespace|null $context Reflection context, eg. ReflectionClass, ReflectionMethod, etc
3541
*/
36-
public function __construct(mixed $context)
42+
public function __construct(\ReflectionClass|\ReflectionFunction|\ReflectionMethod|\ReflectionClassConstant|\ReflectionParameter|\ReflectionAttribute|\ReflectionProperty|ReflectionFileNamespace|null $context)
3743
{
3844
$this->context = $context;
3945
}
@@ -62,7 +68,11 @@ public function enterNode(Node $node)
6268

6369
if ($staticVariable->var->name instanceof Node\Expr) {
6470
$expressionSolver->process($staticVariable->var->name);
65-
$name = $expressionSolver->getValue();
71+
$resolvedName = $expressionSolver->getValue();
72+
if (!is_string($resolvedName)) {
73+
throw new \InvalidArgumentException("Unknown value for the key, " . gettype($resolvedName) . " has given, but string is expected");
74+
}
75+
$name = $resolvedName;
6676
} else {
6777
$name = $staticVariable->var->name;
6878
}
@@ -75,6 +85,8 @@ public function enterNode(Node $node)
7585

7686
/**
7787
* Returns an associative map of static variables in the method/function body
88+
*
89+
* @return array<string, mixed>
7890
*/
7991
public function getStaticVariables(): array
8092
{

0 commit comments

Comments
 (0)