Skip to content

Commit f261ce6

Browse files
committed
Merge remote-tracking branch 'origin/1.12.x' into 2.1.x
2 parents a758cf3 + 2d09553 commit f261ce6

File tree

14 files changed

+213
-39
lines changed

14 files changed

+213
-39
lines changed

changelog-generator/run.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ protected function execute(InputInterface $input, OutputInterface $output)
118118
$issuesToReference = [];
119119
foreach ($items as $responseItem) {
120120
if (isset($responseItem['pull_request'])) {
121+
if ($responseItem['number'] === 13191) {
122+
continue;
123+
}
121124
$parenthesis = sprintf('[#%d](%s)', $responseItem['number'], 'https://github.com/phpstan/phpstan-src/pull/' . $responseItem['number']);
122125
$thanks = $responseItem['user']['login'];
123126
} else {

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ parameters:
351351
-
352352
message: '#^Dead catch \- PHPStan\\BetterReflection\\Identifier\\Exception\\InvalidIdentifierName is never thrown in the try block\.$#'
353353
identifier: catch.neverThrown
354-
count: 3
354+
count: 4
355355
path: src/Reflection/BetterReflection/BetterReflectionProvider.php
356356

357357
-

src/Reflection/BetterReflection/BetterReflectionProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ public function resolveConstantName(Node\Name $nameNode, ?NamespaceAnswerer $nam
465465
return true;
466466
} catch (IdentifierNotFound) {
467467
// pass
468+
} catch (InvalidIdentifierName) {
469+
// pass
468470
} catch (UnableToCompileNode) {
469471
// pass
470472
}

src/Type/Accessory/HasOffsetValueType.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
use PHPStan\Type\ConstantScalarType;
1616
use PHPStan\Type\ErrorType;
1717
use PHPStan\Type\IntegerRangeType;
18+
use PHPStan\Type\IntegerType;
1819
use PHPStan\Type\IntersectionType;
1920
use PHPStan\Type\IsSuperTypeOfResult;
2021
use PHPStan\Type\MixedType;
2122
use PHPStan\Type\ObjectWithoutClassType;
23+
use PHPStan\Type\StringType;
2224
use PHPStan\Type\Traits\MaybeArrayTypeTrait;
2325
use PHPStan\Type\Traits\MaybeCallableTypeTrait;
2426
use PHPStan\Type\Traits\MaybeIterableTypeTrait;
@@ -258,7 +260,10 @@ public function searchArray(Type $needleType): Type
258260
$needleType instanceof ConstantScalarType && $this->valueType instanceof ConstantScalarType
259261
&& $needleType->getValue() === $this->valueType->getValue()
260262
) {
261-
return $this->offsetType;
263+
return new UnionType([
264+
new IntegerType(),
265+
new StringType(),
266+
]);
262267
}
263268

264269
return new MixedType();

src/Type/ArrayType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ public function isList(): TrinaryLogic
244244
return TrinaryLogic::createNo();
245245
}
246246

247+
if ($this->getKeyType()->isSuperTypeOf(new ConstantIntegerType(0))->no()) {
248+
return TrinaryLogic::createNo();
249+
}
250+
247251
return TrinaryLogic::createMaybe();
248252
}
249253

src/Type/Php/ArrayReplaceFunctionReturnTypeExtension.php

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\DependencyInjection\AutowiredService;
88
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\TrinaryLogic;
10+
use PHPStan\Type\Accessory\AccessoryArrayListType;
911
use PHPStan\Type\Accessory\NonEmptyArrayType;
1012
use PHPStan\Type\ArrayType;
13+
use PHPStan\Type\Constant\ConstantArrayType;
14+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
15+
use PHPStan\Type\Constant\ConstantIntegerType;
16+
use PHPStan\Type\Constant\ConstantStringType;
1117
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
18+
use PHPStan\Type\NeverType;
1219
use PHPStan\Type\Type;
1320
use PHPStan\Type\TypeCombinator;
21+
use function array_keys;
1422
use function count;
23+
use function in_array;
1524
use function strtolower;
1625

1726
#[AutowiredService]
@@ -25,54 +34,107 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
2534

2635
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
2736
{
28-
$arrayTypes = $this->collectArrayTypes($functionCall, $scope);
37+
$args = $functionCall->getArgs();
2938

30-
if (count($arrayTypes) === 0) {
39+
if (!isset($args[0])) {
3140
return null;
3241
}
3342

34-
return $this->getResultType(...$arrayTypes);
35-
}
43+
$argTypes = [];
44+
$optionalArgTypes = [];
45+
foreach ($args as $arg) {
46+
$argType = $scope->getType($arg->value);
3647

37-
private function getResultType(Type ...$arrayTypes): Type
38-
{
39-
$keyTypes = [];
40-
$valueTypes = [];
41-
$nonEmptyArray = false;
42-
foreach ($arrayTypes as $arrayType) {
43-
if (!$nonEmptyArray && $arrayType->isIterableAtLeastOnce()->yes()) {
44-
$nonEmptyArray = true;
48+
if ($arg->unpack) {
49+
if ($argType->isConstantArray()->yes()) {
50+
foreach ($argType->getConstantArrays() as $constantArray) {
51+
foreach ($constantArray->getValueTypes() as $valueType) {
52+
$argTypes[] = $valueType;
53+
}
54+
}
55+
} else {
56+
$argTypes[] = $argType->getIterableValueType();
57+
}
58+
59+
if (!$argType->isIterableAtLeastOnce()->yes()) {
60+
// unpacked params can be empty, making them optional
61+
$optionalArgTypesOffset = count($argTypes) - 1;
62+
foreach (array_keys($argTypes) as $key) {
63+
$optionalArgTypes[] = $optionalArgTypesOffset + $key;
64+
}
65+
}
66+
} else {
67+
$argTypes[] = $argType;
4568
}
46-
47-
$keyTypes[] = $arrayType->getIterableKeyType();
48-
$valueTypes[] = $arrayType->getIterableValueType();
4969
}
5070

51-
$keyType = TypeCombinator::union(...$keyTypes);
52-
$valueType = TypeCombinator::union(...$valueTypes);
71+
$allConstant = TrinaryLogic::createYes()->lazyAnd(
72+
$argTypes,
73+
static fn (Type $argType) => $argType->isConstantArray(),
74+
);
75+
76+
if ($allConstant->yes()) {
77+
$newArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
78+
79+
foreach ($argTypes as $argType) {
80+
/** @var array<int|string, ConstantIntegerType|ConstantStringType> $keyTypes */
81+
$keyTypes = [];
82+
foreach ($argType->getConstantArrays() as $constantArray) {
83+
foreach ($constantArray->getKeyTypes() as $keyType) {
84+
$keyTypes[$keyType->getValue()] = $keyType;
85+
}
86+
}
87+
88+
foreach ($keyTypes as $keyType) {
89+
$newArrayBuilder->setOffsetValueType(
90+
$keyType,
91+
$argType->getOffsetValueType($keyType),
92+
!$argType->hasOffsetValueType($keyType)->yes(),
93+
);
94+
}
95+
}
5396

54-
$arrayType = new ArrayType($keyType, $valueType);
55-
return $nonEmptyArray ? TypeCombinator::intersect($arrayType, new NonEmptyArrayType()) : $arrayType;
56-
}
97+
return $newArrayBuilder->getArray();
98+
}
5799

58-
/**
59-
* @return Type[]
60-
*/
61-
private function collectArrayTypes(FuncCall $functionCall, Scope $scope): array
62-
{
63-
$args = $functionCall->getArgs();
100+
$keyTypes = [];
101+
$valueTypes = [];
102+
$nonEmpty = false;
103+
$isList = true;
104+
foreach ($argTypes as $key => $argType) {
105+
$keyType = $argType->getIterableKeyType();
106+
$keyTypes[] = $keyType;
107+
$valueTypes[] = $argType->getIterableValueType();
108+
109+
if (!$argType->isList()->yes()) {
110+
$isList = false;
111+
}
64112

65-
$arrayTypes = [];
66-
foreach ($args as $arg) {
67-
$argType = $scope->getType($arg->value);
68-
if (!$argType->isArray()->yes()) {
113+
if (in_array($key, $optionalArgTypes, true) || !$argType->isIterableAtLeastOnce()->yes()) {
69114
continue;
70115
}
71116

72-
$arrayTypes[] = $arg->unpack ? $argType->getIterableValueType() : $argType;
117+
$nonEmpty = true;
118+
}
119+
120+
$keyType = TypeCombinator::union(...$keyTypes);
121+
if ($keyType instanceof NeverType) {
122+
return new ConstantArrayType([], []);
123+
}
124+
125+
$arrayType = new ArrayType(
126+
$keyType,
127+
TypeCombinator::union(...$valueTypes),
128+
);
129+
130+
if ($nonEmpty) {
131+
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
132+
}
133+
if ($isList) {
134+
$arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType());
73135
}
74136

75-
return $arrayTypes;
137+
return $arrayType;
76138
}
77139

78140
}

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,12 @@ public function testBug12979(): void
14871487
$this->assertNoErrors($errors);
14881488
}
14891489

1490+
public function testBug12095(): void
1491+
{
1492+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-12095.php');
1493+
$this->assertNoErrors($errors);
1494+
}
1495+
14901496
/**
14911497
* @param string[]|null $allAnalysedFiles
14921498
* @return Error[]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12095;
4+
5+
class HelloWorld
6+
{
7+
public const XXX = '$';
8+
9+
public function sayHello(): void
10+
{
11+
if(defined(self::XXX)) {
12+
die(0);
13+
}
14+
}
15+
}

tests/PHPStan/Analyser/nsrt/array-replace.php

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ public function arrayReplace($array1, $array2): void
2323
*/
2424
public function arrayReplaceArrayShapes($array1, $array2): void
2525
{
26-
assertType("non-empty-array<'bar'|'foo', '1'|'2'>", array_replace($array1));
27-
assertType("non-empty-array<'bar'|'foo', '1'|'2'>", array_replace([], $array1));
28-
assertType("non-empty-array<'bar'|'foo', '1'|'2'|'4'>", array_replace($array1, $array2));
26+
assertType("array{foo: '1', bar: '2'}", array_replace($array1));
27+
assertType("array{foo: '1', bar: '2'}", array_replace([], $array1));
28+
assertType("array{foo: '1', bar: '4'}", array_replace($array1, $array2));
2929
}
3030

3131
/**
@@ -68,4 +68,38 @@ public function arrayReplaceUnionTypeArrayShapes($array1, $array2): void
6868
assertType("array<int, array{bar: '2'}|array{bar: '3'}|array{foo: '1'}|array{foo: '2'}>", array_replace($array1, $array2));
6969
assertType("array<int, array{bar: '2'}|array{bar: '3'}|array{foo: '1'}|array{foo: '2'}>", array_replace($array2, $array1));
7070
}
71+
72+
/**
73+
* @param array{foo: '1', bar: '2'} $array1
74+
* @param array<string, int> $array2
75+
* @param array<int, string> $array3
76+
*/
77+
public function arrayReplaceArrayShapeAndGeneralArray($array1, $array2, $array3): void
78+
{
79+
assertType("non-empty-array<string, '1'|'2'|int>", array_replace($array1, $array2));
80+
assertType("non-empty-array<string, '1'|'2'|int>", array_replace($array2, $array1));
81+
82+
assertType("non-empty-array<'bar'|'foo'|int, string>", array_replace($array1, $array3));
83+
assertType("non-empty-array<'bar'|'foo'|int, string>", array_replace($array3, $array1));
84+
85+
assertType("array<int|string, int|string>", array_replace($array2, $array3));
86+
}
87+
88+
/**
89+
* @param array{0: 1, 1: 2} $array1
90+
* @param array{1: 3, 2: 4} $array2
91+
*/
92+
public function arrayReplaceNumericKeys($array1, $array2): void
93+
{
94+
assertType("array{1, 3, 4}", array_replace($array1, $array2));
95+
}
96+
97+
/**
98+
* @param list<int> $array1
99+
* @param list<int> $array2
100+
*/
101+
public function arrayReplaceLists($array1, $array2): void
102+
{
103+
assertType("list<int>", array_replace($array1, $array2));
104+
}
71105
}

tests/PHPStan/Analyser/nsrt/array-search.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function normalArrays(array $arr, string $string): void
2929
}
3030

3131
if (array_key_exists(17, $arr) && $arr[17] === 'foo') {
32-
assertType('17', array_search('foo', $arr, true));
32+
assertType('int', array_search('foo', $arr, true));
3333
assertType('int|false', array_search('foo', $arr));
3434
assertType('int|false', array_search($string, $arr, true));
3535
}

0 commit comments

Comments
 (0)