├── .editorconfig ├── .github └── workflows │ └── continious-integration.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build ├── .gitignore └── phpstan-with-extension.neon ├── composer.json ├── composer.lock ├── ecs.yaml ├── extension.neon ├── phpstan.bootstrap.php ├── phpstan.neon ├── phpunit.xml ├── src └── Type │ ├── ContextGetAspectDynamicReturnTypeExtension.php │ ├── GeneralUtilityDynamicReturnTypeExtension.php │ ├── ObjectManagerGetDynamicReturnTypeExtension.php │ ├── ObjectManagerInterfaceGetDynamicReturnTypeExtension.php │ ├── QueryExecuteReturnTypeExtension.php │ └── QueryInterfaceExecuteReturnTypeExtension.php └── tests ├── StaticCodeAnalysis ├── Constants.php ├── GeneralUtility │ └── MakeInstance │ │ ├── CallWithClassConstant.php │ │ ├── CallWithClassString.php │ │ ├── CallWithSelf.php │ │ └── CallWithStatic.php └── ObjectManager │ └── Get │ ├── CallWithClassConstant.php │ ├── CallWithClassString.php │ ├── CallWithSelf.php │ └── CallWithStatic.php ├── Unit └── Type │ ├── ContextGetAspectDynamicReturnTypeExtensionTest.php │ └── GeneralUtilityDynamicReturnTypeExtensionTest.php └── bootstrap.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_style = space 11 | indent_size = 4 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.neon] 16 | indent_size = 2 17 | 18 | [*.{yaml,yml}] 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/workflows/continious-integration.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | - "push" 3 | 4 | name: "Continuous Integration" 5 | 6 | jobs: 7 | coding-standards: 8 | name: "Coding Standards" 9 | 10 | runs-on: "ubuntu-latest" 11 | 12 | strategy: 13 | matrix: 14 | php-version: 15 | - "7.2" 16 | 17 | dependencies: 18 | - "locked" 19 | 20 | steps: 21 | - name: "Checkout" 22 | uses: "actions/checkout@v2" 23 | 24 | - name: "Install PHP with extensions" 25 | uses: "shivammathur/setup-php@v2" 26 | with: 27 | coverage: "none" 28 | php-version: "${{ matrix.php-version }}" 29 | 30 | - name: "Validate composer.json and composer.lock" 31 | run: "composer validate --strict" 32 | 33 | - name: "Determine composer cache directory" 34 | id: "determine-composer-cache-directory" 35 | run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" 36 | 37 | - name: "Cache dependencies installed with composer" 38 | uses: "actions/cache@v1" 39 | with: 40 | path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" 41 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}" 42 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-" 43 | 44 | - name: "Install locked dependencies with composer" 45 | run: "composer install --no-interaction --no-progress --no-suggest --no-plugins" 46 | 47 | - name: "Run ergebnis/composer-normalize" 48 | run: "composer normalize --dry-run" 49 | 50 | - name: "Run ecs" 51 | run: "vendor/bin/ecs check" 52 | 53 | static-code-analysis: 54 | name: "Static Code Analysis" 55 | 56 | runs-on: "ubuntu-latest" 57 | 58 | strategy: 59 | matrix: 60 | php-version: 61 | - "7.2" 62 | 63 | dependencies: 64 | - "lowest" 65 | - "locked" 66 | - "highest" 67 | 68 | steps: 69 | - name: "Checkout" 70 | uses: "actions/checkout@v2" 71 | 72 | - name: "Install PHP with extensions" 73 | uses: "shivammathur/setup-php@v2" 74 | with: 75 | coverage: "none" 76 | php-version: "${{ matrix.php-version }}" 77 | 78 | - name: "Determine composer cache directory" 79 | id: "determine-composer-cache-directory" 80 | run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" 81 | 82 | - name: "Cache dependencies installed with composer" 83 | uses: "actions/cache@v1" 84 | with: 85 | path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" 86 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}" 87 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-" 88 | 89 | - name: "Install lowest dependencies with composer" 90 | if: "matrix.dependencies == 'lowest'" 91 | run: "composer update --no-interaction --no-progress --no-suggest --no-plugins --prefer-lowest" 92 | 93 | - name: "Install locked dependencies with composer" 94 | if: "matrix.dependencies == 'locked'" 95 | run: "composer install --no-interaction --no-progress --no-suggest --no-plugins" 96 | 97 | - name: "Install highest dependencies with composer" 98 | if: "matrix.dependencies == 'highest'" 99 | run: "composer update --no-interaction --no-progress --no-suggest --no-plugins" 100 | 101 | - name: "Show phpstan/phpstan version" 102 | run: "vendor/bin/phpstan --version" 103 | 104 | - name: "Run phpstan/phpstan" 105 | run: "vendor/bin/phpstan analyse --configuration=build/phpstan-with-extension.neon" 106 | 107 | tests: 108 | name: "Tests" 109 | 110 | runs-on: "ubuntu-latest" 111 | 112 | strategy: 113 | matrix: 114 | php-version: 115 | - "7.2" 116 | 117 | dependencies: 118 | - "locked" 119 | 120 | steps: 121 | - name: "Checkout" 122 | uses: "actions/checkout@v2" 123 | 124 | - name: "Install PHP with extensions" 125 | uses: "shivammathur/setup-php@v2" 126 | with: 127 | coverage: "none" 128 | php-version: "${{ matrix.php-version }}" 129 | 130 | - name: "Determine composer cache directory" 131 | id: "determine-composer-cache-directory" 132 | run: "echo \"::set-output name=directory::$(composer config cache-dir)\"" 133 | 134 | - name: "Cache dependencies installed with composer" 135 | uses: "actions/cache@v1" 136 | with: 137 | path: "${{ steps.determine-composer-cache-directory.outputs.directory }}" 138 | key: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.lock') }}" 139 | restore-keys: "php-${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-" 140 | 141 | - name: "Install lowest dependencies with composer" 142 | if: "matrix.dependencies == 'lowest'" 143 | run: "composer update --no-interaction --no-progress --no-suggest --no-plugins --prefer-lowest" 144 | 145 | - name: "Install locked dependencies with composer" 146 | if: "matrix.dependencies == 'locked'" 147 | run: "composer install --no-interaction --no-progress --no-suggest --no-plugins" 148 | 149 | - name: "Install highest dependencies with composer" 150 | if: "matrix.dependencies == 'highest'" 151 | run: "composer update --no-interaction --no-progress --no-suggest --no-plugins" 152 | 153 | - name: "Run tests with phpunit/phpunit" 154 | run: "vendor/bin/phpunit --configuration=phpunit.xml" 155 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | /vendor 3 | /.phpunit.result.cache 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FriendsOfTYPO3 Team 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 | # TYPO3 extension for PHPStan 2 | 3 | > [!NOTE] 4 | > `friendsoftypo3/phpstan-typo3` is obsolete and has been retired. 5 | > The package dependency should be removed from TYPO3 project or extension development: `composer rem --dev friendsoftypo3/phpstan-typo3`. 6 | > See below for transition options. 7 | 8 | ## Migrating away from friendsoftypo3/phpstan-typo3 since TYPO3 core v12 9 | 10 | ### Remove package and rely on core annotations 11 | 12 | * The list of global TYPO3 specific constants shrunk over time and phpstan finds more of the remaining ones by default. 13 | phpstan may mumble about constants `LF` and `CR` not being defined. They can be made known to phpstan like this in 14 | a `phpstan.neon` config file: 15 | ``` 16 | parameters: 17 | bootstrapFiles: 18 | - phpstan.bootstrap.php 19 | ``` 20 | File `phpstan.bootstrap.php` then contains: 21 | ``` 22 | getAspect()`, `Query->execute()` and `QueryInterface->execute()` 27 | have proper method annotations since TYPO3 v12, the phpstan extension classes are not needed anymore and 28 | phpstan "understands" return values of these methods out of the box. 29 | * `ObjectManagerInterface->get()` and `ObjectManager->get()` extensions have been removed from TYPO3 since v12 30 | and are thus obsolete. 31 | 32 | ## TYPO3 CMS class reflection extension for PHPStan & framework-specific rules 33 | 34 | This extension provides the following features: 35 | 36 | * Provides correct return type for `\TYPO3\CMS\Core\Context\Context->getAspect()`. 37 | * Provides correct return type for `\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance()`. 38 | * Provides correct return type for `\TYPO3\CMS\Extbase\Object\ObjectManagerInterface->get()`. 39 | * Provides correct return type for `\TYPO3\CMS\Extbase\Object\ObjectManager->get()`. 40 | * Provides correct return type for `\TYPO3\CMS\Extbase\Persistence\Generic\Query->execute()`. 41 | * Provides correct return type for `\TYPO3\CMS\Extbase\Persistence\QueryInterface->execute()`. 42 | 43 |
44 | Details on GeneralUtility::makeInstance() 45 | 46 | Dynamic return types are returned for: 47 | 48 | * `GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class)` 49 | * `GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler')` 50 | * `GeneralUtility::makeInstance(self::class)` 51 | * `GeneralUtility::makeInstance(static::class)` 52 |
53 | 54 |
55 | Details on ObjectManagerInterface::get() and ObjectManager::get() 56 | 57 | Dynamic return types are returned for: 58 | 59 | * `ObjectManager->get(\TYPO3\CMS\Core\DataHandling\DataHandler::class)` 60 | * `ObjectManager->get('TYPO3\\CMS\\Core\\DataHandling\\DataHandler')` 61 | * `ObjectManager->get(self::class)` 62 | * `ObjectManager->get(static::class)` 63 |
64 | 65 | ## Installation & Configuration 66 | 67 | To use this extension, require it in [Composer](https://getcomposer.org/): 68 | 69 | ``` 70 | composer require friendsoftypo3/phpstan-typo3 --dev 71 | ``` 72 | 73 | Once installed, put this into your `phpstan.neon` config: 74 | 75 | ``` 76 | includes: 77 | - vendor/friendsoftypo3/phpstan-typo3/extension.neon 78 | ``` 79 | 80 | ## FAQ 81 | 82 | > I found this extension and the one from Sascha (`saschaegerer/phpstan-typo3`). Why are there two extensions and which should I use? 83 | 84 | Well, this package has one specific purpose. It's made to help making the TYPO3 core phpstan max level compatible. To achieve this, the core team needs to be able to have its own extension which can be quickly adjusted as soon as the core itself changes. If for example, a new core version is released, the core team can quickly raise the dependency constraints for `typo3/cms-core` and `typo3/cms-extbase` which cannot be done when working with Sascha's package. 85 | 86 | Also, Sascha's package contains dynamic return type providers that are not needed (yet) to make the core more compatible with phpstan. 87 | 88 | To sum it all up: There is no competition between both extensions and this extension should not be used by users but only by the TYPO3 core. 89 | -------------------------------------------------------------------------------- /build/.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | -------------------------------------------------------------------------------- /build/phpstan-with-extension.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %currentWorkingDirectory%/extension.neon 3 | 4 | parameters: 5 | level: max 6 | 7 | paths: 8 | - %currentWorkingDirectory%/tests/StaticCodeAnalysis/ 9 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendsoftypo3/phpstan-typo3", 3 | "type": "phpstan-extension", 4 | "description": "TYPO3 rules for PHPStan", 5 | "license": "GPL-2.0-or-later", 6 | "require": { 7 | "php": "^7.2 || ^8.0 || ^8.1", 8 | "phpstan/phpstan": "^0.12.99 || ^1.2.0" 9 | }, 10 | "require-dev": { 11 | "ergebnis/composer-normalize": "^2.3", 12 | "jangregor/phpstan-prophecy": "^0.6.2", 13 | "phpunit/phpunit": "^8.5", 14 | "symplify/easy-coding-standard": "^7.2", 15 | "typo3/cms-core": "^10.4 || 11.*.*@dev", 16 | "typo3/cms-extbase": "^10.4 || 11.*.*@dev" 17 | }, 18 | "config": { 19 | "allow-plugins": { 20 | "composer/package-versions-deprecated": true, 21 | "typo3/class-alias-loader": true, 22 | "typo3/cms-composer-installers": true, 23 | "ergebnis/composer-normalize": true 24 | }, 25 | "platform": { 26 | "php": "7.2" 27 | }, 28 | "sort-packages": true 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "FriendsOfTYPO3\\PHPStan\\TYPO3\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "FriendsOfTYPO3\\PHPStan\\TYPO3\\Tests\\": "tests/" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ecs.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | sets: 3 | - 'psr12' 4 | paths: 5 | - 'src' 6 | - 'tests' 7 | -------------------------------------------------------------------------------- /extension.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | bootstrapFiles: 3 | - phpstan.bootstrap.php 4 | 5 | services: 6 | - 7 | class: FriendsOfTYPO3\PHPStan\TYPO3\Type\GeneralUtilityDynamicReturnTypeExtension 8 | tags: 9 | - phpstan.broker.dynamicStaticMethodReturnTypeExtension 10 | - 11 | class: FriendsOfTYPO3\PHPStan\TYPO3\Type\ContextGetAspectDynamicReturnTypeExtension 12 | tags: 13 | - phpstan.broker.dynamicMethodReturnTypeExtension 14 | - 15 | class: FriendsOfTYPO3\PHPStan\TYPO3\Type\ObjectManagerGetDynamicReturnTypeExtension 16 | tags: 17 | - phpstan.broker.dynamicMethodReturnTypeExtension 18 | - 19 | class: FriendsOfTYPO3\PHPStan\TYPO3\Type\ObjectManagerInterfaceGetDynamicReturnTypeExtension 20 | tags: 21 | - phpstan.broker.dynamicMethodReturnTypeExtension 22 | - 23 | class: FriendsOfTYPO3\PHPStan\TYPO3\Type\QueryExecuteReturnTypeExtension 24 | tags: 25 | - phpstan.broker.dynamicMethodReturnTypeExtension 26 | - 27 | class: FriendsOfTYPO3\PHPStan\TYPO3\Type\QueryInterfaceExecuteReturnTypeExtension 28 | tags: 29 | - phpstan.broker.dynamicMethodReturnTypeExtension -------------------------------------------------------------------------------- /phpstan.bootstrap.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | src/ 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Type/ContextGetAspectDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'getAspect'; 34 | } 35 | 36 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type 37 | { 38 | $defaultObjectType = new ObjectType(AspectInterface::class); 39 | 40 | if (!($argument = $methodCall->args[0] ?? null) instanceof Arg) { 41 | return $defaultObjectType; 42 | } 43 | /** @var Arg $argument */ 44 | 45 | if (!($string = $argument->value ?? null) instanceof String_) { 46 | return $defaultObjectType; 47 | } 48 | /** @var String_ $string */ 49 | 50 | switch ($string->value) { 51 | case 'date': 52 | $type = new ObjectType(DateTimeAspect::class); 53 | break; 54 | case 'visibility': 55 | $type = new ObjectType(VisibilityAspect::class); 56 | break; 57 | case 'frontend.user': 58 | case 'backend.user': 59 | $type = new ObjectType(UserAspect::class); 60 | break; 61 | case 'workspace': 62 | $type = new ObjectType(WorkspaceAspect::class); 63 | break; 64 | case 'language': 65 | $type = new ObjectType(LanguageAspect::class); 66 | break; 67 | case 'typoscript': 68 | $type = new ObjectType(TypoScriptAspect::class); 69 | break; 70 | default: 71 | $type = $defaultObjectType; 72 | break; 73 | } 74 | 75 | return $type; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Type/GeneralUtilityDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'makeInstance'; 32 | } 33 | 34 | public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): TypeInterface 35 | { 36 | try { 37 | $classNameArgument = $this->fetchClassNameArgument($methodCall); 38 | $classNameArgumentValueExpression = $classNameArgument->value; 39 | 40 | switch (true) { 41 | case $classNameArgumentValueExpression instanceof StringNode: 42 | /* 43 | * Examples: 44 | * 45 | * - GeneralUtility::makeInstance('foo') 46 | * - GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler') 47 | */ 48 | return $this->createObjectTypeFromString($classNameArgumentValueExpression); 49 | case $classNameArgumentValueExpression instanceof ClassConstFetch: 50 | /* 51 | * Examples: 52 | * 53 | * - GeneralUtility::makeInstance(TYPO3\CMS\Core\DataHandling\DataHandler::class) 54 | * - GeneralUtility::makeInstance(self::class) 55 | * - GeneralUtility::makeInstance(static::class) 56 | */ 57 | return $this->createObjectTypeFromClassConstFetch($classNameArgumentValueExpression, $scope->getClassReflection()); 58 | default: 59 | throw new \InvalidArgumentException( 60 | 'Argument $className is neither a string nor a class constant', 61 | 1584879239 62 | ); 63 | } 64 | } catch (\Throwable $exception) { 65 | return new ObjectWithoutClassType(); 66 | } 67 | } 68 | 69 | private function fetchClassNameArgument(StaticCall $methodCall): ArgumentNode 70 | { 71 | if (empty($methodCall->args)) { 72 | /* 73 | * This usually does not happen as calling GeneralUtility::makeInstance() without the mandatory argument 74 | * $className results in a syntax error. 75 | */ 76 | throw new \LogicException('Method makeInstance is called without arguments.', 1584878263); 77 | } 78 | 79 | return $methodCall->args[0]; 80 | } 81 | 82 | private function createObjectTypeFromString(StringNode $string): TypeInterface 83 | { 84 | $className = $string->value; 85 | 86 | if (!class_exists($className)) { 87 | throw new \LogicException('makeInstance has been called with non class name string', 1584879581); 88 | } 89 | 90 | return new ObjectType($className); 91 | } 92 | 93 | private function createObjectTypeFromClassConstFetch(ClassConstFetch $expression, ?ClassReflection $classReflection): TypeInterface 94 | { 95 | $class = $expression->class; 96 | if (!$class instanceof Name) { 97 | throw new \LogicException('', 1584878823); 98 | } 99 | /** @var Name $class */ 100 | 101 | $className = $class->toString(); 102 | 103 | if ($className === 'self' && $classReflection !== null) { 104 | return new ObjectType($classReflection->getName()); 105 | } 106 | 107 | if ($className === 'static' && $classReflection !== null) { 108 | $callingClass = $classReflection->getName(); 109 | return new StaticType($callingClass); 110 | } 111 | 112 | return new ObjectType($className); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Type/ObjectManagerGetDynamicReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'get'; 32 | } 33 | 34 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): TypeInterface 35 | { 36 | try { 37 | $classNameArgument = $this->fetchClassNameArgument($methodCall); 38 | $classNameArgumentValueExpression = $classNameArgument->value; 39 | 40 | switch (true) { 41 | case $classNameArgumentValueExpression instanceof StringNode: 42 | /* 43 | * Examples: 44 | * 45 | * - GeneralUtility::makeInstance('foo') 46 | * - GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\DataHandling\\DataHandler') 47 | */ 48 | return $this->createObjectTypeFromString($classNameArgumentValueExpression); 49 | case $classNameArgumentValueExpression instanceof ClassConstFetch: 50 | /* 51 | * Examples: 52 | * 53 | * - GeneralUtility::makeInstance(TYPO3\CMS\Core\DataHandling\DataHandler::class) 54 | * - GeneralUtility::makeInstance(self::class) 55 | * - GeneralUtility::makeInstance(static::class) 56 | */ 57 | return $this->createObjectTypeFromClassConstFetch($classNameArgumentValueExpression, $scope->getClassReflection()); 58 | default: 59 | throw new \InvalidArgumentException( 60 | 'Argument $className is neither a string nor a class constant', 61 | 1584879239 62 | ); 63 | } 64 | } catch (\Throwable $exception) { 65 | return new ObjectWithoutClassType(); 66 | } 67 | } 68 | 69 | private function fetchClassNameArgument(MethodCall $methodCall): ArgumentNode 70 | { 71 | if (empty($methodCall->args)) { 72 | /* 73 | * This usually does not happen as calling GeneralUtility::makeInstance() without the mandatory argument 74 | * $className results in a syntax error. 75 | */ 76 | throw new \LogicException('Method makeInstance is called without arguments.', 1584881724); 77 | } 78 | 79 | return $methodCall->args[0]; 80 | } 81 | 82 | private function createObjectTypeFromString(StringNode $string): TypeInterface 83 | { 84 | $className = $string->value; 85 | 86 | if (!class_exists($className)) { 87 | throw new \LogicException('makeInstance has been called with non class name string', 1584881731); 88 | } 89 | 90 | return new ObjectType($className); 91 | } 92 | 93 | private function createObjectTypeFromClassConstFetch(ClassConstFetch $expression, ?ClassReflection $classReflection): TypeInterface 94 | { 95 | $class = $expression->class; 96 | if (!$class instanceof Name) { 97 | throw new \LogicException('', 1584881734); 98 | } 99 | /** @var Name $class */ 100 | 101 | $className = $class->toString(); 102 | 103 | if ($className === 'self' && $classReflection !== null) { 104 | return new ObjectType($classReflection->getName()); 105 | } 106 | 107 | if ($className === 'static' && $classReflection !== null) { 108 | $callingClass = $classReflection->getName(); 109 | return new StaticType($callingClass); 110 | } 111 | 112 | return new ObjectType($className); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Type/QueryExecuteReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'execute'; 31 | } 32 | 33 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): \PHPStan\Type\Type 34 | { 35 | if (empty($methodCall->args)) { 36 | return new ObjectType(QueryResultInterface::class); 37 | } 38 | 39 | $firstArgument = $scope->getType($methodCall->args[0]->value); 40 | if (!$firstArgument instanceof ConstantBooleanType) { 41 | throw new \InvalidArgumentException( 42 | 'Argument $returnRawQueryResult is not a boolean.', 43 | 1584879250 44 | ); 45 | } 46 | 47 | $returnRawQueryResult = $firstArgument->getValue(); 48 | if ($returnRawQueryResult) { 49 | return new ArrayType( 50 | new StringType(), 51 | new MixedType() 52 | ); 53 | } 54 | 55 | return new ObjectType(QueryResultInterface::class); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Type/QueryInterfaceExecuteReturnTypeExtension.php: -------------------------------------------------------------------------------- 1 | getName() === 'execute'; 32 | } 33 | 34 | public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): \PHPStan\Type\Type 35 | { 36 | if (empty($methodCall->args)) { 37 | return new ObjectType(QueryResultInterface::class); 38 | } 39 | 40 | $firstArgument = $scope->getType($methodCall->args[0]->value); 41 | if (!$firstArgument instanceof ConstantBooleanType) { 42 | throw new \InvalidArgumentException( 43 | 'Argument $returnRawQueryResult is not a boolean.', 44 | 1584879250 45 | ); 46 | } 47 | 48 | $returnRawQueryResult = $firstArgument->getValue(); 49 | if ($returnRawQueryResult) { 50 | return new ArrayType( 51 | new StringType(), 52 | new MixedType() 53 | ); 54 | } 55 | 56 | return new ObjectType(QueryResultInterface::class); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/StaticCodeAnalysis/Constants.php: -------------------------------------------------------------------------------- 1 | LF, 7 | 'CR' => CR, 8 | 'CRLF' => CRLF, 9 | 10 | 'FILE_DENY_PATTERN_DEFAULT' => FILE_DENY_PATTERN_DEFAULT, 11 | 12 | 'TYPO3_REQUESTTYPE' => TYPO3_REQUESTTYPE, 13 | 'TYPO3_REQUESTTYPE_FE' => TYPO3_REQUESTTYPE_FE, 14 | 'TYPO3_REQUESTTYPE_BE' => TYPO3_REQUESTTYPE_BE, 15 | 'TYPO3_REQUESTTYPE_CLI' => TYPO3_REQUESTTYPE_CLI, 16 | 'TYPO3_REQUESTTYPE_AJAX' => TYPO3_REQUESTTYPE_AJAX, 17 | 'TYPO3_REQUESTTYPE_INSTALL' => TYPO3_REQUESTTYPE_INSTALL, 18 | 19 | 'TYPO3_MODE' => TYPO3_MODE, 20 | 'TYPO3_mainDir' => TYPO3_mainDir, 21 | 'TYPO3_version' => TYPO3_version, 22 | 'TYPO3_branch' => TYPO3_branch, 23 | ]; 24 | -------------------------------------------------------------------------------- /tests/StaticCodeAnalysis/GeneralUtility/MakeInstance/CallWithClassConstant.php: -------------------------------------------------------------------------------- 1 | storeLogMessages = true; 12 | -------------------------------------------------------------------------------- /tests/StaticCodeAnalysis/GeneralUtility/MakeInstance/CallWithClassString.php: -------------------------------------------------------------------------------- 1 | storeLogMessages = true; 11 | -------------------------------------------------------------------------------- /tests/StaticCodeAnalysis/GeneralUtility/MakeInstance/CallWithSelf.php: -------------------------------------------------------------------------------- 1 | get(DataHandler::class); 13 | $object->storeLogMessages = true; 14 | -------------------------------------------------------------------------------- /tests/StaticCodeAnalysis/ObjectManager/Get/CallWithClassString.php: -------------------------------------------------------------------------------- 1 | get('TYPO3\\CMS\\Core\\DataHandling\\DataHandler'); 12 | $object->storeLogMessages = true; 13 | -------------------------------------------------------------------------------- /tests/StaticCodeAnalysis/ObjectManager/Get/CallWithSelf.php: -------------------------------------------------------------------------------- 1 | get(self::class); 15 | } 16 | 17 | public function bar(ObjectManager $objectManager): self 18 | { 19 | return $objectManager->get(self::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/StaticCodeAnalysis/ObjectManager/Get/CallWithStatic.php: -------------------------------------------------------------------------------- 1 | get(static::class); 15 | } 16 | 17 | public function bar(ObjectManager $objectManager): self 18 | { 19 | return $objectManager->get(static::class); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Unit/Type/ContextGetAspectDynamicReturnTypeExtensionTest.php: -------------------------------------------------------------------------------- 1 | extension = new ContextGetAspectDynamicReturnTypeExtension(); 34 | } 35 | 36 | public function testGetClass(): void 37 | { 38 | static::assertSame(Context::class, $this->extension->getClass()); 39 | } 40 | 41 | /** 42 | * @return \Generator> 43 | */ 44 | public function dataIsMethodSupported(): \Generator 45 | { 46 | yield ['getAspect', true]; 47 | yield ['foo', false]; 48 | } 49 | 50 | /** 51 | * @dataProvider dataIsMethodSupported 52 | * @param string $method 53 | * @param bool $expectedResult 54 | */ 55 | public function testIsMethodSupported(string $method, bool $expectedResult): void 56 | { 57 | $methodReflection = $this->prophesize(MethodReflection::class); 58 | $methodReflection->getName()->willReturn($method); 59 | 60 | self::assertSame($expectedResult, $this->extension->isMethodSupported($methodReflection->reveal())); 61 | } 62 | 63 | public function testGetTypeFromMethodCallReturnsAspectInterfaceAsDefault(): void 64 | { 65 | $scope = $this->prophesize(Scope::class)->reveal(); 66 | $methodCall = $this->prophesize(Expr\MethodCall::class)->reveal(); 67 | $methodReflection = $this->prophesize(MethodReflection::class)->reveal(); 68 | 69 | /** @var ObjectType $type */ 70 | $type = $this->extension->getTypeFromMethodCall($methodReflection, $methodCall, $scope); 71 | 72 | static::assertInstanceOf(ObjectType::class, $type); 73 | static::assertSame(AspectInterface::class, $type->getClassName()); 74 | } 75 | 76 | /** 77 | * @return \Generator> 78 | */ 79 | public function getTypeFromMethodCallDataProvider(): \Generator 80 | { 81 | yield ['date', DateTimeAspect::class]; 82 | yield ['visibility', VisibilityAspect::class]; 83 | yield ['frontend.user', UserAspect::class]; 84 | yield ['backend.user', UserAspect::class]; 85 | yield ['workspace', WorkspaceAspect::class]; 86 | yield ['language', LanguageAspect::class]; 87 | yield ['typoscript', TypoScriptAspect::class]; 88 | yield ['unknown', AspectInterface::class]; 89 | } 90 | 91 | /** 92 | * @dataProvider getTypeFromMethodCallDataProvider 93 | * @param string $argumentValue 94 | * @param string $expectedClassName 95 | */ 96 | public function testGetTypeFromMethodCall(string $argumentValue, string $expectedClassName): void 97 | { 98 | $string = $this->prophesize(String_::class)->reveal(); 99 | $string->value = $argumentValue; 100 | 101 | $arg = $this->prophesize(Arg::class)->reveal(); 102 | $arg->value = $string; 103 | 104 | $methodCall = $this->prophesize(Expr\MethodCall::class)->reveal(); 105 | $methodCall->args = [$arg]; 106 | 107 | /** @var ObjectType $type */ 108 | $type = $this->extension->getTypeFromMethodCall( 109 | $this->prophesize(MethodReflection::class)->reveal(), 110 | $methodCall, 111 | $this->prophesize(Scope::class)->reveal() 112 | ); 113 | 114 | static::assertInstanceOf(ObjectType::class, $type); 115 | static::assertSame($expectedClassName, $type->getClassName()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/Unit/Type/GeneralUtilityDynamicReturnTypeExtensionTest.php: -------------------------------------------------------------------------------- 1 | extension = new GeneralUtilityDynamicReturnTypeExtension(); 31 | } 32 | 33 | public function testGetClass(): void 34 | { 35 | static::assertSame(GeneralUtility::class, $this->extension->getClass()); 36 | } 37 | 38 | /** 39 | * @return \Generator> 40 | */ 41 | public function dataIsMethodSupported(): \Generator 42 | { 43 | yield ['makeInstance', true]; 44 | yield ['foo', false]; 45 | } 46 | 47 | /** 48 | * @dataProvider dataIsMethodSupported 49 | * @param string $method 50 | * @param bool $expectedResult 51 | */ 52 | public function testIsMethodSupported(string $method, bool $expectedResult): void 53 | { 54 | $methodReflection = $this->prophesize(MethodReflection::class); 55 | $methodReflection->getName()->willReturn($method); 56 | 57 | self::assertSame($expectedResult, $this->extension->isStaticMethodSupported($methodReflection->reveal())); 58 | } 59 | 60 | public function testGetTypeFromMethodCall(): void 61 | { 62 | $lexer = new Emulative(['usedAttributes' => []]); 63 | $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7, $lexer); 64 | 65 | $code = <<parse($code); 72 | static::assertIsArray($expressions); 73 | 74 | /** @var array $expressions */ 75 | reset($expressions); 76 | 77 | /** @var Expression $expression */ 78 | $expression = current($expressions); 79 | 80 | /** @var Assign $assignment */ 81 | $assignment = $expression->expr; 82 | 83 | /** @var StaticCall $staticCall */ 84 | $staticCall = $assignment->expr; 85 | 86 | /** @var ObjectType $type */ 87 | $type = $this->extension->getTypeFromStaticMethodCall( 88 | $this->prophesize(MethodReflection::class)->reveal(), 89 | $staticCall, 90 | $this->prophesize(Scope::class)->reveal() 91 | ); 92 | 93 | static::assertInstanceOf(ObjectType::class, $type); 94 | static::assertSame(DataHandler::class, $type->getClassName()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |