@@ -216,3 +216,122 @@ function testGetAttributesFromCallStackWithNullAttributeClass(): void
216216 // Should return all attributes, not just CallStackAttribute
217217 Assert::true (\count ($ attributes ) >= 1 );
218218}
219+
220+ function testGetAttributesFromCallStackIncludesClassAttributes (): void
221+ {
222+ $ obj = new CallStackTestClass ();
223+ $ attributes = $ obj ->methodA (CallStackAttribute::class, true , true );
224+
225+ $ labels = \array_map (
226+ static fn (\ReflectionAttribute $ attr ) => $ attr ->newInstance ()->label ,
227+ $ attributes ,
228+ );
229+
230+ // Should find both method attribute and class attribute
231+ Assert::true (\in_array ('methodA ' , $ labels , true ));
232+ Assert::true (\in_array ('classAttribute ' , $ labels , true ));
233+ }
234+
235+ function testGetAttributesFromCallStackWithoutClassAttributes (): void
236+ {
237+ $ obj = new CallStackTestClass ();
238+ $ attributes = $ obj ->methodA (CallStackAttribute::class, true , false );
239+
240+ $ labels = \array_map (
241+ static fn (\ReflectionAttribute $ attr ) => $ attr ->newInstance ()->label ,
242+ $ attributes ,
243+ );
244+
245+ // Should find only method attribute, not class attribute
246+ Assert::true (\in_array ('methodA ' , $ labels , true ));
247+ Assert::false (\in_array ('classAttribute ' , $ labels , true ));
248+ }
249+
250+ function testGetAttributesFromCallStackIncludesParentClassAttributes (): void
251+ {
252+ $ obj = new CallStackChildClass ();
253+ $ attributes = $ obj ->childMethod (CallStackAttribute::class, true , true , true );
254+
255+ $ labels = \array_map (
256+ static fn (\ReflectionAttribute $ attr ) => $ attr ->newInstance ()->label ,
257+ $ attributes ,
258+ );
259+
260+ // Should find child class and parent class attributes
261+ Assert::true (\in_array ('childClassAttribute ' , $ labels , true ));
262+ Assert::true (\in_array ('baseClassAttribute ' , $ labels , true ));
263+ }
264+
265+ function testGetAttributesFromCallStackWithoutParentClassAttributes (): void
266+ {
267+ $ obj = new CallStackChildClass ();
268+ $ attributes = $ obj ->childMethod (CallStackAttribute::class, true , true , false );
269+
270+ $ labels = \array_map (
271+ static fn (\ReflectionAttribute $ attr ) => $ attr ->newInstance ()->label ,
272+ $ attributes ,
273+ );
274+
275+ // Should find only child class attribute, not parent
276+ Assert::true (\in_array ('childClassAttribute ' , $ labels , true ));
277+ Assert::false (\in_array ('baseClassAttribute ' , $ labels , true ));
278+ }
279+
280+ function testGetAttributesFromCallStackWithLimit (): void
281+ {
282+ $ obj = new CallStackTestClass ();
283+ $ attributes = $ obj ->methodB (CallStackAttribute::class, true , false , true , true , 1 );
284+
285+ // Should return only 1 attribute due to limit
286+ Assert::same (1 , \count ($ attributes ));
287+ }
288+
289+ function testGetAttributesFromCallStackWithLimitGreaterThanResults (): void
290+ {
291+ $ attributes = topLevelFunction (CallStackAttribute::class, true , false , true , true , 100 );
292+
293+ // Should return all available attributes (less than limit)
294+ Assert::same (1 , \count ($ attributes ));
295+ }
296+
297+ function testGetAttributesFromCallStackWithLimitAndNestedCalls (): void
298+ {
299+ $ obj = new CallStackTestClass ();
300+ $ attributes = $ obj ->methodB (CallStackAttribute::class, true , false , true , true , 2 );
301+
302+ // Should return exactly 2 attributes
303+ Assert::same (2 , \count ($ attributes ));
304+
305+ $ labels = \array_map (
306+ static fn (\ReflectionAttribute $ attr ) => $ attr ->newInstance ()->label ,
307+ $ attributes ,
308+ );
309+
310+ // Should find the first two attributes from the call stack
311+ Assert::true (\in_array ('methodA ' , $ labels , true ));
312+ Assert::true (\in_array ('methodB ' , $ labels , true ));
313+ }
314+
315+ function testGetAttributesFromCallStackDuplicatesClassAttributesFromHierarchy (): void
316+ {
317+ $ obj = new CallStackChildClass ();
318+ // Call stack: childMethod() -> overriddenMethod()
319+ // With includeClasses=true and includeParents=true:
320+ // - overriddenMethod scans CallStackChildClass + CallStackBaseClass
321+ // - childMethod scans CallStackChildClass + CallStackBaseClass
322+ // Result: childClassAttribute appears twice, baseClassAttribute appears twice
323+ $ attributes = $ obj ->childMethod (CallStackAttribute::class, true , true , true );
324+
325+ $ labels = \array_map (
326+ static fn (\ReflectionAttribute $ attr ) => $ attr ->newInstance ()->label ,
327+ $ attributes ,
328+ );
329+
330+ // Count occurrences of each label
331+ $ childClassCount = \count (\array_filter ($ labels , static fn ($ l ) => $ l === 'childClassAttribute ' ));
332+ $ baseClassCount = \count (\array_filter ($ labels , static fn ($ l ) => $ l === 'baseClassAttribute ' ));
333+
334+ // Both class attributes should appear twice (once per method in call stack)
335+ Assert::same (2 , $ childClassCount );
336+ Assert::same (2 , $ baseClassCount );
337+ }
0 commit comments