Skip to content

Improve Support for PestPHP via IDE Helpers#532

Open
ace-of-aces wants to merge 26 commits intolaravel:mainfrom
ace-of-aces:feat/pest-helpers
Open

Improve Support for PestPHP via IDE Helpers#532
ace-of-aces wants to merge 26 commits intolaravel:mainfrom
ace-of-aces:feat/pest-helpers

Conversation

@ace-of-aces
Copy link
Copy Markdown
Contributor

closes #524

Since #524 got some upvotes, I decided to start working on this PR.

Summary

Essentially, this does two things:

  • generates a _pest.php helper file that contains declarations of Pests' function API and patches the $this variable via the @param-closure-this docblock tag, so the $this variable actually corresponds to the correct \Tests\TestCase class and doesn't lead to a bunch of "Undefined method" issues in Intelephense.
  • scans the project's Pest.php configuration file for custom expectations and custom extensions and generates corresponding docblock annotations for the Expectation class and writes that also in _pest.php

I added a new Laravel.pest.generateHelpers configuration flag for this, enabled by default.

In action

Full autocompletion (for Laravel-specific Test methods and expectations), no LSP errors reported:

Screen.Recording.2026-01-09.at.17.06.11.mov

An example of a custom expectation having a proper definition:
Screenshot 2026-01-09 at 17 08 03

Caveats / Open for discussion

There are some details to this implementation that are important understand beforehand.

  1. The _pest.php helper file is not generated inside of vendor/_laravel_ide_ like the other helpers, because that won't be picked up by Intelephense. I suspect that's because Intelephense treats all vendor code with lower priority than user code. As the actual definition of Pest's functions are inside of the vendor directory alongside a helper file placed in _laravel_ide_, the helper file cannot override Pest source code.
    Moving the helper file into another file of the project directory is the only way I found to circumvent this.
    Of course, this has the effect that the .ide-helper/_pest.php file appears in version control, and users would need to decide whether to track it in Git or ignore it. As this could potentially lead to confusion for users, I'd love to have some other ideas here.
  2. Expectations built into Pest itself are not auto-completed when using the free version of Intelephense (or any other PHP LSP for VS Code I know of). This is because Pest internally uses the non-standard @mixin PHPDoc tag on the main Expectation class ( the Mixins\Expecation class is where all of the built-in expectations are defined).
    Turns out that feature is only supported in the paid version of Intelephense, which I don't think most users of this extension have...
    This could be mitigated by also having method hints in the PHPDoc of the helper file for all of Pest's built-in expectations. But then there's the question of how (internally parsing the mixin class and resolving all of its public methods, or just hard coding them for now)?
  3. I'm just parsing a given tests/Pest.php configuration file inside a project with regexes. I've looked into how that's done via reflection for other helpers/php-templates, but those are all parsing PHP classes instead of top-level function calls like in Pest.php. I've tried a bunch of configurations tho, and they all seem to get parsed properly and lead to the correct generation of helpers.
  4. A detail: I've hardcoded the return type annotation of custom expectations to be self(which is the Expectation class). Actually figuring out what the custom expectation returns would require a lot more work, and probably something like the new Laravel Surveyor package :)
    I think this should be fine, as the official Pest docs recommend to always return the instance: "Of course, you probably want users to have the ability to "chain" expectations together with your custom expectation. To achieve this, ensure your custom expectation includes a return $this statement."

Looking forward to some feedback on this!❤️

@N1ebieski
Copy link
Copy Markdown
Contributor

N1ebieski commented Jan 10, 2026

Hi @ace-of-aces Great job!

Do I need to do something to make @param-closure-this TestCase $closure work? Right now, it doesn’t work for me :(

obraz

The _pest.php helper file is not generated inside of vendor/laravel_ide like the other helpers, because that won't be picked up by Intelephense.

Hmm for me it's working:

obraz

Maybe you’ve disabled indexing for the _laravel_ide folder using the intelephense.files.exclude configuration?

I'm just parsing a given tests/Pest.php configuration file inside a project with regexes.

This probably won't pass the CR.

Did you try the https://github.com/laravel/vs-code-extension/blob/main/src/support/parser.ts#L143-L165 ?

For example, the command:

laradock@51f642e1618e:/var/www/vs-code-php-parser-cli$ ./php-parser detect "<?php

pest()->extend(Tests\TestCase::class)
    ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
    ->in('Feature');" --debug

gives you:

[
    {
        "type": "methodCall",
        "methodName": "in",
        "className": "pest",
        "arguments": {
            "type": "arguments",
            "autocompletingIndex": 1,
            "children": [
                {
                    "type": "argument",
                    "name": null,
                    "children": [
                        {
                            "type": "string",
                            "value": "Feature",
                            "start": {
                                "line": 4,
                                "column": 9
                            },
                            "end": {
                                "line": 4,
                                "column": 16
                            }
                        }
                    ]
                }
            ]
        }
    },
    {
        "type": "methodCall",
        "methodName": "use",
        "className": "pest",
        "arguments": {
            "type": "arguments",
            "autocompletingIndex": 1,
            "children": [
                {
                    "type": "argument",
                    "name": null,
                    "children": [
                        {
                            "type": "methodCall",
                            "methodName": null,
                            "className": "Illuminate\\Foundation\\Testing\\RefreshDatabase",
                            "arguments": {
                                "type": "arguments",
                                "autocompletingIndex": 0,
                                "children": []
                            },
                            "children": []
                        }
                    ]
                }
            ]
        }
    },
    {
        "type": "methodCall",
        "methodName": "extend",
        "className": "pest",
        "arguments": {
            "type": "arguments",
            "autocompletingIndex": 1,
            "children": [
                {
                    "type": "argument",
                    "name": null,
                    "children": [
                        {
                            "type": "methodCall",
                            "methodName": null,
                            "className": "Tests\\TestCase",
                            "arguments": {
                                "type": "arguments",
                                "autocompletingIndex": 0,
                                "children": []
                            },
                            "children": []
                        }
                    ]
                }
            ]
        }
    },
    {
        "type": "methodCall",
        "methodName": "pest",
        "className": null,
        "arguments": {
            "type": "arguments",
            "autocompletingIndex": 0,
            "children": []
        }
    }
]

That should be enough, right?

@ace-of-aces
Copy link
Copy Markdown
Contributor Author

Hey @N1ebieski, thanks for your feedback!

Do I need to do something to make @param-closure-this TestCase $closure work? Right now, it doesn’t work for me :(

Hmm, that should work out of the box when using Intelephense AFAIK. What version of Intelephense are you running (for me it's the latest 1.16.3)?

So, when there's no helper file present in the project, $this should be interpreted as \PHPUnit\Framework\TestCase.
Screenshot 2026-01-10 at 12 16 48

Hmm for me it's working:

Yeah, the part of the helper file that augments custom expectations and the custom TestCase extensions does work inside of vendor/_laravel_ide_. It's just the function helper declarations with @param-closure-this that don't get picked up when placed inside of the vendor folder.
Prior to 509b55d I had the class helpers separated from the function helpers, but chose to merge them to have everything pest-related in one file. Can be changed of course :)

I suspect that's because in the case of the functions, we're trying to override the @param-closure-this declaration of the Pest source file, and that doesn't work because the helper doesn't have a greater priority than the source when they're both located inside of vendor. For all other helpers, including those for the TestCase and Expectation classes, we're just extending the sources with additional methods/properties/traits and not overriding anything (as I see it).

This probably won't pass the CR.

Did you try the https://github.com/laravel/vs-code-extension/blob/main/src/support/parser.ts#L143-L165 ?

No, I didn't try that as I don't know enough about the vs-code-php-parser-cli yet, but looks like a more reliable approach!😄
It sure looks does look like the detect command can provide the info we need from the Pest config :)
But I don't think we can use the function you quoted here, because that's tied to auto-completion?

@N1ebieski
Copy link
Copy Markdown
Contributor

Hi @ace-of-aces

What version of Intelephense are you running (for me it's the latest 1.16.3)?

You're right - the feature starts working after updating Intelephense to 1.16.3.

But I don't think we can use the function you quoted here, because that's tied to auto-completion?

No. Auto-completion uses https://github.com/laravel/vs-code-extension/blob/main/src/support/parser.ts#L205-L230

Detect function builds the AST for all the .php file, including all elements.

@ace-of-aces
Copy link
Copy Markdown
Contributor Author

You're right - the feature starts working after updating Intelephense to 1.16.3.

🥳

No. Auto-completion uses https://github.com/laravel/vs-code-extension/blob/main/src/support/parser.ts#L205-L230

Detect function builds the AST for all the .php file, including all elements.

@N1ebieski thx, it clicked for me too while currently working on it :)

@ace-of-aces
Copy link
Copy Markdown
Contributor Author

I've now replaced the regex-based approach via the php template with the AST-based detection through detect :)

@ace-of-aces
Copy link
Copy Markdown
Contributor Author

So... this took a bit more effort than I expected😅

But I think it should be ready for review now :)

Current state

  • Thanks to the quick fix of the issue Unions don't work in @param-closure-this tag bmewburn/vscode-intelephense#3512 by Intelephense's maintainer, I was now able to have multiple TestCase classes as the on the @param-closure-this tag as a type union, enabling proper autocompletion of all possible TestCase methods in each and every test.
    Especially useful for Pest setups where there are different base TestCase classes for Feature and Unit.
  • The Pest.php configuration file is now being parsed and analyzed, so that all Pest extension configurations, used traits and custom expectations are all being collected and properly. It supports the very flexible pest() configuration API with the chainable extends method (and its aliases extend, use, uses). Also, it now detects whether the argument is a trait or an actual class (via a small php template helper) and writes it in the right context.
  • For Pest.php configurations using the top-level uses function, that should also be properly analyzed.
  • Also accounted for pest configurations where there's no custom class being extended, but traits being used. In that case, there is a helper being generated with the trait on the default PHPUnit\Framework\TestCase class.

This should cover the entire configuration API of Pest AFAIK.

What's I've not included (yet) are the helper method annotations for the Expectation API, mentioned above:

  1. Expectations built into Pest itself are not auto-completed when using the free version of Intelephense (or any other PHP LSP for VS Code I know of)...

Looking forward to some more feedback on this!

@ace-of-aces ace-of-aces marked this pull request as ready for review January 16, 2026 23:26
@N1ebieski
Copy link
Copy Markdown
Contributor

The _pest.php helper file is not generated inside of vendor/laravel_ide like the other helpers, because that won't be picked up by Intelephense.

I’ve thought about this, and in my opinion it would be a good idea to move all these stubs (not only the _pint stub) from vendor into the project directory. Maybe the storage/framework/vscode would be a good place?

The reason is that I (and probably other people as well) usually run PHPStan/Larastan in CI/CD workflows, and these stubs are required for that process.

Related with #470

@TitasGailius Could you consider this suggestion?

@TitasGailius
Copy link
Copy Markdown
Collaborator

@N1ebieski thanks for the suggestion. I'm totally on board with moving those generated stubs to a path like .vscode/laravel-ide since the .vscode directory is already included in the default Laravel's gitignore.

@ace-of-aces
Copy link
Copy Markdown
Contributor Author

ace-of-aces commented Feb 15, 2026

Hey there!
Since this PR has now been open for a while now, I used the time to encapsulate this into a pure PHP composer package: https://github.com/ace-of-aces/intellipest

Having this as a package brings some advantages:

  • it's framework agnostic and even independent of VS Code (e.g. useful for Laravel package devs)
  • I've added a set of tests to ensure correct and consistent behaviour, which are currently not present in here

There are already some slight variations between my package and this PR. To avoid duplicating maintenance efforts or creating conflicting implementations for users, I’d love to find a middle ground.
I still believe the Laravel VS Code extension should ship with this feature out of the box to set devs up for success 😄

So here's my suggestion: Could we find a way for the vs-code-php-parser-cli to use this package, or perhaps have both projects share a common core?

I absolutely understand if you don't want to have users auto-install a package from me as a random contributor tho :)

Feel free to reach out.

@jakubmisek
Copy link
Copy Markdown

FYI, this is natively supported by DEVSENSE.phptools-vscode. See an announcement at https://blog.devsense.com/2026/vscode-updates-2026-01/#tests-pest-php-phpunit-1013

@ace-of-aces
Copy link
Copy Markdown
Contributor Author

@jakubmisek Not exactly..

Just tried it, but the heuristics your extension uses to determine the TestCase for $this seem to be off.

Also it don't respect a user's Pest.php configuration:

  • if a TestCase is actually used
  • multiple TestCases and traits
  • custom expectations

While I respect your efforts at DEVSENSE, I personally prefer an open source implementation where users can inspect things all the way from their code down to the declarations instead of having to rely on a black box :)

@jakubmisek
Copy link
Copy Markdown

@ace-of-aces, thank you very much for your response — I appreciate your thoughtful explanation.

I initially noticed that the issue (#524) referenced intelephense (which is closed-source) and I was hoping to help address the false warning.

Of course, providing a generated stub for the Pest test is a universal solution. It is also respected by DEVSENSE’s extension, as static analyzers generally cannot evaluate Pest.php configuration dynamically.

@TitasGailius
Copy link
Copy Markdown
Collaborator

hey @ace-of-aces, what's the status of this PR? Is it ready to be reviewed or tested? Is there anything I can help with?

@ace-of-aces
Copy link
Copy Markdown
Contributor Author

Hi @TitasGailius, glad to hear back from you!

I've tested this manually, so it should work in its current state.
FYI: My pure php port of this PR has a proper set of tests.

There are still some open questions for me:

  1. I'd be happy to hear your opinion on my previous suggestion in Improve Support for PestPHP via IDE Helpers #532 (comment).
  2. As you mentioned in Improve Support for PestPHP via IDE Helpers #532 (comment) you'd be open to moving helper files into the .vscode. Currently, this PR uses a random new .ide-helper directory on the root level (not ideal of course). Which subfolder of .vscode would be appropriate? Should this directory be configurable by users?

So I think it would be great to get a clear picture on this before moving on with a full review :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Better Intellisense for PestPHP

4 participants