Repository navigation
Add PHPStan rules for Hypervel's long-lived workers - #742
binaryfire wants to merge 22 commits into
Conversation
Include optional Unsupported guidance in the main diagnostic so terminal and CI formatters retain it. Specify reflection-based argument reading and focused coverage for bare markers and supplied messages, and update the documentation requirements.
Expose resolvedShared() on the container contract so facades can distinguish worker-held instances from fresh resolutions. Retain worker caching for shared roots, keep fresh roots per coroutine, and leave scoped and uncached facades resolving through the container. Generation changes invalidate cached coroutine roots across live contexts, while copied child contexts resolve their own roots. Register the date factory during foundation bootstrap without replacing earlier bindings. Standalone Date facade fallback no longer writes a container binding. Document the supported Date subclass adaptation and cover concurrent resolution, singleton-selecting factories, copied contexts, invalidation and date binding precedence.
Add return templates to filesystem borrowing, pinned Redis connections and SQS connection callbacks without changing runtime behavior. Let filesystem callback parameter templates accept the concrete client, adapter or driver selected by the caller while preserving useful Flysystem bounds for untyped callbacks. Cover typed SDK callbacks, untyped clients and concrete or inferred Flysystem parameters in the max-level type fixtures.
Record the merged attributes prerequisite and completed runtime section. Describe callback parameter templates and their type coverage, remove stale prerequisite gates and instruction line numbers, and keep the remaining implementation at the shared analysis infrastructure.
…d facades Resolve facade accessors and exact root classes from declarations and booted bindings without executing accessors or factories. Respect overrides and unknown construction paths, and include actionable Unsupported attribute guidance in diagnostic messages. Track constants referenced by method attributes and parameter defaults through the existing source dependency helper. Cover native and nullsafe calls, facade factories and binding chains, supported overrides, and uncertain roots.
Share the collected worker-state model across rules and recognize listener, observer, and server-operation contexts. Preserve per-method receiver evidence through inherited handlers and helpers, distinguishing container-built queued listeners from restored job payloads. Report boot-only and test-only calls with their parameter conditions and proven shared receivers. Merge injection evidence across routes, jobs, and constructors; recognize singleton factories and readonly promoted dependencies; and give configuration writes one diagnostic. Add malformed-attribute checks, isolated enablement coverage, and warm-cache regressions for registrations, facade accessors, and constant-backed defaults. Preserve unknown receiver and binding paths without executing application code.
Record the implemented facade resolution, observer discovery, merged injection evidence, and source dependencies. Clarify the bounded receiver and reference-alias analysis and mark section 3.3 as the next implementation step.
Plan removal of the global mutable-date opt-out while preserving useful customization and explicit interoperability conversions. Include stronger return types and PHPStan diagnostics once runtime immutability is guaranteed.
Align resolver, manager, Capsule, testing and queue accessor return types with the Connection base class already required by Eloquent and migrations. Keep custom non-PDO drivers supported and retain ConnectionInterface at independently supplied connection boundaries. Remove redundant type guards and analysis suppressions. Update resolver-backed test doubles without changing their behavioral assertions, including non-PDO rejection coverage and both queue skip-locked outcomes. Document the custom resolver and mock requirements. Expand the native request API porting guidance and remove the ineffective HTTP status assignment during CLI environment bootstrap. Database, queue, foundation, facade metadata and affected integration tests pass; unavailable database drivers remain skipped.
Default LOG_USER facility configuration to the equivalent user string in the framework and both application skeletons. Monolog resolves named string facilities but rejects numeric strings from the environment, so the default now reflects the supported environment value. Preserve the existing LogManager fallback and facility behavior. Static analysis and formatting pass for the combined change.
Detect handles escaping lending callbacks, premature completion of managed transactions, native request APIs, discarded immutable date results and explicit mutable date creation. Match framework declarations and preserve user overrides; share connection inspection with the queue transaction rule. Add configuration environment-cast checks, typed getter guidance, observer registration conventions and application binding-cycle detection. Keep migration column policy disabled by default. Restrict configuration value inference to proven application repository reads so independent repositories retain their declared types. Reuse runtime dependency tracking and configuration source discovery, remove redundant callable guards, and document legitimate framework exceptions with local reasons. Add behavioral rule fixtures and update type expectations. The PHPStan suite, static analysis and formatting pass.
Record the implemented resolver contract, configuration ownership boundary and declaration-aware rules. Describe supported borrowed-resource and transaction identities, regression coverage and the required consumer mock updates. Mark sections 3.3 through 3.12 complete and retain section 3.13 as the next implementation step.
Move exact-catch inspection into the coroutine helper so cancellation checks can share the same verdict without duplicate reports. Resolve receivers assigned inside try blocks while discarding stale input facts and preserving argument-dependent return types through PHPStan inference. Expose source method lookup for inherited and aliased trait methods while keeping constructor ownership intact. Cover a channel assigned from a typed parameter in the existing exact-catch fixture.
Report swallowed cancellation around recognized synchronous database, Redis, HTTP and wait operations in request, job and operation handlers. Follow forwarded methods, model traits and exception causes while respecting overrides, deferred work and schema defaults that only throw. Track exception constructor files for warm result-cache invalidation. Add default-on checks for raw coroutine spawning, direct Socialite context-property access, Swoole tables allocated in provider bodies and request-dependent BindWhen conditions. Inspect condition callables in their own source namespace and retain explicit reasons for framework-owned raw coroutine use. Add focused rule fixtures for accepted alternatives, distinct dispatch paths and verified regressions. Validation includes formatting, framework analysis, targeted rule and cache tests, and the PHPStan test suite.
Record the implemented receiver and callable analysis, supported synchronous operation families, exception propagation checks and cache dependencies. Describe the verified exclusions and regression coverage, and move remaining work to documentation, final verification and approved guidance updates.
Explain the new checks with corrective examples, configuration switches and supported exceptions. Describe deferred execution and receiver ownership boundaries without promising analysis of unknown callbacks or repositories. Update the rule index, method-attribute guidance and worker-context coverage. Record the config-repository typing difference from Larastan.
Explain that PHPStan checks restricted and unsupported methods. Replace the blanket redundant-singleton example with guidance that preserves intentional binding, optional injection and rebinding behavior, and correct the alias-registration description. Record documentation completion and the remaining benchmark and final review. Hold PR creation until the owner has reviewed completion and discussed next steps.
Capture the vendor directory chosen when creating a disposable skeleton copy and expose it alongside the existing skeleton source path. Preserve its lexical spelling so consumers can observe later symlink changes, and reset it with the runtime ownership state. Extend the runtime-copy test to verify the recorded path matches the generated link and restore the field with the other isolated test state.
Track Composer metadata through the skeleton and working vendor paths instead of the disposable runtime directory. Record skeleton vendor directory presence separately because even an empty directory suppresses the working vendor fallback. Preserve ordinary file and symlink dependency handling. Cover unchanged cache reuse, working vendor symlink changes, empty skeleton vendor transitions and newly supplied skeleton metadata. Targeted cache and bootstrap tests, framework analysis, the PHPStan suite and Testbench suite pass. Complete the rules plan and final integration review. Warm analysis now reuses results across runtime copies; retain the owner discussion gate before PR creation.
Lead new diagnostic sections with recommended framework usage, shorten dense coverage explanations and remove repeated diagnostic transcripts. Preserve configuration switches, inference limits and legitimate exceptions. Clarify facade lifetimes, configuration repository ownership and native request API adaptations. Review the wording against the Processes documentation and verify links, anchors and code fences.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
|
@cubic-dev-ai review |
@binaryfire I have started the AI code review. It will take a few minutes to complete. |
PR Summary by QodoAdd PHPStan checks for long-lived worker hazards
AI Description
Diagram
High-Level Assessment
Files changed (196)
|
| $reflection = $this->getClass($receiver); | ||
|
|
||
| return $reflection !== null | ||
| && (! $reflection->is(ShouldQueue::class) || isset($this->queuedListeners[strtolower($reflection->getName())])); |
There was a problem hiding this comment.
🟡 Medium Coroutine/WorkerLifetimes.php:341
isContainerBuilt() returns true for a ShouldQueue class registered as a listener even when that class is also dispatched as a standalone job. Those direct jobs are restored from the payload with unserialize(), so promoted properties are misclassified as container-injected and trigger false runtime-config and restricted-call diagnostics. Track construction provenance per execution context, or keep the result false when the class also runs as a standalone job.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/phpstan/src/Rules/Coroutine/WorkerLifetimes.php around line 341:
`isContainerBuilt()` returns `true` for a `ShouldQueue` class registered as a listener even when that class is also dispatched as a standalone job. Those direct jobs are restored from the payload with `unserialize()`, so promoted properties are misclassified as container-injected and trigger false runtime-config and restricted-call diagnostics. Track construction provenance per execution context, or keep the result false when the class also runs as a standalone job.
Evidence trail:
Reviewed commit c329d6f.
src/phpstan/src/Rules/Coroutine/WorkerLifetimes.php:333-342, 633-711, 720-754
src/phpstan/src/Rules/Coroutine/WorkerStateAnalysis.php:209-233
src/phpstan/src/Rules/Coroutine/WorkerStateRule.php:134-153
src/phpstan/src/Rules/Coroutine/RestrictedCallRule.php:107-134
src/queue/src/Queue.php:282-285
src/queue/src/CallQueuedHandler.php:98-111
| */ | ||
| private function getStore(Expr $target, array $byReference): ?string | ||
| { | ||
| while ($target instanceof ArrayDimFetch || $target instanceof PropertyFetch) { |
There was a problem hiding this comment.
🟡 Medium Coroutine/BorrowedResourceEscapeRule.php:582
A borrowed connection stored on a captured object escapes the callback without being reported; for example, $holder->value = $connection leaves the released connection reachable through $holder. getStore() only treats $this properties and static properties as escapes, but capturing $holder by value still shares the same object. Recognize property writes through captured or otherwise external objects as escapes.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/phpstan/src/Rules/Coroutine/BorrowedResourceEscapeRule.php around line 582:
A borrowed connection stored on a captured object escapes the callback without being reported; for example, `$holder->value = $connection` leaves the released connection reachable through `$holder`. `getStore()` only treats `$this` properties and static properties as escapes, but capturing `$holder` by value still shares the same object. Recognize property writes through captured or otherwise external objects as escapes.
Evidence trail:
e5e6002996: src/phpstan/src/Rules/Coroutine/BorrowedResourceEscapeRule.php:320-335,347-350,580-594; src/redis/src/RedisProxy.php:495-526; src/docs/static-analysis.md:954-974
| return is_string($expr->name) ? $tracked[$expr->name] ?? null : null; | ||
| } | ||
|
|
||
| if (! ($expr instanceof MethodCall || $expr instanceof NullsafeMethodCall || $expr instanceof StaticCall) |
There was a problem hiding this comment.
🟡 Medium Coroutine/BorrowedResourceEscapeRule.php:547
Returning or storing a closure that captures the borrowed resource is not reported, so the closure can use a resource after its lending callback releases it. getBorrowedKind() rejects closure expressions, and the later nested-function analysis treats them as non-escaping; detect captured borrowed values in escaping closures, including closures reached through aliases.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/phpstan/src/Rules/Coroutine/BorrowedResourceEscapeRule.php around line 547:
Returning or storing a closure that captures the borrowed resource is not reported, so the closure can use a resource after its lending callback releases it. `getBorrowedKind()` rejects closure expressions, and the later nested-function analysis treats them as non-escaping; detect captured borrowed values in escaping closures, including closures reached through aliases.
Evidence trail:
src/phpstan/src/Rules/Coroutine/BorrowedResourceEscapeRule.php:344-377, 537-551, 707-735, commit e5e6002996
src/redis/src/RedisProxy.php:491-535, commit 89f062c536
src/docs/static-analysis.md:951-974
| { | ||
| $last = end($statements); | ||
|
|
||
| if (! $last instanceof Expression || ! $last->expr instanceof Throw_ || $this->leaves($statements, 0)) { |
There was a problem hiding this comment.
🟡 Medium Collectors/SwallowedCancellationCollector.php:476
rethrows() classifies the catch as propagating when an earlier branch throws a replacement exception, so cancellation is swallowed on that branch and the collector misses it. leaves() does not detect that throw path; check earlier escaping throws as well as the final throw $e.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/phpstan/src/Collectors/SwallowedCancellationCollector.php around line 476:
`rethrows()` classifies the catch as propagating when an earlier branch throws a replacement exception, so cancellation is swallowed on that branch and the collector misses it. `leaves()` does not detect that throw path; check earlier escaping throws as well as the final `throw $e`.
Evidence trail:
src/phpstan/src/Collectors/SwallowedCancellationCollector.php:394-415, 442-463, 472-499, 527-565 at reviewed commit 81e4571
|
|
||
| $polarity = $this->guardPolarity($first->cond, $variable); | ||
|
|
||
| return $polarity !== null |
There was a problem hiding this comment.
🟡 Medium Coroutine/CoroutineRuleHelper.php:368
A catch with if (! Coroutine::causedByCancellation($e)) { report($e); return; } throw $e; is reported as handling cancellation as an ordinary failure, even though cancellation skips the failure branch and is rethrown. keepsCancellationApart() only accepts a terminal throw inside a positive guard (or a one-statement catch); it should also recognize a negated guard whose branch returns followed by a catch-level rethrow.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/phpstan/src/Rules/Coroutine/CoroutineRuleHelper.php around line 368:
A catch with `if (! Coroutine::causedByCancellation($e)) { report($e); return; } throw $e;` is reported as handling cancellation as an ordinary failure, even though cancellation skips the failure branch and is rethrown. `keepsCancellationApart()` only accepts a terminal throw inside a positive guard (or a one-statement catch); it should also recognize a negated guard whose branch returns followed by a catch-level rethrow.
Evidence trail:
Reviewed commit c329d6f. src/phpstan/src/Rules/Coroutine/CoroutineRuleHelper.php:273-290, 342-371. src/phpstan/src/Collectors/SwallowedCancellationCollector.php:435-463. src/phpstan/src/Rules/Coroutine/ExactCancellationCatchRule.php:56-64.
| default => [], | ||
| }; | ||
|
|
||
| foreach ($targets as $target) { |
There was a problem hiding this comment.
🟡 Medium Rules/ManagedTransactionRule.php:258
Reassigning $connection through array destructuring leaves it marked as the managed connection, so the later commit() is incorrectly reported as ending the transaction. List_ targets are not Variable nodes, so recursively collect variables from destructuring targets before retaining owner identities.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/phpstan/src/Rules/ManagedTransactionRule.php around line 258:
Reassigning `$connection` through array destructuring leaves it marked as the managed connection, so the later `commit()` is incorrectly reported as ending the transaction. `List_` targets are not `Variable` nodes, so recursively collect variables from destructuring targets before retaining owner identities.
Evidence trail:
Reviewed commit e5e6002: src/phpstan/src/Rules/ManagedTransactionRule.php:123-160, 252-262, 315-320; src/phpstan/src/Concerns/InspectsConnections.php:33-53, 90-98; PHP-Parser node definition: https://github.com/nikic/PHP-Parser/blob/v5.0.0/lib/PhpParser/Node/Expr/List_.php
|
|
||
| $declaration = match (true) { | ||
| $condition instanceof FuncCall && $condition->isFirstClassCallable() => $this->findFunction($condition, $scope), | ||
| $condition instanceof StaticCall && $condition->isFirstClassCallable() => $this->findStaticMethod($condition, $scope), |
There was a problem hiding this comment.
🟡 Medium Rules/RequestDependentBindingRule.php:184
#[BindWhen(..., RequestContext::has(...))] produces no diagnostic, so a request-dependent implementation can be bound permanently for the worker. This branch scans only the callable's declaration, where RequestContext::has() delegates to CoroutineContext::has(), which readsRequest() does not recognize. Check whether the first-class callable target itself is a recognized request or user read before scanning its body.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @src/phpstan/src/Rules/RequestDependentBindingRule.php around line 184:
`#[BindWhen(..., RequestContext::has(...))]` produces no diagnostic, so a request-dependent implementation can be bound permanently for the worker. This branch scans only the callable's declaration, where `RequestContext::has()` delegates to `CoroutineContext::has()`, which `readsRequest()` does not recognize. Check whether the first-class callable target itself is a recognized request or user read before scanning its body.
Evidence trail:
Reviewed commit c329d6f. Inspect `src/phpstan/src/Rules/RequestDependentBindingRule.php:182-207` (first-class callable declaration scanning), `:303-320` and `:377-396` (recognized static reads), `src/context/src/RequestContext.php:39-44` (`has()` delegates to `CoroutineContext::has()`), `src/context/src/CoroutineContext.php:75-85`, and `src/container/src/Attributes/BindWhen.php:11-15` (matching bindings are worker-lifetime).
Code Review by Qodo
1. Wildcard listeners evade call checks
|
There was a problem hiding this comment.
29 issues found across 196 files
Confidence score: 3/5
NoPipeSeparatedValidationRulesRule.phpcan throw aTypeErrorwhen it analyzes a first-class callable such asvalidator(...). Restore the early return for first-class callables.SafeSocket.phpsuppresses the raw-coroutine warning even thoughcritical()andclose()can still throw outside the protective catch. Protect those calls or let the raw-coroutine check report the issue.NoMutableDatesRule.phpmisses mutable-date constructions and static calls through imported or aliased class names. Resolve the class throughScopebefore matching it.Connection.php's ignore does not cover exceptions from calls outside the innertry. Narrow the suppression or handle those exceptions explicitly.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/PHPStan/Rules/NoNativeRequestApisRuleTest.php">
<violation number="1" location="tests/PHPStan/Rules/NoNativeRequestApisRuleTest.php:38">
P3: The fixture and assertions cover only 7 of the 16 native APIs and superglobals the rule maps. Add fixture lines for the remaining entries (e.g. `setcookie`, `headers_list`, `connection_status`, `$_COOKIE`, `$_REQUEST`) so each tip mapping is asserted.</violation>
</file>
<file name="tests/PHPStan/WorkerStateDependencyTest.php">
<violation number="1" location="tests/PHPStan/WorkerStateDependencyTest.php:75">
P2: This facade test runs with `reuseCollectedDataRuleErrors` at its default `false`, so the warm-cache path its docblock describes is never exercised. Pass `reuseCollectedDataRuleErrors: true` to both `analyse()` calls, as the other cache-recheck tests do, or parametrize it like the first two tests.</violation>
</file>
<file name="src/testbench/src/Bootstrapper.php">
<violation number="1" location="src/testbench/src/Bootstrapper.php:136">
P3: The `runtimeVendorPath()` docblock says this path is linked "when its skeleton has none", but the value is stored on every copy, including when the skeleton supplies its own `vendor` link. Reword it to say it is the working package's vendor directory, which is tracked for Composer metadata whether or not the copy links it.</violation>
</file>
<file name="src/grpc/src/Client/Connection.php">
<violation number="1" location="src/grpc/src/Client/Connection.php:460">
P2: This ignore says `receive()` contains all receiver failures, but it does not. Only `CanceledException` and the errors thrown inside the inner `try` are handled. Exceptions from `closeRetiredConnectionIfIdle()`, `auditStreams()`, or from `failConnection()` inside the `catch (Throwable)` handler escape the raw coroutine and stop the worker. Either wrap the callback body in a `catch (Throwable)` that handles the failure, or narrow the justification so it does not claim full containment.</violation>
</file>
<file name="src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php">
<violation number="1" location="src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php:155">
P2: This suppression's reason does not meet the documented bar for raw coroutines, which requires the callback to catch every exception itself. The watchdog only catches `TimeoutException`, so any other Throwable escaping it would stop the worker. Either make the callback catch every Throwable, or state the actual reason the escape cannot happen.</violation>
</file>
<file name="src/phpstan/src/Rules/NoMutableDatesRule.php">
<violation number="1" location="src/phpstan/src/Rules/NoMutableDatesRule.php:57">
P2: This lookup uses the source spelling instead of the resolved class name, so imported or aliased `Carbon` constructions and static calls are silently missed. Resolve `$node->class` through `Scope` before matching `MUTABLE_CLASSES` and formatting the subject.</violation>
</file>
<file name="src/engine/src/SafeSocket.php">
<violation number="1" location="src/engine/src/SafeSocket.php:159">
P2: This suppression hides a raw coroutine whose error handler can still throw: `critical()` and `close()` run outside a protective catch. Protect those cleanup/reporting calls (or let `hypervel.rawCoroutine` report the boundary) before suppressing this diagnostic.</violation>
</file>
<file name="src/phpstan/src/Rules/Coroutine/NoCoroutinesInTransactionsRule.php">
<violation number="1" location="src/phpstan/src/Rules/Coroutine/NoCoroutinesInTransactionsRule.php:134">
P2: This removal makes the rule report first-class callable expressions as if they started a coroutine, although `Coroutine::create(...)` only creates a callable and does not execute it. Restore the first-class-callable guard to avoid a false positive (and an extra diagnostic for the existing line-31 fixture).</violation>
</file>
<file name="src/phpstan/src/Rules/Validation/NoPipeSeparatedValidationRulesRule.php">
<violation number="1" location="src/phpstan/src/Rules/Validation/NoPipeSeparatedValidationRulesRule.php:130">
P1: Removing this early return makes first-class callable calls reach `getArgumentValues()`, where `array_any` type-hints `Arg` and throws a TypeError on the `VariadicPlaceholder`. Restore the guard so `validator(...)` and `$request->validate(...)` return early.</violation>
</file>
<file name="tests/PHPStan/ResultCacheTestCase.php">
<violation number="1" location="tests/PHPStan/ResultCacheTestCase.php:65">
P3: The comment says the fixtures read config through `get()`, but they also use the `config()` helper, which this rule checks as well. Mention both so the reason for disabling the check is accurate.</violation>
</file>
<file name="tests/PHPStan/RuntimeInputsPackageDependencyTest.php">
<violation number="1" location="tests/PHPStan/RuntimeInputsPackageDependencyTest.php:73">
P3: The test name says 'VendorSourceChanges', but it exercises only Composer metadata (`installed.json`) and vendor-link repointing. Rename it to something like `testVendorMetadataChangesReanalyzeTheirConsumers` so the name matches what is covered.</violation>
</file>
<file name="src/phpstan/src/Collectors/WorkerStateCallCollector.php">
<violation number="1" location="src/phpstan/src/Collectors/WorkerStateCallCollector.php:83">
P2: Removing `$node->isFirstClassCallable()` here lets `$this->helper(...)` and `$config->set(...)` through. Both create a Closure and run nothing, but they are now recorded as a helper call or a config write, which can produce false positives. Restore the guard. Resolution and route-action lookups already ignore first-class callables, so restoring it does not affect them.</violation>
</file>
<file name="tests/PHPStan/Rules/UnsupportedCallRuleTest.php">
<violation number="1" location="tests/PHPStan/Rules/UnsupportedCallRuleTest.php:73">
P3: The comment says the extender can return another object, but this closure returns its argument unchanged. Return a different `Service` so the test covers the case the comment describes.</violation>
</file>
<file name="src/phpstan/src/Rules/UnsupportedCallRule.php">
<violation number="1" location="src/phpstan/src/Rules/UnsupportedCallRule.php:70">
P2: This reports first-class callables like `$service->bare(...)`, which only create a Closure and never call the method. Skip them in the early return, using `$node->isFirstClassCallable()`, as the sibling rules do.</violation>
</file>
<file name="tests/Support/SupportFacadeNonCoroutineTest.php">
<violation number="1" location="tests/Support/SupportFacadeNonCoroutineTest.php:23">
P2: This test replaces the global `Facade` application with a bare container and never restores it. Add a `tearDown()` that calls `Facade::clearResolvedInstances()` and `NonCoroutineFacadeStub::setFacadeApplication(null)` before `parent::tearDown()`, so later teardown and tests do not resolve facades against this container.</violation>
</file>
<file name="tests/PHPStan/Rules/UncastEnvValueRuleTest.php">
<violation number="1" location="tests/PHPStan/Rules/UncastEnvValueRuleTest.php:28">
P3: `testConfigFile()` never asserts the alias and case-insensitive reader paths. `Environment::GET('foo')` lives in `env-readers.php`, which is not a config file, so it never reaches the rule's reporting. Add an uncast alias read such as `Environment::GET('ALIAS_PORT', 8080)` to `data/config/uncast-env.php` and expect its error here.</violation>
</file>
<file name="tests/PHPStan/Rules/TypedConfigGetterRuleTest.php">
<violation number="1" location="tests/PHPStan/Rules/TypedConfigGetterRuleTest.php:50">
P2: `testPackageApplication` does not set the package flag itself, so it depends on the ambient bootstrap state. Set it to `true` explicitly and restore the previous value, as `testApplication` does.</violation>
</file>
<file name="src/phpstan/src/Rules/NoNativeRequestApisRule.php">
<violation number="1" location="src/phpstan/src/Rules/NoNativeRequestApisRule.php:40">
P2: This tip says both `response()->stream()` generators and `IterableStreamedResponse` stop on disconnect, but cancellation is opt-in and off by default. Reword it to say to call `cancelOnDisconnect()`, and update the matching expectation in `NoNativeRequestApisRuleTest`.</violation>
</file>
<file name="src/phpstan/src/Collectors/WorkerStateWriteCollector.php">
<violation number="1" location="src/phpstan/src/Collectors/WorkerStateWriteCollector.php:108">
P2: This early return drops offset writes that `WorkerStateConfigOffsetCollector` never sees. That collector only handles top-level `Expression` assignments and `Unset_` statements. A write nested in a `return`, an argument, or a nested dim is now silent in both rules. Either widen the config-offset collector to expression-level writes, or narrow this check to the cases that collector covers.</violation>
</file>
<file name="src/phpstan/src/Rules/MigrationColumnTypeRule.php">
<violation number="1" location="src/phpstan/src/Rules/MigrationColumnTypeRule.php:41">
P2: The `json` branch fires on every database driver, but the message and tip only hold on PostgreSQL. On MySQL, `jsonb()` produces the same `json` column, and on SQLite both produce `text` by default, so the advice changes nothing there. Scope this check to PostgreSQL in the message or in the rule's configuration.</violation>
</file>
<file name="src/phpstan/src/Rules/Coroutine/NoRawCoroutinesRule.php">
<violation number="1" location="src/phpstan/src/Rules/Coroutine/NoRawCoroutinesRule.php:91">
P3: Nullsafe calls such as `$coroutine?->execute()` are not checked. The comment assumes PHPStan treats them as `MethodCall`, but the sibling rules in this repo handle `NullsafeMethodCall` as a separate class. Either add `NullsafeMethodCall` to the check and to `FacadeHelper::getCalledMethods()`, or add a fixture that proves the current behavior.</violation>
<violation number="2" location="src/phpstan/src/Rules/Coroutine/NoRawCoroutinesRule.php:96">
P2: This branch also reports first-class callable syntax such as `EngineCoroutine::create(...)`. That form only creates a Closure and starts no coroutine, so skip it when `isFirstClassCallable()` is true.</violation>
</file>
<file name="src/phpstan/src/RuntimeInputsResultCacheValueExtension.php">
<violation number="1" location="src/phpstan/src/RuntimeInputsResultCacheValueExtension.php:92">
P3: This inserts an optional parameter before the existing `$directories` argument, breaking callers that use `createKey()` positionally. Append `$presenceDirectories` after the existing parameters, or require named arguments without changing the established positional order.</violation>
</file>
<file name="src/docs/porting-from-laravel.md">
<violation number="1" location="src/docs/porting-from-laravel.md:550">
P3: Renaming this published fragment breaks existing links to the porting guide. Retain `streamed-responses` as a legacy alias before the new anchor.</violation>
</file>
<file name="src/phpstan/src/Collectors/RestrictedCallCollector.php">
<violation number="1" location="src/phpstan/src/Collectors/RestrictedCallCollector.php:82">
P2: This collector treats a first-class callable as an executed restricted call. Exclude `isFirstClassCallable()` nodes; creating `$registry->fake(...)` does not invoke `fake()`.</violation>
</file>
<file name="src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php">
<violation number="1" location="src/filesystem/src/Concerns/InteractsWithPooledFilesystem.php:542">
P2: These method-level input templates let each caller choose `TDriver`, `TAdapter`, or `TClient`, so PHPStan accepts callbacks requiring arbitrary concrete subtypes that runtime does not guarantee. For example, the added type test accepts `AwsS3V3Adapter` for any `FilesystemAdapter`, although `withAdapter()` invokes the callback with `$this->adapter`, and `ClientPooledFilesystem` is used for both S3 and GCS stacks. Keep only the return template and type each callback parameter as its declared runtime type, or bind a concrete type at the class level.</violation>
</file>
<file name="src/phpstan/src/Rules/Coroutine/BorrowedResourceEscapeRule.php">
<violation number="1" location="src/phpstan/src/Rules/Coroutine/BorrowedResourceEscapeRule.php:547">
P2: Detect borrowed values captured by escaping closures, including closures returned or stored through aliases; otherwise the closure can use the resource after its lending callback releases it.</violation>
<violation number="2" location="src/phpstan/src/Rules/Coroutine/BorrowedResourceEscapeRule.php:583">
P2: Treat writes through captured or otherwise external object properties as escapes. `$holder->value = $connection` leaves the borrowed connection reachable after the callback.</violation>
</file>
<file name="src/phpstan/src/Collectors/SwallowedCancellationCollector.php">
<violation number="1" location="src/phpstan/src/Collectors/SwallowedCancellationCollector.php:476">
P2: Check every escaping throw path before treating the catch as propagating. An earlier branch that throws a replacement exception swallows cancellation even when the final statement rethrows `$e`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| */ | ||
| public function processNode(Node $node, Scope $scope): array | ||
| { | ||
| if ($node->isFirstClassCallable()) { |
There was a problem hiding this comment.
P1: Removing this early return makes first-class callable calls reach getArgumentValues(), where array_any type-hints Arg and throws a TypeError on the VariadicPlaceholder. Restore the guard so validator(...) and $request->validate(...) return early.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/phpstan/src/Rules/Validation/NoPipeSeparatedValidationRulesRule.php, line 130:
<comment>Removing this early return makes first-class callable calls reach `getArgumentValues()`, where `array_any` type-hints `Arg` and throws a TypeError on the `VariadicPlaceholder`. Restore the guard so `validator(...)` and `$request->validate(...)` return early.</comment>
<file context>
@@ -127,10 +127,6 @@ public function getNodeType(): string
- return [];
- }
-
$entry = match (true) {
$node instanceof FuncCall => $this->matchFunction($node, $scope),
$node instanceof New_ => $this->matchConstructor($node, $scope),
</file context>
|
|
||
| $this->edit($applicationPath . '/app/ReportFacade.php', 'return SupportedReporter::class;', 'return UnsupportedReporter::class;'); | ||
|
|
||
| $this->assertSame(['Reporting.php:11 hypervel.unsupportedCall'], $this->diagnostics($this->analyse($applicationPath), 1)); |
There was a problem hiding this comment.
P2: This facade test runs with reuseCollectedDataRuleErrors at its default false, so the warm-cache path its docblock describes is never exercised. Pass reuseCollectedDataRuleErrors: true to both analyse() calls, as the other cache-recheck tests do, or parametrize it like the first two tests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/PHPStan/WorkerStateDependencyTest.php, line 75:
<comment>This facade test runs with `reuseCollectedDataRuleErrors` at its default `false`, so the warm-cache path its docblock describes is never exercised. Pass `reuseCollectedDataRuleErrors: true` to both `analyse()` calls, as the other cache-recheck tests do, or parametrize it like the first two tests.</comment>
<file context>
@@ -39,4 +39,73 @@ public function testChangedLifetimesRecheckUnchangedWrites(bool $reuseCollectedD
+
+ $this->edit($applicationPath . '/app/ReportFacade.php', 'return SupportedReporter::class;', 'return UnsupportedReporter::class;');
+
+ $this->assertSame(['Reporting.php:11 hypervel.unsupportedCall'], $this->diagnostics($this->analyse($applicationPath), 1));
+ }
+
</file context>
| $this->assertSame(['Reporting.php:11 hypervel.unsupportedCall'], $this->diagnostics($this->analyse($applicationPath), 1)); | |
| $this->assertSame(['Reporting.php:11 hypervel.unsupportedCall'], $this->diagnostics($this->analyse($applicationPath, reuseCollectedDataRuleErrors: true), 1)); |
| $this->receiving = true; | ||
|
|
||
| try { | ||
| // @phpstan-ignore hypervel.rawCoroutine (receive() contains cancellation and receiver failures as the terminal owner) |
There was a problem hiding this comment.
P2: This ignore says receive() contains all receiver failures, but it does not. Only CanceledException and the errors thrown inside the inner try are handled. Exceptions from closeRetiredConnectionIfIdle(), auditStreams(), or from failConnection() inside the catch (Throwable) handler escape the raw coroutine and stop the worker. Either wrap the callback body in a catch (Throwable) that handles the failure, or narrow the justification so it does not claim full containment.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/grpc/src/Client/Connection.php, line 460:
<comment>This ignore says `receive()` contains all receiver failures, but it does not. Only `CanceledException` and the errors thrown inside the inner `try` are handled. Exceptions from `closeRetiredConnectionIfIdle()`, `auditStreams()`, or from `failConnection()` inside the `catch (Throwable)` handler escape the raw coroutine and stop the worker. Either wrap the callback body in a `catch (Throwable)` that handles the failure, or narrow the justification so it does not claim full containment.</comment>
<file context>
@@ -457,6 +457,7 @@ private function startReceiver(): void
$this->receiving = true;
try {
+ // @phpstan-ignore hypervel.rawCoroutine (receive() contains cancellation and receiver failures as the terminal owner)
Coroutine::create(function (): void {
$this->receiverCoroutineId = Coroutine::id();
</file context>
| $completed->push(true); | ||
| }); | ||
|
|
||
| // @phpstan-ignore hypervel.rawCoroutine (the test deadline watchdog runs no application code, catches the timeout it throws, and nothing cancels it) |
There was a problem hiding this comment.
P2: This suppression's reason does not meet the documented bar for raw coroutines, which requires the callback to catch every exception itself. The watchdog only catches TimeoutException, so any other Throwable escaping it would stop the worker. Either make the callback catch every Throwable, or state the actual reason the escape cannot happen.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php, line 155:
<comment>This suppression's reason does not meet the documented bar for raw coroutines, which requires the callback to catch every exception itself. The watchdog only catches `TimeoutException`, so any other Throwable escaping it would stop the worker. Either make the callback catch every Throwable, or state the actual reason the escape cannot happen.</comment>
<file context>
@@ -152,6 +152,7 @@ protected function enforceCoroutineTestTimeLimit(?TimeoutException &$timeoutExce
$completed->push(true);
});
+ // @phpstan-ignore hypervel.rawCoroutine (the test deadline watchdog runs no application code, catches the timeout it throws, and nothing cancels it)
Coroutine::create(static function () use ($root, $deadline, $completed, &$timeoutException, &$rootCompleted): void {
$current = Coroutine::getCid();
</file context>
| if (! ($node instanceof New_ || $node instanceof StaticCall) | ||
| || ! $node->class instanceof Name | ||
| || $node->class->isSpecialClassName() | ||
| || ! isset(self::MUTABLE_CLASSES[strtolower($node->class->toString())]) |
There was a problem hiding this comment.
P2: This lookup uses the source spelling instead of the resolved class name, so imported or aliased Carbon constructions and static calls are silently missed. Resolve $node->class through Scope before matching MUTABLE_CLASSES and formatting the subject.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/phpstan/src/Rules/NoMutableDatesRule.php, line 57:
<comment>This lookup uses the source spelling instead of the resolved class name, so imported or aliased `Carbon` constructions and static calls are silently missed. Resolve `$node->class` through `Scope` before matching `MUTABLE_CLASSES` and formatting the subject.</comment>
<file context>
@@ -0,0 +1,87 @@
+ if (! ($node instanceof New_ || $node instanceof StaticCall)
+ || ! $node->class instanceof Name
+ || $node->class->isSpecialClassName()
+ || ! isset(self::MUTABLE_CLASSES[strtolower($node->class->toString())])
+ ) {
+ return [];
</file context>
|
|
||
| // An extender can return another object. | ||
| $container->singleton('unsupported.extended', static fn (): Service => new Service); | ||
| $container->extend('unsupported.extended', static fn (Service $service): Service => $service); |
There was a problem hiding this comment.
P3: The comment says the extender can return another object, but this closure returns its argument unchanged. Return a different Service so the test covers the case the comment describes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/PHPStan/Rules/UnsupportedCallRuleTest.php, line 73:
<comment>The comment says the extender can return another object, but this closure returns its argument unchanged. Return a different `Service` so the test covers the case the comment describes.</comment>
<file context>
@@ -0,0 +1,81 @@
+
+ // An extender can return another object.
+ $container->singleton('unsupported.extended', static fn (): Service => new Service);
+ $container->extend('unsupported.extended', static fn (Service $service): Service => $service);
+
+ // Two factories on one line, which reflection's line numbers cannot tell apart.
</file context>
| $container->extend('unsupported.extended', static fn (Service $service): Service => $service); | |
| $container->extend('unsupported.extended', static fn (Service $service): Service => new Service); |
| ]; | ||
|
|
||
| // The readers fixture declares an Env subclass with its own get(). | ||
| $this->analyse([__DIR__ . '/data/config/uncast-env.php', __DIR__ . '/data/env-readers.php'], [ |
There was a problem hiding this comment.
P3: testConfigFile() never asserts the alias and case-insensitive reader paths. Environment::GET('foo') lives in env-readers.php, which is not a config file, so it never reaches the rule's reporting. Add an uncast alias read such as Environment::GET('ALIAS_PORT', 8080) to data/config/uncast-env.php and expect its error here.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/PHPStan/Rules/UncastEnvValueRuleTest.php, line 28:
<comment>`testConfigFile()` never asserts the alias and case-insensitive reader paths. `Environment::GET('foo')` lives in `env-readers.php`, which is not a config file, so it never reaches the rule's reporting. Add an uncast alias read such as `Environment::GET('ALIAS_PORT', 8080)` to `data/config/uncast-env.php` and expect its error here.</comment>
<file context>
@@ -0,0 +1,51 @@
+ ];
+
+ // The readers fixture declares an Env subclass with its own get().
+ $this->analyse([__DIR__ . '/data/config/uncast-env.php', __DIR__ . '/data/env-readers.php'], [
+ $error("env('DB_PORT')", 'an int', 14),
+ $error("Hypervel\\Support\\Env::get('APP_DEBUG')", 'a bool', 15),
</file context>
| if ($function !== null && strtolower($function) === self::FUNCTION) { | ||
| $spawn = $function; | ||
| } | ||
| } elseif (($node instanceof MethodCall || $node instanceof StaticCall) |
There was a problem hiding this comment.
P3: Nullsafe calls such as $coroutine?->execute() are not checked. The comment assumes PHPStan treats them as MethodCall, but the sibling rules in this repo handle NullsafeMethodCall as a separate class. Either add NullsafeMethodCall to the check and to FacadeHelper::getCalledMethods(), or add a fixture that proves the current behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/phpstan/src/Rules/Coroutine/NoRawCoroutinesRule.php, line 91:
<comment>Nullsafe calls such as `$coroutine?->execute()` are not checked. The comment assumes PHPStan treats them as `MethodCall`, but the sibling rules in this repo handle `NullsafeMethodCall` as a separate class. Either add `NullsafeMethodCall` to the check and to `FacadeHelper::getCalledMethods()`, or add a fixture that proves the current behavior.</comment>
<file context>
@@ -0,0 +1,118 @@
+ if ($function !== null && strtolower($function) === self::FUNCTION) {
+ $spawn = $function;
+ }
+ } elseif (($node instanceof MethodCall || $node instanceof StaticCall)
+ && $node->name instanceof Identifier
+ && isset(self::METHODS[strtolower($node->name->toString())])
</file context>
| public function createKey( | ||
| array $files = [], | ||
| array $presenceFiles = [], | ||
| array $presenceDirectories = [], |
There was a problem hiding this comment.
P3: This inserts an optional parameter before the existing $directories argument, breaking callers that use createKey() positionally. Append $presenceDirectories after the existing parameters, or require named arguments without changing the established positional order.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/phpstan/src/RuntimeInputsResultCacheValueExtension.php, line 92:
<comment>This inserts an optional parameter before the existing `$directories` argument, breaking callers that use `createKey()` positionally. Append `$presenceDirectories` after the existing parameters, or require named arguments without changing the established positional order.</comment>
<file context>
@@ -86,6 +89,7 @@ public function __construct(
public function createKey(
array $files = [],
array $presenceFiles = [],
+ array $presenceDirectories = [],
array $directories = [],
array $linkedDirectories = [],
</file context>
|
|
||
| <a name="streamed-responses"></a> | ||
| ### Streamed Responses | ||
| <a name="native-request-functions"></a> |
There was a problem hiding this comment.
P3: Renaming this published fragment breaks existing links to the porting guide. Retain streamed-responses as a legacy alias before the new anchor.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At src/docs/porting-from-laravel.md, line 550:
<comment>Renaming this published fragment breaks existing links to the porting guide. Retain `streamed-responses` as a legacy alias before the new anchor.</comment>
<file context>
@@ -549,8 +547,10 @@ Hypervel installs `hypervel/guzzle`, a Guzzle 8 fork that replaces `guzzlehttp/g
-<a name="streamed-responses"></a>
-### Streamed Responses
+<a name="native-request-functions"></a>
+### Native Request Functions
+
</file context>
| <a name="native-request-functions"></a> | |
| <a name="streamed-responses"></a> | |
| <a name="native-request-functions"></a> |
[High impact] The changes reach application-wide static analysis and shared facade behavior across request coroutines. The PR appears safe to merge, with non-blocking fixes recommended for four gaps in the new checks.
|
| if ($root instanceof Variable && is_string($root->name) && ($node->expr instanceof Closure || $node->expr instanceof ArrowFunction)) { | ||
| $closures[$root->name][] = $node->expr; |
There was a problem hiding this comment.
collectWrites() misses $tasks = [fn () => $connection->get('key')]; parallel($tasks); inside withConnection(). It records only assignments whose right side is a closure, so getPassedClosures() finds no tasks for this variable. Developers get no warning even though cancellation can leave a child using the connection after the borrow ends.
Collect closures inside assigned array literals too, and test this alongside the existing $tasks[] case.
| if ($context !== null && $receiver->hasNativeMethod($method)) { | ||
| $this->addContext($receiver->getName(), $receiver->getNativeMethod($method)->getDeclaringClass()->getName(), $method, $context, $shared); |
There was a problem hiding this comment.
addHandlerContext() keeps the container's $shared flag for queued listener methods. But CallQueuedListener clones listeners using InteractsWithQueue before calling them. A registered method such as record() that writes a scalar property now gets hypervel.workerSharedState, even though each job changes its own clone. Valid listeners therefore need unnecessary ignores.
Keep the job context, but distinguish the cloned receiver from the shared objects its properties may reference.
| if ($accessor === null | ||
| || ($resolver !== Facade::class && $resolver !== Date::class) | ||
| || $facade->getNativeMethod('getFacadeRoot')->getDeclaringClass()->getName() !== Facade::class) { |
There was a problem hiding this comment.
Custom facade calls get misread
resolveRootClass() checks overrides of resolveFacadeInstance() and getFacadeRoot(), but not __callStatic(). A facade subclass can override __callStatic() to handle a call itself or send it elsewhere. The new rules still inspect the registered root and can report Unsupported or lifetime restrictions on a method that never runs.
Treat unknown dispatch overrides as unknown, while preserving the known forwarding behavior of Hash and Crypt.
| if (! $last instanceof Expression || ! $last->expr instanceof Throw_ || $this->leaves($statements, 0)) { | ||
| return false; |
There was a problem hiding this comment.
Earlier throws hide cancellation
rethrows() accepts a catch ending in throw $exception, but leaves() does not reject earlier throws that discard it. For example, if ($retryLimitReached) { throw new RuntimeException('Failed'); } throw $exception; passes the check around a query. The first branch turns cancellation into an ordinary error, so the new check misses it.
Check earlier throw paths too, and require them to preserve the caught exception.
This adds a set of PHPStan rules for mistakes that are easy to make in a long-lived Swoole worker and hard to see in review: calling a boot-only method while handling a request, letting a pooled connection escape the callback that borrowed it, swallowing a coroutine cancellation, creating a Swoole table in every process, and similar. It builds on the method attributes from #741, which mark the methods these rules check.
Laravel code mostly runs unchanged on Hypervel, but a worker serves many requests and coroutines at once, so some patterns that are harmless under PHP-FPM leak state or misbehave here. The framework already documents these. These rules turn that guidance into diagnostics, each with a fix and, where there is one, the legitimate exception that should be ignored with a reason.
A few runtime changes came out of the work, so they're described first. They're in their own commits.
Runtime changes
bind()orTransientservice therefore kept the first request's object for every later request. The container gainsresolvedShared(), which reports whether it holds a worker-shared instance, including auto-singletons. Facades cache those for the worker as before. Other roots are kept per coroutine, so each request gets its own and concurrent requests never share one. Clearing a facade's resolved instance now makes later facade calls resolve again in every coroutine.dateis bound by the framework. TheDatefacade used to callswap()on first use whendatewasn't bound, which registered a container instance in the middle of a request.FoundationServiceProvidernow bindsdatetoDateFactorywithsingletonIf(). ADatefacade subclass'sDEFAULT_FACADEtherefore only applies whiledateis unbound; an application should binddateto its factory instead. This is listed in the Support README's differences.Connection.ConnectionResolverInterface::connection()and the resolvers implementing it now declare the driver-neutralConnection, notConnectionInterface. Every built-in andDB::extend()connection already is one, and Eloquent and the migrator already required it, so a resolver returning anything else was already broken. The change removes several ignores andinstanceofchecks. Laravel's contract documentsConnectionInterface, so this is listed in the Database README and the porting guide.withDriver(),withAdapter()andwithClient(), the RediswithPinnedConnection()methods and the SQS pool'swithConnection()are now generic over their callback's return type. The filesystem methods also accept a callback that narrows its parameter to the disk's concrete driver, adapter or client.logging.syslog.facilityreadLOG_SYSLOG_FACILITYwith the integerLOG_USERas its default. An environment variable is always a string, and Monolog only accepts facility names as strings, so the variable has to hold a name such aslocal0. The default is now'user', which Monolog maps toLOG_USER, in the framework config and both skeletons.LoadEnvironmentVariablesno longer callshttp_response_code(). It was left over from Laravel's FPM bootstrap and does nothing in Hypervel, which bootstraps from the CLI before any response exists.The rules
Each rule has its own section in the static analysis docs, with an example, the fix and its exceptions. Every rule except unsupported calls can be turned off with a parameter.
BootOnly,TestsOnlyorBootOrTestsOnlyfrom request, job or operation code, applying each marker's conditions. Also markers whose arguments would misstate the restriction.checkRestrictedCallsUnsupported, including its message.checkBorrowedResourcescommit()orrollBack()that ends a transactiontransaction()manages before its closure returns, androllBack(0).checkManagedTransactionsheader(),setcookie(),session_*(),$_GET,$_SESSIONand the other PHP functions and superglobals that act on the process instead of the request.noNativeRequestApischeckDiscardedDatesCarbon.noMutableDatesenv()read with a boolean, integer or float default that isn't cast.checkConfigEnvCastsconfig('key')orget()for a key that always holds one type, which should usestring(),integer()and so on. Only runs when analyzing an application.checkTypedConfigGettersobserve()in a service provider for a model the application owns, which should useObservedBy.checkObserverAttributesjson(),enum()andset()columns. Off by default, since existing migrations shouldn't be edited.checkMigrationColumnTypescheckBindingCyclesnoRawCoroutinescheckSocialiteProvidersnew Swoole\Tablein a service provider'sregister()orboot(), which every process runs.checkSwooleTablesBindWhencondition that reads the current request or user, which would bind that request's choice for the worker's lifetime.checkBindWhenConditionscheckCancellationHandlingThe framework's own reports are fixed or, where the code is intentional, ignored with a reason: for example the coroutine primitives themselves, Horizon's
ignore_user_abort(), and Sentry'sspotlightsetting, which accepts a boolean or a URL.Shared analysis
Several rules need to know where code runs and whether a value is the worker's shared instance.
OnReceiveandOnClose, and Eloquent observers registered withobserve()orObservedBy, are analyzed as code that runs during an operation. The existing worker-state checks use these contexts too.whenSharedrestriction is only reported when the call is known to reach the shared instance: through a facade whose service the container shares, a named application-container resolution such asapp('router'), or a dependency the container injects into a controller action, a job handler or a readonly promoted constructor property. A factory you create withnewis not reported.Configuration types only for the application repository
Configuration value types now apply only to reads from the application's repository: the
config()helper, theConfigfacade, and calls such asapp('config')->get(...). A repository received as a parameter or property, or built withnew Repository($items), may hold other values, so it keeps its declared return types. Larastan applies the application's configuration types toget()on any repository. This is listed in the PHPStan README's differences.Result cache for package analysis
Package analysis boots a Testbench copy of the skeleton that is deleted when the process exits. The result cache recorded the copy's Composer metadata by its path inside that copy, so every warm run saw it as missing and reanalyzed everything that depends on the booted application, about a third of this repository. The cache now records the metadata where Testbench takes it from: the skeleton's
vendordirectory and the working package's, by their own paths, including whether the skeleton'svendordirectory exists. Testbench'sBootstrapper::runtimeVendorPath()returns the working vendor directory for this. A warmcomposer analyseof this repository now reanalyzes nothing and takes a few seconds instead of about forty.Docs
AGENTS.mdsays PHPStan checks the lifetime attributes, rewords binding pattern 3 to say what explicit registration changes, and corrects the description of binding an alias.Validation
Formatting, PHPStan, the analyzer test suite, the Testbench suite and the Support tests pass. Each rule has tests for what it reports, the allowed alternatives and its documented exceptions, and the result cache changes have warm-cache tests. The skeleton was installed the way CI installs it and analyzed with these rules, with no reports.
Compared with
0.4, a coldcomposer analyseof this repository is a few percent slower. Turning the new rules off didn't measurably change that, and warm runs are much faster because of the result cache fix.Note
Add PHPStan rules for Hypervel's long-lived workers and per-coroutine facade roots
RestrictedCallRule), swallowed cancellation (SwallowedCancellationRule), borrowed-resource escapes, raw coroutines, mutable/discarded dates, managed transactions, binding cycles, request-dependent bindings, and moreWorkerStateHelper/WorkerStateAnalysismodel with a rewrittenWorkerLifetimesthat tracks event, listener, observer, and injection contextsFacade::resolveFacadeInstanceto keep cached roots per coroutine for non-shared, non-scoped services, and addsContainer::resolvedShared()(contract included) so the facade knows when a root is worker-sharedDatabaseQueue::getDatabase()now returnConnectioninstead ofConnectionInterface(docs updated); also bindsdatetoDateFactoryinFoundationServiceProviderConnectionResolverInterface::connection()return types (custom resolvers and test mocks must returnConnection), facade root caching semantics in Facade.php, andDate::resolveFacadeInstancewhich now caches the default instance directly withoutswap()📊 Macroscope summarized c329d6f. 99 files reviewed, 10 issues evaluated, 3 issues filtered, 7 comments posted
🗂️ Filtered Issues
src/phpstan/src/Collectors/WorkerStateClassCollector.php — 0 comments posted, 1 evaluated, 1 filtered
array_is_list($observables)unnecessarily drops every observer for models with keyed or non-contiguously indexed custom events, e.g.protected array $observables = [1 => 'archived'];.HasEvents::getObservableEvents()merges that array andregisterObserver()iterates its values without requiring a list, so these observers really run. Returning[[], []]here suppresses analysis even of their built-increated/updatedhandlers. Accept string-valued arrays and normalize their values instead. [ Out of scope (post-validation triage) ]src/phpstan/src/Rules/Coroutine/RestrictedAttributeRule.php — 0 comments posted, 1 evaluated, 1 filtered
whenProvidedcondition. For example,#[BootOnly(whenProvided: 'items')] public function configure(string ...$items)can legitimately be called without items, but this emits “every call is restricted” and recommends removing the condition. PHPStan 2.3.5'sPhpFunctionFromParserNodeReflection::getParameters()explicitly treats variadics as optional, whilePhpParameterFromParserNodeReflection::getDefaultValue()returns the declared default (null here). The collector also skips the restriction when no argument is supplied. Exempt variadic parameters forwhenProvided, or check requiredness rather than default presence for that condition. [ Out of scope (post-validation triage) ]src/phpstan/src/Rules/Coroutine/WorkerStateHelper.php — 0 comments posted, 1 evaluated, 1 filtered
isConfigWrite()treats first-class method callables as writes. For example,$setter = config()->set(...);inside a queued job'shandle()now produceshypervel.runtimeConfigMutation, even if$setteris never invoked: the collector no longer filters first-class callables, this method returns true, andWorkerStateRulereports the collected write. PHP's first-class callable documentation states that this syntax creates a Closure, rather than calling the method (https://www.php.net/manual/en/functions.first_class_callable_syntax.php; the project requires PHP ^8.4). Restore anisFirstClassCallable()exclusion here so acquiring a callable isn't reported as mutating configuration. [ Out of scope (triage) ]