Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace error-prone instanceof in Rules classes #3858

Open
wants to merge 22 commits into
base: 2.1.x
Choose a base branch
from

Conversation

zonuexe
Copy link
Contributor

@zonuexe zonuexe commented Mar 6, 2025

Reduce usage of $type instanceof *Type pattern in Rules classes.

refs phpstan/phpstan#8311

&& !$isFirstDeclaration
!$isFirstDeclaration
&& !$method->isPrivate()
&& ($returnType->isNull()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logical operators are short-circuit evaluated, so rearranging expressions can lead to micro-optimizations.

@zonuexe zonuexe marked this pull request as draft March 6, 2025 16:34
Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to replace these calls by just accessing the first element. Union types should be properly handled. Multiple values should be iterated over.

@zonuexe zonuexe force-pushed the refactor/error-prone-instanceof-type branch from bbbe15f to 776e845 Compare March 6, 2025 16:46
@zonuexe
Copy link
Contributor Author

zonuexe commented Mar 6, 2025

Since ConstantArrayType and ConstantStringType each return return [$this]; from the getConstant*() methods, I thought it would be effective to simply replace them with count() === 0 or count() === 1 to minimize the change difference, but I also prefer to replace it with foreach instead of $types[0], so I will change my approach and fix it that way.

@zonuexe zonuexe force-pushed the refactor/error-prone-instanceof-type branch 7 times, most recently from 9fc4e20 to c57fe20 Compare March 7, 2025 18:00
$class = $type->getClassName();
$referencedClassReflection = $type->getClassReflection();
$classNames = $type->getObjectClassNames();
sort($classNames);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorting this array because tests seem to fail randomly.

@zonuexe
Copy link
Contributor Author

zonuexe commented Mar 7, 2025

MissingTypehintCheck was reverted in this branch because the literal substitution seemed to change its behavior. a408a58

@zonuexe zonuexe marked this pull request as ready for review March 7, 2025 18:50
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

$errors[] = RuleErrorBuilder::message(sprintf('PHPDoc tag @phpstan-require-extends contains non-object type %s.', $type->describe(VerbosityLevel::typeOnly())))
->identifier('requireExtends.nonObject')
->build();
continue;
}

$class = $type->getClassName();
$referencedClassReflection = $type->getClassReflection();
$classNames = $type->getObjectClassNames();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduce this var earlier so you don't need to call getObjectClassNames twice (methods called on Type can get expensive)

@@ -49,34 +51,38 @@ public function processNode(Node $node, Scope $scope): array
$errors = [];
foreach ($implementsTags as $implementsTag) {
$type = $implementsTag->getType();
if (!$type instanceof ObjectType) {
if (count($type->getObjectClassNames()) === 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduce a var so you don't need to call getObjectClassNames twice

*/
private function findConstantStrings(Type $type): array
{
if ($type instanceof ConstantStringType) {
return [$type];
if (count($type->getConstantStrings()) > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduce a var so you don't need to call getConstantStrings twice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

$arrayKeyValue = $keyType->toArrayKey();
if ($arrayKeyValue instanceof ConstantIntegerType) {
$arrayKeyValues = $keyType->toArrayKey()->getConstantScalarValues();
if (count($arrayKeyValues) === 1 && is_int($arrayKeyValues[0])) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it better/possible to handle union of scalar values here (?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the following?

modified   src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php
@@ -71,10 +71,12 @@ final class DuplicateKeysInLiteralArraysRule implements Rule
 			} else {
 				$keyType = $itemNode->getScope()->getType($key);
 				$arrayKeyValues = $keyType->toArrayKey()->getConstantScalarValues();
-				if (count($arrayKeyValues) === 1 && is_int($arrayKeyValues[0])) {
-					$autoGeneratedIndex = $autoGeneratedIndex === null
-						? $arrayKeyValues[0]
-						: max($autoGeneratedIndex, $arrayKeyValues[0]);
+				foreach ($arrayKeyValues as $arrayKeyValue) {
+					if (is_int($arrayKeyValue)) {
+						$autoGeneratedIndex = $autoGeneratedIndex === null
+							? $arrayKeyValue
+							: max($autoGeneratedIndex, $arrayKeyValue);
+					}
 				}
 			}
Or is it type-level arithmetic?
modified   src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php
@@ -42,7 +42,6 @@ final class DuplicateKeysInLiteralArraysRule implements Rule
 		$valueLines = [];
 
 		/**
-		 * @var int|false|null $autoGeneratedIndex
 		 * - An int value represent the biggest integer used as array key.
 		 *   When no key is provided this value + 1 will be used.
 		 * - Null is used as initializer instead of 0 to avoid issue with negative keys.
@@ -63,27 +62,37 @@ final class DuplicateKeysInLiteralArraysRule implements Rule
 				}
 
 				if ($autoGeneratedIndex === null) {
-					$autoGeneratedIndex = 0;
-					$keyType = new ConstantIntegerType(0);
+					$autoGeneratedIndex = new ConstantIntegerType(0);
 				} else {
-					$keyType = new ConstantIntegerType(++$autoGeneratedIndex);
+					$autoGeneratedIndex = $scope->getType(new Plus(
+						new Int_(1),
+						new TypeExpr($autoGeneratedIndex),
+					));
 				}
+				$keyType = $autoGeneratedIndex;
 			} else {
 				$keyType = $itemNode->getScope()->getType($key);
-				$arrayKeyValues = $keyType->toArrayKey()->getConstantScalarValues();
-				if (count($arrayKeyValues) === 1 && is_int($arrayKeyValues[0])) {
+				$arrayKeyType = $keyType->toArrayKey();
+				if ($autoGeneratedIndex !== false && $arrayKeyType->isInteger()->yes()) {
 					$autoGeneratedIndex = $autoGeneratedIndex === null
-						? $arrayKeyValues[0]
-						: max($autoGeneratedIndex, $arrayKeyValues[0]);
+						? $keyType
+						: $scope->getType(new FuncCall(
+							new Name('\max'),
+							[
+								new Arg(new TypeExpr($autoGeneratedIndex)),
+								new Arg(new TypeExpr($arrayKeyType)),
+							]
+						));
 				}
 			}

$autoGeneratedIndex is explained as follows:

		/**
		 * - An int value represent the biggest integer used as array key.
		 *   When no key is provided this value + 1 will be used.
		 * - Null is used as initializer instead of 0 to avoid issue with negative keys.
		 * - False means a non-scalar value was encountered and we cannot be sure of the next keys.
		 */
		$autoGeneratedIndex = null;

Since the key value actually changes depending on the input value, more appropriate typing is possible with type operations. However, since DuplicateKeysInLiteralArraysRule has some subtle behavior, I would like to keep the changes in this branch to a minimum.

@@ -57,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
return $ruleErrorBuilder->treatPhpDocTypesAsCertainTip();
};

if (!$nodeType->getValue()) {
if (in_array(false, $nodeType->getConstantScalarValues(), true)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure but isn't $nodeType->isFalse()->yes() better ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct, thanks!

$assertValue = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->toBoolean();
if (!$assertValue instanceof ConstantBooleanType) {
$assertValues = ($this->treatPhpDocTypesAsCertain ? $scope->getType($arg) : $scope->getNativeType($arg))->getConstantScalarValues();
if (count($assertValues) !== 1 || !is_bool($assertValues[0])) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could maybe have kept the toBoolean and checked

$assertValue->isTrue()->yes() || $assertValue->isFalse()->yes()

@zonuexe zonuexe marked this pull request as draft March 11, 2025 16:26
@zonuexe zonuexe force-pushed the refactor/error-prone-instanceof-type branch from 83b0029 to b031d1d Compare March 11, 2025 21:03
zonuexe and others added 2 commits March 12, 2025 06:12
Co-authored-by: Vincent Langlet <[email protected]>
@zonuexe zonuexe force-pushed the refactor/error-prone-instanceof-type branch from b031d1d to 5d96286 Compare March 11, 2025 21:12
@zonuexe zonuexe marked this pull request as ready for review March 11, 2025 22:58
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

Comment on lines +76 to +77
/** @var bool */
return $assertValue->getConstantScalarValues()[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can return $assertValue->isTrue()->yes() if you want to avoid the var phpdoc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants