From 91b96b8f1983af0654af9662e79b59ec3326efbc Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 10 Aug 2026 21:30:52 +0800 Subject: [PATCH 1/7] reorganize documentation sections --- README.md | 7 +- docs/.vitepress/config.mts | 40 ++-- .../how-it-works.md | 0 .../type-aliases.md | 0 .../generics-and-bounds.md | 0 .../cli-commands.md} | 0 docs/index.md | 130 +++++++------ docs/public/laravel-error-screen.png | Bin 0 -> 78216 bytes docs/public/pest-error-screen.png | Bin 0 -> 27898 bytes docs/supported-types/type-aliases.md | 177 ++++++++++++++++++ docs/{advanced => }/troubleshooting.md | 0 11 files changed, 281 insertions(+), 73 deletions(-) rename docs/{architecture => advanced}/how-it-works.md (100%) rename docs/{core-concepts => advanced}/type-aliases.md (100%) rename docs/{core-concepts => generics}/generics-and-bounds.md (100%) rename docs/{production/cache-commands.md => getting-started/cli-commands.md} (100%) create mode 100644 docs/public/laravel-error-screen.png create mode 100644 docs/public/pest-error-screen.png create mode 100644 docs/supported-types/type-aliases.md rename docs/{advanced => }/troubleshooting.md (100%) diff --git a/README.md b/README.md index 3b0cb52..2052ed8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@

TypePHP

+

+ No transpilation. No build steps. No C-extensions.
+ Drop TypePHP into your existing codebase and let your DocBlocks scream when types fail.
+

+

Build Status Latest Stable Version @@ -11,7 +16,7 @@ ------ -TypePHP is the first pure-PHP library that transparently enforces extended PHPDoc type contracts (generics, array shapes, scalar refinements, and callables) at runtime during execution, without introducing any new syntax or requiring C-extensions. +TypePHP is a transparent, pure-PHP runtime type checker. You don't have to refactor a single line of your codebase, setup complex build toolchains, or compile C-extensions and simply run your existing code, and TypePHP will enforce your extended PHPDoc contracts (generics, array shapes, `key-of`/`value-of` extractions, and scalar refinements) dynamically at runtime. **[Read the full TypePHP documentation »](https://typephp-php.github.io/typephp/)** diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 7dc502b..68bc05b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -9,8 +9,9 @@ export default defineConfig({ nav: [ { text: 'Home', link: '/' }, { text: 'Documentation', link: '/getting-started/installation' }, - { text: 'Architecture', link: '/architecture/how-it-works' }, - { text: 'CLI', link: '/production/cache-commands' }, + { text: 'Generics', link: '/generics/generics-and-bounds' }, + { text: 'CLI', link: '/getting-started/cli-commands' }, + { text: 'FAQ', link: '/troubleshooting' }, { text: 'GitHub', link: 'https://github.com/typephp-php/typephp' } ], sidebar: [ @@ -20,52 +21,57 @@ export default defineConfig({ { text: 'Installation', link: '/getting-started/installation' }, { text: 'Quick Start', link: '/getting-started/quick-start' }, { text: 'Configuration', link: '/getting-started/configuration' }, + { text: 'CLI Commands', link: '/getting-started/cli-commands' }, ] }, { - text: 'Architecture', - items: [ - { text: 'How It Works', link: '/architecture/how-it-works' }, - ] - }, - { - text: 'Core Concepts', + text: 'Enforcement Boundaries', items: [ { text: 'Function Contracts', link: '/core-concepts/function-contracts' }, - { text: 'Inline Variables', link: '/core-concepts/inline-variables' }, { text: 'Property Validation', link: '/core-concepts/property-validation' }, - { text: 'Generics & Bounds', link: '/core-concepts/generics-and-bounds' }, - { text: 'Type Aliases', link: '/core-concepts/type-aliases' }, + { text: 'Inline Variables', link: '/core-concepts/inline-variables' }, ] }, { - text: 'Supported Types', + text: 'Type Reference', items: [ { text: 'Primitives & Scalars', link: '/supported-types/primitives-and-scalars' }, { text: 'Arrays & Shapes', link: '/supported-types/arrays-and-shapes' }, { text: 'Callables & Closures', link: '/supported-types/callables-and-closures' }, { text: 'Iterators & Generators', link: '/supported-types/iterators-and-generators' }, { text: 'Unions, Intersections & Conditionals', link: '/supported-types/unions-intersections-and-conditionals' }, + { text: 'Type Aliases', link: '/supported-types/type-aliases' }, + ] + }, + { + text: 'Runtime Generics', + items: [ + { text: 'Generics & Bounds', link: '/generics/generics-and-bounds' }, ] }, { - text: 'Advanced Features', + text: 'Advanced & Architecture', items: [ + { text: 'How It Works', link: '/advanced/how-it-works' }, { text: 'Liskov & Inheritance', link: '/advanced/liskov-and-inheritance' }, { text: 'Vendor Isolation', link: '/advanced/vendor-and-path-filtering' }, { text: 'Ignore Annotations', link: '/advanced/ignore-annotations' }, { text: 'Extensions', link: '/advanced/extensions' }, { text: 'Exception Handling', link: '/advanced/exception-handling' }, - { text: 'Troubleshooting & FAQ', link: '/advanced/troubleshooting' } ] }, { - text: 'Production & Performance', + text: 'Production & Operations', items: [ { text: 'Production Readiness', link: '/production/production-readiness' }, - { text: 'Cache CLI Commands', link: '/production/cache-commands' }, { text: 'Performance Considerations', link: '/production/performance-considerations' }, ] + }, + { + text: 'Help & Support', + items: [ + { text: 'Troubleshooting & FAQ', link: '/troubleshooting' }, + ] } ], socialLinks: [ diff --git a/docs/architecture/how-it-works.md b/docs/advanced/how-it-works.md similarity index 100% rename from docs/architecture/how-it-works.md rename to docs/advanced/how-it-works.md diff --git a/docs/core-concepts/type-aliases.md b/docs/advanced/type-aliases.md similarity index 100% rename from docs/core-concepts/type-aliases.md rename to docs/advanced/type-aliases.md diff --git a/docs/core-concepts/generics-and-bounds.md b/docs/generics/generics-and-bounds.md similarity index 100% rename from docs/core-concepts/generics-and-bounds.md rename to docs/generics/generics-and-bounds.md diff --git a/docs/production/cache-commands.md b/docs/getting-started/cli-commands.md similarity index 100% rename from docs/production/cache-commands.md rename to docs/getting-started/cli-commands.md diff --git a/docs/index.md b/docs/index.md index 7a23df7..29f8b6a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ layout: home hero: name: "TypePHP" text: "Transparent Runtime Type Enforcement" - tagline: "The first pure PHP library to enforce DocBlock types at runtime transparently without introducing any new syntax. Validates generics, array shapes, and advanced type contracts during execution." + tagline: "No transpilation. No build steps. No C-extensions. Just 100% pure PHP that makes your existing DocBlocks scream the moment types fail." actions: - theme: brand text: "Get Started →" @@ -16,17 +16,52 @@ hero: features: - title: "Zero Production Overhead" details: "Install as a development dependency to enforce strict types during local testing and CI/CD pipelines, guaranteeing absolute zero performance cost in live production environments." + - title: "No Transpilation or C-Extensions" + details: "Operates 100% in pure PHP user-land using native stream wrappers and AST transformations. No build scripts, Node.js tools, or C-extensions required." - title: "True Runtime Generics" details: "Binds generic template types to specific object instances dynamically using native WeakMap memory tracking." - - title: "Typed Arrays & Shapes" - details: "Deeply validates sequential lists, typed class arrays, and strict associative array shape structures right out of the box." - - title: "PHP 8.4 Support" - details: "Native support for intercepting and validating PHP 8.4 Property Hooks (get/set) and Asymmetric Visibility (public private(set))." + - title: "Arrays, Shapes & Extractions" + details: "Deeply validates sequential lists, typed arrays, array shapes, and key-of / value-of constant extractions out of the box." --- +::: tip Pure PHP • Zero Transpilation • Zero Build Steps +**You don't have to change a single line of code, and you don't need a compilation build toolchain.** TypePHP operates entirely in native PHP user-land and no custom PHP binaries, C-extensions, or Node.js transpilers needed. Drop TypePHP into your existing project, run your code, and your DocBlocks will instantly start screaming at runtime when dynamic data violates a type contract. +::: + ## See It In Action -TypePHP is the first pure PHP library that operates entirely in user-land using native stream wrappers and AST transformations. Because it requires no C-extensions or FFI, you can drop it into any PHP 8.1+ project effortlessly. It parses your standard PHPDoc annotations and enforces them the moment your code runs. +TypePHP operates entirely in user-land using native stream wrappers and AST transformations. Because it requires no C-extensions or FFI, you can drop it into any PHP 8.1+ project or web framework effortlessly. It reads your existing PHPDoc annotations and enforces them the moment your code runs. + +### Real-World Framework Guard Rails (Laravel / Symfony) +Prevent dynamic data bugs from leaking into database queries or API responses: + +```php +namespace App\Models; + +use App\Enums\Role; +use Illuminate\Database\Eloquent\Model; + +class User extends Model +{ + /** + * @return list + */ + public function assignableRoles(): array + { + if ($this->isSuperAdmin()) { + // Bug! Returns an array of Role Enum instances instead of integers: + return Role::cases(); + } + + return [Role::STAFF->value]; + } +} + +// Executing $user->assignableRoles() throws: +// TypePHP\Exception\TypeError: User::assignableRoles(): Return value[0] must be of type int, App\Enums\Role returned +``` + +--- ### True Runtime Generics Define generic templates and TypePHP will track their state in memory per object instance: @@ -51,68 +86,53 @@ $users->add(new Product('SKU-100')); // Throws TypeError: Argument $item (template T = User) must be of type User, Product given ``` -### Array Shapes & Typed Arrays -Enforce strict associative array structures and collections of specific objects: - -```php -/** - * @param array{status: 'active'|'pending', tags: list} $options - * @param User[] $collaborators - */ -function processBatch(array $options, array $collaborators): void -{ - // ... -} +--- -processBatch( - options: ['status' => 'active', 'tags' => ['php', 'types']], - collaborators: [new User(), new User()] -); // Valid +### Array Shapes & Key/Value Extractions +Enforce strict associative array structures and constant extractions: -processBatch( - options: ['status' => 'archived', 'tags' => ['php']], - collaborators: [] -); -// Throws TypeError: Argument $options['status'] must be of type ('active' | 'pending') -``` +```php +namespace App\Services; -### Scalar Refinements & Function Boundaries -Catch invalid parameters before your function executes, and invalid return values before they leak out: +use App\Database\DriverManager; -```php /** - * @param positive-int $id - * @return non-empty-string + * @phpstan-type ConnectionParams array{ + * driver: key-of, + * driverClass?: value-of + * } */ -function generateUserToken(int $id): string +class DatabaseService { - return ""; // Throws TypeError: Return value must be of type non-empty-string + /** + * @param ConnectionParams $params + */ + public function connect(array $params): void + { + // ... + } } -generateUserToken(-5); -// Throws TypeError: Argument $id must be of type positive-int, negative int (-5) given +$service = new DatabaseService(); + +$service->connect(['driver' => 'pdo_mysql']); // Valid + +$service->connect(['driver' => 'pdo_invalid']); +// Throws TypeError: Argument $params['driver'] must be a key of DriverManager::DRIVER_MAP ``` --- -## Precise Stack Trace & Error Reporting +## Precise Call-Site Trace Attribution -TypePHP injects single-line guard rails without shifting your source file line numbers. +A common problem with AST code injection is that adding new statements pushes subsequent code down, causing line numbers in stack traces to drift out of sync. -When an inline variable or type contract fails, framework error handlers and test runners (like Pest, PHPUnit, and Whoops) point **directly to the exact line number** where the invalid assignment or argument occurred in your application code: +TypePHP solves this with **Zero Line-Drift Formatting**. Injected guard rails are squashed onto single lines and appended directly to existing code blocks. **Line numbers in your source files remain 100% identical before and after transformation.** -``` - FAILED Tests\SomeTest > test - - TypeError: Variable $typeArray[3] must be of type int, string '1' given - - at tests/SomeTest.php:7 - 3| declare(strict_types=1); - 4| - 5| test('test', function () { - 6| /** @var array */ - ➜ 7| $typeArray = [1, 2, 3, '1']; - 8| - 9| expect($typeArray)->toBeArray(); - 10| }); -``` +When a type contract fails, web exception handlers (**Laravel Ignition, Whoops, Symfony ErrorHandler**) and CLI test runners (**Pest, PHPUnit**) point **directly to the exact line number** where the invalid assignment or return value occurred in your application code: + +### Web Framework Trace (Laravel Ignition) +![Laravel Ignition Exception Trace](/laravel-error-screen.png) + +### CLI Test Runner Trace (Pest PHP) +![Pest CLI Exception Trace](/pest-error-screen.png) diff --git a/docs/public/laravel-error-screen.png b/docs/public/laravel-error-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..3b54539760c0d51e579bbeac93501934b9099511 GIT binary patch literal 78216 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�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 Q{)ga^w1QOj1HCu@3$EXwLI3~& literal 0 HcmV?d00001 diff --git a/docs/public/pest-error-screen.png b/docs/public/pest-error-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..a0de0814dad0f46eb8d3172dd3fe624d36c38c8b GIT binary patch literal 27898 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%0AYFb$rEM5>gZZZEIUF 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 JIdk>ye*uQFE?xit literal 0 HcmV?d00001 diff --git a/docs/supported-types/type-aliases.md b/docs/supported-types/type-aliases.md new file mode 100644 index 0000000..2cfa125 --- /dev/null +++ b/docs/supported-types/type-aliases.md @@ -0,0 +1,177 @@ +# Type Aliases + +TypePHP supports declaring local type aliases (`@phpstan-type` / `@psalm-type`) and importing type aliases from other classes (`@phpstan-import-type` / `@psalm-import-type`). This allows you to centralize and reuse complex array shapes, unions, and generic structures across your application. + +> **Tooling Compatibility:** Both PHPStan syntax (`@phpstan-type`, `@phpstan-import-type`) and Psalm syntax (`@psalm-type`, `@psalm-import-type`) are parsed identically and enforced at runtime. + +--- + +## Local Type Aliases (`@phpstan-type` / `@psalm-type`) + +Declare a local type alias above a class or interface definition using `@phpstan-type` or `@psalm-type`. Once declared, you can reference the alias in any parameter, return, or `@var` docblock within that class: + +```php +updateUser(['id' => 10, 'username' => 'Alice', 'role' => 'admin'], 'active'); + +// Invalid Call ($id is negative, violating UserShape) +$service->updateUser(['id' => -5, 'username' => 'Alice', 'role' => 'admin'], 'active'); +// Throws: TypeError: UserService::updateUser(): Argument $user['id'] must be of type positive-int +``` + +--- + +## Imported Type Aliases (`@phpstan-import-type` / `@psalm-import-type`) + +To share type aliases across multiple classes, declare your aliases in a central class (e.g. `GlobalTypes`) and import them into other classes using `@phpstan-import-type` or `@psalm-import-type`: + +### Central Type Definitions (`GlobalTypes.php`) + +```php +namespace App\Types; + +/** + * Shared Type Definitions + * + * @phpstan-type SharedUserShape array{id: positive-int, email: non-empty-string} + * @psalm-type SharedRole 'admin'|'user' + */ +class GlobalTypes +{ +} +``` + +### Importing the Shared Type Alias (`UserApi.php`) + +```php +namespace App\Api; + +use App\Types\GlobalTypes; + +/** + * Import shared types from GlobalTypes + * + * @phpstan-import-type SharedUserShape from GlobalTypes + * @psalm-import-type SharedRole from GlobalTypes + */ +class UserApi +{ + /** + * @param SharedUserShape $user + * @param SharedRole $role + */ + public function saveUser(array $user, string $role): bool + { + return true; + } +} + +$api = new UserApi(); + +// Valid Call +$api->saveUser(['id' => 42, 'email' => 'alice@example.com'], 'admin'); + +// Invalid Call ($email is empty string) +$api->saveUser(['id' => 42, 'email' => ''], 'admin'); +// Throws: TypeError: UserApi::saveUser(): Argument $user['email'] must be of type non-empty-string +``` + +--- + +## Importing with Local Alias Renaming (`as`) + +Use the `as` keyword to rename an imported type alias locally to prevent naming collisions or improve local code clarity: + +```php +namespace App\Services; + +use App\Types\GlobalTypes; + +/** + * Import and rename the shared type alias + * + * @phpstan-import-type SharedUserShape from GlobalTypes as LocalUserShape + */ +class AccountService +{ + /** + * @param LocalUserShape $payload + */ + public function createAccount(array $payload): void + { + // ... + } +} +``` + +--- + +## Naming Collisions (When `as` is Omitted) + +If a class defines a local `@phpstan-type Status` AND imports a type alias with the exact same name (`@phpstan-import-type Status from GlobalTypes`) **without using the `as` keyword**: + +1. **Resolution Priority:** The imported type alias will **overwrite** the local type alias. +2. **Best Practice:** Always use the `as` keyword whenever an imported alias name collides with a local alias name to make your type contracts explicit: + +```php +/** + * Local alias: 'active'|'pending' + * @phpstan-type Status 'active'|'pending' + * + * Imported alias renamed to GlobalStatus to prevent overwriting local 'Status' + * @phpstan-import-type Status from GlobalTypes as GlobalStatus + */ +class OrderService +{ + // ... +} +``` + +--- + +## Chained Type Alias Imports + +TypePHP recursively resolves multi-level type alias import chains down to the root definition: + +* **Level 1 (`GlobalTypes`):** Defines `@phpstan-type UserShape array{id: positive-int}`. +* **Level 2 (`MidService`):** Imports `@phpstan-import-type UserShape from GlobalTypes`. +* **Level 3 (`FinalService`):** Imports `@phpstan-import-type UserShape from MidService as LocalShape`. + +When `FinalService` validates `$payload` against `LocalShape`, TypePHP automatically follows the 3-class import chain back to `GlobalTypes` and enforces `array{id: positive-int}`! + +```php +$service = new FinalService(); + +// Valid Call +$service->process(['id' => 100]); + +// Invalid Call (id is negative) +$service->process(['id' => -5]); +// Throws: TypeError: FinalService::process(): Argument $payload['id'] must be of type positive-int +``` diff --git a/docs/advanced/troubleshooting.md b/docs/troubleshooting.md similarity index 100% rename from docs/advanced/troubleshooting.md rename to docs/troubleshooting.md From e082391464d6c6869279778a817d3b05e97ac3b7 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 10 Aug 2026 21:36:27 +0800 Subject: [PATCH 2/7] Refactor GenericValidator to handle invalid class syntax gracefully and improve test cases for unsupported type syntax --- src/Validator/GenericValidator.php | 9 ++++++-- tests/SomeTest.php | 10 ++++---- .../IgnoreUnrecognizeDoctypeTest.php | 23 ++++++++++++------- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/Validator/GenericValidator.php b/src/Validator/GenericValidator.php index 5ef417f..23905c1 100644 --- a/src/Validator/GenericValidator.php +++ b/src/Validator/GenericValidator.php @@ -111,7 +111,7 @@ private function validateKeyOf(mixed $value, GenericTypeNode $node, string $cont $enumClass = $targetType->name; if (ClassNameValidator::isValid($enumClass) && enum_exists($enumClass)) { if (! isset(self::$enumKeyCache[$enumClass])) { - self::$enumKeyCache[$enumClass] = array_map(fn ($case) => $case->name, $enumClass::cases()); + self::$enumKeyCache[$enumClass] = array_map(fn($case) => $case->name, $enumClass::cases()); } if (! \in_array($value, self::$enumKeyCache[$enumClass], true)) { @@ -196,7 +196,7 @@ private function validateValueOf(mixed $value, GenericTypeNode $node, string $co $enumClass = $targetType->name; if (ClassNameValidator::isValid($enumClass) && enum_exists($enumClass) && is_subclass_of($enumClass, \BackedEnum::class)) { if (! isset(self::$enumValueCache[$enumClass])) { - self::$enumValueCache[$enumClass] = array_map(fn ($case) => $case->value, $enumClass::cases()); + self::$enumValueCache[$enumClass] = array_map(fn($case) => $case->value, $enumClass::cases()); } if (! \in_array($value, self::$enumValueCache[$enumClass], true)) { @@ -365,9 +365,14 @@ private function validateArray(mixed $value, GenericTypeNode $node, string $cont /** * Validates object generic instances and binds template parameters. + * Gracefully ignores generic annotations with invalid class syntax (e.g. custom-generic). */ private function validateObjectGeneric(mixed $value, GenericTypeNode $node, string $context): ?ErrorMessage { + if (! ClassNameValidator::isValid($node->type->name)) { + return null; + } + if (! \is_object($value)) { return ErrorFactory::createError($context . ' must be an object of type ' . $node->type->name . ', ' . TypeFormatter::formatGivenValue($value) . ' given'); } diff --git a/tests/SomeTest.php b/tests/SomeTest.php index 27e4116..38a8b05 100644 --- a/tests/SomeTest.php +++ b/tests/SomeTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -// test('test', function () { -// /** @var array */ -// $typeArray = [1, 2, 3, '1']; +test('test', function () { + /** @var array */ + $typeArray = [1, 2, 3, '1']; -// expect($typeArray)->toBeArray(); -// }); + expect($typeArray)->toBeArray(); +}); diff --git a/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php b/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php index 0cb2978..622b64a 100644 --- a/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php +++ b/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php @@ -12,6 +12,16 @@ function testUnsupportedTypeSyntax(string $specialType): string return $specialType; } +/** + * Custom generic annotation with invalid class syntax (contains hyphens) + * + * @param custom-generic $customGeneric + */ +function testUnsupportedGenericSyntax(string $customGeneric): string +{ + return $customGeneric; +} + /** * Valid PHP class name syntax for a class that does not exist at runtime * @@ -23,17 +33,14 @@ function testNonExistentClassType(mixed $param): mixed } test('ignores custom unsupported type syntax with hyphens gracefully', function () { - $result = testUnsupportedTypeSyntax('special-type'); - - expect($result)->toBe('special-type'); + expect(testUnsupportedTypeSyntax('special-type'))->toBe('special-type'); + expect(testUnsupportedGenericSyntax('hello'))->toBe('hello'); }); test('strictly validates valid class syntax even if class does not exist at runtime', function () { expect(fn () => testNonExistentClassType('hello')) - ->toThrow(TypeError::class, 'must be of type NonExistentClass, string \'hello\' given') - ; + ->toThrow(TypeError::class, 'must be of type NonExistentClass, string \'hello\' given'); expect(fn () => testNonExistentClassType(new stdClass())) - ->toThrow(TypeError::class, 'must be of type NonExistentClass, stdClass given') - ; -}); + ->toThrow(TypeError::class, 'must be of type NonExistentClass, stdClass given'); +}); \ No newline at end of file From 84c3b08fa321365da88486bbe384d7b5acfe8a9d Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 10 Aug 2026 22:24:36 +0800 Subject: [PATCH 3/7] Add support for new types: uppercase-string, non-empty-uppercase-string, and array-key; update validators and tests accordingly --- .../supported-types/primitives-and-scalars.md | 3 + src/Internal/Checker/InlineChecker.php | 24 ++++- src/Resolver/SpecialTypeResolver.php | 4 +- src/Validator/IdentifierValidator.php | 5 +- tests/Fixtures/Types/CurrencyFormatter.php | 27 ++++++ tests/SomeTest.php | 10 +- .../TypeChecking/UppercaseAndArrayKeyTest.php | 97 +++++++++++++++++++ tests/Unit/ValidatorsTest.php | 22 ++++- 8 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 tests/Fixtures/Types/CurrencyFormatter.php create mode 100644 tests/TypeChecking/UppercaseAndArrayKeyTest.php diff --git a/docs/supported-types/primitives-and-scalars.md b/docs/supported-types/primitives-and-scalars.md index a8ce431..d60812d 100644 --- a/docs/supported-types/primitives-and-scalars.md +++ b/docs/supported-types/primitives-and-scalars.md @@ -122,6 +122,9 @@ Validate string lengths, formatting, character casing, and truthiness at runtime | **`numeric-string`** | `is_numeric($val) === true` | `'123'`, `'45.67'`, `'-10'` | `'abc'`, `''` | | **`lowercase-string`** | `strtolower($val) === $val` | `'hello'`, `'user_100'` | `'Hello'`, `'ADMIN'` | | **`non-empty-lowercase-string`** | Non-empty & lowercase | `'hello'`, `'abc'` | `''`, `'Hello'` | +| **`uppercase-string`** | `strtoupper($val) === $val` | `'USD'`, `'HELLO_100'` | `'Hello'`, `'admin'` | +| **`non-empty-uppercase-string`** | Non-empty & uppercase | `'EUR'`, `'ABC'` | `''`, `'Eur'` | +| **`array-key`** | `is_int($val) \|\| is_string($val)` | `100`, `'user_100'` | `true`, `[]`, `null` | | **`literal-string`** | String scalar | `'active'`, `'user'` | Non-strings | | **`truthy-string`**, **`non-falsy-string`** | Evaluates to `true` in boolean context | `'hello'`, `'1'` | `''`, `'0'` | diff --git a/src/Internal/Checker/InlineChecker.php b/src/Internal/Checker/InlineChecker.php index 48bece3..a148f42 100644 --- a/src/Internal/Checker/InlineChecker.php +++ b/src/Internal/Checker/InlineChecker.php @@ -239,7 +239,29 @@ private static function shouldValidateType(TypeNode $node, array $config): bool return $checkArrays; } - if (\in_array($lower, ['int', 'integer', 'string', 'bool', 'boolean', 'float', 'double', 'null', 'true', 'false', 'scalar', 'numeric', 'positive-int', 'negative-int', 'non-empty-string', 'numeric-string', 'truthy', 'falsy'], true)) { + if (\in_array($lower, [ + 'int', + 'integer', + 'string', + 'bool', + 'boolean', + 'float', + 'double', + 'null', + 'true', + 'false', + 'scalar', + 'numeric', + 'positive-int', + 'negative-int', + 'non-empty-string', + 'numeric-string', + 'truthy', + 'falsy', + 'uppercase-string', + 'non-empty-uppercase-string', + 'array-key' + ], true)) { return (bool) ($config['scalars'] ?? false); } diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index e507061..f430e6a 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -528,7 +528,9 @@ private static function isBuiltInTypeKeyword(string $name): bool 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int', 'unsigned-int', 'positive-float', 'negative-float', 'non-positive-float', 'non-negative-float', 'non-zero-float', 'class-string', 'interface-string', 'trait-string', 'enum-string', 'callable-string', 'numeric-string', - 'non-empty-string', 'lowercase-string', 'non-empty-lowercase-string', 'literal-string', 'truthy-string', + 'non-empty-string', 'lowercase-string', 'non-empty-lowercase-string', + 'uppercase-string', 'non-empty-uppercase-string', 'array-key', + 'literal-string', 'truthy-string', 'non-empty-array', 'non-empty-list', 'number', 'numeric', 'truthy', 'falsy', 'falsey', 'min', 'max', '*', 'never', 'never-return', 'never-returns', 'no-return', 'open-resource', 'closed-resource', ], true); diff --git a/src/Validator/IdentifierValidator.php b/src/Validator/IdentifierValidator.php index c820172..0b8a6c2 100644 --- a/src/Validator/IdentifierValidator.php +++ b/src/Validator/IdentifierValidator.php @@ -62,6 +62,9 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali 'non-empty-string' => \is_string($value) && $value !== '', 'lowercase-string' => \is_string($value) && strtolower($value) === $value, 'non-empty-lowercase-string' => \is_string($value) && $value !== '' && strtolower($value) === $value, + 'uppercase-string' => \is_string($value) && strtoupper($value) === $value, + 'non-empty-uppercase-string' => \is_string($value) && $value !== '' && strtoupper($value) === $value, + 'array-key' => \is_int($value) || \is_string($value), 'literal-string' => \is_string($value), 'truthy-string', 'non-falsy-string' => \is_string($value) && (bool) $value === true, 'non-empty-array' => \is_array($value) && \count($value) > 0, @@ -94,4 +97,4 @@ private function validateClassOrIgnore(mixed $value, string $name): bool return \is_object($value) && is_a($value, $name); } -} +} \ No newline at end of file diff --git a/tests/Fixtures/Types/CurrencyFormatter.php b/tests/Fixtures/Types/CurrencyFormatter.php new file mode 100644 index 0000000..5b02703 --- /dev/null +++ b/tests/Fixtures/Types/CurrencyFormatter.php @@ -0,0 +1,27 @@ + */ - $typeArray = [1, 2, 3, '1']; +// test('test', function () { +// /** @var array */ +// $typeArray = [1, 2, 3, '1']; - expect($typeArray)->toBeArray(); -}); +// expect($typeArray)->toBeArray(); +// }); diff --git a/tests/TypeChecking/UppercaseAndArrayKeyTest.php b/tests/TypeChecking/UppercaseAndArrayKeyTest.php new file mode 100644 index 0000000..0629a1b --- /dev/null +++ b/tests/TypeChecking/UppercaseAndArrayKeyTest.php @@ -0,0 +1,97 @@ +toBe(100); + expect(testArrayKeyParam('user_100'))->toBe('user_100'); + }); + + test('throws TypeError when array-key is a boolean or array', function () { + expect(fn () => testArrayKeyParam(true)) + ->toThrow(TypeError::class, 'must be of type array-key'); + + expect(fn () => testArrayKeyParam([])) + ->toThrow(TypeError::class, 'must be of type array-key'); + }); + + }); + + describe('uppercase-string & non-empty-uppercase-string', function () { + + test('accepts valid uppercase strings', function () { + expect(testUppercaseStringParam('USD'))->toBe('USD'); + expect(testUppercaseStringParam(''))->toBe(''); + expect(testNonEmptyUppercaseStringParam('EUR'))->toBe('EUR'); + }); + + test('throws TypeError on lowercase or mixed-case string for uppercase-string', function () { + expect(fn () => testUppercaseStringParam('Usd')) + ->toThrow(TypeError::class, 'must be of type uppercase-string'); + + expect(fn () => testUppercaseStringParam('usd')) + ->toThrow(TypeError::class, 'must be of type uppercase-string'); + }); + + test('throws TypeError on empty string for non-empty-uppercase-string', function () { + expect(fn () => testNonEmptyUppercaseStringParam('')) + ->toThrow(TypeError::class, 'must be of type non-empty-uppercase-string'); + }); + + }); + + describe('Fixture Class Method Verification (CurrencyFormatter)', function () { + + test('validates parameters on CurrencyFormatter class methods', function () { + $formatter = new CurrencyFormatter(); + + expect($formatter->formatAccount('USD', 101))->toBe('USD_101'); + expect($formatter->formatAccount('GBP', 'acc_202'))->toBe('GBP_acc_202'); + + expect(fn () => $formatter->formatAccount('usd', 101)) + ->toThrow(TypeError::class, 'must be of type non-empty-uppercase-string'); + + expect(fn () => $formatter->formatAccount('USD', true)) + ->toThrow(TypeError::class, 'must be of type array-key'); + }); + + test('validates static method returns on CurrencyFormatter', function () { + expect(CurrencyFormatter::sanitizeCode('CAD'))->toBe('CAD'); + + expect(fn () => CurrencyFormatter::sanitizeCode('cad')) + ->toThrow(TypeError::class, 'Return value'); + }); + + }); + +}); \ No newline at end of file diff --git a/tests/Unit/ValidatorsTest.php b/tests/Unit/ValidatorsTest.php index 4cd621a..6c06924 100644 --- a/tests/Unit/ValidatorsTest.php +++ b/tests/Unit/ValidatorsTest.php @@ -124,6 +124,26 @@ function parseType(string $typeString, Lexer $lexer, TypeParser $typeParser): Ty $neverNode = parseType('never', $this->lexer, $this->typeParser); expect($this->registry->validate('returned_value', $neverNode, 'arg'))->toBeInstanceOf(ErrorMessage::class); }); + + test('validates array-key pseudo-type (int|string)', function () { + $arrayKeyNode = parseType('array-key', $this->lexer, $this->typeParser); + + expect($this->registry->validate(123, $arrayKeyNode, 'arg'))->toBeNull(); + expect($this->registry->validate('custom_key', $arrayKeyNode, 'arg'))->toBeNull(); + expect($this->registry->validate(true, $arrayKeyNode, 'arg'))->toBeInstanceOf(ErrorMessage::class); + expect($this->registry->validate([], $arrayKeyNode, 'arg'))->toBeInstanceOf(ErrorMessage::class); + }); + + test('validates uppercase-string and non-empty-uppercase-string', function () { + $uppercase = parseType('uppercase-string', $this->lexer, $this->typeParser); + expect($this->registry->validate('USD', $uppercase, 'arg'))->toBeNull(); + expect($this->registry->validate('hello', $uppercase, 'arg'))->toBeInstanceOf(ErrorMessage::class); + + $nonEmptyUppercase = parseType('non-empty-uppercase-string', $this->lexer, $this->typeParser); + expect($this->registry->validate('EUR', $nonEmptyUppercase, 'arg'))->toBeNull(); + expect($this->registry->validate('', $nonEmptyUppercase, 'arg'))->toBeInstanceOf(ErrorMessage::class); + expect($this->registry->validate('eur', $nonEmptyUppercase, 'arg'))->toBeInstanceOf(ErrorMessage::class); + }); }); describe('ConstValidator', function () { @@ -281,7 +301,7 @@ function parseType(string $typeString, Lexer $lexer, TypeParser $typeParser): Ty test('edge case: object failing one interface in intersection', function () { $intersection = parseType('Countable&ArrayAccess', $this->lexer, $this->typeParser); - $countableOnly = new class () implements Countable { + $countableOnly = new class() implements Countable { public function count(): int { return 0; From 488b07f2f62b98ed328a86b7af64eb73012fe592 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 10 Aug 2026 22:54:40 +0800 Subject: [PATCH 4/7] Add int-mask type checking support --- src/Internal/Checker/InlineChecker.php | 16 +- src/Validator/GenericValidator.php | 175 +++++++++++++----- src/Validator/IdentifierValidator.php | 2 +- tests/Fixtures/Types/BitmaskFlags.php | 28 +++ tests/Fixtures/Types/CurrencyFormatter.php | 2 +- .../IgnoreUnrecognizeDoctypeTest.php | 8 +- .../InlineVariableValidationTest.php | 47 +++++ tests/TypeChecking/IntMaskTest.php | 78 ++++++++ tests/TypeChecking/KeyOfValueOfTest.php | 3 +- .../TypeChecking/UppercaseAndArrayKeyTest.php | 25 ++- tests/Unit/ValidatorsTest.php | 2 +- 11 files changed, 321 insertions(+), 65 deletions(-) create mode 100644 tests/Fixtures/Types/BitmaskFlags.php create mode 100644 tests/TypeChecking/IntMaskTest.php diff --git a/src/Internal/Checker/InlineChecker.php b/src/Internal/Checker/InlineChecker.php index a148f42..a98636e 100644 --- a/src/Internal/Checker/InlineChecker.php +++ b/src/Internal/Checker/InlineChecker.php @@ -80,9 +80,17 @@ public static function checkVariable(mixed $value, string $typeString, string $v } if ($className !== null) { - $classAliases = ContractParser::parseClassAliases($className); - if (\count($classAliases) > 0) { - $typeNode = TemplateSubstitutor::substitute($typeNode, $classAliases); + if (class_exists($className) || interface_exists($className) || trait_exists($className)) { + try { + $refClass = new \ReflectionClass($className); + $typeNode = SpecialTypeResolver::resolve($typeNode, $refClass); + + $classAliases = ContractParser::parseClassAliases($className); + if (\count($classAliases) > 0) { + $typeNode = TemplateSubstitutor::substitute($typeNode, $classAliases); + } + } catch (\ReflectionException $e) { + } } } @@ -260,7 +268,7 @@ private static function shouldValidateType(TypeNode $node, array $config): bool 'falsy', 'uppercase-string', 'non-empty-uppercase-string', - 'array-key' + 'array-key', ], true)) { return (bool) ($config['scalars'] ?? false); } diff --git a/src/Validator/GenericValidator.php b/src/Validator/GenericValidator.php index 23905c1..d822ff5 100644 --- a/src/Validator/GenericValidator.php +++ b/src/Validator/GenericValidator.php @@ -19,7 +19,7 @@ use TypePHP\Internal\TypeFormatter; /** - * @internal values against generic AST structures (int ranges, class-string, list, array, object generics). + * @internal Validates values against generic AST structures (int ranges, class-string, list, array, object generics, key-of, value-of, int-mask, int-mask-of). */ final class GenericValidator implements TypeValidatorInterface { @@ -38,6 +38,9 @@ final class GenericValidator implements TypeValidatorInterface */ private static array $enumValueCache = []; + /** + * Validates a value against a GenericTypeNode AST. + */ public function validate(mixed $value, TypeNode $node, string $context, TypeValidatorRegistry $registry): ?ErrorMessage { /** @var GenericTypeNode $genericNode */ @@ -51,10 +54,43 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali 'array', 'non-empty-array', 'iterable', 'traversable', 'generator', 'iterator' => $this->validateArray($value, $genericNode, $context, $registry), 'key-of' => $this->validateKeyOf($value, $genericNode, $context), 'value-of' => $this->validateValueOf($value, $genericNode, $context), + 'int-mask' => $this->validateIntMask($value, $genericNode, $context), + 'int-mask-of' => $this->validateIntMaskOf($value, $genericNode, $context), default => $this->validateObjectGeneric($value, $genericNode, $context), }; } + /** + * Helper to resolve and cache class or global constant values in static memory. + */ + private function resolveConstantValue(string $fqcn, string $constName): mixed + { + $cacheKey = $fqcn !== '' ? "$fqcn::$constName" : $constName; + + if (! \array_key_exists($cacheKey, self::$constantCache)) { + $constValue = false; + if ($fqcn !== '') { + if (class_exists($fqcn) || interface_exists($fqcn)) { + try { + $refClass = new \ReflectionClass($fqcn); + if ($refClass->hasConstant($constName)) { + $constValue = $refClass->getConstant($constName); + } + } catch (\ReflectionException $e) { + // Silently ignore reflection errors + } + } + } else { + if (\defined($constName)) { + $constValue = \constant($constName); + } + } + self::$constantCache[$cacheKey] = $constValue; + } + + return self::$constantCache[$cacheKey]; + } + /** * Validates key-of generic structures with O(1) in-memory caching. * @@ -78,27 +114,7 @@ private function validateKeyOf(mixed $value, GenericTypeNode $node, string $cont $constName = $constExpr->name; $cacheKey = $fqcn !== '' ? "$fqcn::$constName" : $constName; - if (! \array_key_exists($cacheKey, self::$constantCache)) { - $constValue = false; - if ($fqcn !== '') { - if (class_exists($fqcn) || interface_exists($fqcn)) { - try { - $refClass = new \ReflectionClass($fqcn); - if ($refClass->hasConstant($constName)) { - $constValue = $refClass->getConstant($constName); - } - } catch (\ReflectionException $e) { - } - } - } else { - if (\defined($constName)) { - $constValue = \constant($constName); - } - } - self::$constantCache[$cacheKey] = $constValue; - } - - $constValue = self::$constantCache[$cacheKey]; + $constValue = $this->resolveConstantValue($fqcn, $constName); if (\is_array($constValue)) { if ((! \is_int($value) && ! \is_string($value)) || ! \array_key_exists($value, $constValue)) { @@ -111,7 +127,7 @@ private function validateKeyOf(mixed $value, GenericTypeNode $node, string $cont $enumClass = $targetType->name; if (ClassNameValidator::isValid($enumClass) && enum_exists($enumClass)) { if (! isset(self::$enumKeyCache[$enumClass])) { - self::$enumKeyCache[$enumClass] = array_map(fn($case) => $case->name, $enumClass::cases()); + self::$enumKeyCache[$enumClass] = array_map(fn ($case) => $case->name, $enumClass::cases()); } if (! \in_array($value, self::$enumKeyCache[$enumClass], true)) { @@ -163,27 +179,7 @@ private function validateValueOf(mixed $value, GenericTypeNode $node, string $co $constName = $constExpr->name; $cacheKey = $fqcn !== '' ? "$fqcn::$constName" : $constName; - if (! \array_key_exists($cacheKey, self::$constantCache)) { - $constValue = false; - if ($fqcn !== '') { - if (class_exists($fqcn) || interface_exists($fqcn)) { - try { - $refClass = new \ReflectionClass($fqcn); - if ($refClass->hasConstant($constName)) { - $constValue = $refClass->getConstant($constName); - } - } catch (\ReflectionException $e) { - } - } - } else { - if (\defined($constName)) { - $constValue = \constant($constName); - } - } - self::$constantCache[$cacheKey] = $constValue; - } - - $constValue = self::$constantCache[$cacheKey]; + $constValue = $this->resolveConstantValue($fqcn, $constName); if (\is_array($constValue)) { if (! \in_array($value, $constValue, true)) { @@ -196,7 +192,7 @@ private function validateValueOf(mixed $value, GenericTypeNode $node, string $co $enumClass = $targetType->name; if (ClassNameValidator::isValid($enumClass) && enum_exists($enumClass) && is_subclass_of($enumClass, \BackedEnum::class)) { if (! isset(self::$enumValueCache[$enumClass])) { - self::$enumValueCache[$enumClass] = array_map(fn($case) => $case->value, $enumClass::cases()); + self::$enumValueCache[$enumClass] = array_map(fn ($case) => $case->value, $enumClass::cases()); } if (! \in_array($value, self::$enumValueCache[$enumClass], true)) { @@ -210,6 +206,95 @@ private function validateValueOf(mixed $value, GenericTypeNode $node, string $co return null; } + /** + * Validates int-mask<1, 2, 4> bitmask flags combinations. + */ + private function validateIntMask(mixed $value, GenericTypeNode $node, string $context): ?ErrorMessage + { + if (! \is_int($value)) { + return ErrorFactory::createError($context . ' must be of type int (bitmask), ' . TypeFormatter::formatGivenValue($value) . ' given'); + } + + $allowedMask = 0; + + foreach ($node->genericTypes as $typeNode) { + if ($typeNode instanceof ConstTypeNode) { + $expr = $typeNode->constExpr; + if ($expr instanceof ConstExprIntegerNode) { + $allowedMask |= (int) $expr->value; + } elseif ($expr instanceof ConstFetchNode) { + $constVal = $this->resolveConstantValue($expr->className, $expr->name); + if (\is_int($constVal)) { + $allowedMask |= $constVal; + } + } + } + } + + if (($value & ~$allowedMask) !== 0) { + return ErrorFactory::createError($context . ' must be a valid bitmask combination of the allowed flags, ' . TypeFormatter::formatGivenValue($value) . ' given'); + } + + return null; + } + + /** + * Validates int-mask-of bitmask flags combinations from constant patterns. + */ + private function validateIntMaskOf(mixed $value, GenericTypeNode $node, string $context): ?ErrorMessage + { + if (! \is_int($value)) { + return ErrorFactory::createError($context . ' must be of type int (bitmask), ' . TypeFormatter::formatGivenValue($value) . ' given'); + } + + $targetType = $node->genericTypes[0] ?? null; + $allowedMask = 0; + $foundFlags = false; + + if ($targetType instanceof ConstTypeNode && $targetType->constExpr instanceof ConstFetchNode) { + $constExpr = $targetType->constExpr; + $fqcn = $constExpr->className; + $pattern = $constExpr->name; + + if ($fqcn !== '' && (class_exists($fqcn) || interface_exists($fqcn))) { + try { + $refClass = new \ReflectionClass($fqcn); + + if (str_contains($pattern, '*')) { + $regex = '/^' . str_replace('\*', '.*', preg_quote($pattern, '/')) . '$/i'; + foreach ($refClass->getConstants() as $cName => $cValue) { + if (\is_int($cValue) && preg_match($regex, $cName) === 1) { + $allowedMask |= $cValue; + $foundFlags = true; + } + } + } else { + $cValue = $this->resolveConstantValue($fqcn, $pattern); + if (\is_int($cValue)) { + $allowedMask |= $cValue; + $foundFlags = true; + } elseif (\is_array($cValue)) { + foreach ($cValue as $item) { + if (\is_int($item)) { + $allowedMask |= $item; + $foundFlags = true; + } + } + } + } + } catch (\ReflectionException $e) { + // Silently ignore reflection errors + } + } + } + + if ($foundFlags && ($value & ~$allowedMask) !== 0) { + return ErrorFactory::createError($context . ' must be a valid bitmask combination of the allowed flags, ' . TypeFormatter::formatGivenValue($value) . ' given'); + } + + return null; + } + /** * Validates integer ranges (e.g. int<1, 100> or int). */ diff --git a/src/Validator/IdentifierValidator.php b/src/Validator/IdentifierValidator.php index 0b8a6c2..4b47815 100644 --- a/src/Validator/IdentifierValidator.php +++ b/src/Validator/IdentifierValidator.php @@ -97,4 +97,4 @@ private function validateClassOrIgnore(mixed $value, string $name): bool return \is_object($value) && is_a($value, $name); } -} \ No newline at end of file +} diff --git a/tests/Fixtures/Types/BitmaskFlags.php b/tests/Fixtures/Types/BitmaskFlags.php new file mode 100644 index 0000000..a89b0d7 --- /dev/null +++ b/tests/Fixtures/Types/BitmaskFlags.php @@ -0,0 +1,28 @@ + $mask + */ + public static function checkLiteralMask(int $mask): int + { + return $mask; + } + + /** + * @param int-mask-of $mask + */ + public static function checkWildcardMask(int $mask): int + { + return $mask; + } +} diff --git a/tests/Fixtures/Types/CurrencyFormatter.php b/tests/Fixtures/Types/CurrencyFormatter.php index 5b02703..30833b7 100644 --- a/tests/Fixtures/Types/CurrencyFormatter.php +++ b/tests/Fixtures/Types/CurrencyFormatter.php @@ -24,4 +24,4 @@ public static function sanitizeCode(string $code): string { return $code; } -} \ No newline at end of file +} diff --git a/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php b/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php index 622b64a..b45e245 100644 --- a/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php +++ b/tests/TypeChecking/IgnoreUnrecognizeDoctypeTest.php @@ -39,8 +39,10 @@ function testNonExistentClassType(mixed $param): mixed test('strictly validates valid class syntax even if class does not exist at runtime', function () { expect(fn () => testNonExistentClassType('hello')) - ->toThrow(TypeError::class, 'must be of type NonExistentClass, string \'hello\' given'); + ->toThrow(TypeError::class, 'must be of type NonExistentClass, string \'hello\' given') + ; expect(fn () => testNonExistentClassType(new stdClass())) - ->toThrow(TypeError::class, 'must be of type NonExistentClass, stdClass given'); -}); \ No newline at end of file + ->toThrow(TypeError::class, 'must be of type NonExistentClass, stdClass given') + ; +}); diff --git a/tests/TypeChecking/InlineVariableValidationTest.php b/tests/TypeChecking/InlineVariableValidationTest.php index b85041c..dba6320 100644 --- a/tests/TypeChecking/InlineVariableValidationTest.php +++ b/tests/TypeChecking/InlineVariableValidationTest.php @@ -233,3 +233,50 @@ function fetchBroadTuple(int $id, string $name): array expect($token)->toBe('valid_token'); }); }); + +describe('Inline @var Validation for New Features', function () { + + test('enforces array-key on inline local variables', function () { + /** @var array-key $key */ + $key = 'user_123'; + expect($key)->toBe('user_123'); + + $key = 456; + expect($key)->toBe(456); + + expect(fn () => $key = false) + ->toThrow(TypeError::class, 'Variable $key must be of type array-key') + ; + }); + + test('enforces uppercase-string on inline local variables', function () { + /** @var non-empty-uppercase-string $code */ + $code = 'USD'; + expect($code)->toBe('USD'); + + expect(fn () => $code = 'usd') + ->toThrow(TypeError::class, 'Variable $code must be of type non-empty-uppercase-string') + ; + }); + + test('enforces key-of on inline local variables', function () { + /** @var key-of $driver */ + $driver = 'pdo_mysql'; + expect($driver)->toBe('pdo_mysql'); + + expect(fn () => $driver = 'pdo_invalid') + ->toThrow(TypeError::class, 'Variable $driver') + ; + }); + + test('enforces int-mask on inline local variables', function () { + /** @var int-mask<1, 2, 4> $mask */ + $mask = 3; // 1 | 2 + expect($mask)->toBe(3); + + expect(fn () => $mask = 8) + ->toThrow(TypeError::class, 'Variable $mask') + ; + }); + +}); diff --git a/tests/TypeChecking/IntMaskTest.php b/tests/TypeChecking/IntMaskTest.php new file mode 100644 index 0000000..4d12888 --- /dev/null +++ b/tests/TypeChecking/IntMaskTest.php @@ -0,0 +1,78 @@ + $mask + */ +function testLiteralIntMask(int $mask): int +{ + return $mask; +} + +describe('int-mask and int-mask-of Annotations', function () { + + describe('int-mask<1, 2, 4>', function () { + + test('accepts valid bitmask flag combinations and zero', function () { + expect(testLiteralIntMask(0))->toBe(0); // No flags + expect(testLiteralIntMask(1))->toBe(1); // READ + expect(testLiteralIntMask(3))->toBe(3); // READ | WRITE + expect(testLiteralIntMask(7))->toBe(7); // READ | WRITE | EXECUTE + expect(BitmaskFlags::checkLiteralMask(5))->toBe(5); // READ | EXECUTE + }); + + test('throws TypeError when bitmask contains illegal bits', function () { + expect(fn () => testLiteralIntMask(8)) // 8 (1000) is outside allowed mask 7 (0111) + ->toThrow(TypeError::class, 'must be a valid bitmask combination') + ; + + expect(fn () => BitmaskFlags::checkLiteralMask(10)) + ->toThrow(TypeError::class, 'must be a valid bitmask combination') + ; + }); + }); + + describe('int-mask-of', function () { + + test('accepts valid bitmask combinations matching wildcard constants', function () { + expect(BitmaskFlags::checkWildcardMask(1))->toBe(1); + expect(BitmaskFlags::checkWildcardMask(3))->toBe(3); + expect(BitmaskFlags::checkWildcardMask(7))->toBe(7); + }); + + test('throws TypeError on invalid bitmask for wildcard constants', function () { + expect(fn () => BitmaskFlags::checkWildcardMask(16)) + ->toThrow(TypeError::class, 'must be a valid bitmask combination') + ; + }); + }); + + describe('Inline @var Constant Bitmasks', function () { + + test('enforces int-mask with specific class constants on inline variables', function () { + /** @var int-mask $mask */ + $mask = 1; + expect($mask)->toBe(1); + + $mask = 3; + expect($mask)->toBe(3); + + expect(fn () => $mask = 4) + ->toThrow(TypeError::class, 'Variable $mask must be a valid bitmask combination') + ; + }); + + test('enforces int-mask-of with wildcard constant patterns on inline variables', function () { + /** @var int-mask-of $wildcardMask */ + $wildcardMask = 7; + expect($wildcardMask)->toBe(7); + + expect(fn () => $wildcardMask = 16) + ->toThrow(TypeError::class, 'Variable $wildcardMask must be a valid bitmask combination') + ; + }); + }); +}); diff --git a/tests/TypeChecking/KeyOfValueOfTest.php b/tests/TypeChecking/KeyOfValueOfTest.php index 18a0502..42eb786 100644 --- a/tests/TypeChecking/KeyOfValueOfTest.php +++ b/tests/TypeChecking/KeyOfValueOfTest.php @@ -153,7 +153,8 @@ function testEnumValueOf(string $statusValue): string expect($conn->localAction(['action' => 'start']))->toBeTrue(); expect(fn () => $conn->localAction(['action' => 'pause'])) - ->toThrow(TypeError::class, "['action'] must be a key of TypePHP\Tests\Fixtures\Types\DoctrineLikeConnection::LOCAL_ACTIONS"); + ->toThrow(TypeError::class, "['action'] must be a key of TypePHP\Tests\Fixtures\Types\DoctrineLikeConnection::LOCAL_ACTIONS") + ; }); }); diff --git a/tests/TypeChecking/UppercaseAndArrayKeyTest.php b/tests/TypeChecking/UppercaseAndArrayKeyTest.php index 0629a1b..e585925 100644 --- a/tests/TypeChecking/UppercaseAndArrayKeyTest.php +++ b/tests/TypeChecking/UppercaseAndArrayKeyTest.php @@ -39,10 +39,12 @@ function testNonEmptyUppercaseStringParam(string $str): string test('throws TypeError when array-key is a boolean or array', function () { expect(fn () => testArrayKeyParam(true)) - ->toThrow(TypeError::class, 'must be of type array-key'); + ->toThrow(TypeError::class, 'must be of type array-key') + ; expect(fn () => testArrayKeyParam([])) - ->toThrow(TypeError::class, 'must be of type array-key'); + ->toThrow(TypeError::class, 'must be of type array-key') + ; }); }); @@ -51,21 +53,24 @@ function testNonEmptyUppercaseStringParam(string $str): string test('accepts valid uppercase strings', function () { expect(testUppercaseStringParam('USD'))->toBe('USD'); - expect(testUppercaseStringParam(''))->toBe(''); + expect(testUppercaseStringParam(''))->toBe(''); expect(testNonEmptyUppercaseStringParam('EUR'))->toBe('EUR'); }); test('throws TypeError on lowercase or mixed-case string for uppercase-string', function () { expect(fn () => testUppercaseStringParam('Usd')) - ->toThrow(TypeError::class, 'must be of type uppercase-string'); + ->toThrow(TypeError::class, 'must be of type uppercase-string') + ; expect(fn () => testUppercaseStringParam('usd')) - ->toThrow(TypeError::class, 'must be of type uppercase-string'); + ->toThrow(TypeError::class, 'must be of type uppercase-string') + ; }); test('throws TypeError on empty string for non-empty-uppercase-string', function () { expect(fn () => testNonEmptyUppercaseStringParam('')) - ->toThrow(TypeError::class, 'must be of type non-empty-uppercase-string'); + ->toThrow(TypeError::class, 'must be of type non-empty-uppercase-string') + ; }); }); @@ -79,10 +84,12 @@ function testNonEmptyUppercaseStringParam(string $str): string expect($formatter->formatAccount('GBP', 'acc_202'))->toBe('GBP_acc_202'); expect(fn () => $formatter->formatAccount('usd', 101)) - ->toThrow(TypeError::class, 'must be of type non-empty-uppercase-string'); + ->toThrow(TypeError::class, 'must be of type non-empty-uppercase-string') + ; expect(fn () => $formatter->formatAccount('USD', true)) - ->toThrow(TypeError::class, 'must be of type array-key'); + ->toThrow(TypeError::class, 'must be of type array-key') + ; }); test('validates static method returns on CurrencyFormatter', function () { @@ -94,4 +101,4 @@ function testNonEmptyUppercaseStringParam(string $str): string }); -}); \ No newline at end of file +}); diff --git a/tests/Unit/ValidatorsTest.php b/tests/Unit/ValidatorsTest.php index 6c06924..5062bf5 100644 --- a/tests/Unit/ValidatorsTest.php +++ b/tests/Unit/ValidatorsTest.php @@ -301,7 +301,7 @@ function parseType(string $typeString, Lexer $lexer, TypeParser $typeParser): Ty test('edge case: object failing one interface in intersection', function () { $intersection = parseType('Countable&ArrayAccess', $this->lexer, $this->typeParser); - $countableOnly = new class() implements Countable { + $countableOnly = new class () implements Countable { public function count(): int { return 0; From 43cf120ad9f3e34f2280e8f9eca32235db98ec07 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 10 Aug 2026 23:42:38 +0800 Subject: [PATCH 5/7] Added runtime checking for offset array access --- src/Contract/ContractParser.php | 24 ++- src/Resolver/SpecialTypeResolver.php | 201 +++++++++++++++++- .../Fixtures/Types/OffsetAccessContainer.php | 31 +++ tests/TypeChecking/OffsetAccessTest.php | 44 ++++ 4 files changed, 282 insertions(+), 18 deletions(-) create mode 100644 tests/Fixtures/Types/OffsetAccessContainer.php create mode 100644 tests/TypeChecking/OffsetAccessTest.php diff --git a/src/Contract/ContractParser.php b/src/Contract/ContractParser.php index 2ef8142..1f289d1 100644 --- a/src/Contract/ContractParser.php +++ b/src/Contract/ContractParser.php @@ -12,6 +12,7 @@ use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; +use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use TypePHP\Internal\Config; @@ -263,14 +264,14 @@ private static function parseFunction(\ReflectionFunction $ref): array if ($isVariadic) { $type = new ArrayTypeNode($type); } - $resolvedType = SpecialTypeResolver::resolve($type, $ref); - $types[$paramName] = self::substituteAliases($resolvedType, $aliases); + $substitutedType = self::substituteAliases($type, $aliases); + $types[$paramName] = SpecialTypeResolver::resolve($substitutedType, $ref); } $returnTags = $phpDocNode->getReturnTagValues(); if (\count($returnTags) > 0) { - $resolvedReturn = SpecialTypeResolver::resolve($returnTags[0]->type, $ref); - $returnType = self::substituteAliases($resolvedReturn, $aliases); + $substitutedReturn = self::substituteAliases($returnTags[0]->type, $aliases); + $returnType = SpecialTypeResolver::resolve($substitutedReturn, $ref); } return [ @@ -379,8 +380,8 @@ private static function parseMethodHierarchyDocs( if ($isVariadic) { $type = new ArrayTypeNode($type); } - $resolvedType = SpecialTypeResolver::resolve($type, $hierRef); - $types[$baseParamName] = self::substituteAliases($resolvedType, $aliases); + $substitutedType = self::substituteAliases($type, $aliases); + $types[$baseParamName] = SpecialTypeResolver::resolve($substitutedType, $hierRef); } } } @@ -388,8 +389,8 @@ private static function parseMethodHierarchyDocs( if ($returnType === null) { $returnTags = $phpDocNode->getReturnTagValues(); if (\count($returnTags) > 0) { - $resolvedReturn = SpecialTypeResolver::resolve($returnTags[0]->type, $hierRef); - $returnType = self::substituteAliases($resolvedReturn, $aliases); + $substitutedReturn = self::substituteAliases($returnTags[0]->type, $aliases); + $returnType = SpecialTypeResolver::resolve($substitutedReturn, $hierRef); } } } @@ -441,6 +442,13 @@ private static function substituteAliases(TypeNode $node, array $aliases): TypeN return $node; } + if ($node instanceof OffsetAccessTypeNode) { + return new OffsetAccessTypeNode( + self::substituteAliases($node->type, $aliases), + self::substituteAliases($node->offset, $aliases) + ); + } + if ($node instanceof ArrayTypeNode) { return new ArrayTypeNode(self::substituteAliases($node->type, $aliases)); } diff --git a/src/Resolver/SpecialTypeResolver.php b/src/Resolver/SpecialTypeResolver.php index f430e6a..77deb81 100644 --- a/src/Resolver/SpecialTypeResolver.php +++ b/src/Resolver/SpecialTypeResolver.php @@ -6,6 +6,8 @@ use PhpParser\Node\Stmt; use PhpParser\ParserFactory; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; @@ -20,6 +22,7 @@ use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; +use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; @@ -144,6 +147,68 @@ public static function resolve(TypeNode $node, \ReflectionClass|\ReflectionFunct ); } + if ($node instanceof OffsetAccessTypeNode) { + $baseType = self::resolve($node->type, $context, $thisObj); + $offsetType = self::resolve($node->offset, $context, $thisObj); + + $offsetKey = null; + if ($offsetType instanceof ConstTypeNode) { + $expr = $offsetType->constExpr; + if ($expr instanceof ConstExprStringNode) { + $offsetKey = $expr->value; + } elseif ($expr instanceof ConstExprIntegerNode) { + $offsetKey = (int) $expr->value; + } + } elseif ($offsetType instanceof IdentifierTypeNode) { + $offsetKey = $offsetType->name; + } + + if ($offsetKey !== null) { + if ($baseType instanceof ArrayShapeNode) { + foreach ($baseType->items as $item) { + $itemKey = null; + if ($item->keyName instanceof ConstExprStringNode) { + $itemKey = $item->keyName->value; + } elseif ($item->keyName instanceof IdentifierTypeNode) { + $itemKey = $item->keyName->name; + } elseif ($item->keyName instanceof ConstExprIntegerNode) { + $itemKey = (int) $item->keyName->value; + } + + if ((string) $itemKey === (string) $offsetKey) { + return $item->valueType; + } + } + } + + if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { + $constExpr = $baseType->constExpr; + $fqcn = $constExpr->className; + $constName = $constExpr->name; + + if ($fqcn !== '' && (class_exists($fqcn) || interface_exists($fqcn))) { + try { + $refClass = new \ReflectionClass($fqcn); + if ($refClass->hasConstant($constName)) { + $constValue = $refClass->getConstant($constName); + if (\is_array($constValue) && \array_key_exists($offsetKey, $constValue)) { + $val = $constValue[$offsetKey]; + if (\is_string($val)) { + return new ConstTypeNode(new ConstExprStringNode($val, ConstExprStringNode::SINGLE_QUOTED)); + } elseif (\is_int($val)) { + return new ConstTypeNode(new ConstExprIntegerNode((string) $val)); + } + } + } + } catch (\ReflectionException $e) { + } + } + } + } + + return new OffsetAccessTypeNode($baseType, $offsetType); + } + if ($node instanceof ArrayShapeNode) { $items = array_map(function ($item) use ($context, $thisObj) { return new ArrayShapeItemNode( @@ -277,6 +342,68 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode ); } + if ($node instanceof OffsetAccessTypeNode) { + $baseType = self::resolveForFile($node->type, $file); + $offsetType = self::resolveForFile($node->offset, $file); + + $offsetKey = null; + if ($offsetType instanceof ConstTypeNode) { + $expr = $offsetType->constExpr; + if ($expr instanceof ConstExprStringNode) { + $offsetKey = $expr->value; + } elseif ($expr instanceof ConstExprIntegerNode) { + $offsetKey = (int) $expr->value; + } + } elseif ($offsetType instanceof IdentifierTypeNode) { + $offsetKey = $offsetType->name; + } + + if ($offsetKey !== null) { + if ($baseType instanceof ArrayShapeNode) { + foreach ($baseType->items as $item) { + $itemKey = null; + if ($item->keyName instanceof ConstExprStringNode) { + $itemKey = $item->keyName->value; + } elseif ($item->keyName instanceof IdentifierTypeNode) { + $itemKey = $item->keyName->name; + } elseif ($item->keyName instanceof ConstExprIntegerNode) { + $itemKey = (int) $item->keyName->value; + } + + if ((string) $itemKey === (string) $offsetKey) { + return $item->valueType; + } + } + } + + if ($baseType instanceof ConstTypeNode && $baseType->constExpr instanceof ConstFetchNode) { + $constExpr = $baseType->constExpr; + $fqcn = $constExpr->className; + $constName = $constExpr->name; + + if ($fqcn !== '' && (class_exists($fqcn) || interface_exists($fqcn))) { + try { + $refClass = new \ReflectionClass($fqcn); + if ($refClass->hasConstant($constName)) { + $constValue = $refClass->getConstant($constName); + if (\is_array($constValue) && \array_key_exists($offsetKey, $constValue)) { + $val = $constValue[$offsetKey]; + if (\is_string($val)) { + return new ConstTypeNode(new ConstExprStringNode($val, ConstExprStringNode::SINGLE_QUOTED)); + } elseif (\is_int($val)) { + return new ConstTypeNode(new ConstExprIntegerNode((string) $val)); + } + } + } + } catch (\ReflectionException $e) { + } + } + } + } + + return new OffsetAccessTypeNode($baseType, $offsetType); + } + if ($node instanceof ArrayShapeNode) { $items = array_map(function ($item) use ($file) { return new ArrayShapeItemNode( @@ -523,16 +650,70 @@ public static function resolveFqcnForFile(string $name, string $file): string private static function isBuiltInTypeKeyword(string $name): bool { return \in_array(strtolower($name), [ - 'int', 'integer', 'string', 'float', 'double', 'bool', 'boolean', 'array', 'list', 'object', 'callable', - 'iterable', 'resource', 'null', 'true', 'false', 'mixed', 'scalar', 'void', 'self', 'static', 'parent', '$this', - 'positive-int', 'negative-int', 'non-positive-int', 'non-negative-int', 'non-zero-int', 'unsigned-int', - 'positive-float', 'negative-float', 'non-positive-float', 'non-negative-float', 'non-zero-float', - 'class-string', 'interface-string', 'trait-string', 'enum-string', 'callable-string', 'numeric-string', - 'non-empty-string', 'lowercase-string', 'non-empty-lowercase-string', - 'uppercase-string', 'non-empty-uppercase-string', 'array-key', - 'literal-string', 'truthy-string', - 'non-empty-array', 'non-empty-list', 'number', 'numeric', 'truthy', 'falsy', 'falsey', 'min', 'max', '*', - 'never', 'never-return', 'never-returns', 'no-return', 'open-resource', 'closed-resource', + 'int', + 'integer', + 'string', + 'float', + 'double', + 'bool', + 'boolean', + 'array', + 'list', + 'object', + 'callable', + 'iterable', + 'resource', + 'null', + 'true', + 'false', + 'mixed', + 'scalar', + 'void', + 'self', + 'static', + 'parent', + '$this', + 'positive-int', + 'negative-int', + 'non-positive-int', + 'non-negative-int', + 'non-zero-int', + 'unsigned-int', + 'positive-float', + 'negative-float', + 'non-positive-float', + 'non-negative-float', + 'non-zero-float', + 'class-string', + 'interface-string', + 'trait-string', + 'enum-string', + 'callable-string', + 'numeric-string', + 'non-empty-string', + 'lowercase-string', + 'non-empty-lowercase-string', + 'uppercase-string', + 'non-empty-uppercase-string', + 'array-key', + 'literal-string', + 'truthy-string', + 'non-empty-array', + 'non-empty-list', + 'number', + 'numeric', + 'truthy', + 'falsy', + 'falsey', + 'min', + 'max', + '*', + 'never', + 'never-return', + 'never-returns', + 'no-return', + 'open-resource', + 'closed-resource', ], true); } diff --git a/tests/Fixtures/Types/OffsetAccessContainer.php b/tests/Fixtures/Types/OffsetAccessContainer.php new file mode 100644 index 0000000..6dd6a04 --- /dev/null +++ b/tests/Fixtures/Types/OffsetAccessContainer.php @@ -0,0 +1,31 @@ + 'PDO\MySQL\Driver', + ]; + + /** + * @param UserShape['id'] $id + */ + public function setUserId(int $id): int + { + return $id; + } + + /** + * @param self::CONFIG_MAP['mysql'] $driverClass + */ + public static function setDriver(string $driverClass): string + { + return $driverClass; + } +} diff --git a/tests/TypeChecking/OffsetAccessTest.php b/tests/TypeChecking/OffsetAccessTest.php new file mode 100644 index 0000000..75dab54 --- /dev/null +++ b/tests/TypeChecking/OffsetAccessTest.php @@ -0,0 +1,44 @@ +setUserId(42))->toBe(42); + + expect(fn () => $container->setUserId(-5)) + ->toThrow(TypeError::class, 'must be of type positive-int') + ; + }); + + test('resolves constant offset from self::CONFIG_MAP[\'mysql\'] to literal string', function () { + expect(OffsetAccessContainer::setDriver('PDO\MySQL\Driver'))->toBe('PDO\MySQL\Driver'); + + expect(fn () => OffsetAccessContainer::setDriver('PDO\PgSQL\Driver')) + ->toThrow(TypeError::class, 'must be literal') + ; + }); + + test('resolves direct inline shape offset array{...}[\'status\'] to union status', function () { + expect(testDirectShapeOffset('active'))->toBe('active'); + expect(testDirectShapeOffset('pending'))->toBe('pending'); + + expect(fn () => testDirectShapeOffset('archived')) + ->toThrow(TypeError::class, "('active' | 'pending')") + ; + }); + +}); From b734de35e77af7594ac6e4204ae59b90c9244fb3 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Mon, 10 Aug 2026 23:58:45 +0800 Subject: [PATCH 6/7] Add support for offset access types and integer bitmasks in documentation --- docs/supported-types/arrays-and-shapes.md | 43 +++++++++++++++++++ .../supported-types/primitives-and-scalars.md | 36 ++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/docs/supported-types/arrays-and-shapes.md b/docs/supported-types/arrays-and-shapes.md index 34d9a61..40d74ef 100644 --- a/docs/supported-types/arrays-and-shapes.md +++ b/docs/supported-types/arrays-and-shapes.md @@ -378,7 +378,50 @@ $service->configure(['driver' => 'pdo_pgsql'], 'id'); $service->configure(['driver' => 'pdo_mysql'], 'invalid'); // Throws: TypeError: Argument $shapeKey must be a key of the specified array shape ``` +--- +## Offset Access Types (`T[K]`) + +TypePHP supports evaluating offset access lookups on array shapes, constant arrays, and `@phpstan-type` aliases at runtime using `T[K]` syntax. + +> **AST Reduction:** TypePHP evaluates and reduces offset access lookups (e.g. `UserShape['id']` $\rightarrow$ `positive-int`) at the AST level before validation runs, executing type checks at **$O(1)$ constant speed**. + +```php +namespace App\Services; + +/** + * @phpstan-type UserShape array{id: positive-int, username: non-empty-string} + */ +class UserService +{ + public const CONFIG_MAP = [ + 'mysql' => 'PDO\MySQL\Driver', + ]; + /** + * Resolves UserShape['id'] directly to positive-int + * + * @param UserShape['id'] $userId + * @param self::CONFIG_MAP['mysql'] $driverClass + */ + public function findUser(int $userId, string $driverClass): void + { + // ... + } +} + +$service = new UserService(); + +// Valid +$service->findUser(42, 'PDO\MySQL\Driver'); + +// Invalid $userId (-5 violates positive-int extracted from UserShape['id']) +$service->findUser(-5, 'PDO\MySQL\Driver'); +// Throws: TypeError: Argument $userId must be of type positive-int + +// Invalid $driverClass ('PDO\PgSQL\Driver' violates literal 'PDO\MySQL\Driver') +$service->findUser(42, 'PDO\PgSQL\Driver'); +// Throws: TypeError: Argument $driverClass must be literal 'PDO\MySQL\Driver' +``` --- ## Object Shapes (`object{prop: type}` & `stdClass{prop: type}`) diff --git a/docs/supported-types/primitives-and-scalars.md b/docs/supported-types/primitives-and-scalars.md index d60812d..81b66e8 100644 --- a/docs/supported-types/primitives-and-scalars.md +++ b/docs/supported-types/primitives-and-scalars.md @@ -112,6 +112,42 @@ setRange(150, 0, 5); --- +## Integer Bitmasks (`int-mask<...>` & `int-mask-of<...>`) + +TypePHP enforces bitwise flag combinations created by bitwise `OR` (`|`) operations on integers using `int-mask` and `int-mask-of`: + +| Refinement Keyword | Constraint Rule | Valid Examples | Invalid Examples | +| :--- | :--- | :--- | :--- | +| **`int-mask<1, 2, 4>`** | Value must be a valid bitwise combination of the allowed integer flags (or `0`). | `0`, `1`, `3` (`1\|2`), `7` (`1\|2\|4`) | `8`, `10`, `-1` | +| **`int-mask-of`** | Value must be a valid bitwise combination of class constants matching the wildcard pattern. | `1`, `3`, `7` | `16` | + +```php +class BitmaskFlags +{ + public const FLAG_READ = 1; // 0001 + public const FLAG_WRITE = 2; // 0010 + public const FLAG_EXECUTE = 4; // 0100 + + /** + * @param int-mask<1, 2, 4> $mask + * @param int-mask-of $wildcardMask + */ + public static function setPermissions(int $mask, int $wildcardMask): void + { + // ... + } +} + +// Valid Call +BitmaskFlags::setPermissions(3, 7); // 3 = READ | WRITE, 7 = READ | WRITE | EXECUTE + +// Invalid (8 contains bits outside allowed mask 1|2|4 = 7) +BitmaskFlags::setPermissions(8, 7); +// Throws: TypeError: Argument $mask must be a valid bitmask combination of the allowed flags +``` + +--- + ## String Refinements Validate string lengths, formatting, character casing, and truthiness at runtime: From cfdf83a2c2ea6bac1f9674f43158b1e60cf108e7 Mon Sep 17 00:00:00 2001 From: "Reymart A. Calicdan" Date: Tue, 11 Aug 2026 00:27:52 +0800 Subject: [PATCH 7/7] fix name argument parsing mismatch cuasing false positive type-errors and document it. --- docs/advanced/liskov-and-inheritance.md | 39 ++++++----- docs/core-concepts/function-contracts.md | 27 ++++++++ src/Contract/ContractParser.php | 28 ++++---- tests/Fixtures/Attributes/BaseField.php | 22 +++++++ .../Attributes/DeepMultiLevelField.php | 22 +++++++ .../Fixtures/Attributes/GrandParentField.php | 18 ++++++ .../Attributes/MeasurementSystemEntity.php | 19 ++++++ tests/Fixtures/Attributes/OnDeleteOption.php | 11 ++++ .../Fixtures/Attributes/OneToManyRelation.php | 23 +++++++ tests/Fixtures/Attributes/ParentField.php | 21 ++++++ .../Services/BaseNamedParamService.php | 20 ++++++ .../Fixtures/Services/ShiftedParamService.php | 19 ++++++ .../AttributeConstructorInheritanceTest.php | 64 +++++++++++++++++++ tests/TypeChecking/NamedArgumentsTest.php | 63 ++++++++++++++++++ 14 files changed, 369 insertions(+), 27 deletions(-) create mode 100644 tests/Fixtures/Attributes/BaseField.php create mode 100644 tests/Fixtures/Attributes/DeepMultiLevelField.php create mode 100644 tests/Fixtures/Attributes/GrandParentField.php create mode 100644 tests/Fixtures/Attributes/MeasurementSystemEntity.php create mode 100644 tests/Fixtures/Attributes/OnDeleteOption.php create mode 100644 tests/Fixtures/Attributes/OneToManyRelation.php create mode 100644 tests/Fixtures/Attributes/ParentField.php create mode 100644 tests/Fixtures/Services/BaseNamedParamService.php create mode 100644 tests/Fixtures/Services/ShiftedParamService.php create mode 100644 tests/TypeChecking/AttributeConstructorInheritanceTest.php create mode 100644 tests/TypeChecking/NamedArgumentsTest.php diff --git a/docs/advanced/liskov-and-inheritance.md b/docs/advanced/liskov-and-inheritance.md index 3c110fc..add5fe3 100644 --- a/docs/advanced/liskov-and-inheritance.md +++ b/docs/advanced/liskov-and-inheritance.md @@ -292,35 +292,42 @@ $service->update(10, 'Charlie'); --- -## Parameter Renaming ($id $\rightarrow$ $userId$) +## Parameter Renaming ($id → $userId) & Position Shifts -PHP permits child classes to rename parameters when implementing an interface or extending a class. TypePHP maps inherited parameter contracts by **index position** (0, 1, 2...) rather than parameter name: +When a child class or attribute constructor overrides a parent method, parameter positions or parameter names may shift. TypePHP resolves parameter contract inheritance using **Name-First Resolution**: + +1. **Name Matching:** If a parameter name in the child method matches a parameter name in the parent class (e.g. `$api`), the parent's contract is inherited by that parameter regardless of its position index in the child. +2. **Position Fallback:** If a parameter is renamed in the child class (e.g., `$id` $\rightarrow$ `$userId`), TypePHP falls back to matching by position index. ```php -interface UserApiInterface +class BaseField { /** - * Interface uses parameter name $id + * Parent constructor has $api at position #1 * - * @param positive-int $id + * @param string $type + * @param bool|array{admin-api: bool} $api */ - public function find(int $id): bool; + public function __construct(string $type, bool|array $api = false) {} } -class UserApi implements UserApiInterface +class OneToManyRelation extends BaseField { - // Child renames parameter $id to $userId - public function find(int $userId): bool - { - return true; + /** + * Child inserts $entity, $ref, $onDelete BEFORE $api (position shift!) + */ + public function __construct( + string $entity, + string $ref, + OnDeleteOption $onDelete = OnDeleteOption::NO_ACTION, + bool|array $api = false + ) { + parent::__construct('one-to-many', $api); } } -$api = new UserApi(); - -// $userId = -50 is checked at index 0 against interface's @param positive-int $id! -$api->find(-50); -// Throws: TypeError: UserApi::find(): Argument $userId must be of type positive-int +// $onDelete (position #2 in child) is NOT overwritten by $api's type (position #1 in parent)! +$attr = new OneToManyRelation('unit', 'unit_id', OnDeleteOption::CASCADE, true); ``` --- diff --git a/docs/core-concepts/function-contracts.md b/docs/core-concepts/function-contracts.md index ec32f44..543da06 100644 --- a/docs/core-concepts/function-contracts.md +++ b/docs/core-concepts/function-contracts.md @@ -35,6 +35,33 @@ registerUser(-5, 'Alice', 'admin'); > **Execution Order Note:** Native PHP type hints (e.g., `int $id`, `string $username`) are evaluated by PHP's C-engine *before* function execution begins. TypePHP's extended PHPDoc contracts (e.g., `positive-int`, `non-empty-string`) execute at the very start of the function/method body. If a native type hint fails, PHP throws its native `TypeError` before TypePHP's guard rails run. +--- +## PHP 8.0+ Named Arguments + +TypePHP natively supports PHP 8.0+ Named Arguments. Because parameter contracts are mapped by parameter name rather than argument position index, you can pass named arguments in any order, and TypePHP will accurately validate each parameter: + +```php + $age + */ +function registerUser(int $id, string $username, int $age): void +{ + // ... +} + +// Valid Call: Arguments passed in completely reversed/swapped order +registerUser(age: 25, username: 'Alice', id: 42); + +// Invalid Call: $id (-5) passed as 3rd named argument +registerUser(age: 25, username: 'Alice', id: -5); +// Throws: TypeError: registerUser(): Argument $id must be of type positive-int, negative int (-5) given +``` --- ## Class Methods (Instance & Static) diff --git a/src/Contract/ContractParser.php b/src/Contract/ContractParser.php index 1f289d1..b8bde46 100644 --- a/src/Contract/ContractParser.php +++ b/src/Contract/ContractParser.php @@ -332,10 +332,12 @@ private static function parseMethodHierarchyDocs( $hierarchy = HierarchyResolver::getMethodHierarchy($ref); $baseParams = $ref->getParameters(); $baseParamNames = []; + $baseParamSet = []; $baseParamVariadic = []; foreach ($baseParams as $idx => $p) { $baseParamNames[$idx] = $p->getName(); + $baseParamSet[$p->getName()] = $idx; $baseParamVariadic[$p->getName()] = $p->isVariadic(); } @@ -369,20 +371,24 @@ private static function parseMethodHierarchyDocs( foreach ($phpDocNode->getParamTagValues() as $paramTag) { $paramName = ltrim($paramTag->parameterName, '$'); - $paramIndex = $hierNameToIndex[$paramName] ?? null; - if ($paramIndex !== null && isset($baseParamNames[$paramIndex])) { - $baseParamName = $baseParamNames[$paramIndex]; + if (isset($baseParamSet[$paramName])) { + $targetParamName = $paramName; + } else { + $paramIndex = $hierNameToIndex[$paramName] ?? null; + $targetParamName = ($paramIndex !== null && isset($baseParamNames[$paramIndex])) + ? $baseParamNames[$paramIndex] + : null; + } - if (! isset($types[$baseParamName])) { - $type = $paramTag->type; - $isVariadic = $paramTag->isVariadic || $baseParamVariadic[$baseParamName]; - if ($isVariadic) { - $type = new ArrayTypeNode($type); - } - $substitutedType = self::substituteAliases($type, $aliases); - $types[$baseParamName] = SpecialTypeResolver::resolve($substitutedType, $hierRef); + if ($targetParamName !== null && ! isset($types[$targetParamName])) { + $type = $paramTag->type; + $isVariadic = $paramTag->isVariadic || ($baseParamVariadic[$targetParamName] ?? false); + if ($isVariadic) { + $type = new ArrayTypeNode($type); } + $substitutedType = self::substituteAliases($type, $aliases); + $types[$targetParamName] = SpecialTypeResolver::resolve($substitutedType, $hierRef); } } diff --git a/tests/Fixtures/Attributes/BaseField.php b/tests/Fixtures/Attributes/BaseField.php new file mode 100644 index 0000000..d919a39 --- /dev/null +++ b/tests/Fixtures/Attributes/BaseField.php @@ -0,0 +1,22 @@ +|null + */ + #[OneToManyRelation( + entity: 'measurement_display_unit', + ref: 'measurement_system_id', + onDelete: OnDeleteOption::CASCADE, + api: true + )] + public ?array $units = null; +} diff --git a/tests/Fixtures/Attributes/OnDeleteOption.php b/tests/Fixtures/Attributes/OnDeleteOption.php new file mode 100644 index 0000000..c73a6e0 --- /dev/null +++ b/tests/Fixtures/Attributes/OnDeleteOption.php @@ -0,0 +1,11 @@ + $userId, $name -> $userName, $role -> $userRole + * and adds an optional parameter $notify + * + * @param bool $notify + */ + public function registerUser(int $userId, string $userName, string $userRole = 'user', bool $notify = false): bool + { + return parent::registerUser($userId, $userName, $userRole); + } +} diff --git a/tests/TypeChecking/AttributeConstructorInheritanceTest.php b/tests/TypeChecking/AttributeConstructorInheritanceTest.php new file mode 100644 index 0000000..ee295c1 --- /dev/null +++ b/tests/TypeChecking/AttributeConstructorInheritanceTest.php @@ -0,0 +1,64 @@ +toBeInstanceOf(OneToManyRelation::class); + }); + + test('reproduces parameter index mismatch bug when instantiated via PHP 8 ReflectionAttribute::newInstance()', function () { + $refProp = new ReflectionProperty(MeasurementSystemEntity::class, 'units'); + $refAttr = $refProp->getAttributes(OneToManyRelation::class)[0]; + + $attrInstance = $refAttr->newInstance(); + + expect($attrInstance)->toBeInstanceOf(OneToManyRelation::class); + }); + + }); + + describe('Multi-Level 3-Tier Parameter Shift Edge Cases', function () { + + test('correctly maps inherited contracts across 3 hierarchy levels with multiple position shifts', function () { + $field = new DeepMultiLevelField(10, 'entity', 'unit_name', true); + + expect($field)->toBeInstanceOf(DeepMultiLevelField::class); + }); + + test('throws TypeError when $id violates local child contract in 3-tier hierarchy', function () { + expect(fn () => new DeepMultiLevelField(-5, 'entity', 'unit_name', true)) + ->toThrow(TypeError::class, 'Argument $id must be of type positive-int') + ; + }); + + test('throws TypeError when $type violates 3rd-tier grand-parent contract', function () { + expect(fn () => new DeepMultiLevelField(10, '', 'unit_name', true)) + ->toThrow(TypeError::class, 'Argument $type must be of type non-empty-string') + ; + }); + + test('throws TypeError when $name violates 2nd-tier parent contract', function () { + expect(fn () => new DeepMultiLevelField(10, 'entity', '', true)) + ->toThrow(TypeError::class, 'Argument $name must be of type non-empty-string') + ; + }); + + }); + +}); diff --git a/tests/TypeChecking/NamedArgumentsTest.php b/tests/TypeChecking/NamedArgumentsTest.php new file mode 100644 index 0000000..a44a3d3 --- /dev/null +++ b/tests/TypeChecking/NamedArgumentsTest.php @@ -0,0 +1,63 @@ + $age + */ +function testNamedArgsFunction(int $id, string $username, int $age): bool +{ + return true; +} + +describe('PHP 8.0+ Named Arguments and Renamed Parameter Positions', function () { + + describe('Standalone Function with Swapped Named Arguments', function () { + test('accepts valid named arguments passed in completely reversed/swapped order', function () { + expect(testNamedArgsFunction(age: 25, username: 'Alice', id: 42))->toBeTrue(); + }); + + test('throws TypeError on invalid named argument regardless of passed argument position', function () { + expect(fn () => testNamedArgsFunction(age: 25, username: 'Alice', id: -5)) + ->toThrow(TypeError::class, 'Argument $id must be of type positive-int') + ; + + expect(fn () => testNamedArgsFunction(username: '', id: 42, age: 25)) + ->toThrow(TypeError::class, 'Argument $username must be of type non-empty-string') + ; + + expect(fn () => testNamedArgsFunction(id: 42, age: 150, username: 'Alice')) + ->toThrow(TypeError::class, 'Argument $age') + ; + }); + + }); + + describe('Class Method with Renamed Parameters in Method Inheritance', function () { + + test('correctly maps inherited contracts when child renames parameters and is called with named arguments in random order', function () { + $service = new ShiftedParamService(); + expect($service->registerUser(notify: true, userRole: 'admin', userId: 100, userName: 'Bob'))->toBeTrue(); + }); + + test('throws TypeError on invalid parameter when child renames parameters in method inheritance', function () { + $service = new ShiftedParamService(); + + expect(fn () => $service->registerUser(userRole: 'admin', userName: 'Bob', userId: -10)) + ->toThrow(TypeError::class, 'Argument $userId must be of type positive-int') + ; + + expect(fn () => $service->registerUser(userRole: 'superadmin', userName: 'Bob', userId: 100)) + ->toThrow(TypeError::class, 'Argument $userRole') + ; + }); + + }); + +});