# Static analysis. # # Level 5 with everything already failing captured in a baseline. The # alternative — level 0 with no baseline — reports nothing interesting, and # level 9 buys a fortnight of triage before it finds its first real bug. Five is # where the class of defect this codebase actually has starts showing up: # nullable entity getters (`?int getId()`, `?Account getAccount()`) flowing into # parameters and return types that are not nullable. # # The baseline is a debt ledger, not a licence. It shrinks; raise the level when # it is small enough that doing so is a day's work rather than a fortnight's. includes: - phpstan-baseline.neon - vendor/phpstan/phpstan-symfony/extension.neon - vendor/phpstan/phpstan-symfony/rules.neon - vendor/phpstan/phpstan-doctrine/extension.neon - vendor/phpstan/phpstan-doctrine/rules.neon parameters: level: 5 # The whole project rather than just src/ and tests/: bin/, config/ and # public/ hold real PHP that can break the same way anything else does — # config/bootstrap_generated_secrets.php is loaded into every process # through composer's autoload.files. # # Listed explicitly rather than as `./`, and that is not a style # preference. PHPStan walks the tree with a RecursiveDirectoryIterator # *before* it applies excludePaths, so a directory it cannot read is a hard # crash rather than a skip — and no exclusion can prevent it, because the # crash happens during enumeration. `.claude/worktrees` holds entire git # worktrees of this repository (37 of them at the time of writing), several # containing a root-owned `var/secrets` left by a container. With `./` the # gate could not be run at all from a working checkout: it died on a # directory belonging to a different checkout of itself. # # Everything with PHP in it is named below. A new top-level directory that # holds code has to be added here, which is the cost of this; the previous # arrangement's cost was that the command did not run. paths: - bin - config - public - src - tests - importmap.php # Read for its classes, not analysed: migrations are generated and nobody # edits them by hand, but a test that runs one (tests/Migrations) names its # class, and without this that is a class PHPStan has never heard of. scanDirectories: - migrations excludePaths: analyseAndScan: # Not PHP. - assets - docs - frankenphp - templates - translations # Third-party code, and its symbols come from the autoloader # anyway — analysing it reports other people's debt. - vendor - node_modules (?) # Build and run artefacts. public/assets is compiled output and # var/ holds the container dumps this very config reads. # # The `(?)` marks a path that may legitimately be absent: PHPStan # treats an excludePath that matches nothing as a config error, and # none of these exist on a fresh clone or in a CI job that has not # built assets yet. - public/assets (?) - public/bundles (?) - var (?) - playwright (?) - playwright-report (?) - test-results (?) # Generated: a ~1000-line dump of the framework config tree that # exists to be read. (migrations/ is not analysed either, by not # being in `paths`; see scanDirectories.) - config/reference.php (?) # Twig extensions lifted from upstream rather than written here. # NOTE: only Vendor/ — the rest of src/Twig is application code # (SidebarCounts and friends) and must stay analysed. - src/Twig/Vendor # ignoreErrors is deliberately empty. # # The rule usually wanted here — silencing "Property …::$id (int|null) is # never assigned int" on Doctrine identifiers — is not needed: the # phpstan-doctrine extension included above already understands that the # ORM assigns identifiers by reflection, so the error never fires. Adding # the pattern anyway would fail the build, because PHPStan treats an ignore # that matches nothing as a config error. # # What property.onlyWritten does catch in this project is a different and # genuine thing: controllers injecting repositories they never use # (MailController::$mailboxRepository, ComposeController::$mailboxRepository # and about fifteen more). Those are in the baseline as debt to delete, not # noise to suppress — which is why there is no blanket ignore for them. ignoreErrors: [] # Docblocks here are prose as much as types — plenty predate the signatures # beside them. Trusting them as certain turns a stale @var into a stream of # "condition is always false" on code that is fine. treatPhpDocTypesAsCertain: false symfony: containerXmlPath: var/cache/test/App_KernelTestDebugContainer.xml consoleApplicationLoader: tests/phpstan-console-loader.php doctrine: objectManagerLoader: tests/phpstan-object-manager.php