|
| 1 | +# PHPStan - PHP Static Analysis Tool |
| 2 | + |
| 3 | +PHPStan finds bugs in PHP code without running it. It analyses the entire codebase and reports type errors, undefined methods/properties/variables, dead code, incorrect PHPDoc types, and many other issues. It understands PHP's type system deeply, including generics, union/intersection types, literal types, conditional return types, and template types expressed through PHPDocs. |
| 4 | + |
| 5 | +## Key concepts |
| 6 | + |
| 7 | +- **Rule levels 0-10**: Incremental adoption from basic checks (level 0) to strict mixed type enforcement (level 10). Levels are cumulative. Level 0 covers unknown classes/functions/methods and undefined variables. Level 5 checks argument types. Level 6 requires typehints. Level 9 enforces explicit `mixed`. Level 10 enforces implicit `mixed`. |
| 8 | +- **Baseline**: Allows adopting higher rule levels by recording existing errors in a baseline file, so only new errors are reported. |
| 9 | +- **Bleeding edge**: Preview of next major version features, shipped in current stable release via `bleedingEdge.neon`. |
| 10 | +- **Result cache**: PHPStan caches analysis results and only re-analyses changed files and their dependents. |
| 11 | +- **Parallel analysis**: Files are analysed in parallel across multiple child processes using React PHP. |
| 12 | +- **Configuration**: NEON format (Nette configuration). Main config is `phpstan.neon`, level configs in `conf/config.level*.neon`, services in `conf/services.neon`. |
| 13 | + |
| 14 | +## Running PHPStan |
| 15 | + |
| 16 | +```bash |
| 17 | +# Analyse with a specific level |
| 18 | +vendor/bin/phpstan analyse -l 8 src tests |
| 19 | + |
| 20 | +# Clear result cache |
| 21 | +vendor/bin/phpstan clear-result-cache |
| 22 | + |
| 23 | +# Generate baseline |
| 24 | +vendor/bin/phpstan analyse --generate-baseline |
| 25 | + |
| 26 | +# Debug mode (shows which files are being analysed) |
| 27 | +vendor/bin/phpstan analyse --debug |
| 28 | +``` |
| 29 | + |
| 30 | +## Running tests |
| 31 | + |
| 32 | +```bash |
| 33 | +vendor/bin/phpunit |
| 34 | +``` |
| 35 | + |
| 36 | +Rules are tested using `PHPStan\Testing\RuleTestCase`, type extensions with `PHPStan\Testing\TypeInferenceTestCase`. |
| 37 | + |
| 38 | +## Architecture |
| 39 | + |
| 40 | +The codebase lives under `src/` with PSR-4 autoloading mapping `PHPStan\` to `src/`. Key architectural components: |
| 41 | + |
| 42 | +### PHPStan\Analyser - Core analysis engine |
| 43 | + |
| 44 | +The analysis pipeline: `Analyser` orchestrates `FileAnalyser`, which uses `NodeScopeResolver` to walk the AST and invoke registered `Rule` implementations. |
| 45 | + |
| 46 | +**`NodeScopeResolver`** (`src/Analyser/NodeScopeResolver.php`) - The central engine of PHPStan. It traverses the AST (from nikic/php-parser) and maintains a `MutatingScope` that gets updated at each node. It handles: |
| 47 | +- Control flow analysis (if/else, switch, try/catch, loops, match) |
| 48 | +- Variable assignments and type narrowing |
| 49 | +- Function/method call resolution |
| 50 | +- Closure and arrow function scoping |
| 51 | +- Type narrowing from conditions (`instanceof`, `is_*()`, `===`, etc.) |
| 52 | +- PHPDoc type resolution and `@var`/`@param`/`@return` processing |
| 53 | +- Invoking registered Rules and Collectors at each AST node |
| 54 | + |
| 55 | +**`MutatingScope`** (`src/Analyser/MutatingScope.php`) - Holds the current state of analysis after each AST node. It tracks: |
| 56 | +- Variable types (assigned, possibly-defined, narrowed) |
| 57 | +- Current context: namespace, class, method/function, trait, anonymous function |
| 58 | +- Property types and initialization state |
| 59 | +- Constant values |
| 60 | +- Expression types via `getType(Expr $expr): Type` |
| 61 | +- Native types vs PHPDoc types |
| 62 | + |
| 63 | +The `Scope` interface (`src/Analyser/Scope.php`) is the public API that rules and extensions use. `MutatingScope` is the internal implementation. |
| 64 | + |
| 65 | +**`TypeSpecifier`** (`src/Analyser/TypeSpecifier.php`) - Narrows types based on conditions. When NodeScopeResolver processes an `if ($x instanceof Foo)`, TypeSpecifier determines that `$x` is `Foo` in the truthy branch and `not Foo` in the falsy branch. It uses `TypeSpecifierContext` (truthy/falsey/true/false/null) to track which branch is being processed. Extensible via `FunctionTypeSpecifyingExtension`, `MethodTypeSpecifyingExtension`, and `StaticMethodTypeSpecifyingExtension`. |
| 66 | + |
| 67 | +**`ExpressionTypeHolder`** (`src/Analyser/ExpressionTypeHolder.php`) - Stores an expression's type together with a `TrinaryLogic` certainty (Yes = definitely this type, Maybe = possibly this type). This is the building block of how `MutatingScope` tracks variable types - it maps expression keys to `ExpressionTypeHolder` instances. |
| 68 | + |
| 69 | +**`ConstantResolver`** (`src/Analyser/ConstantResolver.php`) - Resolves PHP constant names to their types. Handles 150+ predefined PHP constants with PHP-version-aware types (e.g. different return types based on `PHP_VERSION_ID`). Also resolves user-configured constant type mappings. |
| 70 | + |
| 71 | +**`InitializerExprTypeResolver`** (`src/Reflection/InitializerExprTypeResolver.php`) - Resolves types of constant expressions and initializers (default parameter values, property defaults, constant values). Handles arithmetic, casts, function calls, and binary operations in constant contexts where a full Scope is not available. |
| 72 | + |
| 73 | +### Analysis pipeline in detail |
| 74 | + |
| 75 | +1. `AnalyseCommand` parses CLI arguments, builds the DI container |
| 76 | +2. `Analyser` receives the list of files, sets up `NodeScopeResolver` with the full file list |
| 77 | +3. For parallel runs, `ParallelAnalyser` distributes files across worker processes via TCP using NDJSON protocol (React event loop). Default: up to 32 processes, 20 files per job, 600s timeout. |
| 78 | +4. Each worker runs `FileAnalyser::analyseFile()` which parses a file to AST, then calls `NodeScopeResolver::processStmtNodes()` |
| 79 | +5. `NodeScopeResolver` walks the AST depth-first. At each node, it updates `MutatingScope` and invokes all registered `Rule` and `Collector` callbacks for that node type |
| 80 | +6. Results (errors, dependencies, collected data) flow back to the main process |
| 81 | +7. After all files are processed, `CollectedDataNode` rules run with the aggregated collector data |
| 82 | +8. `ResultCacheManager` saves results keyed by file SHA256 hashes and a dependency graph for incremental re-analysis |
| 83 | + |
| 84 | +### Result cache (`src/Analyser/ResultCache/`) |
| 85 | + |
| 86 | +`ResultCacheManager` enables incremental analysis. It tracks: |
| 87 | +- SHA256 hashes of all analysed files |
| 88 | +- Dependency graph between files (so changing one file re-analyses its dependents) |
| 89 | +- Exported nodes (class/function signatures) to detect API changes |
| 90 | +- PHP version, loaded extensions, and config hash for cache invalidation |
| 91 | + |
| 92 | +On subsequent runs, only changed files and their transitive dependents are re-analysed. |
| 93 | + |
| 94 | +### Error ignoring (`src/Analyser/Ignore/`) |
| 95 | + |
| 96 | +`IgnoredErrorHelper` processes error ignore patterns from configuration and inline `@phpstan-ignore` comments. Supports regex patterns, error identifiers, and file-specific ignoring. Tracks which ignore patterns were matched so unmatched patterns can be reported. |
| 97 | + |
| 98 | +### PHPStan\Type - Type system |
| 99 | + |
| 100 | +Implementations of the `Type` interface (`src/Type/Type.php`) represent everything PHPStan knows about types. Each type knows: |
| 101 | +- What it accepts (`accepts()`) and what is a supertype of it (`isSuperTypeOf()`) |
| 102 | +- What properties/methods/constants it has |
| 103 | +- What operations result in what types (array operations, arithmetic, string operations, etc.) |
| 104 | +- How to describe itself for error messages (`describe()`) |
| 105 | +- How to narrow itself (`tryRemove()`, generalize, traverse) |
| 106 | + |
| 107 | +Key type classes: |
| 108 | +- `ObjectType`, `StringType`, `IntegerType`, `FloatType`, `BooleanType`, `NullType`, `ArrayType`, `MixedType`, `NeverType`, `VoidType` |
| 109 | +- `UnionType`, `IntersectionType` - composite types |
| 110 | +- `Constant\ConstantStringType`, `Constant\ConstantIntegerType`, `Constant\ConstantArrayType` - literal/known values |
| 111 | +- `Generic\GenericObjectType`, `Generic\TemplateType` - generics |
| 112 | +- `Accessory\AccessoryNonEmptyStringType`, `Accessory\NonEmptyArrayType`, etc. - combined via intersection for refined types like `non-empty-string` |
| 113 | +- `IntegerRangeType` - integer ranges like `int<0, 100>` |
| 114 | +- `Enum\EnumCaseObjectType` - enum cases |
| 115 | +- `ClosureType`, `CallableType` - callable types |
| 116 | +- `StaticType`, `ThisType` - late static binding |
| 117 | + |
| 118 | +**`TypeCombinator`** - Used instead of constructing `UnionType`/`IntersectionType` directly. Handles type normalization (e.g. `mixed|int` becomes `mixed`, `string&int` becomes `never`). |
| 119 | + |
| 120 | +**`TrinaryLogic`** - Three-valued logic (yes/no/maybe) used throughout the type system. Many Type methods return `TrinaryLogic` instead of `bool` because type relationships aren't always certain (e.g. `mixed` might be a string - that's `maybe`). |
| 121 | + |
| 122 | +To query whether a type is a specific type, use `isSuperTypeOf()`, not `instanceof`. For example, `(new StringType())->isSuperTypeOf($type)->yes()` correctly handles union types, intersection types with accessory types, etc. There are also shortcut methods like `$type->isString()`, `$type->isInteger()`, etc. |
| 123 | + |
| 124 | +### PHPStan\Rules - Static analysis checks |
| 125 | + |
| 126 | +Rules implement the `Rule<TNodeType>` interface (`src/Rules/Rule.php`): |
| 127 | +- `getNodeType(): string` - returns the AST node class to listen for |
| 128 | +- `processNode(Node $node, Scope $scope): array` - returns errors found at this node |
| 129 | + |
| 130 | +Rules are organized into subdirectories by category: `Classes/`, `Methods/`, `Properties/`, `Functions/`, `Variables/`, `DeadCode/`, `Generics/`, `PhpDoc/`, `Cast/`, `Comparison/`, `Exceptions/`, `Pure/`, `Arrays/`, `Types/`, etc. |
| 131 | + |
| 132 | +Rules are registered in configuration via the `phpstan.rules.rule` service tag or the `rules:` section. Different rule levels activate different rules via `conf/config.level*.neon`. |
| 133 | + |
| 134 | +**Collectors** (`src/Collectors/`) - For rules that need cross-file information (e.g. unused code detection). Collectors gather data across all files in parallel processes, then a rule registered for `CollectedDataNode` processes the aggregated data. |
| 135 | + |
| 136 | +### PHPStan\Reflection - Code metadata |
| 137 | + |
| 138 | +PHPStan has its own reflection layer, primarily backed by the BetterReflection library (`ondrejmirtes/better-reflection`), which provides static reflection (reading code without loading it). |
| 139 | + |
| 140 | +**`ReflectionProvider`** (`src/Reflection/ReflectionProvider.php`) - Central entry point for looking up classes, functions, and constants. |
| 141 | + |
| 142 | +**`ClassReflection`** (`src/Reflection/ClassReflection.php`) - Represents classes, interfaces, traits, and enums. Provides access to methods, properties, constants, parent classes, interfaces, traits, generics, PHPDocs, and attributes. |
| 143 | + |
| 144 | +**Class reflection extensions** allow describing magic properties/methods from `__get`/`__set`/`__call`. |
| 145 | + |
| 146 | +The reflection layer also includes `ParametersAcceptor` for function/method signatures (with multi-variant support for overloaded built-in functions), `SignatureMap` for built-in PHP function signatures, and stub files for overriding third-party type information. |
| 147 | + |
| 148 | +### PHPStan\Node - Virtual AST nodes |
| 149 | + |
| 150 | +PHPStan augments the nikic/php-parser AST with custom virtual nodes (`src/Node/`): |
| 151 | +- `FileNode` - wraps an entire file |
| 152 | +- `InClassNode`, `InClassMethodNode`, `InFunctionNode`, `InTraitNode` - provide scope-aware context (e.g. `InClassNode` lets rules access `$scope->getClassReflection()`) |
| 153 | +- `ClassPropertiesNode`, `ClassMethodsNode`, `ClassConstantsNode` - aggregate all properties/methods/constants of a class (useful for checking completeness) |
| 154 | +- `ClassPropertyNode` - unifies traditional and promoted properties |
| 155 | +- `CollectedDataNode` - carries aggregated data from collectors |
| 156 | +- `ExecutionEndNode` - marks unreachable code points |
| 157 | +- `MatchExpressionNode`, `BooleanAndNode`, `BooleanOrNode` - enhanced representations of expressions |
| 158 | + |
| 159 | +### PHPStan\PhpDoc - PHPDoc parsing |
| 160 | + |
| 161 | +Uses `phpstan/phpdoc-parser` to parse PHPDoc comments into an AST, then resolves PHPDoc types into `PHPStan\Type\Type` objects via `TypeNodeResolver`. Handles `@param`, `@return`, `@var`, `@throws`, `@template`, `@extends`, `@implements`, `@phpstan-assert`, `@phpstan-type`, `@phpstan-import-type`, conditional return types, and more. |
| 162 | + |
| 163 | +### PHPStan\DependencyInjection - Service container |
| 164 | + |
| 165 | +Uses Nette DI container. Services are configured in NEON files. Extensions register via service tags like `phpstan.rules.rule`, `phpstan.broker.dynamicMethodReturnTypeExtension`, `phpstan.typeSpecifier.functionTypeSpecifyingExtension`, `phpstan.collector`, etc. |
| 166 | + |
| 167 | +The `#[AutowiredService]` and `#[AutowiredParameter]` attributes are used for automatic service registration. |
| 168 | + |
| 169 | +### PHPStan\Node\ClassStatementsGatherer |
| 170 | + |
| 171 | +Collects structural information about a class during AST traversal: properties, methods, method calls, property reads/writes. Used by virtual nodes like `ClassPropertiesNode` and `ClassMethodsNode` to provide aggregate information for rules that check class-level invariants (e.g. unused private properties, uninitialized properties). |
| 172 | + |
| 173 | +### PHPStan\Fixable - Auto-fixing |
| 174 | + |
| 175 | +`Patcher` coordinates automatic error fixes. Rules can attach fix information to errors via `RuleErrorBuilder`. The `PhpPrinter` handles code generation, `ReplacingNodeVisitor` performs AST node replacements, and indentation is preserved via `PhpPrinterIndentationDetectorVisitor`. |
| 176 | + |
| 177 | +### Other components |
| 178 | + |
| 179 | +- **`PHPStan\Parser`** - Wraps nikic/php-parser with caching, visitor registration, and PHPStan-specific AST enrichment (e.g. `ArrayMapArgVisitor`, `ImmediatelyInvokedClosureVisitor`) |
| 180 | +- **`PHPStan\Parallel`** - `ParallelAnalyser` + `Scheduler` + `ProcessPool` distribute file analysis across child processes via React TCP server |
| 181 | +- **`PHPStan\Command`** - CLI commands (`AnalyseCommand`, `ClearResultCacheCommand`, etc.) and error formatters (table, json, github actions, etc.) |
| 182 | +- **`PHPStan\Dependency`** - Tracks file dependencies for incremental analysis / result cache. `ExportedNode` represents a class/function/constant signature for detecting API changes. |
| 183 | +- **`PHPStan\File`** - File path resolution and reading |
| 184 | +- **`PHPStan\Php`** - `PhpVersion` abstraction with version source tracking (runtime, config, composer platform). Methods like `supportsEnums()`, `supportsReadonlyProperties()`, etc. for version-specific behavior. |
| 185 | +- **`PHPStan\Cache`** - Caching infrastructure |
| 186 | +- **`PHPStan\Testing`** - `RuleTestCase`, `TypeInferenceTestCase`, and other test utilities |
| 187 | + |
| 188 | +## Extension points |
| 189 | + |
| 190 | +PHPStan is highly extensible. Key extension interfaces: |
| 191 | + |
| 192 | +- **Custom rules** - `PHPStan\Rules\Rule` interface, tag: `phpstan.rules.rule` |
| 193 | +- **Dynamic return type extensions** - `DynamicMethodReturnTypeExtension`, `DynamicStaticMethodReturnTypeExtension`, `DynamicFunctionReturnTypeExtension` |
| 194 | +- **Type-specifying extensions** - `MethodTypeSpecifyingExtension`, `StaticMethodTypeSpecifyingExtension`, `FunctionTypeSpecifyingExtension` - for custom type narrowing (like `is_int()`) |
| 195 | +- **Class reflection extensions** - `PropertiesClassReflectionExtension`, `MethodsClassReflectionExtension` - for magic properties/methods |
| 196 | +- **Dynamic throw type extensions** - describe when functions throw based on arguments |
| 197 | +- **Closure type extensions** - override closure parameter/return types or `$this` binding |
| 198 | +- **Custom PHPDoc types** - `TypeNodeResolverExtension` for custom type syntax |
| 199 | +- **Collectors** - `PHPStan\Collectors\Collector` for cross-file analysis |
| 200 | +- **Error formatters** - custom output formats |
| 201 | +- **Restricted usage extensions** - simple interfaces to restrict where methods/properties/functions can be called from |
| 202 | +- **Allowed subtypes** - define sealed class hierarchies |
| 203 | +- **Always-read/written properties, always-used constants/methods** - suppress false positives for dead code detection |
| 204 | + |
| 205 | +## Important dependencies |
| 206 | + |
| 207 | +- `nikic/php-parser` ^5.7.0 - PHP AST parsing |
| 208 | +- `ondrejmirtes/better-reflection` - Static reflection (reading code without loading it) |
| 209 | +- `phpstan/phpdoc-parser` - PHPDoc parsing |
| 210 | +- `nette/di` - Dependency injection container |
| 211 | +- `nette/neon` - Configuration file format |
| 212 | +- `react/child-process`, `react/async` - Parallel analysis |
| 213 | +- `symfony/console` - CLI interface |
| 214 | +- `hoa/compiler` - Used for regex type parsing |
0 commit comments