Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Read property virtuality off the native reflection when exporting nodes
Exporting a non-private property records whether it is virtual, and asked
ClassReflection::getNativeProperty() for it. Building a PhpPropertyReflection
walks the class' interfaces, which reads its @implements tags - and reading a
class' PHPDoc runs the StubFilesExtensions, which the result cache restore
must not do before the deferred bootstrapFiles.

The value itself comes from BetterReflection either way, so it is read
straight off the native reflection. With this, resolving exported nodes no
longer touches PHPStan's PHPDoc layer at all: a probe on
StubFilesProvider::getStubFiles() sees no pre-bootstrap call across an
incremental analysis of PHPStan's own source with 609 changed files.

Closes phpstan/phpstan#15120

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FA4hJBE4kDNQeReb4yFnAk
  • Loading branch information
ondrejmirtes and claude committed Aug 31, 2026
commit 133e106fee9bb796cd1969586aef2396d4ec63f3
9 changes: 9 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,15 @@ jobs:
../bashunit -a contains 'No errors' "$OUTPUT"
mv src/Repository.php.orig src/Repository.php
../../bin/phpstan analyse -vvv
# a changed file with a non-private property - exporting it asks whether the
# property is virtual
patch -b src/Entity.php < property-change.patch
OUTPUT=$(../../bin/phpstan analyse -vvv 2>&1 || true)
echo "$OUTPUT"
../bashunit -a not_contains 'Undefined constant' "$OUTPUT"
../bashunit -a contains 'No errors' "$OUTPUT"
mv src/Entity.php.orig src/Entity.php
../../bin/phpstan analyse -vvv
- script: |
cd e2e/nested-trait-use
../../bin/phpstan analyze
Expand Down
13 changes: 13 additions & 0 deletions e2e/bug-15120b/property-change.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
--- src/Entity.php
+++ src/Entity.php
@@ -13,4 +13,10 @@
return $foo->get();
}

+ /** @return non-empty-string */
+ public function label(Foo $foo): string
+ {
+ return $this->name . ': ' . $this->describe($foo);
+ }
+
}
16 changes: 16 additions & 0 deletions e2e/bug-15120b/src/Entity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types = 1);

namespace Bug15120b;

class Entity
{

public string $name = 'entity';

/** @return non-empty-string */
public function describe(Foo $foo): string
{
return $foo->get();
}

}
9 changes: 6 additions & 3 deletions src/Dependency/ExportedNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,14 @@ private function exportClassStatement(Node\Stmt $node, string $fileName, string
$docComment = $node->getDocComment();

$names = array_map(static fn (Node\PropertyItem $prop): string => $prop->name->toString(), $node->props);
// Virtuality is decided by the native reflection, so it is read straight off it.
// PhpPropertyReflection would give the same answer, but building it resolves the
// class PHPDoc - see exportPhpDocNode() for why that is not allowed here.
$virtual = false;
if ($this->reflectionProvider->hasClass($namespacedName)) {
$classReflection = $this->reflectionProvider->getClass($namespacedName);
if ($classReflection->hasNativeProperty($names[0])) {
$virtual = $classReflection->getNativeProperty($names[0])->isVirtual()->yes();
$nativeReflection = $this->reflectionProvider->getClass($namespacedName)->getNativeReflection();
if ($nativeReflection->hasProperty($names[0])) {
$virtual = $nativeReflection->getProperty($names[0])->isVirtual();
}
}

Expand Down
Loading