77use phpDocumentor \Reflection \DocBlockFactoryInterface ;
88use phpDocumentor \Reflection \Fqsen ;
99use phpDocumentor \Reflection \Location ;
10+ use phpDocumentor \Reflection \NodeVisitor \FindingVisitor ;
1011use phpDocumentor \Reflection \Php \AsymmetricVisibility ;
1112use phpDocumentor \Reflection \Php \Factory \Reducer \Reducer ;
1213use phpDocumentor \Reflection \Php \Property as PropertyElement ;
1516use phpDocumentor \Reflection \Php \Visibility ;
1617use PhpParser \Comment \Doc ;
1718use PhpParser \Modifiers ;
19+ use PhpParser \Node ;
1820use PhpParser \Node \ComplexType ;
1921use PhpParser \Node \Expr ;
22+ use PhpParser \Node \Expr \PropertyFetch ;
23+ use PhpParser \Node \Expr \Variable ;
2024use PhpParser \Node \Identifier ;
2125use PhpParser \Node \Name ;
2226use PhpParser \Node \Param ;
2327use PhpParser \Node \PropertyHook as PropertyHookNode ;
28+ use PhpParser \NodeTraverser ;
2429use PhpParser \PrettyPrinter \Standard as PrettyPrinter ;
2530
2631use function array_filter ;
2732use function array_map ;
33+ use function count ;
2834use function method_exists ;
2935
3036/**
@@ -141,6 +147,14 @@ public function hooks(array $hooks): self
141147
142148 public function build (ContextStack $ context ): PropertyElement
143149 {
150+ $ hooks = array_filter (array_map (
151+ fn (PropertyHookNode $ hook ) => $ this ->buildHook ($ hook , $ context , $ this ->visibility ),
152+ $ this ->hooks ,
153+ ));
154+
155+ // Check if this is a virtual property by examining all hooks
156+ $ isVirtual = $ this ->isVirtualProperty ($ this ->hooks , $ this ->fqsen ->getName ());
157+
144158 return new PropertyElement (
145159 $ this ->fqsen ,
146160 $ this ->visibility ,
@@ -151,10 +165,8 @@ public function build(ContextStack $context): PropertyElement
151165 $ this ->endLocation ,
152166 (new Type ())->fromPhpParser ($ this ->type ),
153167 $ this ->readOnly ,
154- array_filter (array_map (
155- fn (PropertyHookNode $ hook ) => $ this ->buildHook ($ hook , $ context , $ this ->visibility ),
156- $ this ->hooks ,
157- )),
168+ $ hooks ,
169+ $ isVirtual ,
158170 );
159171 }
160172
@@ -264,6 +276,59 @@ private function buildHook(PropertyHookNode $hook, ContextStack $context, Visibi
264276 return $ result ;
265277 }
266278
279+ /**
280+ * Detects if a property is virtual by checking if any of its hooks reference the property itself.
281+ *
282+ * A virtual property is one where no defined hook references the property itself.
283+ * For example, in the 'get' hook, it doesn't use $this->propertyName.
284+ *
285+ * @param PropertyHookNode[] $hooks The property hooks to check
286+ * @param string $propertyName The name of the property
287+ *
288+ * @return bool True if the property is virtual, false otherwise
289+ */
290+ private function isVirtualProperty (array $ hooks , string $ propertyName ): bool
291+ {
292+ if (empty ($ hooks )) {
293+ return false ;
294+ }
295+
296+ foreach ($ hooks as $ hook ) {
297+ $ stmts = $ hook ->getStmts ();
298+
299+ if ($ stmts === null || count ($ stmts ) === 0 ) {
300+ continue ;
301+ }
302+
303+ $ finder = new FindingVisitor (
304+ static function (Node $ node ) use ($ propertyName ) {
305+ // Check if the node is a property fetch that references the property
306+ return $ node instanceof PropertyFetch && $ node ->name instanceof Identifier &&
307+ $ node ->name ->toString () === $ propertyName &&
308+ $ node ->var instanceof Variable &&
309+ $ node ->var ->name === 'this ' ;
310+ },
311+ );
312+
313+ $ traverser = new NodeTraverser ($ finder );
314+ $ traverser ->traverse ($ stmts );
315+
316+ if ($ finder ->getFoundNode () !== null ) {
317+ return false ;
318+ }
319+ }
320+
321+ return true ;
322+ }
323+
324+ /**
325+ * Builds the hook visibility based on the hook name and property visibility.
326+ *
327+ * @param string $hookName The name of the hook ('get' or 'set')
328+ * @param Visibility $propertyVisibility The visibility of the property
329+ *
330+ * @return Visibility The appropriate visibility for the hook
331+ */
267332 private function buildHookVisibility (string $ hookName , Visibility $ propertyVisibility ): Visibility
268333 {
269334 if ($ propertyVisibility instanceof AsymmetricVisibility === false ) {
0 commit comments