├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer-require-checker.json ├── composer.json ├── docs ├── caching.md ├── implementing_custom_types.md ├── native_adapters.md ├── php_doc_properties_and_methods.md └── types.md ├── infection.json5.dist ├── src ├── AliasReflection.php ├── Annotated │ ├── CustomTypeResolver.php │ ├── CustomTypeResolvers.php │ ├── NullCustomTypeResolver.php │ └── TypeContext.php ├── AttributeReflection.php ├── Cache │ └── FreshCache.php ├── ClassConstantReflection.php ├── ClassReflection.php ├── Collection.php ├── ConstantReflection.php ├── Deprecation.php ├── Exception │ ├── DeclarationNotFound.php │ ├── FileIsNotReadable.php │ └── ReflectionException.php ├── FunctionReflection.php ├── Internal │ ├── Annotated │ │ ├── AnnotatedDeclarations.php │ │ ├── AnnotatedDeclarationsDiscoverer.php │ │ └── NullAnnotatedDeclarationsDiscoverer.php │ ├── Cache │ │ ├── Cache.php │ │ ├── InMemoryPsr16Cache.php │ │ └── InvalidCacheKey.php │ ├── CompleteReflection │ │ ├── CleanUpInternallyDefined.php │ │ ├── CompleteEnum.php │ │ ├── CopyPromotedParameterToProperty.php │ │ ├── RemoveCode.php │ │ ├── RemoveContext.php │ │ ├── SetAttributeRepeated.php │ │ ├── SetClassCloneable.php │ │ ├── SetInterfaceMethodAbstract.php │ │ ├── SetParameterIndex.php │ │ ├── SetParameterOptional.php │ │ ├── SetReadonlyClassPropertyReadonly.php │ │ ├── SetStringableInterface.php │ │ └── SetTemplateIndex.php │ ├── ConstantExpression │ │ ├── ArrayElement.php │ │ ├── ArrayExpression.php │ │ ├── ArrayFetch.php │ │ ├── ArrayFetchCoalesce.php │ │ ├── BinaryOperation.php │ │ ├── ClassConstantFetch.php │ │ ├── CompilationContext.php │ │ ├── ConstantFetch.php │ │ ├── Expression.php │ │ ├── Instantiation.php │ │ ├── MagicClassInTrait.php │ │ ├── ParentClass.php │ │ ├── ParentClassInTrait.php │ │ ├── SelfClass.php │ │ ├── SelfClassInTrait.php │ │ ├── Ternary.php │ │ ├── UnaryOperation.php │ │ ├── Value.php │ │ └── Values.php │ ├── Context │ │ ├── Context.php │ │ ├── ContextProvider.php │ │ └── ContextVisitor.php │ ├── Data.php │ ├── Data │ │ ├── AliasTypeKey.php │ │ ├── ArgumentsExpressionKey.php │ │ ├── AttributeClassNameKey.php │ │ ├── AttributesKey.php │ │ ├── BackingTypeKey.php │ │ ├── BackingValueExpressionKey.php │ │ ├── BoolKeys.php │ │ ├── ChangeDetectorKey.php │ │ ├── ClassKind.php │ │ ├── ClassKindKey.php │ │ ├── CodeKey.php │ │ ├── ConstraintKey.php │ │ ├── ContextKey.php │ │ ├── DeclaringClassIdKey.php │ │ ├── DefaultValueExpressionKey.php │ │ ├── DeprecationKey.php │ │ ├── FileKey.php │ │ ├── InterfacesKey.php │ │ ├── LocationKey.php │ │ ├── NamedDataKeys.php │ │ ├── NamespaceKey.php │ │ ├── ParameterIndexKey.php │ │ ├── ParentsKey.php │ │ ├── PassedBy.php │ │ ├── PassedByKey.php │ │ ├── PhpDocKey.php │ │ ├── PhpExtensionKey.php │ │ ├── ThrowsTypeKey.php │ │ ├── TraitMethodAlias.php │ │ ├── TraitMethodAliasesKey.php │ │ ├── TraitMethodPrecedenceKey.php │ │ ├── TypeData.php │ │ ├── TypeDataKeys.php │ │ ├── UnresolvedInterfacesKey.php │ │ ├── UnresolvedParentKey.php │ │ ├── UnresolvedTraitsKey.php │ │ ├── UsePhpDocsKey.php │ │ ├── ValueExpressionKey.php │ │ ├── VarianceKey.php │ │ ├── Visibility.php │ │ └── VisibilityKey.php │ ├── Hook │ │ ├── ClassHook.php │ │ ├── ConstantHook.php │ │ ├── FunctionHook.php │ │ ├── HookPriorities.php │ │ └── Hooks.php │ ├── Inheritance │ │ ├── ClassInheritance.php │ │ ├── MethodInheritance.php │ │ ├── PropertyInheritance.php │ │ ├── ResolveClassInheritance.php │ │ ├── TypeInheritance.php │ │ └── TypeResolvers.php │ ├── Misc │ │ └── NonSerializable.php │ ├── NativeAdapter │ │ ├── AttributeAdapter.php │ │ ├── ClassAdapter.php │ │ ├── ClassConstantAdapter.php │ │ ├── EnumAdapter.php │ │ ├── EnumBackedCaseAdapter.php │ │ ├── EnumUnitCaseAdapter.php │ │ ├── FunctionAdapter.php │ │ ├── IntersectionTypeAdapter.php │ │ ├── MethodAdapter.php │ │ ├── NamedTypeAdapter.php │ │ ├── NativeTraitInfo.php │ │ ├── NativeTraitInfoKey.php │ │ ├── NonConvertableType.php │ │ ├── ParameterAdapter.php │ │ ├── PropertyAdapter.php │ │ ├── ToNativeTypeConverter.php │ │ └── UnionTypeAdapter.php │ ├── NativeReflector │ │ ├── DefinedConstantReflector.php │ │ └── NativeReflectionBasedReflector.php │ ├── PhpDoc │ │ ├── AlwaysTrimmingConstExprParser.php │ │ ├── InvalidPhpDocType.php │ │ ├── NamedObjectTypeDestructurizer.php │ │ ├── PhpDoc.php │ │ ├── PhpDocConstantExpressionCompiler.php │ │ ├── PhpDocParser.php │ │ ├── PhpDocReflector.php │ │ ├── PhpDocTagPrioritizer.php │ │ ├── PhpDocTypeReflector.php │ │ └── PrefixBasedPhpDocTagPrioritizer.php │ ├── PhpParser │ │ ├── CodeReflector.php │ │ ├── CollectIdReflectorsVisitor.php │ │ ├── ConstantExpressionCompiler.php │ │ ├── ConstantExpressionTypeReflector.php │ │ ├── FixNodeLocationVisitor.php │ │ ├── GeneratorVisitor.php │ │ ├── NameParser.php │ │ ├── NodeContextAttribute.php │ │ ├── NodeReflector.php │ │ ├── PhpParserChecker.php │ │ └── UnresolvedConstantType.php │ ├── Type │ │ └── IsNativeTypeNullable.php │ └── functions.php ├── KeyIsNotDefined.php ├── Location.php ├── Locator │ ├── AnonymousLocator.php │ ├── ComposerLocator.php │ ├── ConstantLocator.php │ ├── FileAnonymousLocator.php │ ├── Locators.php │ ├── NamedClassLocator.php │ ├── NamedFunctionLocator.php │ ├── NativeReflectionClassLocator.php │ ├── NativeReflectionFunctionLocator.php │ ├── NoSymfonyPolyfillLocator.php │ ├── OnlyLoadedClassLocator.php │ ├── Resource.php │ └── ScannedResourceLocator.php ├── MethodReflection.php ├── ModifierKind.php ├── ParameterReflection.php ├── PropertyReflection.php ├── ReflectionCollections.php ├── TemplateReflection.php ├── TypeKind.php └── TyphoonReflector.php └── tools ├── composer-require-checker ├── composer.json └── composer.lock ├── composer-unused ├── composer.json └── composer.lock ├── infection ├── composer.json └── composer.lock └── psalm ├── composer.json └── composer.lock /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.4.4] 2024-08-18 9 | 10 | ### Fixed 11 | 12 | - Resolve templates in type arguments of inherited parents and interfaces. 13 | 14 | ## [0.4.3] 2024-08-06 15 | 16 | ### Added 17 | 18 | - Add `ConstantReflection` and `TyphoonReflector::reflectConstant()`. 19 | 20 | ## [0.4.2] 2024-08-05 21 | 22 | ### Added 23 | 24 | - Add `AttributeReflection::evaluate()` to emphasize the risk of runtime errors. 25 | - Add `AttributeReflection::evaluateArguments()` to emphasize the risk of runtime errors. 26 | - Add `ClassConstantReflection::evaluate()` to emphasize the risk of runtime errors. 27 | - Add `ParameterReflection::evaluateDefault()` to emphasize the risk of runtime errors. 28 | - Add `PropertyReflection::evaluateDefault()` to emphasize the risk of runtime errors. 29 | 30 | ### Deprecated 31 | 32 | - Deprecate calling `AttributeReflection::newInstance()` in favor of `evaluate()`. 33 | - Deprecate calling `AttributeReflection::arguments()` in favor of `evaluateArguments()`. 34 | - Deprecate calling `ClassConstantReflection::value()` in favor of `evaluate()`. 35 | - Deprecate calling `ParameterReflection::defaultValue()` in favor of `evaluateDefault()`. 36 | - Deprecate calling `PropertyReflection::defaultValue()` in favor of `evaluateDefault()`. 37 | 38 | ## [0.4.1] 2024-08-05 39 | 40 | ### Fixed 41 | 42 | - Reflect trait @use PHPDoc in classes without a class-level PHPDoc. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Valentin Udaltsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typhoon Reflection 2 | 3 | [![PHP Version Requirement](https://img.shields.io/packagist/dependency-v/typhoon/reflection/php)](https://packagist.org/packages/typhoon/reflection) 4 | [![GitHub Release](https://img.shields.io/github/v/release/typhoon-php/reflection)](https://github.com/typhoon-php/reflection/releases) 5 | [![Psalm Level](https://shepherd.dev/github/typhoon-php/reflection/level.svg)](https://shepherd.dev/github/typhoon-php/reflection) 6 | [![Psalm Type Coverage](https://shepherd.dev/github/typhoon-php/reflection/coverage.svg)](https://shepherd.dev/github/typhoon-php/reflection) 7 | [![Code Coverage](https://codecov.io/gh/typhoon-php/reflection/branch/0.4.x/graph/badge.svg)](https://codecov.io/gh/typhoon-php/reflection/tree/0.4.x) 8 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Ftyphoon-php%2Freflection%2F0.4.x)](https://dashboard.stryker-mutator.io/reports/github.com/typhoon-php/reflection/0.4.x) 9 | 10 | Typhoon Reflection is an alternative to [native PHP Reflection](https://www.php.net/manual/en/book.reflection.php). It 11 | is: 12 | 13 | - static (does not run or autoload reflected code), 14 | - fast (due to lazy loading and caching), 15 | - [fully compatible with native reflection](reflection/native_adapters.md), 16 | - supports most of the Psalm and PHPStan phpDoc types, 17 | - can resolve templates, 18 | - does not leak memory and can be safely used 19 | with [zend.enable_gc=0](https://www.php.net/manual/en/info.configuration.php#ini.zend.enable-gc). 20 | 21 | ## Installation 22 | 23 | ``` 24 | composer require typhoon/reflection typhoon/phpstorm-reflection-stubs 25 | ``` 26 | 27 | `typhoon/phpstorm-reflection-stubs` is a bridge for `jetbrains/phpstorm-stubs`. Without this package internal classes 28 | and functions are reflected from native reflection without templates. 29 | 30 | ## Basic Usage 31 | 32 | ```php 33 | use Typhoon\Reflection\TyphoonReflector; 34 | use Typhoon\Type\types; 35 | use function Typhoon\Type\stringify; 36 | 37 | /** 38 | * @template TTag of non-empty-string 39 | */ 40 | final readonly class Article 41 | { 42 | /** 43 | * @param list $tags 44 | */ 45 | public function __construct( 46 | private array $tags, 47 | ) {} 48 | } 49 | 50 | $reflector = TyphoonReflector::build(); 51 | $class = $reflector->reflectClass(Article::class); 52 | $tagsType = $class->properties()['tags']->type(); 53 | 54 | var_dump(stringify($tagsType)); // "list" 55 | 56 | $templateResolver = $class->createTemplateResolver([ 57 | types::union( 58 | types::string('PHP'), 59 | types::string('Architecture'), 60 | ), 61 | ]); 62 | 63 | var_dump(stringify($tagsType->accept($templateResolver))); // "list<'PHP'|'Architecture'>" 64 | ``` 65 | 66 | ## Documentation 67 | 68 | - [Native reflection adapters](docs/native_adapters.md) 69 | - [Reflecting Types](docs/types.md) 70 | - [Reflecting PHPDoc properties and methods](docs/php_doc_properties_and_methods.md) 71 | - [Implementing custom types](docs/implementing_custom_types.md) 72 | - [Caching](docs/caching.md) 73 | 74 | Documentation is still far from being complete. Don't hesitate to create issues to clarify how things work. 75 | -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist" : [ 3 | "Composer\\Autoload\\ClassLoader", 4 | "PhpParser\\Node\\Scalar\\DNumber", 5 | "PhpParser\\Node\\Scalar\\LNumber", 6 | "Typhoon\\PhpStormReflectionStubs\\PhpStormStubsLocator" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typhoon/reflection", 3 | "description": "Static PHP reflection with phpDoc support", 4 | "license": "MIT", 5 | "type": "library", 6 | "authors": [ 7 | { 8 | "name": "Valentin Udaltsov", 9 | "email": "udaltsov.valentin@gmail.com" 10 | }, 11 | { 12 | "name": "Typhoon Team", 13 | "homepage": "https://github.com/orgs/typhoon-php/people" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.1", 18 | "ext-tokenizer": "*", 19 | "nikic/php-parser": "^4.18 || ^5.0", 20 | "phpstan/phpdoc-parser": "^1.21", 21 | "psr/simple-cache": "^3.0", 22 | "symfony/deprecation-contracts": "^3.0", 23 | "typhoon/change-detector": "^0.4.4", 24 | "typhoon/declaration-id": "^0.4", 25 | "typhoon/type": "^0.4.4", 26 | "typhoon/typed-map": "^0.4" 27 | }, 28 | "require-dev": { 29 | "bamarni/composer-bin-plugin": "^1.8.2", 30 | "dragon-code/benchmark": "^2.6", 31 | "ergebnis/composer-normalize": "^2.44.0", 32 | "friendsofphp/php-cs-fixer": "^3.64.0", 33 | "php-defer/php-defer": "^5.0", 34 | "phpstan/phpstan": "^1.12.6", 35 | "phpunit/phpunit": "^10.5.36", 36 | "phpyh/coding-standard": "^2.6.2", 37 | "symfony/var-dumper": "^6.4.11 || ^7.1.3", 38 | "typhoon/opcache": "^0.2.1", 39 | "typhoon/phpstorm-reflection-stubs": "^0.4.4" 40 | }, 41 | "conflict": { 42 | "typhoon/phpstorm-reflection-stubs": "<0.4.3" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Typhoon\\Reflection\\": "src/" 47 | }, 48 | "files": [ 49 | "src/Internal/functions.php" 50 | ] 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "Typhoon\\Reflection\\": "tests/" 55 | }, 56 | "files": [ 57 | "tests/functions.php" 58 | ] 59 | }, 60 | "config": { 61 | "allow-plugins": { 62 | "bamarni/composer-bin-plugin": true, 63 | "ergebnis/composer-normalize": true 64 | }, 65 | "platform": { 66 | "php": "8.1" 67 | }, 68 | "sort-packages": true 69 | }, 70 | "extra": { 71 | "bamarni-bin": { 72 | "bin-links": false, 73 | "forward-command": true, 74 | "target-directory": "tools" 75 | } 76 | }, 77 | "scripts": { 78 | "bump-dev": [ 79 | "@composer bump --dev-only", 80 | "@composer bin all bump --dev-only" 81 | ], 82 | "check-require": "tools/composer-require-checker/vendor/bin/composer-require-checker", 83 | "check-unused": "tools/composer-unused/vendor/bin/composer-unused", 84 | "fixcs": "php-cs-fixer fix --diff", 85 | "infection": "tools/infection/vendor/bin/infection --show-mutations", 86 | "pre-command-run": "mkdir -p var", 87 | "psalm": "tools/psalm/vendor/bin/psalm --show-info --no-diff --no-cache", 88 | "test": "phpunit" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/caching.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | 3 | By default, Typhoon Reflection uses in-memory LRU cache which should be enough for the majority of use cases. 4 | 5 | However, if you need persistent cache, you can use any [PSR-16](https://www.php-fig.org/psr/psr-16/) implementation. We 6 | highly recommend [Typhoon OPcache](https://github.com/typhoon-php/opcache). It stores values as opcacheable php files. 7 | 8 | ```php 9 | use Typhoon\Reflection\TyphoonReflector; 10 | use Typhoon\OPcache\TyphoonOPcache; 11 | 12 | $reflector = TyphoonReflector::build( 13 | cache: new TyphoonOPcache('path/to/cache/dir'), 14 | ); 15 | ``` 16 | 17 | To detect file changes during development, decorate your cache 18 | with [FreshCache](../../src/Reflection/Cache/FreshCache.php). 19 | 20 | ```php 21 | use Typhoon\Reflection\TyphoonReflector; 22 | use Typhoon\Reflection\Cache\FreshCache; 23 | use Typhoon\OPcache\TyphoonOPcache; 24 | 25 | $reflector = TyphoonReflector::build( 26 | cache: new FreshCache(new TyphoonOPcache('path/to/cache/dir')), 27 | ); 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/implementing_custom_types.md: -------------------------------------------------------------------------------- 1 | # Implementing custom types 2 | 3 | ```php 4 | use Typhoon\Reflection\Annotated\CustomTypeResolver; 5 | use Typhoon\Reflection\Annotated\TypeContext; 6 | use Typhoon\Reflection\TyphoonReflector; 7 | use Typhoon\Type\Type; 8 | use Typhoon\Type\types; 9 | use Typhoon\Type\TypeVisitor; 10 | use function Typhoon\Type\stringify; 11 | 12 | /** 13 | * @implements Type 14 | */ 15 | enum binary: string implements Type, CustomTypeResolver 16 | { 17 | case int16 = 'int16'; 18 | case int32 = 'int32'; 19 | case int64 = 'int64'; 20 | case float32 = 'float32'; 21 | case float64 = 'float64'; 22 | 23 | public function accept(TypeVisitor $visitor): mixed 24 | { 25 | /** 26 | * We need to suppress here, because Psalm does not support var annotations on enum cases yet ;( 27 | * @psalm-suppress InvalidArgument 28 | */ 29 | return match ($this) { 30 | self::int16 => $visitor->int($this, types::int(-32768), types::int(32767)), 31 | self::int32 => $visitor->int($this, types::int(-2147483648), types::int(2147483647)), 32 | self::int64 => $visitor->int($this, types::PHP_INT_MIN, types::PHP_INT_MAX), 33 | self::float32 => $visitor->float($this, types::float(-3.40282347E+38), types::float(3.40282347E+38)), 34 | self::float64 => $visitor->float($this, types::PHP_FLOAT_MIN, types::PHP_FLOAT_MAX), 35 | }; 36 | } 37 | 38 | public function resolveCustomType(string $name, array $typeArguments, TypeContext $context): ?Type 39 | { 40 | return self::tryFrom($name); 41 | } 42 | } 43 | 44 | final readonly class Message 45 | { 46 | /** 47 | * @param list $some16bitIntegers 48 | */ 49 | public function __construct( 50 | public array $some16bitIntegers, 51 | ) {} 52 | } 53 | 54 | $reflector = TyphoonReflector::build(customTypeResolvers: [binary::int16]); 55 | 56 | $propertyType = $reflector 57 | ->reflectClass(Message::class) 58 | ->properties()['some16bitIntegers'] 59 | ->type(); 60 | 61 | var_dump(stringify($propertyType)); // "list>" 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/php_doc_properties_and_methods.md: -------------------------------------------------------------------------------- 1 | # Reflecting PHPDoc properties and methods 2 | 3 | PHPDoc properties and methods are reflected as usual. To differentiate them from the native ones, use 4 | `isAnnotated()` and `isNative()` methods. 5 | 6 | PHPDoc methods support templates, variadic parameters and default values. 7 | 8 | ```php 9 | use Typhoon\Reflection\TyphoonReflector; 10 | use function Typhoon\Type\stringify; 11 | 12 | /** 13 | * @property-read non-empty-string $property 14 | * @method TReturn method(TArg $arg, string $default = __CLASS__, ...$variadic) 15 | */ 16 | final class A {} 17 | 18 | $reflector = TyphoonReflector::build(); 19 | 20 | $class = $reflector->reflectClass('A'); 21 | 22 | $property = $class->properties()['property']; 23 | 24 | var_dump($property->isAnnotated()); // true 25 | var_dump($property->isNative()); // false 26 | var_dump($property->isReadonly()); // true 27 | var_dump(stringify($property->type())); // "non-empty-string" 28 | 29 | $method = $class->methods()['method']; 30 | 31 | var_dump($method->isAnnotated()); // true 32 | var_dump($method->isNative()); // false 33 | var_dump(stringify($method->returnType())); // "TReturn#A::method()" 34 | var_dump(stringify($method->parameters()['arg']->type())); // "TArg#A::method()" 35 | var_dump($method->parameters()['default']->evaluateDefault()); // "A" 36 | var_dump($method->parameters()['variadic']->isVariadic()); // true 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/types.md: -------------------------------------------------------------------------------- 1 | # Reflecting types 2 | 3 | Typhoon can reflect 5 kinds of types (see the [TypeKind](../../src/Reflection/TypeKind.php) enum): 4 | - **Native** 5 | - **Tentative** ([PHP 8.1: Return types in PHP built-in class methods and deprecation notices](https://php.watch/versions/8.1/internal-method-return-types)) 6 | - **Annotated** (phpDocs by default) 7 | - **Inferred** (from constant value) 8 | - **Resolved** (`$annotated ?? $inferred ?? $tentative ?? $native ?? types::mixed`) 9 | 10 | By default `type()` and `returnType()` reflection methods return the `TypeKind::Resolved` type. To get any other type 11 | kind pass the corresponding `TypeKind` case. 12 | 13 | Here's an example: 14 | 15 | ```php 16 | use Typhoon\Reflection\TypeKind; 17 | use Typhoon\Reflection\TyphoonReflector; 18 | use function Typhoon\Type\stringify; 19 | 20 | final class A 21 | { 22 | const int CONSTANT = 1; 23 | 24 | /** 25 | * @var non-empty-string 26 | */ 27 | public string $property; 28 | } 29 | 30 | $reflector = TyphoonReflector::build(); 31 | 32 | $class = $reflector->reflectClass(A::class); 33 | 34 | $constant = $class->constants()['CONSTANT']; 35 | 36 | var_dump(stringify($constant->type())); // "1" 37 | var_dump($constant->type(TypeKind::Annotated)); // null 38 | var_dump(stringify($constant->type(TypeKind::Inferred))); // "1" 39 | var_dump(stringify($constant->type(TypeKind::Native))); // "int" 40 | 41 | $property = $class->properties()['property']; 42 | 43 | var_dump(stringify($property->type())); // "non-empty-string" 44 | var_dump(stringify($property->type(TypeKind::Annotated))); // "non-empty-string" 45 | var_dump($property->type(TypeKind::Inferred)); // null 46 | var_dump(stringify($property->type(TypeKind::Native))); // "string" 47 | 48 | $getIterator = $reflector 49 | ->reflectClass(IteratorAggregate::class) 50 | ->methods()['getIterator']; 51 | 52 | var_dump($getIterator->returnType(TypeKind::Native)); // null 53 | var_dump(stringify($getIterator->returnType(TypeKind::Tentative))); // "Traversable" 54 | ``` 55 | -------------------------------------------------------------------------------- /infection.json5.dist: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "vendor/infection/infection/resources/schema.json", 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "logs": { 9 | "text": "var/infection.log", 10 | "stryker": { 11 | "report": "/^\\d+\\.\\d+\\.x$/" 12 | }, 13 | }, 14 | "tmpDir": "var", 15 | "minCoveredMsi": 10, 16 | "mutators": { 17 | "@default": true, 18 | }, 19 | "testFrameworkOptions": "--testsuite=infection", 20 | } 21 | -------------------------------------------------------------------------------- /src/AliasReflection.php: -------------------------------------------------------------------------------- 1 | id = $id; 38 | $this->data = $data; 39 | } 40 | 41 | public function location(): ?Location 42 | { 43 | return $this->data[Data::Location]; 44 | } 45 | 46 | public function type(): Type 47 | { 48 | return $this->data[Data::AliasType]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Annotated/CustomTypeResolver.php: -------------------------------------------------------------------------------- 1 | $typeArguments 17 | */ 18 | public function resolveCustomType(string $unresolvedName, array $typeArguments, TypeContext $context): ?Type; 19 | } 20 | -------------------------------------------------------------------------------- /src/Annotated/CustomTypeResolvers.php: -------------------------------------------------------------------------------- 1 | $resolvers 16 | */ 17 | public function __construct( 18 | private readonly iterable $resolvers, 19 | ) {} 20 | 21 | public function resolveCustomType(string $unresolvedName, array $typeArguments, TypeContext $context): ?Type 22 | { 23 | foreach ($this->resolvers as $resolver) { 24 | $type = $resolver->resolveCustomType($unresolvedName, $typeArguments, $context); 25 | 26 | if ($type !== null) { 27 | return $type; 28 | } 29 | } 30 | 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Annotated/NullCustomTypeResolver.php: -------------------------------------------------------------------------------- 1 | $arguments 29 | */ 30 | public function resolveNameAsType(string $unresolvedName, array $arguments = []): Type; 31 | } 32 | -------------------------------------------------------------------------------- /src/AttributeReflection.php: -------------------------------------------------------------------------------- 1 | data = $data; 48 | } 49 | 50 | /** 51 | * @return non-negative-int 52 | */ 53 | public function index(): int 54 | { 55 | return $this->index; 56 | } 57 | 58 | /** 59 | * Attribute's class. 60 | * 61 | * @return non-empty-string Not class-string, because the class might not exist. Same is true for {@see \ReflectionAttribute::getName()}. 62 | */ 63 | public function className(): string 64 | { 65 | return $this->data[Data::AttributeClassName]; 66 | } 67 | 68 | /** 69 | * Attribute's class reflection. 70 | * 71 | * @return ClassReflection> 72 | */ 73 | public function class(): ClassReflection 74 | { 75 | /** @var ClassReflection> */ 76 | return $this->reflector->reflectClass($this->className()); 77 | } 78 | 79 | public function targetId(): NamedFunctionId|AnonymousFunctionId|ParameterId|NamedClassId|AnonymousClassId|ClassConstantId|MethodId|PropertyId 80 | { 81 | return $this->targetId; 82 | } 83 | 84 | public function target(): FunctionReflection|ClassReflection|ClassConstantReflection|PropertyReflection|MethodReflection|ParameterReflection|AliasReflection|TemplateReflection 85 | { 86 | return $this->reflector->reflect($this->targetId); 87 | } 88 | 89 | public function location(): ?Location 90 | { 91 | return $this->data[Data::Location]; 92 | } 93 | 94 | public function isRepeated(): bool 95 | { 96 | return $this->data[Data::AttributeRepeated]; 97 | } 98 | 99 | /** 100 | * This method returns the actual attribute's constructor arguments and thus might trigger autoloading or throw errors. 101 | */ 102 | public function evaluateArguments(): array 103 | { 104 | return $this->data[Data::ArgumentsExpression]->evaluate($this->reflector); 105 | } 106 | 107 | /** 108 | * @deprecated since 0.4.2 in favor of evaluateArguments() 109 | */ 110 | public function arguments(): array 111 | { 112 | trigger_deprecation('typhoon/reflection', '0.4.2', 'Calling %s is deprecated in favor of %s::evaluateArguments()', __METHOD__, self::class); 113 | 114 | return $this->evaluateArguments(); 115 | } 116 | 117 | /** 118 | * This method returns the actual attribute object and thus might trigger autoloading or throw errors. 119 | */ 120 | public function evaluate(): object 121 | { 122 | /** @psalm-suppress InvalidStringClass */ 123 | return new ($this->className())(...$this->evaluateArguments()); 124 | } 125 | 126 | /** 127 | * @deprecated since 0.4.2 in favor of evaluate() 128 | */ 129 | public function newInstance(): object 130 | { 131 | trigger_deprecation('typhoon/reflection', '0.4.2', 'Calling %s is deprecated in favor of %s::evaluate()', __METHOD__, self::class); 132 | 133 | return $this->evaluate(); 134 | } 135 | 136 | private ?AttributeAdapter $native = null; 137 | 138 | public function toNativeReflection(): \ReflectionAttribute 139 | { 140 | return $this->native ??= new AttributeAdapter($this); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Cache/FreshCache.php: -------------------------------------------------------------------------------- 1 | changed(); 29 | } 30 | 31 | public function get(string $key, mixed $default = null): mixed 32 | { 33 | $value = $this->cache->get($key, $default); 34 | 35 | return self::isStale($value) ? $default : $value; 36 | } 37 | 38 | public function set(string $key, mixed $value, null|\DateInterval|int $ttl = null): bool 39 | { 40 | return $this->cache->set($key, $value, $ttl); 41 | } 42 | 43 | public function delete(string $key): bool 44 | { 45 | return $this->cache->delete($key); 46 | } 47 | 48 | public function clear(): bool 49 | { 50 | return $this->cache->clear(); 51 | } 52 | 53 | /** 54 | * @param iterable $keys 55 | * @return \Generator 56 | */ 57 | public function getMultiple(iterable $keys, mixed $default = null): iterable 58 | { 59 | foreach ($this->cache->getMultiple($keys) as $key => $value) { 60 | yield $key => self::isStale($value) ? $default : $value; 61 | } 62 | } 63 | 64 | public function setMultiple(iterable $values, null|\DateInterval|int $ttl = null): bool 65 | { 66 | return $this->cache->setMultiple($values, $ttl); 67 | } 68 | 69 | public function deleteMultiple(iterable $keys): bool 70 | { 71 | return $this->cache->deleteMultiple($keys); 72 | } 73 | 74 | public function has(string $key): bool 75 | { 76 | return $this->cache->get($key, $this) !== $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | 20 | * 21 | * @implements \IteratorAggregate 22 | */ 23 | final class Collection implements \ArrayAccess, \IteratorAggregate, \Countable 24 | { 25 | /** 26 | * @internal 27 | * @psalm-internal Typhoon\Reflection 28 | * @param array $values 29 | */ 30 | public function __construct( 31 | private readonly array $values, 32 | ) {} 33 | 34 | public function offsetExists(mixed $offset): bool 35 | { 36 | return isset($this->values[$offset]); 37 | } 38 | 39 | public function offsetGet(mixed $offset): mixed 40 | { 41 | return $this->values[$offset] ?? throw new KeyIsNotDefined($offset); 42 | } 43 | 44 | /** 45 | * @template TNewValue 46 | * @param callable(TValue, TKey): TNewValue $mapper 47 | * @return self 48 | */ 49 | public function map(callable $mapper): self 50 | { 51 | $values = []; 52 | 53 | foreach ($this->values as $name => $value) { 54 | $values[$name] = $mapper($value, $name); 55 | } 56 | 57 | return new self($values); 58 | } 59 | 60 | /** 61 | * @param callable(TValue, TKey): bool $filter 62 | * @return self 63 | */ 64 | public function filter(callable $filter): self 65 | { 66 | $values = []; 67 | 68 | foreach ($this->values as $name => $value) { 69 | if ($filter($value, $name)) { 70 | $values[$name] = $value; 71 | } 72 | } 73 | 74 | return new self($values); 75 | } 76 | 77 | /** 78 | * @return list 79 | */ 80 | public function keys(): array 81 | { 82 | return array_keys($this->values); 83 | } 84 | 85 | /** 86 | * @return ?TKey 87 | */ 88 | public function firstKey(): null|int|string 89 | { 90 | return array_key_first($this->values); 91 | } 92 | 93 | /** 94 | * @return ?TKey 95 | */ 96 | public function lastKey(): null|int|string 97 | { 98 | return array_key_last($this->values); 99 | } 100 | 101 | /** 102 | * @return ?TValue 103 | */ 104 | public function first(): mixed 105 | { 106 | return array_value_first($this->values); 107 | } 108 | 109 | /** 110 | * @return ?TValue 111 | */ 112 | public function last(): mixed 113 | { 114 | return array_value_last($this->values); 115 | } 116 | 117 | /** 118 | * @return self 119 | */ 120 | public function toIndexed(): self 121 | { 122 | return new self(array_values($this->values)); 123 | } 124 | 125 | /** 126 | * @return array 127 | */ 128 | public function toArray(): array 129 | { 130 | return $this->values; 131 | } 132 | 133 | /** 134 | * @return list 135 | */ 136 | public function toList(): array 137 | { 138 | return array_values($this->values); 139 | } 140 | 141 | /** 142 | * @param callable(TValue, TKey): bool $predicate 143 | */ 144 | public function any(callable $predicate): bool 145 | { 146 | foreach ($this->values as $name => $value) { 147 | if ($predicate($value, $name)) { 148 | return true; 149 | } 150 | } 151 | 152 | return false; 153 | } 154 | 155 | /** 156 | * @param callable(TValue, TKey): bool $predicate 157 | */ 158 | public function all(callable $predicate): bool 159 | { 160 | foreach ($this->values as $name => $value) { 161 | if (!$predicate($value, $name)) { 162 | return false; 163 | } 164 | } 165 | 166 | return true; 167 | } 168 | 169 | public function getIterator(): \Traversable 170 | { 171 | return new \ArrayIterator($this->values); 172 | } 173 | 174 | public function isEmpty(): bool 175 | { 176 | return $this->values === []; 177 | } 178 | 179 | /** 180 | * @return non-negative-int 181 | */ 182 | public function count(): int 183 | { 184 | return \count($this->values); 185 | } 186 | 187 | public function offsetSet(mixed $offset, mixed $value): never 188 | { 189 | throw new \BadMethodCallException(\sprintf('%s is immutable', self::class)); 190 | } 191 | 192 | public function offsetUnset(mixed $offset): never 193 | { 194 | throw new \BadMethodCallException(\sprintf('%s is immutable', self::class)); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/ConstantReflection.php: -------------------------------------------------------------------------------- 1 | id = $id; 42 | $this->data = $data; 43 | } 44 | 45 | /** 46 | * @return ?non-empty-string 47 | */ 48 | public function extension(): ?string 49 | { 50 | return $this->data[Data::PhpExtension]; 51 | } 52 | 53 | public function namespace(): string 54 | { 55 | return $this->data[Data::Namespace]; 56 | } 57 | 58 | public function changeDetector(): ChangeDetector 59 | { 60 | return $this->data[Data::ChangeDetector]; 61 | } 62 | 63 | public function location(): ?Location 64 | { 65 | return $this->data[Data::Location]; 66 | } 67 | 68 | public function isInternallyDefined(): bool 69 | { 70 | return $this->data[Data::InternallyDefined]; 71 | } 72 | 73 | /** 74 | * @return ?non-empty-string 75 | */ 76 | public function phpDoc(): ?string 77 | { 78 | return $this->data[Data::PhpDoc]?->getText(); 79 | } 80 | 81 | /** 82 | * This method returns the actual class constant's value and thus might trigger autoloading or throw errors. 83 | */ 84 | public function evaluate(): mixed 85 | { 86 | return $this->data[Data::ValueExpression]->evaluate($this->reflector); 87 | } 88 | 89 | /** 90 | * @return ($kind is TypeKind::Resolved ? Type : ?Type) 91 | */ 92 | public function type(TypeKind $kind = TypeKind::Resolved): ?Type 93 | { 94 | return $this->data[Data::Type]->get($kind); 95 | } 96 | 97 | public function isDeprecated(): bool 98 | { 99 | return $this->data[Data::Deprecation] !== null; 100 | } 101 | 102 | public function deprecation(): ?Deprecation 103 | { 104 | return $this->data[Data::Deprecation]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Deprecation.php: -------------------------------------------------------------------------------- 1 | describe()))); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Exception/FileIsNotReadable.php: -------------------------------------------------------------------------------- 1 | $templateNames 15 | * @param list $aliasNames 16 | */ 17 | public function __construct( 18 | public readonly array $templateNames = [], 19 | public readonly array $aliasNames = [], 20 | ) {} 21 | } 22 | -------------------------------------------------------------------------------- /src/Internal/Annotated/AnnotatedDeclarationsDiscoverer.php: -------------------------------------------------------------------------------- 1 | cache->get(self::key($id)); 30 | 31 | if ($value instanceof TypedMap) { 32 | return $value; 33 | } 34 | 35 | return null; 36 | } 37 | 38 | public function set(Id $id, TypedMap $data): void 39 | { 40 | $this->cache->set(self::key($id), $data); 41 | } 42 | 43 | private static function key(Id $id): string 44 | { 45 | return hash('xxh128', \sprintf('typhoon.reflection.%d.%d.%s', self::VERSION, \PHP_VERSION_ID, $id->encode())); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Internal/Cache/InMemoryPsr16Cache.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $values = []; 21 | 22 | public function get(string $key, mixed $default = null): mixed 23 | { 24 | self::validateKey($key); 25 | 26 | if (!\array_key_exists($key, $this->values)) { 27 | return $default; 28 | } 29 | 30 | $value = $this->values[$key]; 31 | unset($this->values[$key]); 32 | 33 | return $this->values[$key] = $value; 34 | } 35 | 36 | public function set(string $key, mixed $value, null|\DateInterval|int $ttl = null): bool 37 | { 38 | self::validateKey($key); 39 | 40 | unset($this->values[$key]); 41 | $this->values[$key] = $value; 42 | $this->evict(); 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * @psalm-suppress PossiblyUnusedReturnValue 49 | */ 50 | public function delete(string $key): bool 51 | { 52 | self::validateKey($key); 53 | 54 | unset($this->values[$key]); 55 | 56 | return true; 57 | } 58 | 59 | public function clear(): bool 60 | { 61 | $this->values = []; 62 | 63 | return true; 64 | } 65 | 66 | public function getMultiple(iterable $keys, mixed $default = null): iterable 67 | { 68 | $values = []; 69 | 70 | foreach ($keys as $key) { 71 | $values[$key] = $this->get($key, $default); 72 | } 73 | 74 | return $values; 75 | } 76 | 77 | public function setMultiple(iterable $values, null|\DateInterval|int $ttl = null): bool 78 | { 79 | foreach ($values as $key => $value) { 80 | \assert(\is_string($key), 'Cache key must be string'); 81 | self::validateKey($key); 82 | 83 | unset($this->values[$key]); 84 | $this->values[$key] = $value; 85 | } 86 | 87 | $this->evict(); 88 | 89 | return true; 90 | } 91 | 92 | public function deleteMultiple(iterable $keys): bool 93 | { 94 | foreach ($keys as $key) { 95 | $this->delete($key); 96 | } 97 | 98 | return true; 99 | } 100 | 101 | public function has(string $key): bool 102 | { 103 | return $this->get($key, $this) !== $this; 104 | } 105 | 106 | private function evict(): void 107 | { 108 | if (\count($this->values) > self::CAPACITY) { 109 | $this->values = \array_slice($this->values, -self::CAPACITY); 110 | } 111 | } 112 | 113 | private static function validateKey(string $key): void 114 | { 115 | if (preg_match('#[{}()/\\\@:]#', $key)) { 116 | throw new InvalidCacheKey($key); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Internal/Cache/InvalidCacheKey.php: -------------------------------------------------------------------------------- 1 | with(Data::Constants, array_map(self::cleanUp(...), $data[Data::Constants])) 59 | ->with(Data::Properties, array_map(self::cleanUp(...), $data[Data::Properties])) 60 | ->with(Data::Methods, array_map(self::cleanUpFunctionLike(...), $data[Data::Methods])); 61 | } 62 | 63 | private static function cleanUpFunctionLike(TypedMap $data): TypedMap 64 | { 65 | return self::cleanUp($data) 66 | ->with(Data::Parameters, array_map(self::cleanUp(...), $data[Data::Parameters])); 67 | } 68 | 69 | private static function cleanUp(TypedMap $data): TypedMap 70 | { 71 | return $data->without(Data::Location, Data::PhpDoc); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/CompleteEnum.php: -------------------------------------------------------------------------------- 1 | with(Data::NativeReadonly, true) 47 | ->with(Data::Type, new TypeData(types::string)) 48 | ->with(Data::Visibility, Visibility::Public); 49 | 50 | $methods['cases'] = (new TypedMap()) 51 | ->with(Data::Static, true) 52 | ->with(Data::Type, new TypeData(types::array, types::list($staticType))) 53 | ->with(Data::Visibility, Visibility::Public) 54 | ->with(Data::InternallyDefined, true); 55 | 56 | if ($backingType !== null) { 57 | $interfaces[\BackedEnum::class] = []; 58 | 59 | $properties['value'] = (new TypedMap()) 60 | ->with(Data::NativeReadonly, true) 61 | ->with(Data::Type, new TypeData($backingType)) 62 | ->with(Data::Visibility, Visibility::Public); 63 | 64 | $methods['from'] = $methods['cases'] 65 | ->with(Data::Type, new TypeData($staticType)) 66 | ->with(Data::Parameters, [ 67 | 'value' => TypedMap::one(Data::Type, new TypeData(types::arrayKey, $backingType)), 68 | ]); 69 | 70 | $methods['tryFrom'] = $methods['from'] 71 | ->with(Data::Type, new TypeData(types::nullable($staticType))); 72 | } 73 | 74 | return $data 75 | ->with(Data::UnresolvedInterfaces, $interfaces) 76 | ->with(Data::Properties, $properties) 77 | ->with(Data::Methods, $methods); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/CopyPromotedParameterToProperty.php: -------------------------------------------------------------------------------- 1 | $parameter) { 48 | if ($parameter[Data::Promoted]) { 49 | $parameters[$name] = $parameter->without(Data::NativeReadonly, Data::AnnotatedReadonly, Data::Visibility); 50 | $properties[$name] = $parameter->without(Data::DefaultValueExpression); 51 | } 52 | } 53 | 54 | return $data 55 | ->with(Data::Methods, [ 56 | ...$data[Data::Methods], 57 | '__construct' => $constructor->with(Data::Parameters, $parameters), 58 | ]) 59 | ->with(Data::Properties, $properties); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/RemoveCode.php: -------------------------------------------------------------------------------- 1 | without(Data::Code); 36 | } 37 | 38 | public function processFunction(NamedFunctionId|AnonymousFunctionId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 39 | { 40 | return $data->without(Data::Code); 41 | } 42 | 43 | public function processClass(NamedClassId|AnonymousClassId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 44 | { 45 | return $data->without(Data::Code); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/RemoveContext.php: -------------------------------------------------------------------------------- 1 | without(Data::Context); 36 | } 37 | 38 | public function processFunction(NamedFunctionId|AnonymousFunctionId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 39 | { 40 | return $data->without(Data::Context); 41 | } 42 | 43 | public function processClass(NamedClassId|AnonymousClassId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 44 | { 45 | return $data 46 | ->without(Data::Context) 47 | ->with(Data::Methods, array_map( 48 | static fn(TypedMap $data): TypedMap => $data->without(Data::Context), 49 | $data[Data::Methods], 50 | )); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetAttributeRepeated.php: -------------------------------------------------------------------------------- 1 | with(Data::Constants, array_map(self::processAttributes(...), $data[Data::Constants])) 40 | ->with(Data::Properties, array_map(self::processAttributes(...), $data[Data::Properties])) 41 | ->with(Data::Methods, array_map(self::processFunctionLike(...), $data[Data::Methods])); 42 | } 43 | 44 | private static function processFunctionLike(TypedMap $data): TypedMap 45 | { 46 | return self::processAttributes($data) 47 | ->with(Data::Parameters, array_map(self::processAttributes(...), $data[Data::Parameters])); 48 | } 49 | 50 | private static function processAttributes(TypedMap $data): TypedMap 51 | { 52 | $attributes = $data[Data::Attributes]; 53 | 54 | if ($attributes === []) { 55 | return $data; 56 | } 57 | 58 | $repeated = []; 59 | 60 | foreach ($attributes as $attribute) { 61 | $class = $attribute[Data::AttributeClassName]; 62 | $repeated[$class] = isset($repeated[$class]); 63 | } 64 | 65 | return $data->with(Data::Attributes, array_map( 66 | static fn(TypedMap $attribute): TypedMap => $attribute->with( 67 | Data::AttributeRepeated, 68 | $repeated[$attribute[Data::AttributeClassName]], 69 | ), 70 | $attributes, 71 | )); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetClassCloneable.php: -------------------------------------------------------------------------------- 1 | with(Data::Cloneable, true); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetInterfaceMethodAbstract.php: -------------------------------------------------------------------------------- 1 | with(Data::Methods, array_map( 36 | static fn(TypedMap $method): TypedMap => $method->with(Data::Abstract, true), 37 | $data[Data::Methods], 38 | )); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetParameterIndex.php: -------------------------------------------------------------------------------- 1 | with(Data::Methods, array_map(self::processParameters(...), $data[Data::Methods])); 39 | } 40 | 41 | private static function processParameters(TypedMap $data): TypedMap 42 | { 43 | return $data->with(Data::Parameters, array_map( 44 | static function (TypedMap $parameter): TypedMap { 45 | /** @var non-negative-int */ 46 | static $index = 0; 47 | 48 | return $parameter->with(Data::Index, $index++); 49 | }, 50 | $data[Data::Parameters], 51 | )); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetParameterOptional.php: -------------------------------------------------------------------------------- 1 | with(Data::Methods, array_map(self::processParameters(...), $data[Data::Methods])); 39 | } 40 | 41 | private static function processParameters(TypedMap $data): TypedMap 42 | { 43 | return $data->with(Data::Parameters, array_map( 44 | static fn(TypedMap $parameter): TypedMap => $parameter->with(Data::Optional, self::isOptional($parameter)), 45 | $data[Data::Parameters], 46 | )); 47 | } 48 | 49 | private static function isOptional(TypedMap $parameter): bool 50 | { 51 | return $parameter[Data::Optional] 52 | || $parameter[Data::DefaultValueExpression] 53 | || $parameter[Data::Variadic]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetReadonlyClassPropertyReadonly.php: -------------------------------------------------------------------------------- 1 | with(Data::Properties, array_map( 37 | static fn(TypedMap $property): TypedMap => $property->with(Data::NativeReadonly, true), 38 | $data[Data::Properties], 39 | )); 40 | } 41 | 42 | if ($data[Data::AnnotatedReadonly]) { 43 | $data = $data->with(Data::Properties, array_map( 44 | static fn(TypedMap $property): TypedMap => $property->with(Data::AnnotatedReadonly, true), 45 | $data[Data::Properties], 46 | )); 47 | } 48 | 49 | return $data; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetStringableInterface.php: -------------------------------------------------------------------------------- 1 | name === \Stringable::class || !isset($data[Data::Methods]['__toString'])) { 31 | return $data; 32 | } 33 | 34 | return $data->with(Data::UnresolvedInterfaces, [ 35 | ...$data[Data::UnresolvedInterfaces], 36 | \Stringable::class => [], 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Internal/CompleteReflection/SetTemplateIndex.php: -------------------------------------------------------------------------------- 1 | with(Data::Methods, array_map(self::processTemplates(...), $data[Data::Methods])); 40 | } 41 | 42 | private static function processTemplates(TypedMap $data): TypedMap 43 | { 44 | return $data->with(Data::Templates, array_map( 45 | static function (TypedMap $parameter): TypedMap { 46 | /** @var non-negative-int */ 47 | static $index = 0; 48 | 49 | return $parameter->with(Data::Index, $index++); 50 | }, 51 | $data[Data::Templates], 52 | )); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/ArrayElement.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ArrayExpression implements Expression 15 | { 16 | /** 17 | * @param non-empty-list $elements 18 | */ 19 | public function __construct( 20 | private readonly array $elements, 21 | ) {} 22 | 23 | public function recompile(CompilationContext $context): Expression 24 | { 25 | return new self(array_map( 26 | static fn(ArrayElement $element): ArrayElement => new ArrayElement( 27 | key: $element->key instanceof Expression ? $element->key->recompile($context) : $element->key, 28 | value: $element->value->recompile($context), 29 | ), 30 | $this->elements, 31 | )); 32 | } 33 | 34 | public function evaluate(?TyphoonReflector $reflector = null): mixed 35 | { 36 | $array = []; 37 | 38 | foreach ($this->elements as $element) { 39 | $value = $element->value->evaluate($reflector); 40 | 41 | if ($element->key === null) { 42 | $array[] = $value; 43 | 44 | continue; 45 | } 46 | 47 | if ($element->key === true) { 48 | /** @psalm-suppress InvalidOperand */ 49 | $array = [...$array, ...$value]; 50 | 51 | continue; 52 | } 53 | 54 | /** @psalm-suppress MixedArrayOffset */ 55 | $array[$element->key->evaluate($reflector)] = $value; 56 | } 57 | 58 | return $array; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/ArrayFetch.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ArrayFetch implements Expression 15 | { 16 | public function __construct( 17 | private readonly Expression $array, 18 | private readonly Expression $key, 19 | ) {} 20 | 21 | public function recompile(CompilationContext $context): Expression 22 | { 23 | return new self( 24 | array: $this->array->recompile($context), 25 | key: $this->key->recompile($context), 26 | ); 27 | } 28 | 29 | public function evaluate(?TyphoonReflector $reflector = null): mixed 30 | { 31 | /** @psalm-suppress MixedArrayAccess, MixedArrayOffset */ 32 | return $this->array->evaluate($reflector)[$this->key->evaluate($reflector)]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/ArrayFetchCoalesce.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ArrayFetchCoalesce implements Expression 15 | { 16 | public function __construct( 17 | private readonly Expression $array, 18 | private readonly Expression $key, 19 | private readonly Expression $default, 20 | ) {} 21 | 22 | public function recompile(CompilationContext $context): Expression 23 | { 24 | return new self( 25 | array: $this->array->recompile($context), 26 | key: $this->key->recompile($context), 27 | default: $this->default->recompile($context), 28 | ); 29 | } 30 | 31 | public function evaluate(?TyphoonReflector $reflector = null): mixed 32 | { 33 | /** @psalm-suppress MixedArrayOffset */ 34 | return $this->array->evaluate($reflector)[$this->key->evaluate($reflector)] ?? $this->default->evaluate($reflector); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/BinaryOperation.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class BinaryOperation implements Expression 15 | { 16 | public function __construct( 17 | private readonly Expression $left, 18 | private readonly Expression $right, 19 | private readonly string $operator, 20 | ) {} 21 | 22 | public function recompile(CompilationContext $context): Expression 23 | { 24 | return new self( 25 | left: $this->left->recompile($context), 26 | right: $this->right->recompile($context), 27 | operator: $this->operator, 28 | ); 29 | } 30 | 31 | public function evaluate(?TyphoonReflector $reflector = null): mixed 32 | { 33 | /** @psalm-suppress MixedOperand */ 34 | return match ($this->operator) { 35 | '&' => $this->left->evaluate($reflector) & $this->right->evaluate($reflector), 36 | '|' => $this->left->evaluate($reflector) | $this->right->evaluate($reflector), 37 | '^' => $this->left->evaluate($reflector) ^ $this->right->evaluate($reflector), 38 | '&&' => $this->left->evaluate($reflector) && $this->right->evaluate($reflector), 39 | '||' => $this->left->evaluate($reflector) || $this->right->evaluate($reflector), 40 | '??' => $this->left->evaluate($reflector) ?? $this->right->evaluate($reflector), 41 | '.' => $this->left->evaluate($reflector) . $this->right->evaluate($reflector), 42 | '/' => $this->left->evaluate($reflector) / $this->right->evaluate($reflector), 43 | '==' => $this->left->evaluate($reflector) == $this->right->evaluate($reflector), 44 | '>' => $this->left->evaluate($reflector) > $this->right->evaluate($reflector), 45 | '>=' => $this->left->evaluate($reflector) >= $this->right->evaluate($reflector), 46 | '===' => $this->left->evaluate($reflector) === $this->right->evaluate($reflector), 47 | 'and' => $this->left->evaluate($reflector) and $this->right->evaluate($reflector), 48 | 'or' => $this->left->evaluate($reflector) or $this->right->evaluate($reflector), 49 | 'xor' => $this->left->evaluate($reflector) xor $this->right->evaluate($reflector), 50 | '-' => $this->left->evaluate($reflector) - $this->right->evaluate($reflector), 51 | '%' => $this->left->evaluate($reflector) % $this->right->evaluate($reflector), 52 | '*' => $this->left->evaluate($reflector) * $this->right->evaluate($reflector), 53 | '!=' => $this->left->evaluate($reflector) != $this->right->evaluate($reflector), 54 | '!==' => $this->left->evaluate($reflector) !== $this->right->evaluate($reflector), 55 | '+' => $this->left->evaluate($reflector) + $this->right->evaluate($reflector), 56 | '**' => $this->left->evaluate($reflector) ** $this->right->evaluate($reflector), 57 | '<<' => $this->left->evaluate($reflector) << $this->right->evaluate($reflector), 58 | '>>' => $this->left->evaluate($reflector) >> $this->right->evaluate($reflector), 59 | '<' => $this->left->evaluate($reflector) < $this->right->evaluate($reflector), 60 | '<=' => $this->left->evaluate($reflector) <= $this->right->evaluate($reflector), 61 | '<=>' => $this->left->evaluate($reflector) <=> $this->right->evaluate($reflector), 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/ClassConstantFetch.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ClassConstantFetch implements Expression 15 | { 16 | public function __construct( 17 | public readonly Expression $class, 18 | private readonly Expression $name, 19 | ) {} 20 | 21 | /** 22 | * @return non-empty-string 23 | */ 24 | public function evaluateClass(?TyphoonReflector $reflector = null): string 25 | { 26 | $class = $this->class->evaluate($reflector); 27 | \assert(\is_string($class) && $class !== ''); 28 | 29 | return $class; 30 | } 31 | 32 | /** 33 | * @return non-empty-string 34 | */ 35 | public function evaluateName(?TyphoonReflector $reflector = null): string 36 | { 37 | $name = $this->name->evaluate($reflector); 38 | \assert(\is_string($name) && $name !== ''); 39 | 40 | return $name; 41 | } 42 | 43 | public function recompile(CompilationContext $context): Expression 44 | { 45 | return new self( 46 | class: $this->class->recompile($context), 47 | name: $this->name->recompile($context), 48 | ); 49 | } 50 | 51 | public function evaluate(?TyphoonReflector $reflector = null): mixed 52 | { 53 | $class = $this->evaluateClass($reflector); 54 | $name = $this->evaluateName($reflector); 55 | 56 | if ($name === 'class') { 57 | return $class; 58 | } 59 | 60 | if ($reflector === null) { 61 | return \constant($class . '::' . $name); 62 | } 63 | 64 | return $reflector->reflectClass($class)->constants()[$name]->evaluate(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/CompilationContext.php: -------------------------------------------------------------------------------- 1 | context->resolveConstantName($unresolvedName); 32 | } 33 | 34 | /** 35 | * @param non-empty-string $unresolvedName 36 | * @return non-empty-string 37 | */ 38 | public function resolveClassName(string $unresolvedName): string 39 | { 40 | return $this->context->resolveClassName($unresolvedName); 41 | } 42 | 43 | /** 44 | * @return Expression 45 | */ 46 | public function magicFile(): Expression 47 | { 48 | return Value::from($this->context->file ?? ''); 49 | } 50 | 51 | /** 52 | * @return Expression 53 | */ 54 | public function magicDir(): Expression 55 | { 56 | return Value::from($this->context->directory() ?? ''); 57 | } 58 | 59 | /** 60 | * @return Expression 61 | */ 62 | public function magicNamespace(): Expression 63 | { 64 | return Value::from($this->context->namespace()); 65 | } 66 | 67 | /** 68 | * @return Expression 69 | */ 70 | public function magicFunction(): Expression 71 | { 72 | $id = $this->context->currentId; 73 | 74 | if ($id instanceof NamedFunctionId) { 75 | return Value::from($id->name); 76 | } 77 | 78 | if ($id instanceof AnonymousFunctionId) { 79 | $namespace = $this->context->namespace(); 80 | 81 | if ($namespace === '') { 82 | return Value::from(self::ANONYMOUS_FUNCTION_NAME); 83 | } 84 | 85 | return Value::from($namespace . '\\' . self::ANONYMOUS_FUNCTION_NAME); 86 | } 87 | 88 | if ($id instanceof MethodId) { 89 | return Value::from($id->name); 90 | } 91 | 92 | return Value::from(''); 93 | } 94 | 95 | /** 96 | * @return Expression 97 | */ 98 | public function magicClass(): Expression 99 | { 100 | if ($this->context->self !== null) { 101 | // todo anonymous 102 | return Value::from($this->context->self->name ?? throw new \LogicException('anonymous')); 103 | } 104 | 105 | if ($this->context->trait !== null) { 106 | return new MagicClassInTrait($this->context->trait->name); 107 | } 108 | 109 | return Value::from(''); 110 | } 111 | 112 | /** 113 | * @return Expression 114 | */ 115 | public function magicTrait(): Expression 116 | { 117 | return Value::from($this->context->trait?->name ?? ''); 118 | } 119 | 120 | /** 121 | * @return Expression 122 | */ 123 | public function magicMethod(): Expression 124 | { 125 | $id = $this->context->currentId; 126 | 127 | if (!$id instanceof MethodId) { 128 | return Value::from(''); 129 | } 130 | 131 | return Value::from(\sprintf('%s::%s', $id->class->name ?? '', $id->name)); 132 | } 133 | 134 | /** 135 | * @return Expression 136 | */ 137 | public function self(): Expression 138 | { 139 | if ($this->context->self !== null) { 140 | return new SelfClass($this->context->self); 141 | } 142 | 143 | if ($this->context->trait !== null) { 144 | return new SelfClassInTrait($this->context->trait); 145 | } 146 | 147 | throw new \LogicException('Not in a class!'); 148 | } 149 | 150 | /** 151 | * @return Expression 152 | */ 153 | public function parent(): Expression 154 | { 155 | if ($this->context->parent !== null) { 156 | return new ParentClass($this->context->parent); 157 | } 158 | 159 | if ($this->context->trait !== null) { 160 | return ParentClassInTrait::Instance; 161 | } 162 | 163 | throw new \LogicException('No parent!'); 164 | } 165 | 166 | public function static(): never 167 | { 168 | throw new \LogicException('Unexpected static type usage in a constant expression'); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/ConstantFetch.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ConstantFetch implements Expression 16 | { 17 | /** 18 | * @param non-empty-string $namespacedName 19 | * @param ?non-empty-string $globalName 20 | */ 21 | public function __construct( 22 | private readonly string $namespacedName, 23 | private readonly ?string $globalName = null, 24 | ) {} 25 | 26 | /** 27 | * @return non-empty-string 28 | */ 29 | public function name(?TyphoonReflector $reflector = null): string 30 | { 31 | if (\defined($this->namespacedName)) { 32 | return $this->namespacedName; 33 | } 34 | 35 | if ($reflector !== null) { 36 | try { 37 | return $reflector->reflectConstant($this->namespacedName)->id->name; 38 | } catch (DeclarationNotFound) { 39 | } 40 | } 41 | 42 | if ($this->globalName === null) { 43 | throw new \LogicException(\sprintf('Constant %s is not defined', $this->namespacedName)); 44 | } 45 | 46 | if (\defined($this->globalName)) { 47 | return $this->globalName; 48 | } 49 | 50 | if ($reflector !== null) { 51 | try { 52 | return $reflector->reflectConstant($this->globalName)->id->name; 53 | } catch (DeclarationNotFound) { 54 | } 55 | } 56 | 57 | throw new \LogicException(\sprintf('Constants %s and %s are not defined', $this->namespacedName, $this->globalName)); 58 | } 59 | 60 | public function recompile(CompilationContext $context): Expression 61 | { 62 | return $this; 63 | } 64 | 65 | public function evaluate(?TyphoonReflector $reflector = null): mixed 66 | { 67 | if (\defined($this->namespacedName)) { 68 | return \constant($this->namespacedName); 69 | } 70 | 71 | if ($reflector !== null) { 72 | try { 73 | return $reflector->reflectConstant($this->namespacedName); 74 | } catch (DeclarationNotFound) { 75 | } 76 | } 77 | 78 | if ($this->globalName === null) { 79 | throw new \LogicException(\sprintf('Constant %s is not defined', $this->namespacedName)); 80 | } 81 | 82 | if (\defined($this->globalName)) { 83 | return \constant($this->globalName); 84 | } 85 | 86 | if ($reflector !== null) { 87 | try { 88 | return $reflector->reflectConstant($this->globalName); 89 | } catch (DeclarationNotFound) { 90 | } 91 | } 92 | 93 | throw new \LogicException(\sprintf('Constants %s and %s are not defined', $this->namespacedName, $this->globalName)); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/Expression.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function recompile(CompilationContext $context): self; 20 | 21 | /** 22 | * @return T 23 | */ 24 | public function evaluate(?TyphoonReflector $reflector = null): mixed; 25 | } 26 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/Instantiation.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Instantiation implements Expression 15 | { 16 | /** 17 | * @param array $arguments 18 | */ 19 | public function __construct( 20 | private readonly Expression $class, 21 | private readonly array $arguments, 22 | ) {} 23 | 24 | public function recompile(CompilationContext $context): Expression 25 | { 26 | return new self( 27 | class: $this->class->recompile($context), 28 | arguments: array_map( 29 | static fn(Expression $expression): Expression => $expression->recompile($context), 30 | $this->arguments, 31 | ), 32 | ); 33 | } 34 | 35 | public function evaluate(?TyphoonReflector $reflector = null): mixed 36 | { 37 | /** @psalm-suppress MixedMethodCall */ 38 | return new ($this->class->evaluate($reflector))(...array_map( 39 | static fn(Expression $expression): mixed => $expression->evaluate($reflector), 40 | $this->arguments, 41 | )); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/MagicClassInTrait.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class MagicClassInTrait implements Expression 15 | { 16 | public function __construct( 17 | private readonly string $trait, 18 | ) {} 19 | 20 | public function recompile(CompilationContext $context): Expression 21 | { 22 | $expression = $context->magicClass(); 23 | 24 | if ($expression instanceof self) { 25 | return $expression; 26 | } 27 | 28 | // even when trait is used in a class, __CONSTANT__ is not inlined 29 | return new self($expression->evaluate()); 30 | } 31 | 32 | public function evaluate(?TyphoonReflector $reflector = null): mixed 33 | { 34 | return $this->trait; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/ParentClass.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ParentClass implements Expression 16 | { 17 | public function __construct( 18 | private readonly NamedClassId $resolvedClass, 19 | ) {} 20 | 21 | public function recompile(CompilationContext $context): Expression 22 | { 23 | return $context->parent(); 24 | } 25 | 26 | public function evaluate(?TyphoonReflector $reflector = null): mixed 27 | { 28 | return $this->resolvedClass->name; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/ParentClassInTrait.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | enum ParentClassInTrait implements Expression 15 | { 16 | case Instance; 17 | 18 | public function recompile(CompilationContext $context): Expression 19 | { 20 | return $context->parent(); 21 | } 22 | 23 | public function evaluate(?TyphoonReflector $reflector = null): mixed 24 | { 25 | throw new \LogicException('Parent in trait!'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/SelfClass.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class SelfClass implements Expression 17 | { 18 | public function __construct( 19 | private readonly NamedClassId|AnonymousClassId $class, 20 | ) {} 21 | 22 | public function recompile(CompilationContext $context): Expression 23 | { 24 | return $context->self(); 25 | } 26 | 27 | public function evaluate(?TyphoonReflector $reflector = null): mixed 28 | { 29 | return $this->class->name ?? throw new \LogicException('Anonymous!'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/SelfClassInTrait.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class SelfClassInTrait implements Expression 16 | { 17 | public function __construct( 18 | private readonly NamedClassId $trait, 19 | ) {} 20 | 21 | public function recompile(CompilationContext $context): Expression 22 | { 23 | return $context->self(); 24 | } 25 | 26 | public function evaluate(?TyphoonReflector $reflector = null): mixed 27 | { 28 | return $this->trait->name; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/Ternary.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class Ternary implements Expression 15 | { 16 | public function __construct( 17 | private readonly Expression $condition, 18 | private readonly ?Expression $if, 19 | private readonly Expression $else, 20 | ) {} 21 | 22 | public function recompile(CompilationContext $context): Expression 23 | { 24 | return new self( 25 | condition: $this->condition->recompile($context), 26 | if: $this->if?->recompile($context), 27 | else: $this->else, 28 | ); 29 | } 30 | 31 | public function evaluate(?TyphoonReflector $reflector = null): mixed 32 | { 33 | if ($this->if === null) { 34 | return $this->condition->evaluate($reflector) ?: $this->else->evaluate($reflector); 35 | } 36 | 37 | return $this->condition->evaluate($reflector) ? $this->if->evaluate($reflector) : $this->else->evaluate($reflector); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/UnaryOperation.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class UnaryOperation implements Expression 15 | { 16 | /** 17 | * @param non-empty-string $operator 18 | */ 19 | public function __construct( 20 | private readonly Expression $expression, 21 | private readonly string $operator, 22 | ) {} 23 | 24 | public function recompile(CompilationContext $context): Expression 25 | { 26 | return new self( 27 | expression: $this->expression->recompile($context), 28 | operator: $this->operator, 29 | ); 30 | } 31 | 32 | public function evaluate(?TyphoonReflector $reflector = null): mixed 33 | { 34 | return match ($this->operator) { 35 | '+' => +$this->expression->evaluate($reflector), 36 | '-' => -$this->expression->evaluate($reflector), 37 | '!' => !$this->expression->evaluate($reflector), 38 | '~' => ~$this->expression->evaluate($reflector), 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/Value.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class Value implements Expression 16 | { 17 | /** 18 | * @param TValue $value 19 | */ 20 | private function __construct( 21 | public readonly mixed $value, 22 | ) {} 23 | 24 | /** 25 | * @template T 26 | * @param T $value 27 | * @return Expression 28 | */ 29 | public static function from(mixed $value): Expression 30 | { 31 | /** @var Expression */ 32 | return match ($value) { 33 | null => Values::Null, 34 | true => Values::True, 35 | false => Values::False, 36 | -1 => Values::MinusOne, 37 | 0 => Values::Zero, 38 | 1 => Values::One, 39 | '' => Values::EmptyString, 40 | [] => Values::EmptyArray, 41 | default => new self($value), 42 | }; 43 | } 44 | 45 | public function recompile(CompilationContext $context): Expression 46 | { 47 | return $this; 48 | } 49 | 50 | public function evaluate(?TyphoonReflector $reflector = null): mixed 51 | { 52 | return $this->value; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Internal/ConstantExpression/Values.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | enum Values implements Expression 15 | { 16 | case Null; 17 | case True; 18 | case False; 19 | case MinusOne; 20 | case Zero; 21 | case One; 22 | case EmptyString; 23 | case EmptyArray; 24 | 25 | public function recompile(CompilationContext $context): Expression 26 | { 27 | return $this; 28 | } 29 | 30 | public function evaluate(?TyphoonReflector $reflector = null): mixed 31 | { 32 | return match ($this) { 33 | self::Null => null, 34 | self::True => true, 35 | self::False => false, 36 | self::MinusOne => -1, 37 | self::Zero => 0, 38 | self::One => 1, 39 | self::EmptyString => '', 40 | self::EmptyArray => [], 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/Context/ContextProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum AliasTypeKey implements Key 16 | { 17 | case Key; 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Data/ArgumentsExpressionKey.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | enum ArgumentsExpressionKey implements OptionalKey 18 | { 19 | case Key; 20 | 21 | public function default(TypedMap $map): mixed 22 | { 23 | return Value::from([]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Internal/Data/AttributeClassNameKey.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | enum AttributeClassNameKey implements Key 15 | { 16 | case Key; 17 | } 18 | -------------------------------------------------------------------------------- /src/Internal/Data/AttributesKey.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | enum AttributesKey implements OptionalKey 16 | { 17 | case Key; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Data/BackingTypeKey.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | enum BackingTypeKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/BackingValueExpressionKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum BackingValueExpressionKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/BoolKeys.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum BoolKeys implements OptionalKey 16 | { 17 | case AttributeRepeated; 18 | case InternallyDefined; 19 | case IsAbstract; 20 | case AnnotatedFinal; 21 | case AnnotatedReadonly; 22 | case ReturnsReference; 23 | case EnumCase; 24 | case Generator; 25 | case NativeFinal; 26 | case NativeReadonly; 27 | case Promoted; 28 | case IsStatic; 29 | case Variadic; 30 | case Annotated; 31 | case Optional; 32 | case Cloneable; 33 | 34 | public function default(TypedMap $map): mixed 35 | { 36 | return false; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Internal/Data/ChangeDetectorKey.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | enum ChangeDetectorKey implements OptionalKey 18 | { 19 | case Key; 20 | 21 | public function default(TypedMap $map): mixed 22 | { 23 | return new InMemoryChangeDetector(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Internal/Data/ClassKind.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | enum ClassKindKey implements Key 15 | { 16 | case Key; 17 | } 18 | -------------------------------------------------------------------------------- /src/Internal/Data/CodeKey.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | enum CodeKey implements Key 15 | { 16 | case Key; 17 | } 18 | -------------------------------------------------------------------------------- /src/Internal/Data/ConstraintKey.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | enum ConstraintKey implements OptionalKey 18 | { 19 | case Key; 20 | 21 | public function default(TypedMap $map): mixed 22 | { 23 | return types::mixed; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Internal/Data/ContextKey.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum ContextKey implements Key 16 | { 17 | case Key; 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Data/DeclaringClassIdKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum DeclaringClassIdKey implements Key 17 | { 18 | case Key; 19 | } 20 | -------------------------------------------------------------------------------- /src/Internal/Data/DefaultValueExpressionKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum DefaultValueExpressionKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/DeprecationKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum DeprecationKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/FileKey.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum FileKey implements OptionalKey 16 | { 17 | case Key; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Data/InterfacesKey.php: -------------------------------------------------------------------------------- 1 | >> 15 | */ 16 | enum InterfacesKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/LocationKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum LocationKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/NamedDataKeys.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | enum NamedDataKeys implements OptionalKey 16 | { 17 | case Aliases; 18 | case Constants; 19 | case Methods; 20 | case Parameters; 21 | case Properties; 22 | case Templates; 23 | 24 | public function default(TypedMap $map): mixed 25 | { 26 | return []; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Data/NamespaceKey.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | enum NamespaceKey implements Key 15 | { 16 | case Key; 17 | } 18 | -------------------------------------------------------------------------------- /src/Internal/Data/ParameterIndexKey.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | enum ParameterIndexKey implements Key 15 | { 16 | case Key; 17 | } 18 | -------------------------------------------------------------------------------- /src/Internal/Data/ParentsKey.php: -------------------------------------------------------------------------------- 1 | >> 15 | */ 16 | enum ParentsKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/PassedBy.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum PassedByKey implements OptionalKey 16 | { 17 | case Key; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return PassedBy::Value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Data/PhpDocKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum PhpDocKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/PhpExtensionKey.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum PhpExtensionKey implements OptionalKey 16 | { 17 | case Key; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Data/ThrowsTypeKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum ThrowsTypeKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/TraitMethodAlias.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | enum TraitMethodAliasesKey implements OptionalKey 16 | { 17 | case Key; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return []; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Data/TraitMethodPrecedenceKey.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | enum TraitMethodPrecedenceKey implements OptionalKey 18 | { 19 | case Key; 20 | 21 | public function default(TypedMap $map): mixed 22 | { 23 | return []; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Internal/Data/TypeData.php: -------------------------------------------------------------------------------- 1 | native = $native; 37 | 38 | return $data; 39 | } 40 | 41 | public function withAnnotated(?Type $annotated): self 42 | { 43 | $data = clone $this; 44 | $data->annotated = $annotated; 45 | 46 | return $data; 47 | } 48 | 49 | public function withTentative(?Type $tentative): self 50 | { 51 | $data = clone $this; 52 | $data->tentative = $tentative; 53 | 54 | return $data; 55 | } 56 | 57 | /** 58 | * @param TypeVisitor $typeResolver 59 | */ 60 | public function inherit(TypeVisitor $typeResolver): self 61 | { 62 | return new self( 63 | native: $this->native?->accept($typeResolver), 64 | annotated: $this->annotated?->accept($typeResolver), 65 | tentative: $this->tentative?->accept($typeResolver), 66 | inferred: $this->inferred?->accept($typeResolver), 67 | ); 68 | } 69 | 70 | /** 71 | * @return ($kind is TypeKind::Resolved ? Type : ?Type) 72 | */ 73 | public function get(TypeKind $kind = TypeKind::Resolved): ?Type 74 | { 75 | return match ($kind) { 76 | TypeKind::Resolved => $this->annotated ?? $this->inferred ?? $this->tentative ?? $this->native ?? types::mixed, 77 | TypeKind::Native => $this->native, 78 | TypeKind::Tentative => $this->tentative, 79 | TypeKind::Inferred => $this->inferred, 80 | TypeKind::Annotated => $this->annotated, 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Internal/Data/TypeDataKeys.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum TypeDataKeys implements OptionalKey 16 | { 17 | case Type; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return new TypeData(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Data/UnresolvedInterfacesKey.php: -------------------------------------------------------------------------------- 1 | >> 15 | */ 16 | enum UnresolvedInterfacesKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/UnresolvedParentKey.php: -------------------------------------------------------------------------------- 1 | }> 15 | */ 16 | enum UnresolvedParentKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/UnresolvedTraitsKey.php: -------------------------------------------------------------------------------- 1 | >> 15 | */ 16 | enum UnresolvedTraitsKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/UsePhpDocsKey.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | enum UsePhpDocsKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return []; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/ValueExpressionKey.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum ValueExpressionKey implements Key 16 | { 17 | case Key; 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Data/VarianceKey.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | enum VarianceKey implements OptionalKey 17 | { 18 | case Key; 19 | 20 | public function default(TypedMap $map): mixed 21 | { 22 | return Variance::Invariant; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Data/Visibility.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum VisibilityKey implements OptionalKey 16 | { 17 | case Key; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Hook/ClassHook.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class Hooks implements \IteratorAggregate 21 | { 22 | /** 23 | * @var list 24 | */ 25 | private array $constantHooks = []; 26 | 27 | /** 28 | * @var list 29 | */ 30 | private array $functionHooks = []; 31 | 32 | /** 33 | * @var list 34 | */ 35 | private array $classHooks = []; 36 | 37 | /** 38 | * @param iterable> $hooks 39 | */ 40 | public function __construct(iterable $hooks = []) 41 | { 42 | foreach ($hooks as $hook) { 43 | if (is_iterable($hook)) { 44 | foreach ($hook as $level2Hook) { 45 | $this->add($level2Hook); 46 | } 47 | } else { 48 | $this->add($hook); 49 | } 50 | } 51 | 52 | usort($this->constantHooks, self::sort(...)); 53 | usort($this->functionHooks, self::sort(...)); 54 | usort($this->classHooks, self::sort(...)); 55 | } 56 | 57 | private function add(ConstantHook|FunctionHook|ClassHook $hook): void 58 | { 59 | if ($hook instanceof ConstantHook) { 60 | $this->constantHooks[] = $hook; 61 | } 62 | 63 | if ($hook instanceof FunctionHook) { 64 | $this->functionHooks[] = $hook; 65 | } 66 | 67 | if ($hook instanceof ClassHook) { 68 | $this->classHooks[] = $hook; 69 | } 70 | } 71 | 72 | private static function sort(ConstantHook|FunctionHook|ClassHook $a, ConstantHook|FunctionHook|ClassHook $b): int 73 | { 74 | return $b->priority() <=> $a->priority(); 75 | } 76 | 77 | public function process(ConstantId|NamedFunctionId|AnonymousFunctionId|NamedClassId|AnonymousClassId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 78 | { 79 | return match (true) { 80 | $id instanceof ConstantId => $this->processConstant($id, $data, $reflector), 81 | $id instanceof NamedFunctionId, 82 | $id instanceof AnonymousFunctionId => $this->processFunction($id, $data, $reflector), 83 | $id instanceof NamedClassId, 84 | $id instanceof AnonymousClassId => $this->processClass($id, $data, $reflector), 85 | }; 86 | } 87 | 88 | public function processConstant(ConstantId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 89 | { 90 | foreach ($this->constantHooks as $constantHook) { 91 | $data = $constantHook->processConstant($id, $data, $reflector); 92 | } 93 | 94 | return $data; 95 | } 96 | 97 | public function processFunction(NamedFunctionId|AnonymousFunctionId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 98 | { 99 | foreach ($this->functionHooks as $functionHook) { 100 | $data = $functionHook->processFunction($id, $data, $reflector); 101 | } 102 | 103 | return $data; 104 | } 105 | 106 | public function processClass(NamedClassId|AnonymousClassId $id, TypedMap $data, TyphoonReflector $reflector): TypedMap 107 | { 108 | foreach ($this->classHooks as $classHook) { 109 | $data = $classHook->processClass($id, $data, $reflector); 110 | } 111 | 112 | return $data; 113 | } 114 | 115 | public function getIterator(): \Generator 116 | { 117 | yield from $this->constantHooks; 118 | yield from $this->functionHooks; 119 | yield from $this->classHooks; 120 | } 121 | 122 | public function merge(self $hooks): self 123 | { 124 | $copy = clone $this; 125 | $copy->constantHooks = [...$copy->constantHooks, ...$hooks->constantHooks]; 126 | $copy->functionHooks = [...$copy->functionHooks, ...$hooks->functionHooks]; 127 | $copy->classHooks = [...$copy->classHooks, ...$hooks->classHooks]; 128 | usort($copy->constantHooks, self::sort(...)); 129 | usort($copy->functionHooks, self::sort(...)); 130 | usort($copy->classHooks, self::sort(...)); 131 | 132 | return $copy; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Internal/Inheritance/MethodInheritance.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | private array $parameters = []; 26 | 27 | private readonly TypeInheritance $returnType; 28 | 29 | private readonly TypeInheritance $throwsType; 30 | 31 | public function __construct() 32 | { 33 | $this->returnType = new TypeInheritance(); 34 | $this->throwsType = new TypeInheritance(); 35 | } 36 | 37 | public function applyOwn(TypedMap $data): void 38 | { 39 | $this->data = $data; 40 | 41 | foreach ($data[Data::Parameters] as $name => $parameter) { 42 | ($this->parameters[$name] = new PropertyInheritance())->applyOwn($parameter); 43 | } 44 | 45 | $this->returnType->applyOwn($data[Data::Type]); 46 | $this->throwsType->applyOwn(new TypeData(annotated: $data[Data::ThrowsType])); 47 | } 48 | 49 | /** 50 | * @param TypeVisitor $typeResolver 51 | */ 52 | public function applyUsed(TypedMap $data, TypeVisitor $typeResolver): void 53 | { 54 | if ($this->data !== null) { 55 | $usedParameters = array_values($data[Data::Parameters]); 56 | 57 | foreach (array_values($this->parameters) as $index => $parameter) { 58 | if (isset($usedParameters[$index])) { 59 | $parameter->applyInherited($usedParameters[$index], $typeResolver); 60 | } 61 | } 62 | 63 | $this->returnType->applyInherited($data[Data::Type], $typeResolver); 64 | 65 | return; 66 | } 67 | 68 | $this->data = $data; 69 | 70 | foreach ($data[Data::Parameters] as $name => $parameter) { 71 | ($this->parameters[$name] = new PropertyInheritance())->applyInherited($parameter, $typeResolver); 72 | } 73 | 74 | $this->returnType->applyInherited($data[Data::Type], $typeResolver); 75 | $this->throwsType->applyInherited(new TypeData(annotated: $data[Data::ThrowsType]), $typeResolver); 76 | } 77 | 78 | /** 79 | * @param TypeVisitor $typeResolver 80 | */ 81 | public function applyInherited(TypedMap $data, TypeVisitor $typeResolver): void 82 | { 83 | if ($data[Data::Visibility] === Visibility::Private) { 84 | return; 85 | } 86 | 87 | if ($this->data !== null) { 88 | $usedParameters = array_values($data[Data::Parameters]); 89 | 90 | foreach (array_values($this->parameters) as $index => $parameter) { 91 | if (isset($usedParameters[$index])) { 92 | $parameter->applyInherited($usedParameters[$index], $typeResolver); 93 | } 94 | } 95 | 96 | $this->returnType->applyInherited($data[Data::Type], $typeResolver); 97 | $this->throwsType->applyInherited(new TypeData(annotated: $data[Data::ThrowsType]), $typeResolver); 98 | 99 | return; 100 | } 101 | 102 | $this->data = $data; 103 | 104 | foreach ($data[Data::Parameters] as $name => $parameter) { 105 | ($this->parameters[$name] = new PropertyInheritance())->applyInherited($parameter, $typeResolver); 106 | } 107 | 108 | $this->returnType->applyInherited($data[Data::Type], $typeResolver); 109 | $this->throwsType->applyInherited(new TypeData(annotated: $data[Data::ThrowsType]), $typeResolver); 110 | } 111 | 112 | public function build(): ?TypedMap 113 | { 114 | return $this 115 | ->data 116 | ?->with(Data::Parameters, array_filter(array_map( 117 | static fn(PropertyInheritance $parameter): ?TypedMap => $parameter->build(), 118 | $this->parameters, 119 | ))) 120 | ->with(Data::Type, $this->returnType->build()) 121 | ->with(Data::ThrowsType, $this->throwsType->build()->annotated); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Internal/Inheritance/PropertyInheritance.php: -------------------------------------------------------------------------------- 1 | type = new TypeInheritance(); 28 | } 29 | 30 | public function applyOwn(TypedMap $data): void 31 | { 32 | $this->data = $data; 33 | $this->type->applyOwn($data[Data::Type]); 34 | } 35 | 36 | /** 37 | * @param TypeVisitor $typeResolver 38 | */ 39 | public function applyUsed(TypedMap $data, TypeVisitor $typeResolver): void 40 | { 41 | $this->data ??= $data; 42 | $this->type->applyInherited($data[Data::Type], $typeResolver); 43 | } 44 | 45 | /** 46 | * @param TypeVisitor $typeResolver 47 | */ 48 | public function applyInherited(TypedMap $data, TypeVisitor $typeResolver): void 49 | { 50 | if ($data[Data::Visibility] === Visibility::Private) { 51 | return; 52 | } 53 | 54 | $this->data ??= $data; 55 | $this->type->applyInherited($data[Data::Type], $typeResolver); 56 | } 57 | 58 | public function build(): ?TypedMap 59 | { 60 | return $this->data?->with(Data::Type, $this->type->build()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Internal/Inheritance/ResolveClassInheritance.php: -------------------------------------------------------------------------------- 1 | }> 21 | */ 22 | private array $inherited = []; 23 | 24 | private static function typesEqual(?Type $a, ?Type $b): bool 25 | { 26 | // Comparison operator == is intentionally used here. 27 | // Of course, we need a proper type comparator, 28 | // but for now simple equality check should do the job 90% of the time. 29 | return $a == $b; 30 | } 31 | 32 | public function applyOwn(TypeData $data): void 33 | { 34 | $this->own = $data; 35 | } 36 | 37 | /** 38 | * @param TypeVisitor $typeResolver 39 | */ 40 | public function applyInherited(TypeData $data, TypeVisitor $typeResolver): void 41 | { 42 | $this->inherited[] = [$data, $typeResolver]; 43 | } 44 | 45 | public function build(): TypeData 46 | { 47 | if ($this->own !== null) { 48 | if ($this->own->annotated !== null) { 49 | return $this->own; 50 | } 51 | 52 | $ownResolved = $this->own->get(); 53 | 54 | foreach ($this->inherited as [$inherited, $typeResolver]) { 55 | // If own type is different (weakened parameter type or strengthened return type), we want to keep it. 56 | // This should be compared according to variance with a proper type comparator, 57 | // but for now simple inequality check should do the job 90% of the time. 58 | if (!self::typesEqual($inherited->native, $this->own->native)) { 59 | continue; 60 | } 61 | 62 | // If inherited type resolves to equal own type, we should continue to look for something more interesting. 63 | if (self::typesEqual($inherited->get(), $ownResolved)) { 64 | continue; 65 | } 66 | 67 | return $inherited->withTentative(null)->inherit($typeResolver); 68 | } 69 | 70 | return $this->own; 71 | } 72 | 73 | \assert($this->inherited !== []); 74 | 75 | if (\count($this->inherited) !== 1) { 76 | foreach ($this->inherited as [$inherited, $typeResolver]) { 77 | // If inherited type resolves to its native type, we should continue to look for something more interesting. 78 | if (self::typesEqual($inherited->get(), $inherited->native)) { 79 | continue; 80 | } 81 | 82 | return $inherited->inherit($typeResolver); 83 | } 84 | } 85 | 86 | [$inherited, $typeResolver] = $this->inherited[0]; 87 | 88 | return $inherited->inherit($typeResolver); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Internal/Inheritance/TypeResolvers.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class TypeResolvers extends DefaultTypeVisitor 17 | { 18 | /** 19 | * @param iterable> $resolvers 20 | */ 21 | public function __construct( 22 | private readonly iterable $resolvers = [], 23 | ) {} 24 | 25 | protected function default(Type $type): mixed 26 | { 27 | foreach ($this->resolvers as $resolver) { 28 | $type = $type->accept($resolver); 29 | } 30 | 31 | return $type; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Internal/Misc/NonSerializable.php: -------------------------------------------------------------------------------- 1 | 24 | * @psalm-import-type Attributes from ReflectionCollections 25 | */ 26 | final class AttributeAdapter extends \ReflectionAttribute 27 | { 28 | public function __construct( 29 | private readonly AttributeReflection $reflection, 30 | ) {} 31 | 32 | /** 33 | * @param ?non-empty-string $name 34 | * @param Attributes $attributes 35 | * @return list<\ReflectionAttribute> 36 | */ 37 | public static function fromList(Collection $attributes, ?string $name, int $flags): array 38 | { 39 | if ($name !== null) { 40 | if ($flags & \ReflectionAttribute::IS_INSTANCEOF) { 41 | $attributes = $attributes->filter(static fn(AttributeReflection $attribute): bool => $attribute->class()->isInstanceOf($name)); 42 | } else { 43 | $attributes = $attributes->filter(static fn(AttributeReflection $attribute): bool => $attribute->className() === $name); 44 | } 45 | } 46 | 47 | return $attributes 48 | ->map(static fn(AttributeReflection $attribute): \ReflectionAttribute => $attribute->toNativeReflection()) 49 | ->toList(); 50 | } 51 | 52 | public function __toString(): string 53 | { 54 | $targetId = $this->reflection->targetId(); 55 | 56 | if ($targetId instanceof AnonymousFunctionId) { 57 | throw new \LogicException('Cannot resolve string representation of anonymous function'); 58 | } 59 | 60 | return (string) $targetId->reflect()->getAttributes()[$this->reflection->index()]; 61 | } 62 | 63 | public function getArguments(): array 64 | { 65 | return $this->reflection->evaluateArguments(); 66 | } 67 | 68 | public function getName(): string 69 | { 70 | return $this->reflection->className(); 71 | } 72 | 73 | public function getTarget(): int 74 | { 75 | /** @psalm-suppress ParadoxicalCondition */ 76 | return match ($this->reflection->targetId()::class) { 77 | NamedFunctionId::class, AnonymousFunctionId::class => \Attribute::TARGET_FUNCTION, 78 | NamedClassId::class, AnonymousClassId::class => \Attribute::TARGET_CLASS, 79 | ClassConstantId::class => \Attribute::TARGET_CLASS_CONSTANT, 80 | PropertyId::class => \Attribute::TARGET_PROPERTY, 81 | MethodId::class => \Attribute::TARGET_METHOD, 82 | ParameterId::class => \Attribute::TARGET_PARAMETER, 83 | }; 84 | } 85 | 86 | public function isRepeated(): bool 87 | { 88 | return $this->reflection->isRepeated(); 89 | } 90 | 91 | /** 92 | * @psalm-suppress InvalidReturnType, InvalidReturnStatement 93 | */ 94 | public function newInstance(): object 95 | { 96 | return $this->reflection->evaluate(); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Internal/NativeAdapter/EnumBackedCaseAdapter.php: -------------------------------------------------------------------------------- 1 | name, $this->class); 23 | } 24 | 25 | /** 26 | * @psalm-suppress PossiblyUnusedMethod 27 | */ 28 | public function __get(string $name) 29 | { 30 | return $this->constant->__get($name); 31 | } 32 | 33 | public function __isset(string $name): bool 34 | { 35 | return $this->constant->__isset($name); 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | return $this->constant->__toString(); 41 | } 42 | 43 | public function getAttributes(?string $name = null, int $flags = 0): array 44 | { 45 | return $this->constant->getAttributes($name, $flags); 46 | } 47 | 48 | public function getBackingValue(): int|string 49 | { 50 | return $this->reflection->enumBackingValue() ?? throw new \ReflectionException('Not a backed enum'); 51 | } 52 | 53 | public function getDeclaringClass(): \ReflectionClass 54 | { 55 | return $this->constant->getDeclaringClass(); 56 | } 57 | 58 | public function getDocComment(): string|false 59 | { 60 | return $this->constant->getDocComment(); 61 | } 62 | 63 | public function getEnum(): \ReflectionEnum 64 | { 65 | $enum = $this->constant->getDeclaringClass(); 66 | \assert($enum instanceof \ReflectionEnum); 67 | 68 | return $enum; 69 | } 70 | 71 | public function getModifiers(): int 72 | { 73 | return $this->constant->getModifiers(); 74 | } 75 | 76 | public function getName(): string 77 | { 78 | return $this->constant->name; 79 | } 80 | 81 | /** 82 | * @psalm-suppress PossiblyUnusedMethod, UnusedPsalmSuppress 83 | */ 84 | public function getType(): ?\ReflectionType 85 | { 86 | return $this->constant->getType(); 87 | } 88 | 89 | public function getValue(): \UnitEnum 90 | { 91 | $value = $this->constant->getValue(); 92 | \assert($value instanceof \UnitEnum); 93 | 94 | return $value; 95 | } 96 | 97 | /** 98 | * @psalm-suppress PossiblyUnusedMethod, UnusedPsalmSuppress 99 | */ 100 | public function hasType(): bool 101 | { 102 | return $this->constant->hasType(); 103 | } 104 | 105 | public function isEnumCase(): bool 106 | { 107 | return $this->constant->isEnumCase(); 108 | } 109 | 110 | public function isFinal(): bool 111 | { 112 | return $this->constant->isFinal(); 113 | } 114 | 115 | public function isPrivate(): bool 116 | { 117 | return $this->constant->isPrivate(); 118 | } 119 | 120 | public function isProtected(): bool 121 | { 122 | return $this->constant->isProtected(); 123 | } 124 | 125 | public function isPublic(): bool 126 | { 127 | return $this->constant->isPublic(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Internal/NativeAdapter/EnumUnitCaseAdapter.php: -------------------------------------------------------------------------------- 1 | name, $this->class); 20 | } 21 | 22 | /** 23 | * @psalm-suppress PossiblyUnusedMethod 24 | */ 25 | public function __get(string $name) 26 | { 27 | return $this->constant->__get($name); 28 | } 29 | 30 | public function __isset(string $name): bool 31 | { 32 | return $this->constant->__isset($name); 33 | } 34 | 35 | public function __toString(): string 36 | { 37 | return $this->constant->__toString(); 38 | } 39 | 40 | public function getAttributes(?string $name = null, int $flags = 0): array 41 | { 42 | return $this->constant->getAttributes($name, $flags); 43 | } 44 | 45 | public function getDeclaringClass(): \ReflectionClass 46 | { 47 | return $this->constant->getDeclaringClass(); 48 | } 49 | 50 | public function getDocComment(): string|false 51 | { 52 | return $this->constant->getDocComment(); 53 | } 54 | 55 | public function getEnum(): \ReflectionEnum 56 | { 57 | $enum = $this->constant->getDeclaringClass(); 58 | \assert($enum instanceof \ReflectionEnum); 59 | 60 | return $enum; 61 | } 62 | 63 | public function getModifiers(): int 64 | { 65 | return $this->constant->getModifiers(); 66 | } 67 | 68 | public function getName(): string 69 | { 70 | return $this->constant->name; 71 | } 72 | 73 | /** 74 | * @psalm-suppress PossiblyUnusedMethod, UnusedPsalmSuppress 75 | */ 76 | public function getType(): ?\ReflectionType 77 | { 78 | return $this->constant->getType(); 79 | } 80 | 81 | public function getValue(): \UnitEnum 82 | { 83 | $value = $this->constant->getValue(); 84 | \assert($value instanceof \UnitEnum); 85 | 86 | return $value; 87 | } 88 | 89 | /** 90 | * @psalm-suppress PossiblyUnusedMethod, UnusedPsalmSuppress 91 | */ 92 | public function hasType(): bool 93 | { 94 | return $this->constant->hasType(); 95 | } 96 | 97 | public function isEnumCase(): bool 98 | { 99 | return $this->constant->isEnumCase(); 100 | } 101 | 102 | public function isFinal(): bool 103 | { 104 | return $this->constant->isFinal(); 105 | } 106 | 107 | public function isPrivate(): bool 108 | { 109 | return $this->constant->isPrivate(); 110 | } 111 | 112 | public function isProtected(): bool 113 | { 114 | return $this->constant->isProtected(); 115 | } 116 | 117 | public function isPublic(): bool 118 | { 119 | return $this->constant->isPublic(); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Internal/NativeAdapter/IntersectionTypeAdapter.php: -------------------------------------------------------------------------------- 1 | $types 15 | */ 16 | public function __construct( 17 | private readonly array $types, 18 | ) {} 19 | 20 | public function allowsNull(): bool 21 | { 22 | return false; 23 | } 24 | 25 | public function getTypes(): array 26 | { 27 | return $this->types; 28 | } 29 | 30 | public function __toString(): string 31 | { 32 | return implode('&', $this->types); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Internal/NativeAdapter/NamedTypeAdapter.php: -------------------------------------------------------------------------------- 1 | _name = $name; 105 | } 106 | 107 | public function isNull(): bool 108 | { 109 | return $this->_name === 'null'; 110 | } 111 | 112 | public function isIterable(): bool 113 | { 114 | return $this->_name === 'iterable'; 115 | } 116 | 117 | public function allowsNull(): bool 118 | { 119 | return $this->nullable || $this->_name === 'null' || $this->_name === 'mixed'; 120 | } 121 | 122 | public function getName(): string 123 | { 124 | return $this->_name; 125 | } 126 | 127 | public function isBuiltin(): bool 128 | { 129 | return $this->builtIn; 130 | } 131 | 132 | public function toNullable(): self 133 | { 134 | return new self($this->_name, $this->builtIn, nullable: true); 135 | } 136 | 137 | public function __toString(): string 138 | { 139 | return ($this->nullable ? '?' : '') . $this->_name; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Internal/NativeAdapter/NativeTraitInfo.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public readonly array $aliases; 19 | 20 | /** 21 | * @param list $names 22 | * @param list $aliases 23 | */ 24 | public function __construct( 25 | public readonly array $names = [], 26 | array $aliases = [], 27 | ) { 28 | $resolvedAliases = []; 29 | 30 | foreach ($aliases as $alias) { 31 | if ($alias->newName !== null) { 32 | $resolvedAliases[$alias->newName] = $alias->trait . '::' . $alias->method; 33 | } 34 | } 35 | 36 | $this->aliases = $resolvedAliases; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Internal/NativeAdapter/NativeTraitInfoKey.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | enum NativeTraitInfoKey implements OptionalKey 16 | { 17 | case Key; 18 | 19 | public function default(TypedMap $map): mixed 20 | { 21 | return new NativeTraitInfo(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/NativeAdapter/NonConvertableType.php: -------------------------------------------------------------------------------- 1 | $types 15 | */ 16 | public function __construct( 17 | private readonly array $types, 18 | ) {} 19 | 20 | public function allowsNull(): bool 21 | { 22 | foreach ($this->types as $type) { 23 | if ($type->allowsNull()) { 24 | return true; 25 | } 26 | } 27 | 28 | return false; 29 | } 30 | 31 | /** 32 | * @psalm-suppress InvalidReturnType, InvalidReturnStatement, UnusedPsalmSuppress 33 | */ 34 | public function getTypes(): array 35 | { 36 | return $this->types; 37 | } 38 | 39 | public function __toString(): string 40 | { 41 | return implode('|', $this->types); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/NativeReflector/DefinedConstantReflector.php: -------------------------------------------------------------------------------- 1 | name)) { 25 | return null; 26 | } 27 | 28 | $value = \constant($id->name); 29 | $extension = $this->constantExtensions()[$id->name] ?? null; 30 | 31 | return (new TypedMap()) 32 | ->with(Data::Type, new TypeData(inferred: types::value($value))) 33 | ->with(Data::Namespace, get_namespace($id->name)) 34 | ->with(Data::ValueExpression, Value::from($value)) 35 | ->with(Data::PhpExtension, $extension) 36 | ->with(Data::ChangeDetector, new ConstantChangeDetector(name: $id->name, exists: true, value: $value)) 37 | ->with(Data::InternallyDefined, $extension !== null); 38 | } 39 | 40 | /** 41 | * @var ?array 42 | */ 43 | private ?array $constantExtensions = null; 44 | 45 | /** 46 | * @return array 47 | */ 48 | private function constantExtensions(): array 49 | { 50 | if ($this->constantExtensions !== null) { 51 | return $this->constantExtensions; 52 | } 53 | 54 | $this->constantExtensions = []; 55 | 56 | foreach (get_defined_constants(categorize: true) as $category => $constants) { 57 | if ($category === 'user') { 58 | continue; 59 | } 60 | 61 | foreach ($constants as $name => $_value) { 62 | /** 63 | * @var non-empty-string $name 64 | * @var non-empty-string $category 65 | */ 66 | $this->constantExtensions[$name] = $category; 67 | } 68 | } 69 | 70 | return $this->constantExtensions; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Internal/PhpDoc/AlwaysTrimmingConstExprParser.php: -------------------------------------------------------------------------------- 1 | }> 16 | */ 17 | final class NamedObjectTypeDestructurizer extends DefaultTypeVisitor 18 | { 19 | public function namedObject(Type $type, NamedClassId|AnonymousClassId $classId, array $typeArguments): mixed 20 | { 21 | return [$classId, $typeArguments]; 22 | } 23 | 24 | protected function default(Type $type): mixed 25 | { 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/PhpDoc/PhpDocConstantExpressionCompiler.php: -------------------------------------------------------------------------------- 1 | context = new CompilationContext($context); 39 | } 40 | 41 | /** 42 | * @return ($expr is null ? null : Expression) 43 | */ 44 | public function compile(?ConstExprNode $expr): ?Expression 45 | { 46 | return match (true) { 47 | $expr === null => null, 48 | $expr instanceof ConstExprNullNode => Values::Null, 49 | $expr instanceof ConstExprTrueNode => Values::True, 50 | $expr instanceof ConstExprFalseNode => Values::False, 51 | $expr instanceof ConstExprIntegerNode => Value::from((int) $expr->value), 52 | $expr instanceof ConstExprFloatNode => Value::from((float) $expr->value), 53 | $expr instanceof ConstExprStringNode => Value::from($expr->value), 54 | $expr instanceof ConstExprArrayNode => $this->compileArray($expr), 55 | $expr instanceof ConstFetchNode => $this->compileConstFetch($expr), 56 | default => throw new \LogicException(\sprintf('Unsupported expression %s', $expr::class)), 57 | }; 58 | } 59 | 60 | /** 61 | * @return Expression 62 | */ 63 | private function compileArray(ConstExprArrayNode $expr): Expression 64 | { 65 | if ($expr->items === []) { 66 | return Value::from([]); 67 | } 68 | 69 | return new ArrayExpression( 70 | array_map( 71 | fn(ConstExprArrayItemNode $item): ArrayElement => new ArrayElement( 72 | key: $item->key === null ? null : $this->compile($item->key), 73 | value: $this->compile($item->value), 74 | ), 75 | $expr->items, 76 | ), 77 | ); 78 | } 79 | 80 | private function compileConstFetch(ConstFetchNode $expr): Expression 81 | { 82 | if ($expr->className !== '') { 83 | return new ClassConstantFetch( 84 | class: $this->compileClassName($expr->className), 85 | name: Value::from($expr->name), 86 | ); 87 | } 88 | 89 | return match ($expr->name) { 90 | '__LINE__' => self::compileNodeLine($expr), 91 | '__FILE__' => $this->context->magicFile(), 92 | '__DIR__' => $this->context->magicDir(), 93 | '__NAMESPACE__' => $this->context->magicNamespace(), 94 | '__FUNCTION__' => $this->context->magicFunction(), 95 | '__CLASS__' => $this->context->magicClass(), 96 | '__TRAIT__' => $this->context->magicTrait(), 97 | '__METHOD__' => $this->context->magicMethod(), 98 | default => new ConstantFetch(...$this->context->resolveConstantName($expr->name)), 99 | }; 100 | } 101 | 102 | /** 103 | * @param non-empty-string $name 104 | */ 105 | private function compileClassName(string $name): Expression 106 | { 107 | return match (strtolower($name)) { 108 | 'self' => $this->context->self(), 109 | 'parent' => $this->context->parent(), 110 | 'static' => $this->context->static(), 111 | default => Value::from($this->context->resolveClassName($name)), 112 | }; 113 | } 114 | 115 | /** 116 | * @return Expression 117 | */ 118 | private static function compileNodeLine(ConstExprNode $expr): Expression 119 | { 120 | $line = $expr->getAttribute(Attribute::START_LINE); 121 | \assert(\is_int($line) && $line > 0); 122 | 123 | return Value::from($line); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Internal/PhpDoc/PhpDocTagPrioritizer.php: -------------------------------------------------------------------------------- 1 | 3, 15 | '@phpstan' => 2, 16 | '@phan' => 1, 17 | ]; 18 | 19 | /** 20 | * @param array $prefixPriorities 21 | */ 22 | public function __construct( 23 | private readonly array $prefixPriorities = self::DEFAULT_PREFIX_PRIORITIES, 24 | ) {} 25 | 26 | public function priorityFor(string $tagName): int 27 | { 28 | foreach ($this->prefixPriorities as $prefix => $priority) { 29 | if (str_starts_with($tagName, $prefix)) { 30 | return $priority; 31 | } 32 | } 33 | 34 | return 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Internal/PhpParser/CodeReflector.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function reflectCode(string $code, ?string $file = null): IdMap 36 | { 37 | $nodes = $this->phpParser->parse($code) ?? throw new \LogicException(); 38 | 39 | /** @psalm-suppress MixedArgument, ArgumentTypeCoercion, UnusedPsalmSuppress */ 40 | $linesFixer = method_exists($this->phpParser, 'getTokens') 41 | ? new FixNodeLocationVisitor($this->phpParser->getTokens()) 42 | : FixNodeLocationVisitor::fromCode($code); 43 | $nameResolver = new NameResolver(); 44 | $contextVisitor = new ContextVisitor( 45 | code: $code, 46 | file: $file, 47 | nameContext: $nameResolver->getNameContext(), 48 | annotatedDeclarationsDiscoverer: $this->annotatedDeclarationsDiscoverer, 49 | ); 50 | $collector = new CollectIdReflectorsVisitor($this->nodeReflector, $contextVisitor); 51 | 52 | $traverser = new NodeTraverser(); 53 | 54 | if (!PhpParserChecker::isVisitorLeaveReversed()) { 55 | $traverser->addVisitor($collector); 56 | } 57 | 58 | $traverser->addVisitor($linesFixer); 59 | $traverser->addVisitor(new GeneratorVisitor()); 60 | $traverser->addVisitor($nameResolver); 61 | $traverser->addVisitor($contextVisitor); 62 | 63 | if (PhpParserChecker::isVisitorLeaveReversed()) { 64 | $traverser->addVisitor($collector); 65 | } 66 | 67 | $traverser->traverse($nodes); 68 | 69 | return $collector->idReflectors; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Internal/PhpParser/CollectIdReflectorsVisitor.php: -------------------------------------------------------------------------------- 1 | 36 | */ 37 | public IdMap $idReflectors; 38 | 39 | public function __construct( 40 | private readonly NodeReflector $nodeReflector, 41 | private readonly ContextProvider $contextProvider, 42 | private readonly ConstExprEvaluator $evaluator = new ConstExprEvaluator(), 43 | ) { 44 | /** @var IdMap */ 45 | $this->idReflectors = new IdMap(); 46 | } 47 | 48 | /** 49 | * @throws ConstExprEvaluationException 50 | */ 51 | public function leaveNode(Node $node): ?int 52 | { 53 | if ($node instanceof Function_) { 54 | $context = $this->contextProvider->get(); 55 | \assert($context->currentId instanceof NamedFunctionId); 56 | 57 | $nodeReflector = $this->nodeReflector; 58 | $this->idReflectors = $this->idReflectors->with( 59 | $context->currentId, 60 | static fn(): TypedMap => $nodeReflector->reflectFunction($node, $context), 61 | ); 62 | 63 | return null; 64 | } 65 | 66 | if ($node instanceof ClassLike) { 67 | $context = $this->contextProvider->get(); 68 | \assert($context->currentId instanceof NamedClassId || $context->currentId instanceof AnonymousClassId); 69 | 70 | $nodeReflector = $this->nodeReflector; 71 | $this->idReflectors = $this->idReflectors->with( 72 | $context->currentId, 73 | static fn(): TypedMap => $nodeReflector->reflectClassLike($node, $context), 74 | ); 75 | 76 | return null; 77 | } 78 | 79 | if ($node instanceof ClassMethod) { 80 | NodeContextAttribute::set($node, $this->contextProvider->get()); 81 | 82 | return null; 83 | } 84 | 85 | if ($node instanceof Const_) { 86 | $context = $this->contextProvider->get(); 87 | $nodeReflector = $this->nodeReflector; 88 | 89 | foreach ($node->consts as $key => $const) { 90 | \assert($const->namespacedName !== null); 91 | 92 | $this->idReflectors = $this->idReflectors->with( 93 | Id::constant($const->namespacedName->toString()), 94 | static fn(): TypedMap => $nodeReflector->reflectConstant($node, $key, $context), 95 | ); 96 | } 97 | 98 | return null; 99 | } 100 | 101 | if ($node instanceof FuncCall 102 | && $node->name instanceof Name 103 | && strtolower($node->name->toString()) === 'define' 104 | ) { 105 | $nameArg = $node->args[0] ?? $node->args['constant_name'] ?? null; 106 | $valueArg = $node->args[1] ?? $node->args['value'] ?? null; 107 | 108 | if (!($nameArg instanceof Arg && $valueArg instanceof Arg)) { 109 | return null; 110 | } 111 | 112 | $name = $this->evaluator->evaluateSilently($nameArg->value); 113 | \assert(\is_string($name) && $name !== ''); 114 | 115 | $context = $this->contextProvider->get(); 116 | $nodeReflector = $this->nodeReflector; 117 | $this->idReflectors = $this->idReflectors->with( 118 | Id::constant($name), 119 | static fn(): TypedMap => $nodeReflector->reflectDefine($node, $context), 120 | ); 121 | 122 | return null; 123 | } 124 | 125 | return null; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Internal/PhpParser/FixNodeLocationVisitor.php: -------------------------------------------------------------------------------- 1 | $tokens 29 | */ 30 | public function __construct( 31 | private readonly array $tokens, 32 | ) {} 33 | 34 | public static function fromCode(string $code): self 35 | { 36 | return new self(\PhpToken::tokenize($code)); 37 | } 38 | 39 | public function enterNode(Node $node): ?int 40 | { 41 | if ($node instanceof Function_ 42 | || $node instanceof ArrowFunction 43 | || $node instanceof Closure 44 | || $node instanceof Param 45 | || $node instanceof ClassLike 46 | || $node instanceof ClassConst 47 | || $node instanceof EnumCase 48 | || $node instanceof Property 49 | || $node instanceof ClassMethod 50 | ) { 51 | $this->fixNode($node, $node->attrGroups); 52 | 53 | return null; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | /** 60 | * @param array $attrGroups 61 | */ 62 | private function fixNode(Node $node, array $attrGroups): void 63 | { 64 | if ($attrGroups === []) { 65 | return; 66 | } 67 | 68 | $lastAttrPosition = array_value_last($attrGroups)->getEndFilePos() + 1; 69 | 70 | foreach ($this->tokens as $index => $token) { 71 | if ($token->pos >= $lastAttrPosition && !$token->isIgnorable()) { 72 | $node->setAttribute('startLine', $token->line); 73 | $node->setAttribute('startFilePos', $token->pos); 74 | $node->setAttribute('startTokenPos', $index); 75 | 76 | return; 77 | } 78 | } 79 | 80 | throw new \LogicException(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Internal/PhpParser/GeneratorVisitor.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | private array $functionStack = []; 25 | 26 | public static function isGenerator(FunctionLike $function): bool 27 | { 28 | $attribute = $function->getAttribute(self::ATTRIBUTE); 29 | 30 | if (\is_bool($attribute)) { 31 | return $attribute; 32 | } 33 | 34 | throw new \LogicException(\sprintf('%s was not used during traversal', self::class)); 35 | } 36 | 37 | public function beforeTraverse(array $nodes): ?array 38 | { 39 | $this->functionStack = []; 40 | 41 | return null; 42 | } 43 | 44 | public function enterNode(Node $node): ?int 45 | { 46 | if ($node instanceof FunctionLike) { 47 | $node->setAttribute(self::ATTRIBUTE, false); 48 | $this->functionStack[] = $node; 49 | 50 | return null; 51 | } 52 | 53 | if ($node instanceof Yield_) { 54 | array_value_last($this->functionStack)?->setAttribute(self::ATTRIBUTE, true); 55 | 56 | return null; 57 | } 58 | 59 | return null; 60 | } 61 | 62 | public function leaveNode(Node $node): ?int 63 | { 64 | if ($node instanceof FunctionLike) { 65 | array_pop($this->functionStack); 66 | 67 | return null; 68 | } 69 | 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Internal/PhpParser/NameParser.php: -------------------------------------------------------------------------------- 1 | getAttribute(Context::class); 21 | \assert($context instanceof Context); 22 | 23 | return $context; 24 | } 25 | 26 | public static function set(ClassMethod $node, Context $context): void 27 | { 28 | $node->setAttribute(Context::class, $context); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Internal/PhpParser/PhpParserChecker.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class UnresolvedConstantType implements Type 17 | { 18 | /** 19 | * @param non-empty-string $namespacedName 20 | * @param non-empty-string $globalName 21 | */ 22 | public function __construct( 23 | private readonly string $namespacedName, 24 | private readonly string $globalName, 25 | ) {} 26 | 27 | public function accept(TypeVisitor $visitor): mixed 28 | { 29 | if (\defined($this->namespacedName)) { 30 | return $visitor->constant($this, Id::constant($this->namespacedName)); 31 | } 32 | 33 | return $visitor->constant($this, Id::constant($this->globalName)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Internal/Type/IsNativeTypeNullable.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class IsNativeTypeNullable extends DefaultTypeVisitor 16 | { 17 | public function null(Type $type): mixed 18 | { 19 | return true; 20 | } 21 | 22 | public function union(Type $type, array $ofTypes): mixed 23 | { 24 | foreach ($ofTypes as $ofType) { 25 | if ($ofType->accept($this)) { 26 | return true; 27 | } 28 | } 29 | 30 | return false; 31 | } 32 | 33 | public function mixed(Type $type): mixed 34 | { 35 | return true; 36 | } 37 | 38 | protected function default(Type $type): mixed 39 | { 40 | return false; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Internal/functions.php: -------------------------------------------------------------------------------- 1 | $iterable 14 | * @param callable(TValue, TKey): TNewValue $mapper 15 | * @return ( 16 | * $iterable is non-empty-list ? non-empty-list : 17 | * $iterable is list ? list : 18 | * $iterable is non-empty-array ? non-empty-array : 19 | * array 20 | * ) 21 | */ 22 | function map(iterable $iterable, callable $mapper): array 23 | { 24 | $mapped = []; 25 | 26 | foreach ($iterable as $key => $value) { 27 | $mapped[$key] = $mapper($value, $key); 28 | } 29 | 30 | return $mapped; 31 | } 32 | 33 | /** 34 | * @internal 35 | * @psalm-internal Typhoon\Reflection 36 | * @psalm-pure 37 | * @template TValue 38 | * @param array $array 39 | * @return ($array is non-empty-array ? TValue : ?TValue) 40 | */ 41 | function array_value_first(array $array): mixed 42 | { 43 | $key = array_key_first($array); 44 | 45 | return $key === null ? null : $array[$key]; 46 | } 47 | 48 | /** 49 | * @internal 50 | * @psalm-internal Typhoon\Reflection 51 | * @psalm-pure 52 | * @template TValue 53 | * @param array $array 54 | * @return ($array is non-empty-array ? TValue : ?TValue) 55 | */ 56 | function array_value_last(array $array): mixed 57 | { 58 | $key = array_key_last($array); 59 | 60 | return $key === null ? null : $array[$key]; 61 | } 62 | 63 | /** 64 | * @internal 65 | * @psalm-internal Typhoon\Reflection 66 | * @psalm-assert-if-true class-string $name 67 | */ 68 | function class_like_exists(string $name, bool $autoload = true): bool 69 | { 70 | return class_exists($name, $autoload) || interface_exists($name, $autoload) || trait_exists($name, $autoload); 71 | } 72 | 73 | /** 74 | * @internal 75 | * @psalm-internal Typhoon\Reflection 76 | * @psalm-pure 77 | */ 78 | function get_namespace(string $name): string 79 | { 80 | $lastSlashPosition = strrpos($name, '\\'); 81 | 82 | if ($lastSlashPosition === false) { 83 | return ''; 84 | } 85 | 86 | return substr($name, 0, $lastSlashPosition); 87 | } 88 | 89 | /** 90 | * @internal 91 | * @psalm-internal Typhoon\Reflection 92 | * @psalm-pure 93 | */ 94 | function get_short_name(string $name): string 95 | { 96 | $lastSlashPosition = strrpos($name, '\\'); 97 | 98 | if ($lastSlashPosition === false) { 99 | return $name; 100 | } 101 | 102 | return substr($name, $lastSlashPosition + 1); 103 | } 104 | -------------------------------------------------------------------------------- /src/KeyIsNotDefined.php: -------------------------------------------------------------------------------- 1 | findFile($id->name); 24 | 25 | if ($file !== false) { 26 | \assert($file !== ''); 27 | 28 | return Resource::fromFile($file); 29 | } 30 | } 31 | 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Locator/ConstantLocator.php: -------------------------------------------------------------------------------- 1 | file); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Locator/Locators.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $constantLocators = []; 22 | 23 | /** 24 | * @var list 25 | */ 26 | private array $namedFunctionLocators = []; 27 | 28 | /** 29 | * @var list 30 | */ 31 | private array $namedClassLocators = []; 32 | 33 | /** 34 | * @var list 35 | */ 36 | private array $anonymousLocators = []; 37 | 38 | /** 39 | * @param iterable $locators 40 | */ 41 | public function __construct(iterable $locators) 42 | { 43 | foreach ($locators as $locator) { 44 | $this->add($locator); 45 | } 46 | } 47 | 48 | public function locate(ConstantId|NamedFunctionId|AnonymousFunctionId|NamedClassId|AnonymousClassId $id): ?Resource 49 | { 50 | $locators = match (true) { 51 | $id instanceof ConstantId => $this->constantLocators, 52 | $id instanceof NamedFunctionId => $this->namedFunctionLocators, 53 | $id instanceof NamedClassId => $this->namedClassLocators, 54 | $id instanceof AnonymousFunctionId, 55 | $id instanceof AnonymousClassId => $this->anonymousLocators, 56 | }; 57 | 58 | foreach ($locators as $locator) { 59 | /** @psalm-suppress PossiblyInvalidArgument */ 60 | $resource = $locator->locate($id); 61 | 62 | if ($resource !== null) { 63 | return $resource; 64 | } 65 | } 66 | 67 | return null; 68 | } 69 | 70 | public function with(ConstantLocator|NamedFunctionLocator|NamedClassLocator|AnonymousLocator $locator): self 71 | { 72 | $copy = clone $this; 73 | $copy->add($locator); 74 | 75 | return $copy; 76 | } 77 | 78 | private function add(ConstantLocator|NamedFunctionLocator|NamedClassLocator|AnonymousLocator $locator): void 79 | { 80 | if ($locator instanceof ConstantLocator) { 81 | $this->constantLocators[] = $locator; 82 | } 83 | 84 | if ($locator instanceof NamedFunctionLocator) { 85 | $this->namedFunctionLocators[] = $locator; 86 | } 87 | 88 | if ($locator instanceof NamedClassLocator) { 89 | $this->namedClassLocators[] = $locator; 90 | } 91 | 92 | if ($locator instanceof AnonymousLocator) { 93 | $this->anonymousLocators[] = $locator; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Locator/NamedClassLocator.php: -------------------------------------------------------------------------------- 1 | name); 20 | } catch (\ReflectionException) { 21 | return null; 22 | } 23 | 24 | $file = $reflection->getFileName(); 25 | 26 | if ($file === false) { 27 | return null; 28 | } 29 | 30 | $data = new TypedMap(); 31 | $extension = $reflection->getExtensionName(); 32 | 33 | if ($extension !== false) { 34 | $data = $data->with(Data::PhpExtension, $extension); 35 | } 36 | 37 | return Resource::fromFile($file, $data); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Locator/NativeReflectionFunctionLocator.php: -------------------------------------------------------------------------------- 1 | name); 21 | } catch (\ReflectionException) { 22 | return null; 23 | } 24 | 25 | $file = $reflection->getFileName(); 26 | 27 | if ($file === false) { 28 | return null; 29 | } 30 | 31 | $data = new TypedMap(); 32 | $extension = $reflection->getExtensionName(); 33 | 34 | if ($extension !== false) { 35 | $data = $data->with(Data::PhpExtension, $extension); 36 | } 37 | 38 | return Resource::fromFile($file, $data); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Locator/NoSymfonyPolyfillLocator.php: -------------------------------------------------------------------------------- 1 | locator = new Locators([$locator]); 23 | } 24 | 25 | public function locate(NamedFunctionId|NamedClassId $id): ?Resource 26 | { 27 | $resource = $this->locator->locate($id); 28 | 29 | if ($resource === null) { 30 | return null; 31 | } 32 | 33 | $file = $resource->data[Data::File]; 34 | 35 | if ($file !== null && str_contains($file, self::PATTERN)) { 36 | return null; 37 | } 38 | 39 | return $resource; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Locator/OnlyLoadedClassLocator.php: -------------------------------------------------------------------------------- 1 | name)) { 22 | return $this->namedClassLocator->locate($id); 23 | } 24 | 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Locator/Resource.php: -------------------------------------------------------------------------------- 1 | $hooks 28 | */ 29 | public static function fromCode(string $code, TypedMap $data = new TypedMap(), iterable $hooks = []): self 30 | { 31 | return new self( 32 | data: $data->with(Data::Code, $code), 33 | hooks: new Hooks($hooks), 34 | ); 35 | } 36 | 37 | /** 38 | * @param non-empty-string $file 39 | * @param iterable $hooks 40 | */ 41 | public static function fromFile(string $file, TypedMap $data = new TypedMap(), iterable $hooks = []): self 42 | { 43 | $mtime = @filemtime($file); 44 | 45 | if ($mtime === false) { 46 | throw new FileIsNotReadable($file); 47 | } 48 | 49 | $code = @file_get_contents($file); 50 | 51 | if ($code === false) { 52 | throw new FileIsNotReadable($file); 53 | } 54 | 55 | return new self( 56 | data: $data 57 | ->with(Data::Code, $code) 58 | ->with(Data::File, $file) 59 | ->with(Data::ChangeDetector, new FileChangeDetector( 60 | file: $file, 61 | mtime: $mtime, 62 | xxh3: hash(FileChangeDetector::HASHING_ALGORITHM, $code), 63 | )), 64 | hooks: new Hooks($hooks), 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Locator/ScannedResourceLocator.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | private readonly array $idsMap; 23 | 24 | /** 25 | * @param list $ids 26 | */ 27 | public function __construct( 28 | array $ids, 29 | private readonly Resource $resource, 30 | ) { 31 | $idsMap = []; 32 | 33 | foreach ($ids as $id) { 34 | $idsMap[$id->encode()] = true; 35 | } 36 | 37 | $this->idsMap = $idsMap; 38 | } 39 | 40 | public function locate(ConstantId|NamedFunctionId|AnonymousFunctionId|NamedClassId|AnonymousClassId $id): ?Resource 41 | { 42 | return isset($this->idsMap[$id->encode()]) ? $this->resource : null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ModifierKind.php: -------------------------------------------------------------------------------- 1 | 10 | * @psalm-type Aliases = Collection 11 | * @psalm-type Templates = Collection 12 | * @psalm-type ClassConstants = Collection 13 | * @psalm-type Methods = Collection 14 | * @psalm-type Properties = Collection 15 | * @psalm-type Parameters = Collection 16 | */ 17 | enum ReflectionCollections {} 18 | -------------------------------------------------------------------------------- /src/TemplateReflection.php: -------------------------------------------------------------------------------- 1 | id = $id; 39 | $this->data = $data; 40 | } 41 | 42 | /** 43 | * @return non-negative-int 44 | */ 45 | public function index(): int 46 | { 47 | return $this->data[Data::Index]; 48 | } 49 | 50 | public function variance(): Variance 51 | { 52 | return $this->data[Data::Variance]; 53 | } 54 | 55 | public function constraint(): Type 56 | { 57 | return $this->data[Data::Constraint]; 58 | } 59 | 60 | public function location(): ?Location 61 | { 62 | return $this->data[Data::Location]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/TypeKind.php: -------------------------------------------------------------------------------- 1 |