-
Notifications
You must be signed in to change notification settings - Fork 461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve in_array
extension to get rid of related impossible check adaptions
#1514
Changes from all commits
e994f69
b02bd1b
12c1301
7b35446
7d3a859
46a92e1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,13 +9,22 @@ | |
use PHPStan\Analyser\TypeSpecifierAwareExtension; | ||
use PHPStan\Analyser\TypeSpecifierContext; | ||
use PHPStan\Reflection\FunctionReflection; | ||
use PHPStan\Type\Accessory\HasOffsetType; | ||
use PHPStan\Type\Accessory\NonEmptyArrayType; | ||
use PHPStan\Type\ArrayType; | ||
use PHPStan\Type\Constant\ConstantArrayType; | ||
use PHPStan\Type\Constant\ConstantArrayTypeBuilder; | ||
use PHPStan\Type\Constant\ConstantBooleanType; | ||
use PHPStan\Type\Constant\ConstantIntegerType; | ||
use PHPStan\Type\ConstantScalarType; | ||
use PHPStan\Type\FunctionTypeSpecifyingExtension; | ||
use PHPStan\Type\IntersectionType; | ||
use PHPStan\Type\MixedType; | ||
use PHPStan\Type\Type; | ||
use PHPStan\Type\TypeCombinator; | ||
use PHPStan\Type\TypeTraverser; | ||
use PHPStan\Type\TypeUtils; | ||
use PHPStan\Type\UnionType; | ||
use function count; | ||
use function strtolower; | ||
|
||
|
@@ -51,11 +60,21 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n | |
|
||
$specifiedTypes = new SpecifiedTypes(); | ||
|
||
if ( | ||
$arrayType instanceof ConstantArrayType && !$arrayType->isEmpty() | ||
&& count(TypeUtils::getConstantScalars($needleType)) === 0 && $arrayValueType->isSuperTypeOf($needleType)->yes() | ||
) { | ||
// Avoid false-positives with e.g. a string needle and array{'self', string} as haystack | ||
// For such cases there seems to be nothing more that we can specify unfortunately | ||
return $specifiedTypes; | ||
} | ||
|
||
Comment on lines
+63
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is an edge case I'd really like to not have here. But I didn't find a way to specify anything here, that doesn't get normalized away (e.g. $needle is |
||
if ( | ||
$context->truthy() | ||
|| count(TypeUtils::getConstantScalars($arrayValueType)) > 0 | ||
|| count(TypeUtils::getEnumCaseObjects($arrayValueType)) > 0 | ||
) { | ||
// Specify needle type | ||
$specifiedTypes = $this->typeSpecifier->create( | ||
$node->getArgs()[0]->value, | ||
$arrayValueType, | ||
|
@@ -65,27 +84,69 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n | |
); | ||
} | ||
|
||
// If e.g. needle is 'a' and haystack non-empty-array<int, 'a'> we can be sure that this always evaluates to true | ||
// Belows HasOffset::isSuperTypeOf cannot deal with that since it calls ArrayType::hasOffsetValueType and that returns maybe at max | ||
if ($needleType instanceof ConstantScalarType && $arrayType->isIterableAtLeastOnce()->yes() && $arrayValueType->equals($needleType)) { | ||
return $specifiedTypes; | ||
} | ||
|
||
Comment on lines
+87
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also dislike this edge case, maybe it is possible to adapt |
||
if ( | ||
$context->truthy() | ||
|| count(TypeUtils::getConstantScalars($needleType)) > 0 | ||
|| count(TypeUtils::getEnumCaseObjects($needleType)) > 0 | ||
) { | ||
if ($context->truthy()) { | ||
$arrayType = TypeCombinator::intersect( | ||
new ArrayType(new MixedType(), TypeCombinator::union($arrayValueType, $needleType)), | ||
new NonEmptyArrayType(), | ||
// Specify haystack type | ||
if ($arrayType instanceof ConstantArrayType) { | ||
$newArrayType = TypeTraverser::map( | ||
$arrayType->getOffsetType($needleType), | ||
static function (Type $offsetType, callable $traverse) use ($context, $arrayType, $needleType): Type { | ||
if ($offsetType instanceof UnionType || $offsetType instanceof IntersectionType) { | ||
return $traverse($offsetType); | ||
} | ||
|
||
$resultArray = $arrayType; | ||
if ($context->truthy()) { | ||
$resultArray = $resultArray->makeOffsetRequired($offsetType); | ||
} elseif ($offsetType instanceof ConstantIntegerType && $resultArray->isOptionalKey($offsetType->getValue())) { | ||
$resultArray = $resultArray->unsetOffset($offsetType); | ||
} | ||
|
||
if ($offsetType instanceof ConstantIntegerType && $resultArray instanceof ConstantArrayType && $resultArray->hasOffsetValueType($offsetType)->yes()) { | ||
// If haystack is e.g. {string, string|null} and needle null, we can further narrow string|null | ||
$builder = ConstantArrayTypeBuilder::createFromConstantArray($resultArray); | ||
$builder->setOffsetValueType( | ||
$offsetType, | ||
$context->truthy() ? $needleType : TypeCombinator::remove($resultArray->getOffsetValueType($offsetType), $needleType), | ||
$resultArray->isOptionalKey($offsetType->getValue()), | ||
); | ||
$resultArray = $builder->getArray(); | ||
} | ||
|
||
return $resultArray; | ||
}, | ||
); | ||
} else { | ||
$arrayType = new ArrayType(new MixedType(), TypeCombinator::remove($arrayValueType, $needleType)); | ||
if ($context->truthy()) { | ||
$newArrayType = TypeCombinator::intersect( | ||
new ArrayType(new MixedType(), TypeCombinator::union($arrayValueType, $needleType)), | ||
new HasOffsetType(new MixedType(), $needleType), | ||
new NonEmptyArrayType(), | ||
); | ||
} else { | ||
$newArrayType = new ArrayType( | ||
new MixedType(), | ||
TypeCombinator::remove($arrayValueType, $needleType), | ||
); | ||
} | ||
} | ||
|
||
$specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( | ||
$node->getArgs()[1]->value, | ||
$arrayType, | ||
TypeSpecifierContext::createTrue(), | ||
false, | ||
$scope, | ||
)); | ||
$specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( | ||
$node->getArgs()[1]->value, | ||
$newArrayType, | ||
TypeSpecifierContext::createTrue(), | ||
false, | ||
$scope, | ||
)); | ||
} | ||
|
||
return $specifiedTypes; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,15 +14,15 @@ class HelloWorld | |
public function sayHello(array $array): void | ||
{ | ||
if(in_array("thing", $array, true)){ | ||
assertType('non-empty-array<int, string>', $array); | ||
assertType('non-empty-array<int, string>&hasOffset(mixed)', $array); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is obviously weird. I'd prefer to rather have an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe print offset and offsetValue type within the bracket, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that would be easy to do and makes sense, yeah. I did not do it yet because I was scared that it will lead to too many changes in e.g. baselines. but we can't think always about that, right? I'll check it out :) |
||
} | ||
} | ||
|
||
/** @param array<int> $haystack */ | ||
public function nonConstantNeedle(int $needle, array $haystack): void | ||
{ | ||
if (in_array($needle, $haystack, true)) { | ||
assertType('non-empty-array<int>', $haystack); | ||
assertType('non-empty-array<int>&hasOffset(mixed)', $haystack); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,12 +10,14 @@ class Foo | |
* @param string $r | ||
* @param $mixed | ||
* @param string[] $strings | ||
* @param string[] $moreStrings | ||
*/ | ||
public function doFoo( | ||
string $s, | ||
string $r, | ||
$mixed, | ||
array $strings | ||
array $strings, | ||
array $moreStrings | ||
) | ||
{ | ||
if (!in_array($s, ['foo', 'bar'], true)) { | ||
|
@@ -26,7 +28,7 @@ public function doFoo( | |
return; | ||
} | ||
|
||
if (in_array($r, $strings, true)) { | ||
if (in_array($r, $moreStrings, true)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had to do that because |
||
return; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,6 +108,10 @@ public function testImpossibleCheckTypeFunctionCall(): void | |
'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'baz\', \'lorem\'} and true will always evaluate to false.', | ||
244, | ||
], | ||
[ | ||
'Call to function in_array() with arguments \'bar\'|\'foo\', array{\'foo\', \'bar\'} and true will always evaluate to true.', | ||
248, | ||
], | ||
[ | ||
'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', | ||
252, | ||
|
@@ -233,6 +237,18 @@ public function testImpossibleCheckTypeFunctionCall(): void | |
'Call to function property_exists() with CheckTypeFunctionCall\Bug2221 and \'foo\' will always evaluate to true.', | ||
786, | ||
], | ||
[ | ||
'Call to function in_array() with arguments \'foo\', non-empty-array<string> and true will always evaluate to true.', | ||
890, | ||
], | ||
[ | ||
'Call to function in_array() with arguments \'foo\', array{\'foo\'} and true will always evaluate to true.', | ||
896, | ||
], | ||
[ | ||
'Call to function in_array() with arguments \'foo\', array and true will always evaluate to false.', | ||
900, | ||
], | ||
], | ||
); | ||
} | ||
|
@@ -329,6 +345,10 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void | |
'Call to function is_numeric() with \'blabla\' will always evaluate to false.', | ||
693, | ||
], | ||
[ | ||
'Call to function in_array() with arguments \'foo\', array and true will always evaluate to false.', | ||
900, | ||
], | ||
], | ||
); | ||
} | ||
|
@@ -483,6 +503,10 @@ public function testBugInArrayDateFormat(): void | |
'Call to function in_array() with arguments int, array{} and true will always evaluate to false.', | ||
47, | ||
], | ||
[ | ||
'Call to function in_array() with arguments int, array<int, string> and true will always evaluate to false.', | ||
61, | ||
], | ||
]); | ||
} | ||
|
||
|
@@ -525,7 +549,12 @@ public function testSlevomatCsInArrayBug(): void | |
{ | ||
$this->checkAlwaysTrueCheckTypeFunctionCall = true; | ||
$this->treatPhpDocTypesAsCertain = true; | ||
$this->analyse([__DIR__ . '/data/slevomat-cs-in-array.php'], []); | ||
$this->analyse([__DIR__ . '/data/slevomat-cs-in-array.php'], [ | ||
[ | ||
'Call to function in_array() with arguments \'abstract methods\'|\'constructor\'|\'destructor\'|\'final methods\'|\'magic methods\'|\'private constants\'|\'private methods\'|\'private properties\'|\'private static…\'|\'private static…\'|\'protected abstract…\'|\'protected constants\'|\'protected final…\'|\'protected methods\'|\'protected properties\'|\'protected static…\'|\'protected static…\'|\'protected static…\'|\'protected static…\'|\'public abstract…\'|\'public constants\'|\'public final methods\'|\'public methods\'|\'public properties\'|\'public static…\'|\'public static final…\'|\'public static…\'|\'public static…\'|\'static constructors\'|\'static methods\'|\'static properties\', array{0: \'final methods\'|\'private static…\'|\'protected final…\'|\'public abstract…\'|\'public constants\'|\'public final methods\'|\'public static…\'|\'static constructors\'|\'static properties\', 1: \'abstract methods\'|\'private methods\'|\'protected abstract…\'|\'protected constants\'|\'protected final…\'|\'protected static…\'|\'protected static…\'|\'public properties\'|\'public static final…\', 2?: \'private constants\'|\'private static…\'|\'protected abstract…\'|\'protected properties\'|\'protected static…\'|\'public abstract…\'|\'public static…\'|\'public static final…\'|\'static methods\', 3?: \'constructor\'|\'private properties\'|\'protected static…\'|\'protected static…\'|\'public static…\', 4?: \'destructor\'|\'protected static…\'|\'protected static…\'|\'public static…\', 5?: \'protected methods\'|\'public methods\'|\'public static…\', 6?: \'protected methods\'|\'protected static…\', 7?: \'private methods\'|\'private static…\', ...} and true will always evaluate to true.', | ||
132, | ||
], | ||
Comment on lines
+553
to
+556
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is showing up because |
||
]); | ||
} | ||
|
||
public function testNonEmptySpecifiedString(): void | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure if
isSuperTypeOf
and friends are really correct, e.g. I did not touchisSubTypeOf
oraccept
at all