cucumber / cucumber/language-service
Support PHP 8 Attributes as step definition annotations (#[Given], #[When], #[Then])
- Dominant language
- TypeScript
- Stars
- 29
- Forks
- 44
- PR merge metrics
- No merged PRs in 30d
Description
### 🤔 What's the problem you're trying to solve?
Behat 3.x introduced first-class support for **PHP 8 Attributes** as an alternative to docblock annotations for declaring step definitions and hooks (see [Behat docs](https://behat.org/en/latest/user_guide/context/definitions.html)):
```php
use Behat\Step\Given;
use Behat\Step\When;
use Behat\Step\Then;
final class TestRepositoryContext implements Context
{
#[Given('a test repository instance exists')]
public function assertRepositoryInjected(): void { ... }
#[When('I request the entity class name managed by the repository')]
public function requestEntityClassName(): void { ... }
#[Then('I should get the fully qualified class name of TestEntity')]
public function assertEntityClassNameIsTestEntity(): void { ... }
}
```
The `cucumber/language-service` currently only recognises **docblock annotations** (`@Given`, `@When`, `@Then`) as step definition markers when scanning PHP glue files. PHP 8 Attribute-based definitions are completely invisible to the language service, which produces two problems:
1. **All Gherkin steps are underlined as "undefined"** in the editor, even though they are correctly wired at runtime.
2. **"Go to step definition" and autocomplete do not work** — the language service cannot resolve the link between a `.feature` step and its implementing method.
This makes Attribute-based Behat projects essentially broken from an IDE-assistance perspective, despite being fully functional at test execution time.
### ✨ What's your proposed solution?
Extend the PHP step definition parser in `cucumber/language-service` to recognise PHP 8 Attributes from the following namespaces alongside the existing docblock strategy:
| Attribute class | Equivalent docblock |
|---|---|
| `Behat\Step\Given` | `@Given` |
| `Behat\Step\When` | `@When` |
| `Behat\Step\Then` | `@Then` |
| `Behat\Step\Step` | (generic) |
The pattern to match in PHP source files:
```php
// Fully-qualified in use statement, then short form on method:
use Behat\Step\Given;
#[Given('some step text')]
public function someMethod(): void { ... }
// Or fully-qualified inline:
#[\Behat\Step\Given('some step text')]
public function someMethod(): void { ... }
```
The step expression (the string argument to the attribute constructor) should be extracted exactly the same way as the pattern in a docblock annotation, so all existing expression parsing (Cucumber Expressions, regex) continues to apply unchanged.
### ⛏ Have you considered any alternatives or workarounds?
- **Keeping docblock annotations**: Reverting to `/** @Given('...') */` works for the language service but goes against the direction of modern PHP (attributes are the idiomatic, statically-analysable mechanism since PHP 8.0) and produces a split-style codebase when the rest of the project uses attributes.
- **Duplicating both forms**: Writing both a docblock and an attribute on every method is noisy and error-prone.
- **Custom `cucumber.glue` regex override**: The extension does not currently expose a hook to customise how step definitions are extracted from source files.
None of these are satisfying for a project that has deliberately adopted PHP 8 Attributes as its annotation standard.
### 📚 Any additional context?
- **Behat version that introduced Attribute support**: Behat 3.x (PHP 8 attributes). The official Behat docs now show attributes as the primary example.
- **Relevant Behat namespaces**: `Behat\Step\Given`, `Behat\Step\When`, `Behat\Step\Then`, `Behat\Step\Step`; hooks use `Behat\Hook\BeforeScenario`, `Behat\Hook\AfterScenario`, etc.
- **PHP version floor**: PHP 8.0+ (attributes are not available on PHP 7.x, so the parser can safely assume PHP 8 syntax when it encounters `#[`).
- **Existing language-service PHP parser**: The fix is likely localised to the PHP step definition extractor — the attribute argument string is a plain PHP string literal in the same position as the docblock pattern string, so the expression-parsing layer should need no changes.
- **Impact**: Any Behat project targeting PHP 8+ that follows the current Behat documentation will hit this issue out of the box.
Contributor guide
Research direction
Start at the PHP step definition extractor and compare its existing docblock annotation handling with the requested Behat\Step attributes. Confirm that Given, When, Then, and generic Step attributes are detected and their string arguments use the existing expression parsing; verify that feature steps no longer appear undefined and can resolve to their definitions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php, typescript
- Domain
- devtools
- Issue type
- Feature
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 56/100