Skip to content

Commit a706950

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

7 files changed

Lines changed: 151 additions & 6 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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
"phpstan/phpstan": "^2.1.40"
1717
},
1818
"require-dev": {
19-
"nette/tester": "^2.6",
20-
"nette/schema": "^1.3"
19+
"nette/component-model": "^3.1",
20+
"nette/forms": "^3.2",
21+
"nette/schema": "^1.3",
22+
"nette/tester": "^2.6"
2123
},
2224
"autoload": {
2325
"psr-4": {

extension-nette.neon

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ parameters:
66
- Nette\Security\SimpleIdentity
77
- Nette\Utils\Html
88

9-
# nette/schema
9+
1010
services:
11+
# nette/component-model
12+
-
13+
create: Nette\PHPStan\ComponentModel\GetComponentReturnTypeExtension
14+
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
15+
16+
# nette/schema
1117
-
1218
create: Nette\PHPStan\Schema\ExpectArrayReturnTypeExtension
1319
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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 PHPStan\Type\TypeCombinator;
11+
use function count, in_array, ucfirst;
12+
13+
14+
/**
15+
* Narrows return types of Container::getComponent() and Container::offsetGet()
16+
* based on the corresponding createComponent<Name>() factory method.
17+
*/
18+
class GetComponentReturnTypeExtension implements DynamicMethodReturnTypeExtension
19+
{
20+
public function getClass(): string
21+
{
22+
return 'Nette\ComponentModel\Container';
23+
}
24+
25+
26+
public function isMethodSupported(MethodReflection $methodReflection): bool
27+
{
28+
return in_array($methodReflection->getName(), ['getComponent', 'offsetGet'], true);
29+
}
30+
31+
32+
public function getTypeFromMethodCall(
33+
MethodReflection $methodReflection,
34+
MethodCall $methodCall,
35+
Scope $scope,
36+
): ?Type
37+
{
38+
$args = $methodCall->getArgs();
39+
if ($args === []) {
40+
return null;
41+
}
42+
43+
$nameType = $scope->getType($args[0]->value);
44+
$constantStrings = $nameType->getConstantStrings();
45+
if (count($constantStrings) !== 1) {
46+
return null;
47+
}
48+
49+
$componentName = $constantStrings[0]->getValue();
50+
$factoryMethodName = 'createComponent' . ucfirst($componentName);
51+
52+
$callerType = $scope->getType($methodCall->var);
53+
if (!$callerType->hasMethod($factoryMethodName)->yes()) {
54+
return null;
55+
}
56+
57+
$factoryMethod = $callerType->getMethod($factoryMethodName, $scope);
58+
$returnType = $factoryMethod->getVariants()[0]->getReturnType();
59+
60+
// Respect $throw parameter for getComponent()
61+
if ($methodReflection->getName() === 'getComponent' && count($args) >= 2) {
62+
$throwType = $scope->getType($args[1]->value);
63+
if (!$throwType->isTrue()->yes()) {
64+
$returnType = TypeCombinator::addNull($returnType);
65+
}
66+
}
67+
68+
return $returnType;
69+
}
70+
}
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+
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+
// $throw = false → nullable
41+
assertType('PollControl|null', $this->getComponent('poll', false));
42+
assertType('CalendarControl|null', $this->getComponent('calendar', false));
43+
}
44+
}
45+
46+
47+
// no factory method → falls back to declared return type
48+
class EmptyPresenter extends Container implements \ArrayAccess
49+
{
50+
use \Nette\ComponentModel\ArrayAccess;
51+
52+
public function test(): void
53+
{
54+
assertType('Nette\ComponentModel\IComponent', $this->getComponent('unknown'));
55+
assertType('Nette\ComponentModel\IComponent', $this['unknown']);
56+
57+
// $throw = false → nullable (falls back to declared conditional return type)
58+
assertType('Nette\ComponentModel\IComponent|null', $this->getComponent('unknown', false));
59+
}
60+
}

tests/extensions.phpt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ TypeAssert::assertTypes(__DIR__ . '/Php/failing-return-type.php');
99
TypeAssert::assertNoErrors(__DIR__ . '/Php/arrow-function-void.php', [__DIR__ . '/Php/arrow-function-void.neon']);
1010
TypeAssert::assertNoErrors(__DIR__ . '/Php/closure-type-check.php');
1111

12+
// ComponentModel
13+
TypeAssert::assertTypes(__DIR__ . '/ComponentModel/get-component-return-type.php');
14+
1215
// Schema
1316
TypeAssert::assertTypes(__DIR__ . '/Schema/expect-array-return-type.php');
1417

0 commit comments

Comments
 (0)