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
12 changes: 6 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -163,22 +163,22 @@ cs-fix: cs-install
XDEBUG_MODE=off php build-cs/vendor/bin/phpcbf

phpstan:
php bin/phpstan clear-result-cache -q && php -d memory_limit=450M bin/phpstan -v
php bin/phpstan clear-result-cache -q && php -d memory_limit=512M bin/phpstan -v

phpstan-result-cache:
php -d memory_limit=450M bin/phpstan
php -d memory_limit=512M bin/phpstan

phpstan-fix:
php -d memory_limit=450M bin/phpstan --fix
php -d memory_limit=512M bin/phpstan --fix

phpstan-generate-baseline:
php -d memory_limit=450M bin/phpstan --generate-baseline
php -d memory_limit=512M bin/phpstan --generate-baseline

phpstan-generate-baseline-php:
php -d memory_limit=450M bin/phpstan analyse --generate-baseline phpstan-baseline.php
php -d memory_limit=512M bin/phpstan analyse --generate-baseline phpstan-baseline.php

phpstan-pro:
php -d memory_limit=450M bin/phpstan --pro
php -d memory_limit=512M bin/phpstan --pro

# Regenerates src/Type/Regex/Parser/RegexParser.php from
# build/regex-parser/RegexGrammar.pp3 with the phplrt compiler. The compiler
Expand Down
60 changes: 0 additions & 60 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1015,12 +1015,6 @@ parameters:
count: 2
path: src/Type/Enum/EnumCaseObjectType.php

-
rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.'
identifier: phpstanApi.instanceofType
count: 2
path: src/Type/ExponentiateHelper.php

-
rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
Expand Down Expand Up @@ -1459,60 +1453,6 @@ parameters:
count: 4
path: src/Type/ObjectWithoutClassType.php

-
rawMessage: Binary operation "&" between bool|float|int|string|null and bool|float|int|string|null results in an error.
identifier: binaryOp.invalid
count: 1
path: src/Type/OperatorTypeHelper.php

-
rawMessage: Binary operation "*" between bool|float|int|string|null and bool|float|int|string|null results in an error.
identifier: binaryOp.invalid
count: 1
path: src/Type/OperatorTypeHelper.php

-
rawMessage: Binary operation "+" between bool|float|int|string|null and bool|float|int|string|null results in an error.
identifier: binaryOp.invalid
count: 1
path: src/Type/OperatorTypeHelper.php

-
rawMessage: Binary operation "-" between bool|float|int|string|null and bool|float|int|string|null results in an error.
identifier: binaryOp.invalid
count: 1
path: src/Type/OperatorTypeHelper.php

-
rawMessage: Binary operation "^" between bool|float|int|string|null and bool|float|int|string|null results in an error.
identifier: binaryOp.invalid
count: 1
path: src/Type/OperatorTypeHelper.php

-
rawMessage: Binary operation "|" between bool|float|int|string|null and bool|float|int|string|null results in an error.
identifier: binaryOp.invalid
count: 1
path: src/Type/OperatorTypeHelper.php

-
rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.'
identifier: phpstanApi.instanceofType
count: 16
path: src/Type/OperatorTypeHelper.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
count: 6
path: src/Type/OperatorTypeHelper.php

-
rawMessage: PHPDoc tag @var with type float|int is not subtype of native type int.
identifier: varTag.nativeType
count: 1
path: src/Type/OperatorTypeHelper.php

-
rawMessage: Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead.
identifier: phpstanApi.runtimeReflection
Expand Down
21 changes: 15 additions & 6 deletions src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
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 @@ -640,7 +639,7 @@ public function specifyEqual(

// 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());
$phpVersions = $evaluationScope->getPhpVersion()->getLooseComparisonVersions();

return $this->createLooselyEqualTypes($evaluationScope, $left, $leftResult, $leftType, $rightType, $phpVersions, $context)
->unionWith($this->createLooselyEqualTypes($evaluationScope, $right, $rightResult, $rightType, $leftType, $phpVersions, $context));
Expand All @@ -651,14 +650,24 @@ public function specifyEqual(
*/
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);
// in the true branch any of the versions' values, in the false branch those of every version
$types = [];
foreach ($phpVersions as $phpVersion) {
$types[] = $context->true() ? $otherType->getPossiblyLooselyEqualType($phpVersion) : $otherType->getAlwaysLooselyEqualType($phpVersion);
}
$type = $context->true() ? TypeCombinator::union(...$types) : TypeCombinator::intersect(...$types);
// 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)) {
// a mixed subject stays mixed rather than gain objects of classes like Stringable or GMP,
// which say nothing about the value; enum cases are precise enough to narrow to
if (
$context->true()
&& $subjectType instanceof MixedType
&& !$type->isObject()->no()
&& !TypeCombinator::intersect($type, new ObjectWithoutClassType())->isEnum()->yes()
) {
return new SpecifiedTypes([], []);
}

Expand Down
36 changes: 36 additions & 0 deletions src/Php/PhpVersions.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace PHPStan\Php;

use PHPStan\Analyser\ConstantResolver;
use PHPStan\TrinaryLogic;
use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\Type;
use function count;
use function is_int;

/**
* Range-aware PHP version check that handles version uncertainty.
Expand All @@ -22,6 +25,9 @@
final class PhpVersions
{

// PHP 8.0 compares a number with a non-numeric string as strings, PHP 8.4 adds BcMath\Number
private const LOOSE_COMPARISON_PHP_VERSION_BOUNDARIES = [80000, 80400];

public function __construct(
private Type $phpVersions,
)
Expand All @@ -33,6 +39,36 @@ public function getType(): Type
return $this->phpVersions;
}

/**
* One PHP version for each range of the versions that loosely compares (`==`) the same way.
*
* @return non-empty-list<PhpVersion>
*/
public function getLooseComparisonVersions(): array
{
$versionIds = $this->phpVersions->getConstantScalarValues();
if (count($versionIds) === 1 && is_int($versionIds[0])) {
return [new PhpVersion($versionIds[0])];
}

$mins = [ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, ...self::LOOSE_COMPARISON_PHP_VERSION_BOUNDARIES];
$versions = [];
foreach ($mins as $i => $min) {
$segment = IntegerRangeType::fromInterval($min, isset($mins[$i + 1]) ? $mins[$i + 1] - 1 : null);
if ($segment->isSuperTypeOf($this->phpVersions)->no()) {
continue;
}

$versions[] = new PhpVersion($min);
}
if ($versions === []) {
// no PHP version reaches this code, any of them will do
return [new PhpVersion($mins[0])];
}

return $versions;
}

public function supportsNoncapturingCatches(): TrinaryLogic
{
return IntegerRangeType::fromInterval(80000, null)->isSuperTypeOf($this->phpVersions)->result;
Expand Down
44 changes: 30 additions & 14 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
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\NonexistentParentClassType;
Expand All @@ -92,12 +91,14 @@
use PHPStan\Type\TypeWithClassName;
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
use PHPStan\Type\UnionType;
use PHPStan\Type\ValueSet;
use stdClass;
use Throwable;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function array_slice;
use function array_values;
use function count;
use function dirname;
Expand Down Expand Up @@ -997,7 +998,7 @@ public function getSpaceshipType(Expr $left, Expr $right, callable $getTypeCallb
$resultTypes = [];
foreach ($leftValues as $leftValue) {
foreach ($rightValues as $rightValue) {
$resultType = new ConstantIntegerType(LooseComparisonHelper::compareValues($leftValue, $rightValue, $this->phpVersion) ?? 1);
$resultType = new ConstantIntegerType(ValueSet::compareScalarValues($leftValue, $rightValue, $this->phpVersion) ?? 1);
$resultTypes[] = $resultType;
}
}
Expand Down Expand Up @@ -1182,24 +1183,39 @@ public function resolveEqualType(Type $leftType, Type $rightType, ?PhpVersions $
return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType, $phpVersions));
}

$versions = $phpVersions !== null ? LooseComparisonHelper::getRepresentativePhpVersions($phpVersions) : [$this->phpVersion];
$result = LooseComparisonHelper::looseCompareInVersions($leftType, $rightType, $versions);
// constant only when every PHP version agrees
$versions = $phpVersions !== null ? $phpVersions->getLooseComparisonVersions() : [$this->phpVersion];
$result = $leftType->looseCompare($rightType, $versions[0]);
foreach (array_slice($versions, 1) as $version) {
if (!$result->equals($leftType->looseCompare($rightType, $version))) {
$result = new BooleanType();
break;
}
}
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), []);
// either side's possibly loosely equal type in any version rules the other side out
foreach ([[$leftType, $rightType], [$rightType, $leftType]] as [$type, $otherType]) {
$possiblyEqualTypes = [];
foreach ($versions as $version) {
$possiblyEqualTypes[] = $type->getPossiblyLooselyEqualType($version);
}
if (TypeCombinator::union(...$possiblyEqualTypes)->isSuperTypeOf($otherType)->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), []);
// either side's always loosely equal type in every version holds the other side
foreach ([[$leftType, $rightType], [$rightType, $leftType]] as [$type, $otherType]) {
$alwaysEqualTypes = [];
foreach ($versions as $version) {
$alwaysEqualTypes[] = $type->getAlwaysLooselyEqualType($version);
}
if (TypeCombinator::intersect(...$alwaysEqualTypes)->isSuperTypeOf($otherType)->yes()) {
return new TypeResult(new ConstantBooleanType(true), []);
}
}

return new TypeResult($result, []);
Expand Down
11 changes: 8 additions & 3 deletions src/Rules/Comparison/SwitchConditionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\LooseComparisonHelper;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function count;
Expand Down Expand Up @@ -53,7 +52,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
$nextCaseIsDeadForType = false;
$nextCaseIsDeadForNativeType = false;
$seenCases = [];
$phpVersions = LooseComparisonHelper::getRepresentativePhpVersions($scope->getPhpVersion());
$phpVersions = $scope->getPhpVersion()->getLooseComparisonVersions();

foreach ($node->getArms() as $arm) {
if (
Expand Down Expand Up @@ -182,7 +181,13 @@ private function isDuplicateCase(Type $seenType, Type $caseValueType, array $php
return true;
}

return LooseComparisonHelper::looseCompareInVersions($seenType, $caseValueType, $phpVersions)->isTrue()->yes();
foreach ($phpVersions as $phpVersion) {
if (!$seenType->looseCompare($caseValueType, $phpVersion)->isTrue()->yes()) {
return false;
}
}

return true;
}

}
6 changes: 6 additions & 0 deletions src/Type/Accessory/AccessoryArrayListType.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use PHPStan\Type\ValueSet;
use PHPStan\Type\VerbosityLevel;

/** @api */
Expand Down Expand Up @@ -593,4 +594,9 @@ public function hasTemplateOrLateResolvableType(): bool
return false;
}

public function toValueSet(): ValueSet
{
return ValueSet::createArrays($this);
}

}
6 changes: 6 additions & 0 deletions src/Type/Accessory/AccessoryDecimalIntegerStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\ValueSet;
use PHPStan\Type\VerbosityLevel;

/**
Expand Down Expand Up @@ -494,4 +495,9 @@ public function hasTemplateOrLateResolvableType(): bool
return false;
}

public function toValueSet(): ValueSet
{
return ValueSet::createStrings($this);
}

}
6 changes: 6 additions & 0 deletions src/Type/Accessory/AccessoryLiteralStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\ValueSet;
use PHPStan\Type\VerbosityLevel;

#[InstanceofDeprecated(insteadUse: 'Type::isLiteralString()')]
Expand Down Expand Up @@ -415,4 +416,9 @@ public function hasTemplateOrLateResolvableType(): bool
return false;
}

public function toValueSet(): ValueSet
{
return ValueSet::createStrings($this);
}

}
6 changes: 6 additions & 0 deletions src/Type/Accessory/AccessoryLowercaseStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\ValueSet;
use PHPStan\Type\VerbosityLevel;

#[InstanceofDeprecated(insteadUse: 'Type::isLowercaseString()')]
Expand Down Expand Up @@ -420,4 +421,9 @@ public function hasTemplateOrLateResolvableType(): bool
return false;
}

public function toValueSet(): ValueSet
{
return ValueSet::createStrings($this);
}

}
Loading
Loading