Ctrl K
ComunidadGuías…

phpstan-tests.neon

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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: []