Skip to content

fix: recognize bound trait methods on $this in test closures - #8

Open
maks-oleksyuk wants to merge 1 commit into
pestphp:5.xfrom
maks-oleksyuk:fix/this-type-bound-trait-methods
Open

fix: recognize bound trait methods on $this in test closures#8
maks-oleksyuk wants to merge 1 commit into
pestphp:5.xfrom
maks-oleksyuk:fix/this-type-bound-trait-methods

Conversation

@maks-oleksyuk

Copy link
Copy Markdown

Problem

$this->someMethod() inside a test closure is reported as Call to an undefined method whenever someMethod() comes from a trait bound via pest()->use() / uses(), rather than a real extend()-ed class. TestClosureThisTypeExtension already resolves the bound classes/traits for $this's type, but it explicitly filters out traits when building the ObjectType for $this — a trait isn't a valid ObjectType target, so there's no way to represent "also has these trait methods" in $this's inferred type directly.

In practice this forces manual @phpstan-ignore-next-line method.notFound workarounds for very common patterns, e.g.:

uses(\phpmock\phpunit\PHPMock::class);

it('mocks a global function', function (): void {
    /** @phpstan-ignore-next-line method.notFound */
    $this->getFunctionMock('App\Some\Namespace', 'some_function');
});

Fix

Add BoundTraitMethodCallIgnoreExtension, an IgnoreErrorExtension that suppresses method.notFound specifically when:

  • the call is $this->someMethod(),
  • and the current file's Pest config — resolved via the existing PestConfigReader, covering both file-scoped uses()/pest()->use() and directory-scoped .in() bindings — declares a bound trait that natively has someMethod().

This is scoped per-file via Scope::getFile(), so it can't leak a trait method's availability onto unrelated test files that don't bind that trait — unlike a global "mixin" registration, which would apply everywhere regardless of which file is being analysed.

I considered representing this precisely in $this's actual type instead (e.g. a custom Type decorator overriding hasMethod()/getMethod()), which would also give correct return-type inference for calls into the trait. I went with the ignore-extension approach instead because it mirrors the existing, already-established pattern for this kind of known-safe suppression in this package (ProtectedMethodCallIgnoreExtension, PestInternalClassAccessIgnoreExtension, ArchExpectationPropertyIgnoreExtension), and is a much smaller, lower-risk surface than hand-implementing PHPStan's Type interface. $this's inferred type itself is unchanged — confirmed by the existing local-uses-trait-only.php fixture, which still asserts $this types as plain PHPUnit\Framework\TestCase when only a trait is bound.

Testing

  • New unit tests in BoundTraitMethodCallIgnoreExtensionTest.php: the positive case (method declared on a bound trait), a method not declared on any bound trait, a non-method.notFound error identifier, and a method call on a variable other than $this.
  • pest --parallel: all tests pass. phpstan analyse: no errors. pint / rector --dry-run: clean.
  • Verified against real-world usage in a consuming app: a test file doing uses(PHPMock::class); ... $this->getFunctionMock(...) inside an it() closure no longer needs a manual @phpstan-ignore-next-line.

`$this->someMethod()` inside a test closure was reported as
`method.notFound` whenever `someMethod()` came from a trait bound via
`pest()->use()`/`uses()`, since a trait isn't a valid ObjectType and
can't be mixed into `$this`'s inferred type. This forced manual
`@phpstan-ignore-next-line` workarounds for common patterns like
`use PHPMock;` in a test file.

Add `BoundTraitMethodCallIgnoreExtension`, which suppresses that
specific `method.notFound` error when the current file's Pest config
(file-scoped `uses()` or directory-scoped `.in()` bindings, via
`PestConfigReader`) declares a bound trait that natively has the
called method. It's scoped per-file via `Scope::getFile()`, so it
doesn't leak trait methods onto unrelated test files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant