Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ parameters:
path: ../src/Diagnose/PHPStanDiagnoseExtension.php
- identifier: ternary.shortNotAllowed
-
# operators take the configured PHP version, like InitializerExprTypeResolver does;
# operators and loose comparisons take the configured PHP version, like InitializerExprTypeResolver does;
# not ignored inline because PHP 7.4 reads the class attribute as a comment,
# so the error is on a different line there
identifier: phpstan.phpVersionInjection
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/BinaryOpHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -891,7 +891,7 @@ private function resolveEqualType(MutatingScope $scope, BinaryOp\Equal $expr, Ex
$leftType = $leftResult->getTypeOnScope($scope, $scope->nativeTypesPromoted);
$rightType = $rightResult->getTypeOnScope($scope, $scope->nativeTypesPromoted);

return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type;
return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType, $scope->getPhpVersion())->type;
}

private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
Expand Down
54 changes: 41 additions & 13 deletions src/Analyser/ExprHandler/CastHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FloatType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

/**
* @implements ExprHandler<Cast>
Expand Down Expand Up @@ -86,25 +95,44 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
// a cast's truthiness is a loose comparison of the inner
// expression - composed from its result; the fabricated
// literal is only printed into entries, never walked
if (($expr instanceof Cast\Bool_ || $expr instanceof Cast\Int_ || $expr instanceof Cast\Double) && !$context->null()) {
if ($expr instanceof Cast\Bool_) {
$literal = new ConstFetch(new FullyQualified('true'));
$equalContext = $context;
} elseif ($expr instanceof Cast\Int_) {
$literal = new Int_(0);
$equalContext = $context->negate();
} else {
$literal = new Float_(0.0);
$equalContext = $context->negate();
}

if ($expr instanceof Cast\Bool_ && !$context->null()) {
// the literal side never reads its stand-in result
$types = $this->identicalNarrowingHelper->specifyEqual($nodeScopeResolver, $expr->expr, $literal, $exprResult, $exprResult, $equalContext, $evaluationScope, $subjectArgResult, null);
$types = $this->identicalNarrowingHelper->specifyEqual($nodeScopeResolver, $expr->expr, new ConstFetch(new FullyQualified('true')), $exprResult, $exprResult, $context, $evaluationScope, $subjectArgResult, null);
if ($types !== null) {
return $types->setRootExpr($expr);
}
}

// a number cast is zero for more values than those loosely equal
// to zero: (int) 'abc', (int) 0.5 and (int) INF are 0 too
if (($expr instanceof Cast\Int_ || $expr instanceof Cast\Double) && !$context->null()) {
$zeroContext = $context->negate();
if ($zeroContext->true()) {
$zeroType = TypeCombinator::union(
new NullType(),
new ConstantBooleanType(false),
new ConstantIntegerType(0),
$expr instanceof Cast\Int_ ? new FloatType() : new ConstantFloatType(0.0),
new StringType(),
new ConstantArrayType([], []),
// GMP casts to its number
new ObjectWithoutClassType(),
);
} else {
$zeroType = TypeCombinator::union(
new NullType(),
new ConstantBooleanType(false),
new ConstantIntegerType(0),
new ConstantFloatType(0.0),
new ConstantStringType(''),
new ConstantStringType('0'),
new ConstantArrayType([], []),
);
}

return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $expr->expr, $exprResult, $zeroType, $zeroContext)->setRootExpr($expr);
}

if ($expr instanceof Cast\Bool_) {
return $evaluationScope->obtainResultForNode(new Equal($expr->expr, new ConstFetch(new FullyQualified('true'))))->getSpecifiedTypes($context, $nativeTypesPromoted)->setRootExpr($expr);
}
Expand Down
47 changes: 42 additions & 5 deletions src/Analyser/ExprHandler/CastStringHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
namespace PHPStan\Analyser\ExprHandler;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\NotEqual;
use PhpParser\Node\Expr\Cast;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
use PHPStan\Analyser\ExpressionContext;
use PHPStan\Analyser\ExpressionResult;
use PHPStan\Analyser\ExpressionResultFactory;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\ExprHandler;
use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper;
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
Expand All @@ -23,7 +22,14 @@
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\ShouldNotHappenException;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_merge;

/**
Expand All @@ -38,6 +44,7 @@ public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
private ImplicitToStringCallHelper $implicitToStringCallHelper,
private ExpressionResultFactory $expressionResultFactory,
private DefaultNarrowingHelper $defaultNarrowingHelper,
)
{
}
Expand Down Expand Up @@ -76,9 +83,39 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex

throw new ShouldNotHappenException();
}),
specifyTypesCallback: static fn (TypeSpecifierContext $context, bool $nativeTypesPromoted): SpecifiedTypes => ($nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope)->obtainResultForNode(
new NotEqual($expr->expr, new String_('')),
)->getSpecifiedTypes($context, $nativeTypesPromoted)->setRootExpr($expr),
specifyTypesCallback: function (TypeSpecifierContext $context, bool $nativeTypesPromoted) use ($expr, $exprResult, $beforeScope): SpecifiedTypes {
if ($context->null()) {
return new SpecifiedTypes([], []);
}

// the cast is falsy when it is '' or '0', not only for the values loosely equal to ''
$falsyContext = $context->negate();
if ($falsyContext->true()) {
$falsyType = TypeCombinator::union(
new NullType(),
new ConstantBooleanType(false),
new ConstantIntegerType(0),
// -0.0 casts to '-0'
new ConstantFloatType(0.0),
new ConstantStringType(''),
new ConstantStringType('0'),
// a Stringable object casts to its string
new ObjectWithoutClassType(),
);
} else {
$falsyType = TypeCombinator::union(
new NullType(),
new ConstantBooleanType(false),
new ConstantIntegerType(0),
new ConstantStringType(''),
new ConstantStringType('0'),
);
}

$evaluationScope = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope;

return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $expr->expr, $exprResult, $falsyType, $falsyContext)->setRootExpr($expr);
},
dependencies: $exprResult->getDependencies(),
);
}
Expand Down
64 changes: 36 additions & 28 deletions src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\AlwaysRememberedExpr;
use PHPStan\Node\Printer\ExprPrinter;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Turbo\ShadowedByTurboExtension;
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
Expand All @@ -35,6 +36,7 @@
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\LooseComparisonHelper;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
Expand Down Expand Up @@ -581,12 +583,12 @@ public function specifyEqual(
$leftScalarValues = $leftType->getConstantScalarValues();
$rightScalarValues = $rightType->getConstantScalarValues();
if (count($leftScalarValues) === 1 && !$unwrappedRight instanceof Expr\ConstFetch) {
$constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $right, $rightResult, $leftScalarValues[0], $leftType, $rightType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback);
$constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $right, $rightResult, $leftScalarValues[0], $leftType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback);
if ($constantSideTypes !== false) {
return $constantSideTypes;
}
} elseif (count($rightScalarValues) === 1 && !$unwrappedLeft instanceof Expr\ConstFetch) {
$constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $left, $leftResult, $rightScalarValues[0], $rightType, $leftType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback);
$constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $left, $leftResult, $rightScalarValues[0], $rightType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback);
if ($constantSideTypes !== false) {
return $constantSideTypes;
}
Expand All @@ -612,9 +614,10 @@ public function specifyEqual(
return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $left, $leftResult, new NonEmptyArrayType(), $context->negate());
}

// same-type sides cannot coerce - loose equals strict
// same-type sides cannot coerce - loose equals strict, except for
// two numeric strings, which compare as numbers
if (
($leftType->isString()->yes() && $rightType->isString()->yes())
($leftType->isString()->yes() && $rightType->isString()->yes() && ($leftType->isNumericString()->no() || $rightType->isNumericString()->no()))
|| ($leftType->isInteger()->yes() && $rightType->isInteger()->yes())
|| ($leftType->isFloat()->yes() && $rightType->isFloat()->yes())
|| ($leftType->isEnum()->yes() && $rightType->isEnum()->yes())
Expand All @@ -628,14 +631,38 @@ public function specifyEqual(
if (!$left instanceof Expr\Variable || !$right instanceof Expr\Variable) {
return new SpecifiedTypes([], []);
}

// a variable equals itself unless it holds NAN
if ($context->false() && $leftType->isFloat()->no() && $leftType->isArray()->no()) {
return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $left, $leftResult, $leftType, $context);
}
}

$leftTypes = $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $left, $leftResult, $leftType, $context);
$rightTypes = $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $right, $rightResult, $rightType, $context);
// each side is one of the values the other side can equal, and in
// the false branch none of the values the other side always equals
$phpVersions = LooseComparisonHelper::getRepresentativePhpVersions($evaluationScope->getPhpVersion());

return $context->true()
? $leftTypes->unionWith($rightTypes)
: $this->defaultNarrowingHelper->toSureTypes($leftTypes, $evaluationScope)->intersectWith($this->defaultNarrowingHelper->toSureTypes($rightTypes, $evaluationScope));
return $this->createLooselyEqualTypes($evaluationScope, $left, $leftResult, $leftType, $rightType, $phpVersions, $context)
->unionWith($this->createLooselyEqualTypes($evaluationScope, $right, $rightResult, $rightType, $leftType, $phpVersions, $context));
}

/**
* @param non-empty-list<PhpVersion> $phpVersions
*/
private function createLooselyEqualTypes(MutatingScope $evaluationScope, Expr $subject, ExpressionResult $subjectResult, Type $subjectType, Type $otherType, array $phpVersions, TypeSpecifierContext $context): SpecifiedTypes
{
$type = $context->true()
? LooseComparisonHelper::getPossiblyLooselyEqualTypeInVersions($otherType, $phpVersions)
: LooseComparisonHelper::getAlwaysLooselyEqualTypeInVersions($otherType, $phpVersions);
// nothing to intersect with, or nothing to remove
if ($type->isSuperTypeOf(new MixedType())->yes() || ($context->false() && $type instanceof NeverType)) {
return new SpecifiedTypes([], []);
}
if ($context->true() && $subjectType instanceof MixedType && LooseComparisonHelper::keepsMixedUnnarrowed($type)) {
return new SpecifiedTypes([], []);
}

return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $subject, $subjectResult, $type, $context);
}

/**
Expand Down Expand Up @@ -738,7 +765,6 @@ private function specifyEqualAgainstConstantSide(
ExpressionResult $subjectResult,
mixed $value,
Type $constantType,
Type $otherType,
TypeSpecifierContext $context,
MutatingScope $evaluationScope,
?ExpressionResult $leftArgResult,
Expand Down Expand Up @@ -773,24 +799,6 @@ private function specifyEqualAgainstConstantSide(
);
}

/* There is a difference between php 7.x and 8.x on the equality
* behavior between zero and the empty string, so to be conservative
* we leave it untouched regardless of the language version */
if ($value === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) {
$trueTypes = $context->true()
? [new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new StringType()]
: [new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new ConstantStringType('0')];

return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $subject, $subjectResult, new UnionType($trueTypes), $context);
}
if ($value === '') {
$trueTypes = $context->true()
? [new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new ConstantStringType('')]
: [new NullType(), new ConstantBooleanType(false), new ConstantStringType('')];

return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $subject, $subjectResult, new UnionType($trueTypes), $context);
}

// loose equals strict for these call results and class names
if (
$unwrappedSubject instanceof Expr\FuncCall
Expand Down
27 changes: 24 additions & 3 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Php\PhpVersion;
use PHPStan\Php\PhpVersions;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
Expand Down Expand Up @@ -1164,7 +1165,7 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResul
/**
* @return TypeResult<BooleanType>
*/
public function resolveEqualType(Type $leftType, Type $rightType): TypeResult
public function resolveEqualType(Type $leftType, Type $rightType, ?PhpVersions $phpVersions = null): TypeResult
{
if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
return new TypeResult(new ConstantBooleanType(false), []);
Expand All @@ -1178,10 +1179,30 @@ public function resolveEqualType(Type $leftType, Type $rightType): TypeResult
}

if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) {
return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType));
return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType, $phpVersions));
}

return new TypeResult($leftType->looseCompare($rightType, $this->phpVersion), []);
$versions = $phpVersions !== null ? LooseComparisonHelper::getRepresentativePhpVersions($phpVersions) : [$this->phpVersion];
$result = LooseComparisonHelper::looseCompareInVersions($leftType, $rightType, $versions);
if ($result->isTrue()->yes() || $result->isFalse()->yes()) {
return new TypeResult($result, []);
}

if (
LooseComparisonHelper::getPossiblyLooselyEqualTypeInVersions($leftType, $versions)->isSuperTypeOf($rightType)->no()
|| LooseComparisonHelper::getPossiblyLooselyEqualTypeInVersions($rightType, $versions)->isSuperTypeOf($leftType)->no()
) {
return new TypeResult(new ConstantBooleanType(false), []);
}

if (
LooseComparisonHelper::getAlwaysLooselyEqualTypeInVersions($leftType, $versions)->isSuperTypeOf($rightType)->yes()
|| LooseComparisonHelper::getAlwaysLooselyEqualTypeInVersions($rightType, $versions)->isSuperTypeOf($leftType)->yes()
) {
return new TypeResult(new ConstantBooleanType(true), []);
}

return new TypeResult($result, []);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\Conditional;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Php\PhpVersions;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Rules\IdentifierRuleError;
use PHPStan\Rules\Rule;
Expand Down Expand Up @@ -94,9 +95,10 @@ public function processNode(Node $node, Scope $scope): array
/** @var array<string, array{Type, list<string>, bool}> $deadValueTypes */
$deadValueTypes = [];
$anyPossibleMatch = false;
$phpVersions = $scope->getPhpVersion();
foreach ($constantArrays as $constantArray) {
foreach ($constantArray->getValueTypes() as $valueType) {
$canNeverMatchReasons = $this->findCanNeverMatchReasons($needleType, $valueType, $isStrict);
$canNeverMatchReasons = $this->findCanNeverMatchReasons($needleType, $valueType, $isStrict, $phpVersions);
if ($canNeverMatchReasons === null) {
$anyPossibleMatch = true;
continue;
Expand Down Expand Up @@ -151,7 +153,7 @@ private function isAnotherValueOfNeedleType(Type $needleType, Type $valueType):
/**
* @return list<string>|null Reasons for the impossibility when the value can never match, null when it can match.
*/
private function findCanNeverMatchReasons(Type $needleType, Type $valueType, TrinaryLogic $isStrict): ?array
private function findCanNeverMatchReasons(Type $needleType, Type $valueType, TrinaryLogic $isStrict, PhpVersions $phpVersions): ?array
{
if ($isStrict->yes()) {
$result = $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType);
Expand All @@ -160,7 +162,7 @@ private function findCanNeverMatchReasons(Type $needleType, Type $valueType, Tri
}

if ($isStrict->no()) {
$result = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType);
$result = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType, $phpVersions);

return $result->type->isFalse()->yes() ? $result->reasons : null;
}
Expand All @@ -170,7 +172,7 @@ private function findCanNeverMatchReasons(Type $needleType, Type $valueType, Tri
return null;
}

$equalResult = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType);
$equalResult = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType, $phpVersions);
if (!$equalResult->type->isFalse()->yes()) {
return null;
}
Expand Down
Loading
Loading