diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 872d536314..208df4952f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -233,6 +233,16 @@ jobs: cd e2e/bug-11857 composer install ../../bin/phpstan + - script: | + cd e2e/result-cache-meta-extension + composer install + ../../bin/phpstan -vvv + ../../bin/phpstan -vvv --fail-without-result-cache + echo 'modified-hash' > hash.txt + OUTPUT=$(../bashunit -a exit_code "2" "../../bin/phpstan -vvv --fail-without-result-cache") + echo "$OUTPUT" + ../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT" + ../bashunit -a contains 'Result cache not used because the metadata do not match: metaExtensions' "$OUTPUT" steps: - name: "Checkout" diff --git a/composer.json b/composer.json index 6e2c1cb8c4..6edf082303 100644 --- a/composer.json +++ b/composer.json @@ -15,16 +15,16 @@ "hoa/compiler": "3.17.08.08", "hoa/exception": "^1.0", "hoa/file": "1.17.07.11", - "jetbrains/phpstorm-stubs": "dev-master#db675e059f57071e8209c99075128b92d8a727e7", + "jetbrains/phpstorm-stubs": "dev-master#62a683f61d9ea11ef8caf8b2ad54e59e92b2c670", "nette/bootstrap": "^3.0", "nette/di": "^3.1.4", "nette/neon": "3.3.4", "nette/php-generator": "3.6.9", "nette/schema": "^1.2.2", "nette/utils": "^3.2.5", - "nikic/php-parser": "^5.3.0", + "nikic/php-parser": "^5.4.0", "ondram/ci-detector": "^3.4.0", - "ondrejmirtes/better-reflection": "6.49.0.0", + "ondrejmirtes/better-reflection": "6.51.0.1", "phpstan/php-8-stubs": "0.4.9", "phpstan/phpdoc-parser": "2.0.0", "psr/http-message": "^1.1", diff --git a/composer.lock b/composer.lock index 880e113451..f9f801df4c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f3a19a9abe4cf8cfbe9a6a76cf161369", + "content-hash": "bf40a89cec9c4598324b1e8394b7367c", "packages": [ { "name": "clue/ndjson-react", @@ -1442,12 +1442,12 @@ "source": { "type": "git", "url": "https://github.com/JetBrains/phpstorm-stubs.git", - "reference": "db675e059f57071e8209c99075128b92d8a727e7" + "reference": "62a683f61d9ea11ef8caf8b2ad54e59e92b2c670" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/db675e059f57071e8209c99075128b92d8a727e7", - "reference": "db675e059f57071e8209c99075128b92d8a727e7", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/62a683f61d9ea11ef8caf8b2ad54e59e92b2c670", + "reference": "62a683f61d9ea11ef8caf8b2ad54e59e92b2c670", "shasum": "" }, "require-dev": { @@ -1482,7 +1482,7 @@ "support": { "source": "https://github.com/JetBrains/phpstorm-stubs/tree/master" }, - "time": "2024-12-23T11:36:45+00:00" + "time": "2025-01-04T20:30:22+00:00" }, { "name": "nette/bootstrap", @@ -2057,16 +2057,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -2109,9 +2109,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "ondram/ci-detector", @@ -2187,22 +2187,22 @@ }, { "name": "ondrejmirtes/better-reflection", - "version": "6.49.0.0", + "version": "6.51.0.1", "source": { "type": "git", "url": "https://github.com/ondrejmirtes/BetterReflection.git", - "reference": "11abb6b4c9c8b29ee2730a3307ebae77b17fa94d" + "reference": "739c4cc0a01ef79055688606be07cff93551815d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/11abb6b4c9c8b29ee2730a3307ebae77b17fa94d", - "reference": "11abb6b4c9c8b29ee2730a3307ebae77b17fa94d", + "url": "https://api.github.com/repos/ondrejmirtes/BetterReflection/zipball/739c4cc0a01ef79055688606be07cff93551815d", + "reference": "739c4cc0a01ef79055688606be07cff93551815d", "shasum": "" }, "require": { "ext-json": "*", - "jetbrains/phpstorm-stubs": "dev-master#b61d4a5f40c3940be440d85355fef4e2416b8527", - "nikic/php-parser": "^5.3.1", + "jetbrains/phpstorm-stubs": "dev-master#dfcad4524db603bd20bdec3aab1a31c5f5128ea3", + "nikic/php-parser": "^5.4.0", "php": "^7.4 || ^8.0" }, "conflict": { @@ -2212,7 +2212,7 @@ "doctrine/coding-standard": "^12.0.0", "phpstan/phpstan": "^1.10.60", "phpstan/phpstan-phpunit": "^1.3.16", - "phpunit/phpunit": "^11.5.1", + "phpunit/phpunit": "^11.5.2", "rector/rector": "1.2.10" }, "suggest": { @@ -2252,9 +2252,9 @@ ], "description": "Better Reflection - an improved code reflection API", "support": { - "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.49.0.0" + "source": "https://github.com/ondrejmirtes/BetterReflection/tree/6.51.0.1" }, - "time": "2024-12-20T19:27:15+00:00" + "time": "2025-01-04T12:23:15+00:00" }, { "name": "phpstan/php-8-stubs", diff --git a/conf/config.level0.neon b/conf/config.level0.neon index dbb2b4836c..9160281651 100644 --- a/conf/config.level0.neon +++ b/conf/config.level0.neon @@ -1,10 +1,6 @@ parameters: customRulesetUsed: false -conditionalTags: - PHPStan\Rules\Properties\UninitializedPropertyRule: - phpstan.rules.rule: %checkUninitializedProperties% - rules: - PHPStan\Rules\Api\ApiInstanceofRule - PHPStan\Rules\Api\ApiInstanceofTypeRule @@ -218,9 +214,6 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Rules\Properties\UninitializedPropertyRule - - class: PHPStan\Rules\Properties\WritingToReadOnlyPropertiesRule arguments: @@ -250,11 +243,6 @@ services: tags: - phpstan.rules.rule - - - class: PHPStan\Reflection\ConstructorsHelper - arguments: - additionalConstructors: %additionalConstructors% - - class: PHPStan\Rules\Keywords\RequireFileExistsRule arguments: diff --git a/conf/config.level3.neon b/conf/config.level3.neon index c946a5ee3f..4e5f80c5ef 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -22,7 +22,6 @@ rules: - PHPStan\Rules\Properties\ReadOnlyPropertyAssignRefRule - PHPStan\Rules\Properties\ReadOnlyByPhpDocPropertyAssignRefRule - PHPStan\Rules\Properties\SetNonVirtualPropertyHookAssignRule - - PHPStan\Rules\Properties\ShortGetPropertyHookReturnTypeRule - PHPStan\Rules\Properties\TypesAssignedToPropertiesRule - PHPStan\Rules\Variables\ParameterOutAssignedTypeRule - PHPStan\Rules\Variables\ParameterOutExecutionEndTypeRule diff --git a/conf/config.neon b/conf/config.neon index b2d222ebb3..9d62f32c60 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -211,6 +211,8 @@ conditionalTags: phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% PHPStan\Rules\Exceptions\MissingCheckedExceptionInPropertyHookThrowsRule: phpstan.rules.rule: %exceptions.check.missingCheckedExceptionInThrows% + PHPStan\Rules\Properties\UninitializedPropertyRule: + phpstan.rules.rule: %checkUninitializedProperties% services: - @@ -320,11 +322,6 @@ services: tags: - phpstan.parser.richParserNodeVisitor - - - class: PHPStan\Parser\PropertyHookNameVisitor - tags: - - phpstan.parser.richParserNodeVisitor - - class: PHPStan\Node\Printer\ExprPrinter @@ -739,6 +736,11 @@ services: tags: - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: PHPStan\Reflection\ConstructorsHelper + arguments: + additionalConstructors: %additionalConstructors% + - class: PHPStan\Reflection\RequireExtension\RequireExtendsMethodsClassReflectionExtension @@ -991,6 +993,9 @@ services: - class: PHPStan\Rules\Methods\MethodParameterComparisonHelper + - + class: PHPStan\Rules\Methods\MethodVisibilityComparisonHelper + - class: PHPStan\Rules\MissingTypehintCheck arguments: @@ -1039,6 +1044,9 @@ services: reportMagicProperties: %reportMagicProperties% checkDynamicProperties: %checkDynamicProperties% + - + class: PHPStan\Rules\Properties\UninitializedPropertyRule + - class: PHPStan\Rules\Properties\LazyReadWritePropertiesExtensionProvider diff --git a/e2e/result-cache-meta-extension/.gitignore b/e2e/result-cache-meta-extension/.gitignore new file mode 100644 index 0000000000..61ead86667 --- /dev/null +++ b/e2e/result-cache-meta-extension/.gitignore @@ -0,0 +1 @@ +/vendor diff --git a/e2e/result-cache-meta-extension/composer.json b/e2e/result-cache-meta-extension/composer.json new file mode 100644 index 0000000000..a072011fe8 --- /dev/null +++ b/e2e/result-cache-meta-extension/composer.json @@ -0,0 +1,5 @@ +{ + "autoload-dev": { + "classmap": ["src/"] + } +} diff --git a/e2e/result-cache-meta-extension/composer.lock b/e2e/result-cache-meta-extension/composer.lock new file mode 100644 index 0000000000..b383d88ac5 --- /dev/null +++ b/e2e/result-cache-meta-extension/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d751713988987e9331980363e24189ce", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/e2e/result-cache-meta-extension/hash.txt b/e2e/result-cache-meta-extension/hash.txt new file mode 100644 index 0000000000..1f34c8dfd2 --- /dev/null +++ b/e2e/result-cache-meta-extension/hash.txt @@ -0,0 +1 @@ +initial-hash diff --git a/e2e/result-cache-meta-extension/phpstan.neon b/e2e/result-cache-meta-extension/phpstan.neon new file mode 100644 index 0000000000..f2f9c41148 --- /dev/null +++ b/e2e/result-cache-meta-extension/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + level: 8 + paths: + - src + +services: + - + class: ResultCacheE2E\MetaExtension\DummyResultCacheMetaExtension + tags: + - phpstan.resultCacheMetaExtension diff --git a/e2e/result-cache-meta-extension/src/DummyResultCacheMetaExtension.php b/e2e/result-cache-meta-extension/src/DummyResultCacheMetaExtension.php new file mode 100644 index 0000000000..81b6332f96 --- /dev/null +++ b/e2e/result-cache-meta-extension/src/DummyResultCacheMetaExtension.php @@ -0,0 +1,21 @@ + $expressionTypes - * @param array $nativeExpressionTypes - * @param array $conditionalExpressions - * @param list $inFunctionCallsStack - * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions - */ public function create( ScopeContext $context, bool $declareStrictTypes = false, diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index 6d8608ec18..8d8daa714f 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -18,7 +18,7 @@ interface InternalScopeFactory * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index ac5b757991..657cb8c865 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -7,10 +7,7 @@ use PHPStan\DependencyInjection\Type\ExpressionTypeResolverExtensionRegistryProvider; use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Php\PhpVersion; -use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\InitializerExprTypeResolver; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection; use PHPStan\Reflection\ReflectionProvider; @@ -25,14 +22,6 @@ public function __construct( { } - /** - * @param array $expressionTypes - * @param array $nativeExpressionTypes - * @param array $conditionalExpressions - * @param array $currentlyAssignedExpressions - * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack - */ public function create( ScopeContext $context, bool $declareStrictTypes = false, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 4a0169048a..297697ee78 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2953,6 +2953,7 @@ public function enterClassMethod( array $parameterOutTypes = [], array $immediatelyInvokedCallableParameters = [], array $phpDocClosureThisTypeParameters = [], + bool $isConstructor = false, ): self { if (!$this->isInClass()) { @@ -2984,6 +2985,7 @@ public function enterClassMethod( array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes), $immediatelyInvokedCallableParameters, array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters), + $isConstructor, ), !$classMethod->isStatic(), ); @@ -3068,6 +3070,7 @@ public function enterPropertyHook( [], [], [], + false, ), true, ); @@ -5631,13 +5634,77 @@ private function exactInstantiation(New_ $node, string $className): ?Type } } - if ($constructorMethod instanceof DummyConstructorReflection || $constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if ($constructorMethod instanceof DummyConstructorReflection) { return new GenericObjectType( $resolvedClassName, $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), ); } + if ($constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) { + if (!$constructorMethod->getDeclaringClass()->isGeneric()) { + return new GenericObjectType( + $resolvedClassName, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + ); + } + $newType = new GenericObjectType($resolvedClassName, $classReflection->typeMapToList($classReflection->getTemplateTypeMap())); + $ancestorType = $newType->getAncestorWithClassName($constructorMethod->getDeclaringClass()->getName()); + if ($ancestorType === null) { + return new GenericObjectType( + $resolvedClassName, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + ); + } + $ancestorClassReflections = $ancestorType->getObjectClassReflections(); + if (count($ancestorClassReflections) !== 1) { + return new GenericObjectType( + $resolvedClassName, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + ); + } + + $newParentNode = new New_(new Name($constructorMethod->getDeclaringClass()->getName()), $node->args); + $newParentType = $this->getType($newParentNode); + $newParentTypeClassReflections = $newParentType->getObjectClassReflections(); + if (count($newParentTypeClassReflections) !== 1) { + return new GenericObjectType( + $resolvedClassName, + $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()), + ); + } + $newParentTypeClassReflection = $newParentTypeClassReflections[0]; + + $ancestorClassReflection = $ancestorClassReflections[0]; + $ancestorMapping = []; + foreach ($ancestorClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $templateType) { + if (!$templateType instanceof TemplateType) { + continue; + } + + $ancestorMapping[$typeName] = $templateType->getName(); + } + + $resolvedTypeMap = []; + foreach ($newParentTypeClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $type) { + if (!array_key_exists($typeName, $ancestorMapping)) { + continue; + } + + if (!array_key_exists($ancestorMapping[$typeName], $resolvedTypeMap)) { + $resolvedTypeMap[$ancestorMapping[$typeName]] = $type; + continue; + } + + $resolvedTypeMap[$ancestorMapping[$typeName]] = TypeCombinator::union($resolvedTypeMap[$ancestorMapping[$typeName]], $type); + } + + return new GenericObjectType( + $resolvedClassName, + $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)), + ); + } + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( $this, $methodCall->getArgs(), diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 791a8920b7..58b6f34df5 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -124,8 +124,8 @@ use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; +use PHPStan\Parser\LineAttributesVisitor; use PHPStan\Parser\Parser; -use PHPStan\Parser\PropertyHookNameVisitor; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -316,13 +316,38 @@ public function processNodes( } $alreadyTerminated = true; - $nextStmt = $this->getFirstUnreachableNode(array_slice($nodes, $i + 1), true); - if (!$nextStmt instanceof Node\Stmt) { + $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true); + $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); + } + } + + /** + * @param Node\Stmt[] $nextStmts + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, callable $nodeCallback): void + { + if ($nextStmts === []) { + return; + } + + $unreachableStatement = null; + $nextStatements = []; + + foreach ($nextStmts as $key => $nextStmt) { + if ($key === 0) { + $unreachableStatement = $nextStmt; continue; } - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + $nextStatements[] = $nextStmt; + } + + if (!$unreachableStatement instanceof Node\Stmt) { + return; } + + $nodeCallback(new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope); } /** @@ -409,11 +434,8 @@ public function processStmtNodes( } $alreadyTerminated = true; - $nextStmt = $this->getFirstUnreachableNode(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); - if ($nextStmt === null) { - continue; - } - $nodeCallback(new UnreachableStatementNode($nextStmt), $scope); + $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); + $this->processUnreachableStatement($nextStmts, $scope, $nodeCallback); } $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints); @@ -626,6 +648,9 @@ private function processStmtNode( [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt); } + $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; + $isConstructor = $isFromTrait || $stmt->name->toLowerString() === '__construct'; + $methodScope = $scope->enterClassMethod( $stmt, $templateTypeMap, @@ -644,6 +669,7 @@ private function processStmtNode( $phpDocParameterOutTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, + $isConstructor, ); if (!$scope->isInClass()) { @@ -652,8 +678,7 @@ private function processStmtNode( $classReflection = $scope->getClassReflection(); - $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct'; - if ($isFromTrait || $stmt->name->toLowerString() === '__construct') { + if ($isConstructor) { foreach ($stmt->params as $param) { if ($param->flags === 0 && $param->hooks === []) { continue; @@ -4808,54 +4833,61 @@ private function processPropertyHooks( $hook, ), $hookScope); + $stmts = $hook->getStmts(); + if ($stmts === null) { + return; + } + if ($hook->body instanceof Expr) { - $this->processExprNode($stmt, $hook->body, $hookScope, $nodeCallback, ExpressionContext::createTopLevel()); - $nodeCallback(new PropertyAssignNode(new PropertyFetch(new Variable('this'), $propertyName, $hook->body->getAttributes()), $hook->body, false), $hookScope); - } elseif (is_array($hook->body)) { - $gatheredReturnStatements = []; - $executionEnds = []; - $methodImpurePoints = []; - $statementResult = $this->processStmtNodes(new PropertyHookStatementNode($hook), $hook->body, $hookScope, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { - $nodeCallback($node, $scope); - if ($scope->getFunction() !== $hookScope->getFunction()) { - return; - } - if ($scope->isInAnonymousFunction()) { - return; - } - if ($node instanceof PropertyAssignNode) { - $hookImpurePoints[] = new ImpurePoint( - $scope, - $node, - 'propertyAssign', - 'property assignment', - true, - ); - return; - } - if ($node instanceof ExecutionEndNode) { - $executionEnds[] = $node; - return; - } - if (!$node instanceof Return_) { - return; - } + // enrich attributes of nodes in short hook body statements + $traverser = new NodeTraverser( + new LineAttributesVisitor($hook->body->getStartLine(), $hook->body->getEndLine()), + ); + $traverser->traverse($stmts); + } - $gatheredReturnStatements[] = new ReturnStatement($scope, $node); - }, StatementContext::createTopLevel()); + $gatheredReturnStatements = []; + $executionEnds = []; + $methodImpurePoints = []; + $statementResult = $this->processStmtNodes(new PropertyHookStatementNode($hook), $stmts, $hookScope, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void { + $nodeCallback($node, $scope); + if ($scope->getFunction() !== $hookScope->getFunction()) { + return; + } + if ($scope->isInAnonymousFunction()) { + return; + } + if ($node instanceof PropertyAssignNode) { + $hookImpurePoints[] = new ImpurePoint( + $scope, + $node, + 'propertyAssign', + 'property assignment', + true, + ); + return; + } + if ($node instanceof ExecutionEndNode) { + $executionEnds[] = $node; + return; + } + if (!$node instanceof Return_) { + return; + } - $nodeCallback(new PropertyHookReturnStatementsNode( - $hook, - $gatheredReturnStatements, - $statementResult, - $executionEnds, - array_merge($statementResult->getImpurePoints(), $methodImpurePoints), - $classReflection, - $hookReflection, - $propertyReflection, - ), $hookScope); - } + $gatheredReturnStatements[] = new ReturnStatement($scope, $node); + }, StatementContext::createTopLevel()); + $nodeCallback(new PropertyHookReturnStatementsNode( + $hook, + $gatheredReturnStatements, + $statementResult, + $executionEnds, + array_merge($statementResult->getImpurePoints(), $methodImpurePoints), + $classReflection, + $hookReflection, + $propertyReflection, + ), $hookScope); } } @@ -5742,7 +5774,10 @@ private function produceArrayDimFetchAssignValueToWrite(array $offsetTypes, Type foreach (array_reverse($offsetTypes) as $i => $offsetType) { /** @var Type $offsetValueType */ $offsetValueType = array_pop($offsetValueTypeStack); - if (!$offsetValueType instanceof MixedType) { + if ( + !$offsetValueType instanceof MixedType + && !$offsetValueType->isConstantArray()->yes() + ) { $types = [ new ArrayType(new MixedType(), new MixedType()), new ObjectType(ArrayAccess::class), @@ -6406,7 +6441,7 @@ public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $n } elseif ($node instanceof Node\Stmt\Function_) { $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\'); } elseif ($node instanceof Node\PropertyHook) { - $propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $node->getAttribute('propertyName'); if ($propertyName !== null) { $functionName = sprintf('$%s::%s', $propertyName, $node->name->toString()); } @@ -6514,22 +6549,31 @@ private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $ } /** - * @template T of Node - * @param array $nodes - * @return T|null + * @param array $nodes + * @return list */ - private function getFirstUnreachableNode(array $nodes, bool $earlyBinding): ?Node + private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array { + $stmts = []; + $isPassedUnreachableStatement = false; foreach ($nodes as $node) { + if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { + continue; + } + if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { + $stmts[] = $node; + continue; + } if ($node instanceof Node\Stmt\Nop) { continue; } - if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { + if (!$node instanceof Node\Stmt) { continue; } - return $node; + $stmts[] = $node; + $isPassedUnreachableStatement = true; } - return null; + return $stmts; } } diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 567b61f798..7e997503a3 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -10,6 +10,7 @@ use PHPStan\Command\Output; use PHPStan\Dependency\ExportedNodeFetcher; use PHPStan\Dependency\RootExportedNode; +use PHPStan\DependencyInjection\Container; use PHPStan\DependencyInjection\ProjectConfigHelper; use PHPStan\File\CouldNotReadFileException; use PHPStan\File\FileFinder; @@ -66,6 +67,7 @@ final class ResultCacheManager * @param string[] $scanDirectories */ public function __construct( + private Container $container, private ExportedNodeFetcher $exportedNodeFetcher, private FileFinder $scanFileFinder, private ReflectionProvider $reflectionProvider, @@ -904,6 +906,7 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a return [ 'cacheVersion' => self::CACHE_VERSION, 'phpstanVersion' => ComposerHelper::getPhpStanVersion(), + 'metaExtensions' => $this->getMetaFromPhpStanExtensions(), 'phpVersion' => PHP_VERSION_ID, 'projectConfig' => $projectConfigArray, 'analysedPaths' => $this->analysedPaths, @@ -1036,4 +1039,29 @@ private function getStubFiles(): array return $stubFiles; } + /** + * @return array + * @throws ShouldNotHappenException + */ + private function getMetaFromPhpStanExtensions(): array + { + $meta = []; + + /** @var ResultCacheMetaExtension $extension */ + foreach ($this->container->getServicesByTag(ResultCacheMetaExtension::EXTENSION_TAG) as $extension) { + if (array_key_exists($extension->getKey(), $meta)) { + throw new ShouldNotHappenException(sprintf( + 'Duplicate ResultCacheMetaExtension with key "%s" found.', + $extension->getKey(), + )); + } + + $meta[$extension->getKey()] = $extension->getHash(); + } + + ksort($meta); + + return $meta; + } + } diff --git a/src/Analyser/ResultCache/ResultCacheMetaExtension.php b/src/Analyser/ResultCache/ResultCacheMetaExtension.php new file mode 100644 index 0000000000..11aac2512f --- /dev/null +++ b/src/Analyser/ResultCache/ResultCacheMetaExtension.php @@ -0,0 +1,39 @@ + $bool, LazyParameterOutTypeExtensionProvider::STATIC_METHOD_TAG => $bool, DiagnoseExtension::EXTENSION_TAG => $bool, + ResultCacheMetaExtension::EXTENSION_TAG => $bool, ])->min(1)); } diff --git a/src/Node/UnreachableStatementNode.php b/src/Node/UnreachableStatementNode.php index e0c8cb0af9..3e2c72e29b 100644 --- a/src/Node/UnreachableStatementNode.php +++ b/src/Node/UnreachableStatementNode.php @@ -10,7 +10,8 @@ final class UnreachableStatementNode extends Stmt implements VirtualNode { - public function __construct(private Stmt $originalStatement) + /** @param Stmt[] $nextStatements */ + public function __construct(private Stmt $originalStatement, private array $nextStatements = []) { parent::__construct($originalStatement->getAttributes()); } @@ -33,4 +34,12 @@ public function getSubNodeNames(): array return []; } + /** + * @return Stmt[] + */ + public function getNextStatements(): array + { + return $this->nextStatements; + } + } diff --git a/src/Parser/CleaningVisitor.php b/src/Parser/CleaningVisitor.php index eb9492f3cd..80f5b2f594 100644 --- a/src/Parser/CleaningVisitor.php +++ b/src/Parser/CleaningVisitor.php @@ -37,7 +37,7 @@ public function enterNode(Node $node): ?Node } if ($node instanceof Node\PropertyHook && is_array($node->body)) { - $propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $node->getAttribute('propertyName'); if ($propertyName !== null) { $node->body = $this->keepVariadicsAndYields($node->body, $propertyName); return $node; diff --git a/src/Parser/LineAttributesVisitor.php b/src/Parser/LineAttributesVisitor.php new file mode 100644 index 0000000000..53f3f3f50f --- /dev/null +++ b/src/Parser/LineAttributesVisitor.php @@ -0,0 +1,28 @@ +getStartLine() === -1) { + $node->setAttribute('startLine', $this->startLine); + } + + if ($node->getEndLine() === -1) { + $node->setAttribute('endLine', $this->endLine); + } + + return $node; + } + +} diff --git a/src/Parser/PropertyHookNameVisitor.php b/src/Parser/PropertyHookNameVisitor.php deleted file mode 100644 index 5a49a70915..0000000000 --- a/src/Parser/PropertyHookNameVisitor.php +++ /dev/null @@ -1,60 +0,0 @@ -hooks) === 0) { - return null; - } - - $propertyName = null; - foreach ($node->props as $prop) { - $propertyName = $prop->name->toString(); - break; - } - - if (!isset($propertyName)) { - return null; - } - - foreach ($node->hooks as $hook) { - $hook->setAttribute(self::ATTRIBUTE_NAME, $propertyName); - } - - return $node; - } - - if ($node instanceof Node\Param) { - if (count($node->hooks) === 0) { - return null; - } - if (!$node->var instanceof Node\Expr\Variable) { - return null; - } - if (!is_string($node->var->name)) { - return null; - } - - foreach ($node->hooks as $hook) { - $hook->setAttribute(self::ATTRIBUTE_NAME, $node->var->name); - } - - return $node; - } - - return null; - } - -} diff --git a/src/Parser/SimpleParser.php b/src/Parser/SimpleParser.php index 71bab19964..8fbd112742 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -17,7 +17,6 @@ public function __construct( private NameResolver $nameResolver, private VariadicMethodsVisitor $variadicMethodsVisitor, private VariadicFunctionsVisitor $variadicFunctionsVisitor, - private PropertyHookNameVisitor $propertyHookNameVisitor, ) { } @@ -53,7 +52,6 @@ public function parseString(string $sourceCode): array $nodeTraverser->addVisitor($this->nameResolver); $nodeTraverser->addVisitor($this->variadicMethodsVisitor); $nodeTraverser->addVisitor($this->variadicFunctionsVisitor); - $nodeTraverser->addVisitor($this->propertyHookNameVisitor); /** @var array */ return $nodeTraverser->traverse($nodes); diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 229dccb72d..96bf233209 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -38,4 +38,9 @@ public function supportsNamedArguments(): TrinaryLogic return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result; } + public function supportsNamedArgumentAfterUnpackedArgument(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80100, null)->isSuperTypeOf($this->phpVersions)->result; + } + } diff --git a/src/PhpDoc/StubValidator.php b/src/PhpDoc/StubValidator.php index 33fde1e46e..0374aa268f 100644 --- a/src/PhpDoc/StubValidator.php +++ b/src/PhpDoc/StubValidator.php @@ -68,6 +68,7 @@ use PHPStan\Rules\Methods\ExistingClassesInTypehintsRule; use PHPStan\Rules\Methods\MethodParameterComparisonHelper; use PHPStan\Rules\Methods\MethodSignatureRule; +use PHPStan\Rules\Methods\MethodVisibilityComparisonHelper; use PHPStan\Rules\Methods\MissingMethodParameterTypehintRule; use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule; use PHPStan\Rules\Methods\MissingMethodSelfOutTypeRule; @@ -209,7 +210,15 @@ private function getRuleRegistry(Container $container): RuleRegistry new ExistingClassesInTypehintsRule($functionDefinitionCheck), new \PHPStan\Rules\Functions\ExistingClassesInTypehintsRule($functionDefinitionCheck), new ExistingClassesInPropertiesRule($reflectionProvider, $classNameCheck, $unresolvableTypeHelper, $phpVersion, true, false), - new OverridingMethodRule($phpVersion, new MethodSignatureRule($phpClassReflectionExtension, true, true), true, new MethodParameterComparisonHelper($phpVersion), $phpClassReflectionExtension, $container->getParameter('checkMissingOverrideMethodAttribute')), + new OverridingMethodRule( + $phpVersion, + new MethodSignatureRule($phpClassReflectionExtension, true, true), + true, + new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), + $phpClassReflectionExtension, + $container->getParameter('checkMissingOverrideMethodAttribute'), + ), new DuplicateDeclarationRule(), new LocalTypeAliasesRule($localTypeAliasesCheck), new LocalTypeTraitAliasesRule($localTypeAliasesCheck, $reflectionProvider), diff --git a/src/Reflection/InitializerExprContext.php b/src/Reflection/InitializerExprContext.php index eb64cacdbb..650408f349 100644 --- a/src/Reflection/InitializerExprContext.php +++ b/src/Reflection/InitializerExprContext.php @@ -9,7 +9,6 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionFunction; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\ReflectionConstant; -use PHPStan\Parser\PropertyHookNameVisitor; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; use PHPStan\ShouldNotHappenException; use function array_slice; @@ -144,7 +143,7 @@ public static function fromStubParameter( } elseif ($function instanceof ClassMethod) { $functionName = $function->name->toString(); } elseif ($function instanceof PropertyHook) { - $propertyName = $function->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $function->getAttribute('propertyName'); $functionName = sprintf('$%s::%s', $propertyName, $function->name->toString()); } @@ -152,7 +151,7 @@ public static function fromStubParameter( if ($function instanceof ClassMethod && $className !== null) { $methodName = sprintf('%s::%s', $className, $function->name->toString()); } elseif ($function instanceof PropertyHook) { - $propertyName = $function->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $function->getAttribute('propertyName'); $methodName = sprintf('%s::$%s::%s', $className, $propertyName, $function->name->toString()); } elseif ($function instanceof Function_ && $function->namespacedName !== null) { $methodName = $function->namespacedName->toString(); diff --git a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php index af7d9a80f4..66dfbd59ed 100644 --- a/src/Reflection/Php/PhpMethodFromParserNodeReflection.php +++ b/src/Reflection/Php/PhpMethodFromParserNodeReflection.php @@ -2,7 +2,6 @@ namespace PHPStan\Reflection\Php; -use PhpParser\Modifiers; use PhpParser\Node; use PhpParser\Node\Stmt\ClassMethod; use PHPStan\Reflection\Assertions; @@ -63,6 +62,7 @@ public function __construct( array $parameterOutTypes, array $immediatelyInvokedCallableParameters, array $phpDocClosureThisTypeParameters, + private bool $isConstructor, ) { if ($this->classMethod instanceof Node\PropertyHook) { @@ -74,7 +74,10 @@ public function __construct( } $name = strtolower($classMethod->name->name); - if (in_array($name, ['__construct', '__destruct', '__unset', '__wakeup', '__clone'], true)) { + if ($this->isConstructor) { + $realReturnType = new VoidType(); + } + if (in_array($name, ['__destruct', '__unset', '__wakeup', '__clone'], true)) { $realReturnType = new VoidType(); } if ($name === '__tostring') { @@ -230,7 +233,7 @@ public function isFinal(): TrinaryLogic { $method = $this->getClassMethod(); if ($method instanceof Node\PropertyHook) { - return TrinaryLogic::createFromBoolean((bool) ($method->flags & Modifiers::FINAL)); + return TrinaryLogic::createFromBoolean($method->isFinal()); } return TrinaryLogic::createFromBoolean($method->isFinal() || $this->isFinal); @@ -238,12 +241,7 @@ public function isFinal(): TrinaryLogic public function isFinalByKeyword(): TrinaryLogic { - $method = $this->getClassMethod(); - if ($method instanceof Node\PropertyHook) { - return TrinaryLogic::createFromBoolean((bool) ($method->flags & Modifiers::FINAL)); - } - - return TrinaryLogic::createFromBoolean($method->isFinal()); + return TrinaryLogic::createFromBoolean($this->getClassMethod()->isFinal()); } public function isBuiltin(): bool @@ -271,6 +269,11 @@ public function isAbstract(): TrinaryLogic return TrinaryLogic::createFromBoolean($method->isAbstract()); } + public function isConstructor(): bool + { + return $this->isConstructor; + } + public function hasSideEffects(): TrinaryLogic { if ( diff --git a/src/Reflection/Php/PhpPropertyReflection.php b/src/Reflection/Php/PhpPropertyReflection.php index 3ff948eb3a..1284d4a699 100644 --- a/src/Reflection/Php/PhpPropertyReflection.php +++ b/src/Reflection/Php/PhpPropertyReflection.php @@ -234,14 +234,7 @@ public function isAbstract(): TrinaryLogic public function isFinal(): TrinaryLogic { - if ($this->reflection->isFinal()) { - return TrinaryLogic::createYes(); - } - if ($this->reflection->isPrivate()) { - return TrinaryLogic::createNo(); - } - - return TrinaryLogic::createFromBoolean($this->isPrivateSet()); + return TrinaryLogic::createFromBoolean($this->reflection->isFinal()); } public function isVirtual(): TrinaryLogic @@ -277,32 +270,12 @@ public function getHook(string $hookType): ExtendedMethodReflection public function isProtectedSet(): bool { - if ($this->reflection->isProtectedSet()) { - return true; - } - - if ($this->isReadOnly()) { - return !$this->isPrivate() && !$this->reflection->isPrivateSet(); - } - - return false; + return $this->reflection->isProtectedSet(); } public function isPrivateSet(): bool { - if ($this->reflection->isPrivateSet()) { - return true; - } - - if ($this->reflection->isProtectedSet()) { - return false; - } - - if ($this->isReadOnly()) { - return $this->isPrivate(); - } - - return false; + return $this->reflection->isPrivateSet(); } } diff --git a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php index 2dca4eb85e..47b2feb4df 100644 --- a/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/ConstructorWithoutImpurePointsCollector.php @@ -7,7 +7,6 @@ use PHPStan\Collectors\Collector; use PHPStan\Node\MethodReturnStatementsNode; use function count; -use function strtolower; /** * @implements Collector @@ -23,7 +22,7 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope) { $method = $node->getMethodReflection(); - if (strtolower($method->getName()) !== '__construct') { + if (!$method->isConstructor()) { return null; } diff --git a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php index 1ec55619b6..675d150d52 100644 --- a/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php +++ b/src/Rules/DeadCode/MethodWithoutImpurePointsCollector.php @@ -49,11 +49,7 @@ public function processNode(Node $node, Scope $scope) return null; } - $declaringClass = $method->getDeclaringClass(); - if ( - $declaringClass->hasConstructor() - && $declaringClass->getConstructor()->getName() === $method->getName() - ) { + if ($method->isConstructor()) { return null; } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 50371ab23c..bd637913bc 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -101,6 +101,12 @@ public function check( $hasUnpackedArgument = false; $errors = []; foreach ($args as $arg) { + $argumentName = null; + if ($arg->name !== null) { + $hasNamedArguments = true; + $argumentName = $arg->name->toString(); + } + if ($hasNamedArguments && $arg->unpack) { $errors[] = RuleErrorBuilder::message('Named argument cannot be followed by an unpacked (...) argument.') ->identifier('argument.unpackAfterNamed') @@ -109,20 +115,17 @@ public function check( ->build(); } if ($hasUnpackedArgument && !$arg->unpack) { - $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') - ->identifier('argument.nonUnpackAfterUnpacked') - ->line($arg->getStartLine()) - ->nonIgnorable() - ->build(); + if ($argumentName === null || !$scope->getPhpVersion()->supportsNamedArgumentAfterUnpackedArgument()->yes()) { + $errors[] = RuleErrorBuilder::message('Unpacked argument (...) cannot be followed by a non-unpacked argument.') + ->identifier('argument.nonUnpackAfterUnpacked') + ->line($arg->getStartLine()) + ->nonIgnorable() + ->build(); + } } if ($arg->unpack) { $hasUnpackedArgument = true; } - $argumentName = null; - if ($arg->name !== null) { - $hasNamedArguments = true; - $argumentName = $arg->name->toString(); - } if ($arg->unpack) { $type = $scope->getType($arg->value); $arrays = $type->getConstantArrays(); diff --git a/src/Rules/Methods/ConsistentConstructorRule.php b/src/Rules/Methods/ConsistentConstructorRule.php index ab8553b5cc..16226a1074 100644 --- a/src/Rules/Methods/ConsistentConstructorRule.php +++ b/src/Rules/Methods/ConsistentConstructorRule.php @@ -7,6 +7,7 @@ use PHPStan\Node\InClassMethodNode; use PHPStan\Reflection\Dummy\DummyConstructorReflection; use PHPStan\Rules\Rule; +use function array_merge; use function strtolower; /** @implements Rule */ @@ -15,6 +16,7 @@ final class ConsistentConstructorRule implements Rule public function __construct( private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, ) { } @@ -47,7 +49,10 @@ public function processNode(Node $node, Scope $scope): array return []; } - return $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true); + return array_merge( + $this->methodParameterComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method, true), + $this->methodVisibilityComparisonHelper->compare($parentConstructor, $parentConstructor->getDeclaringClass(), $method), + ); } } diff --git a/src/Rules/Methods/MethodVisibilityComparisonHelper.php b/src/Rules/Methods/MethodVisibilityComparisonHelper.php new file mode 100755 index 0000000000..4807453f2a --- /dev/null +++ b/src/Rules/Methods/MethodVisibilityComparisonHelper.php @@ -0,0 +1,51 @@ + */ + public function compare(ExtendedMethodReflection $prototype, ClassReflection $prototypeDeclaringClass, PhpMethodFromParserNodeReflection $method): array + { + /** @var list $messages */ + $messages = []; + + if ($prototype->isPublic()) { + if (!$method->isPublic()) { + $messages[] = RuleErrorBuilder::message(sprintf( + '%s method %s::%s() overriding public method %s::%s() should also be public.', + $method->isPrivate() ? 'Private' : 'Protected', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.visibility') + ->build(); + } + } elseif ($method->isPrivate()) { + $messages[] = RuleErrorBuilder::message(sprintf( + 'Private method %s::%s() overriding protected method %s::%s() should be protected or public.', + $method->getDeclaringClass()->getDisplayName(), + $method->getName(), + $prototypeDeclaringClass->getDisplayName(true), + $prototype->getName(), + )) + ->nonIgnorable() + ->identifier('method.visibility') + ->build(); + } + + return $messages; + } + +} diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 38c588f9db..81a9b1b0e1 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -35,6 +35,7 @@ public function __construct( private MethodSignatureRule $methodSignatureRule, private bool $checkPhpDocMethodSignatures, private MethodParameterComparisonHelper $methodParameterComparisonHelper, + private MethodVisibilityComparisonHelper $methodVisibilityComparisonHelper, private PhpClassReflectionExtension $phpClassReflectionExtension, private bool $checkMissingOverrideMethodAttribute, ) @@ -165,32 +166,7 @@ public function processNode(Node $node, Scope $scope): array } if ($checkVisibility) { - if ($prototype->isPublic()) { - if (!$method->isPublic()) { - $messages[] = RuleErrorBuilder::message(sprintf( - '%s method %s::%s() overriding public method %s::%s() should also be public.', - $method->isPrivate() ? 'Private' : 'Protected', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototypeDeclaringClass->getDisplayName(true), - $prototype->getName(), - )) - ->nonIgnorable() - ->identifier('method.visibility') - ->build(); - } - } elseif ($method->isPrivate()) { - $messages[] = RuleErrorBuilder::message(sprintf( - 'Private method %s::%s() overriding protected method %s::%s() should be protected or public.', - $method->getDeclaringClass()->getDisplayName(), - $method->getName(), - $prototypeDeclaringClass->getDisplayName(true), - $prototype->getName(), - )) - ->nonIgnorable() - ->identifier('method.visibility') - ->build(); - } + $messages = array_merge($messages, $this->methodVisibilityComparisonHelper->compare($prototype, $prototypeDeclaringClass, $method)); } $prototypeVariants = $prototype->getVariants(); diff --git a/src/Rules/Playground/PromoteParameterRule.php b/src/Rules/Playground/PromoteParameterRule.php new file mode 100644 index 0000000000..cee351e3ff --- /dev/null +++ b/src/Rules/Playground/PromoteParameterRule.php @@ -0,0 +1,61 @@ + + */ +final class PromoteParameterRule implements Rule +{ + + /** + * @param Rule $rule + * @param class-string $nodeType + */ + public function __construct( + private Rule $rule, + private string $nodeType, + private bool $parameterValue, + private string $parameterName, + ) + { + } + + public function getNodeType(): string + { + return $this->nodeType; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($this->parameterValue) { + return []; + } + + if ($this->nodeType !== $this->rule->getNodeType()) { + return []; + } + + $errors = []; + foreach ($this->rule->processNode($node, $scope) as $error) { + $builder = RuleErrorBuilder::message($error->getMessage()) + ->identifier('phpstanPlayground.configParameter') + ->tip(sprintf('This error would be reported if the %s: true parameter was enabled in your %%configurationFile%%.', $this->parameterName)); + if ($error instanceof LineRuleError) { + $builder->line($error->getLine()); + } + $errors[] = $builder->build(); + } + + return $errors; + } + +} diff --git a/src/Rules/Playground/StaticVarWithoutTypeRule.php b/src/Rules/Playground/StaticVarWithoutTypeRule.php new file mode 100644 index 0000000000..28f5369952 --- /dev/null +++ b/src/Rules/Playground/StaticVarWithoutTypeRule.php @@ -0,0 +1,81 @@ + + */ +final class StaticVarWithoutTypeRule implements Rule +{ + + public function __construct( + private FileTypeMapper $fileTypeMapper, + ) + { + } + + public function getNodeType(): string + { + return Node\Stmt\Static_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $docComment = $node->getDocComment(); + $ruleError = RuleErrorBuilder::message('Static variable needs to be typed with PHPDoc @var tag.') + ->identifier('phpstanPlayground.staticWithoutType') + ->build(); + if ($docComment === null) { + return [$ruleError]; + } + $variableNames = []; + foreach ($node->vars as $var) { + if (!is_string($var->var->name)) { + throw new ShouldNotHappenException(); + } + + $variableNames[] = $var->var->name; + } + + $function = $scope->getFunction(); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $function !== null ? $function->getName() : null, + $docComment->getText(), + ); + $varTags = []; + foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) { + $varTags[$key] = $varTag; + } + + if (count($varTags) === 0) { + return [$ruleError]; + } + + if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) { + return []; + } + + foreach ($variableNames as $variableName) { + if (isset($varTags[$variableName])) { + continue; + } + + return [$ruleError]; + } + + return []; + } + +} diff --git a/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php b/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php index 61b1c32a6c..9a2fc917e7 100644 --- a/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php +++ b/src/Rules/Properties/GetNonVirtualPropertyHookReadRule.php @@ -70,7 +70,17 @@ public function processNode(Node $node, Scope $scope): array $errors = []; foreach ($node->getProperties() as $propertyNode) { - if (!$propertyNode->hasHooks()) { + $hasGetHook = false; + foreach ($propertyNode->getHooks() as $hook) { + if ($hook->name->toLowerString() !== 'get') { + continue; + } + + $hasGetHook = true; + break; + } + + if (!$hasGetHook) { continue; } diff --git a/src/Rules/Properties/ShortGetPropertyHookReturnTypeRule.php b/src/Rules/Properties/ShortGetPropertyHookReturnTypeRule.php deleted file mode 100644 index cf30777b19..0000000000 --- a/src/Rules/Properties/ShortGetPropertyHookReturnTypeRule.php +++ /dev/null @@ -1,75 +0,0 @@ - - */ -final class ShortGetPropertyHookReturnTypeRule implements Rule -{ - - public function __construct(private FunctionReturnTypeCheck $returnTypeCheck) - { - } - - public function getNodeType(): string - { - return InPropertyHookNode::class; - } - - public function processNode(Node $node, Scope $scope): array - { - // return statements in long property hook bodies are checked by Methods\ReturnTypeRule - // short set property hook type is checked by TypesAssignedToPropertiesRule - $hookReflection = $node->getHookReflection(); - if ($hookReflection->getPropertyHookName() !== 'get') { - return []; - } - - $originalHookNode = $node->getOriginalNode(); - $hookBody = $originalHookNode->body; - if (!$hookBody instanceof Node\Expr) { - return []; - } - - $methodDescription = sprintf( - 'Get hook for property %s::$%s', - $hookReflection->getDeclaringClass()->getDisplayName(), - $hookReflection->getHookedPropertyName(), - ); - - $returnType = $hookReflection->getReturnType(); - - return $this->returnTypeCheck->checkReturnType( - $scope, - $returnType, - $hookBody, - $node, - sprintf( - '%s should return %%s but empty return statement found.', - $methodDescription, - ), - sprintf( - '%s with return type void returns %%s but should not return anything.', - $methodDescription, - ), - sprintf( - '%s should return %%s but returns %%s.', - $methodDescription, - ), - sprintf( - '%s should never return but return statement found.', - $methodDescription, - ), - $hookReflection->isGenerator(), - ); - } - -} diff --git a/src/Rules/Pure/FunctionPurityCheck.php b/src/Rules/Pure/FunctionPurityCheck.php index 236c8d89ac..ccc85a1c24 100644 --- a/src/Rules/Pure/FunctionPurityCheck.php +++ b/src/Rules/Pure/FunctionPurityCheck.php @@ -40,18 +40,11 @@ public function check( array $impurePoints, array $throwPoints, array $statements, + bool $isConstructor, ): array { $errors = []; $isPure = $functionReflection->isPure(); - $isConstructor = false; - if ( - $functionReflection instanceof ExtendedMethodReflection - && $functionReflection->getDeclaringClass()->hasConstructor() - && $functionReflection->getDeclaringClass()->getConstructor()->getName() === $functionReflection->getName() - ) { - $isConstructor = true; - } if ($isPure->yes()) { foreach ($parameters as $parameter) { diff --git a/src/Rules/Pure/PureFunctionRule.php b/src/Rules/Pure/PureFunctionRule.php index 622a093e0b..e05a0be902 100644 --- a/src/Rules/Pure/PureFunctionRule.php +++ b/src/Rules/Pure/PureFunctionRule.php @@ -36,6 +36,7 @@ public function processNode(Node $node, Scope $scope): array $node->getImpurePoints(), $node->getStatementResult()->getThrowPoints(), $node->getStatements(), + false, ); } diff --git a/src/Rules/Pure/PureMethodRule.php b/src/Rules/Pure/PureMethodRule.php index 5dd972f709..8ef6f87c66 100644 --- a/src/Rules/Pure/PureMethodRule.php +++ b/src/Rules/Pure/PureMethodRule.php @@ -36,6 +36,7 @@ public function processNode(Node $node, Scope $scope): array $node->getImpurePoints(), $node->getStatementResult()->getThrowPoints(), $node->getStatements(), + $method->isConstructor(), ); } diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index f31e3108b4..d630cad147 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -323,9 +323,14 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($type->isNull()->yes()) { + return new ConstantBooleanType(false); + } + if ($type->isString()->yes() && $type->isNonEmptyString()->no()) { return new ConstantBooleanType(false); } + return new BooleanType(); } diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 04fd4fb60e..faac90150e 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -11,6 +11,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\FloatType; @@ -19,6 +20,7 @@ use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StaticTypeFactory; use PHPStan\Type\StringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; use PHPStan\Type\Traits\NonArrayTypeTrait; @@ -322,6 +324,11 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + $falseyTypes = StaticTypeFactory::falsey(); + if ($falseyTypes->isSuperTypeOf($type)->yes()) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index e0f4964c93..9429c7ea73 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -11,6 +11,7 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\ErrorType; @@ -324,6 +325,14 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($type->isNull()->yes()) { + return new ConstantBooleanType(false); + } + + if ($type->isString()->yes() && $type->isNumericString()->no()) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 1514781d0f..114843f993 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -418,9 +418,25 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - if ($this->isIterableAtLeastOnce()->no() && count($type->getConstantScalarValues()) === 1) { - // @phpstan-ignore equal.invalid, equal.notAllowed - return new ConstantBooleanType($type->getConstantScalarValues()[0] == []); // phpcs:ignore + if ($type->isInteger()->yes()) { + return new ConstantBooleanType(false); + } + + if ($this->isIterableAtLeastOnce()->no()) { + if ($type->isIterableAtLeastOnce()->yes()) { + return new ConstantBooleanType(false); + } + + $constantScalarValues = $type->getConstantScalarValues(); + if (count($constantScalarValues) > 0) { + $results = []; + foreach ($constantScalarValues as $constantScalarValue) { + // @phpstan-ignore equal.invalid, equal.notAllowed + $results[] = TrinaryLogic::createFromBoolean($constantScalarValue == []); // phpcs:ignore + } + + return TrinaryLogic::extremeIdentity(...$results)->toBooleanType(); + } } return new BooleanType(); diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 3cc5e6c62e..614ef44910 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -9,7 +9,6 @@ use PHPStan\Broker\AnonymousClassNameHelper; use PHPStan\File\FileHelper; use PHPStan\Parser\Parser; -use PHPStan\Parser\PropertyHookNameVisitor; use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; @@ -281,7 +280,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } elseif ($node instanceof Node\Stmt\Function_) { $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); } elseif ($node instanceof Node\PropertyHook) { - $propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $node->getAttribute('propertyName'); if ($propertyName !== null) { $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); } @@ -299,7 +298,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA return null; } elseif ($node instanceof Node\PropertyHook) { - $propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $node->getAttribute('propertyName'); if ($propertyName !== null) { $docComment = GetLastDocComment::forNode($node); if ($docComment !== null) { @@ -394,7 +393,7 @@ static function (Node $node) use (&$namespace, &$functionStack, &$classStack): v array_pop($functionStack); } elseif ($node instanceof Node\PropertyHook) { - $propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $node->getAttribute('propertyName'); if ($propertyName !== null) { if (count($functionStack) === 0) { throw new ShouldNotHappenException(); @@ -503,7 +502,7 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun } elseif ($node instanceof Node\Stmt\Function_) { $functionStack[] = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); } elseif ($node instanceof Node\PropertyHook) { - $propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $node->getAttribute('propertyName'); if ($propertyName !== null) { $functionStack[] = sprintf('$%s::%s', $propertyName, $node->name->toString()); } @@ -742,7 +741,7 @@ static function (Node $node, $callbackResult) use (&$namespace, &$functionStack, array_pop($functionStack); } elseif ($node instanceof Node\PropertyHook) { - $propertyName = $node->getAttribute(PropertyHookNameVisitor::ATTRIBUTE_NAME); + $propertyName = $node->getAttribute('propertyName'); if ($propertyName !== null) { if (count($functionStack) === 0) { throw new ShouldNotHappenException(); diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index b90f4d31a7..1c956015dd 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -315,6 +315,16 @@ public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryL $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThan($otherType, $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThan($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -332,6 +342,16 @@ public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): T $maxIsSmaller = (new ConstantIntegerType($this->max))->isSmallerThanOrEqual($otherType, $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $zeroInt->isSmallerThanOrEqual($otherType, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -349,6 +369,16 @@ public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryL $maxIsSmaller = $otherType->isSmallerThan((new ConstantIntegerType($this->max)), $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThan($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -366,6 +396,16 @@ public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): T $maxIsSmaller = $otherType->isSmallerThanOrEqual((new ConstantIntegerType($this->max)), $phpVersion); } + // 0 can have different results in contrast to the interval edges, see https://3v4l.org/iGoti + $zeroInt = new ConstantIntegerType(0); + if (!$zeroInt->isSuperTypeOf($this)->no()) { + return TrinaryLogic::extremeIdentity( + $otherType->isSmallerThanOrEqual($zeroInt, $phpVersion), + $minIsSmaller, + $maxIsSmaller, + ); + } + return TrinaryLogic::extremeIdentity($minIsSmaller, $maxIsSmaller); } @@ -694,7 +734,20 @@ public function toPhpDocNode(): TypeNode public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { - if ($this->isSmallerThan($type, $phpVersion)->yes() || $this->isGreaterThan($type, $phpVersion)->yes()) { + $zeroInt = new ConstantIntegerType(0); + if ($zeroInt->isSuperTypeOf($this)->no()) { + if ($type->isTrue()->yes()) { + return new ConstantBooleanType(true); + } + if ($type->isFalse()->yes()) { + return new ConstantBooleanType(false); + } + } + + if ( + $this->isSmallerThan($type, $phpVersion)->yes() + || $this->isGreaterThan($type, $phpVersion)->yes() + ) { return new ConstantBooleanType(false); } diff --git a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php index dc31159355..30bbe16741 100644 --- a/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php +++ b/src/Type/Php/ArrayFilterFunctionReturnTypeHelper.php @@ -22,7 +22,6 @@ use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -256,7 +255,7 @@ private function processKeyAndItemType(MutatingScope $scope, Type $keyType, Type return [ $keyVarName !== null ? $scope->getVariableType($keyVarName) : $keyType, $itemVarName !== null ? $scope->getVariableType($itemVarName) : $itemType, - !$booleanResult instanceof ConstantBooleanType, + !$booleanResult->isTrue()->yes(), ]; } diff --git a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php index 58bb5fe1ad..712bd4edc3 100644 --- a/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArrayMergeFunctionDynamicReturnTypeExtension.php @@ -5,13 +5,14 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\ShouldNotHappenException; +use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; @@ -39,58 +40,53 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $argTypes = []; $optionalArgTypes = []; - $allConstant = true; foreach ($args as $arg) { $argType = $scope->getType($arg->value); if ($arg->unpack) { - if ($argType instanceof ConstantArrayType) { - $argTypesFound = $argType->getValueTypes(); - } else { - $argTypesFound = [$argType->getIterableValueType()]; - } - - foreach ($argTypesFound as $argTypeFound) { - $argTypes[] = $argTypeFound; - if ($argTypeFound instanceof ConstantArrayType) { - continue; + if ($argType->isConstantArray()->yes()) { + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + $argTypes[] = $valueType; + } } - $allConstant = false; + } else { + $argTypes[] = $argType->getIterableValueType(); } if (!$argType->isIterableAtLeastOnce()->yes()) { // unpacked params can be empty, making them optional $optionalArgTypesOffset = count($argTypes) - 1; - foreach (array_keys($argTypesFound) as $key) { + foreach (array_keys($argTypes) as $key) { $optionalArgTypes[] = $optionalArgTypesOffset + $key; } } } else { $argTypes[] = $argType; - if (!$argType instanceof ConstantArrayType) { - $allConstant = false; - } } } - if ($allConstant) { + $allConstant = TrinaryLogic::createYes()->lazyAnd( + $argTypes, + static fn (Type $argType) => $argType->isConstantArray(), + ); + + if ($allConstant->yes()) { $newArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach ($argTypes as $argType) { - if (!$argType instanceof ConstantArrayType) { - throw new ShouldNotHappenException(); + /** @var array $keyTypes */ + $keyTypes = []; + foreach ($argType->getConstantArrays() as $constantArray) { + foreach ($constantArray->getKeyTypes() as $keyType) { + $keyTypes[$keyType->getValue()] = $keyType; + } } - $keyTypes = $argType->getKeyTypes(); - $valueTypes = $argType->getValueTypes(); - $optionalKeys = $argType->getOptionalKeys(); - - foreach ($keyTypes as $k => $keyType) { - $isOptional = in_array($k, $optionalKeys, true); - + foreach ($keyTypes as $keyType) { $newArrayBuilder->setOffsetValueType( $keyType instanceof ConstantIntegerType ? null : $keyType, - $valueTypes[$k], - $isOptional, + $argType->getOffsetValueType($keyType), + !$argType->hasOffsetValueType($keyType)->yes(), ); } } diff --git a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php index 8560faf5a9..762e577211 100644 --- a/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/ArraySearchFunctionDynamicReturnTypeExtension.php @@ -43,7 +43,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $strictArgType = $scope->getType($functionCall->getArgs()[2]->value); - if (!$strictArgType instanceof ConstantBooleanType || $strictArgType->getValue() === false) { + if (!$strictArgType->isTrue()->yes()) { return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false)); } diff --git a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php index 5c680b5b62..a052a43416 100644 --- a/src/Type/Php/ImplodeFunctionReturnTypeExtension.php +++ b/src/Type/Php/ImplodeFunctionReturnTypeExtension.php @@ -4,7 +4,9 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Internal\CombinationsHelper; use PHPStan\Reflection\FunctionReflection; +use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Type\Accessory\AccessoryLiteralStringType; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; @@ -12,7 +14,6 @@ use PHPStan\Type\Accessory\AccessoryUppercaseStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntersectionType; use PHPStan\Type\StringType; @@ -114,14 +115,28 @@ private function inferConstantType(ConstantArrayType $arrayType, ConstantStringT $valueTypes = $array->getValueTypes(); $arrayValues = []; + $combinationsCount = 1; foreach ($valueTypes as $valueType) { - if (!$valueType instanceof ConstantScalarType) { + $constScalars = $valueType->getConstantScalarValues(); + if (count($constScalars) === 0) { return null; } - $arrayValues[] = $valueType->getValue(); + $arrayValues[] = $constScalars; + $combinationsCount *= count($constScalars); } - $strings[] = new ConstantStringType(implode($separatorType->getValue(), $arrayValues)); + if ($combinationsCount > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; + } + + $combinations = CombinationsHelper::combinations($arrayValues); + foreach ($combinations as $combination) { + $strings[] = new ConstantStringType(implode($separatorType->getValue(), $combination)); + } + } + + if (count($strings) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) { + return null; } return TypeCombinator::union(...$strings); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 4605c92efe..eeedd4bc68 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -10,6 +10,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Traits\MaybeCallableTypeTrait; @@ -267,6 +268,10 @@ public function isScalar(): TrinaryLogic public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType { + if ($type->isArray()->yes()) { + return new ConstantBooleanType(false); + } + return new BooleanType(); } diff --git a/tests/PHPStan/Analyser/nsrt/array-merge2.php b/tests/PHPStan/Analyser/nsrt/array-merge2.php index a52f640ef2..f0d86e61b6 100644 --- a/tests/PHPStan/Analyser/nsrt/array-merge2.php +++ b/tests/PHPStan/Analyser/nsrt/array-merge2.php @@ -21,6 +21,10 @@ public function arrayMergeArrayShapes($array1, $array2): void assertType("array{foo: '1', bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1)); assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ['foo' => 3])); assertType("array{foo: 3, bar: '2', lall2: '3', 0: '4', 1: '6', lall: '3', 2: '2', 3: '3'}", array_merge($array2, $array1, ...[['foo' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge(rand(0, 1) ? $array1 : $array2, [])); + assertType("array{foo?: 3, bar?: 3}", array_merge([], ...[rand(0, 1) ? ['foo' => 3] : ['bar' => 3]])); + assertType("array{foo: '1', bar: '2'|'4', lall?: '3', 0: '2'|'4', 1: '3'|'6', lall2?: '3'}", array_merge([], ...[rand(0, 1) ? $array1 : $array2])); + assertType("array{foo: 1, bar: 2, 0: 2, 1: 3}", array_merge(['foo' => 4, 'bar' => 5], ...[['foo' => 1, 'bar' => 2], [2, 3]])); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-10338.php b/tests/PHPStan/Analyser/nsrt/bug-10338.php new file mode 100644 index 0000000000..cb9103eae0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10338.php @@ -0,0 +1,12 @@ + */ + protected $arr = []; + + /** + * @param array $arr + */ + public function __construct(array $arr) { + $this->arr = $arr; + } + + /** + * @return T + */ + public function last() + { + if (!$this->arr) { + throw new \Exception('bad'); + } + return end($this->arr); + } +} + +/** + * @template T + * @extends Collection + */ +class CollectionChild extends Collection { +} + +$dogs = new CollectionChild([new Dog(), new Dog()]); +assertType('Bug2735\\CollectionChild', $dogs); + +/** + * @template X + * @template Y + */ +class ParentWithConstructor +{ + + /** + * @param X $x + * @param Y $y + */ + public function __construct($x, $y) + { + } + +} + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildOne extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildOne(1, new Dog()); + assertType('Bug2735\\ChildOne', $a); +}; + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildTwo extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildTwo(new Cat(), 2); + assertType('Bug2735\\ChildTwo', $a); +}; + +/** + * @template T + * @extends ParentWithConstructor + */ +class ChildThree extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildThree(new Cat(), new Dog()); + assertType('Bug2735\\ChildThree', $a); +}; + +/** + * @template T + * @template U + * @extends ParentWithConstructor + */ +class ChildFour extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildFour(new Cat(), new Dog()); + assertType('Bug2735\\ChildFour', $a); +}; + +/** + * @template T + * @template U + * @extends ParentWithConstructor + */ +class ChildFive extends ParentWithConstructor +{ + +} + +function (): void { + $a = new ChildFive(new Cat(), new Dog()); + assertType('Bug2735\\ChildFive', $a); +}; diff --git a/tests/PHPStan/Analyser/nsrt/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php index 8aa8fae879..1968fab471 100644 --- a/tests/PHPStan/Analyser/nsrt/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -741,7 +741,7 @@ function testClasses() assertType('DateTime', $ab->getB(new \DateTime())); $noConstructor = new NoConstructor(1); - assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); + assertType('PHPStan\Generics\FunctionsAssertType\NoConstructor', $noConstructor); assertType('stdClass', acceptsClassString(\stdClass::class)); assertType('class-string', returnsClassString(new \stdClass())); diff --git a/tests/PHPStan/Analyser/nsrt/implode.php b/tests/PHPStan/Analyser/nsrt/implode.php index b2e2b9aa7c..51e121a4c1 100644 --- a/tests/PHPStan/Analyser/nsrt/implode.php +++ b/tests/PHPStan/Analyser/nsrt/implode.php @@ -21,4 +21,34 @@ public function constants() { assertType("'x,345'", join(',', [self::X, '345'])); assertType("'1,345'", join(',', [self::ONE, '345'])); } + + /** @param array{0: 1|2, 1: 'a'|'b'} $constArr */ + public function constArrays($constArr) { + assertType("'1a'|'1b'|'2a'|'2b'", implode('', $constArr)); + } + + /** @param array{0: 1|2|3, 1: 'a'|'b'|'c'} $constArr */ + public function constArrays2($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'3a'|'3b'|'3c'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 2: 'x'|'y'} $constArr */ + public function constArrays3($constArr) { + assertType("'1ax'|'1ay'|'1bx'|'1by'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 2?: 'x'|'y'} $constArr */ + public function constArrays4($constArr) { + assertType("'1a'|'1ax'|'1ay'|'1b'|'1bx'|'1by'", implode('', $constArr)); + } + + /** @param array{10: 1|2|3, xy: 'a'|'b'|'c'} $constArr */ + public function constArrays5($constArr) { + assertType("'1a'|'1b'|'1c'|'2a'|'2b'|'2c'|'3a'|'3b'|'3c'", implode('', $constArr)); + } + + /** @param array{0: 1, 1: 'a'|'b', 3?: 'c'|'d', 4?: 'e'|'f', 5?: 'g'|'h', 6?: 'x'|'y'} $constArr */ + public function constArrays6($constArr) { + assertType("string", implode('', $constArr)); + } } diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php index 9e00dd0f65..cc5b0f4eb4 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php7.php @@ -61,4 +61,15 @@ public function sayInt( assertType('bool', $int == $phpStr); assertType('bool', $int == 'a'); } + + /** + * @param "abc"|"def" $constNonFalsy + */ + public function sayConstUnion( + $constNonFalsy, + ): void + { + assertType('true', $constNonFalsy == 0); + assertType('true', "" == 0); + } } diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php index a3ca84cf64..3c12092eb7 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons-php8.php @@ -68,4 +68,15 @@ public function sayInt( assertType('false', $intRange == 'a'); } + /** + * @param "abc"|"def" $constNonFalsy + */ + public function sayConstUnion( + $constNonFalsy, + ): void + { + assertType('false', $constNonFalsy == 0); + assertType('false', "" == 0); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php index 16c414170b..c385548cf5 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -526,6 +526,7 @@ public function sayEmptyArray( * @param array{} $emptyArr * @param 'php' $phpStr * @param '' $emptyStr + * @param non-falsy-string $nonFalsyString */ public function sayNonFalsyStr( $true, @@ -540,7 +541,8 @@ public function sayNonFalsyStr( $null, $emptyArr, $phpStr, - $emptyStr + $emptyStr, + $nonFalsyString ): void { assertType('true', $phpStr == $true); @@ -555,6 +557,100 @@ public function sayNonFalsyStr( assertType('false', $phpStr == $emptyArr); assertType('true', $phpStr == $phpStr); assertType('false', $phpStr == $emptyStr); + + assertType('bool', $nonFalsyString == $true); + assertType('false', $nonFalsyString == $false); + assertType('bool', $nonFalsyString == $one); + assertType('false', $nonFalsyString == $zero); + assertType('bool', $nonFalsyString == $minusOne); + assertType('bool', $nonFalsyString == $oneStr); + assertType('false', $nonFalsyString == $zeroStr); + assertType('bool', $nonFalsyString == $minusOneStr); + assertType('bool', $nonFalsyString == $plusOneStr); + assertType('false', $nonFalsyString == $null); + assertType('false', $nonFalsyString == $emptyArr); + assertType('bool', $nonFalsyString == $phpStr); + assertType('false', $nonFalsyString == $emptyStr); + } + + /** + * @param true $true + * @param false $false + * @param 1 $one + * @param 0 $zero + * @param -1 $minusOne + * @param '1' $oneStr + * @param '0' $zeroStr + * @param '-1' $minusOneStr + * @param '+1' $plusOneStr + * @param null $null + * @param array{} $emptyArr + * @param 'php' $phpStr + * @param '' $emptyStr + * @param numeric-string $numericStr + */ + public function sayStr( + $true, + $false, + $one, + $zero, + $minusOne, + $oneStr, + $zeroStr, + $minusOneStr, + $plusOneStr, + $null, + $emptyArr, + string $string, + $phpStr, + $emptyStr, + $numericStr, + ?string $stringOrNull, + ): void + { + assertType('bool', $string == $true); + assertType('bool', $string == $false); + assertType('bool', $string == $one); + assertType('bool', $string == $zero); + assertType('bool', $string == $minusOne); + assertType('bool', $string == $oneStr); + assertType('bool', $string == $zeroStr); + assertType('bool', $string == $minusOneStr); + assertType('bool', $string == $plusOneStr); + assertType('bool', $string == $null); + assertType('bool', $string == $stringOrNull); + assertType('false', $string == $emptyArr); + assertType('bool', $string == $phpStr); + assertType('bool', $string == $emptyStr); + assertType('bool', $string == $numericStr); + + assertType('bool', $numericStr == $true); + assertType('bool', $numericStr == $false); + assertType('bool', $numericStr == $one); + assertType('bool', $numericStr == $zero); + assertType('bool', $numericStr == $minusOne); + assertType('bool', $numericStr == $oneStr); + assertType('bool', $numericStr == $zeroStr); + assertType('bool', $numericStr == $minusOneStr); + assertType('bool', $numericStr == $plusOneStr); + assertType('false', $numericStr == $null); + assertType('bool', $numericStr == $stringOrNull); + assertType('false', $numericStr == $emptyArr); + assertType('bool', $numericStr == $string); + assertType('false', $numericStr == $phpStr); + assertType('false', $numericStr == $emptyStr); + if (is_numeric($string)) { + assertType('bool', $numericStr == $string); + } + + assertType('false', "" == 1); + assertType('true', "" == null); + assertType('false', "" == true); + assertType('true', "" == false); + assertType('false', "" == "1"); + assertType('false', "" == "0"); + assertType('false', "" == "-1"); + assertType('false', "" == []); } /** @@ -616,7 +712,9 @@ public function sayEmptyStr( * @param array{} $emptyArr * @param 'php' $phpStr * @param '' $emptyStr - * @param int<10, 20> $intRange + * @param int<10, 20> $positiveIntRange + * @param int<-20, -10> $negativeIntRange + * @param int<-10, 10> $minusTenToTen */ public function sayInt( $true, @@ -633,6 +731,11 @@ public function sayInt( array $array, int $int, int $intRange, + string $emptyStr, + string $phpStr, + int $positiveIntRange, + int $negativeIntRange, + int $minusTenToTen, ): void { assertType('bool', $int == $true); @@ -648,20 +751,89 @@ public function sayInt( assertType('false', $int == $emptyArr); assertType('false', $int == $array); - assertType('false', $intRange == $emptyArr); - assertType('false', $intRange == $array); + assertType('true', $positiveIntRange == $true); + assertType('false', $positiveIntRange == $false); + assertType('false', $positiveIntRange == $one); + assertType('false', $positiveIntRange == $zero); + assertType('false', $positiveIntRange == $minusOne); + assertType('false', $positiveIntRange == $oneStr); + assertType('false', $positiveIntRange == $zeroStr); + assertType('false', $positiveIntRange == $minusOneStr); + assertType('false', $positiveIntRange == $plusOneStr); + assertType('false', $positiveIntRange == $null); + assertType('false', $positiveIntRange == $emptyArr); + assertType('false', $positiveIntRange == $array); + assertType('true', $negativeIntRange == $true); + assertType('false', $negativeIntRange == $false); + assertType('false', $negativeIntRange == $one); + assertType('false', $negativeIntRange == $zero); + assertType('false', $negativeIntRange == $minusOne); + assertType('false', $negativeIntRange == $oneStr); + assertType('false', $negativeIntRange == $zeroStr); + assertType('false', $negativeIntRange == $minusOneStr); + assertType('false', $negativeIntRange == $plusOneStr); + assertType('false', $negativeIntRange == $null); + assertType('false', $negativeIntRange == $emptyArr); + assertType('false', $negativeIntRange == $array); + + // see https://3v4l.org/VudDK + assertType('bool', $minusTenToTen == $true); + assertType('bool', $minusTenToTen == $false); + assertType('bool', $minusTenToTen == $one); + assertType('bool', $minusTenToTen == $zero); + assertType('bool', $minusTenToTen == $minusOne); + assertType('bool', $minusTenToTen == $oneStr); + assertType('bool', $minusTenToTen == $zeroStr); + assertType('bool', $minusTenToTen == $minusOneStr); + assertType('bool', $minusTenToTen == $plusOneStr); + assertType('bool', $minusTenToTen == $null); + assertType('false', $minusTenToTen == $emptyArr); + assertType('false', $minusTenToTen == $array); + + // see https://3v4l.org/oJl3K + assertType('false', $minusTenToTen < $null); + assertType('bool', $minusTenToTen > $null); + assertType('bool', $minusTenToTen <= $null); + assertType('true', $minusTenToTen >= $null); + + // see https://3v4l.org/oRSgU + assertType('bool', $null < $minusTenToTen); + assertType('false', $null > $minusTenToTen); + assertType('true', $null <= $minusTenToTen); + assertType('bool', $null >= $minusTenToTen); + + assertType('false', 5 == $emptyArr); + assertType('false', $emptyArr == 5); + assertType('false', 5 == $array); + assertType('false', $array == 5); + assertType('false', [] == 5); + assertType('false', 5 == []); + + assertType('false', 5 == $emptyStr); + assertType('false', 5 == $phpStr); + assertType('false', 5 == 'a'); + + assertType('false', $emptyStr == 5); + assertType('false', $phpStr == 5); + assertType('false', 'a' == 5); } /** * @param true|1|"1" $looseOne * @param false|0|"0" $looseZero * @param false|1 $constMix + * @param "abc"|"def" $constNonFalsy + * @param array{abc: string, num?: int, nullable: ?string} $arrShape + * @param array{} $emptyArr */ public function sayConstUnion( $looseOne, $looseZero, - $constMix + $constMix, + $constNonFalsy, + array $arrShape, + array $emptyArr ): void { assertType('true', $looseOne == 1); @@ -696,6 +868,22 @@ public function sayConstUnion( assertType('bool', $constMix == $looseOne); assertType('bool', $looseZero == $constMix); assertType('bool', $constMix == $looseZero); + + assertType('false', $constNonFalsy == 1); + assertType('false', $constNonFalsy == null); + assertType('true', $constNonFalsy == true); + assertType('false', $constNonFalsy == false); + assertType('false', $constNonFalsy == "1"); + assertType('false', $constNonFalsy == "0"); + assertType('false', $constNonFalsy == []); + + assertType('false', $emptyArr == $looseOne); + assertType('bool', $emptyArr == $constMix); + assertType('bool', $emptyArr == $looseZero); + + assertType('bool', $arrShape == $looseOne); + assertType('bool', $arrShape == $constMix); + assertType('bool', $arrShape == $looseZero); } /** @@ -703,6 +891,7 @@ public function sayConstUnion( * @param lowercase-string $lower * @param array{} $emptyArr * @param non-empty-array $nonEmptyArr + * @param array{abc: string, num?: int, nullable: ?string} $arrShape * @param int<10, 20> $intRange */ public function sayIntersection( @@ -712,6 +901,7 @@ public function sayIntersection( array $emptyArr, array $nonEmptyArr, array $arr, + array $arrShape, int $i, int $intRange, ): void @@ -743,11 +933,24 @@ public function sayIntersection( assertType('false', $nonEmptyArr == $i); assertType('false', $arr == $intRange); assertType('false', $nonEmptyArr == $intRange); - assertType('bool', $emptyArr == $nonEmptyArr); // should be false + assertType('false', $emptyArr == $nonEmptyArr); assertType('false', $nonEmptyArr == $emptyArr); assertType('bool', $arr == $nonEmptyArr); assertType('bool', $nonEmptyArr == $arr); + assertType('false', 5 == $arr); + assertType('false', $arr == 5); + assertType('false', 5 == $emptyArr); + assertType('false', $emptyArr == 5); + assertType('false', 5 == $nonEmptyArr); + assertType('false', $nonEmptyArr == 5); + assertType('false', 5 == $arrShape); + assertType('false', $arrShape == 5); + if (count($arr) > 0) { + assertType('false', 5 == $arr); + assertType('false', $arr == 5); + } + assertType('bool', '' == $lower); if ($lower != '') { assertType('false', '' == $lower); diff --git a/tests/PHPStan/Parser/CleaningParserTest.php b/tests/PHPStan/Parser/CleaningParserTest.php index 8dbf569171..69c0e8af10 100644 --- a/tests/PHPStan/Parser/CleaningParserTest.php +++ b/tests/PHPStan/Parser/CleaningParserTest.php @@ -75,7 +75,6 @@ public function testParse( new NameResolver(), new VariadicMethodsVisitor(), new VariadicFunctionsVisitor(), - new PropertyHookNameVisitor(), ), new PhpVersion($phpVersionId), ); diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index 6e56f9dfcd..e49a6100f7 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -210,15 +210,21 @@ public function testBug11694(): void 39, 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], + [ + 'Loose comparison using == between true and int<10, 20> will always evaluate to true.', + 41, + ], + [ + 'Loose comparison using == between int<10, 20> and true will always evaluate to true.', + 42, + ], [ 'Loose comparison using == between false and int<10, 20> will always evaluate to false.', 44, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Loose comparison using == between int<10, 20> and false will always evaluate to false.', 45, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]); diff --git a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php index 77feb89d7d..67f6f42b4e 100644 --- a/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/CallToMethodStatementWithoutImpurePointsRuleTest.php @@ -72,6 +72,14 @@ public function testBug11011(): void ]); } + public function testBug12379(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + $this->analyse([__DIR__ . '/data/bug-12379.php'], []); + } + protected function getCollectors(): array { return [ diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementNextStatementsRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementNextStatementsRuleTest.php new file mode 100644 index 0000000000..36aa85b6bb --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementNextStatementsRuleTest.php @@ -0,0 +1,94 @@ + + */ +class UnreachableStatementNextStatementsRuleTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new class implements Rule { + + public function getNodeType(): string + { + return UnreachableStatementNode::class; + } + + /** + * @param UnreachableStatementNode $node + */ + public function processNode(Node $node, Scope $scope): array + { + $errors = [ + RuleErrorBuilder::message('First unreachable') + ->identifier('tests.nextUnreachableStatements') + ->build(), + ]; + + foreach ($node->getNextStatements() as $nextStatement) { + $errors[] = RuleErrorBuilder::message('Another unreachable') + ->line($nextStatement->getStartLine()) + ->identifier('tests.nextUnreachableStatements') + ->build(); + } + + return $errors; + } + + }; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/multiple_unreachable.php'], [ + [ + 'First unreachable', + 14, + ], + [ + 'Another unreachable', + 15, + ], + [ + 'Another unreachable', + 17, + ], + [ + 'Another unreachable', + 22, + ], + ]); + } + + public function testRuleTopLevel(): void + { + $this->analyse([__DIR__ . '/data/multiple_unreachable_top_level.php'], [ + [ + 'First unreachable', + 9, + ], + [ + 'Another unreachable', + 10, + ], + [ + 'Another unreachable', + 17, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php index da076db1c7..ec97b0481a 100644 --- a/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php @@ -230,4 +230,15 @@ public function testBug11992(): void $this->analyse([__DIR__ . '/data/bug-11992.php'], []); } + public function testMultipleUnreachable(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/multiple_unreachable.php'], [ + [ + 'Unreachable statement - code above always terminates.', + 14, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-12379.php b/tests/PHPStan/Rules/DeadCode/data/bug-12379.php new file mode 100644 index 0000000000..f8dc4ede85 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-12379.php @@ -0,0 +1,22 @@ += 8.1 + +namespace Bug12379; + +class HelloWorld +{ + use myTrait{ + myTrait::__construct as private __myTraitConstruct; + } + + public function __construct( + int $entityManager + ){ + $this->__myTraitConstruct($entityManager); + } +} + +trait myTrait{ + public function __construct( + private readonly int $entityManager + ){} +} diff --git a/tests/PHPStan/Rules/DeadCode/data/multiple_unreachable.php b/tests/PHPStan/Rules/DeadCode/data/multiple_unreachable.php new file mode 100644 index 0000000000..0e9ab15119 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/multiple_unreachable.php @@ -0,0 +1,23 @@ + throw new \InvalidArgumentException(); // error + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php index 82c4c2381f..08e3f10940 100644 --- a/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php +++ b/tests/PHPStan/Rules/Exceptions/data/throws-void-property-hook.php @@ -19,4 +19,11 @@ class Foo } } + public int $j { + /** + * @throws void + */ + get => throw new MyException(); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php index 92998bafa4..6cf4b55072 100644 --- a/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php +++ b/tests/PHPStan/Rules/Exceptions/data/too-wide-throws-property-hook.php @@ -78,4 +78,9 @@ class Foo } } + public int $k { + /** @throws \DomainException */ + get => 11; // error - DomainException unused + } + } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index a6513f03cb..4eb95d8449 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -499,6 +499,20 @@ public function testNamedArguments(): void $this->analyse([__DIR__ . '/data/named-arguments.php'], $errors); } + public function testNamedArgumentsAfterUnpacking(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/named-arguments-after-unpacking.php'], [ + [ + 'Argument for parameter $b has already been passed.', + 14, + ], + ]); + } + public function testBug4514(): void { $this->analyse([__DIR__ . '/data/bug-4514.php'], []); @@ -1611,6 +1625,15 @@ public function testBug9399(): void $this->analyse([__DIR__ . '/data/bug-9399.php'], []); } + public function testBug9559(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0'); + } + + $this->analyse([__DIR__ . '/data/bug-9559.php'], []); + } + public function testBug9923(): void { if (PHP_VERSION_ID < 80000) { @@ -1936,4 +1959,27 @@ public function testBug12051(): void $this->analyse([__DIR__ . '/data/bug-12051.php'], []); } + public function testBug8046(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-8046.php'], []); + } + + public function testBug11418(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11418.php'], []); + } + + public function testBug3107(): void + { + $this->analyse([__DIR__ . '/data/bug-3107.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Functions/data/bug-11418.php b/tests/PHPStan/Rules/Functions/data/bug-11418.php new file mode 100755 index 0000000000..8172892d95 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-11418.php @@ -0,0 +1,9 @@ +val = $mixed; + + $a = []; + $a[$holder->val] = 1; + take($a); +} + +/** @param array $a */ +function take($a): void {} diff --git a/tests/PHPStan/Rules/Functions/data/bug-8046.php b/tests/PHPStan/Rules/Functions/data/bug-8046.php new file mode 100644 index 0000000000..368f656341 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-8046.php @@ -0,0 +1,11 @@ + 7]; + +var_dump(add(...$args, b: 8)); diff --git a/tests/PHPStan/Rules/Functions/data/bug-9559.php b/tests/PHPStan/Rules/Functions/data/bug-9559.php new file mode 100644 index 0000000000..8f452e90f0 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-9559.php @@ -0,0 +1,12 @@ + "3" ])); +} diff --git a/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php new file mode 100755 index 0000000000..29d9ac8b4e --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/named-arguments-after-unpacking.php @@ -0,0 +1,14 @@ + 2, 'a' => 1], d: 40)); // 46 + +var_dump(foo(...[1, 2], b: 20)); // Fatal error. Named parameter $b overwrites previous argument diff --git a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php index 28b0091af9..adcb69cdbe 100644 --- a/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ConsistentConstructorRuleTest.php @@ -12,7 +12,10 @@ class ConsistentConstructorRuleTest extends RuleTestCase protected function getRule(): Rule { - return new ConsistentConstructorRule(self::getContainer()->getByType(MethodParameterComparisonHelper::class)); + return new ConsistentConstructorRule( + self::getContainer()->getByType(MethodParameterComparisonHelper::class), + self::getContainer()->getByType(MethodVisibilityComparisonHelper::class), + ); } public function testRule(): void @@ -42,4 +45,14 @@ public function testRuleNoErrors(): void $this->analyse([__DIR__ . '/data/consistent-constructor-no-errors.php'], []); } + public function testBug12137(): void + { + $this->analyse([__DIR__ . '/data/bug-12137.php'], [ + [ + 'Private method Bug12137\ChildClass::__construct() overriding protected method Bug12137\ParentClass::__construct() should be protected or public.', + 20, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php index 5f75cd0554..580d21787c 100644 --- a/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php +++ b/tests/PHPStan/Rules/Methods/MethodSignatureRuleTest.php @@ -29,6 +29,7 @@ protected function getRule(): Rule new MethodSignatureRule($phpClassReflectionExtension, $this->reportMaybes, $this->reportStatic), true, new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), $phpClassReflectionExtension, false, ); diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index abbe2927ab..9c65a99d35 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -31,6 +31,7 @@ protected function getRule(): Rule new MethodSignatureRule($phpClassReflectionExtension, true, true), false, new MethodParameterComparisonHelper($phpVersion), + new MethodVisibilityComparisonHelper(), $phpClassReflectionExtension, $this->checkMissingOverrideMethodAttribute, ); diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index c524382174..c88200e418 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -877,6 +877,16 @@ public function testBug8573(): void $this->analyse([__DIR__ . '/data/bug-8573.php'], []); } + public function testBug8632(): void + { + $this->analyse([__DIR__ . '/data/bug-8632.php'], []); + } + + public function testBug7857(): void + { + $this->analyse([__DIR__ . '/data/bug-7857.php'], []); + } + public function testBug8879(): void { $this->analyse([__DIR__ . '/data/bug-8879.php'], []); @@ -1138,4 +1148,83 @@ public function testPropertyHooks(): void ]); } + public function testShortGetPropertyHook(): void + { + if (PHP_VERSION_ID < 80400) { + $this->markTestSkipped('Test requires PHP 8.4.'); + } + + $this->analyse([__DIR__ . '/data/short-get-property-hook-return.php'], [ + [ + 'Get hook for property ShortGetPropertyHookReturn\Foo::$i should return int but returns string.', + 9, + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\Foo::$s should return non-empty-string but returns \'\'.', + 18, + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$a should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', + 36, + 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$b should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', + 50, + 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + [ + 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$c should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', + 59, + 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', + ], + ]); + } + + public function testBug1O580(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $this->analyse([__DIR__ . '/data/bug-10580.php'], [ + [ + 'Method Bug10580\FooA::fooThisInterface() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 18, + ], + [ + 'Method Bug10580\FooA::fooThisClass() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 19, + ], + [ + 'Method Bug10580\FooA::fooThisSelf() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 20, + ], + [ + 'Method Bug10580\FooA::fooThisStatic() should return $this(Bug10580\FooA) but returns Bug10580\FooA.', + 21, + ], + [ + 'Method Bug10580\FooB::fooThisInterface() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 27, + ], + [ + 'Method Bug10580\FooB::fooThisClass() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 29, + ], + [ + 'Method Bug10580\FooB::fooThisSelf() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 31, + ], + [ + 'Method Bug10580\FooB::fooThisStatic() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 33, + ], + [ + 'Method Bug10580\FooB::fooThis() should return $this(Bug10580\FooB) but returns Bug10580\FooB.', + 35, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-10580.php b/tests/PHPStan/Rules/Methods/data/bug-10580.php new file mode 100644 index 0000000000..b9c479000a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-10580.php @@ -0,0 +1,36 @@ += 8.0 + +namespace Bug10580; + +interface FooI { + /** @return $this */ + public function fooThisInterface(): FooI; + /** @return $this */ + public function fooThisClass(): FooI; + /** @return $this */ + public function fooThisSelf(): self; + /** @return $this */ + public function fooThisStatic(): static; +} + +final class FooA implements FooI +{ + public function fooThisInterface(): FooI { return new FooA(); } + public function fooThisClass(): FooA { return new FooA(); } + public function fooThisSelf(): self { return new FooA(); } + public function fooThisStatic(): static { return new FooA(); } +} + +final class FooB implements FooI +{ + /** @return $this */ + public function fooThisInterface(): FooI { return new FooB(); } + /** @return $this */ + public function fooThisClass(): FooB { return new FooB(); } + /** @return $this */ + public function fooThisSelf(): self { return new FooB(); } + /** @return $this */ + public function fooThisStatic(): static { return new FooB(); } + /** @return $this */ + public function fooThis(): static { return new FooB(); } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-12137.php b/tests/PHPStan/Rules/Methods/data/bug-12137.php new file mode 100755 index 0000000000..eacb78bbf3 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12137.php @@ -0,0 +1,23 @@ + $page], + $perPage !== null ? ['perPage' => $perPage] : [] + ); + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-8632.php b/tests/PHPStan/Rules/Methods/data/bug-8632.php new file mode 100644 index 0000000000..17c2aa50f6 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-8632.php @@ -0,0 +1,26 @@ + 1, + 'categories' => ['news'], + ]; + } else { + $arr = []; + } + + return array_merge($arr, []); + } +} diff --git a/tests/PHPStan/Rules/Properties/data/short-get-property-hook-return.php b/tests/PHPStan/Rules/Methods/data/short-get-property-hook-return.php similarity index 100% rename from tests/PHPStan/Rules/Properties/data/short-get-property-hook-return.php rename to tests/PHPStan/Rules/Methods/data/short-get-property-hook-return.php diff --git a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php index 48258202dc..ff465ea7e5 100644 --- a/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php @@ -4,6 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -95,4 +96,13 @@ public function testBug7310(): void $this->analyse([__DIR__ . '/data/bug-7310.php'], []); } + public function testBug11939(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1.'); + } + + $this->analyse([__DIR__ . '/data/bug-11939.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-11939.php b/tests/PHPStan/Rules/PhpDoc/data/bug-11939.php new file mode 100644 index 0000000000..759c3b5bb5 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-11939.php @@ -0,0 +1,36 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug11939; + +enum What +{ + case This; + case That; + + /** + * @return ($this is self::This ? 'here' : 'there') + */ + public function where(): string + { + return match ($this) { + self::This => 'here', + self::That => 'there' + }; + } +} + +class Where +{ + /** + * @return ($what is What::This ? 'here' : 'there') + */ + public function __invoke(What $what): string + { + return match ($what) { + What::This => 'here', + What::That => 'there' + }; + } +} diff --git a/tests/PHPStan/Rules/Playground/PromoteParameterRuleTest.php b/tests/PHPStan/Rules/Playground/PromoteParameterRuleTest.php new file mode 100644 index 0000000000..e97673ff5f --- /dev/null +++ b/tests/PHPStan/Rules/Playground/PromoteParameterRuleTest.php @@ -0,0 +1,41 @@ +> + */ +class PromoteParameterRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PromoteParameterRule( + new UninitializedPropertyRule(new ConstructorsHelper( + self::getContainer(), + [], + )), + ClassPropertiesNode::class, + false, + 'checkUninitializedProperties', + ); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/promote-parameter.php'], [ + [ + 'Class PromoteParameter\Foo has an uninitialized property $test. Give it default value or assign it in the constructor.', + 8, + 'This error would be reported if the checkUninitializedProperties: true parameter was enabled in your %configurationFile%.', + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/StaticVarWithoutTypeRuleTest.php b/tests/PHPStan/Rules/Playground/StaticVarWithoutTypeRuleTest.php new file mode 100644 index 0000000000..4ef6334828 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/StaticVarWithoutTypeRuleTest.php @@ -0,0 +1,34 @@ + + */ +class StaticVarWithoutTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new StaticVarWithoutTypeRule(self::getContainer()->getByType(FileTypeMapper::class)); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/static-var-without-type.php'], [ + [ + 'Static variable needs to be typed with PHPDoc @var tag.', + 23, + ], + [ + 'Static variable needs to be typed with PHPDoc @var tag.', + 28, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Playground/data/promote-parameter.php b/tests/PHPStan/Rules/Playground/data/promote-parameter.php new file mode 100644 index 0000000000..da1ea8ad08 --- /dev/null +++ b/tests/PHPStan/Rules/Playground/data/promote-parameter.php @@ -0,0 +1,10 @@ +analyse([__DIR__ . '/data/property-assign-ref.php'], [ [ - 'Property PropertyAssignRef\Foo::$foo with private(set) visibility is assigned by reference.', + 'Property PropertyAssignRef\Foo::$foo with private visibility is assigned by reference.', 25, ], [ diff --git a/tests/PHPStan/Rules/Properties/ShortGetPropertyHookReturnTypeRuleTest.php b/tests/PHPStan/Rules/Properties/ShortGetPropertyHookReturnTypeRuleTest.php deleted file mode 100644 index 8a318db7ed..0000000000 --- a/tests/PHPStan/Rules/Properties/ShortGetPropertyHookReturnTypeRuleTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -final class ShortGetPropertyHookReturnTypeRuleTest extends RuleTestCase -{ - - protected function getRule(): Rule - { - return new ShortGetPropertyHookReturnTypeRule( - new FunctionReturnTypeCheck(new RuleLevelHelper($this->createReflectionProvider(), true, false, true, true, false, false)), - ); - } - - public function testRule(): void - { - if (PHP_VERSION_ID < 80400) { - $this->markTestSkipped('Test requires PHP 8.4.'); - } - - $this->analyse([__DIR__ . '/data/short-get-property-hook-return.php'], [ - [ - 'Get hook for property ShortGetPropertyHookReturn\Foo::$i should return int but returns string.', - 9, - ], - [ - 'Get hook for property ShortGetPropertyHookReturn\Foo::$s should return non-empty-string but returns \'\'.', - 18, - ], - [ - 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$a should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', - 36, - 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', - ], - [ - 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$b should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', - 50, - 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', - ], - [ - 'Get hook for property ShortGetPropertyHookReturn\GenericFoo::$c should return T of ShortGetPropertyHookReturn\Foo but returns ShortGetPropertyHookReturn\Foo.', - 59, - 'Type ShortGetPropertyHookReturn\Foo is not always the same as T. It breaks the contract for some argument types, typically subtypes.', - ], - ]); - } - -} diff --git a/tests/PHPStan/Rules/Properties/data/get-non-virtual-property-hook-read.php b/tests/PHPStan/Rules/Properties/data/get-non-virtual-property-hook-read.php index 077792c406..76ceabe408 100644 --- a/tests/PHPStan/Rules/Properties/data/get-non-virtual-property-hook-read.php +++ b/tests/PHPStan/Rules/Properties/data/get-non-virtual-property-hook-read.php @@ -46,3 +46,12 @@ class Foo } } + +class GetHookIsNotPresentAtAll +{ + public int $i { + set { + $this->i = $value + 10; + } + } +} diff --git a/tests/PHPStan/Rules/Properties/data/set-non-virtual-property-hook-assign.php b/tests/PHPStan/Rules/Properties/data/set-non-virtual-property-hook-assign.php index ffc0e559f6..56133fb556 100644 --- a/tests/PHPStan/Rules/Properties/data/set-non-virtual-property-hook-assign.php +++ b/tests/PHPStan/Rules/Properties/data/set-non-virtual-property-hook-assign.php @@ -65,4 +65,11 @@ class Foo } } + public int $k5 { + get { + return $this->k4 + 1; + } + set => $value; // short body always assigns + } + } diff --git a/tests/PHPStan/Rules/Properties/data/write-asymmetric-visibility.php b/tests/PHPStan/Rules/Properties/data/write-asymmetric-visibility.php index 6ea2ab154b..a6273caf09 100644 --- a/tests/PHPStan/Rules/Properties/data/write-asymmetric-visibility.php +++ b/tests/PHPStan/Rules/Properties/data/write-asymmetric-visibility.php @@ -71,3 +71,14 @@ function (ReadonlyProps $foo): void { $foo->b = 1; $foo->c = 1; }; + +class ArrayProp +{ + + public private(set) array $a = []; + +} + +function (ArrayProp $foo): void { + $foo->a[] = 1; +}; diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index ba60c13ce9..64ee811d81 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -194,12 +194,10 @@ public function dataBug11207(): array ]; } - public function testBug12224(): void + public function testBug12048(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-12224.php'], [ - ['Method PHPStan\Rules\Pure\data\A::pureWithThrowsVoid() is marked as pure but returns void.', 47], - ]); + $this->analyse([__DIR__ . '/data/bug-12048.php'], []); } } diff --git a/tests/PHPStan/Rules/Pure/data/bug-12048.php b/tests/PHPStan/Rules/Pure/data/bug-12048.php new file mode 100644 index 0000000000..ab5d44154a --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-12048.php @@ -0,0 +1,19 @@ +