Skip to content

Latest commit

 

History

History
183 lines (118 loc) · 16.6 KB

File metadata and controls

183 lines (118 loc) · 16.6 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

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.

Commands

composer phpstan          # Run static analysis (level 8)
composer tester           # Run all tests
vendor/bin/tester tests/SomeTest.phpt -s   # Run a single test

Architecture

  • src/ — Extension source code, PSR-4 autoloaded under Nette\PHPStan\ namespace
  • src/Tester/TypeAssert.php — Reusable type inference testing helper for Nette Tester (used by other Nette packages)
  • extension.neon — Entry point, includes extension-php.neon and extension-nette.neon, auto-included by phpstan/extension-installer
  • extension-php.neon — Generic PHP-level extensions (RemoveFailingReturnType, ClosureTypeCheckIgnore)
  • extension-nette.neon — All Nette package extensions (component-model, forms, schema, tester, utils), separated by comments
  • phpstan.neon — Self-analysis config (level 8, analyses src/ and tests/)

How extensions are registered

Each extension class is registered as a service in NEON with the appropriate tag. Common tags:

  • phpstan.rules.rule — custom rules
  • phpstan.collector — collectors
  • phpstan.broker.expressionTypeResolverExtension — expression type resolution (runs before all dynamic extensions)
  • phpstan.broker.dynamicFunctionReturnTypeExtension — dynamic function return types
  • phpstan.broker.dynamicMethodReturnTypeExtension — dynamic instance method return types
  • phpstan.broker.dynamicStaticMethodReturnTypeExtension — dynamic static method return types
  • phpstan.ignoreErrorExtension — conditional error suppression
  • phpstan.broker.propertiesClassReflectionExtension — magic properties
  • phpstan.broker.methodsClassReflectionExtension — magic methods
  • phpstan.broker.typeSpecifyingExtension — type narrowing

Namespace conventions

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\.

ExpectArrayReturnTypeExtension

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 SchemaStructure; mixed/unknown → fallback to declared union. Config: extension-nette.neon.

ArrowFunctionVoidIgnoreExtension

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.

ClosureTypeCheckIgnoreExtension

ClosureTypeCheckIgnoreExtension (IgnoreErrorExtension) suppresses expr.resultUnused for the runtime type validation pattern (function(Type ...$p) {})(...$args). Config: extension-php.neon.

RemoveFailingReturnTypeExtension

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.

FalseToNullReturnTypeExtension

FalseToNullReturnTypeExtension (DynamicStaticMethodReturnTypeExtension) narrows the return type of Helpers::falseToNull() from mixed. It removes false from the argument type and adds null — e.g. string|falsestring|null, falsenull, types without false pass through unchanged. Config: extension-nette.neon.

StringsReturnTypeExtension

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.

ArraysInvokeTypeExtension

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.

HtmlMethodsClassReflectionExtension

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.

GetComponentReturnTypeExtension

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.

FormContainerReturnTypeExtension

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.

AssertTypeNarrowingExtension

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.

MapperTypeResolver (Assets)

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.

GetMapperReturnTypeExtension

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.

MapperGetAssetExtension

MapperGetAssetExtension (DynamicMethodReturnTypeExtension) narrows return type of FilesystemMapper::getAsset() and ViteMapper::getAsset() from Asset to the specific asset class based on file extension (e.g. .jpgImageAsset, .jsScriptAsset). 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.

RegistryGetAssetExtension

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.

TableRowTypeResolver

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.

ExplorerTableReturnTypeExtension

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.

ActiveRowRelatedReturnTypeExtension

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.

ActiveRowRefReturnTypeExtension

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.

Testing

Tests use Nette Tester (not PHPUnit). Test files are .phpt in tests/ with data files in tests/data/.

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.

Workflow After Creating a New Extension

After every new extension is created, always perform these steps:

  1. Write tests — Create a .phpt test file in tests/ with corresponding data files in tests/data/. Run composer tester to verify.
  2. Update CLAUDE.md — Add a new ### section describing the extension (type, what it does, config file) following the existing format.
  3. Update readme.md — Add the extension to the appropriate section in the readme so users know about it.

Related Repositories

  • PHPStan source codeW:\libs.3rd\phpstan

doc/ Directory Reference

Read the relevant documentation files before writing code on PHPStan extensions.

Foundation (read as prerequisites)

File Read when...
doc/core-concepts.md First contact with PHPStan extension development; unsure where to start
doc/abstract-syntax-tree.md Writing a custom rule — need to pick the right AST node for getNodeType()
doc/scope.md Getting expression types via $scope->getType(), determining context (class, method, namespace)
doc/type-system.md Creating, comparing, or combining types; using isSuperTypeOf() or TypeCombinator
doc/trinary-logic.md Working with isSuperTypeOf() results (returns TrinaryLogic, not bool)
doc/reflection.md Need introspection of classes/methods/properties; reading PHPDoc via FileTypeMapper

Infrastructure

File Read when...
doc/dependency-injection-configuration.md Registering any extension in NEON config (services, tags, autowiring)
doc/testing.md Writing tests for any extension — RuleTestCase, TypeInferenceTestCase, assertType()
doc/extension-types.md Don't know which extension type to use — navigation hub for all types
doc/backward-compatibility-promise.md Extending or implementing PHPStan classes/interfaces; checking @api tags
doc/extension-library.md Looking for existing extensions for a framework/library

Extension types — specific triggers

File Read when...
doc/rules.md Writing a custom rule (Rule interface), using RuleErrorBuilder, working with virtual nodes
doc/collectors.md Rule needs data from the entire codebase (unused code detection, cross-file analysis)
doc/restricted-usage-extensions.md Forbidding method/function/class/property/constant usage from certain contexts (simpler than full rules)
doc/class-reflection-extensions.md Class uses magic __get/__set/__call — need to teach PHPStan about dynamic properties/methods
doc/dynamic-return-type-extensions.md Return type depends on arguments and generics/conditional PHPDoc are not sufficient
doc/dynamic-throw-type-extensions.md Thrown exception depends on arguments
doc/type-specifying-extensions.md Custom assertion/is_*() function and PHPStan doesn't recognize type narrowing; @phpstan-assert not sufficient
doc/closure-extensions.md Closure parameter types or $this depend on surrounding context
doc/custom-phpdoc-types.md Creating a custom PHPDoc utility type (TypeNodeResolverExtension)
doc/allowed-subtypes.md Implementing sealed classes — restricting which classes can extend a given class/interface
doc/always-read-written-properties.md PHPStan reports property as unused but it's accessed via reflection/magic
doc/always-used-class-constants.md PHPStan reports constant as unused but it's accessed via reflection
doc/always-used-methods.md PHPStan reports method as unused but it's called via reflection/magic
doc/custom-deprecations.md Using custom deprecation attributes (not standard @deprecated)
doc/error-formatters.md Creating a custom output format for PHPStan errors
doc/ignore-error-extensions.md Conditionally ignoring errors based on context (scope, node, error type)
doc/result-cache-meta-extensions.md Extension depends on external data and needs custom cache invalidation