Skip to content

Commit a9ab57b

Browse files
committed
Merge remote-tracking branch 'origin/2.1.x' into 2.2.x
2 parents a69f7ca + aa6f4af commit a9ab57b

23 files changed

Lines changed: 797 additions & 296 deletions

phpstan-baseline.neon

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,12 @@ parameters:
843843
count: 1
844844
path: src/Type/Accessory/HasMethodType.php
845845

846+
-
847+
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
848+
identifier: phpstanApi.instanceofType
849+
count: 1
850+
path: src/Type/Accessory/HasOffsetType.php
851+
846852
-
847853
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
848854
identifier: phpstanApi.instanceofType
@@ -858,7 +864,7 @@ parameters:
858864
-
859865
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
860866
identifier: phpstanApi.instanceofType
861-
count: 2
867+
count: 3
862868
path: src/Type/Accessory/HasOffsetValueType.php
863869

864870
-
@@ -960,7 +966,7 @@ parameters:
960966
-
961967
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
962968
identifier: phpstanApi.instanceofType
963-
count: 2
969+
count: 3
964970
path: src/Type/Constant/ConstantArrayType.php
965971

966972
-

src/Analyser/ExprHandler/FuncCallHandler.php

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
use PHPStan\Type\TypeTraverser;
6464
use PHPStan\Type\UnionType;
6565
use Throwable;
66-
use function array_fill;
6766
use function array_filter;
6867
use function array_map;
6968
use function array_merge;
@@ -750,64 +749,12 @@ private function getArraySortPreserveListFunctionType(Type $type): Type
750749

751750
private function getArraySortDoNotPreserveListFunctionType(Type $type): Type
752751
{
753-
$isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
754-
if ($isIterableAtLeastOnce->no()) {
755-
return $type;
756-
}
757-
758-
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type {
759-
if ($type instanceof UnionType) {
760-
return $traverse($type);
761-
}
762-
763-
$constantArrays = $type->getConstantArrays();
764-
if (count($constantArrays) > 0) {
765-
$types = [];
766-
foreach ($constantArrays as $constantArray) {
767-
$types[] = new ConstantArrayType(
768-
$constantArray->getKeyTypes(),
769-
$constantArray->getValueTypes(),
770-
$constantArray->getNextAutoIndexes(),
771-
$constantArray->getOptionalKeys(),
772-
$constantArray->isList()->and(TrinaryLogic::createMaybe()),
773-
);
774-
}
775-
776-
return TypeCombinator::union(...$types);
777-
}
778-
779-
$newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType());
780-
if ($isIterableAtLeastOnce->yes()) {
781-
$newArrayType = new IntersectionType([$newArrayType, new NonEmptyArrayType()]);
782-
}
783-
784-
return $newArrayType;
785-
});
752+
return $type->makeListMaybe();
786753
}
787754

788755
private function getArrayWalkResultType(Type $arrayType, Type $newValueType): Type
789756
{
790-
return TypeTraverser::map($arrayType, static function (Type $type, callable $traverse) use ($newValueType): Type {
791-
if ($type instanceof UnionType || $type instanceof IntersectionType) {
792-
return $traverse($type);
793-
}
794-
795-
if ($type instanceof ConstantArrayType) {
796-
return new ConstantArrayType(
797-
$type->getKeyTypes(),
798-
array_fill(0, count($type->getValueTypes()), $newValueType),
799-
$type->getNextAutoIndexes(),
800-
$type->getOptionalKeys(),
801-
$type->isList(),
802-
);
803-
}
804-
805-
if (!$type instanceof ArrayType) {
806-
return $type;
807-
}
808-
809-
return new ArrayType($type->getKeyType(), $newValueType);
810-
});
757+
return $arrayType->mapValueType(static fn (Type $type): Type => $newValueType);
811758
}
812759

813760
public function resolveType(MutatingScope $scope, Expr $expr): Type

src/Analyser/NodeScopeResolver.php

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,12 @@
139139
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
140140
use PHPStan\ShouldNotHappenException;
141141
use PHPStan\TrinaryLogic;
142-
use PHPStan\Type\ArrayType;
143142
use PHPStan\Type\ClosureType;
144143
use PHPStan\Type\Constant\ConstantIntegerType;
145144
use PHPStan\Type\Constant\ConstantStringType;
146145
use PHPStan\Type\FileTypeMapper;
147146
use PHPStan\Type\Generic\TemplateTypeHelper;
148147
use PHPStan\Type\Generic\TemplateTypeMap;
149-
use PHPStan\Type\IntersectionType;
150148
use PHPStan\Type\MixedType;
151149
use PHPStan\Type\NeverType;
152150
use PHPStan\Type\NullType;
@@ -1381,35 +1379,22 @@ public function processStmtNode(
13811379
$keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType());
13821380

13831381
if ($valueTypeChanged || $keyTypeChanged) {
1384-
$newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType, $keyLoopType, $valueTypeChanged, $keyTypeChanged): Type {
1385-
if ($type instanceof UnionType || $type instanceof IntersectionType) {
1386-
return $traverse($type);
1387-
}
1388-
1389-
if (!$type instanceof ArrayType) {
1390-
return $type;
1391-
}
1382+
$newExprType = $exprType;
1383+
if ($valueTypeChanged) {
1384+
$newExprType = $newExprType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopType);
1385+
}
1386+
if ($keyTypeChanged) {
1387+
$newExprType = $newExprType->mapKeyType(static fn (Type $type): Type => $keyLoopType);
1388+
}
13921389

1393-
return new ArrayType(
1394-
$keyTypeChanged ? $keyLoopType : $type->getKeyType(),
1395-
$valueTypeChanged ? $arrayDimFetchLoopType : $type->getIterableValueType(),
1396-
);
1397-
});
13981390
$nativeExprType = $scope->getNativeType($stmt->expr);
1399-
$newExprNativeType = TypeTraverser::map($nativeExprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType, $keyLoopNativeType, $valueTypeChanged, $keyTypeChanged): Type {
1400-
if ($type instanceof UnionType || $type instanceof IntersectionType) {
1401-
return $traverse($type);
1402-
}
1403-
1404-
if (!$type instanceof ArrayType) {
1405-
return $type;
1406-
}
1407-
1408-
return new ArrayType(
1409-
$keyTypeChanged ? $keyLoopNativeType : $type->getKeyType(),
1410-
$valueTypeChanged ? $arrayDimFetchLoopNativeType : $type->getIterableValueType(),
1411-
);
1412-
});
1391+
$newExprNativeType = $nativeExprType;
1392+
if ($valueTypeChanged) {
1393+
$newExprNativeType = $newExprNativeType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopNativeType);
1394+
}
1395+
if ($keyTypeChanged) {
1396+
$newExprNativeType = $newExprNativeType->mapKeyType(static fn (Type $type): Type => $keyLoopNativeType);
1397+
}
14131398

14141399
if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
14151400
$finalScope = $finalScope->assignVariable(

src/Type/Accessory/AccessoryArrayListType.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,44 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen
260260
return $this;
261261
}
262262

263+
public function makeListMaybe(): Type
264+
{
265+
// This accessory is the list assertion itself; weakening the
266+
// list-ness to "maybe" means the accessory no longer applies.
267+
// Returning `MixedType` lets the enclosing `IntersectionType` drop
268+
// it via `TypeCombinator::intersect` while preserving the rest.
269+
return new MixedType();
270+
}
271+
272+
public function mapValueType(callable $cb): Type
273+
{
274+
// Mapping values doesn't disturb list-ness.
275+
return $this;
276+
}
277+
278+
public function mapKeyType(callable $cb): Type
279+
{
280+
return $this;
281+
}
282+
283+
public function makeAllArrayKeysOptional(): Type
284+
{
285+
// Marking keys optional in an arbitrary list keeps it a list.
286+
return $this;
287+
}
288+
289+
public function changeKeyCaseArray(?int $case): Type
290+
{
291+
// List keys are integers; case-folding leaves them alone.
292+
return $this;
293+
}
294+
295+
public function filterArrayRemovingFalsey(): Type
296+
{
297+
// Filtering creates gaps in the integer-key sequence — list-ness lost.
298+
return new MixedType();
299+
}
300+
263301
public function isIterable(): TrinaryLogic
264302
{
265303
return TrinaryLogic::createYes();

src/Type/Accessory/HasOffsetType.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
use PHPStan\Type\UnionType;
3333
use PHPStan\Type\VerbosityLevel;
3434
use function sprintf;
35+
use function strtolower;
36+
use function strtoupper;
37+
use const CASE_LOWER;
38+
use const CASE_UPPER;
3539

3640
class HasOffsetType implements CompoundType, AccessoryType
3741
{
@@ -221,6 +225,59 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen
221225
return new MixedType();
222226
}
223227

228+
public function makeListMaybe(): Type
229+
{
230+
// Having an offset doesn't conflict with list-being-maybe.
231+
return $this;
232+
}
233+
234+
public function mapValueType(callable $cb): Type
235+
{
236+
// `HasOffsetType` only records that an offset exists, not its
237+
// value; the assertion still holds after a value transformation.
238+
return $this;
239+
}
240+
241+
public function mapKeyType(callable $cb): Type
242+
{
243+
// Match the prior `TypeTraverser`-based pattern that left
244+
// accessories untouched while rewriting the array key type.
245+
return $this;
246+
}
247+
248+
public function makeAllArrayKeysOptional(): Type
249+
{
250+
// "Has offset X" is no longer guaranteed when X is now optional.
251+
return new MixedType();
252+
}
253+
254+
public function changeKeyCaseArray(?int $case): Type
255+
{
256+
// A string offset is itself case-folded; an int offset is unchanged.
257+
if (!$this->offsetType instanceof ConstantStringType) {
258+
return $this;
259+
}
260+
261+
$value = $this->offsetType->getValue();
262+
if ($case === CASE_LOWER) {
263+
return new self(new ConstantStringType(strtolower($value)));
264+
}
265+
if ($case === CASE_UPPER) {
266+
return new self(new ConstantStringType(strtoupper($value)));
267+
}
268+
269+
// Unknown case → could be either fold; the accessory weakens to
270+
// "no specific offset known".
271+
return new MixedType();
272+
}
273+
274+
public function filterArrayRemovingFalsey(): Type
275+
{
276+
// We don't track the value at this offset, so we can't guarantee
277+
// it survives a falsey filter. Drop the assertion.
278+
return new MixedType();
279+
}
280+
224281
public function isIterableAtLeastOnce(): TrinaryLogic
225282
{
226283
return TrinaryLogic::createYes();

src/Type/Accessory/HasOffsetValueType.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use PHPStan\Type\IsSuperTypeOfResult;
2222
use PHPStan\Type\MixedType;
2323
use PHPStan\Type\ObjectWithoutClassType;
24+
use PHPStan\Type\StaticTypeFactory;
2425
use PHPStan\Type\StringType;
2526
use PHPStan\Type\Traits\MaybeArrayTypeTrait;
2627
use PHPStan\Type\Traits\MaybeCallableTypeTrait;
@@ -36,6 +37,10 @@
3637
use PHPStan\Type\UnionType;
3738
use PHPStan\Type\VerbosityLevel;
3839
use function sprintf;
40+
use function strtolower;
41+
use function strtoupper;
42+
use const CASE_LOWER;
43+
use const CASE_UPPER;
3944

4045
class HasOffsetValueType implements CompoundType, AccessoryType
4146
{
@@ -309,6 +314,64 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen
309314
return new MixedType();
310315
}
311316

317+
public function makeListMaybe(): Type
318+
{
319+
// Knowing a specific offset/value is independent of list-ness.
320+
return $this;
321+
}
322+
323+
public function mapValueType(callable $cb): Type
324+
{
325+
// The assertion is "offset X has value V"; after the transform
326+
// the value at X is `cb(V)`.
327+
return new self($this->offsetType, $cb($this->valueType));
328+
}
329+
330+
public function mapKeyType(callable $cb): Type
331+
{
332+
// The offset itself is unaffected; passes through.
333+
return $this;
334+
}
335+
336+
public function makeAllArrayKeysOptional(): Type
337+
{
338+
return new MixedType();
339+
}
340+
341+
public function changeKeyCaseArray(?int $case): Type
342+
{
343+
if (!$this->offsetType instanceof ConstantStringType) {
344+
return $this;
345+
}
346+
347+
$value = $this->offsetType->getValue();
348+
if ($case === CASE_LOWER) {
349+
return new self(new ConstantStringType(strtolower($value)), $this->valueType);
350+
}
351+
if ($case === CASE_UPPER) {
352+
return new self(new ConstantStringType(strtoupper($value)), $this->valueType);
353+
}
354+
355+
// Unknown case → drop the specific-offset assertion.
356+
return new MixedType();
357+
}
358+
359+
public function filterArrayRemovingFalsey(): Type
360+
{
361+
$falseyTypes = StaticTypeFactory::falsey();
362+
$isFalsey = $falseyTypes->isSuperTypeOf($this->valueType);
363+
if ($isFalsey->yes()) {
364+
// Definitely filtered out — the offset assertion no longer holds.
365+
return new MixedType();
366+
}
367+
if ($isFalsey->no()) {
368+
// Definitely survives.
369+
return $this;
370+
}
371+
// Maybe filtered: drop the specific-value assertion.
372+
return new MixedType();
373+
}
374+
312375
public function isIterableAtLeastOnce(): TrinaryLogic
313376
{
314377
return TrinaryLogic::createYes();

0 commit comments

Comments
 (0)