Skip to content

fix: detect PHPUnit #[Test] attribute with fully-qualified name#121

Open
zoispag wants to merge 1 commit into
zed-extensions:mainfrom
zoispag:fix/phpunit-fqdn-attribute-runnable
Open

fix: detect PHPUnit #[Test] attribute with fully-qualified name#121
zoispag wants to merge 1 commit into
zed-extensions:mainfrom
zoispag:fix/phpunit-fqdn-attribute-runnable

Conversation

@zoispag

@zoispag zoispag commented May 12, 2026

Copy link
Copy Markdown

Problem

When a PHPUnit test method is annotated with the fully-qualified attribute form:

#[\PHPUnit\Framework\Attributes\Test]
public function it_does_something(): void { ... }

Zed does not show the ▷ gutter play button, so the test cannot be run from the editor. Only the short imported form #[Test] triggers the runnable.

Root cause

The runnables.scm query for the #[Test] case only matches a simple name node inside the attribute:

(attribute (name) @_attribute)
(#eq? @_attribute "Test")

For a fully-qualified attribute, the tree-sitter PHP grammar produces a qualified_name node instead of a bare name. According to the grammar definition, qualified_name has the structure:

(qualified_name
  prefix: (... \PHPUnit\Framework\Attributes\ ...)
  (name) "Test"   <-- direct child, NOT inside the prefix
)

The namespace segments (PHPUnit, Framework, Attributes) are nested inside the prefix field, while the class name (Test) is a direct name child of qualified_name. The existing query never reaches this node.

Fix

Add an alternative branch (qualified_name (name) @_attribute) inside the attribute match so both the short and fully-qualified forms are handled:

(attribute
    [
        (name) @_attribute
        (qualified_name (name) @_attribute)
    ]
)
(#eq? @_attribute "Test")

This covers:

  • #[Test] — short form (existing, unchanged)
  • #[PHPUnit\Framework\Attributes\Test] — relative qualified form
  • #[\PHPUnit\Framework\Attributes\Test] — fully-qualified form (global namespace prefix)

Extends the phpunit-test runnable query to also match methods annotated
with a fully-qualified attribute such as:

    #[\PHPUnit\Framework\Attributes\Test]

Previously only the short form `#[Test]` was matched, because the query
only handled `(attribute (name))`. For a FQDN attribute the tree-sitter
node is a `qualified_name` whose direct `name` child is the class name
("Test"); the namespace segments live inside the nested `prefix` field
and are not matched.

The fix adds an alternative branch `(qualified_name (name) @_attribute)`
alongside the existing `(name) @_attribute` inside the attribute match,
so both forms produce the gutter play button.
@cla-bot

cla-bot Bot commented May 12, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have @zoispag on file. You can sign our CLA at https://zed.dev/cla. Once you've signed, post a comment here that says '@cla-bot check'.

@zoispag

zoispag commented May 12, 2026

Copy link
Copy Markdown
Author

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label May 12, 2026
@cla-bot

cla-bot Bot commented May 12, 2026

Copy link
Copy Markdown

The cla-bot has been summoned, and re-checked this pull request!

roxblnfk added a commit to roxblnfk/zed-php that referenced this pull request Jul 2, 2026
- Require a base_clause on the name-convention (*Test) patterns: a PHPUnit
  test class always extends TestCase (directly or transitively), so a *Test
  class with no `extends` is not PHPUnit and is no longer tagged phpunit-test
  (it is most likely a test from another framework).
- Disambiguate method-level #[Test] by the imported attribute
  (PHPUnit\Framework\Attributes\Test) via the file's `use`, and by a
  fully-qualified #[\PHPUnit\Framework\Attributes\Test], instead of the class
  name — so it no longer misfires on another framework's method-level #[Test].
  This supersedes zed-extensions#121, which matched any attribute whose last name segment is
  "Test" (and would also tag e.g. #[\Testo\Test] or #[\App\Test] as PHPUnit).
@roxblnfk

roxblnfk commented Jul 2, 2026

Copy link
Copy Markdown

Thanks for tackling this — the fully-qualified #[Test] gutter gap is real.

One thing to consider about this approach: capturing only the last segment of the qualified name and checking (#eq? @_attribute "Test") matches any attribute whose class name happens to be Test, regardless of namespace. So it would also light up a PHPUnit gutter for other frameworks' attributes — e.g. #[\Testo\Test] (Testo) or a project's own #[\App\Test] — since they all end in \Test.

To attribute it specifically to PHPUnit, it's safer to match the whole name:

(attribute (qualified_name) @_attribute)
(#eq? @_attribute "\\PHPUnit\\Framework\\Attributes\\Test")

For the short #[Test] form (where the FQN isn't written out) the framework can be told apart by the file's use import, since PHP forbids importing two different classes under the same alias.

I ran into the same problem while adding Testo support and included this fix — the fully-qualified PHPUnit form plus the use-based disambiguation of the short form — in #127

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants