From b1b83e440e47466468d938553cc478f896f7029c Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 5 Oct 2026 09:10:57 -0400 Subject: [PATCH 1/2] Declare Illuminate components and expect the package-tools alias FluentFormRequest extends Foundation's FormRequest and src/ calls Foundation helpers and nine illuminate components it never declared (D3); they are now required, and DeclaredRequirementsTest keeps it that way. package-tools 0.1.3 made hasTranslations() register the hyphen alias of laranail/validation, so RuleMessagesResolveTest went red on main against fresh dependencies; it now asserts the alias resolves to the canonical message, and require raises package-tools to ^0.1.3. --- CHANGELOG.md | 15 +++ composer.json | 12 ++- tests/DeclaredRequirementsTest.php | 166 +++++++++++++++++++++++++++++ tests/RuleMessagesResolveTest.php | 7 +- 4 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 tests/DeclaredRequirementsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 880edcf..f1c6f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,12 @@ Entries below `Unreleased` are written by CI from the GitHub release body — se `vendor/laranail/package-tools/pint.json`. The job now installs dependencies and runs `composer pint-fix`, which is the same command the `lint` gate checks. 259 files are reformatted by this change; no behaviour is affected (6276 tests, 10486 assertions, identical before and after). +- **`RuleMessagesResolveTest` asserted that the dashed translation namespace does not resolve.** + `laranail/package-tools` 0.1.3 made `hasTranslations()` register `laranail-validation` as an alias + of `laranail/validation` over the same files, so against fresh dependencies the test went red on + `main` with no change here. It now asserts the alias resolves to exactly the canonical message, + and `require` raises `laranail/package-tools` to `^0.1.3` so a `prefer-lowest` resolve gets a + version that registers it. ### Changed @@ -34,6 +40,15 @@ Entries below `Unreleased` are written by CI from the GitHub release body — se (`Redirector`). It arrived only transitively before. - Dropped the `vcs` repositories for `laranail/atlas` and `laranail/enumerator`: nothing in this package's `require` or `require-dev` closure installs them (checked with `composer why` after a fresh `composer update`). +- `require` now declares every Illuminate component `src/` uses: `illuminate/auth`, + `illuminate/cache`, `illuminate/config`, `illuminate/container`, `illuminate/database`, + `illuminate/encryption`, `illuminate/events`, `illuminate/filesystem` and + `illuminate/translation`, plus `laravel/framework ^13.0`. `FluentFormRequest` extends + `Illuminate\Foundation\Http\FormRequest`, which has no split package, and the global helpers + `src/` calls (`config()`, `app()`, `trans()`, `event()`, `config_path()`, ...) are defined only in + `Illuminate/Foundation/helpers.php`. All of them arrived only transitively before. + `tests/DeclaredRequirementsTest.php` scans `src/` and fails on any use the manifest does not + declare. ### Added diff --git a/composer.json b/composer.json index 9b67dec..4a3b698 100644 --- a/composer.json +++ b/composer.json @@ -67,13 +67,23 @@ }, "require": { "php": "^8.5", + "illuminate/auth": "^13.0", + "illuminate/cache": "^13.0", + "illuminate/config": "^13.0", + "illuminate/container": "^13.0", "illuminate/contracts": "^13.0", + "illuminate/database": "^13.0", + "illuminate/encryption": "^13.0", + "illuminate/events": "^13.0", + "illuminate/filesystem": "^13.0", "illuminate/http": "^13.0", "illuminate/routing": "^13.0", "illuminate/support": "^13.0", + "illuminate/translation": "^13.0", "illuminate/validation": "^13.0", - "laranail/package-tools": "^0.1", "laranail/console": "^0.1", + "laranail/package-tools": "^0.1.3", + "laravel/framework": "^13.0", "symfony/process": "^7.4 || ^8.0" }, "require-dev": { diff --git a/tests/DeclaredRequirementsTest.php b/tests/DeclaredRequirementsTest.php new file mode 100644 index 0000000..00cafa8 --- /dev/null +++ b/tests/DeclaredRequirementsTest.php @@ -0,0 +1,166 @@ +\...` name, imported or fully qualified; + * - a facade from `Illuminate\Support\Facades`, mapped to the component behind it; + * - a global helper. Every global helper (`config()`, `app()`, `__()`, `now()`, `report()`, ...) is + * defined in `Illuminate/Foundation/helpers.php`, which only `laravel/framework` autoloads, so a + * helper call needs `laravel/framework` as well as the component it reaches. + * + * `Illuminate\Foundation` has no split package, so its declaration is `laravel/framework`, which + * "replaces" every `illuminate/*` split and so is consistent with requiring them too (estate-followups + * decision D3). A line carrying a `class_exists()`-style guard is skipped: that use is optional. + * Measured 2026-10-05 (tooling-hygiene audit H6). + */ + +/** @var array facade => package */ +const VALIDATION_FACADES = [ + 'App' => 'illuminate/container', 'Artisan' => 'illuminate/console', 'Auth' => 'illuminate/auth', + 'Blade' => 'illuminate/view', 'Broadcast' => 'illuminate/broadcasting', 'Bus' => 'illuminate/bus', + 'Cache' => 'illuminate/cache', 'Config' => 'illuminate/config', 'Context' => 'illuminate/log', + 'Cookie' => 'illuminate/cookie', 'Crypt' => 'illuminate/encryption', 'Date' => 'illuminate/support', + 'DB' => 'illuminate/database', 'Event' => 'illuminate/events', 'Facade' => 'illuminate/support', + 'File' => 'illuminate/filesystem', 'Gate' => 'illuminate/auth', 'Hash' => 'illuminate/hashing', + 'Http' => 'illuminate/http', 'Lang' => 'illuminate/translation', 'Log' => 'illuminate/log', + 'Mail' => 'illuminate/mail', 'Notification' => 'illuminate/notifications', 'Password' => 'illuminate/auth', + 'Pipeline' => 'illuminate/pipeline', 'Process' => 'illuminate/process', 'Queue' => 'illuminate/queue', + 'RateLimiter' => 'illuminate/cache', 'Redirect' => 'illuminate/routing', 'Redis' => 'illuminate/redis', + 'Request' => 'illuminate/http', 'Response' => 'illuminate/routing', 'Route' => 'illuminate/routing', + 'Schedule' => 'illuminate/console', 'Schema' => 'illuminate/database', 'Session' => 'illuminate/session', + 'Storage' => 'illuminate/filesystem', 'URL' => 'illuminate/routing', 'Validator' => 'illuminate/validation', + 'View' => 'illuminate/view', 'Vite' => 'laravel/framework', +]; + +/** @var array helper => package it reaches (null: Foundation itself) */ +const VALIDATION_HELPERS = [ + 'abort' => null, 'abort_if' => null, 'abort_unless' => null, 'app' => 'illuminate/container', + 'app_path' => null, 'auth' => 'illuminate/auth', 'base_path' => null, 'bcrypt' => 'illuminate/hashing', + 'cache' => 'illuminate/cache', 'config' => 'illuminate/config', 'config_path' => null, + 'database_path' => null, 'dispatch' => 'illuminate/bus', 'event' => 'illuminate/events', + 'info' => 'illuminate/log', 'lang_path' => null, 'logger' => 'illuminate/log', 'now' => 'illuminate/support', + 'public_path' => null, 'redirect' => 'illuminate/routing', 'report' => null, 'request' => 'illuminate/http', + 'rescue' => null, 'resolve' => 'illuminate/container', 'resource_path' => null, + 'response' => 'illuminate/routing', 'route' => 'illuminate/routing', 'session' => 'illuminate/session', + 'storage_path' => null, 'to_route' => 'illuminate/routing', 'today' => 'illuminate/support', + 'trans' => 'illuminate/translation', 'trans_choice' => 'illuminate/translation', '__' => 'illuminate/translation', + 'url' => 'illuminate/routing', 'validator' => 'illuminate/validation', 'view' => 'illuminate/view', +]; + +/** Source with comments blanked out, line numbers kept: comments carry names that are not uses. */ +function validationCodeWithoutComments(string $path): string +{ + $code = ''; + + foreach (token_get_all((string) file_get_contents($path)) as $token) { + if (! is_array($token)) { + $code .= $token; + + continue; + } + + $code .= in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true) + ? str_repeat("\n", substr_count($token[1], "\n")) + : $token[1]; + } + + return $code; +} + +/** @return list packages one line of source reaches through an `Illuminate\...` name */ +function validationNamedPackages(string $line): array +{ + preg_match_all('/Illuminate\\\\([A-Za-z]+)\\\\(?:Facades\\\\([A-Za-z]+))?/', $line, $names, PREG_SET_ORDER); + + $packages = []; + + foreach ($names as $name) { + $facade = $name[2] ?? ''; + + $packages[] = match (true) { + $name[1] === 'Support' && $facade !== '' => VALIDATION_FACADES[$facade] ?? 'unmapped facade ' . $facade, + $name[1] === 'Foundation' => 'laravel/framework', + default => 'illuminate/' . strtolower($name[1]), + }; + } + + return $packages; +} + +/** @return list global helpers one line of source calls */ +function validationHelperCalls(string $line): array +{ + $bare = (string) preg_replace(["/'(?:\\\\.|[^'\\\\])*'/", '/"(?:\\\\.|[^"\\\\])*"/'], "''", $line); + preg_match_all('/(?:$\\\\])(? array_key_exists($call, VALIDATION_HELPERS))); +} + +/** @return array> package => evidence ("file:line") */ +function validationUsedIlluminatePackages(): array +{ + $root = dirname(__DIR__); + $used = []; + $files = 0; + + /** @var iterable $iterator */ + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/src', FilesystemIterator::SKIP_DOTS)); + + foreach ($iterator as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $files++; + $relative = substr($file->getPathname(), strlen($root) + 1); + + foreach (explode("\n", validationCodeWithoutComments($file->getPathname())) as $index => $line) { + // A guarded line is an optional use. + if (preg_match('/\b(class|interface|trait|function)_exists\s*\(/', $line) === 1) { + continue; + } + + $where = $relative . ':' . ($index + 1); + + foreach (validationNamedPackages($line) as $package) { + $used[$package][] = $where; + } + + foreach (validationHelperCalls($line) as $helper) { + $used['laravel/framework'][] = $where . ' (' . $helper . '())'; + + if (VALIDATION_HELPERS[$helper] !== null) { + $used[VALIDATION_HELPERS[$helper]][] = $where . ' (' . $helper . '())'; + } + } + } + } + + // Non-vacuity: a scan that found no source, or no use, proves nothing about it. + expect($files)->toBeGreaterThanOrEqual(200) + ->and(count($used))->toBeGreaterThanOrEqual(13); + + return $used; +} + +it('declares every illuminate component that src/ uses', function (): void { + /** @var array{require: array} $composer */ + $composer = json_decode((string) file_get_contents(__DIR__ . '/../composer.json'), true, flags: JSON_THROW_ON_ERROR); + + $missing = []; + + foreach (validationUsedIlluminatePackages() as $package => $where) { + if (! array_key_exists($package, $composer['require'])) { + $missing[$package] = $where[0]; + } + } + + expect($missing)->toBeEmpty('Used by src/ but not declared in require: ' . json_encode($missing, JSON_UNESCAPED_SLASHES)); +}); diff --git a/tests/RuleMessagesResolveTest.php b/tests/RuleMessagesResolveTest.php index 44c7f49..ead05fb 100644 --- a/tests/RuleMessagesResolveTest.php +++ b/tests/RuleMessagesResolveTest.php @@ -67,11 +67,16 @@ public function test_the_namespace_is_the_composer_package_name(): void // translator's hasForLocale(): that method is on the concrete // Translator and not on the contract, so reaching it means either a // string container key or a type-hint that does not declare it. + // + // The dashed form is laranail/package-tools' alias (registered by + // hasTranslations() since package-tools 0.1.3), so a host that published + // or wrote the older spelling still gets the same sentence. It must + // resolve to exactly what the canonical name does, never to its own copy. $slashed = 'laranail/validation::validation.iban'; $dashed = 'laranail-validation::validation.iban'; $this->assertNotSame($slashed, trans($slashed), 'The composer-package namespace did not resolve.'); - $this->assertSame($dashed, trans($dashed), 'The dashed namespace resolved, so both are registered.'); + $this->assertSame(trans($slashed), trans($dashed), 'The dashed alias does not resolve to the canonical message.'); } /** From 8ec9ecfda108e3a51492d2bbcb98efb78f22d748 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Mon, 5 Oct 2026 09:36:55 -0400 Subject: [PATCH 2/2] Re-measure complexity baseline for cognitive-complexity 1.3 cognitive-complexity 1.3.0 (2026-09-29) scores the same source differently from 1.2.0, which the baseline was written against, so every complexity entry stopped matching and the required phpstan check went red with no code change. Each entry now records the value 1.3.0 measures; four newly reported ones are added at their measured values. Nothing is removed and no limit is raised. require-dev floors the package at ^1.3 so a prefer-lowest resolve scores on the same scale. --- CHANGELOG.md | 7 ++++++ composer.json | 2 +- phpstan-baseline.neon | 55 ++++++++++++++++++++++++++++++++++++------- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c6f33..9aa114d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,13 @@ Entries below `Unreleased` are written by CI from the GitHub release body — se `Illuminate/Foundation/helpers.php`. All of them arrived only transitively before. `tests/DeclaredRequirementsTest.php` scans `src/` and fails on any use the manifest does not declare. +- `require-dev` raises `tomasvotruba/cognitive-complexity` to `^1.3`, and the PHPStan baseline + records the values 1.3.0 measures. 1.3.0 (2026-09-29) scores the same source differently from + 1.2.0, mostly higher (`RuleSet` 141 -> 157, `RuleConfigBuilder::buildValueClosure()` 52 -> 76, + ...), so every complexity entry stopped matching and the required `phpstan` check went red with no + code change. Each entry is re-measured, none is removed, and four newly reported ones + (`BatchDatabaseChecker` and `DoctorCommand::handle()`) are added at their measured values. The + floor keeps a prefer-lowest resolve on the same scale; no limit was raised. ### Added diff --git a/composer.json b/composer.json index 4a3b698..7cecec7 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,7 @@ "sandermuller/package-boost-laravel": "^1.0", "spaze/phpstan-disallowed-calls": "^4.10", "symplify/phpstan-extensions": "^12.0.2", - "tomasvotruba/cognitive-complexity": "^1.1", + "tomasvotruba/cognitive-complexity": "^1.3", "tomasvotruba/type-coverage": "^2.3.4" }, "suggest": { diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 680be8f..ff033c7 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,5 +1,17 @@ parameters: ignoreErrors: + # Every complexity.* value below was re-measured on 2026-10-05 with + # tomasvotruba/cognitive-complexity 1.3.0, whose scoring differs from + # 1.2.0 (which the earlier numbers in the comments below were written + # against). No source changed between the two measurements, so the + # difference is the scorer's alone; composer.json floors the package at + # ^1.3 so a prefer-lowest resolve scores on the same scale. + # 1.2.0 -> 1.3.0: RuleConfigBuilder class 118 -> 155, buildValueClosure + # 52 -> 76; ItemContextCompiler class 98 -> 125, buildItemAwareClosure + # 59 -> 86; PresenceConditionalCompiler::compile 25 -> 24; RuleSet class + # 141 -> 157; ItemRuleCompiler class 88 -> 111, buildFastChecks 46 -> 68; + # ItemValidator::validate 49 -> 64. BatchDatabaseChecker and + # DoctorCommand::handle were under the limits on 1.2.0 and are new here. # Hot-path closure builders: complexity comes from intentional # inlining (size, in/regex, date comparisons) to avoid closure # allocation + call overhead on every validated item. @@ -15,31 +27,31 @@ parameters: # was extracted, and addDateChecks() dropped under its own limit — # its entry is gone. - - rawMessage: 'Class cognitive complexity is 118, keep it under 80' + rawMessage: 'Class cognitive complexity is 155, keep it under 80' identifier: complexity.classLike count: 1 path: src/FastCheck/RuleConfigBuilder.php - - rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\FastCheck\RuleConfigBuilder::buildValueClosure()" is 52, keep it under 20' + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\FastCheck\RuleConfigBuilder::buildValueClosure()" is 76, keep it under 20' identifier: complexity.functionLike count: 1 path: src/FastCheck/RuleConfigBuilder.php - - rawMessage: 'Class cognitive complexity is 98, keep it under 80' + rawMessage: 'Class cognitive complexity is 125, keep it under 80' identifier: complexity.classLike count: 1 path: src/FastCheck/ItemContextCompiler.php - - rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\FastCheck\ItemContextCompiler::buildItemAwareClosure()" is 59, keep it under 20' + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\FastCheck\ItemContextCompiler::buildItemAwareClosure()" is 86, keep it under 20' identifier: complexity.functionLike count: 1 path: src/FastCheck/ItemContextCompiler.php - - rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\FastCheck\PresenceConditionalCompiler::compile()" is 25, keep it under 20' + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\FastCheck\PresenceConditionalCompiler::compile()" is 24, keep it under 20' identifier: complexity.functionLike count: 1 path: src/FastCheck/PresenceConditionalCompiler.php @@ -67,7 +79,7 @@ parameters: # missing-dependency guard. The after() route itself lives in # Internal\VanillaAfterRoute, not here. - - rawMessage: 'Class cognitive complexity is 141, keep it under 80' + rawMessage: 'Class cognitive complexity is 157, keep it under 80' identifier: complexity.classLike count: 1 path: src/RuleSet.php @@ -78,7 +90,7 @@ parameters: # for native-parity. Dropping the class under 80 is a whole-class refactor # out of scope. - - rawMessage: 'Class cognitive complexity is 88, keep it under 80' + rawMessage: 'Class cognitive complexity is 111, keep it under 80' identifier: complexity.classLike count: 1 path: src/Internal/ItemRuleCompiler.php @@ -87,7 +99,7 @@ parameters: # flat × item-aware vs value-only) — structural complexity that # refactoring would only move. - - rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\Internal\ItemRuleCompiler::buildFastChecks()" is 46, keep it under 20' + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\Internal\ItemRuleCompiler::buildFastChecks()" is 68, keep it under 20' identifier: complexity.functionLike count: 1 path: src/Internal/ItemRuleCompiler.php @@ -99,11 +111,36 @@ parameters: # carries the score, and refactoring those caches has measurable # perf cost (per-item validator reuse + dispatch reduction). - - rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\Internal\ItemValidator::validate()" is 49, keep it under 20' + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\Internal\ItemValidator::validate()" is 64, keep it under 20' identifier: complexity.functionLike count: 1 path: src/Internal/ItemValidator.php + # New on cognitive-complexity 1.3.0 (under the limits on 1.2.0, same code). + - + rawMessage: 'Class cognitive complexity is 100, keep it under 80' + identifier: complexity.classLike + count: 1 + path: src/BatchDatabaseChecker.php + + - + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\BatchDatabaseChecker::collectValues()" is 22, keep it under 20' + identifier: complexity.functionLike + count: 1 + path: src/BatchDatabaseChecker.php + + - + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\BatchDatabaseChecker::collectExpandedValues()" is 23, keep it under 20' + identifier: complexity.functionLike + count: 1 + path: src/BatchDatabaseChecker.php + + - + rawMessage: 'Cognitive complexity for "Simtabi\Laranail\Validation\Commands\DoctorCommand::handle()" is 23, keep it under 20' + identifier: complexity.functionLike + count: 1 + path: src/Commands/DoctorCommand.php + # Cross-version Filament guard. The trait may be mixed into components # that lack getCachedForms (plain Livewire without InteractsWithForms), # so the method_exists() check is required at runtime. PHPStan only ever