Repository navigation
feat: type a bare test() call as the bound test case - #22
Conversation
test() without arguments returns a proxy to the running test case, but PHPStan only saw the declared HigherOrderTapProxy|TestCall, so method calls through it were unchecked. Resolve it the same way $this is resolved in test closures. Files no binding covers keep the declared type, so helper files outside a Pest.php binding report nothing new. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hook property reader only collected `$this->name = …` assignments, so a hook written as `test()->name = …` left the property mixed. A bare test() proxies the running test case, so treat it as the same receiver. Reads through test() resolve via the existing property extension now that test() is typed as the test case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| } | ||
|
|
||
| // @note: a bare test() proxies the running test case, so it gets the same type as $this in a test closure; an unbound file keeps the declared type. | ||
| return $this->pestTestCaseType->resolveIfBound($scope->getFile()); |
There was a problem hiding this comment.
Should this be limited to test and hook closures?
Right now any bare test() call in a bound file gets the test case type, including calls inside normal helper functions. The new testBareTestCallInHelperFunction test makes that behavior explicit.
A file binding tells us which test case Pest will use, but it does not mean test() is always running in a test context. This seems broader than $this inference, which only exists inside the bound closures.
There was a problem hiding this comment.
Ah, good catch! Indeed, $this behaves a bit different, and I agree test() should follow it.
At runtime, a bare test() returns the test case only while a test is running. Inside a helper function, the type depends on the caller, so we can't know it.
Changed in 9ac88da.
Side note: $this (and also a bare test()) is typed as the test case inside a describe() body too, although that body runs before any test does (so it can't be the test case). It was like that before this PR and likely doesn't matter in practice, so I left it alone, but I'm happy to fix it if you want.
A bare test() returns the running test case only while a test runs. In a helper function the type depends on the caller, so it now keeps the declared type. Inside test, hook and describe closures, and in closures nested in them, test() gets the same type as $this. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MrPunyapal
left a comment
There was a problem hiding this comment.
test() doesn't branch on $this, it branches on TestSuite::getInstance()->test instanceof TestCase. So the hasVariableType('this') guard is a proxy for that and the two come apart. In a static closure there's no $this, but the test is still running, so test() is the proxy and we bail. Nothing errors, the declared union still has HigherOrderTapProxy, so the suite stays green, but test()->createHelper() on a custom test case stays unchecked.
Not sure there's a fix. A static closure has no $this to read, so there's nothing to key off. Fine to leave it open, but testBareTestCallInStaticClosureKeepsDeclaredType asserting it as correct should probably go or be inverted.
isFunctionSupported matches any function named test. Pest declares it behind if (! function_exists('test')), so a project that defines its own global test() first never gets Pest's, and this retypes it. FunctionReflection::getFileName() against pestphp/pest/src/Functions.php would be safer.
resolveIfBound() is only used as a null check, the type gets thrown away since we return $scope->getVariableType('this'). Fine today, but isBound() reads better than instanceof Type.
Two things in the description: pest is 531 passed on 9ac88da, not 529. CI hasn't run, all four workflow runs are action_required.
|
Thanks for this! One case it leaves out, which is where we hit the error in practice: a bare function sendWebhook(array $content): TestResponse
{
return test()->postRawContent('/webhook', ...); // still TestCall|HigherOrderTapProxy
}There's no Also, once |
There was a problem hiding this comment.
I retested the current revision locally: all 531 existing tests pass, but the earlier functional concerns remain.
Additional checks confirm that bare test() calls in file-level helpers and static closures still keep TestCall|HigherOrderTapProxy, and test()->todo() reports an undefined method on the bound test case.
Could you address these cases and add regression coverage? The earlier concern about matching a project-defined global test() also remains.
MrPunyapal
left a comment
There was a problem hiding this comment.
Addressed the helper/static callback inference, protected proxy access, todo helper, and function provenance concerns with regression coverage. Verified 543 tests locally, sequentially and in parallel, plus static analysis and lint. All ten CI checks pass on bfdb8d8.
A bare
test()call currently keeps the broadTestCall|HigherOrderTapProxytype, which hides undefined methods and loses properties assigned through test hooks.Resolve Pest's bare
test()to the bound test case in test closures, file-level helpers and static callbacks. Keep calls with a description, unbound files, and project-defined globaltest()functions unchanged. Helpers in bound files are inferred as running-test helpers.Also infer hook properties assigned through
test(), allow protected and bound-trait member access through the proxy, and preserve thetest()->todo()helper without hiding real method typos or overriding a nativetodo()method.Validation:
All ten CI checks pass for the latest revision, covering PHP 8.4/8.5 and lowest/stable dependencies.