├── .editorconfig ├── LICENSE ├── README.md ├── composer.json ├── extension.neon ├── rules.neon ├── src ├── Reflection │ └── Nette │ │ ├── HtmlClassReflectionExtension.php │ │ ├── HtmlMethodReflection.php │ │ ├── HtmlPropertyReflection.php │ │ ├── NetteObjectClassReflectionExtension.php │ │ ├── NetteObjectEventListenerMethodReflection.php │ │ └── NetteObjectPropertyReflection.php ├── Rule │ └── Nette │ │ ├── DoNotExtendNetteObjectRule.php │ │ ├── PresenterInjectedPropertiesExtension.php │ │ ├── RegularExpressionPatternRule.php │ │ └── RethrowExceptionRule.php └── Type │ └── Nette │ ├── ComponentGetPresenterDynamicReturnTypeExtension.php │ ├── ComponentLookupDynamicReturnTypeExtension.php │ ├── ComponentModelArrayAccessDynamicReturnTypeExtension.php │ ├── ComponentModelDynamicReturnTypeExtension.php │ ├── FormContainerUnsafeValuesDynamicReturnTypeExtension.php │ ├── FormContainerValuesDynamicReturnTypeExtension.php │ ├── FormsBaseControlDynamicReturnTypeExtension.php │ ├── PresenterGetSessionReturnTypeExtension.php │ ├── ServiceLocatorDynamicReturnTypeExtension.php │ ├── StringsMatchAllDynamicReturnTypeExtension.php │ ├── StringsMatchDynamicReturnTypeExtension.php │ └── StringsReplaceCallbackClosureTypeExtension.php └── stubs ├── Application ├── Routers │ └── RouteList.stub └── UI │ ├── Component.stub │ ├── Multiplier.stub │ └── Presenter.stub ├── Caching └── Cache.stub ├── ComponentModel ├── Component.stub ├── Container.stub ├── IComponent.stub └── IContainer.stub ├── Database ├── ResultSet.stub └── Table │ ├── ActiveRow.stub │ └── Selection.stub ├── Forms ├── Container.stub ├── Form.stub └── Rules.stub ├── Http ├── FileUpload.stub └── SessionSection.stub ├── Routing └── Router.stub └── Utils ├── ArrayHash.stub ├── Arrays.stub ├── Callback.stub ├── Helpers.stub ├── Html.stub ├── Paginator.stub └── Random.stub /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.{php,phpt}] 10 | indent_style = tab 11 | indent_size = 4 12 | 13 | [*.xml] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.neon] 18 | indent_style = tab 19 | indent_size = 4 20 | 21 | [*.{yaml,yml}] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [composer.json] 26 | indent_style = tab 27 | indent_size = 4 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ondřej Mirtes 4 | Copyright (c) 2025 PHPStan s.r.o. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nette Framework extension for PHPStan 2 | 3 | [![Build](https://github.com/phpstan/phpstan-nette/workflows/Build/badge.svg)](https://github.com/phpstan/phpstan-nette/actions) 4 | [![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan-nette/v/stable)](https://packagist.org/packages/phpstan/phpstan-nette) 5 | [![License](https://poser.pugx.org/phpstan/phpstan-nette/license)](https://packagist.org/packages/phpstan/phpstan-nette) 6 | 7 | * [PHPStan](https://phpstan.org/) 8 | * [Nette Framework](https://nette.org/) 9 | 10 | This extension provides following features: 11 | 12 | * `Nette\ComponentModel\Container::getComponent()` knows type of the component because it reads the return type on `createComponent*` (this works best in presenters and controls) 13 | * `Nette\DI\Container::getByType` and `createInstance` return type based on first parameter (`Foo::class`). 14 | * `Nette\Forms\Container::getValues` return type based on `$asArray` parameter. 15 | * `Nette\ComponentModel\Component::lookup` return type based on `$throw` parameter. 16 | * `Nette\Application\UI\Component::getPresenter` return type based on `$throw` parameter. 17 | * Dynamic methods of [Nette\Utils\Html](https://doc.nette.org/en/2.4/html-elements) 18 | * Magic [Nette\Object and Nette\SmartObject](https://doc.nette.org/en/2.4/php-language-enhancements) properties 19 | * Event listeners through the `on*` properties 20 | * Defines early terminating method calls for Presenter methods to prevent `Undefined variable` errors 21 | * Understand the exact array shape coming from `Nette\Utils\Strings::match()` and `Nette\Utils\Strings::matchAll()` based on pattern 22 | 23 | It also contains these framework-specific rules (can be enabled separately): 24 | 25 | * Do not extend Nette\Object, use Nette\SmartObject trait instead 26 | * Rethrow exceptions that are always meant to be rethrown (like `AbortException`) 27 | 28 | 29 | ## Installation 30 | 31 | To use this extension, require it in [Composer](https://getcomposer.org/): 32 | 33 | ``` 34 | composer require --dev phpstan/phpstan-nette 35 | ``` 36 | 37 | If you also install [phpstan/extension-installer](https://github.com/phpstan/extension-installer) then you're all set! 38 | 39 |
40 | Manual installation 41 | 42 | If you don't want to use `phpstan/extension-installer`, include extension.neon in your project's PHPStan config: 43 | 44 | ``` 45 | includes: 46 | - vendor/phpstan/phpstan-nette/extension.neon 47 | ``` 48 | 49 | To perform framework-specific checks, include also this file: 50 | 51 | ``` 52 | - vendor/phpstan/phpstan-nette/rules.neon 53 | ``` 54 | 55 |
56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpstan/phpstan-nette", 3 | "type": "phpstan-extension", 4 | "description": "Nette Framework class reflection extension for PHPStan", 5 | "license": [ 6 | "MIT" 7 | ], 8 | "require": { 9 | "php": "^7.4 || ^8.0", 10 | "phpstan/phpstan": "^2.1.12" 11 | }, 12 | "conflict": { 13 | "nette/application": "<2.3.0", 14 | "nette/component-model": "<2.3.0", 15 | "nette/di": "<2.3.0", 16 | "nette/forms": "<2.3.0", 17 | "nette/http": "<2.3.0", 18 | "nette/utils": "<2.3.0" 19 | }, 20 | "require-dev": { 21 | "nette/application": "^3.0", 22 | "nette/di": "^3.1.10", 23 | "nette/forms": "^3.0", 24 | "nette/utils": "^2.3.0 || ^3.0.0", 25 | "php-parallel-lint/php-parallel-lint": "^1.2", 26 | "phpstan/phpstan-deprecation-rules": "^2.0", 27 | "phpstan/phpstan-phpunit": "^2.0", 28 | "phpstan/phpstan-strict-rules": "^2.0", 29 | "phpunit/phpunit": "^9.6" 30 | }, 31 | "config": { 32 | "platform": { 33 | "php": "7.4.6" 34 | }, 35 | "sort-packages": true 36 | }, 37 | "extra": { 38 | "phpstan": { 39 | "includes": [ 40 | "extension.neon", 41 | "rules.neon" 42 | ] 43 | } 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "PHPStan\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "classmap": [ 52 | "tests/" 53 | ] 54 | }, 55 | "minimum-stability": "dev", 56 | "prefer-stable": true 57 | } 58 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | additionalConstructors: 3 | - Nette\Application\UI\Presenter::startup 4 | exceptions: 5 | uncheckedExceptionClasses: 6 | - 'Nette\InvalidArgumentException' 7 | stubFiles: 8 | - stubs/Application/Routers/RouteList.stub 9 | - stubs/Application/UI/Component.stub 10 | - stubs/Application/UI/Multiplier.stub 11 | - stubs/Application/UI/Presenter.stub 12 | - stubs/Caching/Cache.stub 13 | - stubs/ComponentModel/Component.stub 14 | - stubs/ComponentModel/Container.stub 15 | - stubs/ComponentModel/IComponent.stub 16 | - stubs/ComponentModel/IContainer.stub 17 | - stubs/Database/ResultSet.stub 18 | - stubs/Database/Table/ActiveRow.stub 19 | - stubs/Database/Table/Selection.stub 20 | - stubs/Forms/Container.stub 21 | - stubs/Forms/Form.stub 22 | - stubs/Forms/Rules.stub 23 | - stubs/Http/SessionSection.stub 24 | - stubs/Http/FileUpload.stub 25 | - stubs/Routing/Router.stub 26 | - stubs/Utils/ArrayHash.stub 27 | - stubs/Utils/Arrays.stub 28 | - stubs/Utils/Callback.stub 29 | - stubs/Utils/Helpers.stub 30 | - stubs/Utils/Html.stub 31 | - stubs/Utils/Paginator.stub 32 | - stubs/Utils/Random.stub 33 | universalObjectCratesClasses: 34 | - Nette\Application\UI\ITemplate 35 | - Nette\Application\UI\Template 36 | - Nette\Bridges\ApplicationLatte\Template 37 | - Nette\Database\IRow 38 | - Nette\Http\SessionSection 39 | - Nette\Security\Identity 40 | - Nette\Security\SimpleIdentity 41 | earlyTerminatingMethodCalls: 42 | Nette\Application\UI\Component: 43 | - error 44 | Nette\Application\UI\Presenter: 45 | - redirect 46 | - redirectUrl 47 | - sendJson 48 | - sendPayload 49 | - sendResponse 50 | - sendTemplate 51 | - terminate 52 | - forward 53 | 54 | services: 55 | - 56 | class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension 57 | tags: 58 | - phpstan.broker.propertiesClassReflectionExtension 59 | - phpstan.broker.methodsClassReflectionExtension 60 | 61 | - 62 | class: PHPStan\Reflection\Nette\NetteObjectClassReflectionExtension 63 | tags: 64 | - phpstan.broker.propertiesClassReflectionExtension 65 | - phpstan.broker.methodsClassReflectionExtension 66 | 67 | - 68 | class: PHPStan\Type\Nette\ComponentModelArrayAccessDynamicReturnTypeExtension 69 | tags: 70 | - phpstan.broker.dynamicMethodReturnTypeExtension 71 | 72 | - 73 | class: PHPStan\Type\Nette\ComponentModelDynamicReturnTypeExtension 74 | tags: 75 | - phpstan.broker.dynamicMethodReturnTypeExtension 76 | 77 | - 78 | class: PHPStan\Type\Nette\ComponentLookupDynamicReturnTypeExtension 79 | tags: 80 | - phpstan.broker.dynamicMethodReturnTypeExtension 81 | 82 | - 83 | class: PHPStan\Type\Nette\ComponentGetPresenterDynamicReturnTypeExtension 84 | tags: 85 | - phpstan.broker.dynamicMethodReturnTypeExtension 86 | 87 | - 88 | class: PHPStan\Type\Nette\FormsBaseControlDynamicReturnTypeExtension 89 | tags: 90 | - phpstan.broker.dynamicMethodReturnTypeExtension 91 | 92 | - 93 | class: PHPStan\Type\Nette\PresenterGetSessionReturnTypeExtension 94 | tags: 95 | - phpstan.broker.dynamicMethodReturnTypeExtension 96 | 97 | - 98 | class: PHPStan\Type\Nette\ServiceLocatorDynamicReturnTypeExtension 99 | tags: 100 | - phpstan.broker.dynamicMethodReturnTypeExtension 101 | 102 | - 103 | class: PHPStan\Type\Nette\FormContainerUnsafeValuesDynamicReturnTypeExtension 104 | tags: 105 | - phpstan.broker.dynamicMethodReturnTypeExtension 106 | 107 | - 108 | class: PHPStan\Type\Nette\FormContainerValuesDynamicReturnTypeExtension 109 | tags: 110 | - phpstan.broker.dynamicMethodReturnTypeExtension 111 | 112 | - 113 | class: PHPStan\Rule\Nette\PresenterInjectedPropertiesExtension 114 | tags: 115 | - phpstan.properties.readWriteExtension 116 | 117 | - 118 | class: PHPStan\Type\Nette\StringsMatchAllDynamicReturnTypeExtension 119 | tags: 120 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 121 | 122 | - 123 | class: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension 124 | tags: 125 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 126 | 127 | - 128 | class: PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension 129 | tags: 130 | - phpstan.staticMethodParameterClosureTypeExtension 131 | -------------------------------------------------------------------------------- /rules.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | methodsThrowingExceptions: 3 | Nette\Application\UI\Presenter: 4 | redirectUrl: Nette\Application\AbortException 5 | sendJson: Nette\Application\AbortException 6 | sendResponse: Nette\Application\AbortException 7 | terminate: Nette\Application\AbortException 8 | forward: Nette\Application\AbortException 9 | Nette\Application\UI\Component: 10 | redirect: Nette\Application\AbortException 11 | redirectPermanent: Nette\Application\AbortException 12 | error: Nette\Application\BadRequestException 13 | 14 | parametersSchema: 15 | methodsThrowingExceptions: arrayOf(arrayOf(string())) 16 | 17 | rules: 18 | - PHPStan\Rule\Nette\DoNotExtendNetteObjectRule 19 | - PHPStan\Rule\Nette\RegularExpressionPatternRule 20 | 21 | services: 22 | - 23 | class: PHPStan\Rule\Nette\RethrowExceptionRule 24 | arguments: 25 | methods: %methodsThrowingExceptions% 26 | tags: 27 | - phpstan.rules.rule 28 | -------------------------------------------------------------------------------- /src/Reflection/Nette/HtmlClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | is('Nette\Utils\Html'); 17 | } 18 | 19 | public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection 20 | { 21 | return new HtmlMethodReflection($methodName, $classReflection); 22 | } 23 | 24 | public function hasProperty(ClassReflection $classReflection, string $propertyName): bool 25 | { 26 | return $classReflection->is('Nette\Utils\Html'); 27 | } 28 | 29 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection 30 | { 31 | return new HtmlPropertyReflection($classReflection); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Reflection/Nette/HtmlMethodReflection.php: -------------------------------------------------------------------------------- 1 | name = $name; 27 | $this->declaringClass = $declaringClass; 28 | } 29 | 30 | public function getDeclaringClass(): ClassReflection 31 | { 32 | return $this->declaringClass; 33 | } 34 | 35 | public function getPrototype(): ClassMemberReflection 36 | { 37 | return $this; 38 | } 39 | 40 | public function isStatic(): bool 41 | { 42 | return false; 43 | } 44 | 45 | public function getVariants(): array 46 | { 47 | return [ 48 | new FunctionVariant( 49 | TemplateTypeMap::createEmpty(), 50 | TemplateTypeMap::createEmpty(), 51 | [], 52 | true, 53 | substr($this->name, 0, 3) === 'get' ? new MixedType() : new ObjectType('Nette\Utils\Html'), 54 | ), 55 | ]; 56 | } 57 | 58 | public function isPrivate(): bool 59 | { 60 | return false; 61 | } 62 | 63 | public function isPublic(): bool 64 | { 65 | return true; 66 | } 67 | 68 | public function getName(): string 69 | { 70 | return $this->name; 71 | } 72 | 73 | public function isDeprecated(): TrinaryLogic 74 | { 75 | return TrinaryLogic::createNo(); 76 | } 77 | 78 | public function getDeprecatedDescription(): ?string 79 | { 80 | return null; 81 | } 82 | 83 | public function isFinal(): TrinaryLogic 84 | { 85 | return TrinaryLogic::createNo(); 86 | } 87 | 88 | public function isInternal(): TrinaryLogic 89 | { 90 | return TrinaryLogic::createNo(); 91 | } 92 | 93 | public function getThrowType(): Type 94 | { 95 | return new VoidType(); 96 | } 97 | 98 | public function getDocComment(): ?string 99 | { 100 | return null; 101 | } 102 | 103 | public function hasSideEffects(): TrinaryLogic 104 | { 105 | return TrinaryLogic::createYes(); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/Reflection/Nette/HtmlPropertyReflection.php: -------------------------------------------------------------------------------- 1 | declaringClass = $declaringClass; 21 | $this->type = new MixedType(); 22 | } 23 | 24 | public function getDeclaringClass(): ClassReflection 25 | { 26 | return $this->declaringClass; 27 | } 28 | 29 | public function isStatic(): bool 30 | { 31 | return false; 32 | } 33 | 34 | public function isPrivate(): bool 35 | { 36 | return false; 37 | } 38 | 39 | public function isPublic(): bool 40 | { 41 | return true; 42 | } 43 | 44 | public function isReadable(): bool 45 | { 46 | return true; 47 | } 48 | 49 | public function isWritable(): bool 50 | { 51 | return true; 52 | } 53 | 54 | public function isDeprecated(): TrinaryLogic 55 | { 56 | return TrinaryLogic::createNo(); 57 | } 58 | 59 | public function getDeprecatedDescription(): ?string 60 | { 61 | return null; 62 | } 63 | 64 | public function isInternal(): TrinaryLogic 65 | { 66 | return TrinaryLogic::createNo(); 67 | } 68 | 69 | public function getDocComment(): ?string 70 | { 71 | return null; 72 | } 73 | 74 | public function getReadableType(): Type 75 | { 76 | return $this->type; 77 | } 78 | 79 | public function getWritableType(): Type 80 | { 81 | return $this->type; 82 | } 83 | 84 | public function canChangeTypeAfterAssignment(): bool 85 | { 86 | return true; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Reflection/Nette/NetteObjectClassReflectionExtension.php: -------------------------------------------------------------------------------- 1 | inheritsFromNetteObject($classReflection->getNativeReflection())) { 27 | return false; 28 | } 29 | 30 | $getterMethod = $this->getMethodByProperty($classReflection, $propertyName); 31 | if ($getterMethod === null) { 32 | return false; 33 | } 34 | if ($getterMethod->isStatic()) { 35 | return false; 36 | } 37 | 38 | return $getterMethod->isPublic(); 39 | } 40 | 41 | private function getMethodByProperty(ClassReflection $classReflection, string $propertyName): ?MethodReflection 42 | { 43 | $getterMethodName = sprintf('get%s', ucfirst($propertyName)); 44 | if (!$classReflection->hasNativeMethod($getterMethodName)) { 45 | return null; 46 | } 47 | 48 | return $classReflection->getNativeMethod($getterMethodName); 49 | } 50 | 51 | public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection 52 | { 53 | /** @var MethodReflection $getterMethod */ 54 | $getterMethod = $this->getMethodByProperty($classReflection, $propertyName); 55 | return new NetteObjectPropertyReflection($classReflection, $getterMethod->getVariants()[0]->getReturnType()); 56 | } 57 | 58 | public function hasMethod(ClassReflection $classReflection, string $methodName): bool 59 | { 60 | $traitNames = $this->getTraitNames($classReflection->getNativeReflection()); 61 | if (!in_array('Nette\SmartObject', $traitNames, true) && !$this->inheritsFromNetteObject($classReflection->getNativeReflection())) { 62 | return false; 63 | } 64 | 65 | if (substr($methodName, 0, 2) !== 'on' || strlen($methodName) <= 2) { 66 | return false; 67 | } 68 | 69 | return $classReflection->hasNativeProperty($methodName) && $classReflection->getNativeProperty($methodName)->isPublic(); 70 | } 71 | 72 | public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection 73 | { 74 | return new NetteObjectEventListenerMethodReflection($methodName, $classReflection); 75 | } 76 | 77 | /** 78 | * @param ReflectionClass|ReflectionEnum $class 79 | * @return string[] 80 | */ 81 | private function getTraitNames($class): array 82 | { 83 | $traitNames = $class->getTraitNames(); 84 | while ($class->getParentClass() !== false) { 85 | $traitNames = array_values(array_unique(array_merge($traitNames, $class->getParentClass()->getTraitNames()))); 86 | $class = $class->getParentClass(); 87 | } 88 | 89 | return $traitNames; 90 | } 91 | 92 | /** 93 | * @param ReflectionClass|ReflectionEnum $class 94 | */ 95 | private function inheritsFromNetteObject($class): bool 96 | { 97 | $class = $class->getParentClass(); 98 | while ($class !== false) { 99 | if (in_array($class->getName(), [ // @phpstan-ignore-line 100 | 'Nette\Object', 101 | 'Nette\LegacyObject', 102 | ], true)) { 103 | return true; 104 | } 105 | $class = $class->getParentClass(); 106 | } 107 | 108 | return false; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/Reflection/Nette/NetteObjectEventListenerMethodReflection.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | $this->declaringClass = $declaringClass; 25 | } 26 | 27 | public function getName(): string 28 | { 29 | return $this->name; 30 | } 31 | 32 | public function getDeclaringClass(): ClassReflection 33 | { 34 | return $this->declaringClass; 35 | } 36 | 37 | public function getPrototype(): ClassMemberReflection 38 | { 39 | return $this; 40 | } 41 | 42 | public function isStatic(): bool 43 | { 44 | return false; 45 | } 46 | 47 | public function getVariants(): array 48 | { 49 | return [ 50 | new FunctionVariant( 51 | TemplateTypeMap::createEmpty(), 52 | TemplateTypeMap::createEmpty(), 53 | [], 54 | true, 55 | new VoidType(), 56 | ), 57 | ]; 58 | } 59 | 60 | public function isPrivate(): bool 61 | { 62 | return false; 63 | } 64 | 65 | public function isPublic(): bool 66 | { 67 | return true; 68 | } 69 | 70 | public function isDeprecated(): TrinaryLogic 71 | { 72 | return TrinaryLogic::createNo(); 73 | } 74 | 75 | public function getDeprecatedDescription(): ?string 76 | { 77 | return null; 78 | } 79 | 80 | public function isFinal(): TrinaryLogic 81 | { 82 | return TrinaryLogic::createNo(); 83 | } 84 | 85 | public function isInternal(): TrinaryLogic 86 | { 87 | return TrinaryLogic::createNo(); 88 | } 89 | 90 | public function getThrowType(): ?Type 91 | { 92 | return null; 93 | } 94 | 95 | public function getDocComment(): ?string 96 | { 97 | return null; 98 | } 99 | 100 | public function hasSideEffects(): TrinaryLogic 101 | { 102 | return TrinaryLogic::createYes(); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/Reflection/Nette/NetteObjectPropertyReflection.php: -------------------------------------------------------------------------------- 1 | declaringClass = $declaringClass; 20 | $this->type = $type; 21 | } 22 | 23 | public function getDeclaringClass(): ClassReflection 24 | { 25 | return $this->declaringClass; 26 | } 27 | 28 | public function isStatic(): bool 29 | { 30 | return false; 31 | } 32 | 33 | public function isPrivate(): bool 34 | { 35 | return false; 36 | } 37 | 38 | public function isPublic(): bool 39 | { 40 | return true; 41 | } 42 | 43 | public function isReadable(): bool 44 | { 45 | return true; 46 | } 47 | 48 | public function isWritable(): bool 49 | { 50 | return true; 51 | } 52 | 53 | public function isDeprecated(): TrinaryLogic 54 | { 55 | return TrinaryLogic::createNo(); 56 | } 57 | 58 | public function getDeprecatedDescription(): ?string 59 | { 60 | return null; 61 | } 62 | 63 | public function isInternal(): TrinaryLogic 64 | { 65 | return TrinaryLogic::createNo(); 66 | } 67 | 68 | public function getDocComment(): ?string 69 | { 70 | return null; 71 | } 72 | 73 | public function getReadableType(): Type 74 | { 75 | return $this->type; 76 | } 77 | 78 | public function getWritableType(): Type 79 | { 80 | return $this->type; 81 | } 82 | 83 | public function canChangeTypeAfterAssignment(): bool 84 | { 85 | return true; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Rule/Nette/DoNotExtendNetteObjectRule.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class DoNotExtendNetteObjectRule implements Rule 17 | { 18 | 19 | public function getNodeType(): string 20 | { 21 | return InClassNode::class; 22 | } 23 | 24 | public function processNode(Node $node, Scope $scope): array 25 | { 26 | $classReflection = $node->getClassReflection(); 27 | $parentClass = $classReflection->getNativeReflection()->getParentClass(); 28 | if ($parentClass !== false && in_array($parentClass->getName(), [ // @phpstan-ignore-line 29 | 'Nette\Object', 30 | 'Nette\LegacyObject', 31 | ], true)) { 32 | return [ 33 | RuleErrorBuilder::message(sprintf( 34 | "Class %s extends %s - it's better to use %s trait.", 35 | $classReflection->getDisplayName(), 36 | 'Nette\Object', 37 | 'Nette\SmartObject', 38 | ))->identifier('class.extendsNetteObject')->build(), 39 | ]; 40 | } 41 | 42 | return []; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/Rule/Nette/PresenterInjectedPropertiesExtension.php: -------------------------------------------------------------------------------- 1 | isInitialized($property, $propertyName); 20 | } 21 | 22 | public function isInitialized(PropertyReflection $property, string $propertyName): bool 23 | { 24 | return $property->isPublic() && 25 | strpos($property->getDocComment() ?? '', '@inject') !== false; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Rule/Nette/RegularExpressionPatternRule.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class RegularExpressionPatternRule implements Rule 21 | { 22 | 23 | public function getNodeType(): string 24 | { 25 | return StaticCall::class; 26 | } 27 | 28 | public function processNode(Node $node, Scope $scope): array 29 | { 30 | $patterns = $this->extractPatterns($node, $scope); 31 | 32 | $errors = []; 33 | foreach ($patterns as $pattern) { 34 | $errorMessage = $this->validatePattern($pattern); 35 | if ($errorMessage === null) { 36 | continue; 37 | } 38 | 39 | $errors[] = RuleErrorBuilder::message(sprintf('Regex pattern is invalid: %s', $errorMessage)) 40 | ->identifier('regexp.pattern') 41 | ->build(); 42 | } 43 | 44 | return $errors; 45 | } 46 | 47 | /** 48 | * @return string[] 49 | */ 50 | private function extractPatterns(StaticCall $staticCall, Scope $scope): array 51 | { 52 | if (!$staticCall->class instanceof Node\Name || !$staticCall->name instanceof Node\Identifier) { 53 | return []; 54 | } 55 | $caller = $scope->resolveTypeByName($staticCall->class); 56 | if (!(new ObjectType(Strings::class))->isSuperTypeOf($caller)->yes()) { 57 | return []; 58 | } 59 | $methodName = strtolower((string) $staticCall->name); 60 | if ( 61 | !in_array($methodName, [ 62 | 'split', 63 | 'match', 64 | 'matchall', 65 | 'replace', 66 | ], true) 67 | ) { 68 | return []; 69 | } 70 | 71 | if (!isset($staticCall->getArgs()[1])) { 72 | return []; 73 | } 74 | $patternNode = $staticCall->getArgs()[1]->value; 75 | $patternType = $scope->getType($patternNode); 76 | 77 | $patternStrings = []; 78 | 79 | foreach ($patternType->getConstantStrings() as $constantStringType) { 80 | $patternStrings[] = $constantStringType->getValue(); 81 | } 82 | 83 | foreach ($patternType->getConstantArrays() as $constantArrayType) { 84 | if ($methodName !== 'replace') { 85 | continue; 86 | } 87 | 88 | foreach ($constantArrayType->getKeyTypes() as $arrayKeyType) { 89 | foreach ($arrayKeyType->getConstantStrings() as $constantString) { 90 | $patternStrings[] = $constantString->getValue(); 91 | } 92 | } 93 | } 94 | 95 | return $patternStrings; 96 | } 97 | 98 | private function validatePattern(string $pattern): ?string 99 | { 100 | try { 101 | Strings::match('', $pattern); 102 | } catch (RegexpException $e) { 103 | return $e->getMessage(); 104 | } 105 | 106 | return null; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/Rule/Nette/RethrowExceptionRule.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class RethrowExceptionRule implements Rule 28 | { 29 | 30 | /** @var array */ 31 | private array $methods; 32 | 33 | /** 34 | * @param string[][] $methods 35 | */ 36 | public function __construct(array $methods) 37 | { 38 | $this->methods = $methods; 39 | } 40 | 41 | public function getNodeType(): string 42 | { 43 | return TryCatch::class; 44 | } 45 | 46 | public function processNode(Node $node, Scope $scope): array 47 | { 48 | $hasGeneralCatch = false; 49 | foreach ($node->catches as $catch) { 50 | foreach ($catch->types as $type) { 51 | $typeClass = (string) $type; 52 | if ($typeClass === 'Exception' || $typeClass === Throwable::class) { 53 | $hasGeneralCatch = true; 54 | break 2; 55 | } 56 | } 57 | } 58 | if (!$hasGeneralCatch) { 59 | return []; 60 | } 61 | 62 | $exceptions = $this->getExceptionTypes($scope, $node->stmts); 63 | if (count($exceptions) === 0) { 64 | return []; 65 | } 66 | 67 | $messages = []; 68 | foreach ($exceptions as $exceptionName) { 69 | $exceptionType = new ObjectType($exceptionName); 70 | foreach ($node->catches as $catch) { 71 | $caughtType = TypeCombinator::union(...array_map(static fn (Name $class): ObjectType => new ObjectType((string) $class), $catch->types)); 72 | if (!$caughtType->isSuperTypeOf($exceptionType)->yes()) { 73 | continue; 74 | } 75 | if ( 76 | count($catch->stmts) === 1 77 | && $catch->stmts[0] instanceof Node\Stmt\Expression 78 | && $catch->stmts[0]->expr instanceof Node\Expr\Throw_ 79 | && $catch->stmts[0]->expr->expr instanceof Variable 80 | && $catch->var !== null 81 | && is_string($catch->var->name) 82 | && is_string($catch->stmts[0]->expr->expr->name) 83 | && $catch->var->name === $catch->stmts[0]->expr->expr->name 84 | ) { 85 | continue 2; 86 | } 87 | } 88 | 89 | $messages[] = RuleErrorBuilder::message(sprintf('Exception %s needs to be rethrown.', $exceptionName))->identifier('nette.rethrowException')->build(); 90 | } 91 | 92 | return $messages; 93 | } 94 | 95 | /** 96 | * @param Node|Node[]|scalar $node 97 | * @return string[] 98 | */ 99 | private function getExceptionTypes(Scope $scope, $node): array 100 | { 101 | $exceptions = []; 102 | if ($node instanceof Node) { 103 | foreach ($node->getSubNodeNames() as $subNodeName) { 104 | $subNode = $node->{$subNodeName}; 105 | $exceptions = array_merge($exceptions, $this->getExceptionTypes($scope, $subNode)); 106 | } 107 | if ($node instanceof Node\Expr\MethodCall) { 108 | $methodCalledOn = $scope->getType($node->var); 109 | foreach ($this->methods as $type => $methods) { 110 | if (!$node->name instanceof Node\Identifier) { 111 | continue; 112 | } 113 | if (!(new ObjectType($type))->isSuperTypeOf($methodCalledOn)->yes()) { 114 | continue; 115 | } 116 | 117 | $methodName = strtolower((string) $node->name); 118 | foreach ($methods as $throwingMethodName => $exception) { 119 | if (strtolower($throwingMethodName) !== $methodName) { 120 | continue; 121 | } 122 | $exceptions[] = $exception; 123 | } 124 | } 125 | } 126 | } elseif (is_array($node)) { 127 | foreach ($node as $subNode) { 128 | $exceptions = array_merge($exceptions, $this->getExceptionTypes($scope, $subNode)); 129 | } 130 | } 131 | 132 | return array_unique($exceptions); 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/Type/Nette/ComponentGetPresenterDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getPresenter'; 25 | } 26 | 27 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 28 | { 29 | $methodDefinition = ParametersAcceptorSelector::selectFromArgs( 30 | $scope, 31 | $methodCall->getArgs(), 32 | $methodReflection->getVariants(), 33 | ); 34 | $defaultReturnType = $methodDefinition->getReturnType(); 35 | $firstParameterExists = count($methodDefinition->getParameters()) > 0; 36 | 37 | if (count($methodCall->getArgs()) < 1) { 38 | if (!$firstParameterExists) { 39 | return TypeCombinator::removeNull($defaultReturnType); 40 | } 41 | 42 | return $defaultReturnType; 43 | } 44 | 45 | $paramNeedExpr = $methodCall->getArgs()[0]->value; 46 | $paramNeedType = $scope->getType($paramNeedExpr); 47 | 48 | if ($paramNeedType->isTrue()->yes()) { 49 | return TypeCombinator::removeNull($defaultReturnType); 50 | } 51 | if ($paramNeedType->isFalse()->yes()) { 52 | return TypeCombinator::addNull($defaultReturnType); 53 | } 54 | 55 | return $defaultReturnType; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/Type/Nette/ComponentLookupDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'lookup'; 25 | } 26 | 27 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 28 | { 29 | $defaultReturnType = ParametersAcceptorSelector::selectFromArgs( 30 | $scope, 31 | $methodCall->getArgs(), 32 | $methodReflection->getVariants(), 33 | )->getReturnType(); 34 | if (count($methodCall->getArgs()) < 2) { 35 | return $defaultReturnType; 36 | } 37 | 38 | $paramNeedExpr = $methodCall->getArgs()[1]->value; 39 | $paramNeedType = $scope->getType($paramNeedExpr); 40 | 41 | if ($paramNeedType->isTrue()->yes()) { 42 | return TypeCombinator::removeNull($defaultReturnType); 43 | } 44 | if ($paramNeedType->isFalse()->yes()) { 45 | return TypeCombinator::addNull($defaultReturnType); 46 | } 47 | 48 | return $defaultReturnType; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Type/Nette/ComponentModelArrayAccessDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'offsetGet'; 34 | } 35 | 36 | public function getTypeFromMethodCall( 37 | MethodReflection $methodReflection, 38 | MethodCall $methodCall, 39 | Scope $scope 40 | ): Type 41 | { 42 | $calledOnType = $scope->getType($methodCall->var); 43 | $defaultType = $calledOnType->getMethod('createComponent', $scope)->getVariants()[0]->getReturnType(); 44 | $defaultType = TypeCombinator::remove($defaultType, new NullType()); 45 | if ($defaultType->isSuperTypeOf(new ObjectType('Nette\ComponentModel\IComponent'))->yes()) { 46 | $defaultType = new MixedType(false, new NullType()); 47 | } 48 | $args = $methodCall->getArgs(); 49 | if (count($args) < 1) { 50 | return $defaultType; 51 | } 52 | 53 | $argType = $scope->getType($args[0]->value); 54 | if (count($argType->getConstantStrings()) === 0) { 55 | return $defaultType; 56 | } 57 | 58 | $types = []; 59 | foreach ($argType->getConstantStrings() as $constantString) { 60 | $componentName = $constantString->getValue(); 61 | 62 | $methodName = sprintf('createComponent%s', ucfirst($componentName)); 63 | if (!$calledOnType->hasMethod($methodName)->yes()) { 64 | return $defaultType; 65 | } 66 | 67 | $method = $calledOnType->getMethod($methodName, $scope); 68 | 69 | $types[] = ParametersAcceptorSelector::selectFromArgs( 70 | $scope, 71 | [new Arg(new String_($componentName))], 72 | $method->getVariants(), 73 | )->getReturnType(); 74 | } 75 | 76 | return TypeCombinator::union(...$types); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Type/Nette/ComponentModelDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getComponent'; 34 | } 35 | 36 | public function getTypeFromMethodCall( 37 | MethodReflection $methodReflection, 38 | MethodCall $methodCall, 39 | Scope $scope 40 | ): Type 41 | { 42 | $calledOnType = $scope->getType($methodCall->var); 43 | $defaultType = $calledOnType->getMethod('createComponent', $scope)->getVariants()[0]->getReturnType(); 44 | if ($defaultType->isSuperTypeOf(new ObjectType('Nette\ComponentModel\IComponent'))->yes()) { 45 | $defaultType = new MixedType(); 46 | } 47 | $args = $methodCall->getArgs(); 48 | if (count($args) < 1) { 49 | return $defaultType; 50 | } 51 | 52 | $throw = true; 53 | if (isset($args[1])) { 54 | $throwType = $scope->getType($args[1]->value); 55 | if (!$throwType->isTrue()->yes()) { 56 | $throw = false; 57 | } 58 | } 59 | 60 | if ($throw) { 61 | $defaultType = TypeCombinator::remove($defaultType, new NullType()); 62 | } 63 | 64 | $argType = $scope->getType($args[0]->value); 65 | if (count($argType->getConstantStrings()) === 0) { 66 | return $defaultType; 67 | } 68 | 69 | $types = []; 70 | foreach ($argType->getConstantStrings() as $constantString) { 71 | $componentName = $constantString->getValue(); 72 | 73 | $methodName = sprintf('createComponent%s', ucfirst($componentName)); 74 | if (!$calledOnType->hasMethod($methodName)->yes()) { 75 | return $defaultType; 76 | } 77 | 78 | $method = $calledOnType->getMethod($methodName, $scope); 79 | 80 | $types[] = ParametersAcceptorSelector::selectFromArgs( 81 | $scope, 82 | [new Arg(new String_($componentName))], 83 | $method->getVariants(), 84 | )->getReturnType(); 85 | } 86 | 87 | return TypeCombinator::union(...$types); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Type/Nette/FormContainerUnsafeValuesDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getUnsafeValues'; 27 | } 28 | 29 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type 30 | { 31 | if (count($methodCall->getArgs()) === 0) { 32 | return null; 33 | } 34 | 35 | $arg = $methodCall->getArgs()[0]->value; 36 | $scopedType = $scope->getType($arg); 37 | if ($scopedType->isNull()->yes()) { 38 | return new ObjectType('Nette\Utils\ArrayHash'); 39 | } 40 | 41 | if (count($scopedType->getConstantStrings()) === 1 && $scopedType->getConstantStrings()[0]->getValue() === 'array') { 42 | return new ArrayType(new StringType(), new MixedType()); 43 | } 44 | 45 | return null; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Type/Nette/FormContainerValuesDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getValues'; 27 | } 28 | 29 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type 30 | { 31 | if (count($methodCall->getArgs()) === 0) { 32 | return new ObjectType('Nette\Utils\ArrayHash'); 33 | } 34 | 35 | $arg = $methodCall->getArgs()[0]->value; 36 | $scopedType = $scope->getType($arg); 37 | if ($scopedType->isTrue()->yes()) { 38 | return new ArrayType(new StringType(), new MixedType()); 39 | } 40 | if ($scopedType->isFalse()->yes()) { 41 | return new ObjectType('Nette\Utils\ArrayHash'); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Type/Nette/FormsBaseControlDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass()->getName() === 'Nette\Forms\Controls\BaseControl'; 26 | } 27 | 28 | public function getTypeFromMethodCall( 29 | MethodReflection $methodReflection, 30 | MethodCall $methodCall, 31 | Scope $scope 32 | ): Type 33 | { 34 | $returnType = ParametersAcceptorSelector::selectFromArgs( 35 | $scope, 36 | $methodCall->getArgs(), 37 | $methodReflection->getVariants(), 38 | )->getReturnType(); 39 | $referencedClasses = $returnType->getReferencedClasses(); 40 | if ( 41 | count($referencedClasses) === 1 42 | && $referencedClasses[0] === 'Nette\Forms\Controls\BaseControl' 43 | ) { 44 | return $scope->getType($methodCall->var); 45 | } 46 | 47 | return $returnType; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Type/Nette/PresenterGetSessionReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getSession'; 24 | } 25 | 26 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 27 | { 28 | if (count($methodCall->getArgs()) === 0 || $scope->getType($methodCall->getArgs()[0]->value)->isNull()->yes()) { 29 | return new ObjectType('Nette\Http\Session'); 30 | } 31 | 32 | return new ObjectType('Nette\Http\SessionSection'); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Type/Nette/ServiceLocatorDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName(), [ 28 | 'getByType', 29 | 'createInstance', 30 | 'getService', 31 | 'createService', 32 | ], true); 33 | } 34 | 35 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 36 | { 37 | $mixedType = new MixedType(); 38 | if (in_array($methodReflection->getName(), [ 39 | 'getService', 40 | 'createService', 41 | ], true)) { 42 | return $mixedType; 43 | } 44 | if (count($methodCall->getArgs()) === 0) { 45 | return $mixedType; 46 | } 47 | $argType = $scope->getType($methodCall->getArgs()[0]->value); 48 | if (count($argType->getConstantStrings()) === 0) { 49 | return $mixedType; 50 | } 51 | 52 | $types = []; 53 | foreach ($argType->getConstantStrings() as $constantString) { 54 | $type = new ObjectType($constantString->getValue()); 55 | if ( 56 | $methodReflection->getName() === 'getByType' 57 | && count($methodCall->getArgs()) >= 2 58 | ) { 59 | $throwType = $scope->getType($methodCall->getArgs()[1]->value); 60 | if (!$throwType->isTrue()->yes()) { 61 | $type = TypeCombinator::addNull($type); 62 | } 63 | } 64 | 65 | $types[] = $type; 66 | } 67 | 68 | return TypeCombinator::union(...$types); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | regexArrayShapeMatcher = $regexArrayShapeMatcher; 30 | } 31 | 32 | public function getClass(): string 33 | { 34 | return Strings::class; 35 | } 36 | 37 | public function isStaticMethodSupported(MethodReflection $methodReflection): bool 38 | { 39 | return $methodReflection->getName() === 'matchAll'; 40 | } 41 | 42 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type 43 | { 44 | $args = $methodCall->getArgs(); 45 | $patternArg = $args[1] ?? null; 46 | 47 | if ($patternArg === null) { 48 | return null; 49 | } 50 | 51 | return $this->regexArrayShapeMatcher->matchAllExpr( 52 | $patternArg->value, 53 | $this->resolveFlagsType($args, $scope), 54 | TrinaryLogic::createYes(), 55 | $scope, 56 | ); 57 | } 58 | 59 | /** 60 | * @param array $args 61 | */ 62 | private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType 63 | { 64 | if (!array_key_exists(2, $args)) { 65 | return new ConstantIntegerType(PREG_SET_ORDER); 66 | } 67 | 68 | $captureOffsetType = $scope->getType($args[2]->value); 69 | 70 | if ($captureOffsetType instanceof ConstantIntegerType) { 71 | $captureOffset = $captureOffsetType->getValue(); 72 | $flags = ($captureOffset & PREG_PATTERN_ORDER) === PREG_PATTERN_ORDER ? $captureOffset : ($captureOffset | PREG_SET_ORDER); 73 | 74 | return new ConstantIntegerType($flags); 75 | } 76 | 77 | $unmatchedAsNullType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false); 78 | $patternOrderType = array_key_exists(5, $args) ? $scope->getType($args[5]->value) : new ConstantBooleanType(false); 79 | 80 | $captureOffset = $captureOffsetType->isTrue()->yes(); 81 | $unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes(); 82 | $patternOrder = $patternOrderType->isTrue()->yes(); 83 | 84 | return new ConstantIntegerType( 85 | ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0), 86 | ); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | regexArrayShapeMatcher = $regexArrayShapeMatcher; 30 | } 31 | 32 | public function getClass(): string 33 | { 34 | return Strings::class; 35 | } 36 | 37 | public function isStaticMethodSupported(MethodReflection $methodReflection): bool 38 | { 39 | return $methodReflection->getName() === 'match'; 40 | } 41 | 42 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type 43 | { 44 | $args = $methodCall->getArgs(); 45 | $patternArg = $args[1] ?? null; 46 | 47 | if ($patternArg === null) { 48 | return null; 49 | } 50 | 51 | $arrayShape = $this->regexArrayShapeMatcher->matchExpr( 52 | $patternArg->value, 53 | $this->resolveFlagsType($args, $scope), 54 | TrinaryLogic::createYes(), 55 | $scope, 56 | ); 57 | 58 | if ($arrayShape === null) { 59 | return null; 60 | } 61 | 62 | return TypeCombinator::union($arrayShape, new NullType()); 63 | } 64 | 65 | /** 66 | * @param array $args 67 | */ 68 | private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType 69 | { 70 | if (!array_key_exists(2, $args)) { 71 | return new ConstantIntegerType(0); 72 | } 73 | 74 | $captureOffsetType = $scope->getType($args[2]->value); 75 | 76 | if ($captureOffsetType instanceof ConstantIntegerType) { 77 | return $captureOffsetType; 78 | } 79 | 80 | $unmatchedAsNullType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false); 81 | 82 | $captureOffset = $captureOffsetType->isTrue()->yes(); 83 | $unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes(); 84 | 85 | return new ConstantIntegerType( 86 | ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0), 87 | ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Type/Nette/StringsReplaceCallbackClosureTypeExtension.php: -------------------------------------------------------------------------------- 1 | regexArrayShapeMatcher = $regexArrayShapeMatcher; 32 | } 33 | 34 | public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool 35 | { 36 | return $methodReflection->getDeclaringClass()->getName() === Strings::class 37 | && $methodReflection->getName() === 'replace' 38 | && $parameter->getName() === 'replacement'; 39 | } 40 | 41 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type 42 | { 43 | $args = $methodCall->getArgs(); 44 | $patternArg = $args[1] ?? null; 45 | $replacementArg = $args[2] ?? null; 46 | 47 | if ($patternArg === null || $replacementArg === null) { 48 | return null; 49 | } 50 | 51 | $replacementType = $scope->getType($replacementArg->value); 52 | 53 | if (!$replacementType->isCallable()->yes()) { 54 | return null; 55 | } 56 | 57 | $matchesType = $this->regexArrayShapeMatcher->matchExpr( 58 | $patternArg->value, 59 | $this->resolveFlagsType($args, $scope), 60 | TrinaryLogic::createYes(), 61 | $scope, 62 | ); 63 | 64 | if ($matchesType === null) { 65 | return null; 66 | } 67 | 68 | return new ClosureType( 69 | [ 70 | $this->createParameterReflectionClass($parameter, $matchesType), 71 | ], 72 | new StringType(), 73 | ); 74 | } 75 | 76 | /** 77 | * @param array $args 78 | */ 79 | private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType 80 | { 81 | $captureOffsetType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false); 82 | $unmatchedAsNullType = array_key_exists(5, $args) ? $scope->getType($args[5]->value) : new ConstantBooleanType(false); 83 | 84 | $captureOffset = $captureOffsetType->isTrue()->yes(); 85 | $unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes(); 86 | 87 | return new ConstantIntegerType(($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0)); 88 | } 89 | 90 | private function createParameterReflectionClass(ParameterReflection $parameter, Type $matchesType): ParameterReflection 91 | { 92 | return new class($parameter, $matchesType) implements ParameterReflection { 93 | 94 | private ParameterReflection $parameter; 95 | 96 | private Type $matchesType; 97 | 98 | public function __construct( 99 | ParameterReflection $parameter, 100 | Type $matchesType 101 | ) 102 | { 103 | $this->parameter = $parameter; 104 | $this->matchesType = $matchesType; 105 | } 106 | 107 | public function getName(): string 108 | { 109 | return $this->parameter->getName(); 110 | } 111 | 112 | public function isOptional(): bool 113 | { 114 | return $this->parameter->isOptional(); 115 | } 116 | 117 | public function getType(): Type 118 | { 119 | return $this->matchesType; 120 | } 121 | 122 | public function passedByReference(): PassedByReference 123 | { 124 | return $this->parameter->passedByReference(); 125 | } 126 | 127 | public function isVariadic(): bool 128 | { 129 | return $this->parameter->isVariadic(); 130 | } 131 | 132 | public function getDefaultValue(): ?Type 133 | { 134 | return $this->parameter->getDefaultValue(); 135 | } 136 | 137 | }; 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /stubs/Application/Routers/RouteList.stub: -------------------------------------------------------------------------------- 1 | 7 | * @phpstan-implements \ArrayAccess 8 | */ 9 | class RouteList implements \ArrayAccess, \IteratorAggregate 10 | { 11 | /** 12 | * @phpstan-return \ArrayIterator 13 | */ 14 | public function getIterator(): \ArrayIterator 15 | { 16 | // nothing 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /stubs/Application/UI/Component.stub: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | abstract class Component implements \ArrayAccess 11 | { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /stubs/Application/UI/Multiplier.stub: -------------------------------------------------------------------------------- 1 | ) : T $factory 12 | */ 13 | public function __construct(callable $factory); 14 | 15 | /** 16 | * @return T 17 | */ 18 | protected function createComponent(string $name): \Nette\ComponentModel\IComponent; 19 | } 20 | -------------------------------------------------------------------------------- /stubs/Application/UI/Presenter.stub: -------------------------------------------------------------------------------- 1 | |null $dependencies 10 | * @param-immediately-invoked-callable $generator 11 | */ 12 | public function load(mixed $key, ?callable $generator = null, ?array $dependencies = null): mixed 13 | { 14 | } 15 | 16 | /** 17 | * @param array $keys 18 | * @return array 19 | * @param-immediately-invoked-callable $generator 20 | */ 21 | public function bulkLoad(array $keys, ?callable $generator = null): array 22 | { 23 | } 24 | 25 | /** 26 | * @param-immediately-invoked-callable $function 27 | */ 28 | public function call(callable $function): mixed 29 | { 30 | } 31 | 32 | /** 33 | * @param array|null $dependencies 34 | * @param-immediately-invoked-callable $function 35 | */ 36 | public function wrap(callable $function, ?array $dependencies = null): \Closure 37 | { 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /stubs/ComponentModel/Component.stub: -------------------------------------------------------------------------------- 1 | $filterType 11 | * @phpstan-return ($filterType is null ? \Iterator : \Iterator) 12 | */ 13 | public function getComponents(bool $deep = false, string $filterType = null): \Iterator 14 | { 15 | // nothing 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /stubs/ComponentModel/IComponent.stub: -------------------------------------------------------------------------------- 1 | 7 | * @phpstan-extends \ArrayAccess 8 | */ 9 | interface IRow extends \Traversable, \ArrayAccess 10 | { 11 | 12 | } 13 | 14 | /** 15 | * @phpstan-extends \Traversable 16 | */ 17 | interface IRowContainer extends \Traversable 18 | { 19 | 20 | } 21 | 22 | /** 23 | * @phpstan-implements \Iterator 24 | */ 25 | class ResultSet implements \Iterator 26 | { 27 | 28 | } 29 | -------------------------------------------------------------------------------- /stubs/Database/Table/ActiveRow.stub: -------------------------------------------------------------------------------- 1 | > 9 | */ 10 | class ActiveRow implements \IteratorAggregate 11 | { 12 | /** 13 | * @phpstan-return \ArrayIterator> 14 | */ 15 | public function getIterator(): \ArrayIterator 16 | { 17 | // nothing 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /stubs/Database/Table/Selection.stub: -------------------------------------------------------------------------------- 1 | 8 | * @phpstan-implements \ArrayAccess 9 | */ 10 | class Selection implements \Iterator, \ArrayAccess 11 | { 12 | /** 13 | * @phpstan-param positive-int|0|null $limit 14 | * @phpstan-param positive-int|0|null $offset 15 | * @return static 16 | */ 17 | public function limit(?int $limit, int $offset = null) 18 | { 19 | } 20 | 21 | /** 22 | * @phpstan-param positive-int|0 $page 23 | * @phpstan-param positive-int|0 $itemsPerPage 24 | * @param int $numOfPages [optional] 25 | * @return static 26 | */ 27 | public function page(int $page, int $itemsPerPage, &$numOfPages = null) 28 | { 29 | } 30 | 31 | /** 32 | * @param string|array $condition 33 | * @param mixed $params 34 | * @return static 35 | */ 36 | public function where($condition, ...$params) 37 | { 38 | } 39 | 40 | /** 41 | * @param string $column 42 | * @return positive-int|0 43 | */ 44 | public function count($column = null) 45 | { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /stubs/Forms/Container.stub: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Container implements \ArrayAccess 14 | { 15 | 16 | /** @var array */ 17 | public $onValidate; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /stubs/Forms/Form.stub: -------------------------------------------------------------------------------- 1 | */ 9 | public $onSuccess; 10 | 11 | /** @var array */ 12 | public $onError; 13 | 14 | /** @var array */ 15 | public $onSubmit; 16 | 17 | /** @var array */ 18 | public $onRender; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /stubs/Forms/Rules.stub: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Rules implements \IteratorAggregate 14 | { 15 | 16 | /** 17 | * @return \ArrayIterator 18 | */ 19 | public function getIterator(): \ArrayIterator 20 | { 21 | // nothing 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /stubs/Http/FileUpload.stub: -------------------------------------------------------------------------------- 1 | 7 | * @implements \ArrayAccess 8 | */ 9 | class SessionSection implements \IteratorAggregate, \ArrayAccess 10 | { 11 | 12 | /** 13 | * @return \Iterator 14 | */ 15 | public function getIterator(): \Iterator 16 | { 17 | // nothing 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /stubs/Routing/Router.stub: -------------------------------------------------------------------------------- 1 | 7 | * @implements \ArrayAccess 8 | */ 9 | class ArrayHash implements \ArrayAccess, \IteratorAggregate 10 | { 11 | 12 | /** 13 | * @return \RecursiveArrayIterator 14 | */ 15 | public function getIterator(): \RecursiveArrayIterator 16 | { 17 | // nothing 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /stubs/Utils/Arrays.stub: -------------------------------------------------------------------------------- 1 | $array 12 | * @param callable(V, K, array): bool $callback 13 | * @param-immediately-invoked-callable $callback 14 | */ 15 | public static function some(iterable $array, callable $callback): bool 16 | { 17 | } 18 | 19 | /** 20 | * @template K of array-key 21 | * @template V 22 | * @param array $array 23 | * @param callable(V, K, array): bool $callback 24 | * @param-immediately-invoked-callable $callback 25 | */ 26 | public static function every(iterable $array, callable $callback): bool 27 | { 28 | } 29 | 30 | /** 31 | * @template K of array-key 32 | * @template V 33 | * @template R 34 | * @param array $array 35 | * @param callable(V, K, array): R $callback 36 | * @return array 37 | * @param-immediately-invoked-callable $callback 38 | */ 39 | public static function map(iterable $array, callable $callback): array 40 | { 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /stubs/Utils/Callback.stub: -------------------------------------------------------------------------------- 1 | 7 | * @implements \ArrayAccess 8 | */ 9 | class Html implements \ArrayAccess, \IteratorAggregate 10 | { 11 | 12 | /** 13 | * @return \ArrayIterator 14 | */ 15 | public function getIterator(): \ArrayIterator 16 | { 17 | // nothing 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /stubs/Utils/Paginator.stub: -------------------------------------------------------------------------------- 1 |