Skip to content

Commit 0abcb28

Browse files
authored
[config] Add FileNameMatchesExtensionRule (#246)
1 parent d64ecd2 commit 0abcb28

File tree

7 files changed

+179
-0
lines changed

7 files changed

+179
-0
lines changed

config/symfony-config-rules.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ rules:
44
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\AlreadyRegisteredAutodiscoveryServiceRule
55
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\TaggedIteratorOverRepeatedServiceCallRule
66

7+
# sync file name and extension name
8+
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\FileNameMatchesExtensionRule
9+
710
# no autowire duplicate
811
- Symplify\PHPStanRules\Rules\Symfony\NoServiceAutowireDuplicateRule
912

src/Enum/RuleIdentifier/SymfonyRuleIdentifier.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,6 @@ final class SymfonyRuleIdentifier
6363
public const NO_SET_CLASS_SERVICE_DUPLICATE = 'symfony.noSetClassServiceDuplicate';
6464

6565
public const NO_CONTROLLER_METHOD_INJECTION = 'symfony.noControllerMethodInjection';
66+
67+
public const FILE_NAME_MATCHES_EXTENSION = 'symfony.fileNameMatchesExtension';
6668
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Rules\Symfony\ConfigClosure;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\Closure;
9+
use PhpParser\Node\Expr\MethodCall;
10+
use PhpParser\Node\Identifier;
11+
use PhpParser\Node\Scalar\String_;
12+
use PhpParser\NodeFinder;
13+
use PhpParser\NodeVisitor;
14+
use PHPStan\Analyser\Scope;
15+
use PHPStan\Rules\IdentifierRuleError;
16+
use PHPStan\Rules\Rule;
17+
use PHPStan\Rules\RuleErrorBuilder;
18+
use Symplify\PHPStanRules\Enum\RuleIdentifier\SymfonyRuleIdentifier;
19+
use Symplify\PHPStanRules\Symfony\NodeAnalyzer\SymfonyClosureDetector;
20+
21+
/**
22+
* @see \Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\FileNameMatchesExtensionRule\FileNameMatchesExtensionRuleTest
23+
* @implements Rule<Closure>
24+
*/
25+
final class FileNameMatchesExtensionRule implements Rule
26+
{
27+
/**
28+
* @var string
29+
*/
30+
public const ERROR_MESSAGE = 'The config uses "%s" extension, but the file name is "%s". Sync them to ease discovery';
31+
32+
public function getNodeType(): string
33+
{
34+
return Closure::class;
35+
}
36+
37+
/**
38+
* @param Closure $node
39+
* @return IdentifierRuleError[]
40+
*/
41+
public function processNode(Node $node, Scope $scope): array
42+
{
43+
if (! SymfonyClosureDetector::detect($node)) {
44+
return [];
45+
}
46+
47+
$extensionName = $this->findExtensionName($node);
48+
if (! is_string($extensionName)) {
49+
return [];
50+
}
51+
52+
// get basefile name
53+
$baseFileName = basename($scope->getFile(), '.php');
54+
if ($baseFileName === $extensionName) {
55+
return [];
56+
}
57+
58+
// find if uses extension and get the name if so
59+
60+
$identifierRuleError = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $extensionName, $baseFileName))
61+
->identifier(SymfonyRuleIdentifier::FILE_NAME_MATCHES_EXTENSION)
62+
->build();
63+
64+
return [$identifierRuleError];
65+
}
66+
67+
private function findExtensionName(Closure|Node $node): ?string
68+
{
69+
$extensionName = null;
70+
71+
$nodeFinder = new NodeFinder();
72+
$nodeFinder->find($node, function (Node $node) use (&$extensionName): ?int {
73+
if (! $node instanceof MethodCall) {
74+
return null;
75+
}
76+
77+
if (! $node->name instanceof Identifier) {
78+
return null;
79+
}
80+
81+
$methodName = $node->name->toString();
82+
if ($methodName !== 'extension') {
83+
return null;
84+
}
85+
86+
foreach ($node->getArgs() as $arg) {
87+
if (! $arg->value instanceof String_) {
88+
continue;
89+
}
90+
91+
$extensionName = $arg->value->value;
92+
}
93+
94+
return NodeVisitor::STOP_TRAVERSAL;
95+
});
96+
97+
return $extensionName;
98+
}
99+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\FileNameMatchesExtensionRule;
6+
7+
use Iterator;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\FileNameMatchesExtensionRule;
12+
13+
final class FileNameMatchesExtensionRuleTest extends RuleTestCase
14+
{
15+
/**
16+
* @param mixed[] $expectedErrorMessagesWithLines
17+
*/
18+
#[DataProvider('provideData')]
19+
public function testRule(string $filePath, array $expectedErrorMessagesWithLines): void
20+
{
21+
$this->analyse([$filePath], $expectedErrorMessagesWithLines);
22+
}
23+
24+
public static function provideData(): Iterator
25+
{
26+
yield [__DIR__ . '/Fixture/framework.php', []];
27+
yield [__DIR__ . '/Fixture/skip_no_extension.php', []];
28+
29+
yield [__DIR__ . '/Fixture/wrong_name.php', [[
30+
sprintf(FileNameMatchesExtensionRule::ERROR_MESSAGE, 'framework', 'wrong_name'),
31+
10,
32+
]]];
33+
}
34+
35+
protected function getRule(): Rule
36+
{
37+
return new FileNameMatchesExtensionRule();
38+
}
39+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Fixture;
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Source\AnotherType;
7+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Source\SomeServiceWithConstructor;
8+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
9+
10+
return function (ContainerConfigurator $container) {
11+
$framework = $container->extension('framework');
12+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Fixture;
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Source\AnotherType;
7+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Source\SomeServiceWithConstructor;
8+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
9+
10+
return function (ContainerConfigurator $container) {
11+
$services = $container->services();;
12+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Fixture;
4+
5+
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
6+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Source\AnotherType;
7+
use Symplify\PHPStanRules\Tests\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule\Source\SomeServiceWithConstructor;
8+
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
9+
10+
return function (ContainerConfigurator $container) {
11+
$framework = $container->extension('framework');
12+
};

0 commit comments

Comments
 (0)