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
10 changes: 8 additions & 2 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,12 @@ parameters:
count: 1
path: src/Type/Accessory/HasMethodType.php

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

-
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
Expand All @@ -852,7 +858,7 @@ parameters:
-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
count: 2
count: 3
path: src/Type/Accessory/HasOffsetValueType.php

-
Expand Down Expand Up @@ -954,7 +960,7 @@ parameters:
-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
identifier: phpstanApi.instanceofType
count: 2
count: 3
path: src/Type/Constant/ConstantArrayType.php

-
Expand Down
57 changes: 2 additions & 55 deletions src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
use Throwable;
use function array_fill;
use function array_filter;
use function array_map;
use function array_merge;
Expand Down Expand Up @@ -750,64 +749,12 @@ private function getArraySortPreserveListFunctionType(Type $type): Type

private function getArraySortDoNotPreserveListFunctionType(Type $type): Type
{
$isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
if ($isIterableAtLeastOnce->no()) {
return $type;
}

return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type {
if ($type instanceof UnionType) {
return $traverse($type);
}

$constantArrays = $type->getConstantArrays();
if (count($constantArrays) > 0) {
$types = [];
foreach ($constantArrays as $constantArray) {
$types[] = new ConstantArrayType(
$constantArray->getKeyTypes(),
$constantArray->getValueTypes(),
$constantArray->getNextAutoIndexes(),
$constantArray->getOptionalKeys(),
$constantArray->isList()->and(TrinaryLogic::createMaybe()),
);
}

return TypeCombinator::union(...$types);
}

$newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType());
if ($isIterableAtLeastOnce->yes()) {
$newArrayType = new IntersectionType([$newArrayType, new NonEmptyArrayType()]);
}

return $newArrayType;
});
return $type->makeListMaybe();
}

private function getArrayWalkResultType(Type $arrayType, Type $newValueType): Type
{
return TypeTraverser::map($arrayType, static function (Type $type, callable $traverse) use ($newValueType): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if ($type instanceof ConstantArrayType) {
return new ConstantArrayType(
$type->getKeyTypes(),
array_fill(0, count($type->getValueTypes()), $newValueType),
$type->getNextAutoIndexes(),
$type->getOptionalKeys(),
$type->isList(),
);
}

if (!$type instanceof ArrayType) {
return $type;
}

return new ArrayType($type->getKeyType(), $newValueType);
});
return $arrayType->mapValueType(static fn (Type $type): Type => $newValueType);
}

public function resolveType(MutatingScope $scope, Expr $expr): Type
Expand Down
43 changes: 14 additions & 29 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,12 @@
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
Expand Down Expand Up @@ -1381,35 +1379,22 @@ public function processStmtNode(
$keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType());

if ($valueTypeChanged || $keyTypeChanged) {
$newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType, $keyLoopType, $valueTypeChanged, $keyTypeChanged): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if (!$type instanceof ArrayType) {
return $type;
}
$newExprType = $exprType;
if ($valueTypeChanged) {
$newExprType = $newExprType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopType);
}
if ($keyTypeChanged) {
$newExprType = $newExprType->mapKeyType(static fn (Type $type): Type => $keyLoopType);
}

return new ArrayType(
$keyTypeChanged ? $keyLoopType : $type->getKeyType(),
$valueTypeChanged ? $arrayDimFetchLoopType : $type->getIterableValueType(),
);
});
$nativeExprType = $scope->getNativeType($stmt->expr);
$newExprNativeType = TypeTraverser::map($nativeExprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType, $keyLoopNativeType, $valueTypeChanged, $keyTypeChanged): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}

if (!$type instanceof ArrayType) {
return $type;
}

return new ArrayType(
$keyTypeChanged ? $keyLoopNativeType : $type->getKeyType(),
$valueTypeChanged ? $arrayDimFetchLoopNativeType : $type->getIterableValueType(),
);
});
$newExprNativeType = $nativeExprType;
if ($valueTypeChanged) {
$newExprNativeType = $newExprNativeType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopNativeType);
}
if ($keyTypeChanged) {
$newExprNativeType = $newExprNativeType->mapKeyType(static fn (Type $type): Type => $keyLoopNativeType);
}

if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
$finalScope = $finalScope->assignVariable(
Expand Down
38 changes: 38 additions & 0 deletions src/Type/Accessory/AccessoryArrayListType.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,44 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen
return $this;
}

public function makeListMaybe(): Type
{
// This accessory is the list assertion itself; weakening the
// list-ness to "maybe" means the accessory no longer applies.
// Returning `MixedType` lets the enclosing `IntersectionType` drop
// it via `TypeCombinator::intersect` while preserving the rest.
return new MixedType();
}

public function mapValueType(callable $cb): Type
{
// Mapping values doesn't disturb list-ness.
return $this;
}

public function mapKeyType(callable $cb): Type
{
return $this;
}

public function makeAllArrayKeysOptional(): Type
{
// Marking keys optional in an arbitrary list keeps it a list.
return $this;
}

public function changeKeyCaseArray(?int $case): Type
{
// List keys are integers; case-folding leaves them alone.
return $this;
}

public function filterArrayRemovingFalsey(): Type
{
// Filtering creates gaps in the integer-key sequence — list-ness lost.
return new MixedType();
}

public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
57 changes: 57 additions & 0 deletions src/Type/Accessory/HasOffsetType.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
use function strtolower;
use function strtoupper;
use const CASE_LOWER;
use const CASE_UPPER;

class HasOffsetType implements CompoundType, AccessoryType
{
Expand Down Expand Up @@ -221,6 +225,59 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen
return new MixedType();
}

public function makeListMaybe(): Type
{
// Having an offset doesn't conflict with list-being-maybe.
return $this;
}

public function mapValueType(callable $cb): Type
{
// `HasOffsetType` only records that an offset exists, not its
// value; the assertion still holds after a value transformation.
return $this;
}

public function mapKeyType(callable $cb): Type
{
// Match the prior `TypeTraverser`-based pattern that left
// accessories untouched while rewriting the array key type.
return $this;
}

public function makeAllArrayKeysOptional(): Type
{
// "Has offset X" is no longer guaranteed when X is now optional.
return new MixedType();
}

public function changeKeyCaseArray(?int $case): Type
{
// A string offset is itself case-folded; an int offset is unchanged.
if (!$this->offsetType instanceof ConstantStringType) {
return $this;
}

$value = $this->offsetType->getValue();
if ($case === CASE_LOWER) {
return new self(new ConstantStringType(strtolower($value)));
}
if ($case === CASE_UPPER) {
return new self(new ConstantStringType(strtoupper($value)));
}

// Unknown case → could be either fold; the accessory weakens to
// "no specific offset known".
return new MixedType();
}

public function filterArrayRemovingFalsey(): Type
{
// We don't track the value at this offset, so we can't guarantee
// it survives a falsey filter. Drop the assertion.
return new MixedType();
}

public function isIterableAtLeastOnce(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
63 changes: 63 additions & 0 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPStan\Type\IsSuperTypeOfResult;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StaticTypeFactory;
use PHPStan\Type\StringType;
use PHPStan\Type\Traits\MaybeArrayTypeTrait;
use PHPStan\Type\Traits\MaybeCallableTypeTrait;
Expand All @@ -36,6 +37,10 @@
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function sprintf;
use function strtolower;
use function strtoupper;
use const CASE_LOWER;
use const CASE_UPPER;

class HasOffsetValueType implements CompoundType, AccessoryType
{
Expand Down Expand Up @@ -309,6 +314,64 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen
return new MixedType();
}

public function makeListMaybe(): Type
{
// Knowing a specific offset/value is independent of list-ness.
return $this;
}

public function mapValueType(callable $cb): Type
{
// The assertion is "offset X has value V"; after the transform
// the value at X is `cb(V)`.
return new self($this->offsetType, $cb($this->valueType));
}

public function mapKeyType(callable $cb): Type
{
// The offset itself is unaffected; passes through.
return $this;
}

public function makeAllArrayKeysOptional(): Type
{
return new MixedType();
}

public function changeKeyCaseArray(?int $case): Type
{
if (!$this->offsetType instanceof ConstantStringType) {
return $this;
}

$value = $this->offsetType->getValue();
if ($case === CASE_LOWER) {
return new self(new ConstantStringType(strtolower($value)), $this->valueType);
}
if ($case === CASE_UPPER) {
return new self(new ConstantStringType(strtoupper($value)), $this->valueType);
}

// Unknown case → drop the specific-offset assertion.
return new MixedType();
}

public function filterArrayRemovingFalsey(): Type
{
$falseyTypes = StaticTypeFactory::falsey();
$isFalsey = $falseyTypes->isSuperTypeOf($this->valueType);
if ($isFalsey->yes()) {
// Definitely filtered out — the offset assertion no longer holds.
return new MixedType();
}
if ($isFalsey->no()) {
// Definitely survives.
return $this;
}
// Maybe filtered: drop the specific-value assertion.
return new MixedType();
}

public function isIterableAtLeastOnce(): TrinaryLogic
{
return TrinaryLogic::createYes();
Expand Down
Loading
Loading