From 1e868966c3de55a087e5ec938189ec34a1648b04 Mon Sep 17 00:00:00 2001
From: David Grudl <david@grudl.com>
Date: Sun, 12 May 2024 19:52:37 +0200
Subject: [PATCH] ApplicationExtension: checks the correct usage of attributes

---
 .../ApplicationDI/ApplicationExtension.php    | 41 +++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php
index a874f55f4..5acd5bf97 100644
--- a/src/Bridges/ApplicationDI/ApplicationExtension.php
+++ b/src/Bridges/ApplicationDI/ApplicationExtension.php
@@ -11,9 +11,11 @@
 
 use Composer\Autoload\ClassLoader;
 use Nette;
+use Nette\Application\Attributes;
 use Nette\Application\UI;
 use Nette\DI\Definitions;
 use Nette\Schema\Expect;
+use Nette\Utils\Reflection;
 use Tracy;
 
 
@@ -24,6 +26,7 @@ final class ApplicationExtension extends Nette\DI\CompilerExtension
 {
 	private readonly array $scanDirs;
 	private int $invalidLinkMode;
+	private array $checked = [];
 
 
 	public function __construct(
@@ -135,6 +138,7 @@ public function beforeCompile(): void
 
 		$counter = 0;
 		foreach ($this->findPresenters() as $class) {
+			$this->checkPresenter($class);
 			if (empty($all[$class])) {
 				$all[$class] = $builder->addDefinition($this->prefix((string) ++$counter))
 					->setType($class);
@@ -247,4 +251,41 @@ public static function generateNewPresenterFileContents(string $file, ?string $c
 
 		return $res . "use Nette;\n\n\nclass $class extends Nette\\Application\\UI\\Presenter\n{\n\$END\$\n}\n";
 	}
+
+
+	private function checkPresenter(string $class): void
+	{
+		if (!is_subclass_of($class, UI\Presenter::class) || isset($this->checked[$class])) {
+			return;
+		}
+		$this->checked[$class] = true;
+
+		$rc = new \ReflectionClass($class);
+		if ($rc->getParentClass()) {
+			$this->checkPresenter($rc->getParentClass()->getName());
+		}
+
+		foreach ($rc->getProperties() as $rp) {
+			if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class))
+				&& (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly())
+			) {
+				throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr));
+			}
+		}
+
+		$re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.';
+		foreach ($rc->getMethods() as $rm) {
+			if (preg_match("#^(?!handleInvalidLink)($re)#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) {
+				throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm)));
+			} elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) {
+				throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm)));
+			} elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF)
+				&& !preg_match("#^$re|createComponent.#", $rm->getName())
+			) {
+				throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render, handle or createComponent methods.', Reflection::toString($rm), Attributes\Requires::class));
+			} elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) {
+				throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render or handle methods.', Reflection::toString($rm), Attributes\Deprecated::class));
+			}
+		}
+	}
 }