Skip to content

Commit 482780a

Browse files
committed
added CLAUDE.md
1 parent 90b5d54 commit 482780a

2 files changed

Lines changed: 184 additions & 0 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.gitattributes export-ignore
22
.github/ export-ignore
33
.gitignore export-ignore
4+
CLAUDE.md export-ignore
45
phpstan*.neon export-ignore
56
tests/ export-ignore
67

CLAUDE.md

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
`nette/phpstan-rules` is a PHPStan extension package for Nette library developers. It provides custom rules and type extensions used when analysing Nette libraries with PHPStan. The package is consumed by individual Nette repositories via their PHPStan configuration.
8+
9+
## Commands
10+
11+
```bash
12+
composer phpstan # Run static analysis (level 8)
13+
composer tester # Run all tests
14+
vendor/bin/tester tests/SomeTest.phpt -s # Run a single test
15+
```
16+
17+
## Architecture
18+
19+
- **`src/`** — Extension source code, PSR-4 autoloaded under `Nette\PHPStan\` namespace
20+
- **`src/Tester/TypeAssert.php`** — Reusable type inference testing helper for Nette Tester (used by other Nette packages)
21+
- **`extension.neon`** — Entry point, includes `extension-php.neon` and `extension-nette.neon`, auto-included by `phpstan/extension-installer`
22+
- **`extension-php.neon`** — Generic PHP-level extensions (RemoveFailingReturnType, ClosureTypeCheckIgnore)
23+
- **`extension-nette.neon`** — All Nette package extensions (component-model, forms, schema, tester, utils), separated by comments
24+
- **`phpstan.neon`** — Self-analysis config (level 8, analyses `src/` and `tests/`)
25+
26+
### How extensions are registered
27+
28+
Each extension class is registered as a service in NEON with the appropriate tag. Common tags:
29+
- `phpstan.rules.rule` — custom rules
30+
- `phpstan.collector` — collectors
31+
- `phpstan.broker.expressionTypeResolverExtension` — expression type resolution (runs before all dynamic extensions)
32+
- `phpstan.broker.dynamicFunctionReturnTypeExtension` — dynamic function return types
33+
- `phpstan.broker.dynamicMethodReturnTypeExtension` — dynamic instance method return types
34+
- `phpstan.broker.dynamicStaticMethodReturnTypeExtension` — dynamic static method return types
35+
- `phpstan.ignoreErrorExtension` — conditional error suppression
36+
- `phpstan.broker.propertiesClassReflectionExtension` — magic properties
37+
- `phpstan.broker.methodsClassReflectionExtension` — magic methods
38+
- `phpstan.broker.typeSpecifyingExtension` — type narrowing
39+
40+
### Namespace conventions
41+
42+
Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\ComponentModel\` for nette/component-model, `Nette\PHPStan\Schema\` for nette/schema, `Nette\PHPStan\Utils\` for nette/utils, future packages follow the same pattern (`Nette\PHPStan\Forms\`, `Nette\PHPStan\Application\`, etc.). Generic PHP-level extensions use `Nette\PHPStan\Php\`.
43+
44+
### ExpectArrayReturnTypeExtension
45+
46+
`ExpectArrayReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows the return type of `Expect::array()` from `Structure|Type` to `Structure` or `Type`. It inspects the argument: no argument, null, empty array, or non-Schema values → `Type`; all values implementing `Schema``Structure`; mixed/unknown → fallback to declared union. Config: `extension-nette.neon`.
47+
48+
### ArrowFunctionVoidIgnoreExtension
49+
50+
`ArrowFunctionVoidIgnoreExtension` (`IgnoreErrorExtension`) suppresses `argument.type` when an arrow function (which always returns a value) is passed to a parameter typed as `Closure(): void`. The list of affected functions/methods is configurable via a flat NEON list — plain names for functions (`testException`), `Class::method` notation for methods (`Tester\Assert::exception`). Config: `extension-nette.neon`.
51+
52+
### ClosureTypeCheckIgnoreExtension
53+
54+
`ClosureTypeCheckIgnoreExtension` (`IgnoreErrorExtension`) suppresses `expr.resultUnused` for the runtime type validation pattern `(function(Type ...$p) {})(...$args)`. Config: `extension-php.neon`.
55+
56+
### RemoveFailingReturnTypeExtension
57+
58+
`RemoveFailingReturnTypeExtension` (`ExpressionTypeResolverExtension`) removes `|false` or `|null` from return types of native PHP functions and methods where the error return value is trivial or outdated. It handles `FuncCall`, `MethodCall`, and `StaticCall` in a single class. Configuration uses a flat list in NEON — plain names for functions (`json_encode`), `Class::method` notation for methods (`Normalizer::normalize`). It runs before all `DynamicReturnTypeExtension` implementations, delegates to them via `DynamicReturnTypeExtensionRegistry`, and strips `|false` from the result. For `preg_replace`, `preg_replace_callback`, `preg_replace_callback_array`, and `preg_filter` it strips `|null` instead (these return null on PCRE error). For `preg_replace_callback_array` pattern validation checks array keys. Config: `extension-php.neon`.
59+
60+
### FalseToNullReturnTypeExtension
61+
62+
`FalseToNullReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows the return type of `Helpers::falseToNull()` from `mixed`. It removes `false` from the argument type and adds `null` — e.g. `string|false``string|null`, `false``null`, types without `false` pass through unchanged. Config: `extension-nette.neon`.
63+
64+
### StringsReturnTypeExtension
65+
66+
`StringsReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows return types of `Strings::match()`, `matchAll()` and `split()` based on boolean arguments. It resolves `captureOffset`, `unmatchedAsNull`, `patternOrder`, and `lazy` to constant booleans and constructs the precise return type — e.g. `match()` with `captureOffset: true` returns `array<array{string, int<0, max>}>|null` instead of `?array`. When a boolean argument is not a constant, falls back to the declared return type. Config: `extension-nette.neon`.
67+
68+
### ArraysInvokeTypeExtension
69+
70+
`ArraysInvokeTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows return types of `Arrays::invoke()` and `Arrays::invokeMethod()` from `array`. For `invoke()`, it extracts the callable return type from the iterable value type and forwards `...$args` via `ParametersAcceptorSelector::selectFromArgs()` to resolve the correct overload. For `invokeMethod()`, it resolves constant method names on the object type, gets method reflection, and forwards remaining args. Handles `callable(): void` by converting void to null. Falls back to declared return type when callbacks are not callable, method names are not constant strings, or methods don't exist on the object type. Config: `extension-nette.neon`.
71+
72+
### HtmlMethodsClassReflectionExtension
73+
74+
`HtmlMethodsClassReflectionExtension` (`MethodsClassReflectionExtension`) resolves `getXxx()`, `setXxx()`, and `addXxx()` magic methods on `Nette\Utils\Html` that go through `__call()` but aren't declared via `@method` annotations. `getXxx()` returns `mixed`, `setXxx()` and `addXxx()` return `static`. Config: `extension-nette.neon`.
75+
76+
### GetComponentReturnTypeExtension
77+
78+
`GetComponentReturnTypeExtension` (`DynamicMethodReturnTypeExtension`) narrows return types of `Container::getComponent()` and `Container::offsetGet()` (i.e. `$this['xxx']`). When the component name is a constant string, it looks for a `createComponent<Name>()` factory method on the caller type and returns its return type — e.g. `$this->getComponent('poll')` returns `PollControl` if `createComponentPoll(): PollControl` exists. Falls back to the declared return type when no factory method is found. Config: `extension-nette.neon`.
79+
80+
### FormContainerReturnTypeExtension
81+
82+
`FormContainerReturnTypeExtension` (`DynamicMethodReturnTypeExtension`) narrows return types of `Forms\Container::getComponent()` and `::offsetGet()` (i.e. `$form['xxx']`) based on `addXxx()` calls in the same function body. When the component name is a constant string, it parses the current file, finds the enclosing function/method, and walks the AST looking for `$form->addText('name')`, `$form->addSelect('name')`, etc. on the same variable. Returns the `addXxx` method's declared return type — e.g. `$form['name']` returns `TextInput` after `$form->addText('name', ...)`. Falls back to `createComponent*()` factory lookup. Only matches simple variable names (not complex expressions). Config: `extension-nette.neon`.
83+
84+
### AssertTypeNarrowingExtension
85+
86+
`AssertTypeNarrowingExtension` (`StaticMethodTypeSpecifyingExtension` + `TypeSpecifierAwareExtension`) narrows variable types after `Tester\Assert` assertion calls. Each assertion method is mapped to an equivalent PHP expression that PHPStan already understands, then delegated to `TypeSpecifier::specifyTypesInCondition()`. Supported methods: `null`, `notNull`, `true`, `false`, `truthy`, `falsey`, `same`, `notSame`, and `type` (with built-in type strings like `'string'`, `'int'`, etc. and class/interface names). Config: `extension-nette.neon`.
87+
88+
### MapperTypeResolver (Assets)
89+
90+
`MapperTypeResolver` is a shared service used by the three assets extensions below. It resolves mapper IDs to mapper class types from a `mapping` config using keywords (`'file'``FilesystemMapper`, `'vite'``ViteMapper`, or FQCN for custom classes), resolves asset references to asset class types based on file extension (mirroring `Helpers::createAssetFromUrl()` logic), parses qualified references (`'mapper:reference'`), and checks whether a mapper is a known type (`FilesystemMapper` or `ViteMapper`). Config: `extension-nette.neon` parameter `nette.assets.mapping`.
91+
92+
### GetMapperReturnTypeExtension
93+
94+
`GetMapperReturnTypeExtension` (`DynamicMethodReturnTypeExtension`) narrows return type of `Registry::getMapper()` from `Mapper` to the specific mapper class based on NEON configuration. When no argument is passed, uses `'default'` as mapper ID (matching the method's default parameter). Falls back to declared return type for unknown mapper IDs. Config: `extension-nette.neon`.
95+
96+
### MapperGetAssetExtension
97+
98+
`MapperGetAssetExtension` (`DynamicMethodReturnTypeExtension`) narrows return type of `FilesystemMapper::getAsset()` and `ViteMapper::getAsset()` from `Asset` to the specific asset class based on file extension (e.g. `.jpg``ImageAsset`, `.js``ScriptAsset`). Single class registered twice in NEON with different `className` argument. For ViteMapper, `.js` narrows to `ScriptAsset` (safe because `EntryAsset extends ScriptAsset`). Config: `extension-nette.neon`.
99+
100+
### RegistryGetAssetExtension
101+
102+
`RegistryGetAssetExtension` (`DynamicMethodReturnTypeExtension`) narrows return types of `Registry::getAsset()` and `Registry::tryGetAsset()` from `Asset`/`?Asset` to specific asset class. Parses the qualified reference to extract mapper ID and asset path, checks if the mapper is a known type (`FilesystemMapper` or `ViteMapper`), then resolves asset type from file extension. For `tryGetAsset()`, adds `|null` via `TypeCombinator::addNull()`. Only narrows for string references; array references fall back to declared type. Config: `extension-nette.neon`.
103+
104+
### TableRowTypeResolver
105+
106+
`TableRowTypeResolver` is a shared service used by the three database extensions below. It resolves database table names to entity row class types using a configurable convention mask (e.g. `App\Entity\*Row` where `*` is replaced by PascalCase table name) and optional explicit table-to-class overrides. Checks class existence via `ReflectionProvider`. Config: `extension-nette.neon` parameters `nette.database.mapping.convention` and `nette.database.mapping.tables`.
107+
108+
### ExplorerTableReturnTypeExtension
109+
110+
`ExplorerTableReturnTypeExtension` (`DynamicMethodReturnTypeExtension`) narrows return type of `Explorer::table()` from `Selection<ActiveRow>` to `Selection<EntityRow>` based on table-to-entity-class mapping. When the table name argument is a constant string and the resolved entity class exists, returns `GenericObjectType('Selection', [$rowType])`. Falls back to declared return type otherwise. Config: `extension-nette.neon`.
111+
112+
### ActiveRowRelatedReturnTypeExtension
113+
114+
`ActiveRowRelatedReturnTypeExtension` (`DynamicMethodReturnTypeExtension`) narrows return type of `ActiveRow::related()` from `GroupedSelection` to `GroupedSelection<EntityRow>`. Handles both plain table names and `table.column` format by extracting the table portion. Config: `extension-nette.neon`.
115+
116+
### ActiveRowRefReturnTypeExtension
117+
118+
`ActiveRowRefReturnTypeExtension` (`DynamicMethodReturnTypeExtension`) narrows return type of `ActiveRow::ref()` from `?self` to `?EntityRow`. Handles both plain table names and `table.column` format. Uses `TypeCombinator::addNull()` to preserve nullability. Config: `extension-nette.neon`.
119+
120+
### Testing
121+
122+
Tests use **Nette Tester** (not PHPUnit). Test files are `.phpt` in `tests/` with data files in `tests/data/`.
123+
124+
Type inference tests use `Nette\PHPStan\Tester\TypeAssert::assertTypes()` which creates a PHPStan DI container, walks AST via `NodeScopeResolver`, and verifies `assertType()` calls from test data files. Important: both `PathRoutingParser` and `NodeScopeResolver` need `setAnalysedFiles()` — without the parser call, function bodies get stripped by `CleaningParser`. This class is designed to be reusable by other Nette packages.
125+
126+
## Workflow After Creating a New Extension
127+
128+
After every new extension is created, always perform these steps:
129+
130+
1. **Write tests** — Create a `.phpt` test file in `tests/` with corresponding data files in `tests/data/`. Run `composer tester` to verify.
131+
2. **Update CLAUDE.md** — Add a new `###` section describing the extension (type, what it does, config file) following the existing format.
132+
3. **Update readme.md** — Add the extension to the appropriate section in the readme so users know about it.
133+
134+
## Related Repositories
135+
136+
- **PHPStan source code**`W:\libs.3rd\phpstan`
137+
138+
## `doc/` Directory Reference
139+
140+
Read the relevant documentation files **before** writing code on PHPStan extensions.
141+
142+
### Foundation (read as prerequisites)
143+
144+
| File | Read when... |
145+
|------|-------------|
146+
| `doc/core-concepts.md` | First contact with PHPStan extension development; unsure where to start |
147+
| `doc/abstract-syntax-tree.md` | Writing a custom rule — need to pick the right AST node for `getNodeType()` |
148+
| `doc/scope.md` | Getting expression types via `$scope->getType()`, determining context (class, method, namespace) |
149+
| `doc/type-system.md` | Creating, comparing, or combining types; using `isSuperTypeOf()` or `TypeCombinator` |
150+
| `doc/trinary-logic.md` | Working with `isSuperTypeOf()` results (returns TrinaryLogic, not bool) |
151+
| `doc/reflection.md` | Need introspection of classes/methods/properties; reading PHPDoc via `FileTypeMapper` |
152+
153+
### Infrastructure
154+
155+
| File | Read when... |
156+
|------|-------------|
157+
| `doc/dependency-injection-configuration.md` | Registering any extension in NEON config (services, tags, autowiring) |
158+
| `doc/testing.md` | Writing tests for any extension — `RuleTestCase`, `TypeInferenceTestCase`, `assertType()` |
159+
| `doc/extension-types.md` | Don't know which extension type to use — navigation hub for all types |
160+
| `doc/backward-compatibility-promise.md` | Extending or implementing PHPStan classes/interfaces; checking `@api` tags |
161+
| `doc/extension-library.md` | Looking for existing extensions for a framework/library |
162+
163+
### Extension types — specific triggers
164+
165+
| File | Read when... |
166+
|------|-------------|
167+
| `doc/rules.md` | Writing a custom rule (`Rule` interface), using `RuleErrorBuilder`, working with virtual nodes |
168+
| `doc/collectors.md` | Rule needs data from the entire codebase (unused code detection, cross-file analysis) |
169+
| `doc/restricted-usage-extensions.md` | Forbidding method/function/class/property/constant usage from certain contexts (simpler than full rules) |
170+
| `doc/class-reflection-extensions.md` | Class uses magic `__get`/`__set`/`__call` — need to teach PHPStan about dynamic properties/methods |
171+
| `doc/dynamic-return-type-extensions.md` | Return type depends on arguments and generics/conditional PHPDoc are not sufficient |
172+
| `doc/dynamic-throw-type-extensions.md` | Thrown exception depends on arguments |
173+
| `doc/type-specifying-extensions.md` | Custom assertion/`is_*()` function and PHPStan doesn't recognize type narrowing; `@phpstan-assert` not sufficient |
174+
| `doc/closure-extensions.md` | Closure parameter types or `$this` depend on surrounding context |
175+
| `doc/custom-phpdoc-types.md` | Creating a custom PHPDoc utility type (`TypeNodeResolverExtension`) |
176+
| `doc/allowed-subtypes.md` | Implementing sealed classes — restricting which classes can extend a given class/interface |
177+
| `doc/always-read-written-properties.md` | PHPStan reports property as unused but it's accessed via reflection/magic |
178+
| `doc/always-used-class-constants.md` | PHPStan reports constant as unused but it's accessed via reflection |
179+
| `doc/always-used-methods.md` | PHPStan reports method as unused but it's called via reflection/magic |
180+
| `doc/custom-deprecations.md` | Using custom deprecation attributes (not standard `@deprecated`) |
181+
| `doc/error-formatters.md` | Creating a custom output format for PHPStan errors |
182+
| `doc/ignore-error-extensions.md` | Conditionally ignoring errors based on context (scope, node, error type) |
183+
| `doc/result-cache-meta-extensions.md` | Extension depends on external data and needs custom cache invalidation |

0 commit comments

Comments
 (0)