Skip to content
Open
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: 0 additions & 12 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ parameters:
count: 1
path: src/Analyser/AnalyserResultFinalizer.php

-
rawMessage: PHPDoc tag @var with type int|string is not subtype of type string.
identifier: varTag.type
count: 1
path: src/Analyser/ArgumentsNormalizer.php

-
rawMessage: Casting to string something that's already string.
identifier: cast.useless
Expand Down Expand Up @@ -1053,12 +1047,6 @@ parameters:
count: 1
path: src/Type/Constant/ConstantStringType.php

-
rawMessage: PHPDoc tag @var with type int|string is not subtype of type string.
identifier: varTag.type
count: 1
path: src/Type/Constant/ConstantStringType.php

-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.'
identifier: phpstanApi.instanceofType
Expand Down
5 changes: 2 additions & 3 deletions src/Analyser/ArgumentsNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
use PHPStan\Type\Constant\ConstantArrayType;
use function array_is_list;
use function array_key_exists;
use function array_key_first;
use function array_keys;
use function array_values;
use function count;
use function is_string;
use function key;
use function ksort;
use function max;
use function sprintf;
Expand Down Expand Up @@ -149,8 +149,7 @@ public static function reorderCallUserFuncArrayArguments(
foreach ($argsArrayArg->value->items as $item) {
$key = null;
if ($item->key instanceof String_) {
/** @var int|string $key */
$key = key([$item->key->value => null]);
$key = array_key_first([$item->key->value => null]);
if ($key === '') {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ private function createCachedDirectorySourceLocator(array $fileHashes, string $c

if ($findInFiles !== []) {
$cacheModified = true;
foreach ($this->symbolFinderInFiles->findSymbols($findInFiles, $this->phpVersion->supportsEnums()) as $file => [$newClasses, $newFunctions, $newConstants]) {
$newHash = $originalFileHashes[$file];
$cached[$file] = [$newHash, $newClasses, $newFunctions, $newConstants];
foreach ($this->symbolFinderInFiles->findSymbols($findInFiles, $this->phpVersion->supportsEnums()) as $scannedFile => [$newClasses, $newFunctions, $newConstants]) {
$newHash = $originalFileHashes[$scannedFile];
$cached[$scannedFile] = [$newHash, $newClasses, $newFunctions, $newConstants];
}
}

Expand Down
11 changes: 7 additions & 4 deletions src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict

public function getKeysArray(): Type
{
return TypeCombinator::intersect(new self(new IntegerType(), $this->getIterableKeyType()), new AccessoryArrayListType());
return TypeCombinator::intersect(new self(new IntegerType(), UnsafeArrayStringKeyCastingTraverser::castReadKeyType($this->getIterableKeyType())), new AccessoryArrayListType());
}

public function getValuesArray(): Type
Expand Down Expand Up @@ -495,15 +495,18 @@ public function fillKeysArray(Type $valueType): Type
return $stringKeyType;
}

return new ArrayType($stringKeyType, $valueType);
// The values become keys, so they go through the same cast as any
// other written key — just like flipArray() below.
return new ArrayType($stringKeyType->toArrayKey(), $valueType);
}

return new ArrayType($itemType, $valueType);
}

public function flipArray(): Type
{
return new self($this->getIterableValueType()->toArrayKey(), $this->getIterableKeyType());
// The keys become values, so they're subject to PHP's array key cast.
return new self($this->getIterableValueType()->toArrayKey(), UnsafeArrayStringKeyCastingTraverser::castReadKeyType($this->getIterableKeyType()));
}

public function intersectKeyArray(Type $otherArraysType): Type
Expand Down Expand Up @@ -585,7 +588,7 @@ public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Typ
return new ConstantBooleanType(false);
}

return TypeCombinator::union($this->getIterableKeyType(), new ConstantBooleanType(false));
return UnsafeArrayStringKeyCastingTraverser::unionWithReadKeyType($this->getIterableKeyType(), new ConstantBooleanType(false));
}

public function shiftArray(): Type
Expand Down
85 changes: 56 additions & 29 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -301,13 +301,8 @@
$keyType = new UnionType($this->keyTypes);
}

if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
$unsealedKeyType = $this->unsealed[0];
if ($unsealedKeyType instanceof MixedType && !$unsealedKeyType instanceof TemplateMixedType) {
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
} elseif ($unsealedKeyType instanceof StrictMixedType && !$unsealedKeyType instanceof TemplateStrictMixedType) {
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
}
$unsealedKeyType = $this->getUnsealedKeyType();
if ($unsealedKeyType !== null) {
$keyType = TypeCombinator::union($keyType, $unsealedKeyType);
}

Expand Down Expand Up @@ -1607,15 +1602,16 @@
$offsetType = $valueType->toArrayKey();
$builder->setOffsetValueType(
$offsetType,
$keyType,
// The keys become values, so they're subject to PHP's array key cast.
UnsafeArrayStringKeyCastingTraverser::castReadKeyType($keyType),
$this->isOptionalKey($i) || count($offsetType->getConstantScalarTypes()) > 1,
);
}

if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
[$unsealedKey, $unsealedValue] = $this->unsealed;
$flippedKey = $unsealedValue->toArrayKey();
$flippedValue = $unsealedKey;
$flippedValue = UnsafeArrayStringKeyCastingTraverser::castReadKeyType($unsealedKey);
// For a non-finite tail key (e.g. `string`), install the
// unsealed extras first; setOffsetValueType then widens any
// overlapping explicit values with the tail's value type.
Expand Down Expand Up @@ -1736,11 +1732,14 @@
}

if (count($matches) > 0) {
// The found key becomes a value of its own, so it's subject to PHP's
// array key cast.
$matchedKeyType = TypeCombinator::union(...$matches);
if ($hasIdenticalValue) {
return TypeCombinator::union(...$matches);
return UnsafeArrayStringKeyCastingTraverser::castReadKeyType($matchedKeyType);
}

return TypeCombinator::union(new ConstantBooleanType(false), ...$matches);
return UnsafeArrayStringKeyCastingTraverser::unionWithReadKeyType($matchedKeyType, new ConstantBooleanType(false));
}

return new ConstantBooleanType(false);
Expand Down Expand Up @@ -2143,17 +2142,12 @@
}
}

if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
$unsealedKeyType = $this->unsealed[0];
if ($unsealedKeyType instanceof MixedType && !$unsealedKeyType instanceof TemplateMixedType) {
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
} elseif ($unsealedKeyType instanceof StrictMixedType && !$unsealedKeyType instanceof TemplateStrictMixedType) {
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
}
$unsealedKeyType = $this->getUnsealedKeyType();
if ($unsealedKeyType !== null) {
$keyTypes[] = $unsealedKeyType;
}

return TypeCombinator::union(...$keyTypes);
return UnsafeArrayStringKeyCastingTraverser::castKeyType(TypeCombinator::union(...$keyTypes));
}

public function getLastIterableKeyType(): Type
Expand All @@ -2166,17 +2160,33 @@
}
}

if ($this->isUnsealed()->yes() && $this->unsealed !== null) {
$unsealedKeyType = $this->unsealed[0];
if ($unsealedKeyType instanceof MixedType && !$unsealedKeyType instanceof TemplateMixedType) {
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
} elseif ($unsealedKeyType instanceof StrictMixedType && !$unsealedKeyType instanceof TemplateStrictMixedType) {
$unsealedKeyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
}
$unsealedKeyType = $this->getUnsealedKeyType();
if ($unsealedKeyType !== null) {
$keyTypes[] = $unsealedKeyType;
}

return TypeCombinator::union(...$keyTypes);
return UnsafeArrayStringKeyCastingTraverser::castKeyType(TypeCombinator::union(...$keyTypes));
}

/**
* The unsealed tail's key type, with an implicit `mixed` spelled out as the
* `array-key` it really is. Null when this shape is sealed.
*/
private function getUnsealedKeyType(): ?Type
{
if (!$this->isUnsealed()->yes() || $this->unsealed === null) {

Check warning on line 2177 in src/Type/Constant/ConstantArrayType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ */ private function getUnsealedKeyType(): ?Type { - if (!$this->isUnsealed()->yes() || $this->unsealed === null) { + if ($this->isUnsealed()->no() || $this->unsealed === null) { return null; }

Check warning on line 2177 in src/Type/Constant/ConstantArrayType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ */ private function getUnsealedKeyType(): ?Type { - if (!$this->isUnsealed()->yes() || $this->unsealed === null) { + if ($this->isUnsealed()->no() || $this->unsealed === null) { return null; }
return null;
}

$unsealedKeyType = $this->unsealed[0];
if (
($unsealedKeyType instanceof MixedType && !$unsealedKeyType instanceof TemplateMixedType)
|| ($unsealedKeyType instanceof StrictMixedType && !$unsealedKeyType instanceof TemplateStrictMixedType)
) {
return (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
}

return $unsealedKeyType;
}

public function getFirstIterableValueType(): Type
Expand Down Expand Up @@ -2437,7 +2447,7 @@

public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
{
$keysArray = $this->getKeysOrValuesArray($this->keyTypes, $this->unsealed[0] ?? null);
$keysArray = $this->getReadKeysArray();

return new IntersectionType([
new ArrayType(
Expand All @@ -2450,7 +2460,24 @@

public function getKeysArray(): self
{
return $this->getKeysOrValuesArray($this->keyTypes, $this->unsealed[0] ?? null);
return $this->getReadKeysArray();
}

/**
* The keys as a list of values - they've left the array, so they're subject
* to PHP's array key cast.
*/
private function getReadKeysArray(): self
{
$unsealedKeyType = $this->unsealed[0] ?? null;

return $this->getKeysOrValuesArray(
array_map(
static fn (Type $keyType): Type => UnsafeArrayStringKeyCastingTraverser::castReadKeyType($keyType),
$this->keyTypes,
),
$unsealedKeyType !== null ? UnsafeArrayStringKeyCastingTraverser::castReadKeyType($unsealedKeyType) : null,
);
}

public function getValuesArray(): self
Expand Down
5 changes: 2 additions & 3 deletions src/Type/Constant/ConstantStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function addcslashes;
use function array_key_first;
use function array_unique;
use function array_values;
use function in_array;
use function is_float;
use function is_int;
use function is_numeric;
use function key;
use function strlen;
use function strtolower;
use function strtoupper;
Expand Down Expand Up @@ -385,8 +385,7 @@ public function toArrayKey(): Type
return $this->arrayKeyType;
}

/** @var int|string $offsetValue */
$offsetValue = key([$this->value => null]);
$offsetValue = array_key_first([$this->value => null]);

if ($offsetValue === $this->value) {
return $this;
Expand Down
4 changes: 2 additions & 2 deletions src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Traverser\UnsafeArrayStringKeyCastingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;

#[AutowiredService]
Expand All @@ -33,7 +33,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
return null;
}

return TypeCombinator::union($arrayType->getIterableKeyType(), new NullType());
return UnsafeArrayStringKeyCastingTraverser::unionWithReadKeyType($arrayType->getIterableKeyType(), new NullType());
}

}
9 changes: 7 additions & 2 deletions src/Type/Php/ArrayFirstLastDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Traverser\UnsafeArrayStringKeyCastingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
Expand Down Expand Up @@ -46,8 +47,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
switch ($functionReflection->getName()) {
case 'array_key_first':
case 'array_key_last':
$resultType = $argType->getIterableKeyType();
break;
$keyType = $argType->getIterableKeyType();
if ($iterableAtLeastOnce->yes()) {
return UnsafeArrayStringKeyCastingTraverser::castReadKeyType($keyType);
}

return UnsafeArrayStringKeyCastingTraverser::unionWithReadKeyType($keyType, new NullType());
case 'array_first':
case 'array_last':
$resultType = $argType->getIterableValueType();
Expand Down
6 changes: 3 additions & 3 deletions src/Type/Php/ArrayKeyDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Traverser\UnsafeArrayStringKeyCastingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

#[AutowiredService]
final class ArrayKeyDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
Expand All @@ -35,10 +35,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,

$keyType = $argType->getIterableKeyType();
if ($iterableAtLeastOnce->yes()) {
return $keyType;
return UnsafeArrayStringKeyCastingTraverser::castReadKeyType($keyType);
}

return TypeCombinator::union($keyType, new NullType());
return UnsafeArrayStringKeyCastingTraverser::unionWithReadKeyType($keyType, new NullType());
}

}
Loading
Loading