Skip to content

Commit b05e44e

Browse files
committed
added Container::getComponent() and $container['…'] return type narrowing
1 parent fd5c9f1 commit b05e44e

7 files changed

Lines changed: 131 additions & 5 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ vendor/bin/tester tests/SomeTest.phpt -s # Run a single test
2020
- **`src/Tester/TypeAssert.php`** — Reusable type inference testing helper for Nette Tester (used by other Nette packages)
2121
- **`extension.neon`** — Entry point, includes `extension-php.neon` and `extension-nette.neon`, auto-included by `phpstan/extension-installer`
2222
- **`extension-php.neon`** — Generic PHP-level extensions (RemoveFailingReturnType, ClosureTypeCheckIgnore)
23-
- **`extension-nette.neon`** — All Nette package extensions (schema, tester, utils), separated by comments
23+
- **`extension-nette.neon`** — All Nette package extensions (component-model, schema, tester, utils), separated by comments
2424
- **`phpstan.neon`** — Self-analysis config (level 8, analyses `src/` and `tests/`)
2525

2626
### How extensions are registered
@@ -39,7 +39,7 @@ Each extension class is registered as a service in NEON with the appropriate tag
3939

4040
### Namespace conventions
4141

42-
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\`.
42+
Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\ComponentModel\` for nette/component-model, `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\`.
4343

4444
### ExpectArrayReturnTypeExtension
4545

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

7070
`ArraysInvokeTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows return types of `Arrays::invoke()` and `Arrays::invokeMethod()` from `array`. For `invoke()`, it extracts the callable return type from the iterable value type and forwards `...$args` via `ParametersAcceptorSelector::selectFromArgs()` to resolve the correct overload. For `invokeMethod()`, it resolves constant method names on the object type, gets method reflection, and forwards remaining args. Handles `callable(): void` by converting void to null. Falls back to declared return type when callbacks are not callable, method names are not constant strings, or methods don't exist on the object type. Config: `extension-nette.neon`.
7171

72+
### GetComponentReturnTypeExtension
73+
74+
`GetComponentReturnTypeExtension` (`DynamicMethodReturnTypeExtension`) narrows return types of `Container::getComponent()` and `Container::offsetGet()` (i.e. `$this['xxx']`). When the component name is a constant string, it looks for a `createComponent<Name>()` factory method on the caller type and returns its return type — e.g. `$this->getComponent('poll')` returns `PollControl` if `createComponentPoll(): PollControl` exists. Falls back to the declared return type when no factory method is found. Config: `extension-nette.neon`.
75+
7276
### AssertTypeNarrowingExtension
7377

7478
`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-nette.neon`.

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
},
1818
"require-dev": {
1919
"nette/tester": "^2.6",
20-
"nette/schema": "^1.3"
20+
"nette/schema": "^1.3",
21+
"nette/component-model": "^3.1",
22+
"nette/forms": "^3.2"
2123
},
2224
"autoload": {
2325
"psr-4": {

extension-nette.neon

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
# nette/schema
1+
# nette/component-model
22
services:
3+
-
4+
class: Nette\PHPStan\ComponentModel\GetComponentReturnTypeExtension
5+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
6+
7+
# nette/schema
38
-
49
class: Nette\PHPStan\Schema\ExpectArrayReturnTypeExtension
510
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ includes:
4141

4242
<!---->
4343

44-
**Precise return types** — narrows return types of `Strings::match()`, `matchAll()`, `split()`, `Helpers::falseToNull()`, `Expect::array()`, `Arrays::invoke()`, and `Arrays::invokeMethod()` based on the arguments you pass.
44+
**Precise return types** — narrows return types of `Strings::match()`, `matchAll()`, `split()`, `Helpers::falseToNull()`, `Expect::array()`, `Arrays::invoke()`, and `Arrays::invokeMethod()` based on the arguments you pass. Also narrows `Container::getComponent()` and `$container['...']` to match the corresponding `createComponent*()` factory return type.
4545

4646
**Removes `|false` and `|null` from PHP functions** — many native functions like `getcwd`, `json_encode`, `preg_split`, `preg_replace`, and [many more](extension-php.neon) include `false` or `null` in their return type even though these error values are unrealistic on modern systems.
4747

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Nette\PHPStan\ComponentModel;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\MethodReflection;
8+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
9+
use PHPStan\Type\Type;
10+
use function count, in_array, ucfirst;
11+
12+
13+
/**
14+
* Narrows return types of Container::getComponent() and Container::offsetGet()
15+
* based on the corresponding createComponent<Name>() factory method.
16+
*/
17+
class GetComponentReturnTypeExtension implements DynamicMethodReturnTypeExtension
18+
{
19+
public function getClass(): string
20+
{
21+
return 'Nette\ComponentModel\Container';
22+
}
23+
24+
25+
public function isMethodSupported(MethodReflection $methodReflection): bool
26+
{
27+
return in_array($methodReflection->getName(), ['getComponent', 'offsetGet'], true);
28+
}
29+
30+
31+
public function getTypeFromMethodCall(
32+
MethodReflection $methodReflection,
33+
MethodCall $methodCall,
34+
Scope $scope,
35+
): ?Type
36+
{
37+
$args = $methodCall->getArgs();
38+
if ($args === []) {
39+
return null;
40+
}
41+
42+
$nameType = $scope->getType($args[0]->value);
43+
$constantStrings = $nameType->getConstantStrings();
44+
if (count($constantStrings) !== 1) {
45+
return null;
46+
}
47+
48+
$componentName = $constantStrings[0]->getValue();
49+
$factoryMethodName = 'createComponent' . ucfirst($componentName);
50+
51+
$callerType = $scope->getType($methodCall->var);
52+
if (!$callerType->hasMethod($factoryMethodName)->yes()) {
53+
return null;
54+
}
55+
56+
$factoryMethod = $callerType->getMethod($factoryMethodName, $scope);
57+
return $factoryMethod->getVariants()[0]->getReturnType();
58+
}
59+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\ComponentModel\Container;
4+
use function PHPStan\Testing\assertType;
5+
6+
7+
class PollControl extends Container implements \ArrayAccess
8+
{
9+
use \Nette\ComponentModel\ArrayAccess;
10+
}
11+
12+
class CalendarControl extends Container implements \ArrayAccess
13+
{
14+
use \Nette\ComponentModel\ArrayAccess;
15+
}
16+
17+
class TestPresenter extends Container implements \ArrayAccess
18+
{
19+
use \Nette\ComponentModel\ArrayAccess;
20+
21+
protected function createComponentPoll(): PollControl
22+
{
23+
return new PollControl;
24+
}
25+
26+
27+
protected function createComponentCalendar(): CalendarControl
28+
{
29+
return new CalendarControl;
30+
}
31+
32+
33+
public function test(): void
34+
{
35+
assertType('PollControl', $this->getComponent('poll'));
36+
assertType('CalendarControl', $this->getComponent('calendar'));
37+
assertType('PollControl', $this['poll']);
38+
assertType('CalendarControl', $this['calendar']);
39+
}
40+
}
41+
42+
43+
// no factory method → falls back to declared return type
44+
class EmptyPresenter extends Container implements \ArrayAccess
45+
{
46+
use \Nette\ComponentModel\ArrayAccess;
47+
48+
public function test(): void
49+
{
50+
assertType('Nette\ComponentModel\IComponent', $this->getComponent('unknown'));
51+
assertType('Nette\ComponentModel\IComponent', $this['unknown']);
52+
}
53+
}

tests/extensions.phpt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ TypeAssert::assertTypes(__DIR__ . '/Tester/assert-type-narrowing.php');
1717
TypeAssert::assertTypes(__DIR__ . '/Tester/assert-in-function.php');
1818
TypeAssert::assertTypes(__DIR__ . '/Tester/assert-type-with-custom-class.php');
1919

20+
// ComponentModel
21+
TypeAssert::assertTypes(__DIR__ . '/ComponentModel/get-component-return-type.php');
22+
2023
// Utils
2124
TypeAssert::assertTypes(__DIR__ . '/Utils/false-to-null-return-type.php');
2225
TypeAssert::assertTypes(__DIR__ . '/Utils/strings-return-type.php');

0 commit comments

Comments
 (0)