Skip to content

Commit d35cd60

Browse files
author
Ismail Turan
committed
Add analysis for route generation by controllers and UrlGeneratorInterface
1 parent 96ee630 commit d35cd60

18 files changed

+422
-0
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"symfony/http-foundation": "^5.1",
3434
"symfony/messenger": "^4.2 || ^5.0",
3535
"symfony/polyfill-php80": "^1.24",
36+
"symfony/routing": "^4.4 || ^5.0",
3637
"symfony/serializer": "^4.0 || ^5.0"
3738
},
3839
"config": {

extension.neon

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ parameters:
1111
constantHassers: true
1212
console_application_loader: null
1313
consoleApplicationLoader: null
14+
url_generating_rules_file: null
15+
urlGeneratingRulesFile: null
1416
stubFiles:
1517
- stubs/Psr/Cache/CacheItemInterface.stub
1618
- stubs/Symfony/Bundle/FrameworkBundle/KernelBrowser.stub
@@ -65,6 +67,8 @@ parametersSchema:
6567
constantHassers: bool()
6668
console_application_loader: schema(string(), nullable())
6769
consoleApplicationLoader: schema(string(), nullable())
70+
url_generating_rules_file: schema(string(), nullable())
71+
urlGeneratingRulesFile: schema(string(), nullable())
6872
])
6973

7074
services:
@@ -89,6 +93,13 @@ services:
8993
-
9094
factory: @symfony.parameterMapFactory::create()
9195

96+
# url generating routes map
97+
symfony.urlGeneratingRoutesMapFactory:
98+
class: PHPStan\Symfony\UrlGeneratingRoutesMapFactory
99+
factory: PHPStan\Symfony\PhpUrlGeneratingRoutesMapFactory
100+
-
101+
factory: @symfony.urlGeneratingRoutesMapFactory::create()
102+
92103
# ControllerTrait::get()/has() return type
93104
-
94105
factory: PHPStan\Type\Symfony\ServiceDynamicReturnTypeExtension(Symfony\Component\DependencyInjection\ContainerInterface)

rules.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ rules:
55
- PHPStan\Rules\Symfony\InvalidArgumentDefaultValueRule
66
- PHPStan\Rules\Symfony\UndefinedOptionRule
77
- PHPStan\Rules\Symfony\InvalidOptionDefaultValueRule
8+
- PHPStan\Rules\Symfony\UrlGeneratorInterfaceUnknownRouteRule
89

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Symfony;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\MethodCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\ShouldNotHappenException;
10+
use PHPStan\Symfony\UrlGeneratingRoutesMap;
11+
use PHPStan\Type\ObjectType;
12+
13+
/**
14+
* @implements Rule<MethodCall>
15+
*/
16+
final class UrlGeneratorInterfaceUnknownRouteRule implements Rule
17+
{
18+
19+
/** @var UrlGeneratingRoutesMap */
20+
private $urlGeneratingRoutesMap;
21+
22+
public function __construct(UrlGeneratingRoutesMap $urlGeneratingRoutesMap)
23+
{
24+
$this->urlGeneratingRoutesMap = $urlGeneratingRoutesMap;
25+
}
26+
27+
public function getNodeType(): string
28+
{
29+
return MethodCall::class;
30+
}
31+
32+
/**
33+
* @param \PhpParser\Node $node
34+
* @param \PHPStan\Analyser\Scope $scope
35+
* @return (string|\PHPStan\Rules\RuleError)[] errors
36+
*/
37+
public function processNode(Node $node, Scope $scope): array
38+
{
39+
if (!$node instanceof MethodCall) {
40+
throw new ShouldNotHappenException();
41+
}
42+
43+
if (!$node->name instanceof Node\Identifier) {
44+
return [];
45+
}
46+
47+
if (in_array($node->name->name, ['generate', 'generateUrl'], true) === false || !isset($node->getArgs()[0])) {
48+
return [];
49+
}
50+
51+
$argType = $scope->getType($node->var);
52+
$isControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\Controller'))->isSuperTypeOf($argType);
53+
$isAbstractControllerType = (new ObjectType('Symfony\Bundle\FrameworkBundle\Controller\AbstractController'))->isSuperTypeOf($argType);
54+
$isUrlGeneratorInterface = (new ObjectType('Symfony\Component\Routing\Generator\UrlGeneratorInterface'))->isSuperTypeOf($argType);
55+
if (
56+
$isControllerType->no()
57+
&& $isAbstractControllerType->no()
58+
&& $isUrlGeneratorInterface->no()
59+
) {
60+
return [];
61+
}
62+
63+
$routeName = $this->urlGeneratingRoutesMap::getRouteNameFromNode($node->getArgs()[0]->value, $scope);
64+
if ($routeName === null) {
65+
return [];
66+
}
67+
68+
if ($this->urlGeneratingRoutesMap->hasRouteName($routeName) === false) {
69+
return [sprintf('Route with name "%s" does not exist.', $routeName)];
70+
}
71+
72+
return [];
73+
}
74+
75+
}

src/Symfony/Configuration.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,9 @@ public function getConsoleApplicationLoader(): ?string
3131
return $this->parameters['consoleApplicationLoader'] ?? $this->parameters['console_application_loader'] ?? null;
3232
}
3333

34+
public function getUrlGeneratingRoutesFile(): ?string
35+
{
36+
return $this->parameters['urlGeneratingRulesFile'] ?? $this->parameters['url_generating_rules_file'] ?? null;
37+
}
38+
3439
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Type\TypeUtils;
8+
use function count;
9+
10+
final class DefaultUrlGeneratingRoutesMap implements UrlGeneratingRoutesMap
11+
{
12+
13+
/** @var \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] */
14+
private $routes;
15+
16+
/**
17+
* @param \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] $routes
18+
*/
19+
public function __construct(array $routes)
20+
{
21+
$this->routes = $routes;
22+
}
23+
24+
public function hasRouteName(string $name): bool
25+
{
26+
foreach ($this->routes as $route) {
27+
if ($route->getName() === $name) {
28+
return true;
29+
}
30+
}
31+
32+
return false;
33+
}
34+
35+
public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
36+
{
37+
$strings = TypeUtils::getConstantStrings($scope->getType($node));
38+
return count($strings) === 1 ? $strings[0]->getValue() : null;
39+
}
40+
41+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use PhpParser\Node\Expr;
6+
use PHPStan\Analyser\Scope;
7+
8+
final class FakeUrlGeneratingRoutesMap implements UrlGeneratingRoutesMap
9+
{
10+
11+
public function hasRouteName(string $name): bool
12+
{
13+
return false;
14+
}
15+
16+
public static function getRouteNameFromNode(Expr $node, Scope $scope): ?string
17+
{
18+
return null;
19+
}
20+
21+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
use function sprintf;
6+
7+
final class PhpUrlGeneratingRoutesMapFactory implements UrlGeneratingRoutesMapFactory
8+
{
9+
10+
/** @var string|null */
11+
private $urlGeneratingRoutesFile;
12+
13+
public function __construct(Configuration $configuration)
14+
{
15+
$this->urlGeneratingRoutesFile = $configuration->getUrlGeneratingRoutesFile();
16+
}
17+
18+
public function create(): UrlGeneratingRoutesMap
19+
{
20+
if ($this->urlGeneratingRoutesFile === null) {
21+
return new FakeUrlGeneratingRoutesMap();
22+
}
23+
24+
if (file_exists($this->urlGeneratingRoutesFile) === false) {
25+
throw new UrlGeneratingRoutesFileNotExistsException(sprintf('File %s containing route generator information does not exist.', $this->urlGeneratingRoutesFile));
26+
}
27+
28+
$urlGeneratingRoutes = require $this->urlGeneratingRoutesFile;
29+
30+
if (!is_array($urlGeneratingRoutes)) {
31+
throw new UrlGeneratingRoutesFileNotExistsException(sprintf('File %s containing route generator information cannot be parsed.', $this->urlGeneratingRoutesFile));
32+
}
33+
34+
/** @var \PHPStan\Symfony\UrlGeneratingRoutesDefinition[] $routes */
35+
$routes = [];
36+
foreach ($urlGeneratingRoutes as $routeName => $routeConfiguration) {
37+
if (!is_string($routeName)) {
38+
continue;
39+
}
40+
41+
if (!is_array($routeConfiguration) || !isset($routeConfiguration[1]['_controller'])) {
42+
continue;
43+
}
44+
45+
$routes[] = new UrlGeneratingRoute($routeName, $routeConfiguration[1]['_controller']);
46+
}
47+
48+
return new DefaultUrlGeneratingRoutesMap($routes);
49+
}
50+
51+
}

src/Symfony/UrlGeneratingRoute.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
class UrlGeneratingRoute implements UrlGeneratingRoutesDefinition
6+
{
7+
8+
/** @var string */
9+
private $name;
10+
11+
/** @var string */
12+
private $controller;
13+
14+
public function __construct(
15+
string $name,
16+
string $controller
17+
)
18+
{
19+
$this->name = $name;
20+
$this->controller = $controller;
21+
}
22+
23+
public function getName(): string
24+
{
25+
return $this->name;
26+
}
27+
28+
public function getController(): ?string
29+
{
30+
return $this->controller;
31+
}
32+
33+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Symfony;
4+
5+
interface UrlGeneratingRoutesDefinition
6+
{
7+
8+
public function getName(): string;
9+
10+
public function getController(): ?string;
11+
12+
}

0 commit comments

Comments
 (0)