From eb79ed5b13bd2cf32cbd8d1674ad08703dd34a0e Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Wed, 16 Sep 2026 13:31:36 -0400 Subject: [PATCH 1/2] Fix what the second CI round found * **Two phpstan.neon files had a duplicated key.** Merging the branch with main put a second `ignoreErrors` / `reportUnmatchedIgnoredErrors` under `parameters:` -- git merges that cleanly because neither side touched the other's lines, and NEON then refuses to load the file at all. PHPStan exits with its usage banner rather than an analysis, so the job fails without ever naming a rule. error-pages and package-scaffolder are folded back to one declaration each. * **Orphaned `@var list` docblocks.** Deleting the `$commandAliases` properties left their docblocks sitting above the next method, where PHPStan reports `PHPDoc tag @var above a method has no effect`. Five in product-demo-mode. * **Pint drift, resolved at the version CI actually installs.** CI resolves laravel/pint 1.32.1; a long-lived local vendor/ had 1.30.5, and the two disagree about `binary_operator_spaces`. Checking against the old one is the same false green as checking against the wrong config -- so this ran 1.32.1. * **console:** PHPStan proves the `property_exists($this, 'commandAliases')` guard false, because it only sees the subclasses inside src/ and none of them declare it. The property belongs to the CONSUMING command -- a trait and a using class cannot both declare it with different defaults -- so the guard is exactly right and the finding is scoped away with that reason. --- src/Commands/DoctorCommand.php | 2 +- tests/BatchDatabaseValidationTest.php | 2 +- tests/ExcludeCoercionParityTest.php | 18 ++++----- .../FluentValidatorConditionalParityTest.php | 6 +-- tests/InlineMessageParamPhase3aTest.php | 30 +++++++-------- tests/InlineMessageParamPhase3bTest.php | 38 +++++++++---------- tests/InlineMessageParamPhase3cTest.php | 36 +++++++++--------- tests/InlineMessageParamPhase3dTest.php | 20 +++++----- tests/IntegrationTest.php | 4 +- tests/PresenceConditionalParityTest.php | 28 +++++++------- tests/ValueConditionalParityTest.php | 22 +++++------ 11 files changed, 103 insertions(+), 103 deletions(-) diff --git a/src/Commands/DoctorCommand.php b/src/Commands/DoctorCommand.php index 1f2d6f4..18f1771 100644 --- a/src/Commands/DoctorCommand.php +++ b/src/Commands/DoctorCommand.php @@ -57,7 +57,7 @@ public function handle(RuleRegistrar $registrar): int : ['WARN', $list::class . ' does not flag a canonical disposable domain']; }], ['role-account list', fn (): array => ['OK', resolve(RoleAccountList::class)::class]], - ['dns resolver', fn (): array => ['OK', resolve(DnsResolver::class)::class]], + ['dns resolver', fn (): array => ['OK', resolve(DnsResolver::class)::class]], ['batch query cap', function (): array { $limit = BatchDatabaseChecker::$maxValuesPerGroup; diff --git a/tests/BatchDatabaseValidationTest.php b/tests/BatchDatabaseValidationTest.php index c947f5f..a732bd1 100644 --- a/tests/BatchDatabaseValidationTest.php +++ b/tests/BatchDatabaseValidationTest.php @@ -86,7 +86,7 @@ function setupTestDatabase(): void $validator = Validator::make( ['email' => 'unknown@example.com'], - ['email' => ['required', Rule::exists('users', 'email')]], + ['email' => ['required', Rule::exists('users', 'email')]], ['email.exists' => 'Custom exists message'], ); $validator->setPresenceVerifier($verifier); diff --git a/tests/ExcludeCoercionParityTest.php b/tests/ExcludeCoercionParityTest.php index b28c455..4095ace 100644 --- a/tests/ExcludeCoercionParityTest.php +++ b/tests/ExcludeCoercionParityTest.php @@ -34,7 +34,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.detail' => ['exclude_unless:items.*.state,null', 'required', 'string']], ['items.*.detail' => [['exclude_unless', 'items.*.state', 'null'], 'required', 'string']], - ['items' => [['state' => null]]], // state IS null → not excluded → required fires + ['items' => [['state' => null]]], // state IS null → not excluded → required fires ); }); @@ -42,7 +42,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.detail' => ['exclude_if:items.*.state,null', 'required', 'string']], ['items.*.detail' => [['exclude_if', 'items.*.state', 'null'], 'required', 'string']], - ['items' => [['state' => null]]], // state IS null → excluded → no error + ['items' => [['state' => null]]], // state IS null → excluded → no error ); }); @@ -78,7 +78,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.x' => ['exclude_unless:items.*.n,1', 'required', 'string']], ['items.*.x' => [['exclude_unless', 'items.*.n', '1'], 'required', 'string']], - ['items' => [['n' => 1]]], // int 1 ↔ '1' loose match → not excluded → required fires + ['items' => [['n' => 1]]], // int 1 ↔ '1' loose match → not excluded → required fires ); }); @@ -145,7 +145,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.name' => ['exclude_if:items.*.flag,null', 'required', 'string']], ['items.*.name' => [['exclude_if', 'items.*.flag', 'null'], 'required', 'string']], - ['items' => [['other' => 'x']]], // flag absent + ['items' => [['other' => 'x']]], // flag absent ); }); @@ -153,7 +153,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.name' => ['exclude_if:items.*.flag,null', 'required', 'string']], ['items.*.name' => [['exclude_if', 'items.*.flag', 'null'], 'required', 'string']], - ['items' => [['flag' => null]]], // flag present and null → excluded → no error + ['items' => [['flag' => null]]], // flag present and null → excluded → no error ); }); @@ -195,7 +195,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.x' => ['exclude_unless:items.*.type,a', 'exclude_if:items.*.other,z', 'required', 'string']], ['items.*.x' => [['exclude_unless', 'items.*.type', 'a'], ['exclude_if', 'items.*.other', 'z'], 'required', 'string']], - ['items' => [['type' => 'a', 'other' => 'z']]], + ['items' => [['type' => 'a', 'other' => 'z']]], ); }); @@ -203,7 +203,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.x' => ['exclude_unless:items.*.type,a', 'exclude_if:items.*.other,z', 'required', 'string']], ['items.*.x' => [['exclude_unless', 'items.*.type', 'a'], ['exclude_if', 'items.*.other', 'z'], 'required', 'string']], - ['items' => [['type' => 'a', 'other' => 'keep']]], // type=a (unless ok), other≠z → kept → required fires + ['items' => [['type' => 'a', 'other' => 'keep']]], // type=a (unless ok), other≠z → kept → required fires ); }); @@ -216,7 +216,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.x' => ['exclude_unless:items.*.type,a', 'required_if:items.*.other,y', 'string']], ['items.*.x' => [['exclude_unless', 'items.*.type', 'a'], ['required_if', 'items.*.other', 'y'], 'string']], - ['items' => [['type' => 'a', 'other' => 'y']]], // x missing + ['items' => [['type' => 'a', 'other' => 'y']]], // x missing ); }); @@ -224,7 +224,7 @@ function assertExcludeParity(array $native, array $fluent, array $data): void assertExcludeParity( ['items.*.x' => ['exclude_unless:items.*.type,a', 'required_unless:items.*.other,skip', 'string']], ['items.*.x' => [['exclude_unless', 'items.*.type', 'a'], ['required_unless', 'items.*.other', 'skip'], 'string']], - ['items' => [['type' => 'a', 'other' => 'go']]], // other≠skip → x required, missing → error + ['items' => [['type' => 'a', 'other' => 'go']]], // other≠skip → x required, missing → error ); }); diff --git a/tests/FluentValidatorConditionalParityTest.php b/tests/FluentValidatorConditionalParityTest.php index e31e614..2e1801c 100644 --- a/tests/FluentValidatorConditionalParityTest.php +++ b/tests/FluentValidatorConditionalParityTest.php @@ -132,7 +132,7 @@ function assertFvExcludeParity(array $rules, array $fluentRules, array $data): v assertFvExcludeParity( ['items.*.detail' => ['exclude_unless:items.*.state,null', 'required', 'string']], ['items.*.detail' => [['exclude_unless', 'items.*.state', 'null'], 'required', 'string']], - ['items' => [['state' => null]]], // state IS null → not excluded → required fires + ['items' => [['state' => null]]], // state IS null → not excluded → required fires ); }); @@ -154,7 +154,7 @@ function assertFvExcludeParity(array $rules, array $fluentRules, array $data): v assertFvExcludeParity( ['items.*.extra' => ['exclude_unless:items.*.type,a', 'required', 'string']], ['items.*.extra' => [['exclude_unless', 'items.*.type', 'a'], 'required', 'string']], - ['items' => ['foo' => ['type' => 'a']]], // type matches → not excluded → required fires + ['items' => ['foo' => ['type' => 'a']]], // type matches → not excluded → required fires ); }); @@ -165,7 +165,7 @@ function assertFvExcludeParity(array $rules, array $fluentRules, array $data): v assertFvExcludeParity( ['items.*.rows.*.detail' => ['exclude_unless:items.*.type,keep', 'required', 'string']], ['items.*.rows.*.detail' => [['exclude_unless', 'items.*.type', 'keep'], 'required', 'string']], - ['items' => ['foo' => ['type' => 'drop', 'rows' => [['x' => 1]]]]], + ['items' => ['foo' => ['type' => 'drop', 'rows' => [['x' => 1]]]]], ); }); diff --git a/tests/InlineMessageParamPhase3aTest.php b/tests/InlineMessageParamPhase3aTest.php index a99a489..9e1faff 100644 --- a/tests/InlineMessageParamPhase3aTest.php +++ b/tests/InlineMessageParamPhase3aTest.php @@ -17,72 +17,72 @@ dataset('phase3a_methods', [ 'required' => [ fn () => FluentRule::string()->required(message: 'x'), - fn () => FluentRule::string()->required()->message('x'), + fn () => FluentRule::string()->required()->message('x'), ['required' => 'x'], ], 'sometimes' => [ fn () => FluentRule::string()->sometimes(message: 'x'), - fn () => FluentRule::string()->sometimes()->message('x'), + fn () => FluentRule::string()->sometimes()->message('x'), ['sometimes' => 'x'], ], 'filled' => [ fn () => FluentRule::string()->filled(message: 'x'), - fn () => FluentRule::string()->filled()->message('x'), + fn () => FluentRule::string()->filled()->message('x'), ['filled' => 'x'], ], 'present' => [ fn () => FluentRule::field()->present(message: 'x'), - fn () => FluentRule::field()->present()->message('x'), + fn () => FluentRule::field()->present()->message('x'), ['present' => 'x'], ], 'prohibited' => [ fn () => FluentRule::string()->prohibited(message: 'x'), - fn () => FluentRule::string()->prohibited()->message('x'), + fn () => FluentRule::string()->prohibited()->message('x'), ['prohibited' => 'x'], ], 'missing' => [ fn () => FluentRule::string()->missing(message: 'x'), - fn () => FluentRule::string()->missing()->message('x'), + fn () => FluentRule::string()->missing()->message('x'), ['missing' => 'x'], ], 'requiredIfAccepted' => [ fn () => FluentRule::string()->requiredIfAccepted('tos', message: 'x'), - fn () => FluentRule::string()->requiredIfAccepted('tos')->message('x'), + fn () => FluentRule::string()->requiredIfAccepted('tos')->message('x'), ['required_if_accepted' => 'x'], ], 'requiredIfDeclined' => [ fn () => FluentRule::string()->requiredIfDeclined('tos', message: 'x'), - fn () => FluentRule::string()->requiredIfDeclined('tos')->message('x'), + fn () => FluentRule::string()->requiredIfDeclined('tos')->message('x'), ['required_if_declined' => 'x'], ], 'prohibitedIfAccepted' => [ fn () => FluentRule::string()->prohibitedIfAccepted('tos', message: 'x'), - fn () => FluentRule::string()->prohibitedIfAccepted('tos')->message('x'), + fn () => FluentRule::string()->prohibitedIfAccepted('tos')->message('x'), ['prohibited_if_accepted' => 'x'], ], 'prohibitedIfDeclined' => [ fn () => FluentRule::string()->prohibitedIfDeclined('tos', message: 'x'), - fn () => FluentRule::string()->prohibitedIfDeclined('tos')->message('x'), + fn () => FluentRule::string()->prohibitedIfDeclined('tos')->message('x'), ['prohibited_if_declined' => 'x'], ], 'rule-string' => [ fn () => FluentRule::string()->rule('max:100', message: 'x'), - fn () => FluentRule::string()->rule('max:100')->message('x'), + fn () => FluentRule::string()->rule('max:100')->message('x'), ['max' => 'x'], ], 'unique' => [ fn () => FluentRule::string()->unique('users', 'email', message: 'x'), - fn () => FluentRule::string()->unique('users', 'email')->message('x'), + fn () => FluentRule::string()->unique('users', 'email')->message('x'), ['unique' => 'x'], ], 'exists' => [ fn () => FluentRule::string()->exists('users', 'email', message: 'x'), - fn () => FluentRule::string()->exists('users', 'email')->message('x'), + fn () => FluentRule::string()->exists('users', 'email')->message('x'), ['exists' => 'x'], ], 'enum' => [ fn () => FluentRule::string()->enum(TestStringEnum::class, message: 'x'), - fn () => FluentRule::string()->enum(TestStringEnum::class)->message('x'), + fn () => FluentRule::string()->enum(TestStringEnum::class)->message('x'), ['enum' => 'x'], ], 'in' => [ @@ -92,7 +92,7 @@ ], 'notIn' => [ fn () => FluentRule::string()->notIn(['banned'], message: 'x'), - fn () => FluentRule::string()->notIn(['banned'])->message('x'), + fn () => FluentRule::string()->notIn(['banned'])->message('x'), ['not_in' => 'x'], ], ]); diff --git a/tests/InlineMessageParamPhase3bTest.php b/tests/InlineMessageParamPhase3bTest.php index bf2339d..17b3acd 100644 --- a/tests/InlineMessageParamPhase3bTest.php +++ b/tests/InlineMessageParamPhase3bTest.php @@ -13,67 +13,67 @@ dataset('phase3b_string', [ 'alpha' => [ fn () => FluentRule::string()->alpha(message: 'x'), - fn () => FluentRule::string()->alpha()->message('x'), + fn () => FluentRule::string()->alpha()->message('x'), ['alpha' => 'x'], ], 'ascii' => [ fn () => FluentRule::string()->ascii(message: 'x'), - fn () => FluentRule::string()->ascii()->message('x'), + fn () => FluentRule::string()->ascii()->message('x'), ['ascii' => 'x'], ], 'between' => [ fn () => FluentRule::string()->between(2, 10, message: 'x'), - fn () => FluentRule::string()->between(2, 10)->message('x'), + fn () => FluentRule::string()->between(2, 10)->message('x'), ['between' => 'x'], ], 'min' => [ fn () => FluentRule::string()->min(2, message: 'x'), - fn () => FluentRule::string()->min(2)->message('x'), + fn () => FluentRule::string()->min(2)->message('x'), ['min' => 'x'], ], 'max' => [ fn () => FluentRule::string()->max(100, message: 'x'), - fn () => FluentRule::string()->max(100)->message('x'), + fn () => FluentRule::string()->max(100)->message('x'), ['max' => 'x'], ], 'exactly' => [ fn () => FluentRule::string()->exactly(5, message: 'x'), - fn () => FluentRule::string()->exactly(5)->message('x'), + fn () => FluentRule::string()->exactly(5)->message('x'), ['size' => 'x'], ], 'url' => [ fn () => FluentRule::string()->url(message: 'x'), - fn () => FluentRule::string()->url()->message('x'), + fn () => FluentRule::string()->url()->message('x'), ['url' => 'x'], ], 'uuid' => [ fn () => FluentRule::string()->uuid(message: 'x'), - fn () => FluentRule::string()->uuid()->message('x'), + fn () => FluentRule::string()->uuid()->message('x'), ['uuid' => 'x'], ], 'regex' => [ fn () => FluentRule::string()->regex('/^[a-z]+$/', message: 'x'), - fn () => FluentRule::string()->regex('/^[a-z]+$/')->message('x'), + fn () => FluentRule::string()->regex('/^[a-z]+$/')->message('x'), ['regex' => 'x'], ], 'same' => [ fn () => FluentRule::string()->same('other', message: 'x'), - fn () => FluentRule::string()->same('other')->message('x'), + fn () => FluentRule::string()->same('other')->message('x'), ['same' => 'x'], ], 'dateFormat' => [ fn () => FluentRule::string()->dateFormat('Y-m-d', message: 'x'), - fn () => FluentRule::string()->dateFormat('Y-m-d')->message('x'), + fn () => FluentRule::string()->dateFormat('Y-m-d')->message('x'), ['date_format' => 'x'], ], 'confirmed' => [ fn () => FluentRule::string()->confirmed(message: 'x'), - fn () => FluentRule::string()->confirmed()->message('x'), + fn () => FluentRule::string()->confirmed()->message('x'), ['confirmed' => 'x'], ], 'currentPassword' => [ fn () => FluentRule::string()->currentPassword(message: 'x'), - fn () => FluentRule::string()->currentPassword()->message('x'), + fn () => FluentRule::string()->currentPassword()->message('x'), ['current_password' => 'x'], ], ]); @@ -81,27 +81,27 @@ dataset('phase3b_numeric', [ 'min' => [ fn () => FluentRule::numeric()->min(0, message: 'x'), - fn () => FluentRule::numeric()->min(0)->message('x'), + fn () => FluentRule::numeric()->min(0)->message('x'), ['min' => 'x'], ], 'max' => [ fn () => FluentRule::numeric()->max(100, message: 'x'), - fn () => FluentRule::numeric()->max(100)->message('x'), + fn () => FluentRule::numeric()->max(100)->message('x'), ['max' => 'x'], ], 'between' => [ fn () => FluentRule::numeric()->between(0, 100, message: 'x'), - fn () => FluentRule::numeric()->between(0, 100)->message('x'), + fn () => FluentRule::numeric()->between(0, 100)->message('x'), ['between' => 'x'], ], 'integer' => [ fn () => FluentRule::numeric()->integer(message: 'x'), - fn () => FluentRule::numeric()->integer()->message('x'), + fn () => FluentRule::numeric()->integer()->message('x'), ['integer' => 'x'], ], 'decimal' => [ fn () => FluentRule::numeric()->decimal(2, message: 'x'), - fn () => FluentRule::numeric()->decimal(2)->message('x'), + fn () => FluentRule::numeric()->decimal(2)->message('x'), ['decimal' => 'x'], ], 'positive' => [ @@ -116,7 +116,7 @@ ], 'multipleOf' => [ fn () => FluentRule::numeric()->multipleOf(5, message: 'x'), - fn () => FluentRule::numeric()->multipleOf(5)->message('x'), + fn () => FluentRule::numeric()->multipleOf(5)->message('x'), ['multiple_of' => 'x'], ], ]); diff --git a/tests/InlineMessageParamPhase3cTest.php b/tests/InlineMessageParamPhase3cTest.php index 33d231f..fb45130 100644 --- a/tests/InlineMessageParamPhase3cTest.php +++ b/tests/InlineMessageParamPhase3cTest.php @@ -12,32 +12,32 @@ dataset('phase3c_array', [ 'min' => [ fn () => FluentRule::array()->min(1, message: 'x'), - fn () => FluentRule::array()->min(1)->message('x'), + fn () => FluentRule::array()->min(1)->message('x'), ['min' => 'x'], ], 'max' => [ fn () => FluentRule::array()->max(10, message: 'x'), - fn () => FluentRule::array()->max(10)->message('x'), + fn () => FluentRule::array()->max(10)->message('x'), ['max' => 'x'], ], 'between' => [ fn () => FluentRule::array()->between(1, 5, message: 'x'), - fn () => FluentRule::array()->between(1, 5)->message('x'), + fn () => FluentRule::array()->between(1, 5)->message('x'), ['between' => 'x'], ], 'exactly' => [ fn () => FluentRule::array()->exactly(3, message: 'x'), - fn () => FluentRule::array()->exactly(3)->message('x'), + fn () => FluentRule::array()->exactly(3)->message('x'), ['size' => 'x'], ], 'list' => [ fn () => FluentRule::array()->list(message: 'x'), - fn () => FluentRule::array()->list()->message('x'), + fn () => FluentRule::array()->list()->message('x'), ['list' => 'x'], ], 'distinct' => [ fn () => FluentRule::array()->distinct(message: 'x'), - fn () => FluentRule::array()->distinct()->message('x'), + fn () => FluentRule::array()->distinct()->message('x'), ['distinct' => 'x'], ], ]); @@ -45,42 +45,42 @@ dataset('phase3c_date', [ 'before' => [ fn () => FluentRule::date()->before('2026-12-31', message: 'x'), - fn () => FluentRule::date()->before('2026-12-31')->message('x'), + fn () => FluentRule::date()->before('2026-12-31')->message('x'), ['before' => 'x'], ], 'after' => [ fn () => FluentRule::date()->after('2020-01-01', message: 'x'), - fn () => FluentRule::date()->after('2020-01-01')->message('x'), + fn () => FluentRule::date()->after('2020-01-01')->message('x'), ['after' => 'x'], ], 'beforeOrEqual' => [ fn () => FluentRule::date()->beforeOrEqual('2026-12-31', message: 'x'), - fn () => FluentRule::date()->beforeOrEqual('2026-12-31')->message('x'), + fn () => FluentRule::date()->beforeOrEqual('2026-12-31')->message('x'), ['before_or_equal' => 'x'], ], 'afterOrEqual' => [ fn () => FluentRule::date()->afterOrEqual('2020-01-01', message: 'x'), - fn () => FluentRule::date()->afterOrEqual('2020-01-01')->message('x'), + fn () => FluentRule::date()->afterOrEqual('2020-01-01')->message('x'), ['after_or_equal' => 'x'], ], 'beforeToday' => [ fn () => FluentRule::date()->beforeToday(message: 'x'), - fn () => FluentRule::date()->beforeToday()->message('x'), + fn () => FluentRule::date()->beforeToday()->message('x'), ['before' => 'x'], ], 'past' => [ fn () => FluentRule::date()->past(message: 'x'), - fn () => FluentRule::date()->past()->message('x'), + fn () => FluentRule::date()->past()->message('x'), ['before' => 'x'], ], 'dateEquals' => [ fn () => FluentRule::date()->dateEquals('2026-04-22', message: 'x'), - fn () => FluentRule::date()->dateEquals('2026-04-22')->message('x'), + fn () => FluentRule::date()->dateEquals('2026-04-22')->message('x'), ['date_equals' => 'x'], ], 'same' => [ fn () => FluentRule::date()->same('other', message: 'x'), - fn () => FluentRule::date()->same('other')->message('x'), + fn () => FluentRule::date()->same('other')->message('x'), ['same' => 'x'], ], ]); @@ -88,22 +88,22 @@ dataset('phase3c_file', [ 'min' => [ fn () => FluentRule::file()->min(100, message: 'x'), - fn () => FluentRule::file()->min(100)->message('x'), + fn () => FluentRule::file()->min(100)->message('x'), ['min' => 'x'], ], 'max' => [ fn () => FluentRule::file()->max('2mb', message: 'x'), - fn () => FluentRule::file()->max('2mb')->message('x'), + fn () => FluentRule::file()->max('2mb')->message('x'), ['max' => 'x'], ], 'between' => [ fn () => FluentRule::file()->between(1, '2mb', message: 'x'), - fn () => FluentRule::file()->between(1, '2mb')->message('x'), + fn () => FluentRule::file()->between(1, '2mb')->message('x'), ['between' => 'x'], ], 'exactly' => [ fn () => FluentRule::file()->exactly(500, message: 'x'), - fn () => FluentRule::file()->exactly(500)->message('x'), + fn () => FluentRule::file()->exactly(500)->message('x'), ['size' => 'x'], ], ]); diff --git a/tests/InlineMessageParamPhase3dTest.php b/tests/InlineMessageParamPhase3dTest.php index 6385fd7..4a1c8d2 100644 --- a/tests/InlineMessageParamPhase3dTest.php +++ b/tests/InlineMessageParamPhase3dTest.php @@ -12,22 +12,22 @@ dataset('phase3d_email', [ 'max' => [ fn () => FluentRule::email()->max(255, message: 'x'), - fn () => FluentRule::email()->max(255)->message('x'), + fn () => FluentRule::email()->max(255)->message('x'), ['max' => 'x'], ], 'confirmed' => [ fn () => FluentRule::email()->confirmed(message: 'x'), - fn () => FluentRule::email()->confirmed()->message('x'), + fn () => FluentRule::email()->confirmed()->message('x'), ['confirmed' => 'x'], ], 'same' => [ fn () => FluentRule::email()->same('other', message: 'x'), - fn () => FluentRule::email()->same('other')->message('x'), + fn () => FluentRule::email()->same('other')->message('x'), ['same' => 'x'], ], 'different' => [ fn () => FluentRule::email()->different('other', message: 'x'), - fn () => FluentRule::email()->different('other')->message('x'), + fn () => FluentRule::email()->different('other')->message('x'), ['different' => 'x'], ], ]); @@ -35,7 +35,7 @@ dataset('phase3d_password', [ 'confirmed' => [ fn () => FluentRule::password()->confirmed(message: 'x'), - fn () => FluentRule::password()->confirmed()->message('x'), + fn () => FluentRule::password()->confirmed()->message('x'), ['confirmed' => 'x'], ], ]); @@ -43,12 +43,12 @@ dataset('phase3d_boolean', [ 'accepted' => [ fn () => FluentRule::boolean()->accepted(message: 'x'), - fn () => FluentRule::boolean()->accepted()->message('x'), + fn () => FluentRule::boolean()->accepted()->message('x'), ['accepted' => 'x'], ], 'declined' => [ fn () => FluentRule::boolean()->declined(message: 'x'), - fn () => FluentRule::boolean()->declined()->message('x'), + fn () => FluentRule::boolean()->declined()->message('x'), ['declined' => 'x'], ], ]); @@ -56,17 +56,17 @@ dataset('phase3d_field', [ 'same' => [ fn () => FluentRule::field()->same('other', message: 'x'), - fn () => FluentRule::field()->same('other')->message('x'), + fn () => FluentRule::field()->same('other')->message('x'), ['same' => 'x'], ], 'different' => [ fn () => FluentRule::field()->different('other', message: 'x'), - fn () => FluentRule::field()->different('other')->message('x'), + fn () => FluentRule::field()->different('other')->message('x'), ['different' => 'x'], ], 'confirmed' => [ fn () => FluentRule::field()->confirmed(message: 'x'), - fn () => FluentRule::field()->confirmed()->message('x'), + fn () => FluentRule::field()->confirmed()->message('x'), ['confirmed' => 'x'], ], ]); diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index cef62d7..161b110 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -102,8 +102,8 @@ it('uses custom error messages from the validator', function (): void { $v = new Validator( new Translator(new ArrayLoader, 'en'), - ['name' => ''], - ['name' => FluentRule::string()->required()], + ['name' => ''], + ['name' => FluentRule::string()->required()], ['name.required' => 'Please enter your name.'], ); diff --git a/tests/PresenceConditionalParityTest.php b/tests/PresenceConditionalParityTest.php index cfb3b7c..58e013f 100644 --- a/tests/PresenceConditionalParityTest.php +++ b/tests/PresenceConditionalParityTest.php @@ -81,12 +81,12 @@ function runPresenceItems(array $items, array $messages = []): array it('required_without: verdicts match native Laravel for every shape', function (): void { $shapes = [ ['postcode' => '1234AB', 'birthdate' => '1990-01-01'], - ['postcode' => '1234AB'], + ['postcode' => '1234AB'], ['birthdate' => '1990-01-01'], [], - ['postcode' => null], - ['postcode' => ''], - ['postcode' => []], + ['postcode' => null], + ['postcode' => ''], + ['postcode' => []], ['birthdate' => null], ['birthdate' => ''], ]; @@ -222,12 +222,12 @@ function runPresenceItemsForRule(string $ruleName, array $items, array $messages it('phase 2: verdicts match native Laravel across all four rules × shape grid', function (): void { $shapes = [ ['postcode' => '1234AB', 'birthdate' => '1990-01-01'], - ['postcode' => '1234AB'], + ['postcode' => '1234AB'], ['birthdate' => '1990-01-01'], [], - ['postcode' => null], - ['postcode' => ''], - ['postcode' => []], + ['postcode' => null], + ['postcode' => ''], + ['postcode' => []], ['birthdate' => null], ['birthdate' => ' '], ]; @@ -303,9 +303,9 @@ function fieldWithMultiParamPresenceRule(string $ruleName, array $params): Field ['b' => 'B'], // one sibling present, target absent [], // none present ['postcode' => 'X'], // only target present - ['a' => '', 'b' => 'B'], // one sibling empty-string - ['a' => null, 'b' => 'B'], // one sibling null - ['a' => ' ', 'b' => 'B'], // one sibling whitespace-only + ['a' => '', 'b' => 'B'], // one sibling empty-string + ['a' => null, 'b' => 'B'], // one sibling null + ['a' => ' ', 'b' => 'B'], // one sibling whitespace-only ]; $ruleNames = ['required_with', 'required_without', 'required_with_all', 'required_without_all']; @@ -431,7 +431,7 @@ static function (array $item) use ($ruleString): bool { $items = [ ['postcode' => 'X', 'birthdate' => '1990-01-01'], // birthdate present → rule active ['birthdate' => '1990-01-01'], // target absent, rule active → fail - ['postcode' => 'X'], // birthdate absent, but null slot matches full item → present + ['postcode' => 'X'], // birthdate absent, but null slot matches full item → present [], // fully empty item ]; @@ -443,7 +443,7 @@ static function (array $item) use ($ruleString): bool { $items = [ ['postcode' => 'X', 'birthdate' => '1990-01-01'], ['birthdate' => '1990-01-01'], - ['postcode' => 'X'], + ['postcode' => 'X'], [], ]; @@ -458,7 +458,7 @@ static function (array $item) use ($ruleString): bool { $items = [ ['postcode' => 'X', 'a,b' => 1, 'c' => 1], ['a,b' => 1], - ['c' => 1], + ['c' => 1], [], ]; diff --git a/tests/ValueConditionalParityTest.php b/tests/ValueConditionalParityTest.php index 8325c84..7af244b 100644 --- a/tests/ValueConditionalParityTest.php +++ b/tests/ValueConditionalParityTest.php @@ -101,10 +101,10 @@ function assertValueParity(string $flatRule, Closure $ruleBuilder, array $shapes ['flag' => 'user', 'postcode' => 'X'], [], ['postcode' => 'X'], - ['flag' => null], - ['flag' => null, 'postcode' => 'X'], - ['flag' => 'admin', 'postcode' => ''], - ['flag' => 'admin', 'postcode' => null], + ['flag' => null], + ['flag' => null, 'postcode' => 'X'], + ['flag' => 'admin', 'postcode' => ''], + ['flag' => 'admin', 'postcode' => null], ], ); }); @@ -148,8 +148,8 @@ function assertValueParity(string $flatRule, Closure $ruleBuilder, array $shapes ['flag' => 'user', 'postcode' => 'X'], [], ['postcode' => 'X'], - ['flag' => null], - ['flag' => null, 'postcode' => 'X'], + ['flag' => null], + ['flag' => null, 'postcode' => 'X'], ], ); }); @@ -214,7 +214,7 @@ function assertValueParity(string $flatRule, Closure $ruleBuilder, array $shapes ['flag' => 'user'], [], ['postcode' => 'X'], - ['flag' => null, 'postcode' => 'X'], + ['flag' => null, 'postcode' => 'X'], ], ); }); @@ -250,7 +250,7 @@ function assertValueParity(string $flatRule, Closure $ruleBuilder, array $shapes ['flag' => 'user'], [], ['postcode' => 'X'], - ['flag' => null, 'postcode' => 'X'], + ['flag' => null, 'postcode' => 'X'], ], ); }); @@ -397,9 +397,9 @@ public function validate(string $attribute, mixed $value, Closure $fail): void static fn () => FluentRule::field()->prohibitedUnless('flag', 'null'), [ [], - ['flag' => null], - ['flag' => null, 'postcode' => 'X'], - ['flag' => 'admin', 'postcode' => 'X'], + ['flag' => null], + ['flag' => null, 'postcode' => 'X'], + ['flag' => 'admin', 'postcode' => 'X'], ['postcode' => 'X'], ], ); From 8c98d8ed9c5591f4d9461a8f293c0b409a2eed9e Mon Sep 17 00:00:00 2001 From: imanimanyara <19682005+imanimanyara@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:33:47 +0000 Subject: [PATCH 2/2] chore: rector --- .gitignore | 1 + AGENTS.md | 41 +++++++++++++++++++++++++++++++ CLAUDE.md | 41 +++++++++++++++++++++++++++++++ tests/DocCountsTest.php | 12 ++++----- tests/Helpers.php | 2 +- tests/PublicApiBoundaryTest.php | 10 ++++---- tests/RuleMessagesResolveTest.php | 4 +-- 7 files changed, 97 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 94e6ccf..7abbc22 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ benchmark-snapshot.json # Generated by boost-core. Edit .ai/ instead. .agents/skills/ .boost/ +.claude/agents/boost/ .claude/commands/ .claude/skills/ .config/boost/ diff --git a/AGENTS.md b/AGENTS.md index 3af50ba..ed1ab53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,6 +59,10 @@ Write a test when the PHPStan error indicates a fault that would surface at runt - Accessing a property or method that does not exist - Any type error that would manifest as a runtime exception +### Annotate Rather Than Suppress + +Some errors are PHPStan reading a signature that says less than the code does — a return type a parameter decides, a bool helper that proves a type. The `backend-quality` skill carries the two annotations that state the missing fact, and the rules for when each one lies. + ### When to Skip the Test Skip the test when the error is purely static and cannot cause a runtime failure: @@ -85,6 +89,29 @@ A missing signature is a blocker to resolve (unlock the agent, re-authenticate 1 --- +## Task Scope and Edits + +For session, branch, and PR scope, see the `single-issue-scope` guideline when the project enables it. + +### The Task Sets the Scope + +- Do not fix a pre-existing bug, a performance problem, or unrelated behaviour you find on the way, unless the requested behaviour cannot work without it. The same holds for refactors, cleanup, and documentation nobody asked for. A defect your own change introduces is not pre-existing: fix it. +- A sibling rule that requires an update on a line you already change still applies. The rule removes extras, not obligations. +- Report the rest as a follow-up in your summary. Propose an issue when the project tracks work that way, and let the user decide whether to file it. Report it; do not fix it. +- Implement every behaviour the task does ask for, completely. This rule cuts extras, never the requested scope. + +### One Reading of an Ambiguous Ask + +Implement the reading that the wording and the surrounding code support most directly. State that assumption in your summary. Do not build for both readings. + +Materially different work is the test. When two readings would produce the same change, pick one and carry on. When they would not, or when a wrong guess is unsafe or makes the work useless, ask before building — through the `clarify` skill where the whole ask is fuzzy, otherwise with a direct question. + +### Edit in Place + +Change only the lines that must change. Rewrite a whole file only when the file is short, or when most of it changes. A rewrite churns lines the task never touched and can drop content by accident. + +--- + ## Verification Before Completion Before claiming any work is complete or successful, run the verification command fresh and confirm the output. Evidence before claims, always. @@ -115,6 +142,12 @@ Use the project's own commands — check its `composer.json` / `package.json` sc Where the project has dedicated quality-check skills synced, delegate to them — `backend-quality` for backend files, `frontend-quality` for frontend files, both when a change spans both. Otherwise, run the project's own equivalent commands directly. +### A Commit Is a Claim Too + +Commit a change once its own checks pass against the tree as it stands, not while the approach is still being tried. A commit reads as a decision. The next defect then gets patched on top of the approach instead of the approach being dropped, and each extra commit raises the cost of the revert that was the right answer. + +Deferring is not "never commit". Uncommitted work is unprotected, and a commit is still the safe way to set work aside or to hand it over. A measurement loop inverts the rule on purpose — it commits before it measures, so a rejected experiment reverts in one step. Where a skill states that it commits first, that skill wins for its own flow. + ### Never Use Without Evidence - "should work now" @@ -124,6 +157,14 @@ Where the project has dedicated quality-check skills synced, delegate to them These phrases indicate missing verification. Run the command first, then report what actually happened. +### Say What You Did Not Verify + +State the limits of your own check. A reader cannot tell a gap you did not mention from a check you ran, so an unmentioned gap counts as a claim you did not make good on. + +When you report a result, name what you ran and what you did not. "The unit suite passes; I did not run the browser tests" is a complete report. "Tests pass" is not, when you ran one suite of three. The same holds for a claim you carried over from an earlier step: if you did not re-run it against the tree as it stands now, say so. + +This is the outward half of the `NEEDS-CONFIRMATION` rule above. That rule stops you asserting an untraced cause. This one stops a traced, true statement from implying more than it covers. + --- # Laravel Package Guidelines diff --git a/CLAUDE.md b/CLAUDE.md index 3af50ba..ed1ab53 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,10 @@ Write a test when the PHPStan error indicates a fault that would surface at runt - Accessing a property or method that does not exist - Any type error that would manifest as a runtime exception +### Annotate Rather Than Suppress + +Some errors are PHPStan reading a signature that says less than the code does — a return type a parameter decides, a bool helper that proves a type. The `backend-quality` skill carries the two annotations that state the missing fact, and the rules for when each one lies. + ### When to Skip the Test Skip the test when the error is purely static and cannot cause a runtime failure: @@ -85,6 +89,29 @@ A missing signature is a blocker to resolve (unlock the agent, re-authenticate 1 --- +## Task Scope and Edits + +For session, branch, and PR scope, see the `single-issue-scope` guideline when the project enables it. + +### The Task Sets the Scope + +- Do not fix a pre-existing bug, a performance problem, or unrelated behaviour you find on the way, unless the requested behaviour cannot work without it. The same holds for refactors, cleanup, and documentation nobody asked for. A defect your own change introduces is not pre-existing: fix it. +- A sibling rule that requires an update on a line you already change still applies. The rule removes extras, not obligations. +- Report the rest as a follow-up in your summary. Propose an issue when the project tracks work that way, and let the user decide whether to file it. Report it; do not fix it. +- Implement every behaviour the task does ask for, completely. This rule cuts extras, never the requested scope. + +### One Reading of an Ambiguous Ask + +Implement the reading that the wording and the surrounding code support most directly. State that assumption in your summary. Do not build for both readings. + +Materially different work is the test. When two readings would produce the same change, pick one and carry on. When they would not, or when a wrong guess is unsafe or makes the work useless, ask before building — through the `clarify` skill where the whole ask is fuzzy, otherwise with a direct question. + +### Edit in Place + +Change only the lines that must change. Rewrite a whole file only when the file is short, or when most of it changes. A rewrite churns lines the task never touched and can drop content by accident. + +--- + ## Verification Before Completion Before claiming any work is complete or successful, run the verification command fresh and confirm the output. Evidence before claims, always. @@ -115,6 +142,12 @@ Use the project's own commands — check its `composer.json` / `package.json` sc Where the project has dedicated quality-check skills synced, delegate to them — `backend-quality` for backend files, `frontend-quality` for frontend files, both when a change spans both. Otherwise, run the project's own equivalent commands directly. +### A Commit Is a Claim Too + +Commit a change once its own checks pass against the tree as it stands, not while the approach is still being tried. A commit reads as a decision. The next defect then gets patched on top of the approach instead of the approach being dropped, and each extra commit raises the cost of the revert that was the right answer. + +Deferring is not "never commit". Uncommitted work is unprotected, and a commit is still the safe way to set work aside or to hand it over. A measurement loop inverts the rule on purpose — it commits before it measures, so a rejected experiment reverts in one step. Where a skill states that it commits first, that skill wins for its own flow. + ### Never Use Without Evidence - "should work now" @@ -124,6 +157,14 @@ Where the project has dedicated quality-check skills synced, delegate to them These phrases indicate missing verification. Run the command first, then report what actually happened. +### Say What You Did Not Verify + +State the limits of your own check. A reader cannot tell a gap you did not mention from a check you ran, so an unmentioned gap counts as a claim you did not make good on. + +When you report a result, name what you ran and what you did not. "The unit suite passes; I did not run the browser tests" is a complete report. "Tests pass" is not, when you ran one suite of three. The same holds for a claim you carried over from an earlier step: if you did not re-run it against the tree as it stands now, say so. + +This is the outward half of the `NEEDS-CONFIRMATION` rule above. That rule stops you asserting an untraced cause. This one stops a traced, true statement from implying more than it covers. + --- # Laravel Package Guidelines diff --git a/tests/DocCountsTest.php b/tests/DocCountsTest.php index 131a224..94bcaa3 100644 --- a/tests/DocCountsTest.php +++ b/tests/DocCountsTest.php @@ -16,7 +16,7 @@ /** @return list> */ function liveRuleClasses(): array { - $root = dirname(__DIR__) . '/src/Rules'; + $root = __DIR__ . '/../src/Rules'; $classes = []; foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root)) as $file) { @@ -39,7 +39,7 @@ function liveRuleClasses(): array it('states the real rule count in the CHANGELOG', function (): void { $actual = count(liveRuleClasses()); - $prose = (string) file_get_contents(dirname(__DIR__) . '/CHANGELOG.md'); + $prose = (string) file_get_contents(__DIR__ . '/../CHANGELOG.md'); preg_match_all('/(\d+) rules across/', $prose, $m); @@ -56,13 +56,13 @@ function liveRuleClasses(): array it('documents every rule family with a section in the rule reference', function (): void { // Directory name → the reference page's heading, where they differ. $headings = ['AntiSpam' => 'Anti-spam', 'Vendor' => 'Vendor identifiers']; - $reference = (string) file_get_contents(dirname(__DIR__) . '/docs/tools/rule-library.md'); + $reference = (string) file_get_contents(__DIR__ . '/../docs/tools/rule-library.md'); - $entries = scandir(dirname(__DIR__) . '/src/Rules'); + $entries = scandir(__DIR__ . '/../src/Rules'); $families = array_values(array_filter( $entries === false ? [] : $entries, static fn (string $entry): bool => ! str_starts_with($entry, '.') - && is_dir(dirname(__DIR__) . '/src/Rules/' . $entry), + && is_dir(__DIR__ . '/../src/Rules/' . $entry), )); foreach ($families as $family) { @@ -75,7 +75,7 @@ function liveRuleClasses(): array }); it('names every ClientCheckable implementer in the CHANGELOG', function (): void { - $prose = (string) file_get_contents(dirname(__DIR__) . '/CHANGELOG.md'); + $prose = (string) file_get_contents(__DIR__ . '/../CHANGELOG.md'); $implementers = array_values(array_filter( liveRuleClasses(), diff --git a/tests/Helpers.php b/tests/Helpers.php index eb26067..dbad09b 100644 --- a/tests/Helpers.php +++ b/tests/Helpers.php @@ -90,7 +90,7 @@ function compiledArray(mixed $compiled): array */ function ruleClassesUnder(?string $implementing = null): array { - $base = dirname(__DIR__) . '/src/Rules'; + $base = __DIR__ . '/../src/Rules'; $classes = []; foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base)) as $file) { diff --git a/tests/PublicApiBoundaryTest.php b/tests/PublicApiBoundaryTest.php index ee24ff8..566acb4 100644 --- a/tests/PublicApiBoundaryTest.php +++ b/tests/PublicApiBoundaryTest.php @@ -47,9 +47,9 @@ it('marks every Internal\\ and FastCheck\\ class @internal', function (): void { $globs = [ - glob(dirname(__DIR__) . '/src/Internal/*.php'), - glob(dirname(__DIR__) . '/src/FastCheck/*.php'), - glob(dirname(__DIR__) . '/src/FastCheck/Shared/*.php'), + glob(__DIR__ . '/../src/Internal/*.php'), + glob(__DIR__ . '/../src/FastCheck/*.php'), + glob(__DIR__ . '/../src/FastCheck/Shared/*.php'), ]; $files = array_merge(...array_map( static fn (array|false $found): array => $found === false ? [] : $found, @@ -67,7 +67,7 @@ it('marks every top-level optimizer class @internal', function (): void { foreach (INTERNAL_TOP_LEVEL as $path) { - expect(str_contains((string) file_get_contents(dirname(__DIR__) . '/' . $path), '@internal'))->toBeTrue( + expect(str_contains((string) file_get_contents(__DIR__ . '/../' . $path), '@internal'))->toBeTrue( $path . ' is optimizer machinery and must carry @internal.', ); } @@ -75,7 +75,7 @@ it('never marks the stable surface @internal at class level', function (): void { foreach (STABLE_SURFACE as $path) { - $source = (string) file_get_contents(dirname(__DIR__) . '/' . $path); + $source = (string) file_get_contents(__DIR__ . '/../' . $path); // Only the CLASS-level docblock counts: a stable class may still // mark an individual method @internal. Inspect the source up to the diff --git a/tests/RuleMessagesResolveTest.php b/tests/RuleMessagesResolveTest.php index 9aa8662..44c7f49 100644 --- a/tests/RuleMessagesResolveTest.php +++ b/tests/RuleMessagesResolveTest.php @@ -82,7 +82,7 @@ public function test_the_namespace_is_the_composer_package_name(): void */ public function test_every_shipped_locale_is_complete(): void { - $root = dirname(__DIR__) . '/resources/lang'; + $root = __DIR__ . '/../resources/lang'; $reference = require $root . '/en/validation.php'; $this->assertIsArray($reference); @@ -121,7 +121,7 @@ public function test_no_message_is_left_as_a_placeholder(): void private function keysReferencedInSource(): array { $keys = []; - $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(dirname(__DIR__) . '/src')); + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__ . '/../src')); foreach ($files as $file) { // RecursiveIteratorIterator is typed as yielding mixed, and the