From 7c6653b65b2b9263fac7f465167518754e417544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 6 Nov 2026 01:22:34 +0100 Subject: [PATCH] Know the static Html::text() and Html::html() factories nette/utils 4.1.5 added `Html::text()` and `Html::html()` as `@method static` on the class and deprecated `Html::fromText()` and `Html::fromHtml()` in their favor. PHPStan never sees those declarations: the stub for the class replaces its docblock, and `HtmlClassReflectionExtension` claims every method name as an attribute setter, an instance method, so a call to either factory is reported as a static call to an instance method. The stub now declares the two methods the way the class does, and the extension leaves their names alone, because PHPStan asks the extension before it reads the annotations and the extension would otherwise keep winning. Either change alone is not enough: with only the stub the extension still answers first, with only the extension the calls are undefined, since the stub also hides `__callStatic()`. --- .../Nette/HtmlClassReflectionExtension.php | 4 ++- stubs/Utils/Html.stub | 2 ++ .../HtmlClassReflectionExtensionTest.php | 16 ++++++++-- tests/Rule/Nette/HtmlStaticFactoriesTest.php | 32 +++++++++++++++++++ .../Rule/Nette/data/html-static-factories.php | 12 +++++++ 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/Rule/Nette/HtmlStaticFactoriesTest.php create mode 100644 tests/Rule/Nette/data/html-static-factories.php diff --git a/src/Reflection/Nette/HtmlClassReflectionExtension.php b/src/Reflection/Nette/HtmlClassReflectionExtension.php index 1f6d391..c71a789 100644 --- a/src/Reflection/Nette/HtmlClassReflectionExtension.php +++ b/src/Reflection/Nette/HtmlClassReflectionExtension.php @@ -7,13 +7,15 @@ use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Reflection\PropertiesClassReflectionExtension; use PHPStan\Reflection\PropertyReflection; +use function in_array; class HtmlClassReflectionExtension implements MethodsClassReflectionExtension, PropertiesClassReflectionExtension { public function hasMethod(ClassReflection $classReflection, string $methodName): bool { - return $classReflection->is('Nette\Utils\Html'); + // The static factories declared in the stub, which would otherwise be taken for attribute setters + return $classReflection->is('Nette\Utils\Html') && !in_array($methodName, ['text', 'html'], true); } public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection diff --git a/stubs/Utils/Html.stub b/stubs/Utils/Html.stub index 1ee2b50..6018297 100644 --- a/stubs/Utils/Html.stub +++ b/stubs/Utils/Html.stub @@ -5,6 +5,8 @@ namespace Nette\Utils; /** * @implements \IteratorAggregate * @implements \ArrayAccess + * @method static static text(mixed $text) + * @method static static html(mixed $html) */ class Html implements \ArrayAccess, \IteratorAggregate { diff --git a/tests/Reflection/Nette/HtmlClassReflectionExtensionTest.php b/tests/Reflection/Nette/HtmlClassReflectionExtensionTest.php index 6dc1978..96f0af1 100644 --- a/tests/Reflection/Nette/HtmlClassReflectionExtensionTest.php +++ b/tests/Reflection/Nette/HtmlClassReflectionExtensionTest.php @@ -28,10 +28,22 @@ public function dataHasMethod(): array return [ [ Html::class, + 'href', true, ], + [ + Html::class, + 'text', + false, + ], + [ + Html::class, + 'html', + false, + ], [ stdClass::class, + 'href', false, ], ]; @@ -40,10 +52,10 @@ public function dataHasMethod(): array /** * @dataProvider dataHasMethod */ - public function testHasMethod(string $className, bool $result): void + public function testHasMethod(string $className, string $methodName, bool $result): void { $classReflection = $this->reflectionProvider->getClass($className); - self::assertSame($result, $this->extension->hasMethod($classReflection, 'href')); + self::assertSame($result, $this->extension->hasMethod($classReflection, $methodName)); } public function testGetMethod(): void diff --git a/tests/Rule/Nette/HtmlStaticFactoriesTest.php b/tests/Rule/Nette/HtmlStaticFactoriesTest.php new file mode 100644 index 0000000..311f1d9 --- /dev/null +++ b/tests/Rule/Nette/HtmlStaticFactoriesTest.php @@ -0,0 +1,32 @@ + + */ +class HtmlStaticFactoriesTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return self::getContainer()->getByType(CallStaticMethodsRule::class); // @phpstan-ignore phpstanApi.classConstant + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/html-static-factories.php'], []); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../extension.neon', + ]; + } + +} diff --git a/tests/Rule/Nette/data/html-static-factories.php b/tests/Rule/Nette/data/html-static-factories.php new file mode 100644 index 0000000..e4d0863 --- /dev/null +++ b/tests/Rule/Nette/data/html-static-factories.php @@ -0,0 +1,12 @@ +html'); + return $text->addHtml($html); +}