Skip to content

Commit dd6b7db

Browse files
committed
added Helpers::falseToNull() return type narrowing (string|false → string|null)
1 parent b2e134b commit dd6b7db

5 files changed

Lines changed: 110 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\
5757

5858
`RemoveFailingReturnTypeExtension` (`ExpressionTypeResolverExtension`) removes `|false` or `|null` from return types of native PHP functions and methods where the error return value is trivial or outdated. It handles `FuncCall`, `MethodCall`, and `StaticCall` in a single class. Configuration uses a flat list in NEON — plain names for functions (`json_encode`), `Class::method` notation for methods (`Normalizer::normalize`). It runs before all `DynamicReturnTypeExtension` implementations, delegates to them via `DynamicReturnTypeExtensionRegistry`, and strips `|false` from the result. For `preg_replace`, `preg_replace_callback`, `preg_replace_callback_array`, and `preg_filter` it strips `|null` instead (these return null on PCRE error). For `preg_replace_callback_array` pattern validation checks array keys. Config: `extension-php.neon`.
5959

60+
### FalseToNullReturnTypeExtension
61+
62+
`FalseToNullReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows the return type of `Helpers::falseToNull()` from `mixed`. It removes `false` from the argument type and adds `null` — e.g. `string|false``string|null`, `false``null`, types without `false` pass through unchanged. Config: `extension-nette.neon`.
63+
6064
### StringsReturnTypeExtension
6165

6266
`StringsReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows return types of `Strings::match()`, `matchAll()` and `split()` based on boolean arguments. It resolves `captureOffset`, `unmatchedAsNull`, `patternOrder`, and `lazy` to constant booleans and constructs the precise return type — e.g. `match()` with `captureOffset: true` returns `array<array{string, int<0, max>}>|null` instead of `?array`. When a boolean argument is not a constant, falls back to the declared return type. Config: `extension-nette.neon`.

extension-nette.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ services:
2222
tags: [phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension]
2323

2424
# nette/utils
25+
-
26+
create: Nette\PHPStan\Utils\FalseToNullReturnTypeExtension
27+
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
2528
-
2629
create: Nette\PHPStan\Utils\StringsReturnTypeExtension
2730
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Nette\PHPStan\Utils;
4+
5+
use Nette\Utils\Helpers;
6+
use PhpParser\Node\Expr\StaticCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Type\Constant\ConstantBooleanType;
10+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
11+
use PHPStan\Type\NeverType;
12+
use PHPStan\Type\NullType;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeCombinator;
15+
16+
17+
/**
18+
* Narrows the return type of Helpers::falseToNull() from mixed.
19+
* Removes false from the argument type and adds null instead.
20+
*/
21+
class FalseToNullReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
22+
{
23+
public function getClass(): string
24+
{
25+
return Helpers::class;
26+
}
27+
28+
29+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
30+
{
31+
return $methodReflection->getName() === 'falseToNull';
32+
}
33+
34+
35+
public function getTypeFromStaticMethodCall(
36+
MethodReflection $methodReflection,
37+
StaticCall $methodCall,
38+
Scope $scope,
39+
): ?Type
40+
{
41+
$args = $methodCall->getArgs();
42+
if ($args === []) {
43+
return null;
44+
}
45+
46+
$argType = $scope->getType($args[0]->value);
47+
$falseType = new ConstantBooleanType(false);
48+
49+
if ($falseType->isSuperTypeOf($argType)->no()) {
50+
return $argType;
51+
}
52+
53+
$withoutFalse = TypeCombinator::remove($argType, $falseType);
54+
if ($withoutFalse instanceof NeverType) {
55+
return new NullType;
56+
}
57+
58+
return TypeCombinator::addNull($withoutFalse);
59+
}
60+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\Utils\Helpers;
4+
use function PHPStan\Testing\assertType;
5+
6+
7+
// false → null
8+
assertType('null', Helpers::falseToNull(false));
9+
10+
// no false in type → unchanged
11+
assertType("'hello'", Helpers::falseToNull('hello'));
12+
assertType('123', Helpers::falseToNull(123));
13+
14+
15+
function testStringFalse(string|false $value): void
16+
{
17+
assertType('string|null', Helpers::falseToNull($value));
18+
}
19+
20+
21+
function testIntFalse(int|false $value): void
22+
{
23+
assertType('int|null', Helpers::falseToNull($value));
24+
}
25+
26+
27+
function testStringFalseNull(string|false|null $value): void
28+
{
29+
assertType('string|null', Helpers::falseToNull($value));
30+
}
31+
32+
33+
function testBool(bool $value): void
34+
{
35+
assertType('true|null', Helpers::falseToNull($value));
36+
}
37+
38+
39+
function testNoFalse(int $value): void
40+
{
41+
assertType('int', Helpers::falseToNull($value));
42+
}

tests/extensions.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ TypeAssert::assertTypes(__DIR__ . '/Tester/assert-in-function.php');
1818
TypeAssert::assertTypes(__DIR__ . '/Tester/assert-type-with-custom-class.php');
1919

2020
// Utils
21+
TypeAssert::assertTypes(__DIR__ . '/Utils/false-to-null-return-type.php');
2122
TypeAssert::assertTypes(__DIR__ . '/Utils/strings-return-type.php');

0 commit comments

Comments
 (0)