diff --git a/README.md b/README.md index 6681e4e..0160c0b 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,6 @@ parameters: shipmonkRules: allowComparingOnlyComparableTypes: enabled: true - allowNamedArgumentOnlyInAttributes: - enabled: true backedEnumGenerics: enabled: true classSuffixNaming: @@ -46,37 +44,11 @@ parameters: forbidArithmeticOperationOnNonNumber: enabled: true allowNumericString: false - forbidAssignmentNotMatchingVarDoc: - enabled: true - allowNarrowing: false forbidCast: enabled: true blacklist: ['(array)', '(object)', '(unset)'] forbidCheckedExceptionInCallable: enabled: true - immediatelyCalledCallables: - array_reduce: 1 - array_intersect_ukey: 2 - array_uintersect: 2 - array_uintersect_assoc: 2 - array_intersect_uassoc: 2 - array_uintersect_uassoc: [2, 3] - array_diff_ukey: 2 - array_udiff: 2 - array_udiff_assoc: 2 - array_diff_uassoc: 2 - array_udiff_uassoc: [2, 3] - array_filter: 1 - array_map: 0 - array_walk_recursive: 1 - array_walk: 1 - call_user_func: 0 - call_user_func_array: 0 - forward_static_call: 0 - forward_static_call_array: 0 - uasort: 1 - uksort: 1 - usort: 1 allowedCheckedExceptionCallables: [] forbidCheckedExceptionInYieldingMethod: enabled: true @@ -113,7 +85,7 @@ parameters: enabled: true forbidReturnValueInYieldingMethod: enabled: true - reportRegardlessOfReturnType: false + reportRegardlessOfReturnType: true forbidVariableTypeOverwriting: enabled: true forbidUnsetClassField: @@ -162,27 +134,6 @@ new DateTime() > '2040-01-02'; // comparing different types is denied 200 > '1e2'; // comparing different types is denied ``` -### allowNamedArgumentOnlyInAttributes -- Allows usage of named arguments only in native attributes -- Before native attributes, we used [DisallowNamedArguments](https://github.com/slevomat/coding-standard/blob/master/doc/functions.md#slevomatcodingstandardfunctionsdisallownamedarguments) sniff. But we used Doctrine annotations, which almost "require" named arguments when converted to native attributes. -```php -class User { - #[Column(type: Types::STRING, nullable: false)] // allowed - private string $email; - - public function __construct(string $email) { - $this->setEmail(email: $email); // forbidden - } -} -``` -- This one is highly opinionated and will probably be disabled/dropped next major version as it does not provide any extra strictness, you can disable it by: -```neon -parameters: - shipmonkRules: - allowNamedArgumentOnlyInAttributes: - enabled: false -``` - ### backedEnumGenerics * - Ensures that every BackedEnum child defines generic type - This rule makes sense only when BackedEnum was hacked to be generic by stub as described in [this article](https://rnd.shipmonk.com/hacking-generics-into-backedenum-in-php-8-1/) @@ -352,58 +303,6 @@ function add(string $a, string $b) { } ``` -### forbidAssignmentNotMatchingVarDoc -- Verifies if defined type in `@var` phpdoc accepts the assigned type during assignment -- No other places except assignment are checked - -```php -/** @var string $foo */ -$foo = $this->methodReturningInt(); // invalid var phpdoc -``` - -- For reasons of imperfect implementation of [type infering in phpstan-doctrine](https://github.com/phpstan/phpstan-doctrine#query-type-inference), there is an option to check only array-shapes and forget all other types by using `check-shape-only` -- This is helpful for cases where field nullability is eliminated by WHERE field IS NOT NULL which is not propagated to the inferred types -```php -/** @var array $result check-shape-only */ -$result = $queryBuilder->select('t.id') - ->from(Table::class, 't') - ->andWhere('t.id IS NOT NULL') - ->getResult(); -``` - -- It is possible to explicitly allow narrowing of types by `@var` phpdoc by using `allow-narrowing` -```php -/** @var SomeClass $result allow-narrowing */ -$result = $service->getSomeClassOrNull(); -``` -- Or you can enable it widely by using: -```neon -parameters: - shipmonkRules: - forbidAssignmentNotMatchingVarDoc: - allowNarrowing: true -``` - -#### Differences with native check: - -- Since `phpstan/phpstan:1.10.0` with bleedingEdge, there is a [very similar check within PHPStan itself](https://phpstan.org/blog/phpstan-1-10-comes-with-lie-detector#validate-inline-phpdoc-%40var-tag-type). -- The main difference is that it allows only subtype (narrowing), not supertype (widening) in `@var` phpdoc. -- This rule allows only widening, narrowing is allowed only when marked by `allow-narrowing` or configured by `allowNarrowing: true`. -- Basically, **there are 3 ways for you to check inline `@var` phpdoc**: - - allow only narrowing - - this rule disabled, native check enabled - - allow narrowing and widening - - this rule enabled with `allowNarrowing: true`, native check disabled - - allow only widening - - this rule enabled, native check disabled - -- You can disable native check while keeping bleedingEdge by: -```neon -parameters: - featureToggles: - varTagType: false -``` - ### forbidCast - Deny casting you configure - Possible values to use: @@ -428,8 +327,7 @@ parameters: ### forbidCheckedExceptionInCallable - Denies throwing [checked exception](https://phpstan.org/blog/bring-your-exceptions-under-control) in callables (Closures, Arrow functions and First class callables) as those cannot be tracked as checked by PHPStan analysis, because it is unknown when the callable is about to be called -- It allows configuration of functions/methods, where the callable is called immediately, those cases are allowed and are also added to [dynamic throw type extension](https://phpstan.org/developing-extensions/dynamic-throw-type-extensions) which causes those exceptions to be tracked properly in your codebase (!) - - By default, native functions like `array_map` are present. So it is recommended not to overwrite the defaults here (by `!` char). +- It is allowed to throw checked exceptions in immediately called callables (e.g. params marked by `@param-immediately-invoked-callable`, see [docs](https://phpstan.org/writing-php-code/phpdocs-basics#callables)) - It allows configuration of functions/methods, where the callable is handling all thrown exceptions and it is safe to throw anything from there; this basically makes such calls ignored by this rule - It ignores [implicitly thrown Throwable](https://phpstan.org/blog/bring-your-exceptions-under-control#what-does-absent-%40throws-above-a-function-mean%3F) - Learn more in 🇨🇿 [talk about checked exceptions in general](https://www.youtube.com/watch?v=UQsP1U0sVZM) @@ -438,10 +336,6 @@ parameters: parameters: shipmonkRules: forbidCheckedExceptionInCallable: - immediatelyCalledCallables: - 'Doctrine\ORM\EntityManager::transactional': 0 # 0 is argument index where the closure appears, you can use list if needed - 'Symfony\Contracts\Cache\CacheInterface::get': 1 - 'Acme\my_custom_function': 0 allowedCheckedExceptionCallables: 'Symfony\Component\Console\Question::setValidator': 0 # symfony automatically converts all thrown exceptions to error output, so it is safe to throw anything here ``` @@ -462,16 +356,26 @@ parameters: ```php +class TransactionManager { + /** + * @param-immediately-invoked-callable $callback + */ + public function transactional(callable $callback): void { + // ... + $callback(); + // ... + } +} + class UserEditFacade { /** * @throws UserNotFoundException - * ^ This throws would normally be reported as never thrown in native phpstan, but we know the closure is immediately called */ public function updateUserEmail(UserId $userId, Email $email): void { - $this->entityManager->transactional(function () use ($userId, $email) { - $user = $this->userRepository->get($userId); // throws checked UserNotFoundException + $this->transactionManager->transactional(function () use ($userId, $email) { + $user = $this->userRepository->get($userId); // can throw checked UserNotFoundException $user->updateEmail($email); }) } @@ -852,6 +756,8 @@ parameters: checkMissingCallableSignature: true # https://phpstan.org/config-reference#vague-typehints checkTooWideReturnTypesInProtectedAndPublicMethods: true # https://phpstan.org/config-reference#checktoowidereturntypesinprotectedandpublicmethods reportAnyTypeWideningInVarTag: true # https://phpstan.org/config-reference#reportanytypewideninginvartag + reportPossiblyNonexistentConstantArrayOffset: true # https://phpstan.org/config-reference#reportpossiblynonexistentconstantarrayoffset + reportPossiblyNonexistentGeneralArrayOffset: true # https://phpstan.org/config-reference#reportpossiblynonexistentgeneralarrayoffset ``` ## Contributing diff --git a/bin/verify-inline-ignore.php b/bin/verify-inline-ignore.php new file mode 100644 index 0000000..f38ac47 --- /dev/null +++ b/bin/verify-inline-ignore.php @@ -0,0 +1,33 @@ +isFile() || $entry->getExtension() !== 'php') { + continue; + } + + $realpath = realpath($entry->getPathname()); + $contents = file_get_contents($realpath); + if (strpos($contents, '@phpstan-ignore-') !== false) { + $error = true; + echo $realpath . ' uses @phpstan-ignore-line, use \'@phpstan-ignore identifier (reason)\' instead' . PHP_EOL; + } + } +} + +if ($error) { + exit(1); +} else { + echo 'Ok, no non-identifier based inline ignore found' . PHP_EOL; + exit(0); +} + diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 339ab77..dc3aa07 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -8,5 +8,4 @@ require_once('phar://phpstan.phar/preload.php'); // prepends PHPStan's PharAutolaoder to composer's autoloader return (new Configuration()) - ->addPathToExclude(__DIR__ . '/tests/Rule/data') - ->addPathToExclude(__DIR__ . '/tests/Extension/data'); + ->addPathToExclude(__DIR__ . '/tests/Rule/data'); diff --git a/composer.json b/composer.json index cd7a2b1..56290e0 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "php": "^7.4 || ^8.0", - "phpstan/phpstan": "^1.10.51" + "phpstan/phpstan": "1.11.x-dev" }, "require-dev": { "editorconfig-checker/editorconfig-checker": "^10.6.0", @@ -34,8 +34,7 @@ "ShipMonk\\PHPStan\\": "tests/" }, "classmap": [ - "tests/Rule/data", - "tests/Extension/data" + "tests/Rule/data" ] }, "config": { @@ -61,6 +60,7 @@ "@check:tests", "@check:dependencies", "@check:collisions", + "@check:ignores", "@check:readme" ], "check:collisions": "detect-collisions src tests", @@ -68,6 +68,7 @@ "check:cs": "phpcs", "check:dependencies": "composer-dependency-analyser", "check:ec": "ec src tests", + "check:ignores": "php bin/verify-inline-ignore.php", "check:readme": "php bin/verify-readme-default-config.php", "check:tests": "phpunit -vvv tests", "check:types": "phpstan analyse -vvv --ansi", diff --git a/composer.lock b/composer.lock index 5fcbcc3..b7a7273 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5670f1b56cdde7f4148e5623e7406692", + "content-hash": "7f3aaaca4e751e4a1717ad824db05ff2", "packages": [ { "name": "phpstan/phpstan", - "version": "1.10.66", + "version": "1.11.x-dev", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "94779c987e4ebd620025d9e5fdd23323903950bd" + "reference": "f632067e36ff1b9cba999266fa6745d35027d2e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/94779c987e4ebd620025d9e5fdd23323903950bd", - "reference": "94779c987e4ebd620025d9e5fdd23323903950bd", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f632067e36ff1b9cba999266fa6745d35027d2e0", + "reference": "f632067e36ff1b9cba999266fa6745d35027d2e0", "shasum": "" }, "require": { @@ -26,6 +26,7 @@ "conflict": { "phpstan/phpstan-shim": "*" }, + "default-branch": true, "bin": [ "phpstan", "phpstan.phar" @@ -60,13 +61,9 @@ { "url": "https://github.com/phpstan", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", - "type": "tidelift" } ], - "time": "2024-03-28T16:17:31+00:00" + "time": "2024-04-24T13:53:24+00:00" } ], "packages-dev": [ @@ -3112,12 +3109,14 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "phpstan/phpstan": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^7.4 || ^8.0" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.2.0" } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4db5e68..97a17b2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -20,6 +20,9 @@ parameters: checkBenevolentUnionTypes: true checkImplicitMixed: true checkTooWideReturnTypesInProtectedAndPublicMethods: true + reportAnyTypeWideningInVarTag: true + reportPossiblyNonexistentConstantArrayOffset: true + reportPossiblyNonexistentGeneralArrayOffset: true exceptions: check: missingCheckedExceptionInThrows: true @@ -37,8 +40,8 @@ parameters: PHPStan\Rules\Rule: Rule PhpParser\NodeVisitor: Visitor ShipMonk\PHPStan\RuleTestCase: RuleTest - forbidAssignmentNotMatchingVarDoc: - enabled: false # native check is better now; this rule will be dropped / reworked in 3.0 + enforceClosureParamNativeTypehint: + enabled: false # we support even PHP 7.4, some typehints cannot be used ignoreErrors: - @@ -50,7 +53,8 @@ parameters: message: "#but it's missing from the PHPDoc @throws tag\\.$#" # allow uncatched exceptions in tests path: tests/* - - - message: '#^Call to function method_exists\(\) with PHPStan\\Analyser\\Scope and ''getKeepVoidType'' will always evaluate to true\.$#' - path: src/Rule/ForbidUnusedMatchResultRule.php - reportUnmatched: false # fails only for PHPStan > 1.10.49 + # ignore BC promises + - identifier: phpstanApi.class + - identifier: phpstanApi.method + - identifier: phpstanApi.interface + - identifier: phpstanApi.instanceofAssumption diff --git a/rules.neon b/rules.neon index 6534840..dc2b5db 100644 --- a/rules.neon +++ b/rules.neon @@ -2,8 +2,6 @@ parameters: shipmonkRules: allowComparingOnlyComparableTypes: enabled: true - allowNamedArgumentOnlyInAttributes: - enabled: true backedEnumGenerics: enabled: true classSuffixNaming: @@ -25,37 +23,11 @@ parameters: forbidArithmeticOperationOnNonNumber: enabled: true allowNumericString: false - forbidAssignmentNotMatchingVarDoc: - enabled: true - allowNarrowing: false forbidCast: enabled: true blacklist: ['(array)', '(object)', '(unset)'] forbidCheckedExceptionInCallable: enabled: true - immediatelyCalledCallables: - array_reduce: 1 - array_intersect_ukey: 2 - array_uintersect: 2 - array_uintersect_assoc: 2 - array_intersect_uassoc: 2 - array_uintersect_uassoc: [2, 3] - array_diff_ukey: 2 - array_udiff: 2 - array_udiff_assoc: 2 - array_diff_uassoc: 2 - array_udiff_uassoc: [2, 3] - array_filter: 1 - array_map: 0 - array_walk_recursive: 1 - array_walk: 1 - call_user_func: 0 - call_user_func_array: 0 - forward_static_call: 0 - forward_static_call_array: 0 - uasort: 1 - uksort: 1 - usort: 1 allowedCheckedExceptionCallables: [] forbidCheckedExceptionInYieldingMethod: enabled: true @@ -92,7 +64,7 @@ parameters: enabled: true forbidReturnValueInYieldingMethod: enabled: true - reportRegardlessOfReturnType: false + reportRegardlessOfReturnType: true forbidVariableTypeOverwriting: enabled: true forbidUnsetClassField: @@ -116,9 +88,6 @@ parametersSchema: allowComparingOnlyComparableTypes: structure([ enabled: bool() ]) - allowNamedArgumentOnlyInAttributes: structure([ - enabled: bool() - ]) backedEnumGenerics: structure([ enabled: bool() ]) @@ -149,17 +118,12 @@ parametersSchema: enabled: bool() allowNumericString: bool() ]) - forbidAssignmentNotMatchingVarDoc: structure([ - enabled: bool() - allowNarrowing: bool() - ]) forbidCast: structure([ enabled: bool() blacklist: arrayOf(string()) ]) forbidCheckedExceptionInCallable: structure([ enabled: bool() - immediatelyCalledCallables: arrayOf(anyOf(listOf(int()), int()), string()) allowedCheckedExceptionCallables: arrayOf(anyOf(listOf(int()), int()), string()) ]) forbidCheckedExceptionInYieldingMethod: structure([ @@ -243,8 +207,6 @@ parametersSchema: conditionalTags: ShipMonk\PHPStan\Rule\AllowComparingOnlyComparableTypesRule: phpstan.rules.rule: %shipmonkRules.allowComparingOnlyComparableTypes.enabled% - ShipMonk\PHPStan\Rule\AllowNamedArgumentOnlyInAttributesRule: - phpstan.rules.rule: %shipmonkRules.allowNamedArgumentOnlyInAttributes.enabled% ShipMonk\PHPStan\Rule\BackedEnumGenericsRule: phpstan.rules.rule: %shipmonkRules.backedEnumGenerics.enabled% ShipMonk\PHPStan\Rule\ClassSuffixNamingRule: @@ -263,8 +225,6 @@ conditionalTags: phpstan.rules.rule: %shipmonkRules.enforceReadonlyPublicProperty.enabled% ShipMonk\PHPStan\Rule\ForbidArithmeticOperationOnNonNumberRule: phpstan.rules.rule: %shipmonkRules.forbidArithmeticOperationOnNonNumber.enabled% - ShipMonk\PHPStan\Rule\ForbidAssignmentNotMatchingVarDocRule: - phpstan.rules.rule: %shipmonkRules.forbidAssignmentNotMatchingVarDoc.enabled% ShipMonk\PHPStan\Rule\ForbidCastRule: phpstan.rules.rule: %shipmonkRules.forbidCast.enabled% ShipMonk\PHPStan\Rule\ForbidCheckedExceptionInCallableRule: @@ -318,10 +278,6 @@ conditionalTags: ShipMonk\PHPStan\Rule\UselessPrivatePropertyNullabilityRule: phpstan.rules.rule: %shipmonkRules.uselessPrivatePropertyNullability.enabled% - ShipMonk\PHPStan\Visitor\ImmediatelyCalledCallableVisitor: - phpstan.parser.richParserNodeVisitor: %shipmonkRules.forbidCheckedExceptionInCallable.enabled% - ShipMonk\PHPStan\Visitor\NamedArgumentSourceVisitor: - phpstan.parser.richParserNodeVisitor: %shipmonkRules.allowNamedArgumentOnlyInAttributes.enabled% ShipMonk\PHPStan\Visitor\UnusedExceptionVisitor: phpstan.parser.richParserNodeVisitor: %shipmonkRules.forbidUnusedException.enabled% ShipMonk\PHPStan\Visitor\UnusedMatchVisitor: @@ -331,16 +287,9 @@ conditionalTags: ShipMonk\PHPStan\Visitor\ClassPropertyAssignmentVisitor: phpstan.parser.richParserNodeVisitor: %shipmonkRules.uselessPrivatePropertyNullability.enabled% - ShipMonk\PHPStan\Extension\ImmediatelyCalledCallableThrowTypeExtension: - phpstan.dynamicFunctionThrowTypeExtension: %shipmonkRules.forbidCheckedExceptionInCallable.enabled% - phpstan.dynamicMethodThrowTypeExtension: %shipmonkRules.forbidCheckedExceptionInCallable.enabled% - phpstan.dynamicStaticMethodThrowTypeExtension: %shipmonkRules.forbidCheckedExceptionInCallable.enabled% - services: - class: ShipMonk\PHPStan\Rule\AllowComparingOnlyComparableTypesRule - - - class: ShipMonk\PHPStan\Rule\AllowNamedArgumentOnlyInAttributesRule - class: ShipMonk\PHPStan\Rule\BackedEnumGenericsRule - @@ -367,16 +316,11 @@ services: class: ShipMonk\PHPStan\Rule\ForbidArithmeticOperationOnNonNumberRule arguments: allowNumericString: %shipmonkRules.forbidArithmeticOperationOnNonNumber.allowNumericString% - - - class: ShipMonk\PHPStan\Rule\ForbidAssignmentNotMatchingVarDocRule - arguments: - allowNarrowing: %shipmonkRules.forbidAssignmentNotMatchingVarDoc.allowNarrowing% - class: ShipMonk\PHPStan\Rule\ForbidCastRule - class: ShipMonk\PHPStan\Rule\ForbidCheckedExceptionInCallableRule arguments: - immediatelyCalledCallables: %shipmonkRules.forbidCheckedExceptionInCallable.immediatelyCalledCallables% allowedCheckedExceptionCallables: %shipmonkRules.forbidCheckedExceptionInCallable.allowedCheckedExceptionCallables% - class: ShipMonk\PHPStan\Rule\ForbidCheckedExceptionInYieldingMethodRule @@ -446,13 +390,6 @@ services: class: ShipMonk\PHPStan\Rule\RequirePreviousExceptionPassRule arguments: reportEvenIfExceptionIsNotAcceptableByRethrownOne: %shipmonkRules.requirePreviousExceptionPass.reportEvenIfExceptionIsNotAcceptableByRethrownOne% - - - class: ShipMonk\PHPStan\Visitor\ImmediatelyCalledCallableVisitor - arguments: - immediatelyCalledCallables: %shipmonkRules.forbidCheckedExceptionInCallable.immediatelyCalledCallables% - allowedCheckedExceptionCallables: %shipmonkRules.forbidCheckedExceptionInCallable.allowedCheckedExceptionCallables% - - - class: ShipMonk\PHPStan\Visitor\NamedArgumentSourceVisitor - class: ShipMonk\PHPStan\Visitor\UnusedExceptionVisitor - @@ -461,8 +398,3 @@ services: class: ShipMonk\PHPStan\Visitor\TopLevelConstructorPropertyFetchMarkingVisitor - class: ShipMonk\PHPStan\Visitor\ClassPropertyAssignmentVisitor - - - - class: ShipMonk\PHPStan\Extension\ImmediatelyCalledCallableThrowTypeExtension - arguments: - immediatelyCalledCallables: %shipmonkRules.forbidCheckedExceptionInCallable.immediatelyCalledCallables% diff --git a/src/Extension/ImmediatelyCalledCallableThrowTypeExtension.php b/src/Extension/ImmediatelyCalledCallableThrowTypeExtension.php deleted file mode 100644 index 5079f06..0000000 --- a/src/Extension/ImmediatelyCalledCallableThrowTypeExtension.php +++ /dev/null @@ -1,251 +0,0 @@ - callable argument index(es) - * or - * function => callable argument index(es) - * - * @var array> - */ - private array $immediatelyCalledCallables; - - private NodeScopeResolver $nodeScopeResolver; - - private ReflectionProvider $reflectionProvider; - - /** - * @param array> $immediatelyCalledCallables - */ - public function __construct( - NodeScopeResolver $nodeScopeResolver, - ReflectionProvider $reflectionProvider, - array $immediatelyCalledCallables - ) - { - $this->nodeScopeResolver = $nodeScopeResolver; - $this->reflectionProvider = $reflectionProvider; - $this->immediatelyCalledCallables = $immediatelyCalledCallables; - } - - public function isFunctionSupported(FunctionReflection $functionReflection): bool - { - return $this->isCallSupported($functionReflection); - } - - public function isMethodSupported(MethodReflection $methodReflection): bool - { - return $this->isCallSupported($methodReflection); - } - - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return $this->isCallSupported($methodReflection); - } - - /** - * @param FunctionReflection|MethodReflection $callReflection - */ - private function isCallSupported(object $callReflection): bool - { - return $this->getClosureArgumentPositions($callReflection) !== []; - } - - public function getThrowTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope - ): ?Type - { - return $this->combineCallbackAndCallThrowTypes($functionCall, $functionReflection, $scope); - } - - public function getThrowTypeFromMethodCall( - MethodReflection $methodReflection, - MethodCall $methodCall, - Scope $scope - ): ?Type - { - return $this->combineCallbackAndCallThrowTypes($methodCall, $methodReflection, $scope); - } - - public function getThrowTypeFromStaticMethodCall( - MethodReflection $methodReflection, - StaticCall $methodCall, - Scope $scope - ): ?Type - { - return $this->combineCallbackAndCallThrowTypes($methodCall, $methodReflection, $scope); - } - - /** - * @param FunctionReflection|MethodReflection $callReflection - */ - private function combineCallbackAndCallThrowTypes( - CallLike $call, - object $callReflection, - Scope $scope - ): ?Type - { - if (!$scope instanceof MutatingScope) { // @phpstan-ignore-line ignore bc promise - throw new LogicException('Unexpected scope implementation'); - } - - $argumentPositions = $this->getClosureArgumentPositions($callReflection); - - $throwTypes = $callReflection->getThrowType() !== null - ? [$callReflection->getThrowType()] - : []; - - foreach ($argumentPositions as $argumentPosition) { - $args = $call->getArgs(); - - if (!isset($args[$argumentPosition])) { - continue; - } - - $argumentValue = $args[$argumentPosition]->value; - - if ($argumentValue instanceof Closure) { - $result = $this->nodeScopeResolver->processStmtNodes( - $call, - $argumentValue->getStmts(), - $scope->enterAnonymousFunction($argumentValue), - static function (): void { - }, - ); - - foreach ($result->getThrowPoints() as $throwPoint) { - if ($throwPoint->isExplicit()) { - $throwTypes[] = $throwPoint->getType(); - } - } - } - - if ($argumentValue instanceof ArrowFunction) { - $result = $this->nodeScopeResolver->processStmtNodes( - $call, - $argumentValue->getStmts(), - $scope->enterArrowFunction($argumentValue), - static function (): void { - }, - ); - - foreach ($result->getThrowPoints() as $throwPoint) { - if ($throwPoint->isExplicit()) { - $throwTypes[] = $throwPoint->getType(); - } - } - } - - if ($argumentValue instanceof StaticCall - && $argumentValue->isFirstClassCallable() - && $argumentValue->name instanceof Identifier - && $argumentValue->class instanceof Name - ) { - $methodName = (string) $argumentValue->name; - $className = $scope->resolveName($argumentValue->class); - - $caller = $this->reflectionProvider->getClass($className); - $method = $caller->getMethod($methodName, $scope); - - if ($method->getThrowType() !== null) { - $throwTypes[] = $method->getThrowType(); - } - } - - if ($argumentValue instanceof MethodCall - && $argumentValue->isFirstClassCallable() - && $argumentValue->name instanceof Identifier - ) { - $methodName = (string) $argumentValue->name; - $callerType = $scope->getType($argumentValue->var); - - foreach ($callerType->getObjectClassReflections() as $callerReflection) { - $method = $callerReflection->getMethod($methodName, $scope); - - if ($method->getThrowType() !== null) { - $throwTypes[] = $method->getThrowType(); - } - } - } - } - - if ($throwTypes === []) { - return null; - } - - return TypeCombinator::union(...$throwTypes); - } - - /** - * @param FunctionReflection|MethodReflection $callReflection - * @return list - */ - private function getClosureArgumentPositions(object $callReflection): array - { - if ($callReflection instanceof FunctionReflection) { - return $this->normalizeArgumentIndexes($this->immediatelyCalledCallables[$callReflection->getName()] ?? []); - } - - $argumentPositions = []; - $classReflection = $callReflection->getDeclaringClass(); - - foreach ($this->immediatelyCalledCallables as $immediateCallerAndMethod => $indexes) { - if (strpos($immediateCallerAndMethod, '::') === false) { - continue; - } - - [$callerClass, $methodName] = explode('::', $immediateCallerAndMethod); - - if ($methodName !== $callReflection->getName() || !$classReflection->is($callerClass)) { - continue; - } - - $argumentPositions = array_merge($argumentPositions, $this->normalizeArgumentIndexes($indexes)); - } - - return array_values(array_unique($argumentPositions)); - } - - /** - * @param int|list $argumentIndexes - * @return list - */ - private function normalizeArgumentIndexes($argumentIndexes): array - { - return is_int($argumentIndexes) ? [$argumentIndexes] : $argumentIndexes; - } - -} diff --git a/src/Rule/AllowComparingOnlyComparableTypesRule.php b/src/Rule/AllowComparingOnlyComparableTypesRule.php index fd2e20c..e2d8c42 100644 --- a/src/Rule/AllowComparingOnlyComparableTypesRule.php +++ b/src/Rule/AllowComparingOnlyComparableTypesRule.php @@ -11,8 +11,8 @@ use PhpParser\Node\Expr\BinaryOp\SmallerOrEqual; use PhpParser\Node\Expr\BinaryOp\Spaceship; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; @@ -36,7 +36,7 @@ public function getNodeType(): string /** * @param BinaryOp $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -133,7 +133,7 @@ private function isComparableTogether(Type $leftType, Type $rightType): bool } for ($i = 0; $i < count($leftValueTypes); $i++) { - if (!$this->isComparableTogether($leftValueTypes[$i], $rightValueTypes[$i])) { + if (!$this->isComparableTogether($leftValueTypes[$i], $rightValueTypes[$i])) { // @phpstan-ignore offsetAccess.notFound, offsetAccess.notFound return false; } } diff --git a/src/Rule/AllowNamedArgumentOnlyInAttributesRule.php b/src/Rule/AllowNamedArgumentOnlyInAttributesRule.php deleted file mode 100644 index 1e281b6..0000000 --- a/src/Rule/AllowNamedArgumentOnlyInAttributesRule.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class AllowNamedArgumentOnlyInAttributesRule implements Rule -{ - - public function getNodeType(): string - { - return Arg::class; - } - - /** - * @param Arg $node - * @return list - */ - public function processNode(Node $node, Scope $scope): array - { - if ($node->name === null) { - return []; - } - - if ($node->getAttribute(NamedArgumentSourceVisitor::IS_ATTRIBUTE_NAMED_ARGUMENT) === true) { - return []; - } - - $error = RuleErrorBuilder::message('Named arguments are allowed only within native attributes') - ->identifier('shipmonk.namedArgumentOutsideAttribute') - ->build(); - return [$error]; - } - -} diff --git a/src/Rule/BackedEnumGenericsRule.php b/src/Rule/BackedEnumGenericsRule.php index c515487..9af8ce6 100644 --- a/src/Rule/BackedEnumGenericsRule.php +++ b/src/Rule/BackedEnumGenericsRule.php @@ -7,8 +7,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; use PHPStan\Reflection\ClassReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; @@ -25,7 +25,7 @@ public function getNodeType(): string /** * @param InClassNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ClassSuffixNamingRule.php b/src/Rule/ClassSuffixNamingRule.php index a3a23e0..82d09bc 100644 --- a/src/Rule/ClassSuffixNamingRule.php +++ b/src/Rule/ClassSuffixNamingRule.php @@ -2,11 +2,13 @@ namespace ShipMonk\PHPStan\Rule; +use LogicException; use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\InClassNode; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use function strlen; use function substr_compare; @@ -25,8 +27,14 @@ class ClassSuffixNamingRule implements Rule /** * @param array $superclassToSuffixMapping */ - public function __construct(array $superclassToSuffixMapping = []) + public function __construct(ReflectionProvider $reflectionProvider, array $superclassToSuffixMapping = []) { + foreach ($superclassToSuffixMapping as $className => $suffix) { + if (!$reflectionProvider->hasClass($className)) { + throw new LogicException("Class $className used in 'superclassToSuffixMapping' does not exist"); + } + } + $this->superclassToSuffixMapping = $superclassToSuffixMapping; } @@ -37,7 +45,7 @@ public function getNodeType(): string /** * @param InClassNode $node - * @return list + * @return list */ public function processNode( Node $node, diff --git a/src/Rule/EnforceClosureParamNativeTypehintRule.php b/src/Rule/EnforceClosureParamNativeTypehintRule.php index 852d3f8..7a6d583 100644 --- a/src/Rule/EnforceClosureParamNativeTypehintRule.php +++ b/src/Rule/EnforceClosureParamNativeTypehintRule.php @@ -8,8 +8,8 @@ use PHPStan\Node\InArrowFunctionNode; use PHPStan\Node\InClosureNode; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use function is_string; @@ -39,14 +39,14 @@ public function getNodeType(): string } /** - * @return list + * @return list */ public function processNode( Node $node, Scope $scope ): array { - if (!$node instanceof InClosureNode && !$node instanceof InArrowFunctionNode) { // @phpstan-ignore-line bc promise + if (!$node instanceof InClosureNode && !$node instanceof InArrowFunctionNode) { return []; } diff --git a/src/Rule/EnforceEnumMatchRule.php b/src/Rule/EnforceEnumMatchRule.php index 87b1c09..7b757a8 100644 --- a/src/Rule/EnforceEnumMatchRule.php +++ b/src/Rule/EnforceEnumMatchRule.php @@ -7,13 +7,14 @@ use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\BinaryOp\NotIdentical; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Enum\EnumCaseObjectType; use function array_map; use function array_merge; use function array_unique; +use function array_values; use function count; /** @@ -29,7 +30,7 @@ public function getNodeType(): string /** * @param BinaryOp $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -47,12 +48,12 @@ public function processNode(Node $node, Scope $scope): array $rightType = $scope->getType($node->right); if ($leftType->isEnum()->yes() && $rightType->isEnum()->yes()) { - $enumCases = array_unique( + $enumCases = array_values(array_unique( array_merge( array_map(static fn (EnumCaseObjectType $type) => "{$type->getClassName()}::{$type->getEnumCaseName()}", $leftType->getEnumCases()), array_map(static fn (EnumCaseObjectType $type) => "{$type->getClassName()}::{$type->getEnumCaseName()}", $rightType->getEnumCases()), ), - ); + )); if (count($enumCases) !== 1) { return []; // do not report nonsense comparison diff --git a/src/Rule/EnforceIteratorToArrayPreserveKeysRule.php b/src/Rule/EnforceIteratorToArrayPreserveKeysRule.php index 9ea5594..6bc396f 100644 --- a/src/Rule/EnforceIteratorToArrayPreserveKeysRule.php +++ b/src/Rule/EnforceIteratorToArrayPreserveKeysRule.php @@ -6,9 +6,10 @@ use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; +use function array_values; use function count; /** @@ -24,7 +25,7 @@ public function getNodeType(): string /** * @param FuncCall $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -36,15 +37,17 @@ public function processNode(Node $node, Scope $scope): array return []; } - if (count($node->getArgs()) >= 2) { + $args = array_values($node->getArgs()); + + if (count($args) >= 2) { return []; } - if (count($node->getArgs()) === 0) { + if (count($args) === 0) { return []; } - if ($node->getArgs()[0]->unpack) { + if ($args[0]->unpack) { return []; // not trying to analyse what is being unpacked as this is very non-standard approach here } diff --git a/src/Rule/EnforceListReturnRule.php b/src/Rule/EnforceListReturnRule.php index c14fefa..22965a9 100644 --- a/src/Rule/EnforceListReturnRule.php +++ b/src/Rule/EnforceListReturnRule.php @@ -9,8 +9,8 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\VerbosityLevel; @@ -29,7 +29,7 @@ public function getNodeType(): string /** * @param ReturnStatementsNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -39,7 +39,7 @@ public function processNode(Node $node, Scope $scope): array $methodReflection = $scope->getFunction(); - if ($methodReflection === null || $node instanceof ClosureReturnStatementsNode) { // @phpstan-ignore-line ignore bc promise + if ($methodReflection === null || $node instanceof ClosureReturnStatementsNode) { return []; } diff --git a/src/Rule/EnforceNativeReturnTypehintRule.php b/src/Rule/EnforceNativeReturnTypehintRule.php index 35adbd9..fab1dc6 100644 --- a/src/Rule/EnforceNativeReturnTypehintRule.php +++ b/src/Rule/EnforceNativeReturnTypehintRule.php @@ -8,8 +8,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ReturnStatementsNode; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; @@ -64,7 +64,7 @@ public function getNodeType(): string /** * @param ReturnStatementsNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -255,7 +255,7 @@ private function getUnionTypehint( foreach ($type->getTypes() as $subtype) { $wrap = false; - if ($subtype instanceof IntersectionType) { // @phpstan-ignore-line ignore instanceof intersection + if ($subtype instanceof IntersectionType) { // @phpstan-ignore phpstanApi.instanceofType if ($this->phpVersion->getVersionId() < 80_200) { // DNF return null; } @@ -286,7 +286,7 @@ private function getIntersectionTypehint( bool $alwaysThrowsException ): ?string { - if (!$type instanceof IntersectionType) { // @phpstan-ignore-line ignore instanceof intersection + if (!$type instanceof IntersectionType) { // @phpstan-ignore phpstanApi.instanceofType return null; } diff --git a/src/Rule/EnforceReadonlyPublicPropertyRule.php b/src/Rule/EnforceReadonlyPublicPropertyRule.php index 4fd20bd..d67a959 100644 --- a/src/Rule/EnforceReadonlyPublicPropertyRule.php +++ b/src/Rule/EnforceReadonlyPublicPropertyRule.php @@ -6,8 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertyNode; use PHPStan\Php\PhpVersion; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; /** @@ -30,7 +30,7 @@ public function getNodeType(): string /** * @param ClassPropertyNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php index d8295e1..ce40ea8 100644 --- a/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php +++ b/src/Rule/ForbidArithmeticOperationOnNonNumberRule.php @@ -13,8 +13,8 @@ use PhpParser\Node\Expr\UnaryMinus; use PhpParser\Node\Expr\UnaryPlus; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; @@ -42,7 +42,7 @@ public function getNodeType(): string } /** - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -68,7 +68,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return list + * @return list */ private function processUnary(Expr $expr, Scope $scope, string $operator): array { @@ -90,7 +90,7 @@ private function processUnary(Expr $expr, Scope $scope, string $operator): array } /** - * @return list + * @return list */ private function processBinary(Expr $left, Expr $right, Scope $scope, string $operator): array { @@ -128,7 +128,7 @@ private function isNumeric(Type $type): bool } /** - * @return list + * @return list */ private function buildBinaryErrors(string $operator, string $type, Type $leftType, Type $rightType): array { diff --git a/src/Rule/ForbidAssignmentNotMatchingVarDocRule.php b/src/Rule/ForbidAssignmentNotMatchingVarDocRule.php deleted file mode 100644 index 97489a0..0000000 --- a/src/Rule/ForbidAssignmentNotMatchingVarDocRule.php +++ /dev/null @@ -1,137 +0,0 @@ - - */ -class ForbidAssignmentNotMatchingVarDocRule implements Rule -{ - - private FileTypeMapper $fileTypeMapper; - - private bool $allowNarrowing; - - public function __construct( - FileTypeMapper $fileTypeMapper, - bool $allowNarrowing - ) - { - $this->fileTypeMapper = $fileTypeMapper; - $this->allowNarrowing = $allowNarrowing; - } - - public function getNodeType(): string - { - return Assign::class; - } - - /** - * @param Assign $node - * @return list - */ - public function processNode(Node $node, Scope $scope): array - { - $checkShapeOnly = false; - $allowNarrowing = $this->allowNarrowing; - $phpDoc = $node->getDocComment(); - - if ($phpDoc === null) { - return []; - } - - if (mb_strpos($phpDoc->getText(), 'check-shape-only') !== false) { - $checkShapeOnly = true; // this is needed for example when phpstan-doctrine deduces nullable field, but you added WHERE IS NOT NULL - } - - if (mb_strpos($phpDoc->getText(), 'allow-narrowing') !== false) { - $allowNarrowing = true; - } - - $phpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->getClassReflection() !== null ? $scope->getClassReflection()->getName() : null, - $scope->getTraitReflection() !== null ? $scope->getTraitReflection()->getName() : null, - $scope->getFunctionName(), - $phpDoc->getText(), - ); - /** @var VarTag[] $varTags */ - $varTags = $phpDocBlock->getVarTags(); - - $variable = $node->var; - - if (!$variable instanceof Variable) { - return []; - } - - $variableName = $variable->name; - - if (!is_string($variableName)) { - return []; - } - - if (!isset($varTags[$variableName])) { - return []; - } - - $variableType = $varTags[$variableName]->getType(); - $valueType = $scope->getType($node->expr); - - if ($checkShapeOnly) { - $valueType = $this->weakenTypeToKeepShapeOnly($valueType); - } - - if ($variableType->accepts($valueType, $scope->isDeclareStrictTypes())->yes()) { - return []; - } - - $valueTypeString = $valueType->describe(VerbosityLevel::precise()); - $varPhpDocTypeString = $variableType->describe(VerbosityLevel::precise()); - - if ($valueType->accepts($variableType, $scope->isDeclareStrictTypes())->yes() && $variableType->isArray()->no()) { - if ($allowNarrowing) { - return []; - } - - $error = RuleErrorBuilder::message("Invalid var phpdoc of \${$variableName}. Cannot narrow {$valueTypeString} to {$varPhpDocTypeString}") - ->identifier('shipmonk.invalidVarDocAssignment') - ->build(); - return [$error]; - } - - $error = RuleErrorBuilder::message("Invalid var phpdoc of \${$variableName}. Cannot assign {$valueTypeString} to {$varPhpDocTypeString}") - ->identifier('shipmonk.invalidVarDocAssignment') - ->build(); - return [$error]; - } - - private function weakenTypeToKeepShapeOnly(Type $type): Type - { - return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type { - if ($type instanceof ArrayType || $type instanceof IterableType) { - return $traverse($type); // keep array shapes, but forget all inner types - } - - return new MixedType(); - }); - } - -} diff --git a/src/Rule/ForbidCastRule.php b/src/Rule/ForbidCastRule.php index 7517862..5a83602 100644 --- a/src/Rule/ForbidCastRule.php +++ b/src/Rule/ForbidCastRule.php @@ -13,8 +13,8 @@ use PhpParser\Node\Expr\Cast\String_; use PhpParser\Node\Expr\Cast\Unset_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use function get_class; use function in_array; @@ -47,7 +47,7 @@ public function getNodeType(): string /** * @param Cast $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidCheckedExceptionInCallableRule.php b/src/Rule/ForbidCheckedExceptionInCallableRule.php index 38454be..17df83b 100644 --- a/src/Rule/ForbidCheckedExceptionInCallableRule.php +++ b/src/Rule/ForbidCheckedExceptionInCallableRule.php @@ -4,36 +4,47 @@ use LogicException; use PhpParser\Node; -use PhpParser\Node\Expr; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\CallLike; +use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; +use PhpParser\Node\Expr\NullsafeMethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Expression; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Analyser\Scope; use PHPStan\Node\ClosureReturnStatementsNode; +use PHPStan\Node\FileNode; use PHPStan\Node\FunctionCallableNode; use PHPStan\Node\MethodCallableNode; use PHPStan\Node\StaticMethodCallableNode; +use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Type; -use ShipMonk\PHPStan\Visitor\ImmediatelyCalledCallableVisitor; use function array_map; use function array_merge; -use function array_merge_recursive; +use function array_values; use function explode; use function in_array; use function is_int; +use function spl_object_hash; +use function strpos; /** * @implements Rule @@ -48,34 +59,41 @@ class ForbidCheckedExceptionInCallableRule implements Rule private DefaultExceptionTypeResolver $exceptionTypeResolver; /** - * class::method => Closure argument index + * @var array spl_hash => true + */ + private array $allowedCallables = []; + + /** + * @var array spl_hash => methodName + */ + private array $callablesInArguments = []; + + /** + * class::method => callable argument index * or - * function => Closure argument index + * function => callable argument index * * @var array> */ private array $callablesAllowingCheckedExceptions; /** - * @param array> $immediatelyCalledCallables * @param array> $allowedCheckedExceptionCallables */ public function __construct( NodeScopeResolver $nodeScopeResolver, ReflectionProvider $reflectionProvider, DefaultExceptionTypeResolver $exceptionTypeResolver, - array $immediatelyCalledCallables, array $allowedCheckedExceptionCallables ) { - /** @var array> $callablesWithAllowedCheckedExceptions */ - $callablesWithAllowedCheckedExceptions = array_merge_recursive($immediatelyCalledCallables, $allowedCheckedExceptionCallables); + $this->checkClassExistence($reflectionProvider, $allowedCheckedExceptionCallables); $this->callablesAllowingCheckedExceptions = array_map( function ($argumentIndexes): array { return $this->normalizeArgumentIndexes($argumentIndexes); }, - $callablesWithAllowedCheckedExceptions, + $allowedCheckedExceptionCallables, ); $this->exceptionTypeResolver = $exceptionTypeResolver; $this->reflectionProvider = $reflectionProvider; @@ -88,35 +106,42 @@ public function getNodeType(): string } /** - * @return list + * @return list */ public function processNode( Node $node, Scope $scope ): array { - if ( - $node instanceof MethodCallableNode // @phpstan-ignore-line ignore bc promise - || $node instanceof StaticMethodCallableNode // @phpstan-ignore-line ignore bc promise - || $node instanceof FunctionCallableNode // @phpstan-ignore-line ignore bc promise + $errors = []; + + if ($node instanceof FileNode) { + $this->allowedCallables = []; + $this->callablesInArguments = []; + + } elseif ($node instanceof CallLike) { + $this->whitelistAllowedCallables($node, $scope); + + } elseif ( + $node instanceof MethodCallableNode + || $node instanceof StaticMethodCallableNode + || $node instanceof FunctionCallableNode ) { return $this->processFirstClassCallable($node->getOriginalNode(), $scope); - } - if ($node instanceof ClosureReturnStatementsNode) { // @phpstan-ignore-line ignore bc promise - return $this->processClosure($node, $scope); - } + } elseif ($node instanceof ClosureReturnStatementsNode) { + return $this->processClosure($node); - if ($node instanceof ArrowFunction) { + } elseif ($node instanceof ArrowFunction) { return $this->processArrowFunction($node, $scope); } - return []; + return $errors; } /** * @param MethodCall|StaticCall|FuncCall $callNode - * @return list + * @return list */ public function processFirstClassCallable( CallLike $callNode, @@ -127,50 +152,47 @@ public function processFirstClassCallable( throw new LogicException('This should be ensured by using XxxCallableNode'); } - if ($this->isAllowedToThrowCheckedException($callNode, $scope)) { + $nodeHash = spl_object_hash($callNode); + + if (isset($this->allowedCallables[$nodeHash])) { return []; } $errors = []; + $line = $callNode->getLine(); if ($callNode instanceof MethodCall && $callNode->name instanceof Identifier) { $callerType = $scope->getType($callNode->var); $methodName = $callNode->name->toString(); - $errors = array_merge($errors, $this->processCall($scope, $callerType, $methodName)); + $errors = array_merge($errors, $this->processCall($scope, $callerType, $methodName, $line, $nodeHash)); } if ($callNode instanceof StaticCall && $callNode->class instanceof Name && $callNode->name instanceof Identifier) { $callerType = $scope->resolveTypeByName($callNode->class); $methodName = $callNode->name->toString(); - $errors = array_merge($errors, $this->processCall($scope, $callerType, $methodName)); + $errors = array_merge($errors, $this->processCall($scope, $callerType, $methodName, $line, $nodeHash)); } if ($callNode instanceof FuncCall && $callNode->name instanceof Name) { $functionReflection = $this->reflectionProvider->getFunction($callNode->name, $scope); - $errors = array_merge($errors, $this->processThrowType($functionReflection->getThrowType(), $scope)); + $errors = array_merge($errors, $this->processThrowType($functionReflection->getThrowType(), $scope, $line, $nodeHash)); } return $errors; } /** - * @return list + * @return list */ public function processClosure( - ClosureReturnStatementsNode $node, - Scope $scope + ClosureReturnStatementsNode $node ): array { - $closure = $node->getClosureExpr(); - $parentScope = $scope->getParentScope(); // we need to detect type of caller, so the scope outside of this closure is needed + $nodeHash = spl_object_hash($node->getClosureExpr()); - if ($parentScope === null) { - return []; - } - - if ($this->isAllowedToThrowCheckedException($closure, $parentScope)) { + if (isset($this->allowedCallables[$nodeHash])) { return []; } @@ -183,10 +205,12 @@ public function processClosure( foreach ($throwPoint->getType()->getObjectClassNames() as $exceptionClass) { if ($this->exceptionTypeResolver->isCheckedException($exceptionClass, $throwPoint->getScope())) { - $errors[] = RuleErrorBuilder::message("Throwing checked exception $exceptionClass in closure!") - ->line($throwPoint->getNode()->getLine()) - ->identifier('shipmonk.checkedExceptionInCallable') - ->build(); + $errors[] = $this->buildError( + $exceptionClass, + 'closure', + $throwPoint->getNode()->getLine(), + $this->callablesInArguments[$nodeHash] ?? null, + ); } } } @@ -195,43 +219,47 @@ public function processClosure( } /** - * @return list + * @return list */ public function processArrowFunction( ArrowFunction $node, Scope $scope ): array { - if (!$scope instanceof MutatingScope) { // @phpstan-ignore-line ignore BC promise + if (!$scope instanceof MutatingScope) { throw new LogicException('Unexpected scope implementation'); } - if ($this->isAllowedToThrowCheckedException($node, $scope)) { + $nodeHash = spl_object_hash($node); + + if (isset($this->allowedCallables[$nodeHash])) { return []; } - $result = $this->nodeScopeResolver->processExprNode( // @phpstan-ignore-line ignore BC promise + $result = $this->nodeScopeResolver->processExprNode( new Expression($node->expr), $node->expr, $scope->enterArrowFunction($node), static function (): void { }, - ExpressionContext::createDeep(), // @phpstan-ignore-line ignore BC promise + ExpressionContext::createDeep(), ); $errors = []; - foreach ($result->getThrowPoints() as $throwPoint) { // @phpstan-ignore-line ignore BC promise + foreach ($result->getThrowPoints() as $throwPoint) { if (!$throwPoint->isExplicit()) { continue; } foreach ($throwPoint->getType()->getObjectClassNames() as $exceptionClass) { if ($this->exceptionTypeResolver->isCheckedException($exceptionClass, $throwPoint->getScope())) { - $errors[] = RuleErrorBuilder::message("Throwing checked exception $exceptionClass in arrow function!") - ->line($throwPoint->getNode()->getLine()) - ->identifier('shipmonk.checkedExceptionInArrowFunction') - ->build(); + $errors[] = $this->buildError( + $exceptionClass, + 'arrow function', + $throwPoint->getNode()->getLine(), + $this->callablesInArguments[$nodeHash] ?? null, + ); } } } @@ -240,29 +268,33 @@ static function (): void { } /** - * @return list + * @return list */ private function processCall( Scope $scope, Type $callerType, - string $methodName + string $methodName, + int $line, + string $nodeHash ): array { $methodReflection = $scope->getMethodReflection($callerType, $methodName); if ($methodReflection !== null) { - return $this->processThrowType($methodReflection->getThrowType(), $scope); + return $this->processThrowType($methodReflection->getThrowType(), $scope, $line, $nodeHash); } return []; } /** - * @return list + * @return list */ private function processThrowType( ?Type $throwType, - Scope $scope + Scope $scope, + int $line, + string $nodeHash ): array { if ($throwType === null) { @@ -273,49 +305,103 @@ private function processThrowType( foreach ($throwType->getObjectClassNames() as $exceptionClass) { if ($this->exceptionTypeResolver->isCheckedException($exceptionClass, $scope)) { - $errors[] = RuleErrorBuilder::message("Throwing checked exception $exceptionClass in first-class-callable!") - ->identifier('shipmonk.checkedExceptionInCallable') - ->build(); + $errors[] = $this->buildError( + $exceptionClass, + 'first-class-callable', + $line, + $this->callablesInArguments[$nodeHash] ?? null, + ); } } return $errors; } - public function isAllowedToThrowCheckedException( - Node $node, - Scope $scope - ): bool + /** + * @param int|list $argumentIndexes + * @return list + */ + private function normalizeArgumentIndexes($argumentIndexes): array + { + return is_int($argumentIndexes) ? [$argumentIndexes] : $argumentIndexes; + } + + /** + * @param array> $callables + */ + private function checkClassExistence( + ReflectionProvider $reflectionProvider, + array $callables + ): void { - /** @var Expr|Name|null $callerNodeWithClosureAsArg */ - $callerNodeWithClosureAsArg = $node->getAttribute(ImmediatelyCalledCallableVisitor::CALLER_WITH_CALLABLE_POSSIBLY_ALLOWING_CHECKED_EXCEPTION); - /** @var string|null $methodNameWithClosureAsArg */ - $methodNameWithClosureAsArg = $node->getAttribute(ImmediatelyCalledCallableVisitor::METHOD_WITH_CALLABLE_POSSIBLY_ALLOWING_CHECKED_EXCEPTION); - /** @var int|null $argumentIndexWithClosureAsArg */ - $argumentIndexWithClosureAsArg = $node->getAttribute(ImmediatelyCalledCallableVisitor::ARGUMENT_INDEX_WITH_CALLABLE_POSSIBLY_ALLOWING_CHECKED_EXCEPTION); - /** @var true|null $isAllowedToThrow */ - $isAllowedToThrow = $node->getAttribute(ImmediatelyCalledCallableVisitor::CALLABLE_ALLOWING_CHECKED_EXCEPTION); - - if ($isAllowedToThrow === true) { - return true; + foreach ($callables as $call => $args) { + if (strpos($call, '::') === false) { + continue; + } + + [$className] = explode('::', $call); + + if (!$reflectionProvider->hasClass($className)) { + throw new LogicException("Class $className used in 'allowedCheckedExceptionCallables' does not exist."); + } } + } - if ($callerNodeWithClosureAsArg === null || $methodNameWithClosureAsArg === null || $argumentIndexWithClosureAsArg === null) { - return false; + /** + * Copied from phpstan https://github.com/phpstan/phpstan-src/commit/cefa296f24b8c0b7d4dc3d383cbceea35267cb3f#diff-0c3f50d118357d9cb6d6f4d0eade75b83797d57056ff3b9c58ec881a13eaa6feR4113 + * + * @param FunctionReflection|MethodReflection $reflection + */ + private function isImmediatelyInvokedCallable(object $reflection, ?ParameterReflection $parameter): bool + { + if ($parameter instanceof ParameterReflectionWithPhpDocs) { + $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); + + if ($parameterCallImmediately->maybe()) { + return $reflection instanceof FunctionReflection; + } + + return $parameterCallImmediately->yes(); } - $callerWithClosureAsArgType = $callerNodeWithClosureAsArg instanceof Expr - ? $scope->getType($callerNodeWithClosureAsArg) - : $scope->resolveTypeByName($callerNodeWithClosureAsArg); + return $reflection instanceof FunctionReflection; + } + + private function isAllowedCheckedExceptionCallable( + ?Type $caller, + string $calledMethodName, + int $argumentIndex + ): bool + { + if ($caller === null) { + foreach ($this->callablesAllowingCheckedExceptions as $immediateFunction => $indexes) { + if (strpos($immediateFunction, '::') !== false) { + continue; + } - foreach ($callerWithClosureAsArgType->getObjectClassReflections() as $callerWithClosureAsArgClassReflection) { + if ( + $immediateFunction === $calledMethodName + && in_array($argumentIndex, $indexes, true) + ) { + return true; + } + } + + return false; + } + + foreach ($caller->getObjectClassReflections() as $callerReflection) { foreach ($this->callablesAllowingCheckedExceptions as $immediateCallerAndMethod => $indexes) { - [$callerClass, $methodName] = explode('::', $immediateCallerAndMethod); + if (strpos($immediateCallerAndMethod, '::') === false) { + continue; + } + + [$callerClass, $methodName] = explode('::', $immediateCallerAndMethod); // @phpstan-ignore offsetAccess.notFound if ( - $methodName === $methodNameWithClosureAsArg - && in_array($argumentIndexWithClosureAsArg, $indexes, true) - && $callerWithClosureAsArgClassReflection->is($callerClass) + $methodName === $calledMethodName + && in_array($argumentIndex, $indexes, true) + && $callerReflection->is($callerClass) ) { return true; } @@ -325,13 +411,127 @@ public function isAllowedToThrowCheckedException( return false; } + private function whitelistAllowedCallables(CallLike $node, Scope $scope): void + { + if ($node instanceof MethodCall && $node->name instanceof Identifier) { + $callerType = $scope->getType($node->var); + $methodReflection = $scope->getMethodReflection($callerType, $node->name->name); + + } elseif ($node instanceof StaticCall && $node->name instanceof Identifier && $node->class instanceof Name) { + $callerType = $scope->resolveTypeByName($node->class); + $methodReflection = $scope->getMethodReflection($callerType, $node->name->name); + + } elseif ($node instanceof New_ && $node->class instanceof Name) { + $callerType = $scope->resolveTypeByName($node->class); + $methodReflection = $scope->getMethodReflection($callerType, '__construct'); + + } elseif ($node instanceof FuncCall && $node->name instanceof Name) { + $callerType = null; + $methodReflection = $this->reflectionProvider->getFunction($node->name, $scope); + + } elseif ($node instanceof FuncCall && $this->isFirstClassCallableOrClosureOrArrowFunction($node->name)) { // immediately called callable syntax + $this->allowedCallables[spl_object_hash($node->name)] = true; + return; + + } else { + return; + } + + if ($methodReflection === null) { + return; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $node->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); + + if ($node instanceof New_) { + $arguments = (ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $node) ?? $node)->getArgs(); + + } elseif ($node instanceof FuncCall) { + $arguments = (ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node) ?? $node)->getArgs(); + + } elseif ($node instanceof MethodCall) { + $arguments = (ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $node) ?? $node)->getArgs(); + + } elseif ($node instanceof StaticCall) { + $arguments = (ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $node) ?? $node)->getArgs(); + + } else { + throw new LogicException('Unexpected node type'); + } + + /** @var list $args */ + $args = array_values($arguments); + $parameters = $parametersAcceptor->getParameters(); + + foreach ($args as $index => $arg) { + $parameterIndex = $this->getParameterIndex($arg, $index, $parameters); + $parameter = $parameters[$parameterIndex] ?? null; + $argHash = spl_object_hash($arg->value); + + if ( + $this->isImmediatelyInvokedCallable($methodReflection, $parameter) + || $this->isAllowedCheckedExceptionCallable($callerType, $methodReflection->getName(), $index) + ) { + $this->allowedCallables[$argHash] = true; + } + + if ($this->isFirstClassCallableOrClosureOrArrowFunction($arg->value)) { + $callerClass = $callerType !== null && $callerType->getObjectClassNames() !== [] ? $callerType->getObjectClassNames()[0] : null; + $methodReference = $callerClass !== null ? "$callerClass::{$methodReflection->getName()}" : $methodReflection->getName(); + $this->callablesInArguments[$argHash] = $methodReference; + } + } + } + /** - * @param int|list $argumentIndexes - * @return list + * @param array $parameters */ - private function normalizeArgumentIndexes($argumentIndexes): array + private function getParameterIndex(Arg $arg, int $argumentIndex, array $parameters): ?int { - return is_int($argumentIndexes) ? [$argumentIndexes] : $argumentIndexes; + if ($arg->name === null) { + return $argumentIndex; + } + + foreach ($parameters as $parameterIndex => $parameter) { + if ($parameter->getName() === $arg->name->toString()) { + return $parameterIndex; + } + } + + return null; + } + + private function isFirstClassCallableOrClosureOrArrowFunction(Node $node): bool + { + return $node instanceof Closure + || $node instanceof ArrowFunction + || ($node instanceof MethodCall && $node->isFirstClassCallable()) + || ($node instanceof NullsafeMethodCall && $node->isFirstClassCallable()) + || ($node instanceof StaticCall && $node->isFirstClassCallable()) + || ($node instanceof FuncCall && $node->isFirstClassCallable()); + } + + private function buildError( + string $exceptionClass, + string $where, + int $line, + ?string $usedAsArgumentOfMethodName + ): IdentifierRuleError + { + $builder = RuleErrorBuilder::message("Throwing checked exception $exceptionClass in $where!") + ->line($line) + ->identifier('shipmonk.checkedExceptionInCallable'); + + if ($usedAsArgumentOfMethodName !== null) { + $builder->tip("If this callable is immediately called within '$usedAsArgumentOfMethodName', you should add @param-immediately-invoked-callable there. Then this error disappears and the exception will be properly propagated."); + } + + return $builder->build(); } } diff --git a/src/Rule/ForbidCheckedExceptionInYieldingMethodRule.php b/src/Rule/ForbidCheckedExceptionInYieldingMethodRule.php index 2e6d45c..1ef8ef8 100644 --- a/src/Rule/ForbidCheckedExceptionInYieldingMethodRule.php +++ b/src/Rule/ForbidCheckedExceptionInYieldingMethodRule.php @@ -6,8 +6,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; /** @@ -30,7 +30,7 @@ public function getNodeType(): string /** * @param MethodReturnStatementsNode $node - * @return list + * @return list */ public function processNode( Node $node, diff --git a/src/Rule/ForbidCustomFunctionsRule.php b/src/Rule/ForbidCustomFunctionsRule.php index a3eab4d..88e4cd3 100644 --- a/src/Rule/ForbidCustomFunctionsRule.php +++ b/src/Rule/ForbidCustomFunctionsRule.php @@ -14,8 +14,8 @@ use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantStringType; use function array_map; @@ -70,6 +70,10 @@ public function __construct(array $forbiddenFunctions, ReflectionProvider $refle throw new LogicException("Unexpected format of forbidden function {$forbiddenFunction}, expected Namespace\Class::methodName"); } + if ($className !== self::FUNCTION && !$reflectionProvider->hasClass($className)) { + throw new LogicException("Class {$className} used in 'forbiddenFunctions' does not exist"); + } + $this->forbiddenFunctions[$className][$methodName] = $description; } } @@ -81,7 +85,7 @@ public function getNodeType(): string /** * @param CallLike $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -126,7 +130,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return list + * @return list */ private function validateConstructorWithDynamicString(Expr $expr, Scope $scope): array { @@ -143,7 +147,7 @@ private function validateConstructorWithDynamicString(Expr $expr, Scope $scope): /** * @param list $methodNames - * @return list + * @return list */ private function validateCallOverExpr(array $methodNames, Expr $expr, Scope $scope): array { @@ -163,7 +167,7 @@ private function validateCallOverExpr(array $methodNames, Expr $expr, Scope $sco /** * @param list $methodNames - * @return list + * @return list */ private function validateMethod(array $methodNames, string $className): array { @@ -194,7 +198,7 @@ private function validateMethod(array $methodNames, string $className): array /** * @param list $functionNames - * @return list + * @return list */ private function validateFunction(array $functionNames): array { diff --git a/src/Rule/ForbidEnumInFunctionArgumentsRule.php b/src/Rule/ForbidEnumInFunctionArgumentsRule.php index 156ac64..81232b0 100644 --- a/src/Rule/ForbidEnumInFunctionArgumentsRule.php +++ b/src/Rule/ForbidEnumInFunctionArgumentsRule.php @@ -9,8 +9,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Type; use PHPStan\Type\UnionType; @@ -68,7 +68,7 @@ public function getNodeType(): string /** * @param FuncCall $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidFetchOnMixedRule.php b/src/Rule/ForbidFetchOnMixedRule.php index 91fff83..fb571a5 100644 --- a/src/Rule/ForbidFetchOnMixedRule.php +++ b/src/Rule/ForbidFetchOnMixedRule.php @@ -11,8 +11,8 @@ use PhpParser\Node\Identifier; use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Type; use PHPStan\Type\TypeUtils; @@ -41,7 +41,7 @@ public function getNodeType(): string } /** - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -58,7 +58,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param PropertyFetch|StaticPropertyFetch|ClassConstFetch $node - * @return list + * @return list */ private function processFetch(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidIdenticalClassComparisonRule.php b/src/Rule/ForbidIdenticalClassComparisonRule.php index 1d08a7c..556d2ba 100644 --- a/src/Rule/ForbidIdenticalClassComparisonRule.php +++ b/src/Rule/ForbidIdenticalClassComparisonRule.php @@ -10,8 +10,8 @@ use PhpParser\Node\Expr\BinaryOp\NotIdentical; use PHPStan\Analyser\Scope; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; @@ -42,7 +42,7 @@ public function __construct( { foreach ($blacklist as $className) { if (!$reflectionProvider->hasClass($className)) { - throw new LogicException("Class {$className} does not exist."); + throw new LogicException("Class {$className} used in 'forbidIdenticalClassComparison' does not exist."); } } @@ -56,7 +56,7 @@ public function getNodeType(): string /** * @param BinaryOp $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidIncrementDecrementOnNonIntegerRule.php b/src/Rule/ForbidIncrementDecrementOnNonIntegerRule.php index 626a66e..5fb9971 100644 --- a/src/Rule/ForbidIncrementDecrementOnNonIntegerRule.php +++ b/src/Rule/ForbidIncrementDecrementOnNonIntegerRule.php @@ -9,8 +9,8 @@ use PhpParser\Node\Expr\PreDec; use PhpParser\Node\Expr\PreInc; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\VerbosityLevel; use function get_class; @@ -28,7 +28,7 @@ public function getNodeType(): string } /** - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -46,7 +46,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param PostInc|PostDec|PreInc|PreDec $node - * @return list + * @return list */ private function process(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidMatchDefaultArmForEnumsRule.php b/src/Rule/ForbidMatchDefaultArmForEnumsRule.php index cbc4d1c..d941918 100644 --- a/src/Rule/ForbidMatchDefaultArmForEnumsRule.php +++ b/src/Rule/ForbidMatchDefaultArmForEnumsRule.php @@ -5,8 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\MatchExpressionNode; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use function count; @@ -23,7 +23,7 @@ public function getNodeType(): string /** * @param MatchExpressionNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidMethodCallOnMixedRule.php b/src/Rule/ForbidMethodCallOnMixedRule.php index d9a8b65..bb61cab 100644 --- a/src/Rule/ForbidMethodCallOnMixedRule.php +++ b/src/Rule/ForbidMethodCallOnMixedRule.php @@ -11,8 +11,8 @@ use PhpParser\Node\Identifier; use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeUtils; use function get_class; @@ -41,7 +41,7 @@ public function getNodeType(): string /** * @param CallLike $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -59,7 +59,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param MethodCall|StaticCall $node - * @return list + * @return list */ private function checkCall(CallLike $node, Scope $scope): array { diff --git a/src/Rule/ForbidNotNormalizedTypeRule.php b/src/Rule/ForbidNotNormalizedTypeRule.php index daf774d..6d8d799 100644 --- a/src/Rule/ForbidNotNormalizedTypeRule.php +++ b/src/Rule/ForbidNotNormalizedTypeRule.php @@ -31,8 +31,8 @@ use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\VerbosityLevel; @@ -85,7 +85,7 @@ public function getNodeType(): string } /** - * @return list + * @return list */ public function processNode( PhpParserNode $node, @@ -114,7 +114,7 @@ public function processNode( } /** - * @return list + * @return list */ private function checkCatchNativeType(Catch_ $node, Scope $scope): array { @@ -123,7 +123,7 @@ private function checkCatchNativeType(Catch_ $node, Scope $scope): array } /** - * @return list + * @return list */ private function checkParamAndReturnAndThrowsPhpDoc( FunctionLike $node, @@ -157,7 +157,7 @@ private function checkParamAndReturnAndThrowsPhpDoc( } /** - * @return list + * @return list */ private function checkPropertyNativeType(Property $node, Scope $scope): array { @@ -176,7 +176,7 @@ private function checkPropertyNativeType(Property $node, Scope $scope): array } /** - * @return list + * @return list */ private function checkParamAndReturnNativeType(FunctionLike $node, Scope $scope): array { @@ -208,7 +208,7 @@ private function checkParamAndReturnNativeType(FunctionLike $node, Scope $scope) } /** - * @return list + * @return list */ private function checkPropertyPhpDoc( Property $node, @@ -237,7 +237,7 @@ private function checkPropertyPhpDoc( } /** - * @return list + * @return list */ private function checkInlineVarDoc(PhpParserNode $node, Scope $scope): array { @@ -307,7 +307,7 @@ private function getFunctionName(PhpParserNode $node): ?string /** * @param array $paramTagValues - * @return list + * @return list */ public function processParamTags( PhpParserNode $sourceNode, @@ -335,7 +335,7 @@ public function processParamTags( /** * @param array $varTagValues - * @return list + * @return list */ public function processVarTags( PhpParserNode $originalNode, @@ -367,7 +367,7 @@ public function processVarTags( /** * @param array $returnTagValues - * @return list + * @return list */ public function processReturnTags( PhpParserNode $originalNode, @@ -391,7 +391,7 @@ public function processReturnTags( /** * @param array $throwsTagValues - * @return list + * @return list */ public function processThrowsTags( PhpParserNode $originalNode, @@ -503,7 +503,7 @@ private function traversePhpDocTypeNode( /** * @param IntersectionType|UnionType $multiTypeNode - * @return list + * @return list */ private function processMultiTypePhpParserNode( ComplexType $multiTypeNode, @@ -520,7 +520,7 @@ private function processMultiTypePhpParserNode( foreach ($innerTypeNodes as $i => $iValue) { for ($j = $i + 1; $j < $countOfNodeTypes; $j++) { $typeNodeA = $iValue; - $typeNodeB = $innerTypeNodes[$j]; + $typeNodeB = $innerTypeNodes[$j]; // @phpstan-ignore offsetAccess.notFound $typeA = $scope->getFunctionType($typeNodeA, false, false); $typeB = $scope->getFunctionType($typeNodeB, false, false); @@ -557,7 +557,7 @@ private function printPhpParserNode(PhpParserNode $node): string /** * @param UnionTypeNode|IntersectionTypeNode $multiTypeNode - * @return list + * @return list */ private function processMultiTypePhpDocNode( TypeNode $multiTypeNode, @@ -588,7 +588,7 @@ private function processMultiTypePhpDocNode( foreach ($innerTypeNodes as $i => $iValue) { for ($j = $i + 1; $j < $countOfNodeTypes; $j++) { $typeNodeA = $iValue; - $typeNodeB = $innerTypeNodes[$j]; + $typeNodeB = $innerTypeNodes[$j]; // @phpstan-ignore offsetAccess.notFound $typeA = $this->typeNodeResolver->resolve($typeNodeA, $nameSpace); $typeB = $this->typeNodeResolver->resolve($typeNodeB, $nameSpace); diff --git a/src/Rule/ForbidNullInAssignOperationsRule.php b/src/Rule/ForbidNullInAssignOperationsRule.php index 4ec6bf0..496e818 100644 --- a/src/Rule/ForbidNullInAssignOperationsRule.php +++ b/src/Rule/ForbidNullInAssignOperationsRule.php @@ -19,8 +19,8 @@ use PhpParser\Node\Expr\AssignOp\ShiftLeft; use PhpParser\Node\Expr\AssignOp\ShiftRight; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeCombinator; use function get_class; @@ -54,7 +54,7 @@ public function getNodeType(): string /** * @param AssignOp $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidNullInBinaryOperationsRule.php b/src/Rule/ForbidNullInBinaryOperationsRule.php index 9071f63..340f8b7 100644 --- a/src/Rule/ForbidNullInBinaryOperationsRule.php +++ b/src/Rule/ForbidNullInBinaryOperationsRule.php @@ -5,8 +5,8 @@ use PhpParser\Node; use PhpParser\Node\Expr\BinaryOp; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; @@ -40,7 +40,7 @@ public function getNodeType(): string /** * @param BinaryOp $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidNullInInterpolatedStringRule.php b/src/Rule/ForbidNullInInterpolatedStringRule.php index 38259f1..0a13d65 100644 --- a/src/Rule/ForbidNullInInterpolatedStringRule.php +++ b/src/Rule/ForbidNullInInterpolatedStringRule.php @@ -7,8 +7,8 @@ use PhpParser\Node\Scalar\EncapsedStringPart; use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\TypeCombinator; @@ -32,7 +32,7 @@ public function getNodeType(): string /** * @param Encapsed $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidPhpDocNullabilityMismatchWithNativeTypehintRule.php b/src/Rule/ForbidPhpDocNullabilityMismatchWithNativeTypehintRule.php index 51a4722..472a909 100644 --- a/src/Rule/ForbidPhpDocNullabilityMismatchWithNativeTypehintRule.php +++ b/src/Rule/ForbidPhpDocNullabilityMismatchWithNativeTypehintRule.php @@ -11,8 +11,8 @@ use PhpParser\Node\Stmt\Property; use PHPStan\Analyser\Scope; use PHPStan\PhpDoc\ResolvedPhpDocBlock; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\MixedType; @@ -42,7 +42,7 @@ public function getNodeType(): string } /** - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -61,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return list + * @return list */ private function checkReturnTypes(FunctionLike $node, Scope $scope): array { @@ -72,7 +72,7 @@ private function checkReturnTypes(FunctionLike $node, Scope $scope): array } /** - * @return list + * @return list */ private function checkPropertyTypes(Property $node, Scope $scope): array { @@ -83,7 +83,7 @@ private function checkPropertyTypes(Property $node, Scope $scope): array } /** - * @return list + * @return list */ private function checkParamTypes(FunctionLike $node, Scope $scope): array { @@ -200,7 +200,7 @@ private function getPhpDocParamType(FunctionLike $node, Scope $scope, string $pa } /** - * @return list + * @return list */ private function comparePhpDocAndNativeType( ?Type $phpDocReturnType, diff --git a/src/Rule/ForbidProtectedEnumMethodRule.php b/src/Rule/ForbidProtectedEnumMethodRule.php index ca7947b..636987c 100644 --- a/src/Rule/ForbidProtectedEnumMethodRule.php +++ b/src/Rule/ForbidProtectedEnumMethodRule.php @@ -5,8 +5,8 @@ use PhpParser\Node; use PHPStan\Analyser\Scope; use PHPStan\Node\ClassMethodsNode; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; /** @@ -22,7 +22,7 @@ public function getNodeType(): string /** * @param ClassMethodsNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidReturnInConstructorRule.php b/src/Rule/ForbidReturnInConstructorRule.php index 8b39a06..aa140cb 100644 --- a/src/Rule/ForbidReturnInConstructorRule.php +++ b/src/Rule/ForbidReturnInConstructorRule.php @@ -6,8 +6,8 @@ use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; /** @@ -23,7 +23,7 @@ public function getNodeType(): string /** * @param Return_ $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidReturnValueInYieldingMethodRule.php b/src/Rule/ForbidReturnValueInYieldingMethodRule.php index ecae572..b379353 100644 --- a/src/Rule/ForbidReturnValueInYieldingMethodRule.php +++ b/src/Rule/ForbidReturnValueInYieldingMethodRule.php @@ -9,8 +9,8 @@ use PHPStan\Node\MethodReturnStatementsNode; use PHPStan\Node\ReturnStatementsNode; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -36,7 +36,7 @@ public function getNodeType(): string /** * @param ReturnStatementsNode $node - * @return list + * @return list */ public function processNode( Node $node, @@ -68,7 +68,7 @@ public function processNode( ? 'this approach is denied' : 'but this method is not marked to return Generator'; - $callType = $node instanceof MethodReturnStatementsNode // @phpstan-ignore-line ignore bc promise + $callType = $node instanceof MethodReturnStatementsNode ? 'method' : 'function'; @@ -85,7 +85,7 @@ private function getReturnType(ReturnStatementsNode $node, Scope $scope): Type { $methodReflection = $scope->getFunction(); - if ($node instanceof ClosureReturnStatementsNode) { // @phpstan-ignore-line ignore bc promise + if ($node instanceof ClosureReturnStatementsNode) { return $scope->getFunctionType($node->getClosureExpr()->getReturnType(), false, false); } diff --git a/src/Rule/ForbidUnsetClassFieldRule.php b/src/Rule/ForbidUnsetClassFieldRule.php index 2087af9..3c5d793 100644 --- a/src/Rule/ForbidUnsetClassFieldRule.php +++ b/src/Rule/ForbidUnsetClassFieldRule.php @@ -6,8 +6,8 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Stmt\Unset_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; /** @@ -23,7 +23,7 @@ public function getNodeType(): string /** * @param Unset_ $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/ForbidUnusedExceptionRule.php b/src/Rule/ForbidUnusedExceptionRule.php index d4607c8..aebe1f7 100644 --- a/src/Rule/ForbidUnusedExceptionRule.php +++ b/src/Rule/ForbidUnusedExceptionRule.php @@ -10,8 +10,8 @@ use PhpParser\Node\Expr\StaticCall; use PhpParser\PrettyPrinter\Standard; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use ShipMonk\PHPStan\Visitor\UnusedExceptionVisitor; use Throwable; @@ -36,7 +36,7 @@ public function getNodeType(): string /** * @param Expr $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array /** * @param MethodCall|StaticCall $node - * @return list + * @return list */ private function processCall(CallLike $node, Scope $scope): array { @@ -72,7 +72,7 @@ private function processCall(CallLike $node, Scope $scope): array } /** - * @return list + * @return list */ private function processNew(New_ $node, Scope $scope): array { diff --git a/src/Rule/ForbidUnusedMatchResultRule.php b/src/Rule/ForbidUnusedMatchResultRule.php index 2d59079..efe4404 100644 --- a/src/Rule/ForbidUnusedMatchResultRule.php +++ b/src/Rule/ForbidUnusedMatchResultRule.php @@ -6,15 +6,13 @@ use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Match_; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; -use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use ShipMonk\PHPStan\Visitor\UnusedMatchVisitor; -use function method_exists; /** * @implements Rule @@ -29,17 +27,14 @@ public function getNodeType(): string /** * @param Match_ $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { $returnedTypes = []; foreach ($node->arms as $arm) { - /** @var Type $armType */ - $armType = method_exists($scope, 'getKeepVoidType') // Needed since https://github.com/phpstan/phpstan/releases/tag/1.10.49, can be dropped once we bump PHPStan version gte that - ? $scope->getKeepVoidType($arm->body) - : $scope->getType($arm->body); + $armType = $scope->getKeepVoidType($arm->body); if (!$armType->isVoid()->yes() && !$armType instanceof NeverType && !$arm->body instanceof Assign) { $returnedTypes[] = $armType; diff --git a/src/Rule/ForbidUselessNullableReturnRule.php b/src/Rule/ForbidUselessNullableReturnRule.php index 99130d2..15777d1 100644 --- a/src/Rule/ForbidUselessNullableReturnRule.php +++ b/src/Rule/ForbidUselessNullableReturnRule.php @@ -7,8 +7,8 @@ use PHPStan\Node\ClosureReturnStatementsNode; use PHPStan\Node\ReturnStatementsNode; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; @@ -29,14 +29,14 @@ public function getNodeType(): string /** * @param ReturnStatementsNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { $verbosity = VerbosityLevel::precise(); $methodReflection = $scope->getFunction(); - if ($node instanceof ClosureReturnStatementsNode) { // @phpstan-ignore-line ignore bc promise + if ($node instanceof ClosureReturnStatementsNode) { $declaredType = $scope->getFunctionType($node->getClosureExpr()->getReturnType(), false, false); } elseif ($methodReflection !== null) { $declaredType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType(); diff --git a/src/Rule/ForbidVariableTypeOverwritingRule.php b/src/Rule/ForbidVariableTypeOverwritingRule.php index 1135c50..acacbb9 100644 --- a/src/Rule/ForbidVariableTypeOverwritingRule.php +++ b/src/Rule/ForbidVariableTypeOverwritingRule.php @@ -7,8 +7,8 @@ use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Variable; use PHPStan\Analyser\Scope; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Accessory\AccessoryType; use PHPStan\Type\Enum\EnumCaseObjectType; @@ -35,7 +35,7 @@ public function getNodeType(): string /** * @param Assign $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -85,7 +85,7 @@ private function generalize(Type $type): Type if ( $type->isConstantValue()->yes() || $type instanceof IntegerRangeType - || $type instanceof EnumCaseObjectType // @phpstan-ignore-line ignore instanceof warning + || $type instanceof EnumCaseObjectType // @phpstan-ignore phpstanApi.instanceofType ) { $type = $type->generalize(GeneralizePrecision::lessSpecific()); } @@ -108,11 +108,11 @@ private function removeNullAccessoryAndSubtractedTypes(Type $type): Type return $type; } - if ($type instanceof IntersectionType) { // @phpstan-ignore-line ignore instanceof intersection + if ($type instanceof IntersectionType) { // @phpstan-ignore phpstanApi.instanceofType $newInnerTypes = []; foreach ($type->getTypes() as $innerType) { - if ($innerType instanceof AccessoryType) { // @phpstan-ignore-line ignore bc promise + if ($innerType instanceof AccessoryType) { // @phpstan-ignore phpstanApi.instanceofType continue; } @@ -122,8 +122,8 @@ private function removeNullAccessoryAndSubtractedTypes(Type $type): Type $type = TypeCombinator::intersect(...$newInnerTypes); } - if ($type instanceof SubtractableType) { // @phpstan-ignore-line ignore bc promise - $type = $type->getTypeWithoutSubtractedType(); // @phpstan-ignore-line ignore bc promise + if ($type instanceof SubtractableType) { + $type = $type->getTypeWithoutSubtractedType(); } return TypeCombinator::removeNull($type); diff --git a/src/Rule/RequirePreviousExceptionPassRule.php b/src/Rule/RequirePreviousExceptionPassRule.php index d0f6e5f..6d8cc19 100644 --- a/src/Rule/RequirePreviousExceptionPassRule.php +++ b/src/Rule/RequirePreviousExceptionPassRule.php @@ -17,8 +17,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptorSelector; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; @@ -52,7 +52,7 @@ public function getNodeType(): string /** * @param TryCatch $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { @@ -95,7 +95,7 @@ public function processNode(Node $node, Scope $scope): array } /** - * @return list + * @return list */ private function processExceptionCreation( bool $strictTypes, diff --git a/src/Rule/UselessPrivatePropertyDefaultValueRule.php b/src/Rule/UselessPrivatePropertyDefaultValueRule.php index f9af2a0..9db73c8 100644 --- a/src/Rule/UselessPrivatePropertyDefaultValueRule.php +++ b/src/Rule/UselessPrivatePropertyDefaultValueRule.php @@ -7,8 +7,8 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\ClassPropertiesNode; use PHPStan\Node\Property\PropertyWrite; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use ShipMonk\PHPStan\Visitor\TopLevelConstructorPropertyFetchMarkingVisitor; @@ -25,7 +25,7 @@ public function getNodeType(): string /** * @param ClassPropertiesNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Rule/UselessPrivatePropertyNullabilityRule.php b/src/Rule/UselessPrivatePropertyNullabilityRule.php index 5620053..6786be2 100644 --- a/src/Rule/UselessPrivatePropertyNullabilityRule.php +++ b/src/Rule/UselessPrivatePropertyNullabilityRule.php @@ -9,8 +9,8 @@ use PHPStan\Node\ClassPropertiesNode; use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\ClassReflection; +use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; -use PHPStan\Rules\RuleError; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\NullType; use PHPStan\Type\TypeCombinator; @@ -29,7 +29,7 @@ public function getNodeType(): string /** * @param ClassPropertiesNode $node - * @return list + * @return list */ public function processNode(Node $node, Scope $scope): array { diff --git a/src/Visitor/ClassPropertyAssignmentVisitor.php b/src/Visitor/ClassPropertyAssignmentVisitor.php index 0f8c708..107d389 100644 --- a/src/Visitor/ClassPropertyAssignmentVisitor.php +++ b/src/Visitor/ClassPropertyAssignmentVisitor.php @@ -8,7 +8,7 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\NodeVisitorAbstract; use function array_pop; -use function count; +use function end; class ClassPropertyAssignmentVisitor extends NodeVisitorAbstract { @@ -16,7 +16,7 @@ class ClassPropertyAssignmentVisitor extends NodeVisitorAbstract public const ASSIGNED_EXPR = ShipMonkNodeVisitor::NODE_ATTRIBUTE_PREFIX . 'assignment'; /** - * @var Node[] + * @var list */ private array $stack = []; @@ -33,7 +33,7 @@ public function beforeTraverse(array $nodes): ?array public function enterNode(Node $node): ?Node { if ($this->stack !== []) { - $parent = $this->stack[count($this->stack) - 1]; + $parent = end($this->stack); if ( $parent instanceof Assign diff --git a/src/Visitor/ImmediatelyCalledCallableVisitor.php b/src/Visitor/ImmediatelyCalledCallableVisitor.php deleted file mode 100644 index fbd1eb8..0000000 --- a/src/Visitor/ImmediatelyCalledCallableVisitor.php +++ /dev/null @@ -1,171 +0,0 @@ - callable argument indexes - * - * @var array> - */ - private array $methodsWithAllowedCheckedExceptions = []; - - /** - * function name => callable argument indexes - * - * @var array> - */ - private array $functionsWithAllowedCheckedExceptions = []; - - /** - * @param array> $immediatelyCalledCallables - * @param array> $allowedCheckedExceptionCallables - */ - public function __construct( - array $immediatelyCalledCallables = [], - array $allowedCheckedExceptionCallables = [] - ) - { - /** @var array> $callablesWithAllowedCheckedExceptions */ - $callablesWithAllowedCheckedExceptions = array_merge_recursive($immediatelyCalledCallables, $allowedCheckedExceptionCallables); - - foreach ($callablesWithAllowedCheckedExceptions as $call => $arguments) { - $normalizedArguments = $this->normalizeArgumentIndexes($arguments); - - if (strpos($call, '::') !== false) { - [, $methodName] = explode('::', $call); - $existingArguments = $this->methodsWithAllowedCheckedExceptions[$methodName] ?? []; - $this->methodsWithAllowedCheckedExceptions[$methodName] = array_values(array_unique(array_merge($existingArguments, $normalizedArguments))); - } else { - $this->functionsWithAllowedCheckedExceptions[$call] = $normalizedArguments; - } - } - } - - public function enterNode(Node $node): ?Node - { - if ($node instanceof MethodCall || $node instanceof NullsafeMethodCall || $node instanceof StaticCall) { - $this->resolveMethodCall($node); - } - - if ($node instanceof FuncCall) { - $this->resolveFuncCall($node); - } - - return null; - } - - /** - * @param StaticCall|MethodCall|NullsafeMethodCall $node - */ - private function resolveMethodCall(CallLike $node): void - { - if (!$node->name instanceof Identifier) { - return; - } - - $methodName = $node->name->name; - $argumentIndexes = $this->methodsWithAllowedCheckedExceptions[$methodName] ?? null; - - if ($argumentIndexes === null) { - return; - } - - foreach ($argumentIndexes as $argumentIndex) { - $argument = $node->getArgs()[$argumentIndex] ?? null; - - if ($argument === null) { - continue; - } - - if (!$this->isFirstClassCallableOrClosureOrArrowFunction($argument->value)) { - continue; - } - - // we cannot decide true/false like in function calls as we dont know caller type yet, this has to be resolved in Rule - $node->getArgs()[$argumentIndex]->value->setAttribute(self::CALLER_WITH_CALLABLE_POSSIBLY_ALLOWING_CHECKED_EXCEPTION, $node instanceof StaticCall ? $node->class : $node->var); - $node->getArgs()[$argumentIndex]->value->setAttribute(self::METHOD_WITH_CALLABLE_POSSIBLY_ALLOWING_CHECKED_EXCEPTION, $node->name->toString()); - $node->getArgs()[$argumentIndex]->value->setAttribute(self::ARGUMENT_INDEX_WITH_CALLABLE_POSSIBLY_ALLOWING_CHECKED_EXCEPTION, $argumentIndex); - } - } - - private function resolveFuncCall(FuncCall $node): void - { - if ($this->isFirstClassCallableOrClosureOrArrowFunction($node->name)) { - // phpcs:ignore Squiz.PHP.CommentedOutCode.Found - $node->name->setAttribute(self::CALLABLE_ALLOWING_CHECKED_EXCEPTION, true); // immediately called closure syntax, e.g. (function(){})() - return; - } - - if (!$node->name instanceof Name) { - return; - } - - $methodName = $node->name->toString(); - $argumentIndexes = $this->functionsWithAllowedCheckedExceptions[$methodName] ?? null; - - if ($argumentIndexes === null) { - return; - } - - foreach ($argumentIndexes as $argumentIndex) { - $argument = $node->getArgs()[$argumentIndex] ?? null; - - if ($argument === null) { - continue; - } - - if (!$this->isFirstClassCallableOrClosureOrArrowFunction($argument->value)) { - continue; - } - - $node->getArgs()[$argumentIndex]->value->setAttribute(self::CALLABLE_ALLOWING_CHECKED_EXCEPTION, true); - } - } - - private function isFirstClassCallableOrClosureOrArrowFunction(Node $node): bool - { - return $node instanceof Closure - || $node instanceof ArrowFunction - || ($node instanceof MethodCall && $node->isFirstClassCallable()) - || ($node instanceof NullsafeMethodCall && $node->isFirstClassCallable()) - || ($node instanceof StaticCall && $node->isFirstClassCallable()) - || ($node instanceof FuncCall && $node->isFirstClassCallable()); - } - - /** - * @param int|list $argumentIndexes - * @return list - */ - private function normalizeArgumentIndexes($argumentIndexes): array - { - return is_int($argumentIndexes) ? [$argumentIndexes] : $argumentIndexes; - } - -} diff --git a/src/Visitor/NamedArgumentSourceVisitor.php b/src/Visitor/NamedArgumentSourceVisitor.php deleted file mode 100644 index f0b4d09..0000000 --- a/src/Visitor/NamedArgumentSourceVisitor.php +++ /dev/null @@ -1,64 +0,0 @@ -stack = []; - return null; - } - - public function enterNode(Node $node): ?Node - { - if ($this->stack !== []) { - $parent = $this->stack[count($this->stack) - 1]; - - if ( - $parent instanceof Attribute - && $node instanceof Arg - && $node->name !== null - ) { - $node->setAttribute(self::IS_ATTRIBUTE_NAMED_ARGUMENT, true); - } - } - - if ($this->shouldBuildStack($node)) { // start adding to stack once Attribute is reached - $this->stack[] = $node; - } - - return null; - } - - private function shouldBuildStack(Node $node): bool - { - return $this->stack !== [] || $node instanceof Attribute; - } - - public function leaveNode(Node $node): ?Node - { - array_pop($this->stack); - return null; - } - -} diff --git a/src/Visitor/TopLevelConstructorPropertyFetchMarkingVisitor.php b/src/Visitor/TopLevelConstructorPropertyFetchMarkingVisitor.php index 4c621af..3345e47 100644 --- a/src/Visitor/TopLevelConstructorPropertyFetchMarkingVisitor.php +++ b/src/Visitor/TopLevelConstructorPropertyFetchMarkingVisitor.php @@ -17,7 +17,7 @@ class TopLevelConstructorPropertyFetchMarkingVisitor extends NodeVisitorAbstract public const IS_TOP_LEVEL_CONSTRUCTOR_FETCH_ASSIGNMENT = ShipMonkNodeVisitor::NODE_ATTRIBUTE_PREFIX . 'topLevelConstructorFetchAssignment'; /** - * @var Node[] + * @var list */ private array $stack = []; @@ -38,9 +38,9 @@ public function enterNode(Node $node): ?Node if ( $nodesInStack >= 3 && $node instanceof PropertyFetch - && $this->stack[$nodesInStack - 1] instanceof Assign - && $this->stack[$nodesInStack - 2] instanceof Expression - && $this->stack[$nodesInStack - 3] instanceof ClassMethod + && $this->stack[$nodesInStack - 1] instanceof Assign // @phpstan-ignore offsetAccess.notFound + && $this->stack[$nodesInStack - 2] instanceof Expression // @phpstan-ignore offsetAccess.notFound + && $this->stack[$nodesInStack - 3] instanceof ClassMethod // @phpstan-ignore offsetAccess.notFound && $this->stack[$nodesInStack - 3]->name->name === '__construct' ) { $node->setAttribute(self::IS_TOP_LEVEL_CONSTRUCTOR_FETCH_ASSIGNMENT, true); diff --git a/src/Visitor/UnusedExceptionVisitor.php b/src/Visitor/UnusedExceptionVisitor.php index 089281c..2646ba7 100644 --- a/src/Visitor/UnusedExceptionVisitor.php +++ b/src/Visitor/UnusedExceptionVisitor.php @@ -19,7 +19,7 @@ use PhpParser\Node\Stmt\Throw_; use PhpParser\NodeVisitorAbstract; use function array_pop; -use function count; +use function end; class UnusedExceptionVisitor extends NodeVisitorAbstract { @@ -27,7 +27,7 @@ class UnusedExceptionVisitor extends NodeVisitorAbstract public const RESULT_USED = ShipMonkNodeVisitor::NODE_ATTRIBUTE_PREFIX . 'resultUsed'; /** - * @var Node[] + * @var list */ private array $stack = []; @@ -44,7 +44,7 @@ public function beforeTraverse(array $nodes): ?array public function enterNode(Node $node): ?Node { if ($this->stack !== []) { - $parent = $this->stack[count($this->stack) - 1]; + $parent = end($this->stack); if ($this->isNodeInInterest($node) && $this->isUsed($parent)) { $node->setAttribute(self::RESULT_USED, true); diff --git a/src/Visitor/UnusedMatchVisitor.php b/src/Visitor/UnusedMatchVisitor.php index cdea9bb..60d92a3 100644 --- a/src/Visitor/UnusedMatchVisitor.php +++ b/src/Visitor/UnusedMatchVisitor.php @@ -20,7 +20,7 @@ use PhpParser\Node\Stmt\Throw_; use PhpParser\NodeVisitorAbstract; use function array_pop; -use function count; +use function end; class UnusedMatchVisitor extends NodeVisitorAbstract { @@ -28,7 +28,7 @@ class UnusedMatchVisitor extends NodeVisitorAbstract public const MATCH_RESULT_USED = ShipMonkNodeVisitor::NODE_ATTRIBUTE_PREFIX . 'matchResultUsed'; /** - * @var Node[] + * @var list */ private array $stack = []; @@ -45,7 +45,7 @@ public function beforeTraverse(array $nodes): ?array public function enterNode(Node $node): ?Node { if ($this->stack !== []) { - $parent = $this->stack[count($this->stack) - 1]; + $parent = end($this->stack); if ($node instanceof Match_ && $this->isUsed($parent)) { $node->setAttribute(self::MATCH_RESULT_USED, true); diff --git a/tests/Extension/ImmediatelyCalledCallableThrowTypeExtensionTest.php b/tests/Extension/ImmediatelyCalledCallableThrowTypeExtensionTest.php deleted file mode 100644 index f7378d8..0000000 --- a/tests/Extension/ImmediatelyCalledCallableThrowTypeExtensionTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ - public static function dataFileAsserts(): iterable - { - yield from self::gatherAssertTypes(__DIR__ . '/data/ImmediatelyCalledCallableThrowTypeExtension/code.php'); - } - - /** - * @dataProvider dataFileAsserts - * @param mixed ...$args - */ - public function testFileAsserts( - string $assertType, - string $file, - ...$args - ): void - { - $this->assertFileAsserts($assertType, $file, ...$args); - } - - /** - * @return list - */ - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/data/ImmediatelyCalledCallableThrowTypeExtension/extension.neon', - ]; - } - -} diff --git a/tests/Extension/data/ImmediatelyCalledCallableThrowTypeExtension/code.php b/tests/Extension/data/ImmediatelyCalledCallableThrowTypeExtension/code.php deleted file mode 100644 index 5f15830..0000000 --- a/tests/Extension/data/ImmediatelyCalledCallableThrowTypeExtension/code.php +++ /dev/null @@ -1,247 +0,0 @@ - throw new \Exception()); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function testArrowFunctionWithoutThrow(): void - { - try { - $result = Immediate::method(fn () => 42); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - - public function testFirstClassCallable(): void - { - try { - $result = Immediate::method($this->throw(...)); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function testStaticFirstClassCallable(): void - { - try { - $result = Immediate::method(static::staticThrow(...)); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function testFirstClassCallableNoThrow(): void - { - try { - $result = Immediate::method($this->noThrow(...)); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - - public function testInheritedMethod(): void - { - try { - $result1 = (new Immediate())->inheritedMethod($this->noThrow(...), $this->noThrow(...)); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result1); - } - - try { - $result2 = (new Immediate())->inheritedMethod($this->throw(...), $this->noThrow(...)); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result2); - } - - try { - $result3 = (new Immediate())->inheritedMethod($this->noThrow(...), $this->throw(...)); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result3); - } - } - -} - - -class FunctionCallExtensionTest -{ - - public function noThrow(): void - { - } - - /** @throws \Exception */ - public function throw(): void - { - throw new \Exception(); - } - - /** @throws \Exception */ - public static function staticThrow(): void - { - throw new \Exception(); - } - - public function testNoThrow(): void - { - try { - $result = array_map('ucfirst', []); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - - public function testClosure(): void - { - try { - $result = array_map(static function (): void { - throw new \Exception(); - }, []); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function testClosureWithoutThrow(): void - { - try { - $result = array_map(static function (): void { - return; - }, []); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - - public function testArrowFunction(): void - { - try { - $result = array_map(fn () => throw new \Exception(), []); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function testArrowFunctionWithoutThrow(): void - { - try { - $result = array_map(fn () => 42, []); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - - public function testFirstClassCallable(): void - { - try { - $result = array_map($this->throw(...), []); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function testStaticFirstClassCallable(): void - { - try { - $result = array_map(static::staticThrow(...), []); - } finally { - assertVariableCertainty(TrinaryLogic::createMaybe(), $result); - } - } - - public function testFirstClassCallableNoThrow(): void - { - try { - $result = array_map($this->noThrow(...), []); - } finally { - assertVariableCertainty(TrinaryLogic::createYes(), $result); - } - } - -} diff --git a/tests/Extension/data/ImmediatelyCalledCallableThrowTypeExtension/extension.neon b/tests/Extension/data/ImmediatelyCalledCallableThrowTypeExtension/extension.neon deleted file mode 100644 index cc0fff4..0000000 --- a/tests/Extension/data/ImmediatelyCalledCallableThrowTypeExtension/extension.neon +++ /dev/null @@ -1,14 +0,0 @@ -services: - - - class: ShipMonk\PHPStan\Extension\ImmediatelyCalledCallableThrowTypeExtension - tags: - - phpstan.dynamicFunctionThrowTypeExtension - - phpstan.dynamicMethodThrowTypeExtension - - phpstan.dynamicStaticMethodThrowTypeExtension - arguments: - immediatelyCalledCallables: - array_map: 0 - ImmediatelyCalledCallableThrowTypeExtension\ImmediateInterface::inheritedMethod: 0 - ImmediatelyCalledCallableThrowTypeExtension\BaseImmediate::inheritedMethod: 1 - ImmediatelyCalledCallableThrowTypeExtension\Immediate::method: [0, 1] - diff --git a/tests/Rule/AllowNamedArgumentOnlyInAttributesRuleTest.php b/tests/Rule/AllowNamedArgumentOnlyInAttributesRuleTest.php deleted file mode 100644 index 84a14cd..0000000 --- a/tests/Rule/AllowNamedArgumentOnlyInAttributesRuleTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ -class AllowNamedArgumentOnlyInAttributesRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new AllowNamedArgumentOnlyInAttributesRule(); - } - - /** - * @return string[] - */ - public static function getAdditionalConfigFiles(): array - { - return array_merge( - parent::getAdditionalConfigFiles(), - [__DIR__ . '/data/AllowNamedArgumentOnlyInAttributesRule/named-argument-visitor.neon'], - ); - } - - public function testClass(): void - { - if (PHP_VERSION_ID < 80_000) { - self::markTestSkipped('Requires PHP 8.0'); - } - - $this->analyseFile(__DIR__ . '/data/AllowNamedArgumentOnlyInAttributesRule/code.php'); - } - -} diff --git a/tests/Rule/ClassSuffixNamingRuleTest.php b/tests/Rule/ClassSuffixNamingRuleTest.php index be6a7c7..5faaa79 100644 --- a/tests/Rule/ClassSuffixNamingRuleTest.php +++ b/tests/Rule/ClassSuffixNamingRuleTest.php @@ -2,6 +2,7 @@ namespace ShipMonk\PHPStan\Rule; +use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Rule; use ShipMonk\PHPStan\RuleTestCase; @@ -13,11 +14,13 @@ class ClassSuffixNamingRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ClassSuffixNamingRule([ // @phpstan-ignore-line ignore non existing class not being class-string - 'ClassSuffixNamingRule\CheckedParent' => 'Suffix', - 'ClassSuffixNamingRule\CheckedInterface' => 'Suffix2', - 'NotExistingClass' => 'Foo', - ]); + return new ClassSuffixNamingRule( + self::getContainer()->getByType(ReflectionProvider::class), + [ + 'ClassSuffixNamingRule\CheckedParent' => 'Suffix', + 'ClassSuffixNamingRule\CheckedInterface' => 'Suffix2', + ], + ); } public function testClass(): void diff --git a/tests/Rule/EnforceClosureParamNativeTypehintRuleTest.php b/tests/Rule/EnforceClosureParamNativeTypehintRuleTest.php index 4cf29bf..055f595 100644 --- a/tests/Rule/EnforceClosureParamNativeTypehintRuleTest.php +++ b/tests/Rule/EnforceClosureParamNativeTypehintRuleTest.php @@ -58,7 +58,7 @@ public function testNoErrorOnPhp74(): void private function createPhpVersion(int $version): PhpVersion { - return new PhpVersion($version); // @phpstan-ignore-line ignore bc promise + return new PhpVersion($version); // @phpstan-ignore phpstanApi.constructor } } diff --git a/tests/Rule/EnforceNativeReturnTypehintRuleTest.php b/tests/Rule/EnforceNativeReturnTypehintRuleTest.php index 2f5eb6b..14d00ae 100644 --- a/tests/Rule/EnforceNativeReturnTypehintRuleTest.php +++ b/tests/Rule/EnforceNativeReturnTypehintRuleTest.php @@ -63,7 +63,7 @@ public function testPhp74(): void private function createPhpVersion(int $version): PhpVersion { - return new PhpVersion($version); // @phpstan-ignore-line ignore bc promise + return new PhpVersion($version); // @phpstan-ignore phpstanApi.constructor } } diff --git a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php index ef4eda7..2a7d27b 100644 --- a/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php +++ b/tests/Rule/EnforceReadonlyPublicPropertyRuleTest.php @@ -34,7 +34,7 @@ public function testPhp80(): void private function createPhpVersion(int $version): PhpVersion { - return new PhpVersion($version); // @phpstan-ignore-line ignore bc promise + return new PhpVersion($version); // @phpstan-ignore phpstanApi.constructor } } diff --git a/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php b/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php index c7fbb75..458030e 100644 --- a/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php +++ b/tests/Rule/ForbidArithmeticOperationOnNonNumberRuleTest.php @@ -35,4 +35,9 @@ public function testNoNumericString(): void $this->analyseFile(__DIR__ . '/data/ForbidArithmeticOperationOnNonNumberRule/no-numeric-string.php'); } + protected function shouldFailOnPhpErrors(): bool + { + return false; // https://github.com/phpstan/phpstan-src/pull/3031 + } + } diff --git a/tests/Rule/ForbidAssignmentNotMatchingVarDocRuleTest.php b/tests/Rule/ForbidAssignmentNotMatchingVarDocRuleTest.php deleted file mode 100644 index ef0d851..0000000 --- a/tests/Rule/ForbidAssignmentNotMatchingVarDocRuleTest.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ -class ForbidAssignmentNotMatchingVarDocRuleTest extends RuleTestCase -{ - - private ?bool $allowNarrowing = null; - - protected function getRule(): Rule - { - self::assertNotNull($this->allowNarrowing); - - return new ForbidAssignmentNotMatchingVarDocRule( - self::getContainer()->getByType(FileTypeMapper::class), - $this->allowNarrowing, - ); - } - - public function testDefault(): void - { - $this->allowNarrowing = false; - $this->analyseFile(__DIR__ . '/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-disabled.php'); - } - - public function testNarrowing(): void - { - $this->allowNarrowing = true; - $this->analyseFile(__DIR__ . '/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-enabled.php'); - } - -} diff --git a/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php b/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php index 00fe662..2c995ec 100644 --- a/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php +++ b/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php @@ -3,7 +3,6 @@ namespace ShipMonk\PHPStan\Rule; use LogicException; -use Nette\Neon\Neon; use PHPStan\Analyser\NodeScopeResolver; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Rules\Exceptions\DefaultExceptionTypeResolver; @@ -33,20 +32,25 @@ protected function getRule(): Rule throw new LogicException('Missing implicitThrows'); } - $visitorConfig = Neon::decodeFile(self::getVisitorConfigFilePath()); - return new ForbidCheckedExceptionInCallableRule( self::getContainer()->getByType(NodeScopeResolver::class), self::getContainer()->getByType(ReflectionProvider::class), - new DefaultExceptionTypeResolver( // @phpstan-ignore-line ignore BC promise + new DefaultExceptionTypeResolver( // @phpstan-ignore phpstanApi.constructor self::getContainer()->getByType(ReflectionProvider::class), [], [], [], $this->checkedExceptions, // everything is checked when no config is provided ), - $visitorConfig['services'][0]['arguments']['immediatelyCalledCallables'], // @phpstan-ignore-line ignore mixed access - $visitorConfig['services'][0]['arguments']['allowedCheckedExceptionCallables'], // @phpstan-ignore-line ignore mixed access + [ + 'ForbidCheckedExceptionInCallableRule\CallableTest::allowThrowInInterface' => [0], + 'ForbidCheckedExceptionInCallableRule\BaseCallableTest::allowThrowInBaseClass' => [0], + 'ForbidCheckedExceptionInCallableRule\ClosureTest::allowThrow' => [0], + 'ForbidCheckedExceptionInCallableRule\FirstClassCallableTest::allowThrow' => [1], + 'ForbidCheckedExceptionInCallableRule\ArrowFunctionTest::allowThrow' => [0], + 'ForbidCheckedExceptionInCallableRule\ArrowFunctionTest::__construct' => [0], + 'allowed_function' => [0], + ], ); } @@ -78,9 +82,7 @@ public function provideSetup(): iterable */ public static function getAdditionalConfigFiles(): array { - $files = [ - self::getVisitorConfigFilePath(), - ]; + $files = []; if (self::$implicitThrows === true) { $files[] = __DIR__ . '/data/ForbidCheckedExceptionInCallableRule/no-implicit-throws.neon'; @@ -89,9 +91,4 @@ public static function getAdditionalConfigFiles(): array return $files; } - private static function getVisitorConfigFilePath(): string - { - return __DIR__ . '/data/ForbidCheckedExceptionInCallableRule/visitor.neon'; - } - } diff --git a/tests/Rule/ForbidNotNormalizedTypeRuleTest.php b/tests/Rule/ForbidNotNormalizedTypeRuleTest.php index a2fe9f7..561a5a8 100644 --- a/tests/Rule/ForbidNotNormalizedTypeRuleTest.php +++ b/tests/Rule/ForbidNotNormalizedTypeRuleTest.php @@ -3,16 +3,7 @@ namespace ShipMonk\PHPStan\Rule; use PhpParser\PrettyPrinter\Standard; -use PHPStan\Broker\AnonymousClassNameHelper; -use PHPStan\File\FileHelper; -use PHPStan\PhpDoc\PhpDocNodeResolver; -use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\TypeNodeResolver; -use PHPStan\PhpDocParser\Lexer\Lexer; -use PHPStan\PhpDocParser\Parser\ConstExprParser; -use PHPStan\PhpDocParser\Parser\PhpDocParser; -use PHPStan\PhpDocParser\Parser\TypeParser; -use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider; use PHPStan\Type\FileTypeMapper; use ShipMonk\PHPStan\RuleTestCase; @@ -25,23 +16,7 @@ class ForbidNotNormalizedTypeRuleTest extends RuleTestCase protected function getRule(): ForbidNotNormalizedTypeRule { return new ForbidNotNormalizedTypeRule( - new FileTypeMapper( // @phpstan-ignore-line - self::getContainer()->getByType(ReflectionProviderProvider::class), // @phpstan-ignore-line - self::getContainer()->getService('currentPhpVersionRichParser'), // @phpstan-ignore-line - new PhpDocStringResolver( // @phpstan-ignore-line - new Lexer(), - new PhpDocParser( - self::getContainer()->getByType(TypeParser::class), - self::getContainer()->getByType(ConstExprParser::class), - false, - false, - ['lines' => true], // simplify after https://github.com/phpstan/phpstan-src/pull/2807 - ), - ), - self::getContainer()->getByType(PhpDocNodeResolver::class), // @phpstan-ignore-line - self::getContainer()->getByType(AnonymousClassNameHelper::class), // @phpstan-ignore-line - self::getContainer()->getByType(FileHelper::class), - ), + self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getByType(TypeNodeResolver::class), self::getContainer()->getByType(Standard::class), true, diff --git a/tests/Rule/data/AllowNamedArgumentOnlyInAttributesRule/code.php b/tests/Rule/data/AllowNamedArgumentOnlyInAttributesRule/code.php deleted file mode 100644 index 3bdbc13..0000000 --- a/tests/Rule/data/AllowNamedArgumentOnlyInAttributesRule/code.php +++ /dev/null @@ -1,40 +0,0 @@ -myMethod(arg: 1); // error: Named arguments are allowed only within native attributes - var_dump(value: 1); // error: Named arguments are allowed only within native attributes - self::myStaticMethod(arg: 1); // error: Named arguments are allowed only within native attributes - } - - public static function myStaticMethod(int $arg): void - { - - } - -} diff --git a/tests/Rule/data/AllowNamedArgumentOnlyInAttributesRule/named-argument-visitor.neon b/tests/Rule/data/AllowNamedArgumentOnlyInAttributesRule/named-argument-visitor.neon deleted file mode 100644 index d0eb433..0000000 --- a/tests/Rule/data/AllowNamedArgumentOnlyInAttributesRule/named-argument-visitor.neon +++ /dev/null @@ -1,5 +0,0 @@ -services: - - - class: ShipMonk\PHPStan\Visitor\NamedArgumentSourceVisitor - tags: - - phpstan.parser.richParserNodeVisitor diff --git a/tests/Rule/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-disabled.php b/tests/Rule/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-disabled.php deleted file mode 100644 index 2c9e766..0000000 --- a/tests/Rule/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-disabled.php +++ /dev/null @@ -1,178 +0,0 @@ -returnArrayShape(); - - /** @var mixed[] $var */ - $var = $this->returnArrayShape(); - - /** @var mixed $var */ - $var = $this->returnArrayShape(); - - /** @var mixed[][] $var */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: int, value: string} to array - - /** @var array{id: int, value: string} $var */ - $var = $this->returnArrayShape(); - - /** @var array{id: int, value: string, notPresent: bool} $var */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: int, value: string} to array{id: int, value: string, notPresent: bool} - - /** @var array{id: int, value: string, notPresent: bool} $var check-shape-only */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: mixed, value: mixed} to array{id: int, value: string, notPresent: bool} - - /** @var array{id: string, value: string} $var */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: int, value: string} to array{id: string, value: string} - - /** @var array{id: string, value: string} $var check-shape-only */ - $var = $this->returnArrayShape(); - - /** @var iterable $var check-shape-only */ - $var = $this->returnIterableWithArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign iterable to iterable - - - /** @var self $var */ - $var = $this->returnSelf(); - - /** @var ExampleClass $var */ - $var = $this->returnSelf(); - - /** @var ExampleInterface $var */ - $var = $this->returnSelf(); - - /** @var ExampleClassParent $var */ - $var = $this->returnSelf(); - - /** @var AnotherClass $var */ - $var = $this->returnSelf(); // error: Invalid var phpdoc of $var. Cannot assign ForbidAssignmentNotMatchingVarDocRule\NoNarrow\ExampleClass to ForbidAssignmentNotMatchingVarDocRule\NoNarrow\AnotherClass - - /** @var ExampleInterface $var */ - $var = $this->returnInterface(); - - /** @var ExampleClass $var */ - $var = $this->returnInterface(); // error: Invalid var phpdoc of $var. Cannot narrow ForbidAssignmentNotMatchingVarDocRule\NoNarrow\ExampleInterface to ForbidAssignmentNotMatchingVarDocRule\NoNarrow\ExampleClass - - /** @var ExampleClass $var allow-narrowing */ - $var = $this->returnInterface(); - - - /** @var int $var */ - $var = $this->returnInt(); - - /** @var int|string $var */ - $var = $this->returnInt(); - - /** @var mixed $var */ - $var = $this->returnInt(); - - - /** @var string $var */ - $var = $this->returnString(); - - /** @var class-string $var */ - $var = $this->returnString(); // error: Invalid var phpdoc of $var. Cannot narrow string to class-string - - /** @var class-string $var allow-narrowing */ - $var = $this->returnString(); - - /** @var string $var */ - $var = $this->returnNullableString(); // error: Invalid var phpdoc of $var. Cannot narrow string|null to string - - /** @var string $var allow-narrowing */ - $var = $this->returnNullableString(); - - /** @var string|null|int $var */ - $var = $this->returnNullableString(); - - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); // error: Invalid var phpdoc of $var. Cannot assign array to array - } - - /** - * @return array - */ - public function returnArrayOfSelf(): array - { - return []; - } - - /** - * @return array{ id: int, value: string } - */ - public function returnArrayShape(): array - { - return ['id' => 1, 'value' => 'foo']; - } - - /** - * @return iterable - */ - public function returnIterableWithArrayShape(): iterable - { - return [['id' => 1, 'value' => 'foo']]; - } - - public function returnInt(): int - { - return 0; - } - - public function returnString(): string - { - return ''; - } - - public function returnNullableString(): ?string - { - return ''; - } - - public function returnSelf(): self - { - return $this; - } - - public function returnInterface(): ExampleInterface - { - return $this; - } - -} diff --git a/tests/Rule/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-enabled.php b/tests/Rule/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-enabled.php deleted file mode 100644 index ea8f883..0000000 --- a/tests/Rule/data/ForbidAssignmentNotMatchingVarDocRule/narrowing-enabled.php +++ /dev/null @@ -1,178 +0,0 @@ -returnArrayShape(); - - /** @var mixed[] $var */ - $var = $this->returnArrayShape(); - - /** @var mixed $var */ - $var = $this->returnArrayShape(); - - /** @var mixed[][] $var */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: int, value: string} to array - - /** @var array{id: int, value: string} $var */ - $var = $this->returnArrayShape(); - - /** @var array{id: int, value: string, notPresent: bool} $var */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: int, value: string} to array{id: int, value: string, notPresent: bool} - - /** @var array{id: int, value: string, notPresent: bool} $var check-shape-only */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: mixed, value: mixed} to array{id: int, value: string, notPresent: bool} - - /** @var array{id: string, value: string} $var */ - $var = $this->returnArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign array{id: int, value: string} to array{id: string, value: string} - - /** @var array{id: string, value: string} $var check-shape-only */ - $var = $this->returnArrayShape(); - - /** @var iterable $var check-shape-only */ - $var = $this->returnIterableWithArrayShape(); // error: Invalid var phpdoc of $var. Cannot assign iterable to iterable - - - /** @var self $var */ - $var = $this->returnSelf(); - - /** @var ExampleClass $var */ - $var = $this->returnSelf(); - - /** @var ExampleInterface $var */ - $var = $this->returnSelf(); - - /** @var ExampleClassParent $var */ - $var = $this->returnSelf(); - - /** @var AnotherClass $var */ - $var = $this->returnSelf(); // error: Invalid var phpdoc of $var. Cannot assign ForbidAssignmentNotMatchingVarDocRule\Narrow\ExampleClass to ForbidAssignmentNotMatchingVarDocRule\Narrow\AnotherClass - - /** @var ExampleInterface $var */ - $var = $this->returnInterface(); - - /** @var ExampleClass $var */ - $var = $this->returnInterface(); - - /** @var ExampleClass $var allow-narrowing */ - $var = $this->returnInterface(); - - - /** @var int $var */ - $var = $this->returnInt(); - - /** @var int|string $var */ - $var = $this->returnInt(); - - /** @var mixed $var */ - $var = $this->returnInt(); - - - /** @var string $var */ - $var = $this->returnString(); - - /** @var class-string $var */ - $var = $this->returnString(); - - /** @var class-string $var allow-narrowing */ - $var = $this->returnString(); - - /** @var string $var */ - $var = $this->returnNullableString(); - - /** @var string $var allow-narrowing */ - $var = $this->returnNullableString(); - - /** @var string|null|int $var */ - $var = $this->returnNullableString(); - - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); - - /** @var array $var */ - $var = $this->returnArrayOfSelf(); // error: Invalid var phpdoc of $var. Cannot assign array to array - } - - /** - * @return array - */ - public function returnArrayOfSelf(): array - { - return []; - } - - /** - * @return array{ id: int, value: string } - */ - public function returnArrayShape(): array - { - return ['id' => 1, 'value' => 'foo']; - } - - /** - * @return iterable - */ - public function returnIterableWithArrayShape(): iterable - { - return [['id' => 1, 'value' => 'foo']]; - } - - public function returnInt(): int - { - return 0; - } - - public function returnString(): string - { - return ''; - } - - public function returnNullableString(): ?string - { - return ''; - } - - public function returnSelf(): self - { - return $this; - } - - public function returnInterface(): ExampleInterface - { - return $this; - } - -} diff --git a/tests/Rule/data/ForbidCheckedExceptionInCallableRule/allowed.neon b/tests/Rule/data/ForbidCheckedExceptionInCallableRule/allowed.neon new file mode 100644 index 0000000..cd5e086 --- /dev/null +++ b/tests/Rule/data/ForbidCheckedExceptionInCallableRule/allowed.neon @@ -0,0 +1,7 @@ +parameters: + allowedCheckedExceptionCallables: + 'ForbidCheckedExceptionInCallableRule\CallableTest::allowThrowInInterface': [0] + 'ForbidCheckedExceptionInCallableRule\BaseCallableTest::allowThrowInBaseClass': [0] + 'ForbidCheckedExceptionInCallableRule\ClosureTest::allowThrow': [0] + 'ForbidCheckedExceptionInCallableRule\FirstClassCallableTest::allowThrow': [1] + 'ForbidCheckedExceptionInCallableRule\ArrowFunctionTest::allowThrow': [0] diff --git a/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php b/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php index e878af4..a9ccb48 100644 --- a/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php +++ b/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php @@ -9,6 +9,8 @@ class CheckedException extends \Exception {} */ function throwing_function() {} +function allowed_function(callable $callable) {} + interface CallableTest { public function allowThrowInInterface(callable $callable): void; @@ -101,6 +103,9 @@ private function denied(callable $callable): void } + /** + * @param-immediately-invoked-callable $callable + */ public function immediateThrow(?callable $denied, callable $callable): void { $callable(); @@ -175,6 +180,16 @@ function () { }, ); + $this->immediateThrow( + denied: function () {}, + ); + + $this->immediateThrow( + denied: function () { + throw new CheckedException(); // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in closure! + }, + ); + $this->allowThrowInBaseClass(function () { $this->throws(); }); @@ -209,6 +224,9 @@ private function denied(callable $callable): void } + /** + * @param-immediately-invoked-callable $callable + */ public function immediateThrow(callable $callable, ?callable $denied = null): void { $callable(); @@ -227,6 +245,11 @@ public function allowThrow(callable $callable): void class ArrowFunctionTest extends BaseCallableTest { + public function __construct($callable) + { + new self(fn () => throw new CheckedException()); + } + public function testDeclarations(): void { $fn = fn () => throw new CheckedException(); // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in arrow function! @@ -279,6 +302,9 @@ private function denied(callable $callable): void } + /** + * @param-immediately-invoked-callable $callable + */ public function immediateThrow(callable $callable): void { $callable(); @@ -294,3 +320,73 @@ public function allowThrow(callable $callable): void } } + +class ArgumentSwappingTest { + + public function test() + { + $this->call( + $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + $this->throws(...), + $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + ); + + $this->call( + second: $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + first: $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + third: $this->throws(...), + ); + + $this->call( + forth: $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + first: $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + ); + + $this->call( + $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + forth: $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + second: $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + third: $this->throws(...), + ); + + $this->call( + $this->noop(...), + $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + forth: $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + third: $this->noop(...), + ); + + // this is not yet supported, the rule do not see this as argument pass + $this->call(... [ + 'third' => $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + 'first' => $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + 'second' => $this->throws(...), // error: Throwing checked exception ForbidCheckedExceptionInCallableRule\CheckedException in first-class-callable! + ]); + } + + /** + * @param-immediately-invoked-callable $third + */ + public function call( + callable $first, + ?callable $second = null, + ?callable $third = null, + ?callable $forth = null + ): void + { + + } + + private function noop(): void + { + } + + /** + * @throws CheckedException + */ + private function throws(): void + { + throw new CheckedException(); + } +} diff --git a/tests/Rule/data/ForbidCheckedExceptionInCallableRule/visitor.neon b/tests/Rule/data/ForbidCheckedExceptionInCallableRule/visitor.neon deleted file mode 100644 index d2e27e2..0000000 --- a/tests/Rule/data/ForbidCheckedExceptionInCallableRule/visitor.neon +++ /dev/null @@ -1,17 +0,0 @@ -services: - - - class: ShipMonk\PHPStan\Visitor\ImmediatelyCalledCallableVisitor - arguments: - immediatelyCalledCallables: - 'array_map': 0 - 'ForbidCheckedExceptionInCallableRule\ClosureTest::immediateThrow': 0 - 'ForbidCheckedExceptionInCallableRule\FirstClassCallableTest::immediateThrow': 1 - 'ForbidCheckedExceptionInCallableRule\ArrowFunctionTest::immediateThrow': 0 - allowedCheckedExceptionCallables: - 'ForbidCheckedExceptionInCallableRule\CallableTest::allowThrowInInterface': [0] - 'ForbidCheckedExceptionInCallableRule\BaseCallableTest::allowThrowInBaseClass': [0] - 'ForbidCheckedExceptionInCallableRule\ClosureTest::allowThrow': [0] - 'ForbidCheckedExceptionInCallableRule\FirstClassCallableTest::allowThrow': [1] - 'ForbidCheckedExceptionInCallableRule\ArrowFunctionTest::allowThrow': [0] - tags: - - phpstan.parser.richParserNodeVisitor diff --git a/tests/RuleTestCase.php b/tests/RuleTestCase.php index fa989ca..b9e73d1 100644 --- a/tests/RuleTestCase.php +++ b/tests/RuleTestCase.php @@ -6,15 +6,18 @@ use PHPStan\Analyser\Error; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase as OriginalRuleTestCase; +use function array_values; use function explode; use function file_get_contents; use function file_put_contents; use function implode; +use function ksort; use function preg_match; use function preg_match_all; use function preg_replace; use function sprintf; use function trim; +use function uniqid; /** * @template TRule of Rule @@ -50,13 +53,17 @@ protected function processActualErrors(array $actualErrors): array $resultToAssert = []; foreach ($actualErrors as $error) { - $resultToAssert[] = $this->formatErrorForAssert($error->getMessage(), $error->getLine()); + $usedLine = $error->getLine() ?? -1; + $key = $usedLine . '-' . uniqid(); + $resultToAssert[$key] = $this->formatErrorForAssert($error->getMessage(), $usedLine); self::assertNotNull($error->getIdentifier(), "Missing error identifier for error: {$error->getMessage()}"); - self::assertStringStartsWith('shipmonk.', $error->getIdentifier()); + self::assertStringStartsWith('shipmonk.', $error->getIdentifier(), "Unexpected error identifier for: {$error->getMessage()}"); } - return $resultToAssert; + ksort($resultToAssert); + + return array_values($resultToAssert); } /** @@ -80,16 +87,20 @@ private function parseExpectedErrors(string $file): array } foreach ($matches[1] as $error) { - $expectedErrors[] = $this->formatErrorForAssert(trim($error), $line + 1); + $actualLine = $line + 1; + $key = $actualLine . '-' . uniqid(); + $expectedErrors[$key] = $this->formatErrorForAssert(trim($error), $actualLine); } } - return $expectedErrors; + ksort($expectedErrors); + + return array_values($expectedErrors); } - private function formatErrorForAssert(string $message, ?int $line): string + private function formatErrorForAssert(string $message, int $line): string { - return sprintf('%02d: %s', $line ?? -1, $message); + return sprintf('%02d: %s', $line, $message); } /**