Skip to content

Commit 4398656

Browse files
committed
added FalseToNullReturnTypeExtension
1 parent cc745ef commit 4398656

7 files changed

Lines changed: 128 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Each extension class is registered as a service in NEON with the appropriate tag
3838

3939
### Namespace conventions
4040

41-
Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\Schema\` for nette/schema, future packages follow the same pattern (`Nette\PHPStan\Forms\`, `Nette\PHPStan\Application\`, etc.). Generic PHP-level extensions use `Nette\PHPStan\Php\`.
41+
Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\Schema\` for nette/schema, `Nette\PHPStan\Utils\` for nette/utils, future packages follow the same pattern (`Nette\PHPStan\Forms\`, `Nette\PHPStan\Application\`, etc.). Generic PHP-level extensions use `Nette\PHPStan\Php\`.
4242

4343
### ExpectArrayReturnTypeExtension
4444

@@ -56,6 +56,10 @@ Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\
5656

5757
`RemoveFalseReturnTypeExtension` (`ExpressionTypeResolverExtension`) removes `|false` from return types of native PHP functions and methods where false 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. Config: `extension-php.neon`.
5858

59+
### FalseToNullReturnTypeExtension
60+
61+
`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-utils.neon`.
62+
5963
### AssertTypeNarrowingExtension
6064

6165
`AssertTypeNarrowingExtension` (`StaticMethodTypeSpecifyingExtension` + `TypeSpecifierAwareExtension`) narrows variable types after `Tester\Assert` assertion calls. Each assertion method is mapped to an equivalent PHP expression that PHPStan already understands, then delegated to `TypeSpecifier::specifyTypesInCondition()`. Supported methods: `null`, `notNull`, `true`, `false`, `truthy`, `falsey`, `same`, `notSame`, and `type` (with built-in type strings like `'string'`, `'int'`, etc. and class/interface names). Config: `extension-tester.neon`.

extension-utils.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
services:
2+
-
3+
class: Nette\PHPStan\Utils\FalseToNullReturnTypeExtension
4+
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]

extension.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ includes:
1717
- extension-php.neon
1818
- extension-schema.neon
1919
- extension-tester.neon
20+
- extension-utils.neon

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Assert::type('int', $value);
8484

8585
### Other
8686

87+
- Narrows the return type of `Helpers::falseToNull()` — e.g. `string|false``string|null`
8788
- Narrows the return type of `Expect::array()` from `Structure|Type` to `Structure` or `Type` based on the argument
8889

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

0 commit comments

Comments
 (0)