Skip to content

Commit

Permalink
LinksRule for checking validity of links
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinMystikJonas committed Aug 9, 2023
1 parent 6ec9016 commit 5378e63
Show file tree
Hide file tree
Showing 25 changed files with 1,197 additions and 0 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 19 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
parameters:
nette:
containerLoader: null
applicationMapping: []
additionalConstructors:
- Nette\Application\UI\Presenter::startup
exceptions:
Expand Down Expand Up @@ -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:
Expand Down
25 changes: 25 additions & 0 deletions src/Exceptions/InvalidLinkDestinationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

use Throwable;
use function sprintf;

class InvalidLinkDestinationException extends InvalidLinkException
{

/** @var string */
private $destination;

public function __construct(string $destination, int $code = 0, ?Throwable $previous = null)
{
parent::__construct(sprintf("Invalid link destination '%s'", $destination), $code, $previous);
$this->destination = $destination;
}

public function getDestination(): string
{
return $this->destination;
}

}
10 changes: 10 additions & 0 deletions src/Exceptions/InvalidLinkException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

use RuntimeException;

class InvalidLinkException extends RuntimeException
{

}
8 changes: 8 additions & 0 deletions src/Exceptions/InvalidLinkParamsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

class InvalidLinkParamsException extends InvalidLinkException
{

}
10 changes: 10 additions & 0 deletions src/Exceptions/LinkCheckFailedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace PHPStan\Exceptions;

use RuntimeException;

class LinkCheckFailedException extends RuntimeException
{

}
66 changes: 66 additions & 0 deletions src/Nette/ContainerResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php declare(strict_types = 1);

namespace PHPStan\Nette;

use Nette\DI\Container;
use PHPStan\ShouldNotHappenException;
use function is_file;
use function is_readable;
use function sprintf;

class ContainerResolver
{

/** @var string|null */
private $containerLoader;

/** @var Container|false|null */
private $container;

public function __construct(?string $containerLoader)
{
$this->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;
}

}
142 changes: 142 additions & 0 deletions src/Nette/PresenterResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php declare(strict_types = 1);

namespace PHPStan\Nette;

use Nette\Application\IPresenterFactory;
use Nette\Application\PresenterFactory;
use PHPStan\Exceptions\LinkCheckFailedException;
use PHPStan\ShouldNotHappenException;
use ReflectionClass;
use ReflectionException;
use function count;
use function is_array;
use function is_string;
use function preg_match;
use function preg_replace;
use function sprintf;
use function str_replace;
use function strrpos;
use function substr;

class PresenterResolver
{

/** @var array<string, string|array{0: string, 1: string, 2: string}> */
protected $mapping;

/** @var ContainerResolver */
private $containerResolver;

/** @var IPresenterFactory */
private $presenterFactory;

/**
* @param array<string, string|array{0: string, 1: string, 2: string}> $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<string, array{0: string, 1: string, 2: string}>
* @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<string, array{0: string, 1: string, 2: string}> $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));
}

}

0 comments on commit 5378e63

Please sign in to comment.