diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4deacb787e..d73a9b7984 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -183,148 +183,10 @@ public function specifyTypesInCondition( $exprNode = $expressions[0]; /** @var ConstantScalarType $constantType */ $constantType = $expressions[1]; - if (!$context->null() && $constantType->getValue() === false) { - $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - return $types->unionWith($this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(), - $rootExpr, - )); - } - if (!$context->null() && $constantType->getValue() === true) { - $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - return $types->unionWith($this->specifyTypesInCondition( - $scope, - $exprNode, - $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(), - $rootExpr, - )); - } - - if ($constantType->getValue() === null) { - return $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - } - - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) - && $constantType instanceof ConstantIntegerType - ) { - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType->isArray()->yes()) { - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr); - return $funcTypes->unionWith($valueTypes); - } - } - } - - if ( - !$context->null() - && $exprNode instanceof FuncCall - && count($exprNode->getArgs()) === 1 - && $exprNode->name instanceof Name - && strtolower((string) $exprNode->name) === 'strlen' - && $constantType instanceof ConstantIntegerType - ) { - if ($context->truthy() || $constantType->getValue() === 0) { - $newContext = $context; - if ($constantType->getValue() === 0) { - $newContext = $newContext->negate(); - } - $argType = $scope->getType($exprNode->getArgs()[0]->value); - if ($argType->isString()->yes()) { - $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); - - $accessory = new AccessoryNonEmptyStringType(); - if ($constantType->getValue() >= 2) { - $accessory = new AccessoryNonFalsyStringType(); - } - $valueTypes = $this->create($exprNode->getArgs()[0]->value, $accessory, $newContext, false, $scope, $rootExpr); - - return $funcTypes->unionWith($valueTypes); - } - } - } - - if ( - $context->truthy() - && $exprNode instanceof FuncCall - && $exprNode->name instanceof Name - && in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords'], true) - && isset($exprNode->getArgs()[0]) - && $constantType instanceof ConstantStringType - && $constantType->getValue() !== '' - ) { - $argType = $scope->getType($exprNode->getArgs()[0]->value); - - if ($argType->isString()->yes()) { - if ($constantType->getValue() !== '0') { - return $this->create( - $exprNode->getArgs()[0]->value, - TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()), - $context, - false, - $scope, - ); - } - - return $this->create( - $exprNode->getArgs()[0]->value, - TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()), - $context, - false, - $scope, - ); - } - } - - if ( - $exprNode instanceof FuncCall - && $exprNode->name instanceof Name - && strtolower($exprNode->name->toString()) === 'gettype' - && isset($exprNode->getArgs()[0]) - && $constantType instanceof ConstantStringType - ) { - $type = null; - if ($constantType->getValue() === 'string') { - $type = new StringType(); - } - if ($constantType->getValue() === 'array') { - $type = new ArrayType(new MixedType(), new MixedType()); - } - if ($constantType->getValue() === 'boolean') { - $type = new BooleanType(); - } - if ($constantType->getValue() === 'resource' || $constantType->getValue() === 'resource (closed)') { - $type = new ResourceType(); - } - if ($constantType->getValue() === 'integer') { - $type = new IntegerType(); - } - if ($constantType->getValue() === 'double') { - $type = new FloatType(); - } - if ($constantType->getValue() === 'NULL') { - $type = new NullType(); - } - if ($constantType->getValue() === 'object') { - $type = new ObjectWithoutClassType(); - } - - if ($type !== null) { - return $this->create($exprNode->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr); - } + $specifiedType = $this->specifyTypesForConstantBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr); + if ($specifiedType !== null) { + return $specifiedType; } } @@ -1034,6 +896,215 @@ public function specifyTypesInCondition( return new SpecifiedTypes([], [], false, [], $rootExpr); } + private function specifyTypesForConstantBinaryExpression( + Expr $exprNode, + ConstantScalarType $constantType, + TypeSpecifierContext $context, + Scope $scope, + ?Expr $rootExpr, + ): ?SpecifiedTypes + { + if (!$context->null() && $constantType->getValue() === false) { + $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + return $types->unionWith($this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createFalse() : TypeSpecifierContext::createFalse()->negate(), + $rootExpr, + )); + } + + if (!$context->null() && $constantType->getValue() === true) { + $types = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + return $types->unionWith($this->specifyTypesInCondition( + $scope, + $exprNode, + $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(), + $rootExpr, + )); + } + + if ($constantType->getValue() === null) { + return $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + } + + if ( + !$context->null() + && $exprNode instanceof FuncCall + && count($exprNode->getArgs()) === 1 + && $exprNode->name instanceof Name + && in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true) + && $constantType instanceof ConstantIntegerType + ) { + if ($context->truthy() || $constantType->getValue() === 0) { + $newContext = $context; + if ($constantType->getValue() === 0) { + $newContext = $newContext->negate(); + } + $argType = $scope->getType($exprNode->getArgs()[0]->value); + if ($argType->isArray()->yes()) { + $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + $valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr); + return $funcTypes->unionWith($valueTypes); + } + } + } + + if ( + !$context->null() + && $exprNode instanceof FuncCall + && count($exprNode->getArgs()) === 1 + && $exprNode->name instanceof Name + && strtolower((string) $exprNode->name) === 'strlen' + && $constantType instanceof ConstantIntegerType + ) { + if ($context->truthy() || $constantType->getValue() === 0) { + $newContext = $context; + if ($constantType->getValue() === 0) { + $newContext = $newContext->negate(); + } + $argType = $scope->getType($exprNode->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr); + + $accessory = new AccessoryNonEmptyStringType(); + if ($constantType->getValue() >= 2) { + $accessory = new AccessoryNonFalsyStringType(); + } + $valueTypes = $this->create($exprNode->getArgs()[0]->value, $accessory, $newContext, false, $scope, $rootExpr); + + return $funcTypes->unionWith($valueTypes); + } + } + + } + + if ($constantType instanceof ConstantStringType) { + return $this->specifyTypesForConstantStringBinaryExpression($exprNode, $constantType, $context, $scope, $rootExpr); + } + + return null; + } + + private function specifyTypesForConstantStringBinaryExpression( + Expr $exprNode, + ConstantStringType $constantType, + TypeSpecifierContext $context, + Scope $scope, + ?Expr $rootExpr, + ): ?SpecifiedTypes + { + if ( + $context->truthy() + && $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && in_array(strtolower($exprNode->name->toString()), ['substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'mb_strtolower', 'mb_strtoupper', 'ucfirst', 'lcfirst', 'ucwords'], true) + && isset($exprNode->getArgs()[0]) + && $constantType->getValue() !== '' + ) { + $argType = $scope->getType($exprNode->getArgs()[0]->value); + + if ($argType->isString()->yes()) { + if ($constantType->getValue() !== '0') { + return $this->create( + $exprNode->getArgs()[0]->value, + TypeCombinator::intersect($argType, new AccessoryNonFalsyStringType()), + $context, + false, + $scope, + ); + } + + return $this->create( + $exprNode->getArgs()[0]->value, + TypeCombinator::intersect($argType, new AccessoryNonEmptyStringType()), + $context, + false, + $scope, + ); + } + } + + if ( + $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && strtolower($exprNode->name->toString()) === 'gettype' + && isset($exprNode->getArgs()[0]) + ) { + $type = null; + if ($constantType->getValue() === 'string') { + $type = new StringType(); + } + if ($constantType->getValue() === 'array') { + $type = new ArrayType(new MixedType(), new MixedType()); + } + if ($constantType->getValue() === 'boolean') { + $type = new BooleanType(); + } + if ($constantType->getValue() === 'resource' || $constantType->getValue() === 'resource (closed)') { + $type = new ResourceType(); + } + if ($constantType->getValue() === 'integer') { + $type = new IntegerType(); + } + if ($constantType->getValue() === 'double') { + $type = new FloatType(); + } + if ($constantType->getValue() === 'NULL') { + $type = new NullType(); + } + if ($constantType->getValue() === 'object') { + $type = new ObjectWithoutClassType(); + } + + if ($type !== null) { + return $this->create($exprNode->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr); + } + } + + if ( + $context->true() + && $exprNode instanceof FuncCall + && $exprNode->name instanceof Name + && strtolower((string) $exprNode->name) === 'get_parent_class' + && isset($exprNode->getArgs()[0]) + ) { + $argType = $scope->getType($exprNode->getArgs()[0]->value); + $objectType = new ObjectType($constantType->getValue()); + $classStringType = new GenericClassStringType($objectType); + + if ($argType->isString()->yes()) { + return $this->create( + $exprNode->getArgs()[0]->value, + $classStringType, + $context, + false, + $scope, + ); + } + + if ((new ObjectWithoutClassType())->isSuperTypeOf($argType)->yes()) { + return $this->create( + $exprNode->getArgs()[0]->value, + $objectType, + $context, + false, + $scope, + ); + } + + return $this->create( + $exprNode->getArgs()[0]->value, + TypeCombinator::union($objectType, $classStringType), + $context, + false, + $scope, + ); + } + + return null; + } + private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, ?Expr $rootExpr, Expr $expr, Scope $scope): SpecifiedTypes { if ($context->null()) { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index b7f0425f68..ce44abdc56 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1050,6 +1050,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/array-offset-unset.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8008.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5552.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-5552.php b/tests/PHPStan/Analyser/data/bug-5552.php new file mode 100644 index 0000000000..4b05d1b053 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5552.php @@ -0,0 +1,55 @@ +', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (A::class === get_parent_class($mixed)) { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (get_parent_class($mixed) === 'Bug5552\A') { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if ('Bug5552\A' === get_parent_class($mixed)) { + assertType('Bug5552\A|class-string', $mixed); + } else { + assertType('mixed', $mixed); + } + assertType('mixed', $mixed); + + if (get_parent_class($o) === A::class) { + assertType('Bug5552\A', $o); + } + if (A::class === get_parent_class($o)) { + assertType('Bug5552\A', $o); + } + + if (get_parent_class($s) === A::class) { + assertType('class-string', $s); + } + if (A::class === get_parent_class($s)) { + assertType('class-string', $s); + } + } +} + +class A {}