composer installcomposer testThis runs in order:
- Code quality checks (Rector dry-run)
- Code style checks (Laravel Pint dry-run)
- Static analysis (PHPStan at max level)
- Unit tests (Pest)
composer lint # Apply Rector + Pint fixes
composer test:lint # Check without applying changes
composer test:types # Run PHPStan
composer test:unit # Run Pest testsPestStan/
├── src/Type/Pest/
│ ├── PestFunctionReturnTypeExtension.php # Pest global helper return types
│ ├── ExpectationMethodReturnTypeExtension.php # Type narrowing for assertion methods
│ ├── OppositeExpectationMethodReturnTypeExtension.php # not() return types
│ ├── TestClosureThisTypeExtension.php # $this binding in closures
│ ├── PestConfigReader.php # Pest.php auto-detection for TestCase
│ ├── TestCallMethodsClassReflectionExtension.php # TestCall fluent methods from Pest mixins and custom testCaseClass
│ ├── TestCasePropertiesExtension.php # Dynamic property support
│ └── PestTestCaseProperty.php # PropertyReflection for dynamic props
├── tests/
│ ├── Pest.php # Pest bootstrap
│ ├── TestCase.php # Base test case
│ ├── CustomTestCaseTestCase.php # Test case for custom TestCase config
│ ├── extension.neon # Test PHPStan config
│ └── Type/
│ ├── ExpectTypeTest.php # Main test runner
│ ├── CustomTestCaseTest.php # Custom TestCase test runner
│ ├── Fixtures/
│ │ └── CustomTestCase.php # Custom test case fixture for $this and TestCall method coverage
│ ├── custom-testcase-extension.neon # PHPStan config for custom TestCase tests
│ └── data/
│ ├── expect-function.php # expect() return type tests
│ ├── expectation-methods.php # Assertion method type tests
│ ├── test-closures.php # $this binding + dynamic property tests
│ ├── test-closures-custom-testcase.php # Custom TestCase $this binding tests
│ ├── test-call-methods.php # TestCall chaining tests
│ ├── pest-functions.php # Pest global helper return type tests
│ └── arch-expectations.php # Architecture testing type tests
├── extension.neon # PHPStan extension config
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── pint.json
└── rector.php
All type information is provided through PHPStan dynamic type extensions (no stubs).
DynamicFunctionReturnTypeExtension that overrides return types for Pest global helpers:
expect($value)→Expectation<typeof $value>(removes|nullfrom Pest's phpdoc)pest()→Configurationuses(...)→UsesCallit()/test()/todo()→TestCalldescribe()→DescribeCallbeforeEach()/afterEach()→ pending call wrappersfixture()→stringwhen the installed Pest version provides the helperbeforeAll()/afterAll()/dataset()/covers()/mutates()→null
DynamicMethodReturnTypeExtension for Pest\Expectation that intercepts methods resolved through the @mixin Pest\Mixins\Expectation annotation:
- Type-narrowing methods (
toBeString,toBeInt, etc.) returnExpectation<narrowedType> - All other assertion methods preserve the caller's generic type parameter
FunctionParameterClosureThisExtension that binds $this in closures passed to it(), test(), describe(), beforeEach(), afterEach(), beforeAll(), and afterAll().
The $this type is resolved in this order:
- Auto-detect:
PestConfigReaderparsesPest.phpfiles to finduses(X::class)->in(...)orpest()->extend(X::class)->in(...)and maps directories to TestCase classes. The longest-matching directory prefix wins. - Manual fallback: Falls back to
peststan.testCaseClassparameter (default:PHPUnit\Framework\TestCase).
Parses Pest.php configuration files using nikic/php-parser with NameResolver to resolve fully-qualified class names from use statements. Discovers Pest.php files automatically from PHPStan's analysis paths, or from explicit peststan.pestConfigFiles.
PropertiesClassReflectionExtension that allows dynamic property access on TestCase subclasses. Pest supports $this->prop = value in beforeEach() closures, accessible in test closures. This extension returns mixed for any undefined property on a TestCase subclass.
Requires universalObjectCratesClasses for PHPUnit\Framework\TestCase (set automatically in extension.neon) because PHPStan only consults PropertiesClassReflectionExtension when allowsDynamicProperties() returns true.
Registers all extensions with PHPStan. Configures universalObjectCratesClasses for dynamic property support and ignoreErrors for Pest's @internal class annotations. Auto-loaded via phpstan/extension-installer or manually included.
MethodsClassReflectionExtension that exposes fluent TestCall methods coming from Pest mixins and the configured custom peststan.testCaseClass.
- Resolves methods from
Pest\Concerns\TestableandPest\Support\HigherOrderCallables - Adds public methods from a custom
testCaseClasswhen it differs fromPHPUnit\Framework\TestCase - Skips native
TestCallmethods so custom helpers do not shadow real API methods
The semantic analysis layer is intentionally narrow and stable.
ExpectationSemanticAnalyzeris the read-only semantic entry point used by rules.ExpectationChainStateResolvercomputes fluent expectation state and broken-chain suppression.ExpectationTypeNarrowerperforms conservative narrowing and overlap checks for propagated expectation values.ExpectationMatcherRegistryand the matcher metadata registries expose the semantic matcher contract without coupling the rules to individual method switches.PestDiagnostic,PestDiagnostics, andPestDiagnosticIdentifiersdefine the diagnostics contract surface that downstream tools can serialize and consume.
This boundary is deliberate: PestStan analyzes and emits stable semantic diagnostics; rector-pest can consume those diagnostics for safe remediation, but the analyzer itself does not perform transformations.
Tests use PHPStan's TypeInferenceTestCase to verify type assertions. Each test data file uses assertType() to declare expected types, and the test runner verifies PHPStan agrees.
Important: The TestCase class overrides getAdditionalConfigFiles() to load tests/extension.neon. This must happen at class definition time (not in beforeAll) because PHPStan's gatherAssertTypes() creates its container before Pest's beforeAll runs.
- Rector: Automated refactoring for PHP 8.2
- Laravel Pint: PSR-12 code style
- PHPStan: Level max with strict rules
- Pest: Test framework