phpstan-tests.neon
Rama master · réplica de lectura del repositorio original.
# S128 — PHPStan for tests/. A SEPARATE config on purpose.
#
# ┌───────────────────────────────────────────────────────────────────────────────┐
# │S343 — src/ KEEPS LEVEL 9 WITH NO BASELINE, NO ignoreErrors AND EMPTY │
# │excludePaths. StaticAnalysisScopeTest asserts all of it against phpstan.neon. │
# │Nothing in this file touches that. Do not merge the two configs. │
# └───────────────────────────────────────────────────────────────────────────────┘
#
# ⚠ THE TRAP THIS FILE EXISTS BECAUSE OF. Before S128, `phpstan.neon` said
# `paths: [src]` — but that was never what made tests/ unanalysed, and "just add
# tests to paths" would NOT have fixed it. The CI step is
#
# ./vendor/bin/phpstan analyze src/ --level=9 --no-progress --error-format=github
#
# and a path given on the COMMAND LINE overrides `parameters.paths` entirely. So
# adding `tests` to phpstan.neon would have changed local runs and left CI analysing
# exactly `src/`, with a green gate and a config that lied about its own scope. That
# is why the fix is a second config plus a second CI step that names it, and why
# tests/Unit/Support/StaticAnalysisScopeTest.php asserts the WORKFLOW, not the config.
#
# LEVEL — measured, never aspirational. The original ladder below was run against
# `tests/` on origin/master @ 12bdea6c (phpstan 2.2.5, PHP 8.3.6, CI no-swoole ini set):
#
# level 0 -> 6 errors level 5 -> 138
# level 1 -> 9 level 6 -> 152
# level 2 -> 51 level 7 -> 214
# level 3 -> 52 level 8 -> 274
# level 4 -> 86 level 9 -> 532
#
# S128 chose level 2 — the highest rung fixable at source in one atomic change (all 51
# fixed, zero config ignores, zero baseline): a test calling a production method that no
# longer exists becomes a static error instead of a runtime surprise on the one branch
# that reaches it.
#
# S186 raised the rung to LEVEL 4. The ladder moved under the plan's own feet: the
# documented "level 4 adds 34" was a stale claim — re-derived at tip 5986b61d the level-4
# delta measured 85 errors across 61 files (identifiers: 18 method.alreadyNarrowedType,
# 13 return.unusedType, 7 nullsafe.neverNull, 6 function.alreadyNarrowedType, 5
# staticMethod.alreadyNarrowedType, 4 method.impossibleType, 4 nullCoalesce.offset, 3
# catch.neverThrown, and 25 others). All 85 were triaged with per-error MUTATION evidence
# and closed AT SOURCE: tautological assertions deleted where a live sibling already
# pinned the fact (or made live via reflection/`constant()` seams where the fold was the
# only reason they looked dead), test-double return types narrowed to what they actually
# return, dead guards dropped, `@param-out` for by-ref contracts, and honest docblocks
# where the old annotation over-promised. Three errors describe code that is intentional
# and are covered by identifier-carrying INLINE suppressions only: two writes whose whole
# purpose is proving PHP rejects a readonly write, and one fixture method whose existence
# is the subject of a reflection-enumeration assertion. Each is self-clearing —
# reportUnmatchedIgnoredErrors turns a stale suppression into a build failure. The
# config-level ignore list below stays EMPTY by policy: it excuses by pattern and keeps
# excusing new code.
parameters:
level: 4
paths:
- tests
bootstrapFiles:
- vendor/autoload.php
# ⚠ CI's phpstan job runs setup-php with `extensions: json` — NO swoole. This box has
# it, so PHPStan resolved Swoole\Coroutine\WaitGroup by reflection and reported
# nothing, while CI failed `Run PHPStan on tests/` with 10 class.notFound errors in
# tests/Integration/Media/Transcoding/PooledConnectionConcurrencyTest.php. A local
# green was CORRECT and still blind.
#
# phpstorm-stubs (bundled in phpstan.phar) ships 19 Swoole\Coroutine stubs and no
# WaitGroup — because WaitGroup is a PHP class inside the extension's own library
# (isUserDefined()=true, '@swoole/library/core/Coroutine/WaitGroup.php'), not an
# extension class, so it is out of scope for a stub set that describes extensions.
# Read that stub's header for the full reasoning and for the two alternatives
# (avoidance, as src/Media/Library/MediaScanner.php:1054 does; excludePaths, as
# phpstan.neon does) and why neither was chosen.
#
# ⚠ REPRODUCE THE CI CONDITION before touching this. -d flags are not inherited by
# child processes and an extension cannot be unloaded, so copy the ini scan dir
# without swoole and point PHP_INI_SCAN_DIR (env vars ARE inherited) at it:
# for f in /etc/php/8.3/cli/conf.d/*.ini; do grep -qi swoole "$f" || cp "$f" DIR/; done
# PHP_INI_SCAN_DIR=DIR php vendor/bin/phpstan analyse -c phpstan-tests.neon
# Both configs must be clean WITH and WITHOUT the extension.
#
# ⚠ `stubFiles:` DOES NOT WORK for this and I measured it before settling on
# scanFiles. A stub only OVERRIDES a class the reflection provider can already find;
# it does not introduce an unknown one. With the identical file registered as a
# stubFile, the no-swoole run still reported all 10 class.notFound errors. Two
# controls, so this is not a guess about my own file: PHPStan is loud about a stub
# path that does not exist ("Stub file … does not exist"), so the path resolved; and
# a stripped-down copy with no docblock behaved identically, so the docblock was not
# at fault. `scanFiles:` READS a file for the symbols it declares, which is exactly
# what is needed here — the same mechanism, and the same reason, as the
# bootstrap_env.php entry below.
scanFiles:
# The one Swoole class PHPStan cannot resolve without ext-swoole. Read that
# file's header: it is a PHP class inside the extension's own library, so
# phpstorm-stubs (which describes EXTENSION classes) structurally cannot ship it.
- phpstan-stubs/Swoole/Coroutine/WaitGroup.stub
# tests/Unit/Scripts/BootstrapEnvTest.php `require_once`s this script for its two
# function definitions (phlix_parse_env_file, phlix_bootstrap_cli_env). PHPStan
# does not follow a runtime require, so without this the three calls read as
# calls to functions that do not exist — the same identifier a genuine typo would
# produce. scanFiles (not bootstrapFiles) because these files must be READ for
# symbols, never EXECUTED: including bootstrap_env.php would run its
# auto-bootstrap during analysis.
- scripts/bootstrap_env.php
treatPhpDocTypesAsCertain: false
# Empty, and it stays empty. A config-level ignore is invisible at the code it
# excuses and matches by pattern, so it keeps excusing new code that happens to look
# the same — that is how an ignore list becomes the baseline this repo forbids. If
# this list is ever non-empty the level was wrong: lower the level and record the new
# ladder position instead.
#
# SEVEN suppressions DO exist, all of them INLINE, each naming its identifier and its
# reason at the line it excuses. They are the cases where PHP's type system genuinely
# cannot describe a test double:
#
# 1 x method.notFound an anonymous subclass declaring an extra METHOD (a
# AccountLinkIntegrationTest property can be expressed as an object-shape
# intersection; a method cannot)
# 3 x method.notFound Mockery::shouldReceive()'s
# PluginLoaderTest ExpectationInterface|HigherOrderMessage union
# (needs phpstan/phpstan-mockery, not a dependency)
# 2 x method.notFound a closure that RECEIVES a PHPUnit MockObject but can
# RequestAuthenticatorTest only be typed as the production interface
# 1 x assign.propertyReadOnly a write to an object-SHAPE field, where the concrete
# PooledMySQLConnectionTest type is an anonymous class built inside a factory
#
# `reportUnmatchedIgnoredErrors` is left at its default (true), which is the point:
# each of the seven becomes an `ignore.unmatchedLine` error the moment it stops being
# needed. Two DEAD suppressions were deleted in this same change for exactly that
# reason — they had been silencing nothing, invisibly, because tests/ was unanalysed.
ignoreErrors: []