From 5378e6332f7081472c2119ac28bb62f01b5ff448 Mon Sep 17 00:00:00 2001 From: Martin Jonas Date: Tue, 8 Aug 2023 23:07:01 +0200 Subject: [PATCH] LinksRule for checking validity of links --- composer.json | 1 + extension.neon | 19 + .../InvalidLinkDestinationException.php | 25 ++ src/Exceptions/InvalidLinkException.php | 10 + src/Exceptions/InvalidLinkParamsException.php | 8 + src/Exceptions/LinkCheckFailedException.php | 10 + src/Nette/ContainerResolver.php | 66 +++ src/Nette/PresenterResolver.php | 142 ++++++ src/Rule/Nette/LinksRule.php | 408 ++++++++++++++++++ tests/Nette/PresenterResolverTest.php | 41 ++ tests/Nette/containerLoader.php | 8 + tests/Rule/Nette/LinksRuleTest.php | 217 ++++++++++ tests/Rule/Nette/data/links-component.php | 26 ++ tests/Rule/Nette/data/links-linkGenerator.php | 32 ++ tests/Rule/Nette/data/links-presenter.php | 45 ++ tests/TestApp/Components/CurrentComponent.php | 18 + tests/TestApp/Components/TestComponent.php | 10 + .../CurrentModule/CurrentPresenter.php | 22 + .../CurrentModule/SubModule/TestPresenter.php | 10 + .../CurrentModule/TestPresenter.php | 10 + .../TestModule/SubModule/TestPresenter.php | 10 + .../Presenters/TestModule/TestPresenter.php | 10 + tests/TestApp/Presenters/TestPresenter.php | 14 + tests/TestApp/TestContainer.php | 26 ++ tests/TestApp/autoload.php | 9 + 25 files changed, 1197 insertions(+) create mode 100644 src/Exceptions/InvalidLinkDestinationException.php create mode 100644 src/Exceptions/InvalidLinkException.php create mode 100644 src/Exceptions/InvalidLinkParamsException.php create mode 100644 src/Exceptions/LinkCheckFailedException.php create mode 100644 src/Nette/ContainerResolver.php create mode 100644 src/Nette/PresenterResolver.php create mode 100644 src/Rule/Nette/LinksRule.php create mode 100644 tests/Nette/PresenterResolverTest.php create mode 100644 tests/Nette/containerLoader.php create mode 100644 tests/Rule/Nette/LinksRuleTest.php create mode 100644 tests/Rule/Nette/data/links-component.php create mode 100644 tests/Rule/Nette/data/links-linkGenerator.php create mode 100644 tests/Rule/Nette/data/links-presenter.php create mode 100644 tests/TestApp/Components/CurrentComponent.php create mode 100644 tests/TestApp/Components/TestComponent.php create mode 100644 tests/TestApp/Presenters/CurrentModule/CurrentPresenter.php create mode 100644 tests/TestApp/Presenters/CurrentModule/SubModule/TestPresenter.php create mode 100644 tests/TestApp/Presenters/CurrentModule/TestPresenter.php create mode 100644 tests/TestApp/Presenters/TestModule/SubModule/TestPresenter.php create mode 100644 tests/TestApp/Presenters/TestModule/TestPresenter.php create mode 100644 tests/TestApp/Presenters/TestPresenter.php create mode 100644 tests/TestApp/TestContainer.php create mode 100644 tests/TestApp/autoload.php diff --git a/composer.json b/composer.json index 78a1277..3437225 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ }, "require-dev": { "nette/application": "^3.0", + "nette/di": "^2.3.0 || ^3.0.0", "nette/forms": "^3.0", "nette/utils": "^2.3.0 || ^3.0.0", "nikic/php-parser": "^4.13.2", diff --git a/extension.neon b/extension.neon index c8c147f..4a00680 100644 --- a/extension.neon +++ b/extension.neon @@ -1,4 +1,7 @@ parameters: + nette: + containerLoader: null + applicationMapping: [] additionalConstructors: - Nette\Application\UI\Presenter::startup exceptions: @@ -48,7 +51,23 @@ parameters: - terminate - forward +parametersSchema: + nette: structure([ + containerLoader: schema(string(), nullable()) + applicationMapping: arrayOf(string(), string()) + ]) + services: + netteContainerResolver: + class: PHPStan\Nette\ContainerResolver + arguments: + - %nette.containerLoader% + + nettePresenterResolver: + class: PHPStan\Nette\PresenterResolver + arguments: + - %nette.applicationMapping% + - class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension tags: diff --git a/src/Exceptions/InvalidLinkDestinationException.php b/src/Exceptions/InvalidLinkDestinationException.php new file mode 100644 index 0000000..9877d5a --- /dev/null +++ b/src/Exceptions/InvalidLinkDestinationException.php @@ -0,0 +1,25 @@ +destination = $destination; + } + + public function getDestination(): string + { + return $this->destination; + } + +} diff --git a/src/Exceptions/InvalidLinkException.php b/src/Exceptions/InvalidLinkException.php new file mode 100644 index 0000000..e027649 --- /dev/null +++ b/src/Exceptions/InvalidLinkException.php @@ -0,0 +1,10 @@ +containerLoader = $containerLoader; + } + + public function getContainer(): ?Container + { + if ($this->container === false) { + return null; + } + + if ($this->container !== null) { + return $this->container; + } + + if ($this->containerLoader === null) { + $this->container = false; + + return null; + } + + $this->container = $this->loadContainer($this->containerLoader); + + return $this->container; + } + + + private function loadContainer(string $containerLoader): ?Container + { + if (!is_file($containerLoader)) { + throw new ShouldNotHappenException(sprintf( + 'Nette container could not be loaded: file "%s" does not exist', + $containerLoader + )); + } + + if (!is_readable($containerLoader)) { + throw new ShouldNotHappenException(sprintf( + 'Nette container could not be loaded: file "%s" is not readable', + $containerLoader + )); + } + + return require $containerLoader; + } + +} diff --git a/src/Nette/PresenterResolver.php b/src/Nette/PresenterResolver.php new file mode 100644 index 0000000..ec47774 --- /dev/null +++ b/src/Nette/PresenterResolver.php @@ -0,0 +1,142 @@ + */ + protected $mapping; + + /** @var ContainerResolver */ + private $containerResolver; + + /** @var IPresenterFactory */ + private $presenterFactory; + + /** + * @param array $mapping + */ + public function __construct(array $mapping, ContainerResolver $containerResolver) + { + $this->mapping = $mapping; + $this->containerResolver = $containerResolver; + } + + protected function getPresenterFactory(): IPresenterFactory + { + if ($this->presenterFactory === null) { + if ($this->containerResolver->getContainer() !== null) { + $this->presenterFactory = $this->containerResolver->getContainer()->getByType(IPresenterFactory::class); + } else { + $this->presenterFactory = new PresenterFactory(); + $this->presenterFactory->setMapping($this->mapping); + } + } + return $this->presenterFactory; + } + + /** + * @return array + * @throws ShouldNotHappenException + * @throws ReflectionException + */ + protected function getCurrentMapping(): array + { + if ($this->mapping !== []) { + $convertedMapping = []; + foreach ($this->mapping as $module => $mask) { + if (is_string($mask)) { + if (preg_match('#^\\\\?([\w\\\\]*\\\\)?(\w*\*\w*?\\\\)?([\w\\\\]*\*\w*)$#D', $mask, $m) !== 1) { + throw new ShouldNotHappenException(sprintf("Invalid mapping mask '%s' in parameters.nette.applicationMapping.", $mask)); + } + $convertedMapping[$module] = [$m[1], $m[2] !== '' ? $m[2] : '*Module\\', $m[3]]; + } elseif (is_array($mask) && count($mask) === 3) { /** @phpstan-ignore-line */ + $convertedMapping[$module] = [$mask[0] !== '' ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]]; + } else { + throw new ShouldNotHappenException(sprintf('Invalid mapping mask for module %s in parameters.nette.applicationMapping.', $module)); + } + } + return $convertedMapping; + } + + $presenterFactory = $this->getPresenterFactory(); + if (!$presenterFactory instanceof PresenterFactory) { + throw new ShouldNotHappenException( + 'PresenterFactory in your container is not instance of Nette\Application\PresenterFactory. We cannot get mapping from it.' . + ' Either set your mappings explicitly in parameters.nette.applicationMapping ' . + ' or replace service nettePresenterResolver with your own override of getCurrentMapping() or unformatPresenterClass().' + ); + } + + $mappingPropertyReflection = (new ReflectionClass($presenterFactory))->getProperty('mapping'); + $mappingPropertyReflection->setAccessible(true); + /** @var array $mapping */ + $mapping = $mappingPropertyReflection->getValue($presenterFactory); + + return $mapping; + } + + public function getPresenterClassByName(string $name, ?string $currentPresenterClass = null): string + { + $name = $this->resolvePresenterName($name, $currentPresenterClass); + return $this->getPresenterFactory()->getPresenterClass($name); + } + + public function resolvePresenterName(string $name, ?string $currentPresenterClass = null): string + { + if ($name[0] === ':') { + return substr($name, 1); + } + + if ($currentPresenterClass === null) { + throw new LinkCheckFailedException(sprintf("Cannot resolve relative presenter name '%s' - current presenter is not set.", $name)); + } + + $currentName = $this->unformatPresenterClass($currentPresenterClass); + $currentNameSepPos = strrpos($currentName, ':'); + if ($currentNameSepPos !== false && $currentNameSepPos !== 0) { + $currentModule = substr($currentName, 0, $currentNameSepPos); + $currentPresenter = substr($currentName, $currentNameSepPos + 1); + } else { + $currentModule = ''; + $currentPresenter = $currentName; + } + + if ($name === 'this') { + return $currentModule . ':' . $currentPresenter; + } + + return $currentModule . ':' . $name; + } + + protected function unformatPresenterClass(string $class): string + { + foreach ($this->getCurrentMapping() as $module => $mapping) { + $mapping = str_replace(['\\', '*'], ['\\\\', '(\w+)'], $mapping); + if (preg_match('#^\\\\?' . $mapping[0] . '((?:' . $mapping[1] . ')*)' . $mapping[2] . '$#Di', $class, $matches) === 1) { + return ($module === '*' ? '' : $module . ':') + . preg_replace('#' . $mapping[1] . '#iA', '$1:', $matches[1]) . $matches[3]; + } + } + + throw new LinkCheckFailedException(sprintf("Cannot convert presenter class '%s' to presenter name. No matching mapping found.", $class)); + } + +} diff --git a/src/Rule/Nette/LinksRule.php b/src/Rule/Nette/LinksRule.php new file mode 100644 index 0000000..7303f9d --- /dev/null +++ b/src/Rule/Nette/LinksRule.php @@ -0,0 +1,408 @@ + + */ +class LinksRule implements Rule +{ + + /** @var PresenterResolver */ + private $presenterResolver; + + /** @var ReflectionProvider */ + private $reflectionProvider; + + public function __construct(PresenterResolver $presenterResolver, ReflectionProvider $reflectionProvider) + { + $this->presenterResolver = $presenterResolver; + $this->reflectionProvider = $reflectionProvider; + } + + public function getNodeType(): string + { + return MethodCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->name instanceof Node\Identifier) { + return []; + } + + $methodName = $node->name->toString(); + $callerType = $scope->getType($node->var); + + if ((new ObjectType(Component::class))->isSuperTypeOf($callerType)->yes()) { + if (in_array($methodName, ['link', 'lazyLink', 'isLinkCurrent', 'redirect', 'redirectPermanent'], true)) { + return $this->checkComponentLink($scope, $callerType, $methodName, $node->getArgs()); + } elseif ((new ObjectType(Presenter::class))->isSuperTypeOf($callerType)->yes()) { + if (in_array($methodName, ['canonicalize', 'forward'], true)) { + return $this->checkComponentLink($scope, $callerType, $methodName, $node->getArgs()); + } + } + } elseif ((new ObjectType(LinkGenerator::class))->isSuperTypeOf($callerType)->yes()) { + if ($methodName === 'link') { + return $this->checkGeneratorLink($scope, $callerType, $methodName, $node->getArgs()); + } + } + + return []; + } + + /** + * @param array $args + * @return array + */ + protected function checkComponentLink(Scope $scope, Type $callerType, string $methodName, array $args): array + { + if (!isset($args[0])) { + return []; + } + + $destinations = $scope->getType($args[0]->value)->getConstantStrings(); + + if (count($destinations) === 0) { + return []; + } + + if (isset($args[1])) { + $arg1Type = $scope->getType($args[1]->value); + if (count($args) === 2 && $arg1Type->isArray()->yes()) { + $paramsOptions = $this->extractParamsFromArray($scope->getType($args[1]->value)); + } else { + $paramsOptions = $this->extractParamsFromArgs($scope, array_slice($args, 1)); + } + } else { + $paramsOptions = [[]]; + } + + return $this->checkDestinations($scope, $callerType->getObjectClassNames(), $methodName, $destinations, $paramsOptions); + } + + /** + * @param array $args + * @return array + */ + protected function checkGeneratorLink(Scope $scope, Type $callerType, string $methodName, array $args): array + { + if (!isset($args[0])) { + return []; + } + + $destinations = $scope->getType($args[0]->value)->getConstantStrings(); + + if (count($destinations) === 0) { + return []; + } + + if (isset($args[1])) { + $paramsOptions = $this->extractParamsFromArray($scope->getType($args[1]->value)); + } else { + $paramsOptions = [[]]; + } + + return $this->checkDestinations($scope, [null], $methodName, $destinations, $paramsOptions); + } + + /** + * @param array $currentClasses + * @param array $destinations + * @param array> $paramsOptions + * @return array + */ + private function checkDestinations(Scope $scope, array $currentClasses, string $methodName, array $destinations, array $paramsOptions): array + { + $errorMessages = []; + foreach ($destinations as $destination) { + if ($destination->getValue() === 'this') { + continue; + } + if ($paramsOptions === []) { + $paramsOptions = [null]; + } + foreach ($paramsOptions as $params) { + foreach ($currentClasses as $currentClass) { + try { + $this->validateLink($scope, $currentClass, $destination->getValue(), $params); + } catch (InvalidLinkDestinationException $e) { + $errorMessages[] = sprintf("Invalid link destination '%s' in %s() call", $e->getDestination(), $methodName) . + ($e->getPrevious() !== null ? ': ' . $e->getPrevious()->getMessage() : '.'); + } catch (InvalidLinkParamsException $e) { + $errorMessages[] = sprintf('Invalid link params in %s() call: ', $methodName) . $e->getMessage(); + } catch (InvalidLinkException $e) { + $errorMessages[] = sprintf('Invalid link in %s() call: ', $methodName) . $e->getMessage(); + } catch (LinkCheckFailedException $e) { + $errorMessages[] = 'Link check failed: ' . $e->getMessage(); + } catch (Throwable $e) { + $errorMessages[] = sprintf('Link check failed: %s: %s in %s on line %s', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()); + } + } + } + } + + $errors = []; + foreach (array_unique($errorMessages) as $errorMessage) { + $errors[] = RuleErrorBuilder::message($errorMessage)->build(); + } + return $errors; + } + + /** + * @param array $params + */ + private function validateLink(Scope $scope, ?string $currentClass, string $destination, ?array $params): void + { + if ($currentClass !== null) { + $reflection = $this->reflectionProvider->getClass($currentClass); + $isComponent = $reflection->is(Component::class); + $isPresenter = $reflection->is(Presenter::class); + $isLinkGenerator = false; + } else { + $isComponent = false; + $isPresenter = false; + $isLinkGenerator = true; + } + + if ($isLinkGenerator && preg_match('~^([\w:]+):(\w*+)(#.*)?()$~D', $destination) !== 1) { + throw new InvalidLinkDestinationException($destination); + } + + if (preg_match('~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', $destination, $matches) !== 1) { + throw new InvalidLinkDestinationException($destination); + } + + $path = $matches['path'] ?? ''; + $signal = $matches['signal'] ?? ''; + $query = $matches['query'] ?? ''; + + if ($query !== '') { + parse_str(substr($query, 1), $args); + $params = $params !== null ? array_merge($args, $params) : null; + } + + if (($isComponent && !$isPresenter) || $signal !== '') { + $pathSepPos = strrpos($path, ':'); + if ($pathSepPos !== false) { + $signal = substr($path, $pathSepPos + 1); + } else { + $signal = $path; + } + if ($signal === '' || $signal === false) { + throw new InvalidLinkDestinationException($destination, 0, new InvalidLinkException('Signal must be non-empty string.')); + } + $path = 'this'; + } + + $pathSepPos = strrpos($path, ':'); + if ($pathSepPos !== false && $pathSepPos !== 0) { + $presenter = substr($path, 0, $pathSepPos); + $action = substr($path, $pathSepPos + 1); + } else { + $presenter = 'this'; + $action = $path; + } + + if ($isLinkGenerator) { + if ($presenter[0] === ':') { + throw new InvalidLinkDestinationException($destination, 0, new InvalidLinkException('Do not use absolute destinations with LinkGenerator.')); + } + + $presenter = ':' . $presenter; + } + + if ($action !== '' && preg_match('/^[a-zA-Z0-9_]+$/', $action) !== 1) { + throw new InvalidLinkDestinationException($destination); + } + + if ($signal !== '' && preg_match('/^[a-zA-Z0-9_]+$/', $signal) !== 1) { + throw new InvalidLinkDestinationException($destination); + } + + if ($action === '') { + $action = 'default'; + } + + if ($isComponent && !$isPresenter) { + $targetClass = $currentClass; + } else { + try { + $targetClass = $this->presenterResolver->getPresenterClassByName($presenter, $isPresenter ? $currentClass : null); + } catch (InvalidPresenterException $e) { + throw new InvalidLinkDestinationException($destination, 0, $e); + } + } + + if ($targetClass === null) { + return; + } + + $targetClassReflection = $this->reflectionProvider->getClass($targetClass); + + if ($signal !== '') { + if ($signal === 'this') { // means "no signal" + if ($params !== null && array_key_exists(0, $params)) { + throw new InvalidLinkParamsException("Unable to pass parameters to 'this!' signal."); + } + } elseif (strpos($signal, '-') === false) { + $signalMethod = 'handle' . Strings::firstUpper($signal); + if (!$targetClassReflection->hasMethod($signalMethod)) { + throw new InvalidLinkDestinationException( + $destination, + 0, + new InvalidLinkException(sprintf("Unknown signal '%s', missing handler %s::%s()", $signal, $targetClass, $signalMethod)) + ); + } + $this->validateParams($scope, $targetClass, $signalMethod, $params); + //detect deprecated? + } + } + + if ($params === null || !$targetClassReflection->isSubclassOf(Presenter::class)) { + return; + } + + $actionMethod = 'action' . Strings::firstUpper($action); + $renderMethod = 'action' . Strings::firstUpper($action); + if ($targetClassReflection->hasMethod($actionMethod)) { + $this->validateParams($scope, $targetClass, $actionMethod, $params); + //detect deprecated? + } elseif ($targetClassReflection->hasMethod($renderMethod)) { + $this->validateParams($scope, $targetClass, $renderMethod, $params); + //detect deprecated? + } elseif ($params && array_key_exists(0, $params)) { + throw new InvalidLinkParamsException(sprintf("Unable to pass parameters to action '%s:%s', missing corresponding method in %s.", $presenter, $action, $targetClass)); + } + } + + /** + * @return array> + */ + private function extractParamsFromArray(Type $type): array + { + $paramsOptions = []; + foreach ($type->getConstantArrays() as $array) { + $params = []; + $keyTypes = $array->getKeyTypes(); + $valueTypes = $array->getValueTypes(); + foreach ($keyTypes as $index => $keyType) { + if ($keyType->isConstantValue()->no()) { + break; + } + $params[$keyType->getValue()] = $valueTypes[$index]; + } + $paramsOptions[] = $params; + } + + return $paramsOptions; + } + + /** + * @param array $args + * @return array> + */ + private function extractParamsFromArgs(Scope $scope, array $args): array + { + $params = []; + foreach ($args as $arg) { + $params[] = $scope->getType($arg->value); + } + return [$params]; + } + + /** + * @param array $args + */ + public function validateParams( + Scope $scope, + string $class, + string $method, + array &$args + ): void + { + $i = 0; + $rm = $this->reflectionProvider->getClass($class)->getMethod($method, $scope); + $declaringClass = $rm->getDeclaringClass()->getName(); + + $selectedVariant = ParametersAcceptorSelector::selectFromTypes($args, $rm->getVariants(), false); + + foreach ($selectedVariant->getParameters() as $param) { + $expectedType = $param->getType(); + $name = $param->getName(); + + if (array_key_exists($i, $args)) { + $args[$name] = $args[$i]; + unset($args[$i]); + $i++; + } + + if (!isset($args[$name])) { + if ( + $param->getDefaultValue() === null + && $expectedType->isNull()->no() + && $expectedType->isScalar()->no() + && $expectedType->isArray()->no() + && $expectedType->isIterable()->no() + ) { + throw new InvalidLinkParamsException(sprintf('Missing parameter $%s required by %s::%s()', $param->getName(), $declaringClass, $method)); + } + continue; + } + + $actualType = $args[$name]; + if ($expectedType->accepts($actualType, false)->no()) { + throw new InvalidLinkParamsException(sprintf( + 'Argument $%s passed to %s() must be %s, %s given.', + $name, + $declaringClass . '::' . $method, + $expectedType->describe(VerbosityLevel::precise()), + $actualType->describe(VerbosityLevel::precise()) + )); + } + } + + if (array_key_exists($i, $args)) { + throw new InvalidLinkParamsException(sprintf('Passed more parameters than method %s::%s() expects.', $declaringClass, $method)); + } + } + +} diff --git a/tests/Nette/PresenterResolverTest.php b/tests/Nette/PresenterResolverTest.php new file mode 100644 index 0000000..9c9c030 --- /dev/null +++ b/tests/Nette/PresenterResolverTest.php @@ -0,0 +1,41 @@ +presenterResolver = new PresenterResolver([], new ContainerResolver(__DIR__ . '/containerLoader.php')); + } + + public function testResolvePresenterName(): void + { + self::assertSame('Test', $this->presenterResolver->resolvePresenterName(':Test')); + self::assertSame('TestModule:Test', $this->presenterResolver->resolvePresenterName(':TestModule:Test')); + self::assertSame('TestModule:SubModule:Test', $this->presenterResolver->resolvePresenterName(':TestModule:SubModule:Test')); + + $currentPresenterClass = 'PHPStan\TestApp\Presenters\CurrentModule\CurrentPresenter'; + self::assertSame('CurrentModule:Current', $this->presenterResolver->resolvePresenterName('this', $currentPresenterClass)); + self::assertSame('CurrentModule:Test', $this->presenterResolver->resolvePresenterName('Test', $currentPresenterClass)); + self::assertSame('CurrentModule:SubModule:Test', $this->presenterResolver->resolvePresenterName('SubModule:Test', $currentPresenterClass)); + } + + public function testGetPresenterClassByName(): void + { + self::assertSame('PHPStan\TestApp\Presenters\TestPresenter', $this->presenterResolver->getPresenterClassByName(':Test')); + self::assertSame('PHPStan\TestApp\Presenters\TestModule\TestPresenter', $this->presenterResolver->getPresenterClassByName(':TestModule:Test')); + + $currentPresenterClass = 'PHPStan\TestApp\Presenters\CurrentModule\CurrentPresenter'; + self::assertSame('PHPStan\TestApp\Presenters\CurrentModule\CurrentPresenter', $this->presenterResolver->getPresenterClassByName('this', $currentPresenterClass)); + self::assertSame('PHPStan\TestApp\Presenters\CurrentModule\TestPresenter', $this->presenterResolver->getPresenterClassByName('Test', $currentPresenterClass)); + self::assertSame('PHPStan\TestApp\Presenters\CurrentModule\SubModule\TestPresenter', $this->presenterResolver->getPresenterClassByName('SubModule:Test', $currentPresenterClass)); + } + +} diff --git a/tests/Nette/containerLoader.php b/tests/Nette/containerLoader.php new file mode 100644 index 0000000..f0243b4 --- /dev/null +++ b/tests/Nette/containerLoader.php @@ -0,0 +1,8 @@ + + */ +class LinksRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new LinksRule( + new PresenterResolver( + ['*' => 'PHPStan\TestApp\Presenters\*\*Presenter'], + new ContainerResolver(null) + ), + $this->getContainer()->getByType(ReflectionProvider::class) + ); + } + + public function testRuleForPresenter(): void + { + require_once __DIR__ . '/../../TestApp/autoload.php'; + $this->analyse([__DIR__ . '/data/links-presenter.php'], [ + [ + 'Invalid link destination \'***\' in link() call.', + 5, + ], + [ + 'Invalid link destination \':Unknown:default\' in link() call: Cannot load presenter \'Unknown\', class \'PHPStan\TestApp\Presenters\UnknownPresenter\' was not found.', + 12, + ], + [ + 'Invalid link params in link() call: Unable to pass parameters to action \':Test:default\', missing corresponding method in PHPStan\TestApp\Presenters\TestPresenter.', + 15, + ], + [ + 'Invalid link params in link() call: Unable to pass parameters to action \':Test:default\', missing corresponding method in PHPStan\TestApp\Presenters\TestPresenter.', + 16, + ], + [ + 'Invalid link params in link() call: Passed more parameters than method PHPStan\TestApp\Presenters\TestPresenter::actionWithParam() expects.', + 24, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\TestPresenter::actionWithParam() must be string, null given.', + 25, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\TestPresenter::actionWithParam() must be string, null given.', + 26, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\TestPresenter::actionWithParam() must be string, null given.', + 27, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\CurrentModule\CurrentPresenter::actionWithParam() must be int, null given.', + 30, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\CurrentModule\CurrentPresenter::actionWithParam() must be int, null given.', + 31, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\CurrentModule\CurrentPresenter::actionWithParam() must be int, null given.', + 32, + ], + [ + 'Invalid link destination \'unknown!\' in link() call: Unknown signal \'unknown\', missing handler PHPStan\TestApp\Presenters\CurrentModule\CurrentPresenter::handleUnknown()', + 36, + ], + [ + 'Invalid link destination \'!\' in link() call.', + 37, + ], + [ + 'Invalid link destination \'*!\' in link() call.', + 38, + ], + [ + 'Invalid link destination \'***\' in lazyLink() call.', + 40, + ], + [ + 'Invalid link destination \'***\' in isLinkCurrent() call.', + 41, + ], + [ + 'Invalid link destination \'***\' in redirect() call.', + 42, + ], + [ + 'Invalid link destination \'***\' in redirectPermanent() call.', + 43, + ], + [ + 'Invalid link destination \'***\' in forward() call.', + 44, + ], + [ + 'Invalid link destination \'***\' in canonicalize() call.', + 45, + ], + ]); + } + + public function testRuleForComponent(): void + { + require_once __DIR__ . '/../../TestApp/autoload.php'; + $this->analyse([__DIR__ . '/data/links-component.php'], [ + [ + 'Invalid link destination \'***\' in link() call.', + 5, + ], + [ + 'Invalid link destination \'Test\' in link() call: Unknown signal \'Test\', missing handler PHPStan\TestApp\Components\CurrentComponent::handleTest()', + 7, + ], + [ + 'Invalid link destination \':Test\' in link() call: Unknown signal \'Test\', missing handler PHPStan\TestApp\Components\CurrentComponent::handleTest()', + 8, + ], + [ + 'Invalid link destination \'unknown\' in link() call: Unknown signal \'unknown\', missing handler PHPStan\TestApp\Components\CurrentComponent::handleUnknown()', + 12, + ], + [ + 'Invalid link destination \'unknown!\' in link() call: Unknown signal \'unknown\', missing handler PHPStan\TestApp\Components\CurrentComponent::handleUnknown()', + 16, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Components\CurrentComponent::handleWithParam() must be int, null given.', + 19, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Components\CurrentComponent::handleWithParam() must be int, null given.', + 20, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Components\CurrentComponent::handleWithParam() must be int, null given.', + 21, + ], + [ + 'Invalid link destination \'***\' in lazyLink() call.', + 23, + ], + [ + 'Invalid link destination \'***\' in isLinkCurrent() call.', + 24, + ], + [ + 'Invalid link destination \'***\' in redirect() call.', + 25, + ], + [ + 'Invalid link destination \'***\' in redirectPermanent() call.', + 26, + ], + ]); + } + + public function testRuleForLinkGenerator(): void + { + require_once __DIR__ . '/../../TestApp/autoload.php'; + $this->analyse([__DIR__ . '/data/links-linkGenerator.php'], [ + [ + 'Invalid link destination \'***\' in link() call.', + 5, + ], + [ + 'Invalid link destination \'Test\' in link() call.', + 8, + ], + [ + 'Invalid link destination \':Test:default\' in link() call: Do not use absolute destinations with LinkGenerator.', + 12, + ], + [ + 'Invalid link destination \'Unknown:default\' in link() call: Cannot load presenter \'Unknown\', class \'PHPStan\TestApp\Presenters\UnknownPresenter\' was not found.', + 14, + ], + [ + 'Invalid link params in link() call: Unable to pass parameters to action \':Test:default\', missing corresponding method in PHPStan\TestApp\Presenters\TestPresenter.', + 18, + ], + [ + 'Invalid link params in link() call: Passed more parameters than method PHPStan\TestApp\Presenters\TestPresenter::actionWithParam() expects.', + 26, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\TestPresenter::actionWithParam() must be string, null given.', + 28, + ], + [ + 'Invalid link params in link() call: Argument $param passed to PHPStan\TestApp\Presenters\TestPresenter::actionWithParam() must be string, null given.', + 29, + ], + [ + 'Invalid link destination \'this!\' in link() call.', + 31, + ], + [ + 'Invalid link destination \'signal!\' in link() call.', + 32, + ], + ]); + } + +} diff --git a/tests/Rule/Nette/data/links-component.php b/tests/Rule/Nette/data/links-component.php new file mode 100644 index 0000000..63334ca --- /dev/null +++ b/tests/Rule/Nette/data/links-component.php @@ -0,0 +1,26 @@ +link('***'); + +$component->link('Test'); +$component->link(':Test'); + +$component->link('this'); +$component->link('signal'); +$component->link('unknown'); + +$component->link('this!'); +$component->link('signal!'); +$component->link('unknown!'); + +$component->link('withParam'); +$component->link('withParam', null); +$component->link('withParam', [null]); +$component->link('withParam', ['param' => null]); + +$component->lazyLink('***'); +$component->isLinkCurrent('***'); +if(false) {$component->redirect('***');} +if(false) {$component->redirectPermanent('***');} diff --git a/tests/Rule/Nette/data/links-linkGenerator.php b/tests/Rule/Nette/data/links-linkGenerator.php new file mode 100644 index 0000000..88bfe0b --- /dev/null +++ b/tests/Rule/Nette/data/links-linkGenerator.php @@ -0,0 +1,32 @@ +link('***'); + +$linkGenerator->link('this'); +$linkGenerator->link('Test'); +$linkGenerator->link('Test:'); +$linkGenerator->link('Test:default'); + +$linkGenerator->link(':Test:default'); + +$linkGenerator->link('Unknown:default'); + +$linkGenerator->link('Test:default'); +$linkGenerator->link('Test:default', 'paramValue'); +$linkGenerator->link('Test:default', ['paramValue']); +$linkGenerator->link('Test:default', ['param' => 'paramValue']); + +$linkGenerator->link('Test:withParam', ['paramValue']); +$linkGenerator->link('Test:withParam', ['param' => 'paramValue']); + +$linkGenerator->link('Test:withParam'); +$linkGenerator->link('Test:withParam', 'paramValue'); +$linkGenerator->link('Test:withParam', ['paramValue', 'paramValue']); +$linkGenerator->link('Test:withParam', null); +$linkGenerator->link('Test:withParam', [null]); +$linkGenerator->link('Test:withParam', ['param' => null]); + +$linkGenerator->link('this!'); +$linkGenerator->link('signal!'); diff --git a/tests/Rule/Nette/data/links-presenter.php b/tests/Rule/Nette/data/links-presenter.php new file mode 100644 index 0000000..714cd5f --- /dev/null +++ b/tests/Rule/Nette/data/links-presenter.php @@ -0,0 +1,45 @@ +link('***'); + +$presenter->link('this'); +$presenter->link('Test:'); +$presenter->link('Test:default'); +$presenter->link('default'); + +$presenter->link(':Unknown:default'); + +$presenter->link(':Test:default'); +$presenter->link(':Test:default', 'paramValue'); +$presenter->link(':Test:default', ['paramValue']); +$presenter->link(':Test:default', ['param' => 'paramValue']); + +$presenter->link(':Test:withParam', 'paramValue'); +$presenter->link(':Test:withParam', ['paramValue']); +$presenter->link(':Test:withParam', ['param' => 'paramValue']); + +$presenter->link(':Test:withParam'); +$presenter->link(':Test:withParam', 'paramValue', 'paramValue'); +$presenter->link(':Test:withParam', null); +$presenter->link(':Test:withParam', [null]); +$presenter->link(':Test:withParam', ['param' => null]); + +$presenter->link('withParam'); +$presenter->link('withParam', null); +$presenter->link('withParam', [null]); +$presenter->link('withParam', ['param' => null]); + +$presenter->link('this!'); +$presenter->link('signal!'); +$presenter->link('unknown!'); +$presenter->link('!'); +$presenter->link('*!'); + +$presenter->lazyLink('***'); +$presenter->isLinkCurrent('***'); +if(false) {$presenter->redirect('***');} +if(false) {$presenter->redirectPermanent('***');} +if(false) {$presenter->forward('***');} +$presenter->canonicalize('***'); diff --git a/tests/TestApp/Components/CurrentComponent.php b/tests/TestApp/Components/CurrentComponent.php new file mode 100644 index 0000000..85f5d25 --- /dev/null +++ b/tests/TestApp/Components/CurrentComponent.php @@ -0,0 +1,18 @@ + [ + 0 => ['presenterFactory'], + ], + ]; + + protected function createServicePresenterFactory(): IPresenterFactory + { + $service = new PresenterFactory(); + $service->setMapping(['*' => 'PHPStan\TestApp\Presenters\*\*Presenter']); + return $service; + } + +} diff --git a/tests/TestApp/autoload.php b/tests/TestApp/autoload.php new file mode 100644 index 0000000..f1a721f --- /dev/null +++ b/tests/TestApp/autoload.php @@ -0,0 +1,9 @@ +