Skip to content

Commit 9858ad6

Browse files
committed
added Html::getXxx(), setXxx(), addXxx() magic method resolution
1 parent e9e31ae commit 9858ad6

6 files changed

Lines changed: 204 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
### HtmlMethodsClassReflectionExtension
73+
74+
`HtmlMethodsClassReflectionExtension` (`MethodsClassReflectionExtension`) resolves `getXxx()`, `setXxx()`, and `addXxx()` magic methods on `Nette\Utils\Html` that go through `__call()` but aren't declared via `@method` annotations. `getXxx()` returns `mixed`, `setXxx()` and `addXxx()` return `static`. Config: `extension-nette.neon`.
75+
7276
### GetComponentReturnTypeExtension
7377

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

extension-nette.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,6 @@ services:
4949
-
5050
class: Nette\PHPStan\Utils\ArraysInvokeTypeExtension
5151
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
52+
-
53+
class: Nette\PHPStan\Utils\HtmlMethodsClassReflectionExtension
54+
tags: [phpstan.broker.methodsClassReflectionExtension]

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ includes:
4343

4444
**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. For forms, `$form['name']` returns the specific control type (e.g. `TextInput`, `SelectBox`) based on the `addText()`, `addSelect()`, etc. call in the same function.
4545

46+
**Html magic methods** — resolves `$html->getXxx()`, `setXxx()`, and `addXxx()` calls on `Nette\Utils\Html` that go through `__call()` but aren't declared via `@method` annotations.
47+
4648
**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.
4749

4850
**Assert type narrowing** — PHPStan understands type guarantees after `Tester\Assert` calls like `notNull()`, `type()`, `true()`, etc.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Nette\PHPStan\Utils;
4+
5+
use PHPStan\Reflection\ClassMemberReflection;
6+
use PHPStan\Reflection\ClassReflection;
7+
use PHPStan\Reflection\FunctionVariant;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\MethodsClassReflectionExtension;
10+
use PHPStan\TrinaryLogic;
11+
use PHPStan\Type\Generic\TemplateTypeMap;
12+
use PHPStan\Type\MixedType;
13+
use PHPStan\Type\StaticType;
14+
use PHPStan\Type\Type;
15+
use function in_array, strlen, substr;
16+
17+
18+
/**
19+
* Resolves getXxx(), setXxx(), addXxx() magic methods on Nette\Utils\Html
20+
* that are handled by __call() but not declared via @method annotations.
21+
*/
22+
final class HtmlMethodsClassReflectionExtension implements MethodsClassReflectionExtension
23+
{
24+
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
25+
{
26+
if (!$classReflection->is('Nette\Utils\Html')) {
27+
return false;
28+
}
29+
30+
return strlen($methodName) > 3
31+
&& in_array(substr($methodName, 0, 3), ['get', 'set', 'add'], true);
32+
}
33+
34+
35+
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
36+
{
37+
$prefix = substr($methodName, 0, 3);
38+
return new HtmlCallMethodReflection($classReflection, $methodName, $prefix);
39+
}
40+
}
41+
42+
43+
/**
44+
* @internal
45+
*/
46+
final class HtmlCallMethodReflection implements MethodReflection
47+
{
48+
public function __construct(
49+
private ClassReflection $declaringClass,
50+
private string $name,
51+
private string $prefix,
52+
) {
53+
}
54+
55+
56+
public function getDeclaringClass(): ClassReflection
57+
{
58+
return $this->declaringClass;
59+
}
60+
61+
62+
public function isStatic(): bool
63+
{
64+
return false;
65+
}
66+
67+
68+
public function isPrivate(): bool
69+
{
70+
return false;
71+
}
72+
73+
74+
public function isPublic(): bool
75+
{
76+
return true;
77+
}
78+
79+
80+
public function getDocComment(): ?string
81+
{
82+
return null;
83+
}
84+
85+
86+
public function getName(): string
87+
{
88+
return $this->name;
89+
}
90+
91+
92+
public function getPrototype(): ClassMemberReflection
93+
{
94+
return $this;
95+
}
96+
97+
98+
public function getVariants(): array
99+
{
100+
if ($this->prefix === 'get') {
101+
return [
102+
new FunctionVariant(
103+
TemplateTypeMap::createEmpty(),
104+
TemplateTypeMap::createEmpty(),
105+
[],
106+
false,
107+
new MixedType,
108+
),
109+
];
110+
}
111+
112+
return [
113+
new FunctionVariant(
114+
TemplateTypeMap::createEmpty(),
115+
TemplateTypeMap::createEmpty(),
116+
[],
117+
true,
118+
new StaticType($this->declaringClass),
119+
),
120+
];
121+
}
122+
123+
124+
public function isDeprecated(): TrinaryLogic
125+
{
126+
return TrinaryLogic::createNo();
127+
}
128+
129+
130+
public function getDeprecatedDescription(): ?string
131+
{
132+
return null;
133+
}
134+
135+
136+
public function isFinal(): TrinaryLogic
137+
{
138+
return TrinaryLogic::createYes();
139+
}
140+
141+
142+
public function isInternal(): TrinaryLogic
143+
{
144+
return TrinaryLogic::createNo();
145+
}
146+
147+
148+
public function getThrowType(): ?Type
149+
{
150+
return null;
151+
}
152+
153+
154+
public function hasSideEffects(): TrinaryLogic
155+
{
156+
return $this->prefix === 'get'
157+
? TrinaryLogic::createNo()
158+
: TrinaryLogic::createYes();
159+
}
160+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types=1);
2+
3+
use Nette\Utils\Html;
4+
use function PHPStan\Testing\assertType;
5+
6+
$html = Html::el('div');
7+
8+
// @property — string, bool, int, float
9+
assertType('string|null', $html->class);
10+
assertType('bool|null', $html->checked);
11+
assertType('int|null', $html->cols);
12+
assertType('float|null', $html->high);
13+
assertType('string|null', $html->id);
14+
15+
// universalObjectCratesClasses — undeclared property
16+
assertType('mixed', $html->nonExistent);
17+
18+
// @method — fluent setters
19+
assertType('Nette\Utils\Html', $html->class('foo'));
20+
assertType('Nette\Utils\Html', $html->checked(true));
21+
assertType('Nette\Utils\Html', $html->cols(80));
22+
assertType('Nette\Utils\Html', $html->high(1.0));
23+
24+
// @method — with second argument (appendAttribute)
25+
assertType('Nette\Utils\Html', $html->class('foo', true));
26+
27+
// __call — getXxx(), setXxx(), addXxx()
28+
assertType('mixed', $html->getClass());
29+
assertType('Nette\Utils\Html', $html->setClass('foo'));
30+
assertType('Nette\Utils\Html', $html->addClass('bar'));
31+
32+
// real methods — return static
33+
assertType('Nette\Utils\Html', $html->href('/path'));
34+
assertType('Nette\Utils\Html', $html->data('key', 'value'));

tests/extensions.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ TypeAssert::assertTypes(__DIR__ . '/Forms/form-component-return-type.php');
2727
TypeAssert::assertTypes(__DIR__ . '/Utils/false-to-null-return-type.php');
2828
TypeAssert::assertTypes(__DIR__ . '/Utils/strings-return-type.php');
2929
TypeAssert::assertTypes(__DIR__ . '/Utils/arrays-invoke-return-type.php');
30+
TypeAssert::assertTypes(__DIR__ . '/Utils/html-virtual-members.php');

0 commit comments

Comments
 (0)