From f24d0ec37e9705e9ff103ac8b479c5394b6d9de0 Mon Sep 17 00:00:00 2001 From: Maks Oleksyuk Date: Thu, 6 Aug 2026 10:48:40 +0300 Subject: [PATCH 1/2] fix: recognize arrow function beforeEach() hooks PestHookPropertyReader only handled Closure hook bodies, so an ArrowFunction beforeEach() silently degraded every $this->prop read to mixed. Covered with 5 new tests. Co-Authored-By: Claude Sonnet 5 --- src/Type/Pest/PestHookPropertyReader.php | 23 +++++++++------- .../ArrowScoped/ArrowScopedTest.php | 3 +++ tests/Type/Fixtures/pesthook-scope/Pest.php | 2 ++ tests/Type/PestHookPropertyScopeTest.php | 12 +++++++++ tests/Type/data/test-hook-properties.php | 27 +++++++++++++++++++ 5 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 tests/Type/Fixtures/pesthook-scope/ArrowScoped/ArrowScopedTest.php diff --git a/src/Type/Pest/PestHookPropertyReader.php b/src/Type/Pest/PestHookPropertyReader.php index 47241b4..f36c16d 100644 --- a/src/Type/Pest/PestHookPropertyReader.php +++ b/src/Type/Pest/PestHookPropertyReader.php @@ -7,6 +7,7 @@ use PhpParser\Comment\Doc; use PhpParser\Node; use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; @@ -18,6 +19,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt\Expression; +use PhpParser\Node\Stmt\Return_; use PhpParser\NodeFinder; final class PestHookPropertyReader @@ -136,7 +138,7 @@ private function extractUsesBeforeEachProperties(NodeFinder $nodeFinder, array $ } foreach ($methodCall->getArgs() as $arg) { - if (! $arg->value instanceof Closure) { + if (! $arg->value instanceof Closure && ! $arg->value instanceof ArrowFunction) { continue; } @@ -249,7 +251,7 @@ private function parseTestFile(string $filePath): array } foreach ($funcCall->getArgs() as $arg) { - if (! $arg->value instanceof Closure) { + if (! $arg->value instanceof Closure && ! $arg->value instanceof ArrowFunction) { continue; } @@ -267,17 +269,18 @@ private function parseTestFile(string $filePath): array * @param array $useMap * @return array> */ - private function extractPropertyAssignments(Closure $closure, array $useMap): array + private function extractPropertyAssignments(Closure|ArrowFunction $closure, array $useMap): array { $properties = []; $localVarMap = $this->buildLocalVarExprMap($closure, $useMap); - foreach ($closure->stmts as $stmt) { - if (! $stmt instanceof Expression) { - continue; - } + foreach ($closure->getStmts() as $stmt) { + $expr = match (true) { + $stmt instanceof Expression => $stmt->expr, + $stmt instanceof Return_ => $stmt->expr, + default => null, + }; - $expr = $stmt->expr; if (! $expr instanceof Assign) { continue; } @@ -335,11 +338,11 @@ private function extractPropertyAssignments(Closure $closure, array $useMap): ar * @param array $useMap * @return array */ - private function buildLocalVarExprMap(Closure $closure, array $useMap): array + private function buildLocalVarExprMap(Closure|ArrowFunction $closure, array $useMap): array { $map = []; - foreach ($closure->stmts as $stmt) { + foreach ($closure->getStmts() as $stmt) { if (! $stmt instanceof Expression) { continue; } diff --git a/tests/Type/Fixtures/pesthook-scope/ArrowScoped/ArrowScopedTest.php b/tests/Type/Fixtures/pesthook-scope/ArrowScoped/ArrowScopedTest.php new file mode 100644 index 0000000..174d7fd --- /dev/null +++ b/tests/Type/Fixtures/pesthook-scope/ArrowScoped/ArrowScopedTest.php @@ -0,0 +1,3 @@ +scopedProperty = 'scoped'; })->in('Scoped'); +pest()->extend(CustomTestCase::class)->beforeEach(fn () => $this->arrowScopedProperty = 'arrow-scoped')->in('ArrowScoped'); + pest()->extend(CustomTestCase::class)->beforeEach(function (): void { $this->untargetedProperty = 'untargeted'; }); diff --git a/tests/Type/PestHookPropertyScopeTest.php b/tests/Type/PestHookPropertyScopeTest.php index 8770b71..569596a 100644 --- a/tests/Type/PestHookPropertyScopeTest.php +++ b/tests/Type/PestHookPropertyScopeTest.php @@ -30,6 +30,18 @@ expect($reader->getPropertyExprs($dir.'/Sibling/SiblingTest.php'))->not->toHaveKey('scopedProperty'); }); +test('an arrow function beforeEach scoped with in() applies to files under that target', function () use ($hooks): void { + [$dir, $reader] = $hooks(); + + expect($reader->getPropertyExprs($dir.'/ArrowScoped/ArrowScopedTest.php'))->toHaveKey('arrowScopedProperty'); +}); + +test('an arrow function beforeEach scoped with in() does not leak into a sibling directory', function () use ($hooks): void { + [$dir, $reader] = $hooks(); + + expect($reader->getPropertyExprs($dir.'/Sibling/SiblingTest.php'))->not->toHaveKey('arrowScopedProperty'); +}); + test('a beforeEach without an in() target binds to no file at all', function () use ($hooks): void { [$dir, $reader] = $hooks(); diff --git a/tests/Type/data/test-hook-properties.php b/tests/Type/data/test-hook-properties.php index 922557c..84027a6 100644 --- a/tests/Type/data/test-hook-properties.php +++ b/tests/Type/data/test-hook-properties.php @@ -248,6 +248,33 @@ function testVarThisAnnotationDoesNotOverridePropertyType(): void }); } +function testBeforeEachArrowFunctionAssignment(): void +{ + beforeEach(fn () => $this->arrowPost = new Post); + + it('resolves property type from an arrow function beforeEach', function (): void { + assertType(Post::class, $this->arrowPost); + }); +} + +function testBeforeEachArrowFunctionStringLiteral(): void +{ + beforeEach(fn () => $this->arrowName = 'test'); + + it('resolves string type from an arrow function beforeEach', function (): void { + assertType("'test'", $this->arrowName); + }); +} + +function testBeforeEachArrowFunctionNonAssignmentStaysMixed(): void +{ + beforeEach(fn () => someFunction()); + + it('returns mixed when an arrow function beforeEach does not assign a property', function (): void { + assertType('mixed', $this->neverAssigned); + }); +} + function testBeforeEachSelfReferentialProperty(): void { beforeEach(function (): void { From 50c0ce0e35e72975435e2223065d5fb068402b70 Mon Sep 17 00:00:00 2001 From: Maks Oleksyuk Date: Thu, 6 Aug 2026 15:08:42 +0300 Subject: [PATCH 2/2] style: add missing return type on arrow function beforeEach() fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leftover from the earlier arrow-function narrowing fix — rector's AddArrowFunctionReturnTypeRector flags this on every run otherwise. Co-Authored-By: Claude Sonnet 5 --- tests/Type/Fixtures/pesthook-scope/Pest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Type/Fixtures/pesthook-scope/Pest.php b/tests/Type/Fixtures/pesthook-scope/Pest.php index ea22d5e..1ac95f5 100644 --- a/tests/Type/Fixtures/pesthook-scope/Pest.php +++ b/tests/Type/Fixtures/pesthook-scope/Pest.php @@ -8,7 +8,7 @@ $this->scopedProperty = 'scoped'; })->in('Scoped'); -pest()->extend(CustomTestCase::class)->beforeEach(fn () => $this->arrowScopedProperty = 'arrow-scoped')->in('ArrowScoped'); +pest()->extend(CustomTestCase::class)->beforeEach(fn (): string => $this->arrowScopedProperty = 'arrow-scoped')->in('ArrowScoped'); pest()->extend(CustomTestCase::class)->beforeEach(function (): void { $this->untargetedProperty = 'untargeted';